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