]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - tests/test_market.py
Add some acceptance tests
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / tests / test_market.py
CommitLineData
c682bdf4
IB
1from .helper import *
2import market, store, portfolio
3import datetime
4
c682bdf4
IB
5class MarketTest(WebMockTestCase):
6 def setUp(self):
7 super().setUp()
8
9 self.ccxt = mock.Mock(spec=market.ccxt.poloniexE)
10
11 def test_values(self):
12 m = market.Market(self.ccxt, self.market_args())
13
14 self.assertEqual(self.ccxt, m.ccxt)
15 self.assertFalse(m.debug)
16 self.assertIsInstance(m.report, market.ReportStore)
17 self.assertIsInstance(m.trades, market.TradeStore)
18 self.assertIsInstance(m.balances, market.BalanceStore)
19 self.assertEqual(m, m.report.market)
20 self.assertEqual(m, m.trades.market)
21 self.assertEqual(m, m.balances.market)
22 self.assertEqual(m, m.ccxt._market)
23
24 m = market.Market(self.ccxt, self.market_args(debug=True))
25 self.assertTrue(m.debug)
26
27 m = market.Market(self.ccxt, self.market_args(debug=False))
28 self.assertFalse(m.debug)
29
30 with mock.patch("market.ReportStore") as report_store:
31 with self.subTest(quiet=False):
32 m = market.Market(self.ccxt, self.market_args(quiet=False))
33 report_store.assert_called_with(m, verbose_print=True)
34 report_store().log_market.assert_called_once()
35 report_store.reset_mock()
36 with self.subTest(quiet=True):
37 m = market.Market(self.ccxt, self.market_args(quiet=True))
38 report_store.assert_called_with(m, verbose_print=False)
39 report_store().log_market.assert_called_once()
40
41 @mock.patch("market.ccxt")
42 def test_from_config(self, ccxt):
43 with mock.patch("market.ReportStore"):
44 ccxt.poloniexE.return_value = self.ccxt
45
46 m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args())
47
48 self.assertEqual(self.ccxt, m.ccxt)
49
50 m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True))
51 self.assertEqual(True, m.debug)
52
53 def test_get_tickers(self):
54 self.ccxt.fetch_tickers.side_effect = [
55 "tickers",
56 market.NotSupported
57 ]
58
59 m = market.Market(self.ccxt, self.market_args())
60 self.assertEqual("tickers", m.get_tickers())
61 self.assertEqual("tickers", m.get_tickers())
62 self.ccxt.fetch_tickers.assert_called_once()
63
64 self.assertIsNone(m.get_tickers(refresh=self.time.time()))
65
66 def test_get_ticker(self):
67 with self.subTest(get_tickers=True):
68 self.ccxt.fetch_tickers.return_value = {
69 "ETH/ETC": { "bid": 1, "ask": 3 },
70 "XVG/ETH": { "bid": 10, "ask": 40 },
71 }
72 m = market.Market(self.ccxt, self.market_args())
73
74 ticker = m.get_ticker("ETH", "ETC")
75 self.assertEqual(1, ticker["bid"])
76 self.assertEqual(3, ticker["ask"])
77 self.assertEqual(2, ticker["average"])
78 self.assertFalse(ticker["inverted"])
79
80 ticker = m.get_ticker("ETH", "XVG")
81 self.assertEqual(0.0625, ticker["average"])
82 self.assertTrue(ticker["inverted"])
83 self.assertIn("original", ticker)
84 self.assertEqual(10, ticker["original"]["bid"])
85 self.assertEqual(25, ticker["original"]["average"])
86
87 ticker = m.get_ticker("XVG", "XMR")
88 self.assertIsNone(ticker)
89
90 with self.subTest(get_tickers=False):
91 self.ccxt.fetch_tickers.return_value = None
92 self.ccxt.fetch_ticker.side_effect = [
93 { "bid": 1, "ask": 3 },
94 market.ExchangeError("foo"),
95 { "bid": 10, "ask": 40 },
96 market.ExchangeError("foo"),
97 market.ExchangeError("foo"),
98 ]
99
100 m = market.Market(self.ccxt, self.market_args())
101
102 ticker = m.get_ticker("ETH", "ETC")
103 self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
104 self.assertEqual(1, ticker["bid"])
105 self.assertEqual(3, ticker["ask"])
106 self.assertEqual(2, ticker["average"])
107 self.assertFalse(ticker["inverted"])
108
109 ticker = m.get_ticker("ETH", "XVG")
110 self.assertEqual(0.0625, ticker["average"])
111 self.assertTrue(ticker["inverted"])
112 self.assertIn("original", ticker)
113 self.assertEqual(10, ticker["original"]["bid"])
114 self.assertEqual(25, ticker["original"]["average"])
115
116 ticker = m.get_ticker("XVG", "XMR")
117 self.assertIsNone(ticker)
118
119 def test_fetch_fees(self):
120 m = market.Market(self.ccxt, self.market_args())
121 self.ccxt.fetch_fees.return_value = "Foo"
122 self.assertEqual("Foo", m.fetch_fees())
123 self.ccxt.fetch_fees.assert_called_once()
124 self.ccxt.reset_mock()
125 self.assertEqual("Foo", m.fetch_fees())
126 self.ccxt.fetch_fees.assert_not_called()
127
128 @mock.patch.object(market.Portfolio, "repartition")
129 @mock.patch.object(market.Market, "get_ticker")
130 @mock.patch.object(market.TradeStore, "compute_trades")
131 def test_prepare_trades(self, compute_trades, get_ticker, repartition):
132 repartition.return_value = {
133 "XEM": (D("0.75"), "long"),
134 "BTC": (D("0.25"), "long"),
135 }
136 def _get_ticker(c1, c2):
137 if c1 == "USDT" and c2 == "BTC":
138 return { "average": D("0.0001") }
139 if c1 == "XVG" and c2 == "BTC":
140 return { "average": D("0.000001") }
141 self.fail("Should be called with {}, {}".format(c1, c2))
142 get_ticker.side_effect = _get_ticker
143
144 with mock.patch("market.ReportStore"):
145 m = market.Market(self.ccxt, self.market_args())
146 self.ccxt.fetch_all_balances.return_value = {
147 "USDT": {
148 "exchange_free": D("10000.0"),
149 "exchange_used": D("0.0"),
150 "exchange_total": D("10000.0"),
151 "total": D("10000.0")
152 },
153 "XVG": {
154 "exchange_free": D("10000.0"),
155 "exchange_used": D("0.0"),
156 "exchange_total": D("10000.0"),
157 "total": D("10000.0")
158 },
159 }
160
161 m.balances.fetch_balances(tag="tag")
162
163 m.prepare_trades()
164 compute_trades.assert_called()
165
166 call = compute_trades.call_args
167 self.assertEqual(1, call[0][0]["USDT"].value)
168 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
169 self.assertEqual(D("0.2525"), call[0][1]["BTC"].value)
170 self.assertEqual(D("0.7575"), call[0][1]["XEM"].value)
171 m.report.log_stage.assert_called_once_with("prepare_trades",
172 base_currency='BTC', compute_value='average',
173 liquidity='medium', only=None, repartition=None)
174 m.report.log_balances.assert_called_once_with(tag="tag")
175
176
177 @mock.patch.object(market.time, "sleep")
178 @mock.patch.object(market.TradeStore, "all_orders")
179 def test_follow_orders(self, all_orders, time_mock):
180 for debug, sleep in [
181 (False, None), (True, None),
182 (False, 12), (True, 12)]:
183 with self.subTest(sleep=sleep, debug=debug), \
184 mock.patch("market.ReportStore"):
185 m = market.Market(self.ccxt, self.market_args(debug=debug))
186
187 order_mock1 = mock.Mock()
188 order_mock2 = mock.Mock()
189 order_mock3 = mock.Mock()
190 all_orders.side_effect = [
191 [order_mock1, order_mock2],
192 [order_mock1, order_mock2],
193
194 [order_mock1, order_mock3],
195 [order_mock1, order_mock3],
196
197 [order_mock1, order_mock3],
198 [order_mock1, order_mock3],
199
200 []
201 ]
202
203 order_mock1.get_status.side_effect = ["open", "open", "closed"]
204 order_mock2.get_status.side_effect = ["open"]
205 order_mock3.get_status.side_effect = ["open", "closed"]
206
207 order_mock1.trade = mock.Mock()
208 order_mock2.trade = mock.Mock()
209 order_mock3.trade = mock.Mock()
210
211 m.follow_orders(sleep=sleep)
212
213 order_mock1.trade.update_order.assert_any_call(order_mock1, 1)
214 order_mock1.trade.update_order.assert_any_call(order_mock1, 2)
215 self.assertEqual(2, order_mock1.trade.update_order.call_count)
216 self.assertEqual(3, order_mock1.get_status.call_count)
217
218 order_mock2.trade.update_order.assert_any_call(order_mock2, 1)
219 self.assertEqual(1, order_mock2.trade.update_order.call_count)
220 self.assertEqual(1, order_mock2.get_status.call_count)
221
222 order_mock3.trade.update_order.assert_any_call(order_mock3, 2)
223 self.assertEqual(1, order_mock3.trade.update_order.call_count)
224 self.assertEqual(2, order_mock3.get_status.call_count)
225 m.report.log_stage.assert_called()
226 calls = [
227 mock.call("follow_orders_begin"),
228 mock.call("follow_orders_tick_1"),
229 mock.call("follow_orders_tick_2"),
230 mock.call("follow_orders_tick_3"),
231 mock.call("follow_orders_end"),
232 ]
233 m.report.log_stage.assert_has_calls(calls)
234 m.report.log_orders.assert_called()
235 self.assertEqual(3, m.report.log_orders.call_count)
236 calls = [
237 mock.call([order_mock1, order_mock2], tick=1),
238 mock.call([order_mock1, order_mock3], tick=2),
239 mock.call([order_mock1, order_mock3], tick=3),
240 ]
241 m.report.log_orders.assert_has_calls(calls)
242 calls = [
243 mock.call(order_mock1, 3, finished=True),
244 mock.call(order_mock3, 3, finished=True),
245 ]
246 m.report.log_order.assert_has_calls(calls)
247
248 if sleep is None:
249 if debug:
250 m.report.log_debug_action.assert_called_with("Set follow_orders tick to 7s")
251 time_mock.assert_called_with(7)
252 else:
253 time_mock.assert_called_with(30)
254 else:
255 time_mock.assert_called_with(sleep)
256
257 with self.subTest("disappearing order"), \
258 mock.patch("market.ReportStore"):
259 all_orders.reset_mock()
260 m = market.Market(self.ccxt, self.market_args())
261
262 order_mock1 = mock.Mock()
263 order_mock2 = mock.Mock()
264 all_orders.side_effect = [
265 [order_mock1, order_mock2],
266 [order_mock1, order_mock2],
267
268 [order_mock1, order_mock2],
269 [order_mock1, order_mock2],
270
271 []
272 ]
273
274 order_mock1.get_status.side_effect = ["open", "closed"]
275 order_mock2.get_status.side_effect = ["open", "error_disappeared"]
276
277 order_mock1.trade = mock.Mock()
278 trade_mock = mock.Mock()
279 order_mock2.trade = trade_mock
280
281 trade_mock.tick_actions_recreate.return_value = "tick1"
282
283 m.follow_orders()
284
285 trade_mock.tick_actions_recreate.assert_called_once_with(2)
286 trade_mock.prepare_order.assert_called_once_with(compute_value="tick1")
287 m.report.log_error.assert_called_once_with("follow_orders", message=mock.ANY)
288
289 @mock.patch.object(market.BalanceStore, "fetch_balances")
290 def test_move_balance(self, fetch_balances):
291 for debug in [True, False]:
292 with self.subTest(debug=debug),\
293 mock.patch("market.ReportStore"):
294 m = market.Market(self.ccxt, self.market_args(debug=debug))
295
296 value_from = portfolio.Amount("BTC", "1.0")
297 value_from.linked_to = portfolio.Amount("ETH", "10.0")
298 value_to = portfolio.Amount("BTC", "10.0")
299 trade1 = portfolio.Trade(value_from, value_to, "ETH", m)
300
301 value_from = portfolio.Amount("BTC", "0.0")
302 value_from.linked_to = portfolio.Amount("ETH", "0.0")
303 value_to = portfolio.Amount("BTC", "-3.0")
304 trade2 = portfolio.Trade(value_from, value_to, "ETH", m)
305
306 value_from = portfolio.Amount("USDT", "0.0")
307 value_from.linked_to = portfolio.Amount("XVG", "0.0")
308 value_to = portfolio.Amount("USDT", "-50.0")
309 trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
310
311 m.trades.all = [trade1, trade2, trade3]
312 balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
313 balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
314 balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
315 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
316
317 m.move_balances()
318
319 fetch_balances.assert_called_with()
320 m.report.log_move_balances.assert_called_once()
321
322 if debug:
323 m.report.log_debug_action.assert_called()
324 self.assertEqual(3, m.report.log_debug_action.call_count)
325 else:
326 self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin")
327 self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
328 self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
329
330 m.report.reset_mock()
331 fetch_balances.reset_mock()
332 with self.subTest(retry=True):
333 with mock.patch("market.ReportStore"):
334 m = market.Market(self.ccxt, self.market_args())
335
336 value_from = portfolio.Amount("BTC", "0.0")
337 value_from.linked_to = portfolio.Amount("ETH", "0.0")
338 value_to = portfolio.Amount("BTC", "-3.0")
339 trade = portfolio.Trade(value_from, value_to, "ETH", m)
340
341 m.trades.all = [trade]
342 balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
343 m.balances.all = {"BTC": balance}
344
345 m.ccxt.transfer_balance.side_effect = [
346 market.ccxt.RequestTimeout,
347 market.ccxt.InvalidNonce,
348 True
349 ]
350 m.move_balances()
351 self.ccxt.transfer_balance.assert_has_calls([
352 mock.call("BTC", 3, "exchange", "margin"),
353 mock.call("BTC", 3, "exchange", "margin"),
354 mock.call("BTC", 3, "exchange", "margin")
355 ])
356 self.assertEqual(3, fetch_balances.call_count)
357 m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY)
358 self.assertEqual(3, m.report.log_move_balances.call_count)
359
360 self.ccxt.transfer_balance.reset_mock()
361 m.report.reset_mock()
362 fetch_balances.reset_mock()
363 with self.subTest(retry=True, too_much=True):
364 with mock.patch("market.ReportStore"):
365 m = market.Market(self.ccxt, self.market_args())
366
367 value_from = portfolio.Amount("BTC", "0.0")
368 value_from.linked_to = portfolio.Amount("ETH", "0.0")
369 value_to = portfolio.Amount("BTC", "-3.0")
370 trade = portfolio.Trade(value_from, value_to, "ETH", m)
371
372 m.trades.all = [trade]
373 balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
374 m.balances.all = {"BTC": balance}
375
376 m.ccxt.transfer_balance.side_effect = [
377 market.ccxt.RequestTimeout,
378 market.ccxt.RequestTimeout,
379 market.ccxt.RequestTimeout,
380 market.ccxt.RequestTimeout,
381 market.ccxt.RequestTimeout,
382 ]
383 with self.assertRaises(market.ccxt.RequestTimeout):
384 m.move_balances()
385
386 self.ccxt.transfer_balance.reset_mock()
387 m.report.reset_mock()
388 fetch_balances.reset_mock()
389 with self.subTest(retry=True, partial_result=True):
390 with mock.patch("market.ReportStore"):
391 m = market.Market(self.ccxt, self.market_args())
392
393 value_from = portfolio.Amount("BTC", "1.0")
394 value_from.linked_to = portfolio.Amount("ETH", "10.0")
395 value_to = portfolio.Amount("BTC", "10.0")
396 trade1 = portfolio.Trade(value_from, value_to, "ETH", m)
397
398 value_from = portfolio.Amount("BTC", "0.0")
399 value_from.linked_to = portfolio.Amount("ETH", "0.0")
400 value_to = portfolio.Amount("BTC", "-3.0")
401 trade2 = portfolio.Trade(value_from, value_to, "ETH", m)
402
403 value_from = portfolio.Amount("USDT", "0.0")
404 value_from.linked_to = portfolio.Amount("XVG", "0.0")
405 value_to = portfolio.Amount("USDT", "-50.0")
406 trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
407
408 m.trades.all = [trade1, trade2, trade3]
409 balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
410 balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
411 balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
412 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
413
414 call_counts = { "BTC": 0, "USDT": 0, "ETC": 0 }
415 def _transfer_balance(currency, amount, from_, to_):
416 call_counts[currency] += 1
417 if currency == "BTC":
418 m.balances.all["BTC"] = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "3" })
419 if currency == "USDT":
420 if call_counts["USDT"] == 1:
421 raise market.ccxt.RequestTimeout
422 else:
423 m.balances.all["USDT"] = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "150" })
424 if currency == "ETC":
425 m.balances.all["ETC"] = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "10" })
426
427
428 m.ccxt.transfer_balance.side_effect = _transfer_balance
429
430 m.move_balances()
431 self.ccxt.transfer_balance.assert_has_calls([
432 mock.call("BTC", 3, "exchange", "margin"),
433 mock.call('USDT', 100, 'exchange', 'margin'),
434 mock.call('USDT', 100, 'exchange', 'margin'),
435 mock.call("ETC", 5, "margin", "exchange")
436 ])
437 self.assertEqual(2, fetch_balances.call_count)
438 m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY)
439 self.assertEqual(2, m.report.log_move_balances.call_count)
440 m.report.log_move_balances.asser_has_calls([
441 mock.call(
442 {
443 'BTC': portfolio.Amount("BTC", "3"),
444 'USDT': portfolio.Amount("USDT", "150"),
445 'ETC': portfolio.Amount("ETC", "10"),
446 },
447 {
448 'BTC': portfolio.Amount("BTC", "3"),
449 'USDT': portfolio.Amount("USDT", "100"),
450 }),
451 mock.call(
452 {
453 'BTC': portfolio.Amount("BTC", "3"),
454 'USDT': portfolio.Amount("USDT", "150"),
455 'ETC': portfolio.Amount("ETC", "10"),
456 },
457 {
458 'BTC': portfolio.Amount("BTC", "0"),
459 'USDT': portfolio.Amount("USDT", "100"),
460 'ETC': portfolio.Amount("ETC", "-5"),
461 }),
462 ])
463
464
465 def test_store_file_report(self):
466 file_open = mock.mock_open()
467 m = market.Market(self.ccxt,
468 self.market_args(report_path="present"), user_id=1)
469 with self.subTest(file="present"),\
470 mock.patch("market.open", file_open),\
471 mock.patch.object(m, "report") as report,\
472 mock.patch.object(market, "datetime") as time_mock:
473
474 report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]]
475 report.to_json.return_value = "json_content"
476
477 m.store_file_report(datetime.datetime(2018, 2, 25))
478
479 file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
480 file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w")
481 file_open().write.assert_any_call("json_content")
482 file_open().write.assert_any_call("Foo\nBar")
483 m.report.to_json.assert_called_once_with()
484
485 m = market.Market(self.ccxt, self.market_args(report_path="error"), user_id=1)
486 with self.subTest(file="error"),\
487 mock.patch("market.open") as file_open,\
488 mock.patch.object(m, "report") as report,\
489 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
490 file_open.side_effect = FileNotFoundError
491
492 m.store_file_report(datetime.datetime(2018, 2, 25))
493
494 self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
495
496 @mock.patch.object(market, "psycopg2")
497 def test_store_database_report(self, psycopg2):
498 connect_mock = mock.Mock()
499 cursor_mock = mock.MagicMock()
500
501 connect_mock.cursor.return_value = cursor_mock
502 psycopg2.connect.return_value = connect_mock
503 m = market.Market(self.ccxt, self.market_args(),
504 pg_config={"config": "pg_config"}, user_id=1)
505 cursor_mock.fetchone.return_value = [42]
506
507 with self.subTest(error=False),\
508 mock.patch.object(m, "report") as report:
509 report.to_json_array.return_value = [
510 ("date1", "type1", "payload1"),
511 ("date2", "type2", "payload2"),
512 ]
513 m.store_database_report(datetime.datetime(2018, 3, 24))
514 connect_mock.assert_has_calls([
515 mock.call.cursor(),
516 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)),
517 mock.call.cursor().fetchone(),
518 mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')),
519 mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')),
520 mock.call.commit(),
521 mock.call.cursor().close(),
522 mock.call.close()
523 ])
524
525 connect_mock.reset_mock()
526 with self.subTest(error=True),\
527 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
528 psycopg2.connect.side_effect = Exception("Bouh")
529 m.store_database_report(datetime.datetime(2018, 3, 24))
530 self.assertEqual(stdout_mock.getvalue(), "impossible to store report to database: Exception; Bouh\n")
531
532 def test_store_report(self):
533 m = market.Market(self.ccxt, self.market_args(report_db=False), user_id=1)
534 with self.subTest(file=None, pg_config=None),\
535 mock.patch.object(m, "report") as report,\
536 mock.patch.object(m, "store_database_report") as db_report,\
537 mock.patch.object(m, "store_file_report") as file_report:
538 m.store_report()
539 report.merge.assert_called_with(store.Portfolio.report)
540
541 file_report.assert_not_called()
542 db_report.assert_not_called()
543
544 report.reset_mock()
545 m = market.Market(self.ccxt, self.market_args(report_db=False, report_path="present"), user_id=1)
546 with self.subTest(file="present", pg_config=None),\
547 mock.patch.object(m, "report") as report,\
548 mock.patch.object(m, "store_file_report") as file_report,\
549 mock.patch.object(m, "store_database_report") as db_report,\
e7d7c0e5 550 mock.patch.object(market.datetime, "datetime") as time_mock:
c682bdf4
IB
551
552 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
553
554 m.store_report()
555
556 report.merge.assert_called_with(store.Portfolio.report)
557 file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
558 db_report.assert_not_called()
559
560 report.reset_mock()
561 m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"), user_id=1)
562 with self.subTest(file="present", pg_config=None, report_db=True),\
563 mock.patch.object(m, "report") as report,\
564 mock.patch.object(m, "store_file_report") as file_report,\
565 mock.patch.object(m, "store_database_report") as db_report,\
e7d7c0e5 566 mock.patch.object(market.datetime, "datetime") as time_mock:
c682bdf4
IB
567
568 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
569
570 m.store_report()
571
572 report.merge.assert_called_with(store.Portfolio.report)
573 file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
574 db_report.assert_not_called()
575
576 report.reset_mock()
577 m = market.Market(self.ccxt, self.market_args(report_db=True), pg_config="present", user_id=1)
578 with self.subTest(file=None, pg_config="present"),\
579 mock.patch.object(m, "report") as report,\
580 mock.patch.object(m, "store_file_report") as file_report,\
581 mock.patch.object(m, "store_database_report") as db_report,\
e7d7c0e5 582 mock.patch.object(market.datetime, "datetime") as time_mock:
c682bdf4
IB
583
584 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
585
586 m.store_report()
587
588 report.merge.assert_called_with(store.Portfolio.report)
589 file_report.assert_not_called()
590 db_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
591
592 report.reset_mock()
593 m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"),
594 pg_config="pg_config", user_id=1)
595 with self.subTest(file="present", pg_config="present"),\
596 mock.patch.object(m, "report") as report,\
597 mock.patch.object(m, "store_file_report") as file_report,\
598 mock.patch.object(m, "store_database_report") as db_report,\
e7d7c0e5 599 mock.patch.object(market.datetime, "datetime") as time_mock:
c682bdf4
IB
600
601 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
602
603 m.store_report()
604
605 report.merge.assert_called_with(store.Portfolio.report)
606 file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
607 db_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
608
609 def test_print_orders(self):
610 m = market.Market(self.ccxt, self.market_args())
611 with mock.patch.object(m.report, "log_stage") as log_stage,\
612 mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
613 mock.patch.object(m, "prepare_trades") as prepare_trades,\
614 mock.patch.object(m.trades, "prepare_orders") as prepare_orders:
615 m.print_orders()
616
617 log_stage.assert_called_with("print_orders")
618 fetch_balances.assert_called_with(tag="print_orders")
619 prepare_trades.assert_called_with(base_currency="BTC",
620 compute_value="average")
621 prepare_orders.assert_called_with(compute_value="average")
622
623 def test_print_balances(self):
624 m = market.Market(self.ccxt, self.market_args())
625
626 with mock.patch.object(m.balances, "in_currency") as in_currency,\
627 mock.patch.object(m.report, "log_stage") as log_stage,\
628 mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
629 mock.patch.object(m.report, "print_log") as print_log:
630
631 in_currency.return_value = {
632 "BTC": portfolio.Amount("BTC", "0.65"),
633 "ETH": portfolio.Amount("BTC", "0.3"),
634 }
635
636 m.print_balances()
637
638 log_stage.assert_called_once_with("print_balances")
639 fetch_balances.assert_called_with()
640 print_log.assert_has_calls([
641 mock.call("total:"),
642 mock.call(portfolio.Amount("BTC", "0.95")),
643 ])
644
645 @mock.patch("market.Processor.process")
646 @mock.patch("market.ReportStore.log_error")
647 @mock.patch("market.Market.store_report")
648 def test_process(self, store_report, log_error, process):
649 m = market.Market(self.ccxt, self.market_args())
650 with self.subTest(before=False, after=False):
651 m.process(None)
652
653 process.assert_not_called()
654 store_report.assert_called_once()
655 log_error.assert_not_called()
656
657 process.reset_mock()
658 log_error.reset_mock()
659 store_report.reset_mock()
660 with self.subTest(before=True, after=False):
661 m.process(None, before=True)
662
663 process.assert_called_once_with("sell_all", steps="before")
664 store_report.assert_called_once()
665 log_error.assert_not_called()
666
667 process.reset_mock()
668 log_error.reset_mock()
669 store_report.reset_mock()
670 with self.subTest(before=False, after=True):
671 m.process(None, after=True)
672
673 process.assert_called_once_with("sell_all", steps="after")
674 store_report.assert_called_once()
675 log_error.assert_not_called()
676
677 process.reset_mock()
678 log_error.reset_mock()
679 store_report.reset_mock()
680 with self.subTest(before=True, after=True):
681 m.process(None, before=True, after=True)
682
683 process.assert_has_calls([
684 mock.call("sell_all", steps="before"),
685 mock.call("sell_all", steps="after"),
686 ])
687 store_report.assert_called_once()
688 log_error.assert_not_called()
689
690 process.reset_mock()
691 log_error.reset_mock()
692 store_report.reset_mock()
693 with self.subTest(action="print_balances"),\
694 mock.patch.object(m, "print_balances") as print_balances:
695 m.process(["print_balances"])
696
697 process.assert_not_called()
698 log_error.assert_not_called()
699 store_report.assert_called_once()
700 print_balances.assert_called_once_with()
701
702 log_error.reset_mock()
703 store_report.reset_mock()
704 with self.subTest(action="print_orders"),\
705 mock.patch.object(m, "print_orders") as print_orders,\
706 mock.patch.object(m, "print_balances") as print_balances:
707 m.process(["print_orders", "print_balances"])
708
709 process.assert_not_called()
710 log_error.assert_not_called()
711 store_report.assert_called_once()
712 print_orders.assert_called_once_with()
713 print_balances.assert_called_once_with()
714
715 log_error.reset_mock()
716 store_report.reset_mock()
717 with self.subTest(action="unknown"):
718 m.process(["unknown"])
719 log_error.assert_called_once_with("market_process", message="Unknown action unknown")
720 store_report.assert_called_once()
721
722 log_error.reset_mock()
723 store_report.reset_mock()
724 with self.subTest(unhandled_exception=True):
725 process.side_effect = Exception("bouh")
726
727 m.process(None, before=True)
728 log_error.assert_called_with("market_process", exception=mock.ANY)
729 store_report.assert_called_once()
730
731
c682bdf4
IB
732class ProcessorTest(WebMockTestCase):
733 def test_values(self):
734 processor = market.Processor(self.m)
735
736 self.assertEqual(self.m, processor.market)
737
738 def test_run_action(self):
739 processor = market.Processor(self.m)
740
741 with mock.patch.object(processor, "parse_args") as parse_args:
742 method_mock = mock.Mock()
743 parse_args.return_value = [method_mock, { "foo": "bar" }]
744
745 processor.run_action("foo", "bar", "baz")
746
747 parse_args.assert_called_with("foo", "bar", "baz")
748
749 method_mock.assert_called_with(foo="bar")
750
751 processor.run_action("wait_for_recent", "bar", "baz")
752
753 method_mock.assert_called_with(foo="bar")
754
755 def test_select_step(self):
756 processor = market.Processor(self.m)
757
758 scenario = processor.scenarios["sell_all"]
759
760 self.assertEqual(scenario, processor.select_steps(scenario, "all"))
761 self.assertEqual(["all_sell"], list(map(lambda x: x["name"], processor.select_steps(scenario, "before"))))
762 self.assertEqual(["wait", "all_buy"], list(map(lambda x: x["name"], processor.select_steps(scenario, "after"))))
763 self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, 2))))
764 self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, "wait"))))
765
766 with self.assertRaises(TypeError):
767 processor.select_steps(scenario, ["wait"])
768
769 @mock.patch("market.Processor.process_step")
770 def test_process(self, process_step):
771 processor = market.Processor(self.m)
772
773 processor.process("sell_all", foo="bar")
774 self.assertEqual(3, process_step.call_count)
775
776 steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls))
777 scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls))
778 kwargs = list(map(lambda x: x[1][2], process_step.mock_calls))
779 self.assertEqual(["all_sell", "wait", "all_buy"], steps)
780 self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names)
781 self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs)
782
783 process_step.reset_mock()
784
785 processor.process("sell_needed", steps=["before", "after"])
786 self.assertEqual(3, process_step.call_count)
787
788 def test_method_arguments(self):
789 ccxt = mock.Mock(spec=market.ccxt.poloniexE)
790 m = market.Market(ccxt, self.market_args())
791
792 processor = market.Processor(m)
793
794 method, arguments = processor.method_arguments("wait_for_recent")
795 self.assertEqual(market.Portfolio.wait_for_recent, method)
796 self.assertEqual(["delta", "poll"], arguments)
797
798 method, arguments = processor.method_arguments("prepare_trades")
799 self.assertEqual(m.prepare_trades, method)
800 self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only'], arguments)
801
802 method, arguments = processor.method_arguments("prepare_orders")
803 self.assertEqual(m.trades.prepare_orders, method)
804
805 method, arguments = processor.method_arguments("move_balances")
806 self.assertEqual(m.move_balances, method)
807
808 method, arguments = processor.method_arguments("run_orders")
809 self.assertEqual(m.trades.run_orders, method)
810
811 method, arguments = processor.method_arguments("follow_orders")
812 self.assertEqual(m.follow_orders, method)
813
814 method, arguments = processor.method_arguments("close_trades")
815 self.assertEqual(m.trades.close_trades, method)
816
817 def test_process_step(self):
818 processor = market.Processor(self.m)
819
820 with mock.patch.object(processor, "run_action") as run_action:
821 step = processor.scenarios["sell_needed"][1]
822
823 processor.process_step("foo", step, {"foo":"bar"})
824
825 self.m.report.log_stage.assert_has_calls([
826 mock.call("process_foo__1_sell_begin"),
827 mock.call("process_foo__1_sell_end"),
828 ])
829 self.m.balances.fetch_balances.assert_has_calls([
830 mock.call(tag="process_foo__1_sell_begin"),
831 mock.call(tag="process_foo__1_sell_end"),
832 ])
833
834 self.assertEqual(5, run_action.call_count)
835
836 run_action.assert_has_calls([
837 mock.call('prepare_trades', {}, {'foo': 'bar'}),
838 mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}),
839 mock.call('run_orders', {}, {'foo': 'bar'}),
840 mock.call('follow_orders', {}, {'foo': 'bar'}),
841 mock.call('close_trades', {}, {'foo': 'bar'}),
842 ])
843
844 self.m.reset_mock()
845 with mock.patch.object(processor, "run_action") as run_action:
846 step = processor.scenarios["sell_needed"][0]
847
848 processor.process_step("foo", step, {"foo":"bar"})
849 self.m.balances.fetch_balances.assert_not_called()
850
851 def test_parse_args(self):
852 processor = market.Processor(self.m)
853
854 with mock.patch.object(processor, "method_arguments") as method_arguments:
855 method_mock = mock.Mock()
856 method_arguments.return_value = [
857 method_mock,
858 ["foo2", "foo"]
859 ]
860 method, args = processor.parse_args("action", {"foo": "bar", "foo2": "bar"}, {"foo": "bar2", "bla": "bla"})
861
862 self.assertEqual(method_mock, method)
863 self.assertEqual({"foo": "bar2", "foo2": "bar"}, args)
864
865 with mock.patch.object(processor, "method_arguments") as method_arguments:
866 method_mock = mock.Mock()
867 method_arguments.return_value = [
868 method_mock,
869 ["repartition"]
870 ]
871 method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {})
872
873 self.assertEqual(1, len(args["repartition"]))
874 self.assertIn("BTC", args["repartition"])
875
876 with mock.patch.object(processor, "method_arguments") as method_arguments:
877 method_mock = mock.Mock()
878 method_arguments.return_value = [
879 method_mock,
880 ["repartition", "base_currency"]
881 ]
882 method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {"base_currency": "USDT"})
883
884 self.assertEqual(1, len(args["repartition"]))
885 self.assertIn("USDT", args["repartition"])
886
887 with mock.patch.object(processor, "method_arguments") as method_arguments:
888 method_mock = mock.Mock()
889 method_arguments.return_value = [
890 method_mock,
891 ["repartition", "base_currency"]
892 ]
893 method, args = processor.parse_args("action", {"repartition": { "ETH": 1 }}, {"base_currency": "USDT"})
894
895 self.assertEqual(1, len(args["repartition"]))
896 self.assertIn("ETH", args["repartition"])
897
898