X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=test.py;h=7b17f9ebc54cd3b68c3a777157a61a5ed60ccf83;hb=5542e9e31a0074f4ed3b91cadce643ad60083cde;hp=d4432f6e7a87636efc8c94913382b385177a4841;hpb=ada1b5f109ebaa6f3adb7cd87b007c6db891811c;p=perso%2FImmae%2FProjets%2FCryptomonnaies%2FCryptoportfolio%2FTrader.git diff --git a/test.py b/test.py index d4432f6..7b17f9e 100644 --- a/test.py +++ b/test.py @@ -7,6 +7,7 @@ from unittest import mock import requests import requests_mock from io import StringIO +import threading import portfolio, market, main, store limits = ["acceptance", "unit"] @@ -22,8 +23,11 @@ for test_type in limits: 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() @@ -33,10 +37,14 @@ class WebMockTestCase(unittest.TestCase): self.patchers = [ mock.patch.multiple(market.Portfolio, - last_date=None, - data=None, - liquidities={}, - report=mock.Mock()), + data=store.LockedVar(None), + liquidities=store.LockedVar({}), + last_date=store.LockedVar(None), + report=mock.Mock(), + worker=None, + worker_notify=None, + worker_started=False, + callback=None), mock.patch.multiple(portfolio.Computation, computations=portfolio.Computation.computations), ] @@ -47,12 +55,12 @@ class WebMockTestCase(unittest.TestCase): 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() @@ -60,7 +68,19 @@ class poloniexETest(unittest.TestCase): def tearDown(self): self.wm.stop() - super(poloniexETest, self).tearDown() + super().tearDown() + + def test__init(self): + with 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') def test_nanoseconds(self): with mock.patch.object(market.ccxt.time, "time") as time: @@ -72,6 +92,58 @@ class poloniexETest(unittest.TestCase): 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")) @@ -441,10 +513,103 @@ class poloniexETest(unittest.TestCase): create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") +@unittest.skipUnless("unit" in limits, "Unit skipped") +class NoopLockTest(unittest.TestCase): + def test_with(self): + noop_lock = store.NoopLock() + with noop_lock: + self.assertTrue(True) + +@unittest.skipUnless("unit" in limits, "Unit skipped") +class LockedVar(unittest.TestCase): + + def test_values(self): + locked_var = store.LockedVar("Foo") + self.assertIsInstance(locked_var.lock, store.NoopLock) + self.assertEqual("Foo", locked_var.val) + + def test_get(self): + with self.subTest(desc="Normal case"): + locked_var = store.LockedVar("Foo") + self.assertEqual("Foo", locked_var.get()) + with self.subTest(desc="Dict"): + locked_var = store.LockedVar({"foo": "bar"}) + self.assertEqual({"foo": "bar"}, locked_var.get()) + self.assertEqual("bar", locked_var.get("foo")) + self.assertIsNone(locked_var.get("other")) + + def test_set(self): + locked_var = store.LockedVar("Foo") + locked_var.set("Bar") + self.assertEqual("Bar", locked_var.get()) + + def test__getattr(self): + dummy = type('Dummy', (object,), {})() + dummy.attribute = "Hey" + + locked_var = store.LockedVar(dummy) + self.assertEqual("Hey", locked_var.attribute) + with self.assertRaises(AttributeError): + locked_var.other + + def test_start_lock(self): + locked_var = store.LockedVar("Foo") + locked_var.start_lock() + self.assertEqual("lock", locked_var.lock.__class__.__name__) + + thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) + thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) + thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) + + with locked_var.lock: + thread1.start() + thread2.start() + thread3.start() + + self.assertEqual("Foo", locked_var.val) + thread1.join() + thread2.join() + thread3.join() + self.assertEqual("Bar", locked_var.get()[0:3]) + + def test_wait_for_notification(self): + with self.assertRaises(RuntimeError): + store.Portfolio.wait_for_notification() + + with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ + mock.patch.object(store.Portfolio, "report") as report,\ + mock.patch.object(store.time, "sleep") as sleep: + store.Portfolio.start_worker(poll=3) + + store.Portfolio.worker_notify.set() + + store.Portfolio.callback.wait() + + report.print_log.assert_called_once_with("Fetching cryptoportfolio") + get.assert_called_once_with(refetch=True) + sleep.assert_called_once_with(3) + self.assertFalse(store.Portfolio.worker_notify.is_set()) + self.assertTrue(store.Portfolio.worker.is_alive()) + + store.Portfolio.callback.clear() + store.Portfolio.worker_started = False + store.Portfolio.worker_notify.set() + store.Portfolio.callback.wait() + + self.assertFalse(store.Portfolio.worker.is_alive()) + + def test_notify_and_wait(self): + with mock.patch.object(store.Portfolio, "callback") as callback,\ + mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: + store.Portfolio.notify_and_wait() + callback.clear.assert_called_once_with() + worker_notify.set.assert_called_once_with() + callback.wait.assert_called_once_with() + @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() @@ -453,86 +618,131 @@ class PortfolioTest(WebMockTestCase): @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") def test_get_cryptoportfolio(self, parse_cryptoportfolio): - self.wm.get(market.Portfolio.URL, [ - {"text":'{ "foo": "bar" }', "status_code": 200}, - {"text": "System Error", "status_code": 500}, - {"exc": requests.exceptions.ConnectTimeout}, - ]) - market.Portfolio.get_cryptoportfolio() - self.assertIn("foo", market.Portfolio.data) - self.assertEqual("bar", market.Portfolio.data["foo"]) - self.assertTrue(self.wm.called) - self.assertEqual(1, self.wm.call_count) - market.Portfolio.report.log_error.assert_not_called() - market.Portfolio.report.log_http_request.assert_called_once() - parse_cryptoportfolio.assert_called_once_with() - market.Portfolio.report.log_http_request.reset_mock() - parse_cryptoportfolio.reset_mock() - market.Portfolio.data = None - - market.Portfolio.get_cryptoportfolio() - self.assertIsNone(market.Portfolio.data) - self.assertEqual(2, self.wm.call_count) - parse_cryptoportfolio.assert_not_called() - market.Portfolio.report.log_error.assert_not_called() - market.Portfolio.report.log_http_request.assert_called_once() - market.Portfolio.report.log_http_request.reset_mock() - parse_cryptoportfolio.reset_mock() - - market.Portfolio.data = "Foo" - market.Portfolio.get_cryptoportfolio() - self.assertEqual(2, self.wm.call_count) - parse_cryptoportfolio.assert_not_called() - - market.Portfolio.get_cryptoportfolio(refetch=True) - self.assertEqual("Foo", market.Portfolio.data) - self.assertEqual(3, self.wm.call_count) - market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", - exception=mock.ANY) - market.Portfolio.report.log_http_request.assert_not_called() + with self.subTest(parallel=False): + self.wm.get(market.Portfolio.URL, [ + {"text":'{ "foo": "bar" }', "status_code": 200}, + {"text": "System Error", "status_code": 500}, + {"exc": requests.exceptions.ConnectTimeout}, + ]) + market.Portfolio.get_cryptoportfolio() + self.assertIn("foo", market.Portfolio.data.get()) + self.assertEqual("bar", market.Portfolio.data.get()["foo"]) + self.assertTrue(self.wm.called) + self.assertEqual(1, self.wm.call_count) + market.Portfolio.report.log_error.assert_not_called() + market.Portfolio.report.log_http_request.assert_called_once() + parse_cryptoportfolio.assert_called_once_with() + market.Portfolio.report.log_http_request.reset_mock() + parse_cryptoportfolio.reset_mock() + market.Portfolio.data = store.LockedVar(None) + + market.Portfolio.get_cryptoportfolio() + self.assertIsNone(market.Portfolio.data.get()) + self.assertEqual(2, self.wm.call_count) + parse_cryptoportfolio.assert_not_called() + market.Portfolio.report.log_error.assert_not_called() + market.Portfolio.report.log_http_request.assert_called_once() + market.Portfolio.report.log_http_request.reset_mock() + parse_cryptoportfolio.reset_mock() + + market.Portfolio.data = store.LockedVar("Foo") + market.Portfolio.get_cryptoportfolio() + self.assertEqual(2, self.wm.call_count) + parse_cryptoportfolio.assert_not_called() + + market.Portfolio.get_cryptoportfolio(refetch=True) + self.assertEqual("Foo", market.Portfolio.data.get()) + self.assertEqual(3, self.wm.call_count) + market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", + exception=mock.ANY) + market.Portfolio.report.log_http_request.assert_not_called() + with self.subTest(parallel=True): + with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ + mock.patch.object(market.Portfolio, "notify_and_wait") as notify: + with self.subTest(worker=True): + market.Portfolio.data = store.LockedVar(None) + market.Portfolio.worker = mock.Mock() + is_worker.return_value = True + self.wm.get(market.Portfolio.URL, [ + {"text":'{ "foo": "bar" }', "status_code": 200}, + ]) + market.Portfolio.get_cryptoportfolio() + self.assertIn("foo", market.Portfolio.data.get()) + parse_cryptoportfolio.reset_mock() + with self.subTest(worker=False): + market.Portfolio.data = store.LockedVar(None) + market.Portfolio.worker = mock.Mock() + is_worker.return_value = False + market.Portfolio.get_cryptoportfolio() + notify.assert_called_once_with() + parse_cryptoportfolio.assert_not_called() def test_parse_cryptoportfolio(self): - market.Portfolio.data = store.json.loads(self.json_response, parse_int=D, - parse_float=D) - market.Portfolio.parse_cryptoportfolio() - - self.assertListEqual( - ["medium", "high"], - list(market.Portfolio.liquidities.keys())) - - liquidities = market.Portfolio.liquidities - self.assertEqual(10, len(liquidities["medium"].keys())) - self.assertEqual(10, len(liquidities["high"].keys())) - - expected = { - 'BTC': (D("0.2857"), "long"), - 'DGB': (D("0.1015"), "long"), - 'DOGE': (D("0.1805"), "long"), - 'SC': (D("0.0623"), "long"), - 'ZEC': (D("0.3701"), "long"), - } - date = portfolio.datetime(2018, 1, 8) - self.assertDictEqual(expected, liquidities["high"][date]) - - expected = { - 'BTC': (D("1.1102e-16"), "long"), - 'ETC': (D("0.1"), "long"), - 'FCT': (D("0.1"), "long"), - 'GAS': (D("0.1"), "long"), - 'NAV': (D("0.1"), "long"), - 'OMG': (D("0.1"), "long"), - 'OMNI': (D("0.1"), "long"), - 'PPC': (D("0.1"), "long"), - 'RIC': (D("0.1"), "long"), - 'VIA': (D("0.1"), "long"), - 'XCP': (D("0.1"), "long"), - } - self.assertDictEqual(expected, liquidities["medium"][date]) - self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date) + with self.subTest(description="Normal case"): + market.Portfolio.data = store.LockedVar(store.json.loads( + self.json_response, parse_int=D, parse_float=D)) + market.Portfolio.parse_cryptoportfolio() + + self.assertListEqual( + ["medium", "high"], + list(market.Portfolio.liquidities.get().keys())) + + liquidities = market.Portfolio.liquidities.get() + self.assertEqual(10, len(liquidities["medium"].keys())) + self.assertEqual(10, len(liquidities["high"].keys())) + + expected = { + 'BTC': (D("0.2857"), "long"), + 'DGB': (D("0.1015"), "long"), + 'DOGE': (D("0.1805"), "long"), + 'SC': (D("0.0623"), "long"), + 'ZEC': (D("0.3701"), "long"), + } + date = portfolio.datetime(2018, 1, 8) + self.assertDictEqual(expected, liquidities["high"][date]) + + expected = { + 'BTC': (D("1.1102e-16"), "long"), + 'ETC': (D("0.1"), "long"), + 'FCT': (D("0.1"), "long"), + 'GAS': (D("0.1"), "long"), + 'NAV': (D("0.1"), "long"), + 'OMG': (D("0.1"), "long"), + 'OMNI': (D("0.1"), "long"), + 'PPC': (D("0.1"), "long"), + 'RIC': (D("0.1"), "long"), + 'VIA': (D("0.1"), "long"), + 'XCP': (D("0.1"), "long"), + } + self.assertDictEqual(expected, liquidities["medium"][date]) + self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date.get()) + + with self.subTest(description="Missing weight"): + data = store.json.loads(self.json_response, parse_int=D, parse_float=D) + del(data["portfolio_2"]["weights"]) + market.Portfolio.data = store.LockedVar(data) + + market.Portfolio.parse_cryptoportfolio() + self.assertListEqual( + ["medium", "high"], + list(market.Portfolio.liquidities.get().keys())) + self.assertEqual({}, market.Portfolio.liquidities.get("medium")) + + with self.subTest(description="All missing weights"): + data = store.json.loads(self.json_response, parse_int=D, parse_float=D) + del(data["portfolio_1"]["weights"]) + del(data["portfolio_2"]["weights"]) + market.Portfolio.data = store.LockedVar(data) + + market.Portfolio.parse_cryptoportfolio() + self.assertEqual({}, market.Portfolio.liquidities.get("medium")) + self.assertEqual({}, market.Portfolio.liquidities.get("high")) + self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) + @mock.patch.object(market.Portfolio, "get_cryptoportfolio") def test_repartition(self, get_cryptoportfolio): - market.Portfolio.liquidities = { + market.Portfolio.liquidities = store.LockedVar({ "medium": { "2018-03-01": "medium_2018-03-01", "2018-03-08": "medium_2018-03-08", @@ -541,8 +751,8 @@ class PortfolioTest(WebMockTestCase): "2018-03-01": "high_2018-03-01", "2018-03-08": "high_2018-03-08", } - } - market.Portfolio.last_date = "2018-03-08" + }) + market.Portfolio.last_date = store.LockedVar("2018-03-08") self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) get_cryptoportfolio.assert_called_once_with() @@ -559,9 +769,9 @@ class PortfolioTest(WebMockTestCase): else: self.assertFalse(refetch) self.call_count += 1 - market.Portfolio.last_date = store.datetime.now()\ + market.Portfolio.last_date = store.LockedVar(store.datetime.now()\ - store.timedelta(10)\ - + store.timedelta(self.call_count) + + store.timedelta(self.call_count)) get_cryptoportfolio.side_effect = _get market.Portfolio.wait_for_recent() @@ -572,7 +782,7 @@ class PortfolioTest(WebMockTestCase): sleep.reset_mock() get_cryptoportfolio.reset_mock() - market.Portfolio.last_date = None + market.Portfolio.last_date = store.LockedVar(None) self.call_count = 0 market.Portfolio.wait_for_recent(delta=15) sleep.assert_not_called() @@ -580,13 +790,45 @@ class PortfolioTest(WebMockTestCase): sleep.reset_mock() get_cryptoportfolio.reset_mock() - market.Portfolio.last_date = None + market.Portfolio.last_date = store.LockedVar(None) self.call_count = 0 market.Portfolio.wait_for_recent(delta=1) sleep.assert_called_with(30) self.assertEqual(9, sleep.call_count) self.assertEqual(10, get_cryptoportfolio.call_count) + def test_is_worker_thread(self): + with self.subTest(worker=None): + self.assertFalse(store.Portfolio.is_worker_thread()) + + with self.subTest(worker="not self"),\ + mock.patch("threading.current_thread") as current_thread: + current = mock.Mock() + current_thread.return_value = current + store.Portfolio.worker = mock.Mock() + self.assertFalse(store.Portfolio.is_worker_thread()) + + with self.subTest(worker="self"),\ + mock.patch("threading.current_thread") as current_thread: + current = mock.Mock() + current_thread.return_value = current + store.Portfolio.worker = current + self.assertTrue(store.Portfolio.is_worker_thread()) + + def test_start_worker(self): + with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: + store.Portfolio.start_worker() + notification.assert_called_once_with(poll=30) + + self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) + self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) + store.Portfolio.report.start_lock.assert_called_once_with() + + self.assertIsNotNone(store.Portfolio.worker) + self.assertIsNotNone(store.Portfolio.worker_notify) + self.assertIsNotNone(store.Portfolio.callback) + self.assertTrue(store.Portfolio.worker_started) + @unittest.skipUnless("unit" in limits, "Unit skipped") class AmountTest(WebMockTestCase): def test_values(self): @@ -912,12 +1154,12 @@ class BalanceTest(WebMockTestCase): @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) @@ -929,28 +1171,30 @@ class MarketTest(WebMockTestCase): 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) + with self.subTest(quiet=True): + m = market.Market(self.ccxt, self.market_args(quiet=True)) + report_store.assert_called_with(m, verbose_print=False) + @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): @@ -959,7 +1203,7 @@ class MarketTest(WebMockTestCase): 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() @@ -972,7 +1216,7 @@ class MarketTest(WebMockTestCase): "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"]) @@ -1000,7 +1244,7 @@ class MarketTest(WebMockTestCase): 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") @@ -1020,7 +1264,7 @@ class MarketTest(WebMockTestCase): 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() @@ -1047,7 +1291,7 @@ class MarketTest(WebMockTestCase): 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"), @@ -1087,7 +1331,7 @@ class MarketTest(WebMockTestCase): (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() @@ -1164,7 +1408,7 @@ class MarketTest(WebMockTestCase): 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") @@ -1200,42 +1444,270 @@ class MarketTest(WebMockTestCase): 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() - with self.subTest(file=None), mock.patch("market.open", file_open): - m = market.Market(self.ccxt, user_id=1) - m.store_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) + + 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() - 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_report() + 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) + 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,\ @@ -1249,7 +1721,7 @@ class MarketTest(WebMockTestCase): 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,\ @@ -1274,7 +1746,7 @@ class MarketTest(WebMockTestCase): @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) @@ -1584,7 +2056,7 @@ class TradeStoreTest(WebMockTestCase): @unittest.skipUnless("unit" in limits, "Unit skipped") class BalanceStoreTest(WebMockTestCase): def setUp(self): - super(BalanceStoreTest, self).setUp() + super().setUp() self.fetch_balance = { "ETC": { @@ -1823,16 +2295,20 @@ class TradeTest(WebMockTestCase): 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") @@ -1850,26 +2326,59 @@ class TradeTest(WebMockTestCase): 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 - self.assertFalse(trade.is_fullfiled) + order1 = mock.Mock() + order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3") - order3 = mock.Mock() - order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19") - trade.orders.append(order3) + 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=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") @@ -1897,6 +2406,27 @@ class TradeTest(WebMockTestCase): order1.filled_amount.assert_called_with(in_base_currency=True) order2.filled_amount.assert_called_with(in_base_currency=True) + def test_reopen_same_order(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) + order = portfolio.Order("buy", portfolio.Amount("ETH", 10), + D("0.1"), "BTC", "long", self.m, "trade") + with mock.patch("portfolio.Order.run") as run: + new_order = trade.reopen_same_order(order) + self.assertEqual("buy", new_order.action) + self.assertEqual(portfolio.Amount("ETH", 10), new_order.amount) + self.assertEqual(D("0.1"), new_order.rate) + self.assertEqual("BTC", new_order.base_currency) + self.assertEqual("long", new_order.trade_type) + self.assertEqual(self.m, new_order.market) + self.assertEqual(False, new_order.close_if_possible) + self.assertEqual(trade, new_order.trade) + run.assert_called_once() + self.assertEqual(1, len(trade.orders)) + self.assertEqual(new_order, trade.orders[0]) + @mock.patch.object(portfolio.Computation, "compute_value") @mock.patch.object(portfolio.Trade, "filled_amount") @mock.patch.object(portfolio, "Order") @@ -2529,7 +3059,9 @@ class OrderTest(WebMockTestCase): 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, "fix_disappeared_order") + @mock.patch.object(portfolio.Order, "mark_finished_order") + def test_fetch(self, mark_finished_order, fix_disappeared_order, fetch_mouvements): order = portfolio.Order("buy", portfolio.Amount("ETH", 10), D("0.1"), "BTC", "long", self.m, "trade") order.id = 45 @@ -2539,6 +3071,8 @@ class OrderTest(WebMockTestCase): 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() + fix_disappeared_order.assert_not_called() fetch_mouvements.assert_not_called() with self.subTest(debug=False): @@ -2555,17 +3089,112 @@ class OrderTest(WebMockTestCase): 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() + fix_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_fix_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" + })) + with mock.patch.object(order, "trade") as trade: + order.fix_disappeared_order() + trade.reopen_same_order.assert_not_called() + + 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" + })) + with mock.patch.object(order, "trade") as trade: + order.fix_disappeared_order() + self.assertEqual("closed", order.status) + trade.reopen_same_order.assert_not_called() + + 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" + })) + with mock.patch.object(order, "trade") as trade: + order.fix_disappeared_order() + self.assertEqual("closed", order.status) + trade.reopen_same_order.assert_not_called() + + 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" + })) + with mock.patch.object(order, "trade") as trade: + trade.reopen_same_order.return_value = "New order" + order.fix_disappeared_order() + self.assertEqual("error_disappeared", order.status) + trade.reopen_same_order.assert_called_once_with(order) + self.m.report.log_error.assert_called_once_with('fetch', + message='Order Order(buy long 10.00000000 ETH at 0.1 BTC [error_disappeared]) disappeared, recreating it as New order') @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), @@ -2584,10 +3213,8 @@ class OrderTest(WebMockTestCase): 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 @@ -2599,7 +3226,6 @@ class OrderTest(WebMockTestCase): 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): @@ -2705,6 +3331,549 @@ class OrderTest(WebMockTestCase): 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): @@ -2782,14 +3951,30 @@ class ReportStoreTest(WebMockTestCase): report_store.set_verbose(False) self.assertFalse(report_store.verbose_print) + def test_merge(self): + report_store1 = market.ReportStore(self.m, verbose_print=False) + report_store2 = market.ReportStore(None, verbose_print=False) + + report_store2.log_stage("1") + report_store1.log_stage("2") + report_store2.log_stage("3") + + report_store1.merge(report_store2) + + self.assertEqual(3, len(report_store1.logs)) + self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) + self.assertEqual(6, len(report_store1.print_logs)) + def test_print_log(self): report_store = market.ReportStore(self.m) with self.subTest(verbose=True),\ + mock.patch.object(store, "datetime") as time_mock,\ mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: + time_mock.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: @@ -2798,6 +3983,14 @@ class ReportStoreTest(WebMockTestCase): 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"}) @@ -2807,6 +4000,20 @@ class ReportStoreTest(WebMockTestCase): 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): @@ -3300,7 +4507,7 @@ class MainTest(WebMockTestCase): 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) @@ -3308,7 +4515,7 @@ class MainTest(WebMockTestCase): 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) @@ -3327,44 +4534,77 @@ class MainTest(WebMockTestCase): 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): - with mock.patch("main.parse_args") as parse_args,\ - mock.patch("main.parse_config") as parse_config,\ - mock.patch("main.fetch_markets") as fetch_markets,\ - mock.patch("main.process") as process: + with self.subTest(parallel=False): + with mock.patch("main.parse_args") as parse_args,\ + mock.patch("main.parse_config") as parse_config,\ + mock.patch("main.fetch_markets") as fetch_markets,\ + mock.patch("main.process") as process: - args_mock = mock.Mock() - args_mock.config = "config" - args_mock.user = "user" - parse_args.return_value = args_mock + args_mock = mock.Mock() + args_mock.parallel = False + args_mock.config = "config" + args_mock.user = "user" + parse_args.return_value = args_mock - parse_config.return_value = ["pg_config", "report_path"] + 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"]) + main.main(["Foo", "Bar"]) - parse_args.assert_called_with(["Foo", "Bar"]) - parse_config.assert_called_with("config") - fetch_markets.assert_called_with("pg_config", "user") + parse_args.assert_called_with(["Foo", "Bar"]) + parse_config.assert_called_with("config") + fetch_markets.assert_called_with("pg_config", "user") - 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), - ]) + self.assertEqual(2, process.call_count) + process.assert_has_calls([ + 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,\ + mock.patch("main.parse_config") as parse_config,\ + mock.patch("main.fetch_markets") as fetch_markets,\ + mock.patch("main.process") as process,\ + mock.patch("store.Portfolio.start_worker") as start: + + args_mock = mock.Mock() + args_mock.parallel = True + args_mock.config = "config" + args_mock.user = "user" + parse_args.return_value = args_mock + + parse_config.return_value = ["pg_config", "report_path"] + + fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]] + + main.main(["Foo", "Bar"]) + + parse_args.assert_called_with(["Foo", "Bar"]) + parse_config.assert_called_with("config") + fetch_markets.assert_called_with("pg_config", "user") + + start.assert_called_once_with() + self.assertEqual(2, process.call_count) + process.assert_has_calls([ + mock.call.__bool__(), + mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"), + mock.call.__bool__(), + mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"), + ]) @mock.patch.object(main.sys, "exit") @mock.patch("main.configparser") @@ -3449,7 +4689,7 @@ class MainTest(WebMockTestCase): 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) @@ -3459,7 +4699,7 @@ class MainTest(WebMockTestCase): 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) @@ -3523,13 +4763,13 @@ class ProcessorTest(WebMockTestCase): 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) method, arguments = processor.method_arguments("wait_for_recent") self.assertEqual(market.Portfolio.wait_for_recent, method) - self.assertEqual(["delta"], arguments) + self.assertEqual(["delta", "poll"], arguments) method, arguments = processor.method_arguments("prepare_trades") self.assertEqual(m.prepare_trades, method)