class WebMockTestCase(unittest.TestCase):
import time
+ def market_args(self, debug=False, quiet=False):
+ return type('Args', (object,), { "debug": debug, "quiet": quiet })()
+
def setUp(self):
- super(WebMockTestCase, self).setUp()
+ super().setUp()
self.wm = requests_mock.Mocker()
self.wm.start()
for patcher in self.patchers:
patcher.stop()
self.wm.stop()
- super(WebMockTestCase, self).tearDown()
+ super().tearDown()
@unittest.skipUnless("unit" in limits, "Unit skipped")
class poloniexETest(unittest.TestCase):
def setUp(self):
- super(poloniexETest, self).setUp()
+ super().setUp()
self.wm = requests_mock.Mocker()
self.wm.start()
def tearDown(self):
self.wm.stop()
- super(poloniexETest, self).tearDown()
+ super().tearDown()
+
+ def test__init(self):
+ with self.subTest("Nominal case"), \
+ mock.patch("market.ccxt.poloniexE.session") as session:
+ session.request.return_value = "response"
+ ccxt = market.ccxt.poloniexE()
+ ccxt._market = mock.Mock
+ ccxt._market.report = mock.Mock()
+
+ ccxt.session.request("GET", "URL", data="data",
+ headers="headers")
+ ccxt._market.report.log_http_request.assert_called_with('GET', 'URL', 'data',
+ 'headers', 'response')
+
+ with self.subTest("Raising"),\
+ mock.patch("market.ccxt.poloniexE.session") as session:
+ session.request.side_effect = market.ccxt.RequestException("Boo")
+
+ ccxt = market.ccxt.poloniexE()
+ ccxt._market = mock.Mock
+ ccxt._market.report = mock.Mock()
+
+ with self.assertRaises(market.ccxt.RequestException, msg="Boo") as cm:
+ ccxt.session.request("GET", "URL", data="data",
+ headers="headers")
+ ccxt._market.report.log_http_request.assert_called_with('GET', 'URL', 'data',
+ 'headers', cm.exception)
+
def test_nanoseconds(self):
with mock.patch.object(market.ccxt.time, "time") as time:
time.return_value = 123456.7890123456
self.assertEqual(123456789012345, self.s.nonce())
+ def test_request(self):
+ with mock.patch.object(market.ccxt.poloniex, "request") as request,\
+ mock.patch("market.ccxt.retry_call") as retry_call:
+ with self.subTest(wrapped=True):
+ with self.subTest(desc="public"):
+ self.s.request("foo")
+ retry_call.assert_called_with(request,
+ delay=1, tries=10, fargs=["foo"],
+ fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
+ exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+ request.assert_not_called()
+
+ with self.subTest(desc="private GET"):
+ self.s.request("foo", api="private")
+ retry_call.assert_called_with(request,
+ delay=1, tries=10, fargs=["foo"],
+ fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
+ exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+ request.assert_not_called()
+
+ with self.subTest(desc="private POST regexp"):
+ self.s.request("returnFoo", api="private", method="POST")
+ retry_call.assert_called_with(request,
+ delay=1, tries=10, fargs=["returnFoo"],
+ fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
+ exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+ request.assert_not_called()
+
+ with self.subTest(desc="private POST non-regexp"):
+ self.s.request("getMarginPosition", api="private", method="POST")
+ retry_call.assert_called_with(request,
+ delay=1, tries=10, fargs=["getMarginPosition"],
+ fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
+ exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+ request.assert_not_called()
+ retry_call.reset_mock()
+ request.reset_mock()
+ with self.subTest(wrapped=False):
+ with self.subTest(desc="private POST non-matching regexp"):
+ self.s.request("marginBuy", api="private", method="POST")
+ request.assert_called_with("marginBuy",
+ api="private", method="POST", params={},
+ headers=None, body=None)
+ retry_call.assert_not_called()
+
+ with self.subTest(desc="private POST non-matching non-regexp"):
+ self.s.request("closeMarginPositionOther", api="private", method="POST")
+ request.assert_called_with("closeMarginPositionOther",
+ api="private", method="POST", params={},
+ headers=None, body=None)
+ retry_call.assert_not_called()
+
def test_order_precision(self):
self.assertEqual(8, self.s.order_precision("FOO"))
@unittest.skipUnless("unit" in limits, "Unit skipped")
class PortfolioTest(WebMockTestCase):
def setUp(self):
- super(PortfolioTest, self).setUp()
+ super().setUp()
with open("test_samples/test_portfolio.json") as example:
self.json_response = example.read()
@unittest.skipUnless("unit" in limits, "Unit skipped")
class MarketTest(WebMockTestCase):
def setUp(self):
- super(MarketTest, self).setUp()
+ super().setUp()
self.ccxt = mock.Mock(spec=market.ccxt.poloniexE)
def test_values(self):
- m = market.Market(self.ccxt)
+ m = market.Market(self.ccxt, self.market_args())
self.assertEqual(self.ccxt, m.ccxt)
self.assertFalse(m.debug)
self.assertEqual(m, m.balances.market)
self.assertEqual(m, m.ccxt._market)
- m = market.Market(self.ccxt, debug=True)
+ m = market.Market(self.ccxt, self.market_args(debug=True))
self.assertTrue(m.debug)
- m = market.Market(self.ccxt, debug=False)
+ 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
- self.ccxt.session.request.return_value = "response"
- m = market.Market.from_config({"key": "key", "secred": "secret"})
+ m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args())
self.assertEqual(self.ccxt, m.ccxt)
- self.ccxt.session.request("GET", "URL", data="data",
- headers="headers")
- m.report.log_http_request.assert_called_with('GET', 'URL', 'data',
- 'headers', 'response')
-
- m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True)
+ m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True))
self.assertEqual(True, m.debug)
def test_get_tickers(self):
market.NotSupported
]
- m = market.Market(self.ccxt)
+ 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()
"ETH/ETC": { "bid": 1, "ask": 3 },
"XVG/ETH": { "bid": 10, "ask": 40 },
}
- m = market.Market(self.ccxt)
+ m = market.Market(self.ccxt, self.market_args())
ticker = m.get_ticker("ETH", "ETC")
self.assertEqual(1, ticker["bid"])
market.ExchangeError("foo"),
]
- m = market.Market(self.ccxt)
+ m = market.Market(self.ccxt, self.market_args())
ticker = m.get_ticker("ETH", "ETC")
self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
self.assertIsNone(ticker)
def test_fetch_fees(self):
- m = market.Market(self.ccxt)
+ 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()
get_ticker.side_effect = _get_ticker
with mock.patch("market.ReportStore"):
- m = market.Market(self.ccxt)
+ m = market.Market(self.ccxt, self.market_args())
self.ccxt.fetch_all_balances.return_value = {
"USDT": {
"exchange_free": D("10000.0"),
(False, 12), (True, 12)]:
with self.subTest(sleep=sleep, debug=debug), \
mock.patch("market.ReportStore"):
- m = market.Market(self.ccxt, debug=debug)
+ m = market.Market(self.ccxt, self.market_args(debug=debug))
order_mock1 = mock.Mock()
order_mock2 = mock.Mock()
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"
+
+ 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)
+
@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, debug=debug)
+ 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")
self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
- def test_store_report(self):
+ 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())
- file_open = mock.mock_open()
- m = market.Market(self.ccxt, user_id=1)
- with self.subTest(file=None),\
- mock.patch.object(m, "report") as report,\
- mock.patch("market.open", file_open):
- m.store_report()
- report.merge.assert_called_with(store.Portfolio.report)
- file_open.assert_not_called()
+ 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)
- report.reset_mock()
+ 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, report_path="present", user_id=1)
+ 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:
- time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+ report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]]
report.to_json.return_value = "json_content"
- m.store_report()
+ 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().write.assert_called_once_with("json_content")
+ 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()
- report.merge.assert_called_with(store.Portfolio.report)
- report.reset_mock()
-
- m = market.Market(self.ccxt, report_path="error", user_id=1)
+ 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)
- self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
+ 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)
+ 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,\
prepare_orders.assert_called_with(compute_value="average")
def test_print_balances(self):
- m = market.Market(self.ccxt)
+ 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("market.ReportStore.log_error")
@mock.patch("market.Market.store_report")
def test_process(self, store_report, log_error, process):
- m = market.Market(self.ccxt)
+ m = market.Market(self.ccxt, self.market_args())
with self.subTest(before=False, after=False):
m.process(None)
@unittest.skipUnless("unit" in limits, "Unit skipped")
class BalanceStoreTest(WebMockTestCase):
def setUp(self):
- super(BalanceStoreTest, self).setUp()
+ super().setUp()
self.fetch_balance = {
"ETC": {
value_to = portfolio.Amount("BTC", "1.0")
trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
- self.assertEqual("buy", trade.order_action(False))
- self.assertEqual("sell", trade.order_action(True))
+ trade.inverted = False
+ self.assertEqual("buy", trade.order_action())
+ trade.inverted = True
+ self.assertEqual("sell", trade.order_action())
value_from = portfolio.Amount("BTC", "0")
value_from.linked_to = portfolio.Amount("ETH", "0")
value_to = portfolio.Amount("BTC", "-1.0")
trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
- self.assertEqual("sell", trade.order_action(False))
- self.assertEqual("buy", trade.order_action(True))
+ trade.inverted = False
+ self.assertEqual("sell", trade.order_action())
+ trade.inverted = True
+ self.assertEqual("buy", trade.order_action())
def test_trade_type(self):
value_from = portfolio.Amount("BTC", "0.5")
self.assertEqual("short", trade.trade_type)
def test_is_fullfiled(self):
- value_from = portfolio.Amount("BTC", "0.5")
- value_from.linked_to = portfolio.Amount("ETH", "10.0")
- value_to = portfolio.Amount("BTC", "1.0")
- trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
+ with self.subTest(inverted=False):
+ value_from = portfolio.Amount("BTC", "0.5")
+ value_from.linked_to = portfolio.Amount("ETH", "10.0")
+ value_to = portfolio.Amount("BTC", "1.0")
+ trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
- order1 = mock.Mock()
- order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
+ order1 = mock.Mock()
+ order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
- order2 = mock.Mock()
- order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
- trade.orders.append(order1)
- trade.orders.append(order2)
+ order2 = mock.Mock()
+ order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
+ trade.orders.append(order1)
+ trade.orders.append(order2)
+
+ self.assertFalse(trade.is_fullfiled)
+
+ order3 = mock.Mock()
+ order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
+ trade.orders.append(order3)
+
+ self.assertTrue(trade.is_fullfiled)
+
+ order1.filled_amount.assert_called_with(in_base_currency=True)
+ order2.filled_amount.assert_called_with(in_base_currency=True)
+ order3.filled_amount.assert_called_with(in_base_currency=True)
+
+ with self.subTest(inverted=True):
+ value_from = portfolio.Amount("BTC", "0.5")
+ value_from.linked_to = portfolio.Amount("USDT", "1000.0")
+ value_to = portfolio.Amount("BTC", "1.0")
+ trade = portfolio.Trade(value_from, value_to, "USDT", self.m)
+ trade.inverted = True
+
+ order1 = mock.Mock()
+ order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
+
+ order2 = mock.Mock()
+ order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
+ trade.orders.append(order1)
+ trade.orders.append(order2)
- self.assertFalse(trade.is_fullfiled)
+ self.assertFalse(trade.is_fullfiled)
- order3 = mock.Mock()
- order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
- trade.orders.append(order3)
+ order3 = mock.Mock()
+ order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
+ trade.orders.append(order3)
+
+ self.assertTrue(trade.is_fullfiled)
+
+ order1.filled_amount.assert_called_with(in_base_currency=False)
+ order2.filled_amount.assert_called_with(in_base_currency=False)
+ order3.filled_amount.assert_called_with(in_base_currency=False)
- self.assertTrue(trade.is_fullfiled)
def test_filled_amount(self):
value_from = portfolio.Amount("BTC", "0.5")
D("125"), "FOO", "long", self.m,
trade, close_if_possible=False)
+ def test_tick_actions_recreate(self):
+ value_from = portfolio.Amount("BTC", "0.5")
+ value_from.linked_to = portfolio.Amount("ETH", "10.0")
+ value_to = portfolio.Amount("BTC", "1.0")
+ trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
+
+ self.assertEqual("average", trade.tick_actions_recreate(0))
+ self.assertEqual("foo", trade.tick_actions_recreate(0, default="foo"))
+ self.assertEqual("average", trade.tick_actions_recreate(1))
+ self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(2))
+ self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(3))
+ self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(5))
+ self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(6))
+ self.assertEqual("default", trade.tick_actions_recreate(7))
+ self.assertEqual("default", trade.tick_actions_recreate(8))
@mock.patch.object(portfolio.Trade, "prepare_order")
def test_update_order(self, prepare_order):
self.m.ccxt.privatePostReturnOrderTrades.return_value = [
{
"tradeID": 42, "type": "buy", "fee": "0.0015",
- "date": "2017-12-30 12:00:12", "rate": "0.1",
+ "date": "2017-12-30 13:00:12", "rate": "0.1",
"amount": "3", "total": "0.3"
},
{
"tradeID": 43, "type": "buy", "fee": "0.0015",
- "date": "2017-12-30 13:00:12", "rate": "0.2",
+ "date": "2017-12-30 12:00:12", "rate": "0.2",
"amount": "2", "total": "0.4"
}
]
self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12})
self.assertEqual(2, len(order.mouvements))
- self.assertEqual(42, order.mouvements[0].id)
- self.assertEqual(43, order.mouvements[1].id)
+ self.assertEqual(43, order.mouvements[0].id)
+ self.assertEqual(42, order.mouvements[1].id)
self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError
order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
self.m.report.log_debug_action.assert_called_once()
@mock.patch.object(portfolio.Order, "fetch_mouvements")
- def test_fetch(self, fetch_mouvements):
+ @mock.patch.object(portfolio.Order, "mark_disappeared_order")
+ @mock.patch.object(portfolio.Order, "mark_finished_order")
+ def test_fetch(self, mark_finished_order, mark_disappeared_order, fetch_mouvements):
order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
D("0.1"), "BTC", "long", self.m, "trade")
order.id = 45
self.m.report.log_debug_action.assert_called_once()
self.m.report.log_debug_action.reset_mock()
self.m.ccxt.fetch_order.assert_not_called()
+ mark_finished_order.assert_not_called()
+ mark_disappeared_order.assert_not_called()
fetch_mouvements.assert_not_called()
with self.subTest(debug=False):
self.assertEqual("timestamp", order.timestamp)
self.assertEqual(1, len(order.results))
self.m.report.log_debug_action.assert_not_called()
+ mark_finished_order.assert_called_once()
+ mark_disappeared_order.assert_called_once()
+ mark_finished_order.reset_mock()
with self.subTest(missing_order=True):
self.m.ccxt.fetch_order.side_effect = [
portfolio.OrderNotCached,
]
order.fetch()
self.assertEqual("closed_unknown", order.status)
+ mark_finished_order.assert_called_once()
+
+ def test_mark_disappeared_order(self):
+ with self.subTest("Open order"):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.id = 45
+ order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+ "tradeID":21336541,
+ "currencyPair":"BTC_XRP",
+ "type":"sell",
+ "rate":"0.00007013",
+ "amount":"0.00000222",
+ "total":"0.00000000",
+ "fee":"0.00150000",
+ "date":"2018-04-02 00:09:13"
+ }))
+ order.mark_disappeared_order()
+ self.assertEqual("pending", order.status)
+
+ with self.subTest("Non-zero amount"):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.id = 45
+ order.status = "closed"
+ order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+ "tradeID":21336541,
+ "currencyPair":"BTC_XRP",
+ "type":"sell",
+ "rate":"0.00007013",
+ "amount":"0.00000222",
+ "total":"0.00000010",
+ "fee":"0.00150000",
+ "date":"2018-04-02 00:09:13"
+ }))
+ order.mark_disappeared_order()
+ self.assertEqual("closed", order.status)
+
+ with self.subTest("Other mouvements"):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.id = 45
+ order.status = "closed"
+ order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+ "tradeID":21336541,
+ "currencyPair":"BTC_XRP",
+ "type":"sell",
+ "rate":"0.00007013",
+ "amount":"0.00000222",
+ "total":"0.00000001",
+ "fee":"0.00150000",
+ "date":"2018-04-02 00:09:13"
+ }))
+ order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+ "tradeID":21336541,
+ "currencyPair":"BTC_XRP",
+ "type":"sell",
+ "rate":"0.00007013",
+ "amount":"0.00000222",
+ "total":"0.00000000",
+ "fee":"0.00150000",
+ "date":"2018-04-02 00:09:13"
+ }))
+ order.mark_disappeared_order()
+ self.assertEqual("error_disappeared", order.status)
+
+ with self.subTest("Order disappeared"):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.id = 45
+ order.status = "closed"
+ order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+ "tradeID":21336541,
+ "currencyPair":"BTC_XRP",
+ "type":"sell",
+ "rate":"0.00007013",
+ "amount":"0.00000222",
+ "total":"0.00000000",
+ "fee":"0.00150000",
+ "date":"2018-04-02 00:09:13"
+ }))
+ order.mark_disappeared_order()
+ self.assertEqual("error_disappeared", order.status)
@mock.patch.object(portfolio.Order, "fetch")
- @mock.patch.object(portfolio.Order, "mark_finished_order")
- def test_get_status(self, mark_finished_order, fetch):
+ def test_get_status(self, fetch):
with self.subTest(debug=True):
self.m.debug = True
order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
return update_status
fetch.side_effect = _fetch(order)
self.assertEqual("open", order.get_status())
- mark_finished_order.assert_not_called()
fetch.assert_called_once()
- mark_finished_order.reset_mock()
fetch.reset_mock()
with self.subTest(debug=False, finished=True):
self.m.debug = False
return update_status
fetch.side_effect = _fetch(order)
self.assertEqual("closed", order.get_status())
- mark_finished_order.assert_called_once()
fetch.assert_called_once()
def test_run(self):
self.assertEqual(5, self.m.report.log_error.call_count)
self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY)
+ self.m.reset_mock()
+ with self.subTest(invalid_nonce=True):
+ with self.subTest(retry_success=True):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.InvalidNonce,
+ portfolio.InvalidNonce,
+ { "id": 123 },
+ ]
+ order.run()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(3, self.m.ccxt.create_order.call_count)
+ self.assertEqual(3, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(2, self.m.report.log_error.call_count)
+ self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY)
+ self.assertEqual(123, order.id)
+
+ self.m.reset_mock()
+ with self.subTest(retry_success=False):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.InvalidNonce,
+ portfolio.InvalidNonce,
+ portfolio.InvalidNonce,
+ portfolio.InvalidNonce,
+ portfolio.InvalidNonce,
+ ]
+ order.run()
+ self.assertEqual(5, self.m.ccxt.create_order.call_count)
+ self.assertEqual(5, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(5, self.m.report.log_error.call_count)
+ self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after invalid nonce", exception=mock.ANY)
+ self.assertEqual("error", order.status)
+
+ self.m.reset_mock()
+ with self.subTest(request_timeout=True):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ with self.subTest(retrieved=False), \
+ mock.patch.object(order, "retrieve_order") as retrieve:
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.RequestTimeout,
+ portfolio.RequestTimeout,
+ { "id": 123 },
+ ]
+ retrieve.return_value = False
+ order.run()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(3, self.m.ccxt.create_order.call_count)
+ self.assertEqual(3, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(2, self.m.report.log_error.call_count)
+ self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY)
+ self.assertEqual(123, order.id)
+
+ self.m.reset_mock()
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ with self.subTest(retrieved=True), \
+ mock.patch.object(order, "retrieve_order") as retrieve:
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.RequestTimeout,
+ ]
+ def _retrieve():
+ order.results.append({"id": 123})
+ return True
+ retrieve.side_effect = _retrieve
+ order.run()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(1, self.m.ccxt.create_order.call_count)
+ self.assertEqual(1, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(1, self.m.report.log_error.call_count)
+ self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order")
+ self.assertEqual(123, order.id)
+
+ self.m.reset_mock()
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ with self.subTest(retrieved=False), \
+ mock.patch.object(order, "retrieve_order") as retrieve:
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.RequestTimeout,
+ portfolio.RequestTimeout,
+ portfolio.RequestTimeout,
+ portfolio.RequestTimeout,
+ portfolio.RequestTimeout,
+ ]
+ retrieve.return_value = False
+ order.run()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(5, self.m.ccxt.create_order.call_count)
+ self.assertEqual(5, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(5, self.m.report.log_error.call_count)
+ self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after timeouts", exception=mock.ANY)
+ self.assertEqual("error", order.status)
+
+ def test_retrieve_order(self):
+ with self.subTest(similar_open_order=True):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+ self.m.ccxt.order_precision.return_value = 8
+ self.m.ccxt.fetch_orders.return_value = [
+ { # Wrong amount
+ 'amount': 0.002, 'cost': 0.1,
+ 'datetime': '2018-03-25T15:15:51.000Z',
+ 'fee': None, 'filled': 0.0,
+ 'id': '1',
+ 'info': {
+ 'amount': '0.002',
+ 'date': '2018-03-25 15:15:51',
+ 'margin': 0, 'orderNumber': '1',
+ 'price': '0.1', 'rate': '0.1',
+ 'side': 'buy', 'startingAmount': '0.002',
+ 'status': 'open', 'total': '0.0002',
+ 'type': 'limit'
+ },
+ 'price': 0.1, 'remaining': 0.002, 'side': 'buy',
+ 'status': 'open', 'symbol': 'ETH/BTC',
+ 'timestamp': 1521990951000, 'trades': None,
+ 'type': 'limit'
+ },
+ { # Margin
+ 'amount': 0.001, 'cost': 0.1,
+ 'datetime': '2018-03-25T15:15:51.000Z',
+ 'fee': None, 'filled': 0.0,
+ 'id': '2',
+ 'info': {
+ 'amount': '0.001',
+ 'date': '2018-03-25 15:15:51',
+ 'margin': 1, 'orderNumber': '2',
+ 'price': '0.1', 'rate': '0.1',
+ 'side': 'buy', 'startingAmount': '0.001',
+ 'status': 'open', 'total': '0.0001',
+ 'type': 'limit'
+ },
+ 'price': 0.1, 'remaining': 0.001, 'side': 'buy',
+ 'status': 'open', 'symbol': 'ETH/BTC',
+ 'timestamp': 1521990951000, 'trades': None,
+ 'type': 'limit'
+ },
+ { # selling
+ 'amount': 0.001, 'cost': 0.1,
+ 'datetime': '2018-03-25T15:15:51.000Z',
+ 'fee': None, 'filled': 0.0,
+ 'id': '3',
+ 'info': {
+ 'amount': '0.001',
+ 'date': '2018-03-25 15:15:51',
+ 'margin': 0, 'orderNumber': '3',
+ 'price': '0.1', 'rate': '0.1',
+ 'side': 'sell', 'startingAmount': '0.001',
+ 'status': 'open', 'total': '0.0001',
+ 'type': 'limit'
+ },
+ 'price': 0.1, 'remaining': 0.001, 'side': 'sell',
+ 'status': 'open', 'symbol': 'ETH/BTC',
+ 'timestamp': 1521990951000, 'trades': None,
+ 'type': 'limit'
+ },
+ { # Wrong rate
+ 'amount': 0.001, 'cost': 0.15,
+ 'datetime': '2018-03-25T15:15:51.000Z',
+ 'fee': None, 'filled': 0.0,
+ 'id': '4',
+ 'info': {
+ 'amount': '0.001',
+ 'date': '2018-03-25 15:15:51',
+ 'margin': 0, 'orderNumber': '4',
+ 'price': '0.15', 'rate': '0.15',
+ 'side': 'buy', 'startingAmount': '0.001',
+ 'status': 'open', 'total': '0.0001',
+ 'type': 'limit'
+ },
+ 'price': 0.15, 'remaining': 0.001, 'side': 'buy',
+ 'status': 'open', 'symbol': 'ETH/BTC',
+ 'timestamp': 1521990951000, 'trades': None,
+ 'type': 'limit'
+ },
+ { # All good
+ 'amount': 0.001, 'cost': 0.1,
+ 'datetime': '2018-03-25T15:15:51.000Z',
+ 'fee': None, 'filled': 0.0,
+ 'id': '5',
+ 'info': {
+ 'amount': '0.001',
+ 'date': '2018-03-25 15:15:51',
+ 'margin': 0, 'orderNumber': '1',
+ 'price': '0.1', 'rate': '0.1',
+ 'side': 'buy', 'startingAmount': '0.001',
+ 'status': 'open', 'total': '0.0001',
+ 'type': 'limit'
+ },
+ 'price': 0.1, 'remaining': 0.001, 'side': 'buy',
+ 'status': 'open', 'symbol': 'ETH/BTC',
+ 'timestamp': 1521990951000, 'trades': None,
+ 'type': 'limit'
+ }
+ ]
+ result = order.retrieve_order()
+ self.assertTrue(result)
+ self.assertEqual('5', order.results[0]["id"])
+ self.m.ccxt.fetch_my_trades.assert_not_called()
+ self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
+
+ self.m.reset_mock()
+ with self.subTest(similar_open_order=False, past_trades=True):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+ self.m.ccxt.order_precision.return_value = 8
+ self.m.ccxt.fetch_orders.return_value = []
+ self.m.ccxt.fetch_my_trades.return_value = [
+ { # Wrong timestamp 1
+ 'amount': 0.0006,
+ 'cost': 0.00006,
+ 'datetime': '2018-03-25T15:15:14.000Z',
+ 'id': '1-1',
+ 'info': {
+ 'amount': '0.0006',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:15:14',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '1',
+ 'rate': '0.1',
+ 'total': '0.00006',
+ 'tradeID': '1-1',
+ 'type': 'buy'
+ },
+ 'order': '1',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983714,
+ 'type': 'limit'
+ },
+ { # Wrong timestamp 2
+ 'amount': 0.0004,
+ 'cost': 0.00004,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '1-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '1',
+ 'rate': '0.1',
+ 'total': '0.00004',
+ 'tradeID': '1-2',
+ 'type': 'buy'
+ },
+ 'order': '1',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ { # Wrong side 1
+ 'amount': 0.0006,
+ 'cost': 0.00006,
+ 'datetime': '2018-03-25T15:15:54.000Z',
+ 'id': '2-1',
+ 'info': {
+ 'amount': '0.0006',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:15:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '2',
+ 'rate': '0.1',
+ 'total': '0.00006',
+ 'tradeID': '2-1',
+ 'type': 'sell'
+ },
+ 'order': '2',
+ 'price': 0.1,
+ 'side': 'sell',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983754,
+ 'type': 'limit'
+ },
+ { # Wrong side 2
+ 'amount': 0.0004,
+ 'cost': 0.00004,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '2-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '2',
+ 'rate': '0.1',
+ 'total': '0.00004',
+ 'tradeID': '2-2',
+ 'type': 'buy'
+ },
+ 'order': '2',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ { # Margin trade 1
+ 'amount': 0.0006,
+ 'cost': 0.00006,
+ 'datetime': '2018-03-25T15:15:54.000Z',
+ 'id': '3-1',
+ 'info': {
+ 'amount': '0.0006',
+ 'category': 'marginTrade',
+ 'date': '2018-03-25 15:15:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '3',
+ 'rate': '0.1',
+ 'total': '0.00006',
+ 'tradeID': '3-1',
+ 'type': 'buy'
+ },
+ 'order': '3',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983754,
+ 'type': 'limit'
+ },
+ { # Margin trade 2
+ 'amount': 0.0004,
+ 'cost': 0.00004,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '3-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'marginTrade',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '3',
+ 'rate': '0.1',
+ 'total': '0.00004',
+ 'tradeID': '3-2',
+ 'type': 'buy'
+ },
+ 'order': '3',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ { # Wrong amount 1
+ 'amount': 0.0005,
+ 'cost': 0.00005,
+ 'datetime': '2018-03-25T15:15:54.000Z',
+ 'id': '4-1',
+ 'info': {
+ 'amount': '0.0005',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:15:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '4',
+ 'rate': '0.1',
+ 'total': '0.00005',
+ 'tradeID': '4-1',
+ 'type': 'buy'
+ },
+ 'order': '4',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983754,
+ 'type': 'limit'
+ },
+ { # Wrong amount 2
+ 'amount': 0.0004,
+ 'cost': 0.00004,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '4-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '4',
+ 'rate': '0.1',
+ 'total': '0.00004',
+ 'tradeID': '4-2',
+ 'type': 'buy'
+ },
+ 'order': '4',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ { # Wrong price 1
+ 'amount': 0.0006,
+ 'cost': 0.000066,
+ 'datetime': '2018-03-25T15:15:54.000Z',
+ 'id': '5-1',
+ 'info': {
+ 'amount': '0.0006',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:15:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '5',
+ 'rate': '0.11',
+ 'total': '0.000066',
+ 'tradeID': '5-1',
+ 'type': 'buy'
+ },
+ 'order': '5',
+ 'price': 0.11,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983754,
+ 'type': 'limit'
+ },
+ { # Wrong price 2
+ 'amount': 0.0004,
+ 'cost': 0.00004,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '5-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '5',
+ 'rate': '0.1',
+ 'total': '0.00004',
+ 'tradeID': '5-2',
+ 'type': 'buy'
+ },
+ 'order': '5',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ { # All good 1
+ 'amount': 0.0006,
+ 'cost': 0.00006,
+ 'datetime': '2018-03-25T15:15:54.000Z',
+ 'id': '7-1',
+ 'info': {
+ 'amount': '0.0006',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:15:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 1,
+ 'orderNumber': '7',
+ 'rate': '0.1',
+ 'total': '0.00006',
+ 'tradeID': '7-1',
+ 'type': 'buy'
+ },
+ 'order': '7',
+ 'price': 0.1,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983754,
+ 'type': 'limit'
+ },
+ { # All good 2
+ 'amount': 0.0004,
+ 'cost': 0.000036,
+ 'datetime': '2018-03-25T15:16:54.000Z',
+ 'id': '7-2',
+ 'info': {
+ 'amount': '0.0004',
+ 'category': 'exchange',
+ 'date': '2018-03-25 15:16:54',
+ 'fee': '0.00150000',
+ 'globalTradeID': 2,
+ 'orderNumber': '7',
+ 'rate': '0.09',
+ 'total': '0.000036',
+ 'tradeID': '7-2',
+ 'type': 'buy'
+ },
+ 'order': '7',
+ 'price': 0.09,
+ 'side': 'buy',
+ 'symbol': 'ETH/BTC',
+ 'timestamp': 1521983814,
+ 'type': 'limit'
+ },
+ ]
+
+ result = order.retrieve_order()
+ self.assertTrue(result)
+ self.assertEqual('7', order.results[0]["id"])
+ self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
+
+ self.m.reset_mock()
+ with self.subTest(similar_open_order=False, past_trades=False):
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+ self.m.ccxt.order_precision.return_value = 8
+ self.m.ccxt.fetch_orders.return_value = []
+ self.m.ccxt.fetch_my_trades.return_value = []
+ result = order.retrieve_order()
+ self.assertFalse(result)
@unittest.skipUnless("unit" in limits, "Unit skipped")
class MouvementTest(WebMockTestCase):
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.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(), "Coucou\n1.00000000 BTC\n")
+ 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.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(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"})
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])
+
@mock.patch.object(market.ReportStore, "print_log")
@mock.patch.object(market.ReportStore, "add_log")
def test_log_stage(self, add_log, print_log):
'response': 'Hey'
})
+ 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',
+ })
+
+ @mock.patch.object(market.ReportStore, "add_log")
+ def test_log_market(self, add_log):
+ report_store = market.ReportStore(self.m)
+ class Args:
+ def __init__(self):
+ self.debug = True
+ self.quiet = False
+
+ report_store.log_market(Args(), 4, 1, "report", True)
+ add_log.assert_called_once_with({
+ "type": "market",
+ "commit": "$Format:%H$",
+ "args": { "debug": True, "quiet": False },
+ "user_id": 4,
+ "market_id": 1,
+ "report_path": "report",
+ "debug": True
+ })
+
@mock.patch.object(market.ReportStore, "print_log")
@mock.patch.object(market.ReportStore, "add_log")
def test_log_error(self, add_log, print_log):
mock.patch("main.parse_config") as main_parse_config:
with self.subTest(debug=False):
main_parse_config.return_value = ["pg_config", "report_path"]
- main_fetch_markets.return_value = [({"key": "market_config"},)]
+ main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
m = main.get_user_market("config_path.ini", 1)
self.assertIsInstance(m, market.Market)
with self.subTest(debug=True):
main_parse_config.return_value = ["pg_config", "report_path"]
- main_fetch_markets.return_value = [({"key": "market_config"},)]
+ main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
m = main.get_user_market("config_path.ini", 1, debug=True)
self.assertIsInstance(m, market.Market)
args_mock.after = "after"
self.assertEqual("", stdout_mock.getvalue())
- main.process("config", 1, "report_path", args_mock)
+ main.process("config", 3, 1, args_mock, "report_path", "pg_config")
market_mock.from_config.assert_has_calls([
- mock.call("config", debug="debug", user_id=1, report_path="report_path"),
+ mock.call("config", args_mock, pg_config="pg_config", market_id=3, user_id=1, report_path="report_path"),
mock.call().process("action", before="before", after="after"),
])
with self.subTest(exception=True):
market_mock.from_config.side_effect = Exception("boo")
- main.process("config", 1, "report_path", args_mock)
+ main.process(3, "config", 1, "report_path", args_mock, "pg_config")
self.assertEqual("Exception: boo\n", stdout_mock.getvalue())
def test_main(self):
parse_config.return_value = ["pg_config", "report_path"]
- fetch_markets.return_value = [["config1", 1], ["config2", 2]]
+ fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]]
main.main(["Foo", "Bar"])
self.assertEqual(2, process.call_count)
process.assert_has_calls([
- mock.call("config1", 1, "report_path", args_mock),
- mock.call("config2", 2, "report_path", args_mock),
+ mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"),
+ mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"),
])
with self.subTest(parallel=True):
with mock.patch("main.parse_args") as parse_args,\
parse_config.return_value = ["pg_config", "report_path"]
- fetch_markets.return_value = [["config1", 1], ["config2", 2]]
+ fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]]
main.main(["Foo", "Bar"])
self.assertEqual(2, process.call_count)
process.assert_has_calls([
mock.call.__bool__(),
- mock.call("config1", 1, "report_path", args_mock),
+ mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"),
mock.call.__bool__(),
- mock.call("config2", 2, "report_path", args_mock),
+ mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"),
])
@mock.patch.object(main.sys, "exit")
rows = list(main.fetch_markets({"foo": "bar"}, None))
psycopg2.connect.assert_called_once_with(foo="bar")
- cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs")
+ cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs")
self.assertEqual(["row_1", "row_2"], rows)
rows = list(main.fetch_markets({"foo": "bar"}, 1))
psycopg2.connect.assert_called_once_with(foo="bar")
- cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1)
+ cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", 1)
self.assertEqual(["row_1", "row_2"], rows)
def test_method_arguments(self):
ccxt = mock.Mock(spec=market.ccxt.poloniexE)
- m = market.Market(ccxt)
+ m = market.Market(ccxt, self.market_args())
processor = market.Processor(m)