+
+ 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(market, "psycopg2")
+ def test_store_database_report(self, psycopg2):
+ connect_mock = mock.Mock()
+ cursor_mock = mock.MagicMock()
+
+ connect_mock.cursor.return_value = cursor_mock
+ psycopg2.connect.return_value = connect_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))
+ connect_mock.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(),
+ mock.call.close()
+ ])
+
+ connect_mock.reset_mock()
+ with self.subTest(error=True),\
+ mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+ psycopg2.connect.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")
+
+ def test_store_report(self):
+ m = market.Market(self.ccxt, self.market_args(), user_id=1)
+ with self.subTest(file=None, pg_config=None),\
+ mock.patch.object(m, "report") as report,\
+ mock.patch.object(m, "store_database_report") as db_report,\
+ mock.patch.object(m, "store_file_report") as file_report:
+ m.store_report()
+ report.merge.assert_called_with(store.Portfolio.report)
+
+ file_report.assert_not_called()
+ db_report.assert_not_called()
+
+ report.reset_mock()
+ m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1)
+ with self.subTest(file="present", pg_config=None),\
+ mock.patch.object(m, "report") as report,\
+ mock.patch.object(m, "store_file_report") as file_report,\
+ mock.patch.object(m, "store_database_report") as db_report,\
+ mock.patch.object(market, "datetime") as time_mock:
+
+ 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()
+
+ report.reset_mock()
+ m = market.Market(self.ccxt, self.market_args(), pg_config="present", user_id=1)
+ with self.subTest(file=None, pg_config="present"),\
+ mock.patch.object(m, "report") as report,\
+ mock.patch.object(m, "store_file_report") as file_report,\
+ mock.patch.object(m, "store_database_report") as db_report,\
+ mock.patch.object(market, "datetime") as time_mock:
+
+ 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))
+
+ report.reset_mock()
+ m = market.Market(self.ccxt, self.market_args(),
+ pg_config="pg_config", report_path="present", user_id=1)
+ with self.subTest(file="present", pg_config="present"),\
+ mock.patch.object(m, "report") as report,\
+ mock.patch.object(m, "store_file_report") as file_report,\
+ mock.patch.object(m, "store_database_report") as db_report,\
+ mock.patch.object(market, "datetime") as time_mock:
+
+ 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))
+
+ def test_print_orders(self):
+ m = market.Market(self.ccxt, self.market_args())
+ with mock.patch.object(m.report, "log_stage") as log_stage,\
+ mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
+ mock.patch.object(m, "prepare_trades") as prepare_trades,\
+ mock.patch.object(m.trades, "prepare_orders") as prepare_orders:
+ m.print_orders()
+
+ log_stage.assert_called_with("print_orders")
+ fetch_balances.assert_called_with(tag="print_orders")
+ prepare_trades.assert_called_with(base_currency="BTC",
+ compute_value="average")
+ prepare_orders.assert_called_with(compute_value="average")
+
+ def test_print_balances(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_balances()
+
+ log_stage.assert_called_once_with("print_balances")
+ fetch_balances.assert_called_with()
+ 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(before=False, after=False):
+ m.process(None)
+
+ 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(None, before=True)
+
+ process.assert_called_once_with("sell_all", 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(None, 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=True, after=True):
+ m.process(None, before=True, after=True)
+
+ process.assert_has_calls([
+ mock.call("sell_all", steps="before"),
+ mock.call("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(action="print_balances"),\
+ mock.patch.object(m, "print_balances") as print_balances:
+ m.process(["print_balances"])
+
+ process.assert_not_called()
+ log_error.assert_not_called()
+ store_report.assert_called_once()
+ print_balances.assert_called_once_with()
+
+ log_error.reset_mock()
+ store_report.reset_mock()
+ with self.subTest(action="print_orders"),\
+ mock.patch.object(m, "print_orders") as print_orders,\
+ mock.patch.object(m, "print_balances") as print_balances:
+ m.process(["print_orders", "print_balances"])
+
+ process.assert_not_called()
+ log_error.assert_not_called()
+ store_report.assert_called_once()
+ print_orders.assert_called_once_with()
+ print_balances.assert_called_once_with()
+
+ log_error.reset_mock()
+ store_report.reset_mock()
+ with self.subTest(action="unknown"):
+ m.process(["unknown"])
+ log_error.assert_called_once_with("market_process", message="Unknown action unknown")
+ store_report.assert_called_once()
+
+ log_error.reset_mock()
+ store_report.reset_mock()
+ with self.subTest(unhandled_exception=True):
+ process.side_effect = Exception("bouh")
+
+ m.process(None, before=True)
+ log_error.assert_called_with("market_process", exception=mock.ANY)
+ store_report.assert_called_once()
+