from .helper import * import market, store, portfolio, dbs import datetime @unittest.skipUnless("unit" in limits, "Unit skipped") class MarketTest(WebMockTestCase): def setUp(self): super().setUp() self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) def test_values(self): m = market.Market(self.ccxt, self.market_args()) self.assertEqual(self.ccxt, m.ccxt) self.assertFalse(m.debug) self.assertIsInstance(m.report, market.ReportStore) self.assertIsInstance(m.trades, market.TradeStore) self.assertIsInstance(m.balances, market.BalanceStore) self.assertEqual(m, m.report.market) self.assertEqual(m, m.trades.market) self.assertEqual(m, m.balances.market) self.assertEqual(m, m.ccxt._market) m = market.Market(self.ccxt, self.market_args(debug=True)) self.assertTrue(m.debug) m = market.Market(self.ccxt, self.market_args(debug=False)) self.assertFalse(m.debug) with mock.patch("market.ReportStore") as report_store: with self.subTest(quiet=False): m = market.Market(self.ccxt, self.market_args(quiet=False)) report_store.assert_called_with(m, verbose_print=True) report_store().log_market.assert_called_once() report_store.reset_mock() with self.subTest(quiet=True): m = market.Market(self.ccxt, self.market_args(quiet=True)) report_store.assert_called_with(m, verbose_print=False) report_store().log_market.assert_called_once() @mock.patch("market.ccxt") def test_from_config(self, ccxt): with mock.patch("market.ReportStore"): ccxt.poloniexE.return_value = self.ccxt m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args()) self.assertEqual(self.ccxt, m.ccxt) m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True)) self.assertEqual(True, m.debug) def test_get_tickers(self): self.ccxt.fetch_tickers.side_effect = [ "tickers", market.NotSupported ] m = market.Market(self.ccxt, self.market_args()) self.assertEqual("tickers", m.get_tickers()) self.assertEqual("tickers", m.get_tickers()) self.ccxt.fetch_tickers.assert_called_once() self.assertIsNone(m.get_tickers(refresh=self.time.time())) def test_get_ticker(self): with self.subTest(get_tickers=True): self.ccxt.fetch_tickers.return_value = { "ETH/ETC": { "bid": 1, "ask": 3 }, "XVG/ETH": { "bid": 10, "ask": 40 }, } m = market.Market(self.ccxt, self.market_args()) ticker = m.get_ticker("ETH", "ETC") self.assertEqual(1, ticker["bid"]) self.assertEqual(3, ticker["ask"]) self.assertEqual(2, ticker["average"]) self.assertFalse(ticker["inverted"]) ticker = m.get_ticker("ETH", "XVG") self.assertEqual(0.0625, ticker["average"]) self.assertTrue(ticker["inverted"]) self.assertIn("original", ticker) self.assertEqual(10, ticker["original"]["bid"]) self.assertEqual(25, ticker["original"]["average"]) ticker = m.get_ticker("XVG", "XMR") self.assertIsNone(ticker) with self.subTest(get_tickers=False): self.ccxt.fetch_tickers.return_value = None self.ccxt.fetch_ticker.side_effect = [ { "bid": 1, "ask": 3 }, market.ExchangeError("foo"), { "bid": 10, "ask": 40 }, market.ExchangeError("foo"), market.ExchangeError("foo"), ] m = market.Market(self.ccxt, self.market_args()) ticker = m.get_ticker("ETH", "ETC") self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") self.assertEqual(1, ticker["bid"]) self.assertEqual(3, ticker["ask"]) self.assertEqual(2, ticker["average"]) self.assertFalse(ticker["inverted"]) ticker = m.get_ticker("ETH", "XVG") self.assertEqual(0.0625, ticker["average"]) self.assertTrue(ticker["inverted"]) self.assertIn("original", ticker) self.assertEqual(10, ticker["original"]["bid"]) self.assertEqual(25, ticker["original"]["average"]) ticker = m.get_ticker("XVG", "XMR") self.assertIsNone(ticker) def test_fetch_fees(self): m = market.Market(self.ccxt, self.market_args()) self.ccxt.fetch_fees.return_value = "Foo" self.assertEqual("Foo", m.fetch_fees()) self.ccxt.fetch_fees.assert_called_once() self.ccxt.reset_mock() self.assertEqual("Foo", m.fetch_fees()) self.ccxt.fetch_fees.assert_not_called() @mock.patch.object(market.Portfolio, "repartition") @mock.patch.object(market.Market, "get_ticker") @mock.patch.object(market.TradeStore, "compute_trades") def test_prepare_trades(self, compute_trades, get_ticker, repartition): with self.subTest(available_balance_only=False),\ mock.patch("market.ReportStore"): def _get_ticker(c1, c2): if c1 == "USDT" and c2 == "BTC": return { "average": D("0.0001") } if c1 == "XVG" and c2 == "BTC": return { "average": D("0.000001") } self.fail("Should not be called with {}, {}".format(c1, c2)) get_ticker.side_effect = _get_ticker repartition.return_value = { "XEM": (D("0.75"), "long"), "BTC": (D("0.25"), "long"), } m = market.Market(self.ccxt, self.market_args()) self.ccxt.fetch_all_balances.return_value = { "USDT": { "exchange_free": D("10000.0"), "exchange_used": D("0.0"), "exchange_total": D("10000.0"), "total": D("10000.0") }, "XVG": { "exchange_free": D("10000.0"), "exchange_used": D("0.0"), "exchange_total": D("10000.0"), "total": D("10000.0") }, } m.balances.fetch_balances(tag="tag") m.prepare_trades() compute_trades.assert_called() call = compute_trades.call_args self.assertEqual(1, call[0][0]["USDT"].value) self.assertEqual(D("0.01"), call[0][0]["XVG"].value) self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) m.report.log_stage.assert_called_once_with("prepare_trades", base_currency='BTC', compute_value='average', available_balance_only=False, liquidity='medium', only=None, repartition=None) m.report.log_balances.assert_called_once_with(tag="tag", checkpoint=None) compute_trades.reset_mock() with self.subTest(available_balance_only=True),\ mock.patch("market.ReportStore"): def _get_ticker(c1, c2): if c1 == "ZRC" and c2 == "BTC": return { "average": D("0.0001") } if c1 == "DOGE" and c2 == "BTC": return { "average": D("0.000001") } if c1 == "ETH" and c2 == "BTC": return { "average": D("0.1") } if c1 == "FOO" and c2 == "BTC": return { "average": D("0.1") } self.fail("Should not be called with {}, {}".format(c1, c2)) get_ticker.side_effect = _get_ticker repartition.return_value = { "DOGE": (D("0.20"), "short"), "BTC": (D("0.20"), "long"), "ETH": (D("0.20"), "long"), "XMR": (D("0.20"), "long"), "FOO": (D("0.20"), "long"), } m = market.Market(self.ccxt, self.market_args()) self.ccxt.fetch_all_balances.return_value = { "ZRC": { "exchange_free": D("2.0"), "exchange_used": D("0.0"), "exchange_total": D("2.0"), "total": D("2.0") }, "DOGE": { "exchange_free": D("5.0"), "exchange_used": D("0.0"), "exchange_total": D("5.0"), "total": D("5.0") }, "BTC": { "exchange_free": D("0.065"), "exchange_used": D("0.02"), "exchange_total": D("0.085"), "margin_available": D("0.035"), "margin_in_position": D("0.01"), "margin_total": D("0.045"), "total": D("0.13") }, "ETH": { "exchange_free": D("1.0"), "exchange_used": D("0.0"), "exchange_total": D("1.0"), "total": D("1.0") }, "FOO": { "exchange_free": D("0.1"), "exchange_used": D("0.0"), "exchange_total": D("0.1"), "total": D("0.1"), }, } m.balances.fetch_balances(tag="tag") m.prepare_trades(available_balance_only=True) compute_trades.assert_called_once() call = compute_trades.call_args[0] values_in_base = call[0] new_repartition = call[1] self.assertEqual(portfolio.Amount("BTC", "-0.025"), new_repartition["DOGE"] - values_in_base["DOGE"]) self.assertEqual(0, new_repartition["ETH"] - values_in_base["ETH"]) self.assertIsNone(new_repartition.get("ZRC")) self.assertEqual(portfolio.Amount("BTC", "0.025"), new_repartition["XMR"]) self.assertEqual(portfolio.Amount("BTC", "0.015"), new_repartition["FOO"] - values_in_base["FOO"]) compute_trades.reset_mock() with self.subTest(available_balance_only=True, balance=0),\ mock.patch("market.ReportStore"): def _get_ticker(c1, c2): if c1 == "ETH" and c2 == "BTC": return { "average": D("0.1") } self.fail("Should not be called with {}, {}".format(c1, c2)) get_ticker.side_effect = _get_ticker repartition.return_value = { "BTC": (D("0.5"), "long"), "ETH": (D("0.5"), "long"), } m = market.Market(self.ccxt, self.market_args()) self.ccxt.fetch_all_balances.return_value = { "ETH": { "exchange_free": D("1.0"), "exchange_used": D("0.0"), "exchange_total": D("1.0"), "total": D("1.0") }, } m.balances.fetch_balances(tag="tag") m.prepare_trades(available_balance_only=True) compute_trades.assert_called_once() call = compute_trades.call_args[0] values_in_base = call[0] new_repartition = call[1] self.assertEqual(new_repartition["ETH"], values_in_base["ETH"]) @mock.patch.object(market.time, "sleep") @mock.patch.object(market.TradeStore, "all_orders") def test_follow_orders(self, all_orders, time_mock): for debug, sleep in [ (False, None), (True, None), (False, 12), (True, 12)]: with self.subTest(sleep=sleep, debug=debug), \ mock.patch("market.ReportStore"): m = market.Market(self.ccxt, self.market_args(debug=debug)) order_mock1 = mock.Mock() order_mock2 = mock.Mock() order_mock3 = mock.Mock() all_orders.side_effect = [ [order_mock1, order_mock2], [order_mock1, order_mock2], [order_mock1, order_mock3], [order_mock1, order_mock3], [order_mock1, order_mock3], [order_mock1, order_mock3], [] ] order_mock1.get_status.side_effect = ["open", "open", "closed"] order_mock2.get_status.side_effect = ["open"] order_mock3.get_status.side_effect = ["open", "closed"] order_mock1.trade = mock.Mock() order_mock2.trade = mock.Mock() order_mock3.trade = mock.Mock() m.follow_orders(sleep=sleep) order_mock1.trade.update_order.assert_any_call(order_mock1, 1) order_mock1.trade.update_order.assert_any_call(order_mock1, 2) self.assertEqual(2, order_mock1.trade.update_order.call_count) self.assertEqual(3, order_mock1.get_status.call_count) order_mock2.trade.update_order.assert_any_call(order_mock2, 1) self.assertEqual(1, order_mock2.trade.update_order.call_count) self.assertEqual(1, order_mock2.get_status.call_count) order_mock3.trade.update_order.assert_any_call(order_mock3, 2) self.assertEqual(1, order_mock3.trade.update_order.call_count) self.assertEqual(2, order_mock3.get_status.call_count) m.report.log_stage.assert_called() calls = [ mock.call("follow_orders_begin"), mock.call("follow_orders_tick_1"), mock.call("follow_orders_tick_2"), mock.call("follow_orders_tick_3"), mock.call("follow_orders_end"), ] m.report.log_stage.assert_has_calls(calls) m.report.log_orders.assert_called() self.assertEqual(3, m.report.log_orders.call_count) calls = [ mock.call([order_mock1, order_mock2], tick=1), mock.call([order_mock1, order_mock3], tick=2), mock.call([order_mock1, order_mock3], tick=3), ] m.report.log_orders.assert_has_calls(calls) calls = [ mock.call(order_mock1, 3, finished=True), mock.call(order_mock3, 3, finished=True), ] m.report.log_order.assert_has_calls(calls) if sleep is None: if debug: m.report.log_debug_action.assert_called_with("Set follow_orders tick to 7s") time_mock.assert_called_with(7) else: time_mock.assert_called_with(30) else: time_mock.assert_called_with(sleep) with self.subTest("disappearing order"), \ mock.patch("market.ReportStore"): all_orders.reset_mock() m = market.Market(self.ccxt, self.market_args()) order_mock1 = mock.Mock() order_mock2 = mock.Mock() all_orders.side_effect = [ [order_mock1, order_mock2], [order_mock1, order_mock2], [order_mock1, order_mock2], [order_mock1, order_mock2], [] ] order_mock1.get_status.side_effect = ["open", "closed"] order_mock2.get_status.side_effect = ["open", "error_disappeared"] order_mock1.trade = mock.Mock() trade_mock = mock.Mock() order_mock2.trade = trade_mock trade_mock.tick_actions_recreate.return_value = "tick1" new_order_mock = mock.Mock() trade_mock.prepare_order.return_value = new_order_mock m.follow_orders() trade_mock.tick_actions_recreate.assert_called_once_with(2) trade_mock.prepare_order.assert_called_once_with(compute_value="tick1") m.report.log_error.assert_called_once_with("follow_orders", message=mock.ANY) m.report.log_order.assert_called_with(order_mock2, 2, new_order=new_order_mock) new_order_mock.run.assert_called_once_with() with self.subTest("disappearing order no action to do"), \ mock.patch("market.ReportStore"): all_orders.reset_mock() m = market.Market(self.ccxt, self.market_args()) order_mock1 = mock.Mock() order_mock2 = mock.Mock() all_orders.side_effect = [ [order_mock1, order_mock2], [order_mock1, order_mock2], [order_mock1, order_mock2], [order_mock1, order_mock2], [] ] order_mock1.get_status.side_effect = ["open", "closed"] order_mock2.get_status.side_effect = ["open", "error_disappeared"] order_mock1.trade = mock.Mock() trade_mock = mock.Mock() order_mock2.trade = trade_mock trade_mock.tick_actions_recreate.return_value = "tick1" trade_mock.prepare_order.return_value = None m.follow_orders() trade_mock.tick_actions_recreate.assert_called_once_with(2) trade_mock.prepare_order.assert_called_once_with(compute_value="tick1") m.report.log_error.assert_called_once_with("follow_orders", message=mock.ANY) m.report.log_order.assert_called_with(order_mock2, 2, finished=True) @mock.patch.object(market.BalanceStore, "fetch_balances") def test_move_balance(self, fetch_balances): for debug in [True, False]: with self.subTest(debug=debug),\ mock.patch("market.ReportStore"): m = market.Market(self.ccxt, self.market_args(debug=debug)) value_from = portfolio.Amount("BTC", "1.0") value_from.linked_to = portfolio.Amount("ETH", "10.0") value_to = portfolio.Amount("BTC", "10.0") trade1 = portfolio.Trade(value_from, value_to, "ETH", m) value_from = portfolio.Amount("BTC", "0.0") value_from.linked_to = portfolio.Amount("ETH", "0.0") value_to = portfolio.Amount("BTC", "-3.0") trade2 = portfolio.Trade(value_from, value_to, "ETH", m) value_from = portfolio.Amount("USDT", "0.0") value_from.linked_to = portfolio.Amount("XVG", "0.0") value_to = portfolio.Amount("USDT", "-50.0") trade3 = portfolio.Trade(value_from, value_to, "XVG", m) m.trades.all = [trade1, trade2, trade3] balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" }) balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" }) m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} m.move_balances() fetch_balances.assert_called_with() m.report.log_move_balances.assert_called_once() if debug: m.report.log_debug_action.assert_called() self.assertEqual(3, m.report.log_debug_action.call_count) else: self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") m.report.reset_mock() fetch_balances.reset_mock() with self.subTest(retry=True): with mock.patch("market.ReportStore"): m = market.Market(self.ccxt, self.market_args()) value_from = portfolio.Amount("BTC", "0.0") value_from.linked_to = portfolio.Amount("ETH", "0.0") value_to = portfolio.Amount("BTC", "-3.0") trade = portfolio.Trade(value_from, value_to, "ETH", m) m.trades.all = [trade] balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) m.balances.all = {"BTC": balance} m.ccxt.transfer_balance.side_effect = [ market.ccxt.RequestTimeout, market.ccxt.InvalidNonce, True ] m.move_balances() self.ccxt.transfer_balance.assert_has_calls([ mock.call("BTC", 3, "exchange", "margin"), mock.call("BTC", 3, "exchange", "margin"), mock.call("BTC", 3, "exchange", "margin") ]) self.assertEqual(3, fetch_balances.call_count) m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) self.assertEqual(3, m.report.log_move_balances.call_count) self.ccxt.transfer_balance.reset_mock() m.report.reset_mock() fetch_balances.reset_mock() with self.subTest(retry=True, too_much=True): with mock.patch("market.ReportStore"): m = market.Market(self.ccxt, self.market_args()) value_from = portfolio.Amount("BTC", "0.0") value_from.linked_to = portfolio.Amount("ETH", "0.0") value_to = portfolio.Amount("BTC", "-3.0") trade = portfolio.Trade(value_from, value_to, "ETH", m) m.trades.all = [trade] balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) m.balances.all = {"BTC": balance} m.ccxt.transfer_balance.side_effect = [ market.ccxt.RequestTimeout, market.ccxt.RequestTimeout, market.ccxt.RequestTimeout, market.ccxt.RequestTimeout, market.ccxt.RequestTimeout, ] with self.assertRaises(market.ccxt.RequestTimeout): m.move_balances() self.ccxt.transfer_balance.reset_mock() m.report.reset_mock() fetch_balances.reset_mock() with self.subTest(retry=True, partial_result=True): with mock.patch("market.ReportStore"): m = market.Market(self.ccxt, self.market_args()) value_from = portfolio.Amount("BTC", "1.0") value_from.linked_to = portfolio.Amount("ETH", "10.0") value_to = portfolio.Amount("BTC", "10.0") trade1 = portfolio.Trade(value_from, value_to, "ETH", m) value_from = portfolio.Amount("BTC", "0.0") value_from.linked_to = portfolio.Amount("ETH", "0.0") value_to = portfolio.Amount("BTC", "-3.0") trade2 = portfolio.Trade(value_from, value_to, "ETH", m) value_from = portfolio.Amount("USDT", "0.0") value_from.linked_to = portfolio.Amount("XVG", "0.0") value_to = portfolio.Amount("USDT", "-50.0") trade3 = portfolio.Trade(value_from, value_to, "XVG", m) m.trades.all = [trade1, trade2, trade3] balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" }) balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" }) m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} call_counts = { "BTC": 0, "USDT": 0, "ETC": 0 } def _transfer_balance(currency, amount, from_, to_): call_counts[currency] += 1 if currency == "BTC": m.balances.all["BTC"] = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "3" }) if currency == "USDT": if call_counts["USDT"] == 1: raise market.ccxt.RequestTimeout else: m.balances.all["USDT"] = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "150" }) if currency == "ETC": m.balances.all["ETC"] = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "10" }) m.ccxt.transfer_balance.side_effect = _transfer_balance m.move_balances() self.ccxt.transfer_balance.assert_has_calls([ mock.call("BTC", 3, "exchange", "margin"), mock.call('USDT', 100, 'exchange', 'margin'), mock.call('USDT', 100, 'exchange', 'margin'), mock.call("ETC", 5, "margin", "exchange") ]) self.assertEqual(2, fetch_balances.call_count) m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) self.assertEqual(2, m.report.log_move_balances.call_count) m.report.log_move_balances.asser_has_calls([ mock.call( { 'BTC': portfolio.Amount("BTC", "3"), 'USDT': portfolio.Amount("USDT", "150"), 'ETC': portfolio.Amount("ETC", "10"), }, { 'BTC': portfolio.Amount("BTC", "3"), 'USDT': portfolio.Amount("USDT", "100"), }), mock.call( { 'BTC': portfolio.Amount("BTC", "3"), 'USDT': portfolio.Amount("USDT", "150"), 'ETC': portfolio.Amount("ETC", "10"), }, { 'BTC': portfolio.Amount("BTC", "0"), 'USDT': portfolio.Amount("USDT", "100"), 'ETC': portfolio.Amount("ETC", "-5"), }), ]) def test_store_file_report(self): file_open = mock.mock_open() m = market.Market(self.ccxt, self.market_args(report_path="present"), user_id=1) with self.subTest(file="present"),\ mock.patch("market.open", file_open),\ mock.patch.object(m, "report") as report,\ mock.patch.object(market, "datetime") as time_mock: report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]] report.to_json.return_value = "json_content" m.store_file_report(datetime.datetime(2018, 2, 25)) file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w") file_open().write.assert_any_call("json_content") file_open().write.assert_any_call("Foo\nBar") m.report.to_json.assert_called_once_with() m = market.Market(self.ccxt, self.market_args(report_path="error"), user_id=1) with self.subTest(file="error"),\ mock.patch("market.open") as file_open,\ mock.patch.object(m, "report") as report,\ mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: file_open.side_effect = FileNotFoundError m.store_file_report(datetime.datetime(2018, 2, 25)) self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") @mock.patch.object(dbs, "psql") def test_store_database_report(self, psql): cursor_mock = mock.MagicMock() psql.cursor.return_value = cursor_mock m = market.Market(self.ccxt, self.market_args(), pg_config={"config": "pg_config"}, user_id=1) cursor_mock.fetchone.return_value = [42] with self.subTest(error=False),\ mock.patch.object(m, "report") as report: report.to_json_array.return_value = [ ("date1", "type1", "payload1"), ("date2", "type2", "payload2"), ] m.store_database_report(datetime.datetime(2018, 3, 24)) psql.assert_has_calls([ mock.call.cursor(), mock.call.cursor().execute('INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;', (datetime.datetime(2018, 3, 24), None, False)), mock.call.cursor().fetchone(), mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')), mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')), mock.call.commit(), mock.call.cursor().close(), ]) with self.subTest(error=True),\ mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: psql.cursor.side_effect = Exception("Bouh") m.store_database_report(datetime.datetime(2018, 3, 24)) self.assertEqual(stdout_mock.getvalue(), "impossible to store report to database: Exception; Bouh\n") @mock.patch.object(dbs, "redis") def test_store_redis_report(self, redis): m = market.Market(self.ccxt, self.market_args(), redis_config={"config": "redis_config"}, market_id=1) with self.subTest(error=False),\ mock.patch.object(m, "report") as report: report.to_json_redis.return_value = [ ("type1", "payload1"), ("type2", "payload2"), ] m.store_redis_report(datetime.datetime(2018, 3, 24)) redis.assert_has_calls([ mock.call.set("/cryptoportfolio/1/2018-03-24T00:00:00/type1", "payload1", ex=31*24*60*60), mock.call.set("/cryptoportfolio/1/latest/type1", "payload1"), mock.call.set("/cryptoportfolio/1/2018-03-24T00:00:00/type2", "payload2", ex=31*24*60*60), mock.call.set("/cryptoportfolio/1/latest/type2", "payload2"), mock.call.set("/cryptoportfolio/1/latest/date", "2018-03-24T00:00:00"), ]) redis.reset_mock() with self.subTest(error=True),\ mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: redis.set.side_effect = Exception("Bouh") m.store_redis_report(datetime.datetime(2018, 3, 24)) self.assertEqual(stdout_mock.getvalue(), "impossible to store report to redis: Exception; Bouh\n") def test_store_report(self): m = market.Market(self.ccxt, self.market_args(report_db=False), user_id=1) with self.subTest(file=None, pg_connected=None),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_file_report") as file_report: psql.return_value = False redis.return_value = False m.store_report() report.merge.assert_called_with(store.Portfolio.report) file_report.assert_not_called() db_report.assert_not_called() redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_db=False, report_path="present"), user_id=1) with self.subTest(file="present", pg_connected=None),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = False redis.return_value = False time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() report.merge.assert_called_with(store.Portfolio.report) file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) db_report.assert_not_called() redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"), user_id=1) with self.subTest(file="present", pg_connected=None, report_db=True),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = False redis.return_value = False time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() report.merge.assert_called_with(store.Portfolio.report) file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) db_report.assert_not_called() redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_db=True), user_id=1) with self.subTest(file=None, pg_connected=True),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = True redis.return_value = False time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() report.merge.assert_called_with(store.Portfolio.report) file_report.assert_not_called() db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"), user_id=1) with self.subTest(file="present", pg_connected=True),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = True redis.return_value = False time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() report.merge.assert_called_with(store.Portfolio.report) file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_redis=False), user_id=1) with self.subTest(redis_connected=True, report_redis=False),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = False redis.return_value = True time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_redis=True), user_id=1) with self.subTest(redis_connected=False, report_redis=True),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = False redis.return_value = False time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() redis_report.assert_not_called() report.reset_mock() m = market.Market(self.ccxt, self.market_args(report_redis=True), user_id=1) with self.subTest(redis_connected=True, report_redis=True),\ mock.patch.object(dbs, "psql_connected") as psql,\ mock.patch.object(dbs, "redis_connected") as redis,\ mock.patch.object(m, "report") as report,\ mock.patch.object(m, "store_file_report") as file_report,\ mock.patch.object(m, "store_redis_report") as redis_report,\ mock.patch.object(m, "store_database_report") as db_report,\ mock.patch.object(market.datetime, "datetime") as time_mock: psql.return_value = False redis.return_value = True time_mock.now.return_value = datetime.datetime(2018, 2, 25) m.store_report() redis_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) def test_print_tickers(self): m = market.Market(self.ccxt, self.market_args()) with mock.patch.object(m.balances, "in_currency") as in_currency,\ mock.patch.object(m.report, "log_stage") as log_stage,\ mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ mock.patch.object(m.report, "print_log") as print_log: in_currency.return_value = { "BTC": portfolio.Amount("BTC", "0.65"), "ETH": portfolio.Amount("BTC", "0.3"), } m.print_tickers() print_log.assert_has_calls([ mock.call("total:"), mock.call(portfolio.Amount("BTC", "0.95")), ]) @mock.patch("market.Processor.process") @mock.patch("market.ReportStore.log_error") @mock.patch("market.Market.store_report") def test_process(self, store_report, log_error, process): m = market.Market(self.ccxt, self.market_args()) with self.subTest(actions=[], before=False, after=False): m.process([]) process.assert_not_called() store_report.assert_called_once() log_error.assert_not_called() process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(before=True, after=False): m.process(["foo"], before=True) process.assert_called_once_with("foo", steps="before") store_report.assert_called_once() log_error.assert_not_called() process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(before=False, after=True): m.process(["sell_all"], after=True) process.assert_called_once_with("sell_all", steps="after") store_report.assert_called_once() log_error.assert_not_called() process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(before=False, after=False): m.process(["foo"]) process.assert_called_once_with("foo", steps="all") store_report.assert_called_once() log_error.assert_not_called() process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(before=True, after=True): m.process(["sell_all"], before=True, after=True) process.assert_called_once_with("sell_all", steps="all") store_report.assert_called_once() log_error.assert_not_called() process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(authentication_error=True): m.ccxt.check_required_credentials.side_effect = market.ccxt.AuthenticationError m.process(["some_action"], before=True) log_error.assert_called_with("market_authentication", message="Impossible to authenticate to market") store_report.assert_called_once() m.ccxt.check_required_credentials.side_effect = True process.reset_mock() log_error.reset_mock() store_report.reset_mock() with self.subTest(unhandled_exception=True): process.side_effect = Exception("bouh") m.process(["some_action"], before=True) log_error.assert_called_with("market_process", exception=mock.ANY, message=mock.ANY) store_report.assert_called_once() @unittest.skipUnless("unit" in limits, "Unit skipped") class ProcessorTest(WebMockTestCase): def test_values(self): processor = market.Processor(self.m) self.assertEqual(self.m, processor.market) def test_run_action(self): processor = market.Processor(self.m) with mock.patch.object(processor, "parse_args") as parse_args: method_mock = mock.Mock() parse_args.return_value = [method_mock, { "foo": "bar" }] processor.run_action("foo", "bar", "baz") parse_args.assert_called_with("foo", "bar", "baz") method_mock.assert_called_with(foo="bar") processor.run_action("wait_for_recent", "bar", "baz") method_mock.assert_called_with(foo="bar") def test_select_step(self): processor = market.Processor(self.m) scenario = processor.scenarios["sell_all"] self.assertEqual(scenario, processor.select_steps(scenario, "all")) self.assertEqual(["all_sell"], list(map(lambda x: x["name"], processor.select_steps(scenario, "before")))) self.assertEqual(["wait", "all_buy"], list(map(lambda x: x["name"], processor.select_steps(scenario, "after")))) self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, 2)))) self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, "wait")))) with self.assertRaises(TypeError): processor.select_steps(scenario, ["wait"]) def test_can_process(self): processor = market.Processor(self.m) with self.subTest(True): self.assertTrue(processor.can_process("sell_all")) with self.subTest(False): self.assertFalse(processor.can_process("unknown_action")) @mock.patch("market.Processor.process_step") def test_process(self, process_step): with self.subTest("unknown action"): processor = market.Processor(self.m) with self.assertRaises(TypeError): processor.process("unknown_action") with self.subTest("nominal case"): processor = market.Processor(self.m) processor.process("sell_all", foo="bar") self.assertEqual(3, process_step.call_count) steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls)) scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls)) kwargs = list(map(lambda x: x[1][2], process_step.mock_calls)) self.assertEqual(["all_sell", "wait", "all_buy"], steps) self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names) self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs) process_step.reset_mock() processor.process("sell_needed", steps=["before", "after"]) self.assertEqual(4, process_step.call_count) def test_method_arguments(self): ccxt = mock.Mock(spec=market.ccxt.poloniexE) m = market.Market(ccxt, self.market_args()) processor = market.Processor(m) method, arguments = processor.method_arguments("wait_for_recent") self.assertEqual(market.Portfolio.wait_for_recent, method) self.assertEqual(["delta", "poll"], arguments) method, arguments = processor.method_arguments("prepare_trades") self.assertEqual(m.prepare_trades, method) self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only', 'available_balance_only'], arguments) method, arguments = processor.method_arguments("prepare_orders") self.assertEqual(m.trades.prepare_orders, method) method, arguments = processor.method_arguments("move_balances") self.assertEqual(m.move_balances, method) method, arguments = processor.method_arguments("run_orders") self.assertEqual(m.trades.run_orders, method) method, arguments = processor.method_arguments("follow_orders") self.assertEqual(m.follow_orders, method) method, arguments = processor.method_arguments("close_trades") self.assertEqual(m.trades.close_trades, method) method, arguments = processor.method_arguments("print_tickers") self.assertEqual(m.print_tickers, method) def test_process_step(self): processor = market.Processor(self.m) with mock.patch.object(processor, "run_action") as run_action: step = processor.scenarios["sell_needed"][2] processor.process_step("foo", step, {"foo":"bar"}) self.m.report.log_stage.assert_has_calls([ mock.call("process_foo__2_sell_begin"), mock.call("process_foo__2_sell_end"), ]) self.m.balances.fetch_balances.assert_has_calls([ mock.call(tag="process_foo__2_sell_begin"), mock.call(tag="process_foo__2_sell_end"), ]) self.assertEqual(5, run_action.call_count) run_action.assert_has_calls([ mock.call('prepare_trades', {}, {'foo': 'bar'}), mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}), mock.call('run_orders', {}, {'foo': 'bar'}), mock.call('follow_orders', {}, {'foo': 'bar'}), mock.call('close_trades', {}, {'foo': 'bar'}), ]) self.m.reset_mock() with mock.patch.object(processor, "run_action") as run_action: step = processor.scenarios["sell_needed"][0] processor.process_step("foo", step, {"foo":"bar"}) self.m.report.log_stage.assert_has_calls([ mock.call("process_foo__0_print_balances_begin"), mock.call("process_foo__0_print_balances_end"), ]) self.m.balances.fetch_balances.assert_has_calls([ mock.call(add_portfolio=True, checkpoint='end', log_tickers=True, add_usdt=True, tag='process_foo__0_print_balances_begin') ]) self.assertEqual(0, run_action.call_count) self.m.reset_mock() with mock.patch.object(processor, "run_action") as run_action: step = processor.scenarios["sell_needed"][1] processor.process_step("foo", step, {"foo":"bar"}) self.m.balances.fetch_balances.assert_not_called() self.m.reset_mock() with mock.patch.object(processor, "run_action") as run_action: step = processor.scenarios["print_balances"][0] processor.process_step("foo", step, {"foo":"bar"}) self.m.balances.fetch_balances.assert_called_once_with( add_portfolio=True, add_usdt=True, log_tickers=True, tag='process_foo__1_print_balances_begin') def test_parse_args(self): processor = market.Processor(self.m) with mock.patch.object(processor, "method_arguments") as method_arguments: method_mock = mock.Mock() method_arguments.return_value = [ method_mock, ["foo2", "foo", "foo3"] ] self.m.options = { "foo3": "coucou"} method, args = processor.parse_args("action", {"foo": "bar", "foo2": "bar"}, {"foo": "bar2", "bla": "bla"}) self.assertEqual(method_mock, method) self.assertEqual({"foo": "bar2", "foo2": "bar", "foo3": "coucou"}, args) with mock.patch.object(processor, "method_arguments") as method_arguments: method_mock = mock.Mock() method_arguments.return_value = [ method_mock, ["repartition"] ] method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {}) self.assertEqual(1, len(args["repartition"])) self.assertIn("BTC", args["repartition"]) with mock.patch.object(processor, "method_arguments") as method_arguments: method_mock = mock.Mock() method_arguments.return_value = [ method_mock, ["repartition", "base_currency"] ] method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {"base_currency": "USDT"}) self.assertEqual(1, len(args["repartition"])) self.assertIn("USDT", args["repartition"]) with mock.patch.object(processor, "method_arguments") as method_arguments: method_mock = mock.Mock() method_arguments.return_value = [ method_mock, ["repartition", "base_currency"] ] method, args = processor.parse_args("action", {"repartition": { "ETH": 1 }}, {"base_currency": "USDT"}) self.assertEqual(1, len(args["repartition"])) self.assertIn("ETH", args["repartition"])