aboutsummaryrefslogblamecommitdiff
path: root/tests/test_store.py
blob: d7620a01fa204b3805e757ad232d3c300df78ada (plain) (tree)
1
2
3
4
5
6
7





                               
                                                      





                                      
                                                      


















































                                                                        
                                                      































































































































































































































                                                                                            
                                                      















































































                                                                                             










                                                                                                 
                                                                                     






                                                                                  
                                                                                        
                                
 
                                                   








                                                                                                  






























                                                                                          
 





















                                                                                
                                                                                




































                                                                                  
                                                      

                                       











                                                                                                    
 
                                                                                                          
 






                                                              










                                                        

                            

















                                                                                              
                                                                                           















                                                                                                                          
                                                                                           






                                                                                    
                                                                                    

















                                                                                                                                                                             













                                                                            
























                                                                                                              

                                                                      



                                                            








                                                
                                   





                                                      
                                   















                                                                   
                                   


















                                                      
                                   
















                                               


                                                       
                                                   

























































































































































































































































                                                                                                                                      





                                                                         
                                                             
 








                                                                  
                                


                                         
 






























































                                                                                                                      




                                                     
                                                                          



                                                                           













































































                                                                                        
                                                      









                                                                  

                                                                                     













                                                                         
                                                           

                                                                 
                                              





                                                          
                                                     



                                                                         
                                              




                                                          
                                                     



















                                                                                            
                                                  






                                                                 
                                                             





















                                                                     
                                                          















                                                                       
                                                                                                        






















                                                                                        


























                                                                                                                               
 




























                                                                                          
                     
                                                                                     
 




























                                                                                                   










                                                               


                                                                                       

























































                                                                                                           












                                                                               













                                                                               
                                                                                         







                                                                    
                                         










                                                                                     
from .helper import *
import requests
import datetime
import threading
import market, portfolio, store

@unittest.skipUnless("unit" in limits, "Unit skipped")
class NoopLockTest(unittest.TestCase):
    def test_with(self):
        noop_lock = store.NoopLock()
        with noop_lock:
            self.assertTrue(True)

@unittest.skipUnless("unit" in limits, "Unit skipped")
class LockedVarTest(unittest.TestCase):

    def test_values(self):
        locked_var = store.LockedVar("Foo")
        self.assertIsInstance(locked_var.lock, store.NoopLock)
        self.assertEqual("Foo", locked_var.val)

    def test_get(self):
        with self.subTest(desc="Normal case"):
            locked_var = store.LockedVar("Foo")
            self.assertEqual("Foo", locked_var.get())
        with self.subTest(desc="Dict"):
            locked_var = store.LockedVar({"foo": "bar"})
            self.assertEqual({"foo": "bar"}, locked_var.get())
            self.assertEqual("bar", locked_var.get("foo"))
            self.assertIsNone(locked_var.get("other"))

    def test_set(self):
        locked_var = store.LockedVar("Foo")
        locked_var.set("Bar")
        self.assertEqual("Bar", locked_var.get())

    def test__getattr(self):
        dummy = type('Dummy', (object,), {})()
        dummy.attribute = "Hey"

        locked_var = store.LockedVar(dummy)
        self.assertEqual("Hey", locked_var.attribute)
        with self.assertRaises(AttributeError):
            locked_var.other

    def test_start_lock(self):
        locked_var = store.LockedVar("Foo")
        locked_var.start_lock()
        self.assertEqual("lock", locked_var.lock.__class__.__name__)

        thread1 = threading.Thread(target=locked_var.set, args=["Bar1"])
        thread2 = threading.Thread(target=locked_var.set, args=["Bar2"])
        thread3 = threading.Thread(target=locked_var.set, args=["Bar3"])

        with locked_var.lock:
            thread1.start()
            thread2.start()
            thread3.start()

            self.assertEqual("Foo", locked_var.val)
        thread1.join()
        thread2.join()
        thread3.join()
        self.assertEqual("Bar", locked_var.get()[0:3])

@unittest.skipUnless("unit" in limits, "Unit skipped")
class TradeStoreTest(WebMockTestCase):
    def test_compute_trades(self):
        self.m.balances.currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"]

        values_in_base = {
                "XMR": portfolio.Amount("BTC", D("0.9")),
                "DASH": portfolio.Amount("BTC", D("0.4")),
                "XVG": portfolio.Amount("BTC", D("-0.5")),
                "BTC": portfolio.Amount("BTC", D("0.5")),
                }
        new_repartition = {
                "DASH": portfolio.Amount("BTC", D("0.5")),
                "XVG": portfolio.Amount("BTC", D("0.1")),
                "BTC": portfolio.Amount("BTC", D("0.4")),
                "ETH": portfolio.Amount("BTC", D("0.3")),
                }
        side_effect = [
                (True, 1),
                (False, 2),
                (False, 3),
                (True, 4),
                (True, 5)
                ]

        with mock.patch.object(market.TradeStore, "trade_if_matching") as trade_if_matching:
            trade_store = market.TradeStore(self.m)
            trade_if_matching.side_effect = side_effect

            trade_store.compute_trades(values_in_base,
                    new_repartition, only="only")

            self.assertEqual(5, trade_if_matching.call_count)
            self.assertEqual(3, len(trade_store.all))
            self.assertEqual([1, 4, 5], trade_store.all)
            self.m.report.log_trades.assert_called_with(side_effect, "only")

    def test_trade_if_matching(self):

        with self.subTest(only="nope"):
            trade_store = market.TradeStore(self.m)
            result = trade_store.trade_if_matching(
                    portfolio.Amount("BTC", D("0")),
                    portfolio.Amount("BTC", D("0.3")),
                    "ETH", only="nope")
            self.assertEqual(False, result[0])
            self.assertIsInstance(result[1], portfolio.Trade)

        with self.subTest(only=None):
            trade_store = market.TradeStore(self.m)
            result = trade_store.trade_if_matching(
                    portfolio.Amount("BTC", D("0")),
                    portfolio.Amount("BTC", D("0.3")),
                    "ETH", only=None)
            self.assertEqual(True, result[0])

        with self.subTest(only="acquire"):
            trade_store = market.TradeStore(self.m)
            result = trade_store.trade_if_matching(
                    portfolio.Amount("BTC", D("0")),
                    portfolio.Amount("BTC", D("0.3")),
                    "ETH", only="acquire")
            self.assertEqual(True, result[0])

        with self.subTest(only="dispose"):
            trade_store = market.TradeStore(self.m)
            result = trade_store.trade_if_matching(
                    portfolio.Amount("BTC", D("0")),
                    portfolio.Amount("BTC", D("0.3")),
                    "ETH", only="dispose")
            self.assertEqual(False, result[0])

    def test_prepare_orders(self):
        trade_store = market.TradeStore(self.m)

        trade_mock1 = mock.Mock()
        trade_mock2 = mock.Mock()
        trade_mock3 = mock.Mock()

        trade_mock1.prepare_order.return_value = 1
        trade_mock2.prepare_order.return_value = 2
        trade_mock3.prepare_order.return_value = 3

        trade_mock1.pending = True
        trade_mock2.pending = True
        trade_mock3.pending = False

        trade_store.all.append(trade_mock1)
        trade_store.all.append(trade_mock2)
        trade_store.all.append(trade_mock3)

        trade_store.prepare_orders()
        trade_mock1.prepare_order.assert_called_with(compute_value="default")
        trade_mock2.prepare_order.assert_called_with(compute_value="default")
        trade_mock3.prepare_order.assert_not_called()
        self.m.report.log_orders.assert_called_once_with([1, 2], None, "default")

        self.m.report.log_orders.reset_mock()

        trade_store.prepare_orders(compute_value="bla")
        trade_mock1.prepare_order.assert_called_with(compute_value="bla")
        trade_mock2.prepare_order.assert_called_with(compute_value="bla")
        self.m.report.log_orders.assert_called_once_with([1, 2], None, "bla")

        trade_mock1.prepare_order.reset_mock()
        trade_mock2.prepare_order.reset_mock()
        self.m.report.log_orders.reset_mock()

        trade_mock1.action = "foo"
        trade_mock2.action = "bar"
        trade_store.prepare_orders(only="bar")
        trade_mock1.prepare_order.assert_not_called()
        trade_mock2.prepare_order.assert_called_with(compute_value="default")
        self.m.report.log_orders.assert_called_once_with([2], "bar", "default")

    def test_print_all_with_order(self):
        trade_mock1 = mock.Mock()
        trade_mock2 = mock.Mock()
        trade_mock3 = mock.Mock()
        trade_store = market.TradeStore(self.m)
        trade_store.all = [trade_mock1, trade_mock2, trade_mock3]

        trade_store.print_all_with_order()

        trade_mock1.print_with_order.assert_called()
        trade_mock2.print_with_order.assert_called()
        trade_mock3.print_with_order.assert_called()

    def test_run_orders(self):
        with mock.patch.object(market.TradeStore, "all_orders") as all_orders:
            order_mock1 = mock.Mock()
            order_mock2 = mock.Mock()
            order_mock3 = mock.Mock()
            trade_store = market.TradeStore(self.m)

            all_orders.return_value = [order_mock1, order_mock2, order_mock3]

            trade_store.run_orders()

            all_orders.assert_called_with(state="pending")

        order_mock1.run.assert_called()
        order_mock2.run.assert_called()
        order_mock3.run.assert_called()

        self.m.report.log_stage.assert_called_with("run_orders")
        self.m.report.log_orders.assert_called_with([order_mock1, order_mock2,
            order_mock3])

    def test_all_orders(self):
        trade_mock1 = mock.Mock()
        trade_mock2 = mock.Mock()

        order_mock1 = mock.Mock()
        order_mock2 = mock.Mock()
        order_mock3 = mock.Mock()

        trade_mock1.orders = [order_mock1, order_mock2]
        trade_mock2.orders = [order_mock3]

        order_mock1.status = "pending"
        order_mock2.status = "open"
        order_mock3.status = "open"

        trade_store = market.TradeStore(self.m)
        trade_store.all.append(trade_mock1)
        trade_store.all.append(trade_mock2)

        orders = trade_store.all_orders()
        self.assertEqual(3, len(orders))

        open_orders = trade_store.all_orders(state="open")
        self.assertEqual(2, len(open_orders))
        self.assertEqual([order_mock2, order_mock3], open_orders)

    def test_update_all_orders_status(self):
        with mock.patch.object(market.TradeStore, "all_orders") as all_orders:
            order_mock1 = mock.Mock()
            order_mock2 = mock.Mock()
            order_mock3 = mock.Mock()

            all_orders.return_value = [order_mock1, order_mock2, order_mock3]
            
            trade_store = market.TradeStore(self.m)

            trade_store.update_all_orders_status()
            all_orders.assert_called_with(state="open")

            order_mock1.get_status.assert_called()
            order_mock2.get_status.assert_called()
            order_mock3.get_status.assert_called()

    def test_close_trades(self):
        trade_mock1 = mock.Mock()
        trade_mock2 = mock.Mock()
        trade_mock3 = mock.Mock()

        trade_store = market.TradeStore(self.m)

        trade_store.all.append(trade_mock1)
        trade_store.all.append(trade_mock2)
        trade_store.all.append(trade_mock3)

        trade_store.close_trades()

        trade_mock1.close.assert_called_once_with()
        trade_mock2.close.assert_called_once_with()
        trade_mock3.close.assert_called_once_with()

    def test_pending(self):
        trade_mock1 = mock.Mock()
        trade_mock1.pending = True
        trade_mock2 = mock.Mock()
        trade_mock2.pending = True
        trade_mock3 = mock.Mock()
        trade_mock3.pending = False

        trade_store = market.TradeStore(self.m)

        trade_store.all.append(trade_mock1)
        trade_store.all.append(trade_mock2)
        trade_store.all.append(trade_mock3)

        self.assertEqual([trade_mock1, trade_mock2], trade_store.pending)

@unittest.skipUnless("unit" in limits, "Unit skipped")
class BalanceStoreTest(WebMockTestCase):
    def setUp(self):
        super().setUp()

        self.fetch_balance = {
                "ETC": {
                    "exchange_free": 0,
                    "exchange_used": 0,
                    "exchange_total": 0,
                    "margin_total": 0,
                    },
                "USDT": {
                    "exchange_free": D("6.0"),
                    "exchange_used": D("1.2"),
                    "exchange_total": D("7.2"),
                    "margin_total": 0,
                    },
                "XVG": {
                    "exchange_free": 16,
                    "exchange_used": 0,
                    "exchange_total": 16,
                    "margin_total": 0,
                    },
                "XMR": {
                    "exchange_free": 0,
                    "exchange_used": 0,
                    "exchange_total": 0,
                    "margin_total": D("-1.0"),
                    "margin_free": 0,
                    },
                }

    def test_in_currency(self):
        self.m.get_ticker.return_value = {
                "bid": D("0.09"),
                "ask": D("0.11"),
                "average": D("0.1"),
                }

        balance_store = market.BalanceStore(self.m)
        balance_store.all = {
                "BTC": portfolio.Balance("BTC", {
                    "total": "0.65",
                    "exchange_total":"0.65",
                    "exchange_free": "0.35",
                    "exchange_used": "0.30"}),
                "ETH": portfolio.Balance("ETH", {
                    "total": 3,
                    "exchange_total": 3,
                    "exchange_free": 3,
                    "exchange_used": 0}),
                }

        amounts = balance_store.in_currency("BTC")
        self.assertEqual("BTC", amounts["ETH"].currency)
        self.assertEqual(D("0.65"), amounts["BTC"].value)
        self.assertEqual(D("0.30"), amounts["ETH"].value)
        self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
                "average", "total")
        self.m.report.log_tickers.reset_mock()

        amounts = balance_store.in_currency("BTC", compute_value="bid")
        self.assertEqual(D("0.65"), amounts["BTC"].value)
        self.assertEqual(D("0.27"), amounts["ETH"].value)
        self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
                "bid", "total")
        self.m.report.log_tickers.reset_mock()

        amounts = balance_store.in_currency("BTC", compute_value="bid", type="exchange_used")
        self.assertEqual(D("0.30"), amounts["BTC"].value)
        self.assertEqual(0, amounts["ETH"].value)
        self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
                "bid", "exchange_used")
        self.m.report.log_tickers.reset_mock()

    def test_fetch_balances(self):
        self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance

        balance_store = market.BalanceStore(self.m)

        with self.subTest(log_tickers=False):
            balance_store.fetch_balances()
            self.assertNotIn("ETC", balance_store.currencies())
            self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies()))

            balance_store.all["ETC"] = portfolio.Balance("ETC", {
                "exchange_total": "1", "exchange_free": "0",
                "exchange_used": "1" })
            balance_store.fetch_balances(tag="foo")
            self.assertEqual(0, balance_store.all["ETC"].total)
            self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
            self.m.report.log_balances.assert_called_with(tag="foo", checkpoint=None)

        with self.subTest(log_tickers=True),\
                mock.patch.object(balance_store, "in_currency") as in_currency:
            in_currency.return_value = "tickers"
            balance_store.fetch_balances(log_tickers=True, ticker_currency="FOO",
                    ticker_compute_value="compute", ticker_type="type")
            self.m.report.log_balances.assert_called_with(compute_value='compute',
                    tag=None, checkpoint=None, ticker_currency='FOO', tickers='tickers',
                    type='type')

        balance_store = market.BalanceStore(self.m)
        with self.subTest(add_portfolio=True),\
                mock.patch.object(market.Portfolio, "repartition") as repartition:
            repartition.return_value = {
                    "DOGE": D("0.5"),
                    "USDT": D("0.5"),
                    }
            balance_store.fetch_balances(add_portfolio=True)
            self.assertListEqual(["USDT", "XVG", "XMR", "DOGE"], list(balance_store.currencies()))

        self.m.ccxt.fetch_all_balances.return_value = {
                "ETC": {
                    "exchange_free": 0,
                    "exchange_used": 0,
                    "exchange_total": 0,
                    "margin_total": 0,
                    },
                "XVG": {
                    "exchange_free": 16,
                    "exchange_used": 0,
                    "exchange_total": 16,
                    "margin_total": 0,
                    },
                "XMR": {
                    "exchange_free": 0,
                    "exchange_used": 0,
                    "exchange_total": 0,
                    "margin_total": D("-1.0"),
                    "margin_free": 0,
                    },
                }

        balance_store = market.BalanceStore(self.m)
        with self.subTest(add_usdt=True),\
                mock.patch.object(market.Portfolio, "repartition") as repartition:
            repartition.return_value = {
                    "DOGE": D("0.5"),
                    "ETH": D("0.5"),
                    }
            balance_store.fetch_balances(add_usdt=True)
            self.assertListEqual(["XVG", "XMR", "USDT"], list(balance_store.currencies()))

    @mock.patch.object(market.Portfolio, "repartition")
    def test_dispatch_assets(self, repartition):
        self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance

        balance_store = market.BalanceStore(self.m)
        balance_store.fetch_balances()

        self.assertNotIn("XEM", balance_store.currencies())

        repartition_hash = {
                "XEM": (D("0.75"), "long"),
                "BTC": (D("0.26"), "long"),
                "DASH": (D("0.10"), "short"),
                }
        repartition.return_value = repartition_hash

        amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1"))
        repartition.assert_called_with(liquidity="medium")
        self.assertIn("XEM", balance_store.currencies())
        self.assertEqual(D("2.6"), amounts["BTC"].value)
        self.assertEqual(D("7.5"), amounts["XEM"].value)
        self.assertEqual(D("-1.0"), amounts["DASH"].value)
        self.m.report.log_balances.assert_called_with(tag=None, checkpoint=None)
        self.m.report.log_dispatch.assert_called_once_with(portfolio.Amount("BTC",
            "11.1"), amounts, "medium", repartition_hash)

    def test_currencies(self):
        balance_store = market.BalanceStore(self.m)

        balance_store.all = {
                "BTC": portfolio.Balance("BTC", {
                    "total": "0.65",
                    "exchange_total":"0.65",
                    "exchange_free": "0.35",
                    "exchange_used": "0.30"}),
                "ETH": portfolio.Balance("ETH", {
                    "total": 3,
                    "exchange_total": 3,
                    "exchange_free": 3,
                    "exchange_used": 0}),
                }
        self.assertListEqual(["BTC", "ETH"], list(balance_store.currencies()))

    def test_as_json(self):
        balance_mock1 = mock.Mock()
        balance_mock1.as_json.return_value = 1

        balance_mock2 = mock.Mock()
        balance_mock2.as_json.return_value = 2

        balance_store = market.BalanceStore(self.m)
        balance_store.all = {
                "BTC": balance_mock1,
                "ETH": balance_mock2,
                }

        as_json = balance_store.as_json()
        self.assertEqual(1, as_json["BTC"])
        self.assertEqual(2, as_json["ETH"])

@unittest.skipUnless("unit" in limits, "Unit skipped")
class ReportStoreTest(WebMockTestCase):
    def test_add_log(self):
        with self.subTest(market=self.m):
            self.m.user_id = 1
            self.m.market_id = 3
            report_store = market.ReportStore(self.m)
            result = report_store.add_log({"foo": "bar"})

            self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": 1, "market_id": 3}, result)
            self.assertEqual(result, report_store.logs[0])

        with self.subTest(market=None):
            report_store = market.ReportStore(None)
            result = report_store.add_log({"foo": "bar"})

            self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": None, "market_id": None}, result)

    def test_add_redis_status(self):
        report_store = market.ReportStore(self.m)
        result = report_store.add_redis_status({"foo": "bar"})

        self.assertEqual({"foo": "bar"}, result)
        self.assertEqual(result, report_store.redis_status[0])

    def test_set_verbose(self):
        report_store = market.ReportStore(self.m)
        with self.subTest(verbose=True):
            report_store.set_verbose(True)
            self.assertTrue(report_store.verbose_print)

        with self.subTest(verbose=False):
            report_store.set_verbose(False)
            self.assertFalse(report_store.verbose_print)

    def test_merge(self):
        self.m.user_id = 1
        self.m.market_id = 3
        report_store1 = market.ReportStore(self.m, verbose_print=False)
        report_store2 = market.ReportStore(None, verbose_print=False)

        report_store2.log_stage("1")
        report_store1.log_stage("2")
        report_store2.log_stage("3")

        report_store1.merge(report_store2)

        self.assertEqual(3, len(report_store1.logs))
        self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs)))
        self.assertEqual(6, len(report_store1.print_logs))

    def test_print_log(self):
        report_store = market.ReportStore(self.m)
        with self.subTest(verbose=True),\
                mock.patch.object(store, "datetime") as time_mock,\
                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
            time_mock.datetime.now.return_value = datetime.datetime(2018, 2, 25, 2, 20, 10)
            report_store.set_verbose(True)
            report_store.print_log("Coucou")
            report_store.print_log(portfolio.Amount("BTC", 1))
            self.assertEqual(stdout_mock.getvalue(), "2018-02-25 02:20:10: Coucou\n2018-02-25 02:20:10: 1.00000000 BTC\n")

        with self.subTest(verbose=False),\
                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
            report_store.set_verbose(False)
            report_store.print_log("Coucou")
            report_store.print_log(portfolio.Amount("BTC", 1))
            self.assertEqual(stdout_mock.getvalue(), "")

    def test_default_json_serial(self):
        report_store = market.ReportStore(self.m)

        self.assertEqual("2018-02-24T00:00:00",
                report_store.default_json_serial(portfolio.datetime.datetime(2018, 2, 24)))
        self.assertEqual("1.00000000 BTC",
                report_store.default_json_serial(portfolio.Amount("BTC", 1)))

    def test_to_json(self):
        report_store = market.ReportStore(self.m)
        report_store.logs.append({"foo": "bar"})
        self.assertEqual('[\n  {\n    "foo": "bar"\n  }\n]', report_store.to_json())
        report_store.logs.append({"date": portfolio.datetime.datetime(2018, 2, 24)})
        self.assertEqual('[\n  {\n    "foo": "bar"\n  },\n  {\n    "date": "2018-02-24T00:00:00"\n  }\n]', report_store.to_json())
        report_store.logs.append({"amount": portfolio.Amount("BTC", 1)})
        self.assertEqual('[\n  {\n    "foo": "bar"\n  },\n  {\n    "date": "2018-02-24T00:00:00"\n  },\n  {\n    "amount": "1.00000000 BTC"\n  }\n]', report_store.to_json())

    def test_to_json_array(self):
        report_store = market.ReportStore(self.m)
        report_store.logs.append({
            "date": "date1", "type": "type1", "foo": "bar", "bla": "bla"
            })
        report_store.logs.append({
            "date": "date2", "type": "type2", "foo": "bar", "bla": "bla"
            })
        logs = list(report_store.to_json_array())

        self.assertEqual(2, len(logs))
        self.assertEqual(("date1", "type1", '{\n  "foo": "bar",\n  "bla": "bla"\n}'), logs[0])
        self.assertEqual(("date2", "type2", '{\n  "foo": "bar",\n  "bla": "bla"\n}'), logs[1])

    def test_to_json_redis(self):
        report_store = market.ReportStore(self.m)
        report_store.redis_status.append({
            "type": "type1", "foo": "bar", "bla": "bla"
            })
        report_store.redis_status.append({
            "type": "type2", "foo": "bar", "bla": "bla"
            })
        logs = list(report_store.to_json_redis())

        self.assertEqual(2, len(logs))
        self.assertEqual(("type1", '{"foo": "bar", "bla": "bla"}'), logs[0])
        self.assertEqual(("type2", '{"foo": "bar", "bla": "bla"}'), logs[1])

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_stage(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        c = lambda x: x
        report_store.log_stage("foo", bar="baz", c=c, d=portfolio.Amount("BTC", 1))
        print_log.assert_has_calls([
            mock.call("-----------"),
            mock.call("[Stage] foo bar=baz, c=c = lambda x: x, d={'currency': 'BTC', 'value': Decimal('1')}"),
            ])
        add_log.assert_called_once_with({
            'type': 'stage',
            'stage': 'foo',
            'args': {
                'bar': 'baz',
                'c': 'c = lambda x: x',
                'd': {
                    'currency': 'BTC',
                    'value': D('1')
                    }
                }
            })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    @mock.patch.object(market.ReportStore, "add_redis_status")
    def test_log_balances(self, add_redis_status, add_log, print_log):
        report_store = market.ReportStore(self.m)
        self.m.balances.as_json.return_value = "json"
        self.m.balances.all = { "FOO": "bar", "BAR": "baz" }

        with self.subTest(tickers=None):
            report_store.log_balances(tag="tag")
            print_log.assert_has_calls([
                mock.call("[Balance]"),
                mock.call("\tbar"),
                mock.call("\tbaz"),
                ])
            add_log.assert_called_once_with({
                'type': 'balance',
                'checkpoint': None,
                'balances': 'json',
                'tag': 'tag'
                })
            add_redis_status.assert_called_once_with({
                'type': 'balance',
                'balances': 'json',
                'checkpoint': None,
                'tag': 'tag'
                })
        add_log.reset_mock()
        add_redis_status.reset_mock()
        with self.subTest(tickers="present"):
            amounts = {
                    "BTC": portfolio.Amount("BTC", 10),
                    "ETH": portfolio.Amount("BTC", D("0.3"))
                    }
            amounts["ETH"].rate = D("0.1")

            report_store.log_balances(tag="tag", tickers=amounts,
                    ticker_currency="BTC", compute_value="default",
                    type="total")
            add_log.assert_called_once_with({
                'type': 'balance',
                'checkpoint': None,
                'balances': 'json',
                'tag': 'tag',
                'tickers': {
                    'compute_value': 'default',
                    'balance_type': 'total',
                    'currency': 'BTC',
                    'balances': {
                        'BTC': D('10'),
                        'ETH': D('0.3')
                        },
                    'rates': {
                        'BTC': None,
                        'ETH': D('0.1')
                        },
                    'total': D('10.3')
                    },
                })
            add_redis_status.assert_called_once_with({
                'type': 'balance',
                'checkpoint': None,
                'balances': 'json',
                'tag': 'tag',
                'tickers': {
                    'compute_value': 'default',
                    'balance_type': 'total',
                    'currency': 'BTC',
                    'balances': {
                        'BTC': D('10'),
                        'ETH': D('0.3')
                        },
                    'rates': {
                        'BTC': None,
                        'ETH': D('0.1')
                        },
                    'total': D('10.3')
                    },
                })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_tickers(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        amounts = {
                "BTC": portfolio.Amount("BTC", 10),
                "ETH": portfolio.Amount("BTC", D("0.3"))
                }
        amounts["ETH"].rate = D("0.1")

        report_store.log_tickers(amounts, "BTC", "default", "total")
        print_log.assert_not_called()
        add_log.assert_called_once_with({
            'type': 'tickers',
            'compute_value': 'default',
            'balance_type': 'total',
            'currency': 'BTC',
            'balances': {
                'BTC': D('10'),
                'ETH': D('0.3')
                },
            'rates': {
                'BTC': None,
                'ETH': D('0.1')
                },
            'total': D('10.3')
            })

        add_log.reset_mock()
        compute_value = lambda x: x["bid"]
        report_store.log_tickers(amounts, "BTC", compute_value, "total")
        add_log.assert_called_once_with({
            'type': 'tickers',
            'compute_value': 'compute_value = lambda x: x["bid"]',
            'balance_type': 'total',
            'currency': 'BTC',
            'balances': {
                'BTC': D('10'),
                'ETH': D('0.3')
                },
            'rates': {
                'BTC': None,
                'ETH': D('0.1')
                },
            'total': D('10.3')
            })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_dispatch(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        amount = portfolio.Amount("BTC", "10.3")
        amounts = {
                "BTC": portfolio.Amount("BTC", 10),
                "ETH": portfolio.Amount("BTC", D("0.3"))
                }
        report_store.log_dispatch(amount, amounts, "medium", "repartition")
        print_log.assert_not_called()
        add_log.assert_called_once_with({
            'type': 'dispatch',
            'liquidity': 'medium',
            'repartition_ratio': 'repartition',
            'total_amount': {
                'currency': 'BTC',
                'value': D('10.3')
                },
            'repartition': {
                'BTC': D('10'),
                'ETH': D('0.3')
                }
            })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_trades(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        trade_mock1 = mock.Mock()
        trade_mock2 = mock.Mock()
        trade_mock1.as_json.return_value = { "trade": "1" }
        trade_mock2.as_json.return_value = { "trade": "2" }

        matching_and_trades = [
                (True, trade_mock1),
                (False, trade_mock2),
                ]
        report_store.log_trades(matching_and_trades, "only")

        print_log.assert_not_called()
        add_log.assert_called_with({
            'type': 'trades',
            'only': 'only',
            'debug': False,
            'trades': [
                {'trade': '1', 'skipped': False},
                {'trade': '2', 'skipped': True}
                ]
            })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_orders(self, add_log, print_log):
        report_store = market.ReportStore(self.m)

        order_mock1 = mock.Mock()
        order_mock2 = mock.Mock()

        order_mock1.as_json.return_value = "order1"
        order_mock2.as_json.return_value = "order2"

        orders = [order_mock1, order_mock2]

        report_store.log_orders(orders, tick="tick",
                only="only", compute_value="compute_value")

        print_log.assert_called_once_with("[Orders]")
        self.m.trades.print_all_with_order.assert_called_once_with(ind="\t")

        add_log.assert_called_with({
            'type': 'orders',
            'only': 'only',
            'compute_value': 'compute_value',
            'tick': 'tick',
            'orders': ['order1', 'order2']
            })

        add_log.reset_mock()
        def compute_value(x, y):
            return x[y]
        report_store.log_orders(orders, tick="tick",
                only="only", compute_value=compute_value)
        add_log.assert_called_with({
            'type': 'orders',
            'only': 'only',
            'compute_value': 'def compute_value(x, y):\n            return x[y]',
            'tick': 'tick',
            'orders': ['order1', 'order2']
            })


    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_order(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        order_mock = mock.Mock()
        order_mock.as_json.return_value = "order"
        new_order_mock = mock.Mock()
        new_order_mock.as_json.return_value = "new_order"
        order_mock.__repr__ = mock.Mock()
        order_mock.__repr__.return_value = "Order Mock"
        new_order_mock.__repr__ = mock.Mock()
        new_order_mock.__repr__.return_value = "New order Mock"

        with self.subTest(finished=True):
            report_store.log_order(order_mock, 1, finished=True)
            print_log.assert_called_once_with("[Order] Finished Order Mock")
            add_log.assert_called_once_with({
                'type': 'order',
                'tick': 1,
                'update': None,
                'order': 'order',
                'compute_value': None,
                'new_order': None
                })

        add_log.reset_mock()
        print_log.reset_mock()

        with self.subTest(update="waiting"):
            report_store.log_order(order_mock, 1, update="waiting")
            print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting")
            add_log.assert_called_once_with({
                'type': 'order',
                'tick': 1,
                'update': 'waiting',
                'order': 'order',
                'compute_value': None,
                'new_order': None
                })

        add_log.reset_mock()
        print_log.reset_mock()
        with self.subTest(update="adjusting"):
            compute_value = lambda x: (x["bid"] + x["ask"]*2)/3
            report_store.log_order(order_mock, 3,
                    update="adjusting", new_order=new_order_mock,
                    compute_value=compute_value)
            print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock")
            add_log.assert_called_once_with({
                'type': 'order',
                'tick': 3,
                'update': 'adjusting',
                'order': 'order',
                'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3',
                'new_order': 'new_order'
                })

        add_log.reset_mock()
        print_log.reset_mock()
        with self.subTest(update="market_fallback"):
            report_store.log_order(order_mock, 7,
                    update="market_fallback", new_order=new_order_mock)
            print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value")
            add_log.assert_called_once_with({
                'type': 'order',
                'tick': 7,
                'update': 'market_fallback',
                'order': 'order',
                'compute_value': None,
                'new_order': 'new_order'
                })

        add_log.reset_mock()
        print_log.reset_mock()
        with self.subTest(update="market_adjusting"):
            report_store.log_order(order_mock, 17,
                    update="market_adjust", new_order=new_order_mock)
            print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock")
            add_log.assert_called_once_with({
                'type': 'order',
                'tick': 17,
                'update': 'market_adjust',
                'order': 'order',
                'compute_value': None,
                'new_order': 'new_order'
                })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_move_balances(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        needed = {
                "BTC": portfolio.Amount("BTC", 10),
                "USDT": 1
                }
        moving = {
                "BTC": portfolio.Amount("BTC", 3),
                "USDT": -2
                }
        report_store.log_move_balances(needed, moving)
        print_log.assert_not_called()
        add_log.assert_called_once_with({
            'type': 'move_balances',
            'debug': False,
            'needed': {
                'BTC': D('10'),
                'USDT': 1
                },
            'moving': {
                'BTC': D('3'),
                'USDT': -2
                }
            })

    def test_log_http_request(self):
        with mock.patch.object(market.ReportStore, "add_log") as add_log:
            report_store = market.ReportStore(self.m)
            response = mock.Mock()
            response.status_code = 200
            response.text = "Hey"
            response.elapsed.total_seconds.return_value = 120

            report_store.log_http_request("method", "url", "body",
                    "headers", response)
            add_log.assert_called_once_with({
                'type': 'http_request',
                'method': 'method',
                'url': 'url',
                'body': 'body',
                'headers': 'headers',
                'status': 200,
                'duration': 120,
                'response': 'Hey',
                'response_same_as': None,
                })

            add_log.reset_mock()
            report_store.log_http_request("method", "url", "body",
                    "headers", ValueError("Foo"))
            add_log.assert_called_once_with({
                'type': 'http_request',
                'method': 'method',
                'url': 'url',
                'body': 'body',
                'headers': 'headers',
                'status': -1,
                'response': None,
                'error': 'ValueError',
                'error_message': 'Foo',
                })

        with self.subTest(no_http_dup=True, duplicate=True):
            self.m.user_id = 1
            self.m.market_id = 3
            report_store = market.ReportStore(self.m, no_http_dup=True)
            original_add_log = report_store.add_log
            with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log:
                report_store.log_http_request("method", "url", "body",
                        "headers", response)
                report_store.log_http_request("method", "url", "body",
                        "headers", response)
                self.assertEqual(2, add_log.call_count)
                self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"])
                self.assertIsNone(add_log.mock_calls[1][1][0]["response"])
                self.assertEqual(add_log.mock_calls[0][1][0]["date"], add_log.mock_calls[1][1][0]["response_same_as"])
        with self.subTest(no_http_dup=True, duplicate=False, case="Different call"):
            self.m.user_id = 1
            self.m.market_id = 3
            report_store = market.ReportStore(self.m, no_http_dup=True)
            original_add_log = report_store.add_log
            with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log:
                report_store.log_http_request("method", "url", "body",
                        "headers", response)
                report_store.log_http_request("method2", "url", "body",
                        "headers", response)
                self.assertEqual(2, add_log.call_count)
                self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"])
                self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"])
        with self.subTest(no_http_dup=True, duplicate=False, case="Call inbetween"):
            self.m.user_id = 1
            self.m.market_id = 3
            report_store = market.ReportStore(self.m, no_http_dup=True)
            original_add_log = report_store.add_log

            response2 = mock.Mock()
            response2.status_code = 200
            response2.text = "Hey there!"

            with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log:
                report_store.log_http_request("method", "url", "body",
                        "headers", response)
                report_store.log_http_request("method", "url", "body",
                        "headers", response2)
                report_store.log_http_request("method", "url", "body",
                        "headers", response)
                self.assertEqual(3, add_log.call_count)
                self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"])
                self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"])
                self.assertIsNone(add_log.mock_calls[2][1][0]["response_same_as"])

    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_market(self, add_log):
        report_store = market.ReportStore(self.m)

        report_store.log_market(self.market_args(debug=True, quiet=False))
        add_log.assert_called_once_with({
            "type": "market",
            "commit": "$Format:%H$",
            "args": { "report_path": None, "debug": True, "quiet": False },
            })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_error(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        with self.subTest(message=None, exception=None):
            report_store.log_error("action")
            print_log.assert_called_once_with("[Error] action")
            add_log.assert_called_once_with({
                'type': 'error',
                'action': 'action',
                'exception_class': None,
                'exception_message': None,
                'message': None
                })

        print_log.reset_mock()
        add_log.reset_mock()
        with self.subTest(message="Hey", exception=None):
            report_store.log_error("action", message="Hey")
            print_log.assert_has_calls([
                    mock.call("[Error] action"),
                    mock.call("\tHey")
                    ])
            add_log.assert_called_once_with({
                'type': 'error',
                'action': 'action',
                'exception_class': None,
                'exception_message': None,
                'message': "Hey"
                })

        print_log.reset_mock()
        add_log.reset_mock()
        with self.subTest(message=None, exception=Exception("bouh")):
            report_store.log_error("action", exception=Exception("bouh"))
            print_log.assert_has_calls([
                    mock.call("[Error] action"),
                    mock.call("\tException: bouh")
                    ])
            add_log.assert_called_once_with({
                'type': 'error',
                'action': 'action',
                'exception_class': "Exception",
                'exception_message': "bouh",
                'message': None
                })

        print_log.reset_mock()
        add_log.reset_mock()
        with self.subTest(message="Hey", exception=Exception("bouh")):
            report_store.log_error("action", message="Hey", exception=Exception("bouh"))
            print_log.assert_has_calls([
                    mock.call("[Error] action"),
                    mock.call("\tException: bouh"),
                    mock.call("\tHey")
                    ])
            add_log.assert_called_once_with({
                'type': 'error',
                'action': 'action',
                'exception_class': "Exception",
                'exception_message': "bouh",
                'message': "Hey"
                })

    @mock.patch.object(market.ReportStore, "print_log")
    @mock.patch.object(market.ReportStore, "add_log")
    def test_log_debug_action(self, add_log, print_log):
        report_store = market.ReportStore(self.m)
        report_store.log_debug_action("Hey")

        print_log.assert_called_once_with("[Debug] Hey")
        add_log.assert_called_once_with({
            'type': 'debug_action',
            'action': 'Hey'
            })

@unittest.skipUnless("unit" in limits, "Unit skipped")
class PortfolioTest(WebMockTestCase):
    def setUp(self):
        super().setUp()

        with open("test_samples/test_portfolio.json") as example:
            self.json_response = example.read()

        self.wm.get(market.Portfolio.URL, text=self.json_response)

    @mock.patch.object(market.Portfolio, "parse_cryptoportfolio")
    @mock.patch.object(market.Portfolio, "store_cryptoportfolio")
    def test_get_cryptoportfolio(self, store_cryptoportfolio, parse_cryptoportfolio):
        with self.subTest(parallel=False):
            self.wm.get(market.Portfolio.URL, [
                {"text":'{ "foo": "bar" }', "status_code": 200},
                {"text": "System Error", "status_code": 500},
                {"exc": requests.exceptions.ConnectTimeout},
                ])
            market.Portfolio.get_cryptoportfolio()
            self.assertIn("foo", market.Portfolio.data.get())
            self.assertEqual("bar", market.Portfolio.data.get()["foo"])
            self.assertTrue(self.wm.called)
            self.assertEqual(1, self.wm.call_count)
            market.Portfolio.report.log_error.assert_not_called()
            market.Portfolio.report.log_http_request.assert_called_once()
            parse_cryptoportfolio.assert_called_once_with()
            store_cryptoportfolio.assert_called_once_with()
            market.Portfolio.report.log_http_request.reset_mock()
            parse_cryptoportfolio.reset_mock()
            store_cryptoportfolio.reset_mock()
            market.Portfolio.data = store.LockedVar(None)

            market.Portfolio.get_cryptoportfolio()
            self.assertIsNone(market.Portfolio.data.get())
            self.assertEqual(2, self.wm.call_count)
            parse_cryptoportfolio.assert_not_called()
            store_cryptoportfolio.assert_not_called()
            market.Portfolio.report.log_error.assert_not_called()
            market.Portfolio.report.log_http_request.assert_called_once()
            market.Portfolio.report.log_http_request.reset_mock()
            parse_cryptoportfolio.reset_mock()
            store_cryptoportfolio.reset_mock()

            market.Portfolio.data = store.LockedVar("Foo")
            market.Portfolio.get_cryptoportfolio()
            self.assertEqual(2, self.wm.call_count)
            parse_cryptoportfolio.assert_not_called()
            store_cryptoportfolio.assert_not_called()

            market.Portfolio.get_cryptoportfolio(refetch=True)
            self.assertEqual("Foo", market.Portfolio.data.get())
            self.assertEqual(3, self.wm.call_count)
            market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio",
                    exception=mock.ANY)
            market.Portfolio.report.log_http_request.assert_not_called()
        with self.subTest(parallel=True):
            with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\
                    mock.patch.object(market.Portfolio, "notify_and_wait") as notify:
                with self.subTest(worker=True):
                    market.Portfolio.data = store.LockedVar(None)
                    market.Portfolio.worker = mock.Mock()
                    is_worker.return_value = True
                    self.wm.get(market.Portfolio.URL, [
                        {"text":'{ "foo": "bar" }', "status_code": 200},
                        ])
                    market.Portfolio.get_cryptoportfolio()
                    self.assertIn("foo", market.Portfolio.data.get())
                parse_cryptoportfolio.reset_mock()
                store_cryptoportfolio.reset_mock()
                with self.subTest(worker=False):
                    market.Portfolio.data = store.LockedVar(None)
                    market.Portfolio.worker = mock.Mock()
                    is_worker.return_value = False
                    market.Portfolio.get_cryptoportfolio()
                    notify.assert_called_once_with()
                    parse_cryptoportfolio.assert_not_called()
                    store_cryptoportfolio.assert_not_called()

    def test_parse_cryptoportfolio(self):
        with self.subTest(description="Normal case"):
            market.Portfolio.data = store.LockedVar(store.json.loads(
                self.json_response, parse_int=D, parse_float=D))
            market.Portfolio.parse_cryptoportfolio()

            self.assertListEqual(
                    ["medium", "high"],
                    list(market.Portfolio.liquidities.get().keys()))

            liquidities = market.Portfolio.liquidities.get()
            self.assertEqual(10, len(liquidities["medium"].keys()))
            self.assertEqual(10, len(liquidities["high"].keys()))

            expected = {
                    'BTC':  (D("0.2857"), "long"),
                    'DGB':  (D("0.1015"), "long"),
                    'DOGE': (D("0.1805"), "long"),
                    'SC':   (D("0.0623"), "long"),
                    'ZEC':  (D("0.3701"), "long"),
                    }
            date = portfolio.datetime.datetime(2018, 1, 8)
            self.assertDictEqual(expected, liquidities["high"][date])

            expected = {
                    'BTC':  (D("1.1102e-16"), "long"),
                    'ETC':  (D("0.1"), "long"),
                    'FCT':  (D("0.1"), "long"),
                    'GAS':  (D("0.1"), "long"),
                    'NAV':  (D("0.1"), "long"),
                    'OMG':  (D("0.1"), "long"),
                    'OMNI': (D("0.1"), "long"),
                    'PPC':  (D("0.1"), "long"),
                    'RIC':  (D("0.1"), "long"),
                    'VIA':  (D("0.1"), "long"),
                    'XCP':  (D("0.1"), "long"),
                    }
            self.assertDictEqual(expected, liquidities["medium"][date])
            self.assertEqual(portfolio.datetime.datetime(2018, 1, 15), market.Portfolio.last_date.get())

        with self.subTest(description="Missing weight"):
            data = store.json.loads(self.json_response, parse_int=D, parse_float=D)
            del(data["portfolio_2"]["weights"])
            market.Portfolio.data = store.LockedVar(data)

            market.Portfolio.parse_cryptoportfolio()
            self.assertListEqual(
                    ["medium", "high"],
                    list(market.Portfolio.liquidities.get().keys()))
            self.assertEqual({}, market.Portfolio.liquidities.get("medium"))

        with self.subTest(description="All missing weights"):
            data = store.json.loads(self.json_response, parse_int=D, parse_float=D)
            del(data["portfolio_1"]["weights"])
            del(data["portfolio_2"]["weights"])
            market.Portfolio.data = store.LockedVar(data)

            market.Portfolio.parse_cryptoportfolio()
            self.assertEqual({}, market.Portfolio.liquidities.get("medium"))
            self.assertEqual({}, market.Portfolio.liquidities.get("high"))
            self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get())

    @mock.patch.object(store.dbs, "redis_connected")
    @mock.patch.object(store.dbs, "redis")
    def test_store_cryptoportfolio(self, redis, redis_connected):
        store.Portfolio.liquidities = store.LockedVar({
                "medium": {
                    datetime.datetime(2018,3,1): "medium_2018-03-01",
                    datetime.datetime(2018,3,8): "medium_2018-03-08",
                    },
                "high": {
                    datetime.datetime(2018,3,1): "high_2018-03-01",
                    datetime.datetime(2018,3,8): "high_2018-03-08",
                    }
                })
        store.Portfolio.last_date = store.LockedVar(datetime.datetime(2018,3,8))

        with self.subTest(redis_connected=False):
            redis_connected.return_value = False
            store.Portfolio.store_cryptoportfolio()
            redis.set.assert_not_called()

        with self.subTest(redis_connected=True):
            redis_connected.return_value = True
            store.Portfolio.store_cryptoportfolio()
            redis.set.assert_has_calls([
                mock.call("/cryptoportfolio/repartition/latest", '{"medium": "medium_2018-03-08", "high": "high_2018-03-08"}'),
                mock.call("/cryptoportfolio/repartition/date", "2018-03-08"),
                ])

    @mock.patch.object(store.dbs, "redis_connected")
    @mock.patch.object(store.dbs, "redis")
    def test_retrieve_cryptoportfolio(self, redis, redis_connected):
        with self.subTest(redis_connected=False):
            redis_connected.return_value = False
            store.Portfolio.retrieve_cryptoportfolio()
            redis.get.assert_not_called()
            self.assertIsNone(store.Portfolio.data.get())

        with self.subTest(redis_connected=True, value=None):
            redis_connected.return_value = True
            redis.get.return_value = None
            store.Portfolio.retrieve_cryptoportfolio()
            self.assertEqual(2, redis.get.call_count)

        redis.reset_mock()
        with self.subTest(redis_connected=True, value="present"):
            redis_connected.return_value = True
            redis.get.side_effect = [
                    b'{ "medium": "medium_repartition", "high": "high_repartition" }',
                    b"2018-03-08"
                    ]
            store.Portfolio.retrieve_cryptoportfolio()
            self.assertEqual(2, redis.get.call_count)
            self.assertEqual(datetime.datetime(2018,3,8), store.Portfolio.last_date.get())
            self.assertEqual("", store.Portfolio.data.get())
            expected_liquidities = {
                    'high': { datetime.datetime(2018, 3, 8): 'high_repartition' },
                    'medium': { datetime.datetime(2018, 3, 8): 'medium_repartition' },
                    }
            self.assertEqual(expected_liquidities, store.Portfolio.liquidities.get())

    @mock.patch.object(market.Portfolio, "get_cryptoportfolio")
    @mock.patch.object(market.Portfolio, "retrieve_cryptoportfolio")
    def test_repartition(self, retrieve_cryptoportfolio, get_cryptoportfolio):
        with self.subTest(from_cache=False):
            market.Portfolio.liquidities = store.LockedVar({
                    "medium": {
                        "2018-03-01": "medium_2018-03-01",
                        "2018-03-08": "medium_2018-03-08",
                        },
                    "high": {
                        "2018-03-01": "high_2018-03-01",
                        "2018-03-08": "high_2018-03-08",
                        }
                    })
            market.Portfolio.last_date = store.LockedVar("2018-03-08")

            self.assertEqual("medium_2018-03-08", market.Portfolio.repartition())
            get_cryptoportfolio.assert_called_once_with()
            retrieve_cryptoportfolio.assert_not_called()
            self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium"))
            self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high"))

        retrieve_cryptoportfolio.reset_mock()
        get_cryptoportfolio.reset_mock()

        with self.subTest(from_cache=True):
            self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(from_cache=True))
            get_cryptoportfolio.assert_called_once_with()
            retrieve_cryptoportfolio.assert_called_once_with()

    @mock.patch.object(market.time, "sleep")
    @mock.patch.object(market.Portfolio, "get_cryptoportfolio")
    def test_wait_for_recent(self, get_cryptoportfolio, sleep):
        self.call_count = 0
        def _get(refetch=False):
            if self.call_count != 0:
                self.assertTrue(refetch)
            else:
                self.assertFalse(refetch)
            self.call_count += 1
            market.Portfolio.last_date = store.LockedVar(store.datetime.datetime.now()\
                - store.datetime.timedelta(10)\
                + store.datetime.timedelta(self.call_count))
        get_cryptoportfolio.side_effect = _get

        market.Portfolio.wait_for_recent()
        sleep.assert_called_with(30)
        self.assertEqual(6, sleep.call_count)
        self.assertEqual(7, get_cryptoportfolio.call_count)
        market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio")

        sleep.reset_mock()
        get_cryptoportfolio.reset_mock()
        market.Portfolio.last_date = store.LockedVar(None)
        self.call_count = 0
        market.Portfolio.wait_for_recent(delta=15)
        sleep.assert_not_called()
        self.assertEqual(1, get_cryptoportfolio.call_count)

        sleep.reset_mock()
        get_cryptoportfolio.reset_mock()
        market.Portfolio.last_date = store.LockedVar(None)
        self.call_count = 0
        market.Portfolio.wait_for_recent(delta=1)
        sleep.assert_called_with(30)
        self.assertEqual(9, sleep.call_count)
        self.assertEqual(10, get_cryptoportfolio.call_count)

    def test_is_worker_thread(self):
        with self.subTest(worker=None):
            self.assertFalse(store.Portfolio.is_worker_thread())

        with self.subTest(worker="not self"),\
                mock.patch("threading.current_thread") as current_thread:
            current = mock.Mock()
            current_thread.return_value = current
            store.Portfolio.worker = mock.Mock()
            self.assertFalse(store.Portfolio.is_worker_thread())

        with self.subTest(worker="self"),\
                mock.patch("threading.current_thread") as current_thread:
            current = mock.Mock()
            current_thread.return_value = current
            store.Portfolio.worker = current
            self.assertTrue(store.Portfolio.is_worker_thread())

    def test_start_worker(self):
        with mock.patch.object(store.Portfolio, "wait_for_notification") as notification:
            store.Portfolio.start_worker()
            notification.assert_called_once_with(poll=30)

            self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__)
            self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__)
            store.Portfolio.report.start_lock.assert_called_once_with()

            self.assertIsNotNone(store.Portfolio.worker)
            self.assertIsNotNone(store.Portfolio.worker_notify)
            self.assertIsNotNone(store.Portfolio.callback)
            self.assertTrue(store.Portfolio.worker_started)

            self.assertFalse(store.Portfolio.worker.is_alive())
            self.assertEqual(1, threading.active_count())

    def test_stop_worker(self):
        with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\
                mock.patch.object(store.Portfolio, "report") as report,\
                mock.patch.object(store.time, "sleep") as sleep:
            store.Portfolio.start_worker(poll=3)
            store.Portfolio.stop_worker()
            store.Portfolio.worker.join()
            get.assert_not_called()
            report.assert_not_called()
            sleep.assert_not_called()
            self.assertFalse(store.Portfolio.worker.is_alive())

    def test_wait_for_notification(self):
        with self.assertRaises(RuntimeError):
            store.Portfolio.wait_for_notification()

        with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\
                mock.patch.object(store.Portfolio, "report") as report,\
                mock.patch.object(store.time, "sleep") as sleep:
            store.Portfolio.start_worker(poll=3)

            store.Portfolio.worker_notify.set()

            store.Portfolio.callback.wait()

            report.print_log.assert_called_once_with("[Worker] Fetching cryptoportfolio")
            get.assert_called_once_with(refetch=True)
            sleep.assert_called_once_with(3)
            self.assertFalse(store.Portfolio.worker_notify.is_set())
            self.assertTrue(store.Portfolio.worker.is_alive())

            store.Portfolio.callback.clear()
            store.Portfolio.worker_started = False
            store.Portfolio.worker_notify.set()
            store.Portfolio.worker.join()
            self.assertFalse(store.Portfolio.worker.is_alive())

    def test_notify_and_wait(self):
        with mock.patch.object(store.Portfolio, "callback") as callback,\
                mock.patch.object(store.Portfolio, "worker_notify") as worker_notify:
            store.Portfolio.notify_and_wait()
            callback.clear.assert_called_once_with()
            worker_notify.set.assert_called_once_with()
            callback.wait.assert_called_once_with()