diff options
-rw-r--r-- | market.py | 10 | ||||
-rw-r--r-- | portfolio.py | 2 | ||||
-rw-r--r-- | store.py | 13 | ||||
-rw-r--r-- | tests/test_market.py | 34 | ||||
-rw-r--r-- | tests/test_portfolio.py | 2 | ||||
-rw-r--r-- | tests/test_store.py | 24 |
6 files changed, 59 insertions, 26 deletions
@@ -427,16 +427,16 @@ class Processor: | |||
427 | self.market.report.log_stage("{}_begin".format(process_name)) | 427 | self.market.report.log_stage("{}_begin".format(process_name)) |
428 | 428 | ||
429 | if "fetch_balances_begin" in step: | 429 | if "fetch_balances_begin" in step: |
430 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name), | 430 | self.run_action("fetch_balances", step["fetch_balances_begin"], |
431 | **step["fetch_balances_begin"]) | 431 | dict(options, tag="{}_begin".format(process_name))) |
432 | 432 | ||
433 | for action in self.ordered_actions: | 433 | for action in self.ordered_actions: |
434 | if action in step: | 434 | if action in step: |
435 | self.run_action(action, step[action], options) | 435 | self.run_action(action, step[action], options) |
436 | 436 | ||
437 | if "fetch_balances_end" in step: | 437 | if "fetch_balances_end" in step: |
438 | self.market.balances.fetch_balances(tag="{}_end".format(process_name), | 438 | self.run_action("fetch_balances", step["fetch_balances_end"], |
439 | **step["fetch_balances_end"]) | 439 | dict(options, tag="{}_end".format(process_name))) |
440 | 440 | ||
441 | self.market.report.log_stage("{}_end".format(process_name)) | 441 | self.market.report.log_stage("{}_end".format(process_name)) |
442 | 442 | ||
@@ -459,6 +459,8 @@ class Processor: | |||
459 | method = self.market.trades.close_trades | 459 | method = self.market.trades.close_trades |
460 | elif action == "print_tickers": | 460 | elif action == "print_tickers": |
461 | method = self.market.print_tickers | 461 | method = self.market.print_tickers |
462 | elif action == "fetch_balances": | ||
463 | method = self.market.balances.fetch_balances | ||
462 | 464 | ||
463 | signature = inspect.getfullargspec(method) | 465 | signature = inspect.getfullargspec(method) |
464 | defaults = signature.defaults or [] | 466 | defaults = signature.defaults or [] |
diff --git a/portfolio.py b/portfolio.py index 94472a3..1d29106 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -52,7 +52,7 @@ class Amount: | |||
52 | ticker=asset_ticker, | 52 | ticker=asset_ticker, |
53 | rate=rate) | 53 | rate=rate) |
54 | else: | 54 | else: |
55 | raise Exception("This asset is not available in the chosen market") | 55 | return Amount(other_currency, 0, linked_to=self, ticker=None, rate=0) |
56 | 56 | ||
57 | def as_json(self): | 57 | def as_json(self): |
58 | return { | 58 | return { |
@@ -304,7 +304,7 @@ class BalanceStore: | |||
304 | compute_value, type) | 304 | compute_value, type) |
305 | return amounts | 305 | return amounts |
306 | 306 | ||
307 | def fetch_balances(self, tag=None, add_portfolio=False, | 307 | def fetch_balances(self, tag=None, add_portfolio=False, liquidity="medium", |
308 | checkpoint=None, log_tickers=False, add_usdt=False, | 308 | checkpoint=None, log_tickers=False, add_usdt=False, |
309 | ticker_currency="BTC", ticker_compute_value="average", ticker_type="total"): | 309 | ticker_currency="BTC", ticker_compute_value="average", ticker_type="total"): |
310 | all_balances = self.market.ccxt.fetch_all_balances() | 310 | all_balances = self.market.ccxt.fetch_all_balances() |
@@ -313,7 +313,7 @@ class BalanceStore: | |||
313 | currency in self.all: | 313 | currency in self.all: |
314 | self.all[currency] = portfolio.Balance(currency, balance) | 314 | self.all[currency] = portfolio.Balance(currency, balance) |
315 | if add_portfolio: | 315 | if add_portfolio: |
316 | for currency in Portfolio.repartition(from_cache=True): | 316 | for currency in Portfolio.repartition(from_cache=True, liquidity=liquidity): |
317 | self.all.setdefault(currency, portfolio.Balance(currency, {})) | 317 | self.all.setdefault(currency, portfolio.Balance(currency, {})) |
318 | if add_usdt: | 318 | if add_usdt: |
319 | self.all.setdefault("USDT", portfolio.Balance("USDT", {})) | 319 | self.all.setdefault("USDT", portfolio.Balance("USDT", {})) |
@@ -517,7 +517,7 @@ class Portfolio: | |||
517 | elif delta < datetime.timedelta(days=1): | 517 | elif delta < datetime.timedelta(days=1): |
518 | return 60*60 | 518 | return 60*60 |
519 | else: | 519 | else: |
520 | raise "Too long waiting" | 520 | raise Exception("Too long waiting") |
521 | 521 | ||
522 | @classmethod | 522 | @classmethod |
523 | def start_worker(cls): | 523 | def start_worker(cls): |
@@ -555,7 +555,10 @@ class Portfolio: | |||
555 | cls.report.print_log("[Worker] Fetching cryptoportfolio") | 555 | cls.report.print_log("[Worker] Fetching cryptoportfolio") |
556 | cls.get_cryptoportfolio(refetch=True) | 556 | cls.get_cryptoportfolio(refetch=True) |
557 | cls.callback.set() | 557 | cls.callback.set() |
558 | time.sleep(cls.next_wait_time()) | 558 | try: |
559 | time.sleep(cls.next_wait_time()) | ||
560 | except Exception: | ||
561 | cls.stop_worker() | ||
559 | 562 | ||
560 | @classmethod | 563 | @classmethod |
561 | def stop_worker(cls): | 564 | def stop_worker(cls): |
@@ -591,6 +594,8 @@ class Portfolio: | |||
591 | if cls.data.get() is not None and not refetch: | 594 | if cls.data.get() is not None and not refetch: |
592 | return | 595 | return |
593 | if cls.worker is not None and not cls.is_worker_thread(): | 596 | if cls.worker is not None and not cls.is_worker_thread(): |
597 | if not cls.worker_started: | ||
598 | raise Exception("Portfolio worker is down and no usable data is present") | ||
594 | cls.notify_and_wait() | 599 | cls.notify_and_wait() |
595 | return | 600 | return |
596 | try: | 601 | try: |
diff --git a/tests/test_market.py b/tests/test_market.py index e6e6f36..9316480 100644 --- a/tests/test_market.py +++ b/tests/test_market.py | |||
@@ -1057,6 +1057,9 @@ class ProcessorTest(WebMockTestCase): | |||
1057 | method, arguments = processor.method_arguments("print_tickers") | 1057 | method, arguments = processor.method_arguments("print_tickers") |
1058 | self.assertEqual(m.print_tickers, method) | 1058 | self.assertEqual(m.print_tickers, method) |
1059 | 1059 | ||
1060 | method, arguments = processor.method_arguments("fetch_balances") | ||
1061 | self.assertEqual(m.balances.fetch_balances, method) | ||
1062 | |||
1060 | def test_process_step(self): | 1063 | def test_process_step(self): |
1061 | processor = market.Processor(self.m) | 1064 | processor = market.Processor(self.m) |
1062 | 1065 | ||
@@ -1069,19 +1072,17 @@ class ProcessorTest(WebMockTestCase): | |||
1069 | mock.call("process_foo__2_sell_begin"), | 1072 | mock.call("process_foo__2_sell_begin"), |
1070 | mock.call("process_foo__2_sell_end"), | 1073 | mock.call("process_foo__2_sell_end"), |
1071 | ]) | 1074 | ]) |
1072 | self.m.balances.fetch_balances.assert_has_calls([ | ||
1073 | mock.call(tag="process_foo__2_sell_begin"), | ||
1074 | mock.call(tag="process_foo__2_sell_end"), | ||
1075 | ]) | ||
1076 | 1075 | ||
1077 | self.assertEqual(5, run_action.call_count) | 1076 | self.assertEqual(7, run_action.call_count) |
1078 | 1077 | ||
1079 | run_action.assert_has_calls([ | 1078 | run_action.assert_has_calls([ |
1079 | mock.call('fetch_balances', {}, {'foo': 'bar', 'tag': 'process_foo__2_sell_begin'}), | ||
1080 | mock.call('prepare_trades', {}, {'foo': 'bar'}), | 1080 | mock.call('prepare_trades', {}, {'foo': 'bar'}), |
1081 | mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}), | 1081 | mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}), |
1082 | mock.call('run_orders', {}, {'foo': 'bar'}), | 1082 | mock.call('run_orders', {}, {'foo': 'bar'}), |
1083 | mock.call('follow_orders', {}, {'foo': 'bar'}), | 1083 | mock.call('follow_orders', {}, {'foo': 'bar'}), |
1084 | mock.call('close_trades', {}, {'foo': 'bar'}), | 1084 | mock.call('close_trades', {}, {'foo': 'bar'}), |
1085 | mock.call('fetch_balances', {}, {'foo': 'bar', 'tag': 'process_foo__2_sell_end'}), | ||
1085 | ]) | 1086 | ]) |
1086 | 1087 | ||
1087 | self.m.reset_mock() | 1088 | self.m.reset_mock() |
@@ -1094,30 +1095,31 @@ class ProcessorTest(WebMockTestCase): | |||
1094 | mock.call("process_foo__0_print_balances_begin"), | 1095 | mock.call("process_foo__0_print_balances_begin"), |
1095 | mock.call("process_foo__0_print_balances_end"), | 1096 | mock.call("process_foo__0_print_balances_end"), |
1096 | ]) | 1097 | ]) |
1097 | self.m.balances.fetch_balances.assert_has_calls([ | ||
1098 | mock.call(add_portfolio=True, checkpoint='end', | ||
1099 | log_tickers=True, | ||
1100 | add_usdt=True, | ||
1101 | tag='process_foo__0_print_balances_begin') | ||
1102 | ]) | ||
1103 | 1098 | ||
1104 | self.assertEqual(0, run_action.call_count) | 1099 | self.assertEqual(1, run_action.call_count) |
1100 | run_action.assert_has_calls([ | ||
1101 | mock.call('fetch_balances', | ||
1102 | {'checkpoint': 'end', 'log_tickers': True, 'add_usdt': True, 'add_portfolio': True}, | ||
1103 | {'foo': 'bar', 'tag': 'process_foo__0_print_balances_begin'}), | ||
1104 | ]) | ||
1105 | 1105 | ||
1106 | self.m.reset_mock() | 1106 | self.m.reset_mock() |
1107 | with mock.patch.object(processor, "run_action") as run_action: | 1107 | with mock.patch.object(processor, "run_action") as run_action: |
1108 | step = processor.scenarios["sell_needed"][1] | 1108 | step = processor.scenarios["sell_needed"][1] |
1109 | 1109 | ||
1110 | processor.process_step("foo", step, {"foo":"bar"}) | 1110 | processor.process_step("foo", step, {"foo":"bar"}) |
1111 | self.m.balances.fetch_balances.assert_not_called() | 1111 | self.assertEqual(1, run_action.call_count) |
1112 | 1112 | ||
1113 | self.m.reset_mock() | 1113 | self.m.reset_mock() |
1114 | with mock.patch.object(processor, "run_action") as run_action: | 1114 | with mock.patch.object(processor, "run_action") as run_action: |
1115 | step = processor.scenarios["print_balances"][0] | 1115 | step = processor.scenarios["print_balances"][0] |
1116 | 1116 | ||
1117 | processor.process_step("foo", step, {"foo":"bar"}) | 1117 | processor.process_step("foo", step, {"foo":"bar"}) |
1118 | self.m.balances.fetch_balances.assert_called_once_with( | 1118 | run_action.assert_has_calls([ |
1119 | add_portfolio=True, add_usdt=True, log_tickers=True, | 1119 | mock.call('fetch_balances', |
1120 | tag='process_foo__1_print_balances_begin') | 1120 | {'log_tickers': True, 'add_usdt': True, 'add_portfolio': True}, |
1121 | {'foo': 'bar', 'tag': 'process_foo__1_print_balances_begin'}), | ||
1122 | ]) | ||
1121 | 1123 | ||
1122 | def test_parse_args(self): | 1124 | def test_parse_args(self): |
1123 | processor = market.Processor(self.m) | 1125 | processor = market.Processor(self.m) |
diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index d4e5ab9..cad3095 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py | |||
@@ -1856,7 +1856,7 @@ class AmountTest(WebMockTestCase): | |||
1856 | with self.subTest(desc="no ticker for currency"): | 1856 | with self.subTest(desc="no ticker for currency"): |
1857 | self.m.get_ticker.return_value = None | 1857 | self.m.get_ticker.return_value = None |
1858 | 1858 | ||
1859 | self.assertRaises(Exception, amount.in_currency, "ETH", self.m) | 1859 | self.assertEqual(portfolio.Amount("ETH", 0), amount.in_currency("ETH", self.m)) |
1860 | 1860 | ||
1861 | with self.subTest(desc="nominal case"): | 1861 | with self.subTest(desc="nominal case"): |
1862 | self.m.get_ticker.return_value = { | 1862 | self.m.get_ticker.return_value = { |
diff --git a/tests/test_store.py b/tests/test_store.py index 4ab9bdf..6f220c8 100644 --- a/tests/test_store.py +++ b/tests/test_store.py | |||
@@ -1379,11 +1379,19 @@ class PortfolioTest(WebMockTestCase): | |||
1379 | with self.subTest(worker=False): | 1379 | with self.subTest(worker=False): |
1380 | market.Portfolio.data = store.LockedVar(None) | 1380 | market.Portfolio.data = store.LockedVar(None) |
1381 | market.Portfolio.worker = mock.Mock() | 1381 | market.Portfolio.worker = mock.Mock() |
1382 | market.Portfolio.worker_started = True | ||
1382 | is_worker.return_value = False | 1383 | is_worker.return_value = False |
1383 | market.Portfolio.get_cryptoportfolio() | 1384 | market.Portfolio.get_cryptoportfolio() |
1384 | notify.assert_called_once_with() | 1385 | notify.assert_called_once_with() |
1385 | parse_cryptoportfolio.assert_not_called() | 1386 | parse_cryptoportfolio.assert_not_called() |
1386 | store_cryptoportfolio.assert_not_called() | 1387 | store_cryptoportfolio.assert_not_called() |
1388 | with self.subTest(worker_started=False): | ||
1389 | market.Portfolio.data = store.LockedVar(None) | ||
1390 | market.Portfolio.worker = mock.Mock() | ||
1391 | market.Portfolio.worker_started = False | ||
1392 | is_worker.return_value = False | ||
1393 | with self.assertRaises(Exception): | ||
1394 | market.Portfolio.get_cryptoportfolio() | ||
1387 | 1395 | ||
1388 | def test_parse_cryptoportfolio(self): | 1396 | def test_parse_cryptoportfolio(self): |
1389 | with self.subTest(description="Normal case"): | 1397 | with self.subTest(description="Normal case"): |
@@ -1649,6 +1657,22 @@ class PortfolioTest(WebMockTestCase): | |||
1649 | store.Portfolio.worker.join() | 1657 | store.Portfolio.worker.join() |
1650 | self.assertFalse(store.Portfolio.worker.is_alive()) | 1658 | self.assertFalse(store.Portfolio.worker.is_alive()) |
1651 | 1659 | ||
1660 | with self.subTest("overdue"),\ | ||
1661 | mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
1662 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
1663 | mock.patch.object(store.Portfolio, "next_wait_time") as wait,\ | ||
1664 | mock.patch.object(store.time, "sleep") as sleep: | ||
1665 | wait.side_effect = Exception("Time over") | ||
1666 | store.Portfolio.start_worker() | ||
1667 | |||
1668 | store.Portfolio.worker_notify.set() | ||
1669 | |||
1670 | store.Portfolio.callback.wait() | ||
1671 | |||
1672 | report.print_log.assert_called_once_with("[Worker] Fetching cryptoportfolio") | ||
1673 | get.assert_called_once_with(refetch=True) | ||
1674 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
1675 | |||
1652 | def test_notify_and_wait(self): | 1676 | def test_notify_and_wait(self): |
1653 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | 1677 | with mock.patch.object(store.Portfolio, "callback") as callback,\ |
1654 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | 1678 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: |