2 import market
, store
, portfolio
, dbs
5 @unittest.skipUnless("unit" in limits
, "Unit skipped")
6 class MarketTest(WebMockTestCase
):
10 self
.ccxt
= mock
.Mock(spec
=market
.ccxt
.poloniexE
)
12 def test_values(self
):
13 m
= market
.Market(self
.ccxt
, self
.market_args())
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
)
25 m
= market
.Market(self
.ccxt
, self
.market_args(debug
=True))
26 self
.assertTrue(m
.debug
)
28 m
= market
.Market(self
.ccxt
, self
.market_args(debug
=False))
29 self
.assertFalse(m
.debug
)
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()
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
47 m
= market
.Market
.from_config({"key": "key", "secred": "secret"}
, self
.market_args())
49 self
.assertEqual(self
.ccxt
, m
.ccxt
)
51 m
= market
.Market
.from_config({"key": "key", "secred": "secret"}
, self
.market_args(debug
=True))
52 self
.assertEqual(True, m
.debug
)
54 def test_get_tickers(self
):
55 self
.ccxt
.fetch_tickers
.side_effect
= [
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()
65 self
.assertIsNone(m
.get_tickers(refresh
=self
.time
.time()))
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 }
,
73 m
= market
.Market(self
.ccxt
, self
.market_args())
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"])
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"])
88 ticker
= m
.get_ticker("XVG", "XMR")
89 self
.assertIsNone(ticker
)
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"),
101 m
= market
.Market(self
.ccxt
, self
.market_args())
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"])
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"])
117 ticker
= m
.get_ticker("XVG", "XMR")
118 self
.assertIsNone(ticker
)
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()
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 with self
.subTest(available_balance_only
=False),\
134 mock
.patch("market.ReportStore"):
135 def _get_ticker(c1
, c2
):
136 if c1
== "USDT" and c2
== "BTC":
137 return { "average": D("0.0001") }
138 if c1
== "XVG" and c2
== "BTC":
139 return { "average": D("0.000001") }
140 self
.fail("Should not be called with {}, {}".format(c1
, c2
))
141 get_ticker
.side_effect
= _get_ticker
143 repartition
.return_value
= {
144 "XEM": (D("0.75"), "long"),
145 "BTC": (D("0.25"), "long"),
147 m
= market
.Market(self
.ccxt
, self
.market_args())
148 self
.ccxt
.fetch_all_balances
.return_value
= {
150 "exchange_free": D("10000.0"),
151 "exchange_used": D("0.0"),
152 "exchange_total": D("10000.0"),
153 "total": D("10000.0")
156 "exchange_free": D("10000.0"),
157 "exchange_used": D("0.0"),
158 "exchange_total": D("10000.0"),
159 "total": D("10000.0")
163 m
.balances
.fetch_balances(tag
="tag")
166 compute_trades
.assert_called()
168 call
= compute_trades
.call_args
169 self
.assertEqual(1, call
[0][0]["USDT"].value
)
170 self
.assertEqual(D("0.01"), call
[0][0]["XVG"].value
)
171 self
.assertEqual(D("0.2525"), call
[0][1]["BTC"].value
)
172 self
.assertEqual(D("0.7575"), call
[0][1]["XEM"].value
)
173 m
.report
.log_stage
.assert_called_once_with("prepare_trades",
174 base_currency
='BTC', compute_value
='average',
175 available_balance_only
=False, liquidity
='medium',
176 only
=None, repartition
=None)
177 m
.report
.log_balances
.assert_called_once_with(tag
="tag", checkpoint
=None)
179 compute_trades
.reset_mock()
180 with self
.subTest(available_balance_only
=True),\
181 mock
.patch("market.ReportStore"):
182 def _get_ticker(c1
, c2
):
183 if c1
== "ZRC" and c2
== "BTC":
184 return { "average": D("0.0001") }
185 if c1
== "DOGE" and c2
== "BTC":
186 return { "average": D("0.000001") }
187 if c1
== "ETH" and c2
== "BTC":
188 return { "average": D("0.1") }
189 self
.fail("Should not be called with {}, {}".format(c1
, c2
))
190 get_ticker
.side_effect
= _get_ticker
192 repartition
.return_value
= {
193 "DOGE": (D("0.25"), "short"),
194 "BTC": (D("0.25"), "long"),
195 "ETH": (D("0.25"), "long"),
196 "XMR": (D("0.25"), "long"),
198 m
= market
.Market(self
.ccxt
, self
.market_args())
199 self
.ccxt
.fetch_all_balances
.return_value
= {
201 "exchange_free": D("2.0"),
202 "exchange_used": D("0.0"),
203 "exchange_total": D("2.0"),
207 "exchange_free": D("5.0"),
208 "exchange_used": D("0.0"),
209 "exchange_total": D("5.0"),
213 "exchange_free": D("0.075"),
214 "exchange_used": D("0.02"),
215 "exchange_total": D("0.095"),
216 "margin_available": D("0.025"),
217 "margin_in_position": D("0.01"),
218 "margin_total": D("0.035"),
222 "exchange_free": D("1.0"),
223 "exchange_used": D("0.0"),
224 "exchange_total": D("1.0"),
229 m
.balances
.fetch_balances(tag
="tag")
230 m
.prepare_trades(available_balance_only
=True)
231 compute_trades
.assert_called_once()
233 call
= compute_trades
.call_args
[0]
234 values_in_base
= call
[0]
235 new_repartition
= call
[1]
237 self
.assertEqual(portfolio
.Amount("BTC", "-0.025"),
238 new_repartition
["DOGE"] - values_in_base
["DOGE"])
239 self
.assertEqual(portfolio
.Amount("BTC", "0.025"),
240 new_repartition
["ETH"] - values_in_base
["ETH"])
242 new_repartition
["ZRC"] - values_in_base
["ZRC"])
243 self
.assertEqual(portfolio
.Amount("BTC", "0.025"),
244 new_repartition
["XMR"])
246 compute_trades
.reset_mock()
247 with self
.subTest(available_balance_only
=True, balance
=0),\
248 mock
.patch("market.ReportStore"):
249 def _get_ticker(c1
, c2
):
250 if c1
== "ETH" and c2
== "BTC":
251 return { "average": D("0.1") }
252 self
.fail("Should not be called with {}, {}".format(c1
, c2
))
253 get_ticker
.side_effect
= _get_ticker
255 repartition
.return_value
= {
256 "BTC": (D("0.5"), "long"),
257 "ETH": (D("0.5"), "long"),
259 m
= market
.Market(self
.ccxt
, self
.market_args())
260 self
.ccxt
.fetch_all_balances
.return_value
= {
262 "exchange_free": D("1.0"),
263 "exchange_used": D("0.0"),
264 "exchange_total": D("1.0"),
269 m
.balances
.fetch_balances(tag
="tag")
270 m
.prepare_trades(available_balance_only
=True)
271 compute_trades
.assert_called_once()
273 call
= compute_trades
.call_args
[0]
274 values_in_base
= call
[0]
275 new_repartition
= call
[1]
277 self
.assertEqual(new_repartition
["ETH"], values_in_base
["ETH"])
279 @mock.patch.object(market
.time
, "sleep")
280 @mock.patch.object(market
.TradeStore
, "all_orders")
281 def test_follow_orders(self
, all_orders
, time_mock
):
282 for debug
, sleep
in [
283 (False, None), (True, None),
284 (False, 12), (True, 12)]:
285 with self
.subTest(sleep
=sleep
, debug
=debug
), \
286 mock
.patch("market.ReportStore"):
287 m
= market
.Market(self
.ccxt
, self
.market_args(debug
=debug
))
289 order_mock1
= mock
.Mock()
290 order_mock2
= mock
.Mock()
291 order_mock3
= mock
.Mock()
292 all_orders
.side_effect
= [
293 [order_mock1
, order_mock2
],
294 [order_mock1
, order_mock2
],
296 [order_mock1
, order_mock3
],
297 [order_mock1
, order_mock3
],
299 [order_mock1
, order_mock3
],
300 [order_mock1
, order_mock3
],
305 order_mock1
.get_status
.side_effect
= ["open", "open", "closed"]
306 order_mock2
.get_status
.side_effect
= ["open"]
307 order_mock3
.get_status
.side_effect
= ["open", "closed"]
309 order_mock1
.trade
= mock
.Mock()
310 order_mock2
.trade
= mock
.Mock()
311 order_mock3
.trade
= mock
.Mock()
313 m
.follow_orders(sleep
=sleep
)
315 order_mock1
.trade
.update_order
.assert_any_call(order_mock1
, 1)
316 order_mock1
.trade
.update_order
.assert_any_call(order_mock1
, 2)
317 self
.assertEqual(2, order_mock1
.trade
.update_order
.call_count
)
318 self
.assertEqual(3, order_mock1
.get_status
.call_count
)
320 order_mock2
.trade
.update_order
.assert_any_call(order_mock2
, 1)
321 self
.assertEqual(1, order_mock2
.trade
.update_order
.call_count
)
322 self
.assertEqual(1, order_mock2
.get_status
.call_count
)
324 order_mock3
.trade
.update_order
.assert_any_call(order_mock3
, 2)
325 self
.assertEqual(1, order_mock3
.trade
.update_order
.call_count
)
326 self
.assertEqual(2, order_mock3
.get_status
.call_count
)
327 m
.report
.log_stage
.assert_called()
329 mock
.call("follow_orders_begin"),
330 mock
.call("follow_orders_tick_1"),
331 mock
.call("follow_orders_tick_2"),
332 mock
.call("follow_orders_tick_3"),
333 mock
.call("follow_orders_end"),
335 m
.report
.log_stage
.assert_has_calls(calls
)
336 m
.report
.log_orders
.assert_called()
337 self
.assertEqual(3, m
.report
.log_orders
.call_count
)
339 mock
.call([order_mock1
, order_mock2
], tick
=1),
340 mock
.call([order_mock1
, order_mock3
], tick
=2),
341 mock
.call([order_mock1
, order_mock3
], tick
=3),
343 m
.report
.log_orders
.assert_has_calls(calls
)
345 mock
.call(order_mock1
, 3, finished
=True),
346 mock
.call(order_mock3
, 3, finished
=True),
348 m
.report
.log_order
.assert_has_calls(calls
)
352 m
.report
.log_debug_action
.assert_called_with("Set follow_orders tick to 7s")
353 time_mock
.assert_called_with(7)
355 time_mock
.assert_called_with(30)
357 time_mock
.assert_called_with(sleep
)
359 with self
.subTest("disappearing order"), \
360 mock
.patch("market.ReportStore"):
361 all_orders
.reset_mock()
362 m
= market
.Market(self
.ccxt
, self
.market_args())
364 order_mock1
= mock
.Mock()
365 order_mock2
= mock
.Mock()
366 all_orders
.side_effect
= [
367 [order_mock1
, order_mock2
],
368 [order_mock1
, order_mock2
],
370 [order_mock1
, order_mock2
],
371 [order_mock1
, order_mock2
],
376 order_mock1
.get_status
.side_effect
= ["open", "closed"]
377 order_mock2
.get_status
.side_effect
= ["open", "error_disappeared"]
379 order_mock1
.trade
= mock
.Mock()
380 trade_mock
= mock
.Mock()
381 order_mock2
.trade
= trade_mock
383 trade_mock
.tick_actions_recreate
.return_value
= "tick1"
387 trade_mock
.tick_actions_recreate
.assert_called_once_with(2)
388 trade_mock
.prepare_order
.assert_called_once_with(compute_value
="tick1")
389 m
.report
.log_error
.assert_called_once_with("follow_orders", message
=mock
.ANY
)
391 @mock.patch.object(market
.BalanceStore
, "fetch_balances")
392 def test_move_balance(self
, fetch_balances
):
393 for debug
in [True, False]:
394 with self
.subTest(debug
=debug
),\
395 mock
.patch("market.ReportStore"):
396 m
= market
.Market(self
.ccxt
, self
.market_args(debug
=debug
))
398 value_from
= portfolio
.Amount("BTC", "1.0")
399 value_from
.linked_to
= portfolio
.Amount("ETH", "10.0")
400 value_to
= portfolio
.Amount("BTC", "10.0")
401 trade1
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
403 value_from
= portfolio
.Amount("BTC", "0.0")
404 value_from
.linked_to
= portfolio
.Amount("ETH", "0.0")
405 value_to
= portfolio
.Amount("BTC", "-3.0")
406 trade2
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
408 value_from
= portfolio
.Amount("USDT", "0.0")
409 value_from
.linked_to
= portfolio
.Amount("XVG", "0.0")
410 value_to
= portfolio
.Amount("USDT", "-50.0")
411 trade3
= portfolio
.Trade(value_from
, value_to
, "XVG", m
)
413 m
.trades
.all
= [trade1
, trade2
, trade3
]
414 balance1
= portfolio
.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }
)
415 balance2
= portfolio
.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" }
)
416 balance3
= portfolio
.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" }
)
417 m
.balances
.all
= {"BTC": balance1, "USDT": balance2, "ETC": balance3}
421 fetch_balances
.assert_called_with()
422 m
.report
.log_move_balances
.assert_called_once()
425 m
.report
.log_debug_action
.assert_called()
426 self
.assertEqual(3, m
.report
.log_debug_action
.call_count
)
428 self
.ccxt
.transfer_balance
.assert_any_call("BTC", 3, "exchange", "margin")
429 self
.ccxt
.transfer_balance
.assert_any_call("USDT", 100, "exchange", "margin")
430 self
.ccxt
.transfer_balance
.assert_any_call("ETC", 5, "margin", "exchange")
432 m
.report
.reset_mock()
433 fetch_balances
.reset_mock()
434 with self
.subTest(retry
=True):
435 with mock
.patch("market.ReportStore"):
436 m
= market
.Market(self
.ccxt
, self
.market_args())
438 value_from
= portfolio
.Amount("BTC", "0.0")
439 value_from
.linked_to
= portfolio
.Amount("ETH", "0.0")
440 value_to
= portfolio
.Amount("BTC", "-3.0")
441 trade
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
443 m
.trades
.all
= [trade
]
444 balance
= portfolio
.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }
)
445 m
.balances
.all
= {"BTC": balance}
447 m
.ccxt
.transfer_balance
.side_effect
= [
448 market
.ccxt
.RequestTimeout
,
449 market
.ccxt
.InvalidNonce
,
453 self
.ccxt
.transfer_balance
.assert_has_calls([
454 mock
.call("BTC", 3, "exchange", "margin"),
455 mock
.call("BTC", 3, "exchange", "margin"),
456 mock
.call("BTC", 3, "exchange", "margin")
458 self
.assertEqual(3, fetch_balances
.call_count
)
459 m
.report
.log_error
.assert_called_with(mock
.ANY
, message
="Retrying", exception
=mock
.ANY
)
460 self
.assertEqual(3, m
.report
.log_move_balances
.call_count
)
462 self
.ccxt
.transfer_balance
.reset_mock()
463 m
.report
.reset_mock()
464 fetch_balances
.reset_mock()
465 with self
.subTest(retry
=True, too_much
=True):
466 with mock
.patch("market.ReportStore"):
467 m
= market
.Market(self
.ccxt
, self
.market_args())
469 value_from
= portfolio
.Amount("BTC", "0.0")
470 value_from
.linked_to
= portfolio
.Amount("ETH", "0.0")
471 value_to
= portfolio
.Amount("BTC", "-3.0")
472 trade
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
474 m
.trades
.all
= [trade
]
475 balance
= portfolio
.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }
)
476 m
.balances
.all
= {"BTC": balance}
478 m
.ccxt
.transfer_balance
.side_effect
= [
479 market
.ccxt
.RequestTimeout
,
480 market
.ccxt
.RequestTimeout
,
481 market
.ccxt
.RequestTimeout
,
482 market
.ccxt
.RequestTimeout
,
483 market
.ccxt
.RequestTimeout
,
485 with self
.assertRaises(market
.ccxt
.RequestTimeout
):
488 self
.ccxt
.transfer_balance
.reset_mock()
489 m
.report
.reset_mock()
490 fetch_balances
.reset_mock()
491 with self
.subTest(retry
=True, partial_result
=True):
492 with mock
.patch("market.ReportStore"):
493 m
= market
.Market(self
.ccxt
, self
.market_args())
495 value_from
= portfolio
.Amount("BTC", "1.0")
496 value_from
.linked_to
= portfolio
.Amount("ETH", "10.0")
497 value_to
= portfolio
.Amount("BTC", "10.0")
498 trade1
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
500 value_from
= portfolio
.Amount("BTC", "0.0")
501 value_from
.linked_to
= portfolio
.Amount("ETH", "0.0")
502 value_to
= portfolio
.Amount("BTC", "-3.0")
503 trade2
= portfolio
.Trade(value_from
, value_to
, "ETH", m
)
505 value_from
= portfolio
.Amount("USDT", "0.0")
506 value_from
.linked_to
= portfolio
.Amount("XVG", "0.0")
507 value_to
= portfolio
.Amount("USDT", "-50.0")
508 trade3
= portfolio
.Trade(value_from
, value_to
, "XVG", m
)
510 m
.trades
.all
= [trade1
, trade2
, trade3
]
511 balance1
= portfolio
.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }
)
512 balance2
= portfolio
.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" }
)
513 balance3
= portfolio
.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" }
)
514 m
.balances
.all
= {"BTC": balance1, "USDT": balance2, "ETC": balance3}
516 call_counts
= { "BTC": 0, "USDT": 0, "ETC": 0 }
517 def _transfer_balance(currency
, amount
, from_
, to_
):
518 call_counts
[currency
] += 1
519 if currency
== "BTC":
520 m
.balances
.all
["BTC"] = portfolio
.Balance("BTC", { "margin_in_position": "0", "margin_available": "3" }
)
521 if currency
== "USDT":
522 if call_counts
["USDT"] == 1:
523 raise market
.ccxt
.RequestTimeout
525 m
.balances
.all
["USDT"] = portfolio
.Balance("USDT", { "margin_in_position": "100", "margin_available": "150" }
)
526 if currency
== "ETC":
527 m
.balances
.all
["ETC"] = portfolio
.Balance("ETC", { "margin_in_position": "10", "margin_available": "10" }
)
530 m
.ccxt
.transfer_balance
.side_effect
= _transfer_balance
533 self
.ccxt
.transfer_balance
.assert_has_calls([
534 mock
.call("BTC", 3, "exchange", "margin"),
535 mock
.call('USDT', 100, 'exchange', 'margin'),
536 mock
.call('USDT', 100, 'exchange', 'margin'),
537 mock
.call("ETC", 5, "margin", "exchange")
539 self
.assertEqual(2, fetch_balances
.call_count
)
540 m
.report
.log_error
.assert_called_with(mock
.ANY
, message
="Retrying", exception
=mock
.ANY
)
541 self
.assertEqual(2, m
.report
.log_move_balances
.call_count
)
542 m
.report
.log_move_balances
.asser_has_calls([
545 'BTC': portfolio
.Amount("BTC", "3"),
546 'USDT': portfolio
.Amount("USDT", "150"),
547 'ETC': portfolio
.Amount("ETC", "10"),
550 'BTC': portfolio
.Amount("BTC", "3"),
551 'USDT': portfolio
.Amount("USDT", "100"),
555 'BTC': portfolio
.Amount("BTC", "3"),
556 'USDT': portfolio
.Amount("USDT", "150"),
557 'ETC': portfolio
.Amount("ETC", "10"),
560 'BTC': portfolio
.Amount("BTC", "0"),
561 'USDT': portfolio
.Amount("USDT", "100"),
562 'ETC': portfolio
.Amount("ETC", "-5"),
567 def test_store_file_report(self
):
568 file_open
= mock
.mock_open()
569 m
= market
.Market(self
.ccxt
,
570 self
.market_args(report_path
="present"), user_id
=1)
571 with self
.subTest(file="present"),\
572 mock
.patch("market.open", file_open
),\
573 mock
.patch
.object(m
, "report") as report
,\
574 mock
.patch
.object(market
, "datetime") as time_mock
:
576 report
.print_logs
= [[time_mock
.now(), "Foo"], [time_mock
.now(), "Bar"]]
577 report
.to_json
.return_value
= "json_content"
579 m
.store_file_report(datetime
.datetime(2018, 2, 25))
581 file_open
.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
582 file_open
.assert_any_call("present/2018-02-25T00:00:00_1.log", "w")
583 file_open().write
.assert_any_call("json_content")
584 file_open().write
.assert_any_call("Foo\nBar")
585 m
.report
.to_json
.assert_called_once_with()
587 m
= market
.Market(self
.ccxt
, self
.market_args(report_path
="error"), user_id
=1)
588 with self
.subTest(file="error"),\
589 mock
.patch("market.open") as file_open
,\
590 mock
.patch
.object(m
, "report") as report
,\
591 mock
.patch('sys.stdout', new_callable
=StringIO
) as stdout_mock
:
592 file_open
.side_effect
= FileNotFoundError
594 m
.store_file_report(datetime
.datetime(2018, 2, 25))
596 self
.assertRegex(stdout_mock
.getvalue(), "impossible to store report file: FileNotFoundError;")
598 @mock.patch.object(dbs
, "psql")
599 def test_store_database_report(self
, psql
):
600 cursor_mock
= mock
.MagicMock()
602 psql
.cursor
.return_value
= cursor_mock
603 m
= market
.Market(self
.ccxt
, self
.market_args(),
604 pg_config
={"config": "pg_config"}
, user_id
=1)
605 cursor_mock
.fetchone
.return_value
= [42]
607 with self
.subTest(error
=False),\
608 mock
.patch
.object(m
, "report") as report
:
609 report
.to_json_array
.return_value
= [
610 ("date1", "type1", "payload1"),
611 ("date2", "type2", "payload2"),
613 m
.store_database_report(datetime
.datetime(2018, 3, 24))
614 psql
.assert_has_calls([
616 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)),
617 mock
.call
.cursor().fetchone(),
618 mock
.call
.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')),
619 mock
.call
.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')),
621 mock
.call
.cursor().close(),
624 with self
.subTest(error
=True),\
625 mock
.patch('sys.stdout', new_callable
=StringIO
) as stdout_mock
:
626 psql
.cursor
.side_effect
= Exception("Bouh")
627 m
.store_database_report(datetime
.datetime(2018, 3, 24))
628 self
.assertEqual(stdout_mock
.getvalue(), "impossible to store report to database: Exception; Bouh\n")
630 @mock.patch.object(dbs
, "redis")
631 def test_store_redis_report(self
, redis
):
632 m
= market
.Market(self
.ccxt
, self
.market_args(),
633 redis_config
={"config": "redis_config"}
, market_id
=1)
635 with self
.subTest(error
=False),\
636 mock
.patch
.object(m
, "report") as report
:
637 report
.to_json_redis
.return_value
= [
638 ("type1", "payload1"),
639 ("type2", "payload2"),
641 m
.store_redis_report(datetime
.datetime(2018, 3, 24))
642 redis
.assert_has_calls([
643 mock
.call
.set("/cryptoportfolio/1/2018-03-24T00:00:00/type1", "payload1", ex
=31*24*60*60),
644 mock
.call
.set("/cryptoportfolio/1/latest/type1", "payload1"),
645 mock
.call
.set("/cryptoportfolio/1/2018-03-24T00:00:00/type2", "payload2", ex
=31*24*60*60),
646 mock
.call
.set("/cryptoportfolio/1/latest/type2", "payload2"),
647 mock
.call
.set("/cryptoportfolio/1/latest/date", "2018-03-24T00:00:00"),
651 with self
.subTest(error
=True),\
652 mock
.patch('sys.stdout', new_callable
=StringIO
) as stdout_mock
:
653 redis
.set.side_effect
= Exception("Bouh")
654 m
.store_redis_report(datetime
.datetime(2018, 3, 24))
655 self
.assertEqual(stdout_mock
.getvalue(), "impossible to store report to redis: Exception; Bouh\n")
657 def test_store_report(self
):
658 m
= market
.Market(self
.ccxt
, self
.market_args(report_db
=False), user_id
=1)
659 with self
.subTest(file=None, pg_connected
=None),\
660 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
661 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
662 mock
.patch
.object(m
, "report") as report
,\
663 mock
.patch
.object(m
, "store_database_report") as db_report
,\
664 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
665 mock
.patch
.object(m
, "store_file_report") as file_report
:
666 psql
.return_value
= False
667 redis
.return_value
= False
669 report
.merge
.assert_called_with(store
.Portfolio
.report
)
671 file_report
.assert_not_called()
672 db_report
.assert_not_called()
673 redis_report
.assert_not_called()
676 m
= market
.Market(self
.ccxt
, self
.market_args(report_db
=False, report_path
="present"), user_id
=1)
677 with self
.subTest(file="present", pg_connected
=None),\
678 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
679 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
680 mock
.patch
.object(m
, "report") as report
,\
681 mock
.patch
.object(m
, "store_file_report") as file_report
,\
682 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
683 mock
.patch
.object(m
, "store_database_report") as db_report
,\
684 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
685 psql
.return_value
= False
686 redis
.return_value
= False
687 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
691 report
.merge
.assert_called_with(store
.Portfolio
.report
)
692 file_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
693 db_report
.assert_not_called()
694 redis_report
.assert_not_called()
697 m
= market
.Market(self
.ccxt
, self
.market_args(report_db
=True, report_path
="present"), user_id
=1)
698 with self
.subTest(file="present", pg_connected
=None, report_db
=True),\
699 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
700 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
701 mock
.patch
.object(m
, "report") as report
,\
702 mock
.patch
.object(m
, "store_file_report") as file_report
,\
703 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
704 mock
.patch
.object(m
, "store_database_report") as db_report
,\
705 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
706 psql
.return_value
= False
707 redis
.return_value
= False
708 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
712 report
.merge
.assert_called_with(store
.Portfolio
.report
)
713 file_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
714 db_report
.assert_not_called()
715 redis_report
.assert_not_called()
718 m
= market
.Market(self
.ccxt
, self
.market_args(report_db
=True), user_id
=1)
719 with self
.subTest(file=None, pg_connected
=True),\
720 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
721 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
722 mock
.patch
.object(m
, "report") as report
,\
723 mock
.patch
.object(m
, "store_file_report") as file_report
,\
724 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
725 mock
.patch
.object(m
, "store_database_report") as db_report
,\
726 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
727 psql
.return_value
= True
728 redis
.return_value
= False
729 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
733 report
.merge
.assert_called_with(store
.Portfolio
.report
)
734 file_report
.assert_not_called()
735 db_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
736 redis_report
.assert_not_called()
739 m
= market
.Market(self
.ccxt
, self
.market_args(report_db
=True, report_path
="present"),
741 with self
.subTest(file="present", pg_connected
=True),\
742 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
743 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
744 mock
.patch
.object(m
, "report") as report
,\
745 mock
.patch
.object(m
, "store_file_report") as file_report
,\
746 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
747 mock
.patch
.object(m
, "store_database_report") as db_report
,\
748 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
749 psql
.return_value
= True
750 redis
.return_value
= False
751 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
755 report
.merge
.assert_called_with(store
.Portfolio
.report
)
756 file_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
757 db_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
758 redis_report
.assert_not_called()
761 m
= market
.Market(self
.ccxt
, self
.market_args(report_redis
=False),
763 with self
.subTest(redis_connected
=True, report_redis
=False),\
764 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
765 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
766 mock
.patch
.object(m
, "report") as report
,\
767 mock
.patch
.object(m
, "store_file_report") as file_report
,\
768 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
769 mock
.patch
.object(m
, "store_database_report") as db_report
,\
770 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
771 psql
.return_value
= False
772 redis
.return_value
= True
773 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
776 redis_report
.assert_not_called()
779 m
= market
.Market(self
.ccxt
, self
.market_args(report_redis
=True),
781 with self
.subTest(redis_connected
=False, report_redis
=True),\
782 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
783 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
784 mock
.patch
.object(m
, "report") as report
,\
785 mock
.patch
.object(m
, "store_file_report") as file_report
,\
786 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
787 mock
.patch
.object(m
, "store_database_report") as db_report
,\
788 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
789 psql
.return_value
= False
790 redis
.return_value
= False
791 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
794 redis_report
.assert_not_called()
797 m
= market
.Market(self
.ccxt
, self
.market_args(report_redis
=True),
799 with self
.subTest(redis_connected
=True, report_redis
=True),\
800 mock
.patch
.object(dbs
, "psql_connected") as psql
,\
801 mock
.patch
.object(dbs
, "redis_connected") as redis
,\
802 mock
.patch
.object(m
, "report") as report
,\
803 mock
.patch
.object(m
, "store_file_report") as file_report
,\
804 mock
.patch
.object(m
, "store_redis_report") as redis_report
,\
805 mock
.patch
.object(m
, "store_database_report") as db_report
,\
806 mock
.patch
.object(market
.datetime
, "datetime") as time_mock
:
807 psql
.return_value
= False
808 redis
.return_value
= True
809 time_mock
.now
.return_value
= datetime
.datetime(2018, 2, 25)
812 redis_report
.assert_called_once_with(datetime
.datetime(2018, 2, 25))
814 def test_print_tickers(self
):
815 m
= market
.Market(self
.ccxt
, self
.market_args())
817 with mock
.patch
.object(m
.balances
, "in_currency") as in_currency
,\
818 mock
.patch
.object(m
.report
, "log_stage") as log_stage
,\
819 mock
.patch
.object(m
.balances
, "fetch_balances") as fetch_balances
,\
820 mock
.patch
.object(m
.report
, "print_log") as print_log
:
822 in_currency
.return_value
= {
823 "BTC": portfolio
.Amount("BTC", "0.65"),
824 "ETH": portfolio
.Amount("BTC", "0.3"),
829 print_log
.assert_has_calls([
831 mock
.call(portfolio
.Amount("BTC", "0.95")),
834 @mock.patch("market.Processor.process")
835 @mock.patch("market.ReportStore.log_error")
836 @mock.patch("market.Market.store_report")
837 def test_process(self
, store_report
, log_error
, process
):
838 m
= market
.Market(self
.ccxt
, self
.market_args())
839 with self
.subTest(actions
=[], before
=False, after
=False):
842 process
.assert_not_called()
843 store_report
.assert_called_once()
844 log_error
.assert_not_called()
847 log_error
.reset_mock()
848 store_report
.reset_mock()
849 with self
.subTest(before
=True, after
=False):
850 m
.process(["foo"], before
=True)
852 process
.assert_called_once_with("foo", steps
="before")
853 store_report
.assert_called_once()
854 log_error
.assert_not_called()
857 log_error
.reset_mock()
858 store_report
.reset_mock()
859 with self
.subTest(before
=False, after
=True):
860 m
.process(["sell_all"], after
=True)
862 process
.assert_called_once_with("sell_all", steps
="after")
863 store_report
.assert_called_once()
864 log_error
.assert_not_called()
867 log_error
.reset_mock()
868 store_report
.reset_mock()
869 with self
.subTest(before
=False, after
=False):
872 process
.assert_called_once_with("foo", steps
="all")
873 store_report
.assert_called_once()
874 log_error
.assert_not_called()
877 log_error
.reset_mock()
878 store_report
.reset_mock()
879 with self
.subTest(before
=True, after
=True):
880 m
.process(["sell_all"], before
=True, after
=True)
882 process
.assert_called_once_with("sell_all", steps
="all")
883 store_report
.assert_called_once()
884 log_error
.assert_not_called()
887 log_error
.reset_mock()
888 store_report
.reset_mock()
889 with self
.subTest(unhandled_exception
=True):
890 process
.side_effect
= Exception("bouh")
892 m
.process(["some_action"], before
=True)
893 log_error
.assert_called_with("market_process", exception
=mock
.ANY
)
894 store_report
.assert_called_once()
897 @unittest.skipUnless("unit" in limits
, "Unit skipped")
898 class ProcessorTest(WebMockTestCase
):
899 def test_values(self
):
900 processor
= market
.Processor(self
.m
)
902 self
.assertEqual(self
.m
, processor
.market
)
904 def test_run_action(self
):
905 processor
= market
.Processor(self
.m
)
907 with mock
.patch
.object(processor
, "parse_args") as parse_args
:
908 method_mock
= mock
.Mock()
909 parse_args
.return_value
= [method_mock
, { "foo": "bar" }
]
911 processor
.run_action("foo", "bar", "baz")
913 parse_args
.assert_called_with("foo", "bar", "baz")
915 method_mock
.assert_called_with(foo
="bar")
917 processor
.run_action("wait_for_recent", "bar", "baz")
919 method_mock
.assert_called_with(foo
="bar")
921 def test_select_step(self
):
922 processor
= market
.Processor(self
.m
)
924 scenario
= processor
.scenarios
["sell_all"]
926 self
.assertEqual(scenario
, processor
.select_steps(scenario
, "all"))
927 self
.assertEqual(["all_sell"], list(map(lambda x
: x
["name"], processor
.select_steps(scenario
, "before"))))
928 self
.assertEqual(["wait", "all_buy"], list(map(lambda x
: x
["name"], processor
.select_steps(scenario
, "after"))))
929 self
.assertEqual(["wait"], list(map(lambda x
: x
["name"], processor
.select_steps(scenario
, 2))))
930 self
.assertEqual(["wait"], list(map(lambda x
: x
["name"], processor
.select_steps(scenario
, "wait"))))
932 with self
.assertRaises(TypeError):
933 processor
.select_steps(scenario
, ["wait"])
935 def test_can_process(self
):
936 processor
= market
.Processor(self
.m
)
938 with self
.subTest(True):
939 self
.assertTrue(processor
.can_process("sell_all"))
941 with self
.subTest(False):
942 self
.assertFalse(processor
.can_process("unknown_action"))
944 @mock.patch("market.Processor.process_step")
945 def test_process(self
, process_step
):
946 with self
.subTest("unknown action"):
947 processor
= market
.Processor(self
.m
)
948 with self
.assertRaises(TypeError):
949 processor
.process("unknown_action")
951 with self
.subTest("nominal case"):
952 processor
= market
.Processor(self
.m
)
954 processor
.process("sell_all", foo
="bar")
955 self
.assertEqual(3, process_step
.call_count
)
957 steps
= list(map(lambda x
: x
[1][1]["name"], process_step
.mock_calls
))
958 scenario_names
= list(map(lambda x
: x
[1][0], process_step
.mock_calls
))
959 kwargs
= list(map(lambda x
: x
[1][2], process_step
.mock_calls
))
960 self
.assertEqual(["all_sell", "wait", "all_buy"], steps
)
961 self
.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names
)
962 self
.assertEqual([{"foo":"bar"}
, {"foo":"bar"}
, {"foo":"bar"}
], kwargs
)
964 process_step
.reset_mock()
966 processor
.process("sell_needed", steps
=["before", "after"])
967 self
.assertEqual(4, process_step
.call_count
)
969 def test_method_arguments(self
):
970 ccxt
= mock
.Mock(spec
=market
.ccxt
.poloniexE
)
971 m
= market
.Market(ccxt
, self
.market_args())
973 processor
= market
.Processor(m
)
975 method
, arguments
= processor
.method_arguments("wait_for_recent")
976 self
.assertEqual(market
.Portfolio
.wait_for_recent
, method
)
977 self
.assertEqual(["delta", "poll"], arguments
)
979 method
, arguments
= processor
.method_arguments("prepare_trades")
980 self
.assertEqual(m
.prepare_trades
, method
)
981 self
.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only', 'available_balance_only'], arguments
)
983 method
, arguments
= processor
.method_arguments("prepare_orders")
984 self
.assertEqual(m
.trades
.prepare_orders
, method
)
986 method
, arguments
= processor
.method_arguments("move_balances")
987 self
.assertEqual(m
.move_balances
, method
)
989 method
, arguments
= processor
.method_arguments("run_orders")
990 self
.assertEqual(m
.trades
.run_orders
, method
)
992 method
, arguments
= processor
.method_arguments("follow_orders")
993 self
.assertEqual(m
.follow_orders
, method
)
995 method
, arguments
= processor
.method_arguments("close_trades")
996 self
.assertEqual(m
.trades
.close_trades
, method
)
998 method
, arguments
= processor
.method_arguments("print_tickers")
999 self
.assertEqual(m
.print_tickers
, method
)
1001 def test_process_step(self
):
1002 processor
= market
.Processor(self
.m
)
1004 with mock
.patch
.object(processor
, "run_action") as run_action
:
1005 step
= processor
.scenarios
["sell_needed"][2]
1007 processor
.process_step("foo", step
, {"foo":"bar"}
)
1009 self
.m
.report
.log_stage
.assert_has_calls([
1010 mock
.call("process_foo__2_sell_begin"),
1011 mock
.call("process_foo__2_sell_end"),
1013 self
.m
.balances
.fetch_balances
.assert_has_calls([
1014 mock
.call(tag
="process_foo__2_sell_begin"),
1015 mock
.call(tag
="process_foo__2_sell_end"),
1018 self
.assertEqual(5, run_action
.call_count
)
1020 run_action
.assert_has_calls([
1021 mock
.call('prepare_trades', {}, {'foo': 'bar'}
),
1022 mock
.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}
, {'foo': 'bar'}
),
1023 mock
.call('run_orders', {}, {'foo': 'bar'}
),
1024 mock
.call('follow_orders', {}, {'foo': 'bar'}
),
1025 mock
.call('close_trades', {}, {'foo': 'bar'}
),
1029 with mock
.patch
.object(processor
, "run_action") as run_action
:
1030 step
= processor
.scenarios
["sell_needed"][0]
1032 processor
.process_step("foo", step
, {"foo":"bar"}
)
1034 self
.m
.report
.log_stage
.assert_has_calls([
1035 mock
.call("process_foo__0_print_balances_begin"),
1036 mock
.call("process_foo__0_print_balances_end"),
1038 self
.m
.balances
.fetch_balances
.assert_has_calls([
1039 mock
.call(add_portfolio
=True, checkpoint
='end',
1041 tag
='process_foo__0_print_balances_begin')
1044 self
.assertEqual(0, run_action
.call_count
)
1047 with mock
.patch
.object(processor
, "run_action") as run_action
:
1048 step
= processor
.scenarios
["sell_needed"][1]
1050 processor
.process_step("foo", step
, {"foo":"bar"}
)
1051 self
.m
.balances
.fetch_balances
.assert_not_called()
1054 with mock
.patch
.object(processor
, "run_action") as run_action
:
1055 step
= processor
.scenarios
["print_balances"][0]
1057 processor
.process_step("foo", step
, {"foo":"bar"}
)
1058 self
.m
.balances
.fetch_balances
.assert_called_once_with(
1059 add_portfolio
=True, log_tickers
=True,
1060 tag
='process_foo__1_print_balances_begin')
1062 def test_parse_args(self
):
1063 processor
= market
.Processor(self
.m
)
1065 with mock
.patch
.object(processor
, "method_arguments") as method_arguments
:
1066 method_mock
= mock
.Mock()
1067 method_arguments
.return_value
= [
1071 method
, args
= processor
.parse_args("action", {"foo": "bar", "foo2": "bar"}
, {"foo": "bar2", "bla": "bla"}
)
1073 self
.assertEqual(method_mock
, method
)
1074 self
.assertEqual({"foo": "bar2", "foo2": "bar"}
, args
)
1076 with mock
.patch
.object(processor
, "method_arguments") as method_arguments
:
1077 method_mock
= mock
.Mock()
1078 method_arguments
.return_value
= [
1082 method
, args
= processor
.parse_args("action", {"repartition": { "base_currency": 1 }
}, {})
1084 self
.assertEqual(1, len(args
["repartition"]))
1085 self
.assertIn("BTC", args
["repartition"])
1087 with mock
.patch
.object(processor
, "method_arguments") as method_arguments
:
1088 method_mock
= mock
.Mock()
1089 method_arguments
.return_value
= [
1091 ["repartition", "base_currency"]
1093 method
, args
= processor
.parse_args("action", {"repartition": { "base_currency": 1 }
}, {"base_currency": "USDT"}
)
1095 self
.assertEqual(1, len(args
["repartition"]))
1096 self
.assertIn("USDT", args
["repartition"])
1098 with mock
.patch
.object(processor
, "method_arguments") as method_arguments
:
1099 method_mock
= mock
.Mock()
1100 method_arguments
.return_value
= [
1102 ["repartition", "base_currency"]
1104 method
, args
= processor
.parse_args("action", {"repartition": { "ETH": 1 }
}, {"base_currency": "USDT"}
)
1106 self
.assertEqual(1, len(args
["repartition"]))
1107 self
.assertIn("ETH", args
["repartition"])