]>
Commit | Line | Data |
---|---|---|
c682bdf4 IB |
1 | from .helper import * |
2 | import requests | |
3 | import datetime | |
4 | import threading | |
5 | import market, portfolio, store | |
6 | ||
3080f31d | 7 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
8 | class NoopLockTest(unittest.TestCase): |
9 | def test_with(self): | |
10 | noop_lock = store.NoopLock() | |
11 | with noop_lock: | |
12 | self.assertTrue(True) | |
13 | ||
3080f31d | 14 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
15 | class LockedVarTest(unittest.TestCase): |
16 | ||
17 | def test_values(self): | |
18 | locked_var = store.LockedVar("Foo") | |
19 | self.assertIsInstance(locked_var.lock, store.NoopLock) | |
20 | self.assertEqual("Foo", locked_var.val) | |
21 | ||
22 | def test_get(self): | |
23 | with self.subTest(desc="Normal case"): | |
24 | locked_var = store.LockedVar("Foo") | |
25 | self.assertEqual("Foo", locked_var.get()) | |
26 | with self.subTest(desc="Dict"): | |
27 | locked_var = store.LockedVar({"foo": "bar"}) | |
28 | self.assertEqual({"foo": "bar"}, locked_var.get()) | |
29 | self.assertEqual("bar", locked_var.get("foo")) | |
30 | self.assertIsNone(locked_var.get("other")) | |
31 | ||
32 | def test_set(self): | |
33 | locked_var = store.LockedVar("Foo") | |
34 | locked_var.set("Bar") | |
35 | self.assertEqual("Bar", locked_var.get()) | |
36 | ||
37 | def test__getattr(self): | |
38 | dummy = type('Dummy', (object,), {})() | |
39 | dummy.attribute = "Hey" | |
40 | ||
41 | locked_var = store.LockedVar(dummy) | |
42 | self.assertEqual("Hey", locked_var.attribute) | |
43 | with self.assertRaises(AttributeError): | |
44 | locked_var.other | |
45 | ||
46 | def test_start_lock(self): | |
47 | locked_var = store.LockedVar("Foo") | |
48 | locked_var.start_lock() | |
49 | self.assertEqual("lock", locked_var.lock.__class__.__name__) | |
50 | ||
51 | thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) | |
52 | thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) | |
53 | thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) | |
54 | ||
55 | with locked_var.lock: | |
56 | thread1.start() | |
57 | thread2.start() | |
58 | thread3.start() | |
59 | ||
60 | self.assertEqual("Foo", locked_var.val) | |
61 | thread1.join() | |
62 | thread2.join() | |
63 | thread3.join() | |
64 | self.assertEqual("Bar", locked_var.get()[0:3]) | |
65 | ||
3080f31d | 66 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
67 | class TradeStoreTest(WebMockTestCase): |
68 | def test_compute_trades(self): | |
69 | self.m.balances.currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] | |
70 | ||
71 | values_in_base = { | |
72 | "XMR": portfolio.Amount("BTC", D("0.9")), | |
73 | "DASH": portfolio.Amount("BTC", D("0.4")), | |
74 | "XVG": portfolio.Amount("BTC", D("-0.5")), | |
75 | "BTC": portfolio.Amount("BTC", D("0.5")), | |
76 | } | |
77 | new_repartition = { | |
78 | "DASH": portfolio.Amount("BTC", D("0.5")), | |
79 | "XVG": portfolio.Amount("BTC", D("0.1")), | |
80 | "BTC": portfolio.Amount("BTC", D("0.4")), | |
81 | "ETH": portfolio.Amount("BTC", D("0.3")), | |
82 | } | |
83 | side_effect = [ | |
84 | (True, 1), | |
85 | (False, 2), | |
86 | (False, 3), | |
87 | (True, 4), | |
88 | (True, 5) | |
89 | ] | |
90 | ||
91 | with mock.patch.object(market.TradeStore, "trade_if_matching") as trade_if_matching: | |
92 | trade_store = market.TradeStore(self.m) | |
93 | trade_if_matching.side_effect = side_effect | |
94 | ||
95 | trade_store.compute_trades(values_in_base, | |
96 | new_repartition, only="only") | |
97 | ||
98 | self.assertEqual(5, trade_if_matching.call_count) | |
99 | self.assertEqual(3, len(trade_store.all)) | |
100 | self.assertEqual([1, 4, 5], trade_store.all) | |
101 | self.m.report.log_trades.assert_called_with(side_effect, "only") | |
102 | ||
103 | def test_trade_if_matching(self): | |
104 | ||
105 | with self.subTest(only="nope"): | |
106 | trade_store = market.TradeStore(self.m) | |
107 | result = trade_store.trade_if_matching( | |
108 | portfolio.Amount("BTC", D("0")), | |
109 | portfolio.Amount("BTC", D("0.3")), | |
110 | "ETH", only="nope") | |
111 | self.assertEqual(False, result[0]) | |
112 | self.assertIsInstance(result[1], portfolio.Trade) | |
113 | ||
114 | with self.subTest(only=None): | |
115 | trade_store = market.TradeStore(self.m) | |
116 | result = trade_store.trade_if_matching( | |
117 | portfolio.Amount("BTC", D("0")), | |
118 | portfolio.Amount("BTC", D("0.3")), | |
119 | "ETH", only=None) | |
120 | self.assertEqual(True, result[0]) | |
121 | ||
122 | with self.subTest(only="acquire"): | |
123 | trade_store = market.TradeStore(self.m) | |
124 | result = trade_store.trade_if_matching( | |
125 | portfolio.Amount("BTC", D("0")), | |
126 | portfolio.Amount("BTC", D("0.3")), | |
127 | "ETH", only="acquire") | |
128 | self.assertEqual(True, result[0]) | |
129 | ||
130 | with self.subTest(only="dispose"): | |
131 | trade_store = market.TradeStore(self.m) | |
132 | result = trade_store.trade_if_matching( | |
133 | portfolio.Amount("BTC", D("0")), | |
134 | portfolio.Amount("BTC", D("0.3")), | |
135 | "ETH", only="dispose") | |
136 | self.assertEqual(False, result[0]) | |
137 | ||
138 | def test_prepare_orders(self): | |
139 | trade_store = market.TradeStore(self.m) | |
140 | ||
141 | trade_mock1 = mock.Mock() | |
142 | trade_mock2 = mock.Mock() | |
143 | trade_mock3 = mock.Mock() | |
144 | ||
145 | trade_mock1.prepare_order.return_value = 1 | |
146 | trade_mock2.prepare_order.return_value = 2 | |
147 | trade_mock3.prepare_order.return_value = 3 | |
148 | ||
149 | trade_mock1.pending = True | |
150 | trade_mock2.pending = True | |
151 | trade_mock3.pending = False | |
152 | ||
153 | trade_store.all.append(trade_mock1) | |
154 | trade_store.all.append(trade_mock2) | |
155 | trade_store.all.append(trade_mock3) | |
156 | ||
157 | trade_store.prepare_orders() | |
158 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | |
159 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | |
160 | trade_mock3.prepare_order.assert_not_called() | |
161 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "default") | |
162 | ||
163 | self.m.report.log_orders.reset_mock() | |
164 | ||
165 | trade_store.prepare_orders(compute_value="bla") | |
166 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | |
167 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | |
168 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "bla") | |
169 | ||
170 | trade_mock1.prepare_order.reset_mock() | |
171 | trade_mock2.prepare_order.reset_mock() | |
172 | self.m.report.log_orders.reset_mock() | |
173 | ||
174 | trade_mock1.action = "foo" | |
175 | trade_mock2.action = "bar" | |
176 | trade_store.prepare_orders(only="bar") | |
177 | trade_mock1.prepare_order.assert_not_called() | |
178 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | |
179 | self.m.report.log_orders.assert_called_once_with([2], "bar", "default") | |
180 | ||
181 | def test_print_all_with_order(self): | |
182 | trade_mock1 = mock.Mock() | |
183 | trade_mock2 = mock.Mock() | |
184 | trade_mock3 = mock.Mock() | |
185 | trade_store = market.TradeStore(self.m) | |
186 | trade_store.all = [trade_mock1, trade_mock2, trade_mock3] | |
187 | ||
188 | trade_store.print_all_with_order() | |
189 | ||
190 | trade_mock1.print_with_order.assert_called() | |
191 | trade_mock2.print_with_order.assert_called() | |
192 | trade_mock3.print_with_order.assert_called() | |
193 | ||
194 | def test_run_orders(self): | |
195 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: | |
196 | order_mock1 = mock.Mock() | |
197 | order_mock2 = mock.Mock() | |
198 | order_mock3 = mock.Mock() | |
199 | trade_store = market.TradeStore(self.m) | |
200 | ||
201 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | |
202 | ||
203 | trade_store.run_orders() | |
204 | ||
205 | all_orders.assert_called_with(state="pending") | |
206 | ||
207 | order_mock1.run.assert_called() | |
208 | order_mock2.run.assert_called() | |
209 | order_mock3.run.assert_called() | |
210 | ||
211 | self.m.report.log_stage.assert_called_with("run_orders") | |
212 | self.m.report.log_orders.assert_called_with([order_mock1, order_mock2, | |
213 | order_mock3]) | |
214 | ||
215 | def test_all_orders(self): | |
216 | trade_mock1 = mock.Mock() | |
217 | trade_mock2 = mock.Mock() | |
218 | ||
219 | order_mock1 = mock.Mock() | |
220 | order_mock2 = mock.Mock() | |
221 | order_mock3 = mock.Mock() | |
222 | ||
223 | trade_mock1.orders = [order_mock1, order_mock2] | |
224 | trade_mock2.orders = [order_mock3] | |
225 | ||
226 | order_mock1.status = "pending" | |
227 | order_mock2.status = "open" | |
228 | order_mock3.status = "open" | |
229 | ||
230 | trade_store = market.TradeStore(self.m) | |
231 | trade_store.all.append(trade_mock1) | |
232 | trade_store.all.append(trade_mock2) | |
233 | ||
234 | orders = trade_store.all_orders() | |
235 | self.assertEqual(3, len(orders)) | |
236 | ||
237 | open_orders = trade_store.all_orders(state="open") | |
238 | self.assertEqual(2, len(open_orders)) | |
239 | self.assertEqual([order_mock2, order_mock3], open_orders) | |
240 | ||
241 | def test_update_all_orders_status(self): | |
242 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: | |
243 | order_mock1 = mock.Mock() | |
244 | order_mock2 = mock.Mock() | |
245 | order_mock3 = mock.Mock() | |
246 | ||
247 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | |
248 | ||
249 | trade_store = market.TradeStore(self.m) | |
250 | ||
251 | trade_store.update_all_orders_status() | |
252 | all_orders.assert_called_with(state="open") | |
253 | ||
254 | order_mock1.get_status.assert_called() | |
255 | order_mock2.get_status.assert_called() | |
256 | order_mock3.get_status.assert_called() | |
257 | ||
258 | def test_close_trades(self): | |
259 | trade_mock1 = mock.Mock() | |
260 | trade_mock2 = mock.Mock() | |
261 | trade_mock3 = mock.Mock() | |
262 | ||
263 | trade_store = market.TradeStore(self.m) | |
264 | ||
265 | trade_store.all.append(trade_mock1) | |
266 | trade_store.all.append(trade_mock2) | |
267 | trade_store.all.append(trade_mock3) | |
268 | ||
269 | trade_store.close_trades() | |
270 | ||
271 | trade_mock1.close.assert_called_once_with() | |
272 | trade_mock2.close.assert_called_once_with() | |
273 | trade_mock3.close.assert_called_once_with() | |
274 | ||
275 | def test_pending(self): | |
276 | trade_mock1 = mock.Mock() | |
277 | trade_mock1.pending = True | |
278 | trade_mock2 = mock.Mock() | |
279 | trade_mock2.pending = True | |
280 | trade_mock3 = mock.Mock() | |
281 | trade_mock3.pending = False | |
282 | ||
283 | trade_store = market.TradeStore(self.m) | |
284 | ||
285 | trade_store.all.append(trade_mock1) | |
286 | trade_store.all.append(trade_mock2) | |
287 | trade_store.all.append(trade_mock3) | |
288 | ||
289 | self.assertEqual([trade_mock1, trade_mock2], trade_store.pending) | |
290 | ||
3080f31d | 291 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
292 | class BalanceStoreTest(WebMockTestCase): |
293 | def setUp(self): | |
294 | super().setUp() | |
295 | ||
296 | self.fetch_balance = { | |
297 | "ETC": { | |
298 | "exchange_free": 0, | |
299 | "exchange_used": 0, | |
300 | "exchange_total": 0, | |
301 | "margin_total": 0, | |
302 | }, | |
303 | "USDT": { | |
304 | "exchange_free": D("6.0"), | |
305 | "exchange_used": D("1.2"), | |
306 | "exchange_total": D("7.2"), | |
307 | "margin_total": 0, | |
308 | }, | |
309 | "XVG": { | |
310 | "exchange_free": 16, | |
311 | "exchange_used": 0, | |
312 | "exchange_total": 16, | |
313 | "margin_total": 0, | |
314 | }, | |
315 | "XMR": { | |
316 | "exchange_free": 0, | |
317 | "exchange_used": 0, | |
318 | "exchange_total": 0, | |
319 | "margin_total": D("-1.0"), | |
320 | "margin_free": 0, | |
321 | }, | |
322 | } | |
323 | ||
324 | def test_in_currency(self): | |
325 | self.m.get_ticker.return_value = { | |
326 | "bid": D("0.09"), | |
327 | "ask": D("0.11"), | |
328 | "average": D("0.1"), | |
329 | } | |
330 | ||
331 | balance_store = market.BalanceStore(self.m) | |
332 | balance_store.all = { | |
333 | "BTC": portfolio.Balance("BTC", { | |
334 | "total": "0.65", | |
335 | "exchange_total":"0.65", | |
336 | "exchange_free": "0.35", | |
337 | "exchange_used": "0.30"}), | |
338 | "ETH": portfolio.Balance("ETH", { | |
339 | "total": 3, | |
340 | "exchange_total": 3, | |
341 | "exchange_free": 3, | |
342 | "exchange_used": 0}), | |
343 | } | |
344 | ||
345 | amounts = balance_store.in_currency("BTC") | |
346 | self.assertEqual("BTC", amounts["ETH"].currency) | |
347 | self.assertEqual(D("0.65"), amounts["BTC"].value) | |
348 | self.assertEqual(D("0.30"), amounts["ETH"].value) | |
349 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | |
350 | "average", "total") | |
351 | self.m.report.log_tickers.reset_mock() | |
352 | ||
353 | amounts = balance_store.in_currency("BTC", compute_value="bid") | |
354 | self.assertEqual(D("0.65"), amounts["BTC"].value) | |
355 | self.assertEqual(D("0.27"), amounts["ETH"].value) | |
356 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | |
357 | "bid", "total") | |
358 | self.m.report.log_tickers.reset_mock() | |
359 | ||
360 | amounts = balance_store.in_currency("BTC", compute_value="bid", type="exchange_used") | |
361 | self.assertEqual(D("0.30"), amounts["BTC"].value) | |
362 | self.assertEqual(0, amounts["ETH"].value) | |
363 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | |
364 | "bid", "exchange_used") | |
365 | self.m.report.log_tickers.reset_mock() | |
366 | ||
367 | def test_fetch_balances(self): | |
368 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | |
369 | ||
370 | balance_store = market.BalanceStore(self.m) | |
371 | ||
2b1ee8f4 IB |
372 | with self.subTest(log_tickers=False): |
373 | balance_store.fetch_balances() | |
374 | self.assertNotIn("ETC", balance_store.currencies()) | |
375 | self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies())) | |
376 | ||
377 | balance_store.all["ETC"] = portfolio.Balance("ETC", { | |
378 | "exchange_total": "1", "exchange_free": "0", | |
379 | "exchange_used": "1" }) | |
380 | balance_store.fetch_balances(tag="foo") | |
381 | self.assertEqual(0, balance_store.all["ETC"].total) | |
382 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) | |
bb127bc8 | 383 | self.m.report.log_balances.assert_called_with(tag="foo", checkpoint=None) |
2b1ee8f4 IB |
384 | |
385 | with self.subTest(log_tickers=True),\ | |
386 | mock.patch.object(balance_store, "in_currency") as in_currency: | |
387 | in_currency.return_value = "tickers" | |
388 | balance_store.fetch_balances(log_tickers=True, ticker_currency="FOO", | |
389 | ticker_compute_value="compute", ticker_type="type") | |
390 | self.m.report.log_balances.assert_called_with(compute_value='compute', | |
bb127bc8 | 391 | tag=None, checkpoint=None, ticker_currency='FOO', tickers='tickers', |
2b1ee8f4 | 392 | type='type') |
c682bdf4 | 393 | |
9b697863 | 394 | balance_store = market.BalanceStore(self.m) |
9b697863 IB |
395 | with self.subTest(add_portfolio=True),\ |
396 | mock.patch.object(market.Portfolio, "repartition") as repartition: | |
397 | repartition.return_value = { | |
398 | "DOGE": D("0.5"), | |
399 | "USDT": D("0.5"), | |
400 | } | |
401 | balance_store.fetch_balances(add_portfolio=True) | |
402 | self.assertListEqual(["USDT", "XVG", "XMR", "DOGE"], list(balance_store.currencies())) | |
403 | ||
3a15ffc7 IB |
404 | self.m.ccxt.fetch_all_balances.return_value = { |
405 | "ETC": { | |
406 | "exchange_free": 0, | |
407 | "exchange_used": 0, | |
408 | "exchange_total": 0, | |
409 | "margin_total": 0, | |
410 | }, | |
411 | "XVG": { | |
412 | "exchange_free": 16, | |
413 | "exchange_used": 0, | |
414 | "exchange_total": 16, | |
415 | "margin_total": 0, | |
416 | }, | |
417 | "XMR": { | |
418 | "exchange_free": 0, | |
419 | "exchange_used": 0, | |
420 | "exchange_total": 0, | |
421 | "margin_total": D("-1.0"), | |
422 | "margin_free": 0, | |
423 | }, | |
424 | } | |
425 | ||
426 | balance_store = market.BalanceStore(self.m) | |
427 | with self.subTest(add_usdt=True),\ | |
428 | mock.patch.object(market.Portfolio, "repartition") as repartition: | |
429 | repartition.return_value = { | |
430 | "DOGE": D("0.5"), | |
431 | "ETH": D("0.5"), | |
432 | } | |
433 | balance_store.fetch_balances(add_usdt=True) | |
434 | self.assertListEqual(["XVG", "XMR", "USDT"], list(balance_store.currencies())) | |
9b697863 | 435 | |
3d6f74ee IB |
436 | @mock.patch.object(market.Portfolio, "repartition") |
437 | def test_available_balances_for_repartition(self, repartition): | |
438 | with self.subTest(available_balance_only=True): | |
439 | def _get_ticker(c1, c2): | |
440 | if c1 == "ZRC" and c2 == "BTC": | |
441 | return { "average": D("0.0001") } | |
442 | if c1 == "DOGE" and c2 == "BTC": | |
443 | return { "average": D("0.000001") } | |
444 | if c1 == "ETH" and c2 == "BTC": | |
445 | return { "average": D("0.1") } | |
446 | if c1 == "FOO" and c2 == "BTC": | |
447 | return { "average": D("0.1") } | |
448 | self.fail("Should not be called with {}, {}".format(c1, c2)) | |
449 | self.m.get_ticker.side_effect = _get_ticker | |
450 | ||
451 | repartition.return_value = { | |
452 | "DOGE": (D("0.20"), "short"), | |
453 | "BTC": (D("0.20"), "long"), | |
454 | "ETH": (D("0.20"), "long"), | |
455 | "XMR": (D("0.20"), "long"), | |
456 | "FOO": (D("0.20"), "long"), | |
457 | } | |
458 | self.m.ccxt.fetch_all_balances.return_value = { | |
459 | "ZRC": { | |
460 | "exchange_free": D("2.0"), | |
461 | "exchange_used": D("0.0"), | |
462 | "exchange_total": D("2.0"), | |
463 | "total": D("2.0") | |
464 | }, | |
465 | "DOGE": { | |
466 | "exchange_free": D("5.0"), | |
467 | "exchange_used": D("0.0"), | |
468 | "exchange_total": D("5.0"), | |
469 | "total": D("5.0") | |
470 | }, | |
471 | "BTC": { | |
472 | "exchange_free": D("0.065"), | |
473 | "exchange_used": D("0.02"), | |
474 | "exchange_total": D("0.085"), | |
475 | "margin_available": D("0.035"), | |
476 | "margin_in_position": D("0.01"), | |
477 | "margin_total": D("0.045"), | |
478 | "total": D("0.13") | |
479 | }, | |
480 | "ETH": { | |
481 | "exchange_free": D("1.0"), | |
482 | "exchange_used": D("0.0"), | |
483 | "exchange_total": D("1.0"), | |
484 | "total": D("1.0") | |
485 | }, | |
486 | "FOO": { | |
487 | "exchange_free": D("0.1"), | |
488 | "exchange_used": D("0.0"), | |
489 | "exchange_total": D("0.1"), | |
490 | "total": D("0.1"), | |
491 | }, | |
492 | } | |
493 | ||
494 | balance_store = market.BalanceStore(self.m) | |
495 | balance_store.fetch_balances() | |
496 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition() | |
497 | repartition.assert_called_with(liquidity="medium") | |
498 | self.assertEqual((D("0.20"), "short"), _repartition["DOGE"]) | |
499 | self.assertEqual((D("0.20"), "long"), _repartition["BTC"]) | |
500 | self.assertEqual((D("0.20"), "long"), _repartition["XMR"]) | |
501 | self.assertEqual((D("0.20"), "long"), _repartition["FOO"]) | |
502 | self.assertIsNone(_repartition.get("ETH")) | |
503 | self.assertEqual(portfolio.Amount("BTC", "0.1"), total_base_value) | |
504 | self.assertEqual(0, amount_in_position["DOGE"]) | |
505 | self.assertEqual(0, amount_in_position["BTC"]) | |
506 | self.assertEqual(0, amount_in_position["XMR"]) | |
507 | self.assertEqual(portfolio.Amount("BTC", "0.1"), amount_in_position["ETH"]) | |
508 | self.assertEqual(portfolio.Amount("BTC", "0.01"), amount_in_position["FOO"]) | |
509 | ||
510 | with self.subTest(available_balance_only=True, balance=0): | |
511 | def _get_ticker(c1, c2): | |
512 | if c1 == "ETH" and c2 == "BTC": | |
513 | return { "average": D("0.1") } | |
514 | self.fail("Should not be called with {}, {}".format(c1, c2)) | |
515 | self.m.get_ticker.side_effect = _get_ticker | |
516 | ||
517 | repartition.return_value = { | |
518 | "BTC": (D("0.5"), "long"), | |
519 | "ETH": (D("0.5"), "long"), | |
520 | } | |
521 | self.m.ccxt.fetch_all_balances.return_value = { | |
522 | "ETH": { | |
523 | "exchange_free": D("1.0"), | |
524 | "exchange_used": D("0.0"), | |
525 | "exchange_total": D("1.0"), | |
526 | "total": D("1.0") | |
527 | }, | |
528 | } | |
529 | ||
530 | balance_store = market.BalanceStore(self.m) | |
531 | balance_store.fetch_balances() | |
532 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(liquidity="high") | |
533 | ||
534 | repartition.assert_called_with(liquidity="high") | |
535 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | |
536 | self.assertIsNone(_repartition.get("ETH")) | |
537 | self.assertEqual(0, total_base_value) | |
538 | self.assertEqual(0, amount_in_position["BTC"]) | |
539 | self.assertEqual(0, amount_in_position["BTC"]) | |
540 | ||
541 | repartition.reset_mock() | |
542 | with self.subTest(available_balance_only=True, balance=0, | |
543 | repartition="present"): | |
544 | def _get_ticker(c1, c2): | |
545 | if c1 == "ETH" and c2 == "BTC": | |
546 | return { "average": D("0.1") } | |
547 | self.fail("Should not be called with {}, {}".format(c1, c2)) | |
548 | self.m.get_ticker.side_effect = _get_ticker | |
549 | ||
550 | _repartition = { | |
551 | "BTC": (D("0.5"), "long"), | |
552 | "ETH": (D("0.5"), "long"), | |
553 | } | |
554 | self.m.ccxt.fetch_all_balances.return_value = { | |
555 | "ETH": { | |
556 | "exchange_free": D("1.0"), | |
557 | "exchange_used": D("0.0"), | |
558 | "exchange_total": D("1.0"), | |
559 | "total": D("1.0") | |
560 | }, | |
561 | } | |
562 | ||
563 | balance_store = market.BalanceStore(self.m) | |
564 | balance_store.fetch_balances() | |
565 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(repartition=_repartition) | |
566 | repartition.assert_not_called() | |
567 | ||
568 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | |
569 | self.assertIsNone(_repartition.get("ETH")) | |
570 | self.assertEqual(0, total_base_value) | |
571 | self.assertEqual(0, amount_in_position["BTC"]) | |
572 | self.assertEqual(portfolio.Amount("BTC", "0.1"), amount_in_position["ETH"]) | |
573 | ||
574 | repartition.reset_mock() | |
575 | with self.subTest(available_balance_only=True, balance=0, | |
576 | repartition="present", base_currency="ETH"): | |
577 | def _get_ticker(c1, c2): | |
578 | if c1 == "ETH" and c2 == "BTC": | |
579 | return { "average": D("0.1") } | |
580 | self.fail("Should not be called with {}, {}".format(c1, c2)) | |
581 | self.m.get_ticker.side_effect = _get_ticker | |
582 | ||
583 | _repartition = { | |
584 | "BTC": (D("0.5"), "long"), | |
585 | "ETH": (D("0.5"), "long"), | |
586 | } | |
587 | self.m.ccxt.fetch_all_balances.return_value = { | |
588 | "ETH": { | |
589 | "exchange_free": D("1.0"), | |
590 | "exchange_used": D("0.0"), | |
591 | "exchange_total": D("1.0"), | |
592 | "total": D("1.0") | |
593 | }, | |
594 | } | |
595 | ||
596 | balance_store = market.BalanceStore(self.m) | |
597 | balance_store.fetch_balances() | |
598 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(repartition=_repartition, base_currency="ETH") | |
599 | ||
600 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | |
601 | self.assertEqual((D("0.5"), "long"), _repartition["ETH"]) | |
602 | self.assertEqual(portfolio.Amount("ETH", 1), total_base_value) | |
603 | self.assertEqual(0, amount_in_position["BTC"]) | |
604 | self.assertEqual(0, amount_in_position["ETH"]) | |
605 | ||
c682bdf4 IB |
606 | @mock.patch.object(market.Portfolio, "repartition") |
607 | def test_dispatch_assets(self, repartition): | |
608 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | |
609 | ||
610 | balance_store = market.BalanceStore(self.m) | |
611 | balance_store.fetch_balances() | |
612 | ||
613 | self.assertNotIn("XEM", balance_store.currencies()) | |
614 | ||
615 | repartition_hash = { | |
616 | "XEM": (D("0.75"), "long"), | |
617 | "BTC": (D("0.26"), "long"), | |
618 | "DASH": (D("0.10"), "short"), | |
619 | } | |
620 | repartition.return_value = repartition_hash | |
621 | ||
622 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) | |
623 | repartition.assert_called_with(liquidity="medium") | |
624 | self.assertIn("XEM", balance_store.currencies()) | |
625 | self.assertEqual(D("2.6"), amounts["BTC"].value) | |
626 | self.assertEqual(D("7.5"), amounts["XEM"].value) | |
627 | self.assertEqual(D("-1.0"), amounts["DASH"].value) | |
bb127bc8 | 628 | self.m.report.log_balances.assert_called_with(tag=None, checkpoint=None) |
c682bdf4 IB |
629 | self.m.report.log_dispatch.assert_called_once_with(portfolio.Amount("BTC", |
630 | "11.1"), amounts, "medium", repartition_hash) | |
631 | ||
632 | def test_currencies(self): | |
633 | balance_store = market.BalanceStore(self.m) | |
634 | ||
635 | balance_store.all = { | |
636 | "BTC": portfolio.Balance("BTC", { | |
637 | "total": "0.65", | |
638 | "exchange_total":"0.65", | |
639 | "exchange_free": "0.35", | |
640 | "exchange_used": "0.30"}), | |
641 | "ETH": portfolio.Balance("ETH", { | |
642 | "total": 3, | |
643 | "exchange_total": 3, | |
644 | "exchange_free": 3, | |
645 | "exchange_used": 0}), | |
646 | } | |
647 | self.assertListEqual(["BTC", "ETH"], list(balance_store.currencies())) | |
648 | ||
649 | def test_as_json(self): | |
650 | balance_mock1 = mock.Mock() | |
651 | balance_mock1.as_json.return_value = 1 | |
652 | ||
653 | balance_mock2 = mock.Mock() | |
654 | balance_mock2.as_json.return_value = 2 | |
655 | ||
656 | balance_store = market.BalanceStore(self.m) | |
657 | balance_store.all = { | |
658 | "BTC": balance_mock1, | |
659 | "ETH": balance_mock2, | |
660 | } | |
661 | ||
662 | as_json = balance_store.as_json() | |
663 | self.assertEqual(1, as_json["BTC"]) | |
664 | self.assertEqual(2, as_json["ETH"]) | |
665 | ||
3080f31d | 666 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
667 | class ReportStoreTest(WebMockTestCase): |
668 | def test_add_log(self): | |
e7d7c0e5 IB |
669 | with self.subTest(market=self.m): |
670 | self.m.user_id = 1 | |
671 | self.m.market_id = 3 | |
672 | report_store = market.ReportStore(self.m) | |
673 | result = report_store.add_log({"foo": "bar"}) | |
674 | ||
675 | self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": 1, "market_id": 3}, result) | |
676 | self.assertEqual(result, report_store.logs[0]) | |
677 | ||
678 | with self.subTest(market=None): | |
679 | report_store = market.ReportStore(None) | |
680 | result = report_store.add_log({"foo": "bar"}) | |
c682bdf4 | 681 | |
e7d7c0e5 | 682 | self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": None, "market_id": None}, result) |
c682bdf4 | 683 | |
1593c7a9 IB |
684 | def test_add_redis_status(self): |
685 | report_store = market.ReportStore(self.m) | |
686 | result = report_store.add_redis_status({"foo": "bar"}) | |
687 | ||
688 | self.assertEqual({"foo": "bar"}, result) | |
689 | self.assertEqual(result, report_store.redis_status[0]) | |
690 | ||
c682bdf4 IB |
691 | def test_set_verbose(self): |
692 | report_store = market.ReportStore(self.m) | |
693 | with self.subTest(verbose=True): | |
694 | report_store.set_verbose(True) | |
695 | self.assertTrue(report_store.verbose_print) | |
696 | ||
697 | with self.subTest(verbose=False): | |
698 | report_store.set_verbose(False) | |
699 | self.assertFalse(report_store.verbose_print) | |
700 | ||
701 | def test_merge(self): | |
e7d7c0e5 IB |
702 | self.m.user_id = 1 |
703 | self.m.market_id = 3 | |
c682bdf4 IB |
704 | report_store1 = market.ReportStore(self.m, verbose_print=False) |
705 | report_store2 = market.ReportStore(None, verbose_print=False) | |
706 | ||
707 | report_store2.log_stage("1") | |
708 | report_store1.log_stage("2") | |
709 | report_store2.log_stage("3") | |
710 | ||
711 | report_store1.merge(report_store2) | |
712 | ||
713 | self.assertEqual(3, len(report_store1.logs)) | |
714 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | |
715 | self.assertEqual(6, len(report_store1.print_logs)) | |
716 | ||
717 | def test_print_log(self): | |
718 | report_store = market.ReportStore(self.m) | |
719 | with self.subTest(verbose=True),\ | |
720 | mock.patch.object(store, "datetime") as time_mock,\ | |
721 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | |
e7d7c0e5 | 722 | time_mock.datetime.now.return_value = datetime.datetime(2018, 2, 25, 2, 20, 10) |
c682bdf4 IB |
723 | report_store.set_verbose(True) |
724 | report_store.print_log("Coucou") | |
725 | report_store.print_log(portfolio.Amount("BTC", 1)) | |
726 | self.assertEqual(stdout_mock.getvalue(), "2018-02-25 02:20:10: Coucou\n2018-02-25 02:20:10: 1.00000000 BTC\n") | |
727 | ||
728 | with self.subTest(verbose=False),\ | |
729 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | |
730 | report_store.set_verbose(False) | |
731 | report_store.print_log("Coucou") | |
732 | report_store.print_log(portfolio.Amount("BTC", 1)) | |
733 | self.assertEqual(stdout_mock.getvalue(), "") | |
734 | ||
735 | def test_default_json_serial(self): | |
736 | report_store = market.ReportStore(self.m) | |
737 | ||
738 | self.assertEqual("2018-02-24T00:00:00", | |
e7d7c0e5 | 739 | report_store.default_json_serial(portfolio.datetime.datetime(2018, 2, 24))) |
c682bdf4 IB |
740 | self.assertEqual("1.00000000 BTC", |
741 | report_store.default_json_serial(portfolio.Amount("BTC", 1))) | |
742 | ||
743 | def test_to_json(self): | |
744 | report_store = market.ReportStore(self.m) | |
745 | report_store.logs.append({"foo": "bar"}) | |
746 | self.assertEqual('[\n {\n "foo": "bar"\n }\n]', report_store.to_json()) | |
e7d7c0e5 | 747 | report_store.logs.append({"date": portfolio.datetime.datetime(2018, 2, 24)}) |
c682bdf4 IB |
748 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n }\n]', report_store.to_json()) |
749 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) | |
750 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json()) | |
751 | ||
752 | def test_to_json_array(self): | |
753 | report_store = market.ReportStore(self.m) | |
754 | report_store.logs.append({ | |
755 | "date": "date1", "type": "type1", "foo": "bar", "bla": "bla" | |
756 | }) | |
757 | report_store.logs.append({ | |
758 | "date": "date2", "type": "type2", "foo": "bar", "bla": "bla" | |
759 | }) | |
760 | logs = list(report_store.to_json_array()) | |
761 | ||
762 | self.assertEqual(2, len(logs)) | |
763 | self.assertEqual(("date1", "type1", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[0]) | |
764 | self.assertEqual(("date2", "type2", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[1]) | |
765 | ||
1593c7a9 IB |
766 | def test_to_json_redis(self): |
767 | report_store = market.ReportStore(self.m) | |
768 | report_store.redis_status.append({ | |
769 | "type": "type1", "foo": "bar", "bla": "bla" | |
770 | }) | |
771 | report_store.redis_status.append({ | |
772 | "type": "type2", "foo": "bar", "bla": "bla" | |
773 | }) | |
774 | logs = list(report_store.to_json_redis()) | |
775 | ||
776 | self.assertEqual(2, len(logs)) | |
777 | self.assertEqual(("type1", '{"foo": "bar", "bla": "bla"}'), logs[0]) | |
778 | self.assertEqual(("type2", '{"foo": "bar", "bla": "bla"}'), logs[1]) | |
779 | ||
c682bdf4 IB |
780 | @mock.patch.object(market.ReportStore, "print_log") |
781 | @mock.patch.object(market.ReportStore, "add_log") | |
782 | def test_log_stage(self, add_log, print_log): | |
783 | report_store = market.ReportStore(self.m) | |
784 | c = lambda x: x | |
785 | report_store.log_stage("foo", bar="baz", c=c, d=portfolio.Amount("BTC", 1)) | |
786 | print_log.assert_has_calls([ | |
787 | mock.call("-----------"), | |
788 | mock.call("[Stage] foo bar=baz, c=c = lambda x: x, d={'currency': 'BTC', 'value': Decimal('1')}"), | |
789 | ]) | |
790 | add_log.assert_called_once_with({ | |
791 | 'type': 'stage', | |
792 | 'stage': 'foo', | |
793 | 'args': { | |
794 | 'bar': 'baz', | |
795 | 'c': 'c = lambda x: x', | |
796 | 'd': { | |
797 | 'currency': 'BTC', | |
798 | 'value': D('1') | |
799 | } | |
800 | } | |
801 | }) | |
802 | ||
803 | @mock.patch.object(market.ReportStore, "print_log") | |
804 | @mock.patch.object(market.ReportStore, "add_log") | |
1593c7a9 IB |
805 | @mock.patch.object(market.ReportStore, "add_redis_status") |
806 | def test_log_balances(self, add_redis_status, add_log, print_log): | |
c682bdf4 IB |
807 | report_store = market.ReportStore(self.m) |
808 | self.m.balances.as_json.return_value = "json" | |
809 | self.m.balances.all = { "FOO": "bar", "BAR": "baz" } | |
810 | ||
2b1ee8f4 IB |
811 | with self.subTest(tickers=None): |
812 | report_store.log_balances(tag="tag") | |
813 | print_log.assert_has_calls([ | |
814 | mock.call("[Balance]"), | |
815 | mock.call("\tbar"), | |
816 | mock.call("\tbaz"), | |
817 | ]) | |
818 | add_log.assert_called_once_with({ | |
819 | 'type': 'balance', | |
bb127bc8 | 820 | 'checkpoint': None, |
2b1ee8f4 IB |
821 | 'balances': 'json', |
822 | 'tag': 'tag' | |
823 | }) | |
824 | add_redis_status.assert_called_once_with({ | |
825 | 'type': 'balance', | |
826 | 'balances': 'json', | |
bb127bc8 | 827 | 'checkpoint': None, |
2b1ee8f4 IB |
828 | 'tag': 'tag' |
829 | }) | |
830 | add_log.reset_mock() | |
831 | add_redis_status.reset_mock() | |
832 | with self.subTest(tickers="present"): | |
833 | amounts = { | |
834 | "BTC": portfolio.Amount("BTC", 10), | |
835 | "ETH": portfolio.Amount("BTC", D("0.3")) | |
836 | } | |
837 | amounts["ETH"].rate = D("0.1") | |
838 | ||
839 | report_store.log_balances(tag="tag", tickers=amounts, | |
840 | ticker_currency="BTC", compute_value="default", | |
841 | type="total") | |
842 | add_log.assert_called_once_with({ | |
843 | 'type': 'balance', | |
bb127bc8 | 844 | 'checkpoint': None, |
2b1ee8f4 IB |
845 | 'balances': 'json', |
846 | 'tag': 'tag', | |
847 | 'tickers': { | |
848 | 'compute_value': 'default', | |
849 | 'balance_type': 'total', | |
850 | 'currency': 'BTC', | |
851 | 'balances': { | |
852 | 'BTC': D('10'), | |
853 | 'ETH': D('0.3') | |
854 | }, | |
855 | 'rates': { | |
856 | 'BTC': None, | |
857 | 'ETH': D('0.1') | |
858 | }, | |
859 | 'total': D('10.3') | |
860 | }, | |
861 | }) | |
862 | add_redis_status.assert_called_once_with({ | |
863 | 'type': 'balance', | |
bb127bc8 | 864 | 'checkpoint': None, |
2b1ee8f4 IB |
865 | 'balances': 'json', |
866 | 'tag': 'tag', | |
867 | 'tickers': { | |
868 | 'compute_value': 'default', | |
869 | 'balance_type': 'total', | |
870 | 'currency': 'BTC', | |
871 | 'balances': { | |
872 | 'BTC': D('10'), | |
873 | 'ETH': D('0.3') | |
874 | }, | |
875 | 'rates': { | |
876 | 'BTC': None, | |
877 | 'ETH': D('0.1') | |
878 | }, | |
879 | 'total': D('10.3') | |
880 | }, | |
881 | }) | |
c682bdf4 IB |
882 | |
883 | @mock.patch.object(market.ReportStore, "print_log") | |
884 | @mock.patch.object(market.ReportStore, "add_log") | |
2b1ee8f4 | 885 | def test_log_tickers(self, add_log, print_log): |
c682bdf4 IB |
886 | report_store = market.ReportStore(self.m) |
887 | amounts = { | |
888 | "BTC": portfolio.Amount("BTC", 10), | |
889 | "ETH": portfolio.Amount("BTC", D("0.3")) | |
890 | } | |
891 | amounts["ETH"].rate = D("0.1") | |
892 | ||
893 | report_store.log_tickers(amounts, "BTC", "default", "total") | |
894 | print_log.assert_not_called() | |
895 | add_log.assert_called_once_with({ | |
896 | 'type': 'tickers', | |
897 | 'compute_value': 'default', | |
898 | 'balance_type': 'total', | |
899 | 'currency': 'BTC', | |
900 | 'balances': { | |
901 | 'BTC': D('10'), | |
902 | 'ETH': D('0.3') | |
903 | }, | |
904 | 'rates': { | |
905 | 'BTC': None, | |
906 | 'ETH': D('0.1') | |
907 | }, | |
908 | 'total': D('10.3') | |
909 | }) | |
910 | ||
911 | add_log.reset_mock() | |
912 | compute_value = lambda x: x["bid"] | |
913 | report_store.log_tickers(amounts, "BTC", compute_value, "total") | |
914 | add_log.assert_called_once_with({ | |
915 | 'type': 'tickers', | |
916 | 'compute_value': 'compute_value = lambda x: x["bid"]', | |
917 | 'balance_type': 'total', | |
918 | 'currency': 'BTC', | |
919 | 'balances': { | |
920 | 'BTC': D('10'), | |
921 | 'ETH': D('0.3') | |
922 | }, | |
923 | 'rates': { | |
924 | 'BTC': None, | |
925 | 'ETH': D('0.1') | |
926 | }, | |
927 | 'total': D('10.3') | |
928 | }) | |
929 | ||
930 | @mock.patch.object(market.ReportStore, "print_log") | |
931 | @mock.patch.object(market.ReportStore, "add_log") | |
932 | def test_log_dispatch(self, add_log, print_log): | |
933 | report_store = market.ReportStore(self.m) | |
934 | amount = portfolio.Amount("BTC", "10.3") | |
935 | amounts = { | |
936 | "BTC": portfolio.Amount("BTC", 10), | |
937 | "ETH": portfolio.Amount("BTC", D("0.3")) | |
938 | } | |
939 | report_store.log_dispatch(amount, amounts, "medium", "repartition") | |
940 | print_log.assert_not_called() | |
941 | add_log.assert_called_once_with({ | |
942 | 'type': 'dispatch', | |
943 | 'liquidity': 'medium', | |
944 | 'repartition_ratio': 'repartition', | |
945 | 'total_amount': { | |
946 | 'currency': 'BTC', | |
947 | 'value': D('10.3') | |
948 | }, | |
949 | 'repartition': { | |
950 | 'BTC': D('10'), | |
951 | 'ETH': D('0.3') | |
952 | } | |
953 | }) | |
954 | ||
955 | @mock.patch.object(market.ReportStore, "print_log") | |
956 | @mock.patch.object(market.ReportStore, "add_log") | |
957 | def test_log_trades(self, add_log, print_log): | |
958 | report_store = market.ReportStore(self.m) | |
959 | trade_mock1 = mock.Mock() | |
960 | trade_mock2 = mock.Mock() | |
961 | trade_mock1.as_json.return_value = { "trade": "1" } | |
962 | trade_mock2.as_json.return_value = { "trade": "2" } | |
963 | ||
964 | matching_and_trades = [ | |
965 | (True, trade_mock1), | |
966 | (False, trade_mock2), | |
967 | ] | |
968 | report_store.log_trades(matching_and_trades, "only") | |
969 | ||
970 | print_log.assert_not_called() | |
971 | add_log.assert_called_with({ | |
972 | 'type': 'trades', | |
973 | 'only': 'only', | |
974 | 'debug': False, | |
975 | 'trades': [ | |
976 | {'trade': '1', 'skipped': False}, | |
977 | {'trade': '2', 'skipped': True} | |
978 | ] | |
979 | }) | |
980 | ||
981 | @mock.patch.object(market.ReportStore, "print_log") | |
982 | @mock.patch.object(market.ReportStore, "add_log") | |
983 | def test_log_orders(self, add_log, print_log): | |
984 | report_store = market.ReportStore(self.m) | |
985 | ||
986 | order_mock1 = mock.Mock() | |
987 | order_mock2 = mock.Mock() | |
988 | ||
989 | order_mock1.as_json.return_value = "order1" | |
990 | order_mock2.as_json.return_value = "order2" | |
991 | ||
992 | orders = [order_mock1, order_mock2] | |
993 | ||
994 | report_store.log_orders(orders, tick="tick", | |
995 | only="only", compute_value="compute_value") | |
996 | ||
997 | print_log.assert_called_once_with("[Orders]") | |
998 | self.m.trades.print_all_with_order.assert_called_once_with(ind="\t") | |
999 | ||
1000 | add_log.assert_called_with({ | |
1001 | 'type': 'orders', | |
1002 | 'only': 'only', | |
1003 | 'compute_value': 'compute_value', | |
1004 | 'tick': 'tick', | |
1005 | 'orders': ['order1', 'order2'] | |
1006 | }) | |
1007 | ||
1008 | add_log.reset_mock() | |
1009 | def compute_value(x, y): | |
1010 | return x[y] | |
1011 | report_store.log_orders(orders, tick="tick", | |
1012 | only="only", compute_value=compute_value) | |
1013 | add_log.assert_called_with({ | |
1014 | 'type': 'orders', | |
1015 | 'only': 'only', | |
1016 | 'compute_value': 'def compute_value(x, y):\n return x[y]', | |
1017 | 'tick': 'tick', | |
1018 | 'orders': ['order1', 'order2'] | |
1019 | }) | |
1020 | ||
1021 | ||
1022 | @mock.patch.object(market.ReportStore, "print_log") | |
1023 | @mock.patch.object(market.ReportStore, "add_log") | |
1024 | def test_log_order(self, add_log, print_log): | |
1025 | report_store = market.ReportStore(self.m) | |
1026 | order_mock = mock.Mock() | |
1027 | order_mock.as_json.return_value = "order" | |
1028 | new_order_mock = mock.Mock() | |
1029 | new_order_mock.as_json.return_value = "new_order" | |
1030 | order_mock.__repr__ = mock.Mock() | |
1031 | order_mock.__repr__.return_value = "Order Mock" | |
1032 | new_order_mock.__repr__ = mock.Mock() | |
1033 | new_order_mock.__repr__.return_value = "New order Mock" | |
1034 | ||
1035 | with self.subTest(finished=True): | |
1036 | report_store.log_order(order_mock, 1, finished=True) | |
1037 | print_log.assert_called_once_with("[Order] Finished Order Mock") | |
1038 | add_log.assert_called_once_with({ | |
1039 | 'type': 'order', | |
1040 | 'tick': 1, | |
1041 | 'update': None, | |
1042 | 'order': 'order', | |
1043 | 'compute_value': None, | |
1044 | 'new_order': None | |
1045 | }) | |
1046 | ||
1047 | add_log.reset_mock() | |
1048 | print_log.reset_mock() | |
1049 | ||
1050 | with self.subTest(update="waiting"): | |
1051 | report_store.log_order(order_mock, 1, update="waiting") | |
1052 | print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting") | |
1053 | add_log.assert_called_once_with({ | |
1054 | 'type': 'order', | |
1055 | 'tick': 1, | |
1056 | 'update': 'waiting', | |
1057 | 'order': 'order', | |
1058 | 'compute_value': None, | |
1059 | 'new_order': None | |
1060 | }) | |
1061 | ||
1062 | add_log.reset_mock() | |
1063 | print_log.reset_mock() | |
1064 | with self.subTest(update="adjusting"): | |
1065 | compute_value = lambda x: (x["bid"] + x["ask"]*2)/3 | |
1066 | report_store.log_order(order_mock, 3, | |
1067 | update="adjusting", new_order=new_order_mock, | |
1068 | compute_value=compute_value) | |
1069 | print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") | |
1070 | add_log.assert_called_once_with({ | |
1071 | 'type': 'order', | |
1072 | 'tick': 3, | |
1073 | 'update': 'adjusting', | |
1074 | 'order': 'order', | |
1075 | 'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3', | |
1076 | 'new_order': 'new_order' | |
1077 | }) | |
1078 | ||
1079 | add_log.reset_mock() | |
1080 | print_log.reset_mock() | |
1081 | with self.subTest(update="market_fallback"): | |
1082 | report_store.log_order(order_mock, 7, | |
1083 | update="market_fallback", new_order=new_order_mock) | |
1084 | print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value") | |
1085 | add_log.assert_called_once_with({ | |
1086 | 'type': 'order', | |
1087 | 'tick': 7, | |
1088 | 'update': 'market_fallback', | |
1089 | 'order': 'order', | |
1090 | 'compute_value': None, | |
1091 | 'new_order': 'new_order' | |
1092 | }) | |
1093 | ||
1094 | add_log.reset_mock() | |
1095 | print_log.reset_mock() | |
1096 | with self.subTest(update="market_adjusting"): | |
1097 | report_store.log_order(order_mock, 17, | |
1098 | update="market_adjust", new_order=new_order_mock) | |
1099 | print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock") | |
1100 | add_log.assert_called_once_with({ | |
1101 | 'type': 'order', | |
1102 | 'tick': 17, | |
1103 | 'update': 'market_adjust', | |
1104 | 'order': 'order', | |
1105 | 'compute_value': None, | |
1106 | 'new_order': 'new_order' | |
1107 | }) | |
1108 | ||
1109 | @mock.patch.object(market.ReportStore, "print_log") | |
1110 | @mock.patch.object(market.ReportStore, "add_log") | |
1111 | def test_log_move_balances(self, add_log, print_log): | |
1112 | report_store = market.ReportStore(self.m) | |
1113 | needed = { | |
1114 | "BTC": portfolio.Amount("BTC", 10), | |
1115 | "USDT": 1 | |
1116 | } | |
1117 | moving = { | |
1118 | "BTC": portfolio.Amount("BTC", 3), | |
1119 | "USDT": -2 | |
1120 | } | |
1121 | report_store.log_move_balances(needed, moving) | |
1122 | print_log.assert_not_called() | |
1123 | add_log.assert_called_once_with({ | |
1124 | 'type': 'move_balances', | |
1125 | 'debug': False, | |
1126 | 'needed': { | |
1127 | 'BTC': D('10'), | |
1128 | 'USDT': 1 | |
1129 | }, | |
1130 | 'moving': { | |
1131 | 'BTC': D('3'), | |
1132 | 'USDT': -2 | |
1133 | } | |
1134 | }) | |
1135 | ||
e7d7c0e5 IB |
1136 | def test_log_http_request(self): |
1137 | with mock.patch.object(market.ReportStore, "add_log") as add_log: | |
1138 | report_store = market.ReportStore(self.m) | |
1139 | response = mock.Mock() | |
1140 | response.status_code = 200 | |
1141 | response.text = "Hey" | |
9fe90554 | 1142 | response.elapsed.total_seconds.return_value = 120 |
c682bdf4 | 1143 | |
e7d7c0e5 IB |
1144 | report_store.log_http_request("method", "url", "body", |
1145 | "headers", response) | |
1146 | add_log.assert_called_once_with({ | |
1147 | 'type': 'http_request', | |
1148 | 'method': 'method', | |
1149 | 'url': 'url', | |
1150 | 'body': 'body', | |
1151 | 'headers': 'headers', | |
1152 | 'status': 200, | |
9fe90554 | 1153 | 'duration': 120, |
e7d7c0e5 IB |
1154 | 'response': 'Hey', |
1155 | 'response_same_as': None, | |
1156 | }) | |
c682bdf4 | 1157 | |
e7d7c0e5 IB |
1158 | add_log.reset_mock() |
1159 | report_store.log_http_request("method", "url", "body", | |
1160 | "headers", ValueError("Foo")) | |
1161 | add_log.assert_called_once_with({ | |
1162 | 'type': 'http_request', | |
1163 | 'method': 'method', | |
1164 | 'url': 'url', | |
1165 | 'body': 'body', | |
1166 | 'headers': 'headers', | |
1167 | 'status': -1, | |
1168 | 'response': None, | |
1169 | 'error': 'ValueError', | |
1170 | 'error_message': 'Foo', | |
1171 | }) | |
1172 | ||
1173 | with self.subTest(no_http_dup=True, duplicate=True): | |
1174 | self.m.user_id = 1 | |
1175 | self.m.market_id = 3 | |
1176 | report_store = market.ReportStore(self.m, no_http_dup=True) | |
1177 | original_add_log = report_store.add_log | |
1178 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | |
1179 | report_store.log_http_request("method", "url", "body", | |
1180 | "headers", response) | |
1181 | report_store.log_http_request("method", "url", "body", | |
1182 | "headers", response) | |
1183 | self.assertEqual(2, add_log.call_count) | |
1184 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | |
1185 | self.assertIsNone(add_log.mock_calls[1][1][0]["response"]) | |
1186 | self.assertEqual(add_log.mock_calls[0][1][0]["date"], add_log.mock_calls[1][1][0]["response_same_as"]) | |
1187 | with self.subTest(no_http_dup=True, duplicate=False, case="Different call"): | |
1188 | self.m.user_id = 1 | |
1189 | self.m.market_id = 3 | |
1190 | report_store = market.ReportStore(self.m, no_http_dup=True) | |
1191 | original_add_log = report_store.add_log | |
1192 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | |
1193 | report_store.log_http_request("method", "url", "body", | |
1194 | "headers", response) | |
1195 | report_store.log_http_request("method2", "url", "body", | |
1196 | "headers", response) | |
1197 | self.assertEqual(2, add_log.call_count) | |
1198 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | |
1199 | self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"]) | |
1200 | with self.subTest(no_http_dup=True, duplicate=False, case="Call inbetween"): | |
1201 | self.m.user_id = 1 | |
1202 | self.m.market_id = 3 | |
1203 | report_store = market.ReportStore(self.m, no_http_dup=True) | |
1204 | original_add_log = report_store.add_log | |
1205 | ||
1206 | response2 = mock.Mock() | |
1207 | response2.status_code = 200 | |
1208 | response2.text = "Hey there!" | |
1209 | ||
1210 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | |
1211 | report_store.log_http_request("method", "url", "body", | |
1212 | "headers", response) | |
1213 | report_store.log_http_request("method", "url", "body", | |
1214 | "headers", response2) | |
1215 | report_store.log_http_request("method", "url", "body", | |
1216 | "headers", response) | |
1217 | self.assertEqual(3, add_log.call_count) | |
1218 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | |
1219 | self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"]) | |
1220 | self.assertIsNone(add_log.mock_calls[2][1][0]["response_same_as"]) | |
c682bdf4 IB |
1221 | |
1222 | @mock.patch.object(market.ReportStore, "add_log") | |
1223 | def test_log_market(self, add_log): | |
1224 | report_store = market.ReportStore(self.m) | |
1225 | ||
e7d7c0e5 | 1226 | report_store.log_market(self.market_args(debug=True, quiet=False)) |
c682bdf4 IB |
1227 | add_log.assert_called_once_with({ |
1228 | "type": "market", | |
1229 | "commit": "$Format:%H$", | |
1230 | "args": { "report_path": None, "debug": True, "quiet": False }, | |
c682bdf4 IB |
1231 | }) |
1232 | ||
1233 | @mock.patch.object(market.ReportStore, "print_log") | |
1234 | @mock.patch.object(market.ReportStore, "add_log") | |
1235 | def test_log_error(self, add_log, print_log): | |
1236 | report_store = market.ReportStore(self.m) | |
1237 | with self.subTest(message=None, exception=None): | |
1238 | report_store.log_error("action") | |
1239 | print_log.assert_called_once_with("[Error] action") | |
1240 | add_log.assert_called_once_with({ | |
1241 | 'type': 'error', | |
1242 | 'action': 'action', | |
1243 | 'exception_class': None, | |
1244 | 'exception_message': None, | |
1245 | 'message': None | |
1246 | }) | |
1247 | ||
1248 | print_log.reset_mock() | |
1249 | add_log.reset_mock() | |
1250 | with self.subTest(message="Hey", exception=None): | |
1251 | report_store.log_error("action", message="Hey") | |
1252 | print_log.assert_has_calls([ | |
1253 | mock.call("[Error] action"), | |
1254 | mock.call("\tHey") | |
1255 | ]) | |
1256 | add_log.assert_called_once_with({ | |
1257 | 'type': 'error', | |
1258 | 'action': 'action', | |
1259 | 'exception_class': None, | |
1260 | 'exception_message': None, | |
1261 | 'message': "Hey" | |
1262 | }) | |
1263 | ||
1264 | print_log.reset_mock() | |
1265 | add_log.reset_mock() | |
1266 | with self.subTest(message=None, exception=Exception("bouh")): | |
1267 | report_store.log_error("action", exception=Exception("bouh")) | |
1268 | print_log.assert_has_calls([ | |
1269 | mock.call("[Error] action"), | |
1270 | mock.call("\tException: bouh") | |
1271 | ]) | |
1272 | add_log.assert_called_once_with({ | |
1273 | 'type': 'error', | |
1274 | 'action': 'action', | |
1275 | 'exception_class': "Exception", | |
1276 | 'exception_message': "bouh", | |
1277 | 'message': None | |
1278 | }) | |
1279 | ||
1280 | print_log.reset_mock() | |
1281 | add_log.reset_mock() | |
1282 | with self.subTest(message="Hey", exception=Exception("bouh")): | |
1283 | report_store.log_error("action", message="Hey", exception=Exception("bouh")) | |
1284 | print_log.assert_has_calls([ | |
1285 | mock.call("[Error] action"), | |
1286 | mock.call("\tException: bouh"), | |
1287 | mock.call("\tHey") | |
1288 | ]) | |
1289 | add_log.assert_called_once_with({ | |
1290 | 'type': 'error', | |
1291 | 'action': 'action', | |
1292 | 'exception_class': "Exception", | |
1293 | 'exception_message': "bouh", | |
1294 | 'message': "Hey" | |
1295 | }) | |
1296 | ||
1297 | @mock.patch.object(market.ReportStore, "print_log") | |
1298 | @mock.patch.object(market.ReportStore, "add_log") | |
1299 | def test_log_debug_action(self, add_log, print_log): | |
1300 | report_store = market.ReportStore(self.m) | |
1301 | report_store.log_debug_action("Hey") | |
1302 | ||
1303 | print_log.assert_called_once_with("[Debug] Hey") | |
1304 | add_log.assert_called_once_with({ | |
1305 | 'type': 'debug_action', | |
1306 | 'action': 'Hey' | |
1307 | }) | |
1308 | ||
3080f31d | 1309 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
c682bdf4 IB |
1310 | class PortfolioTest(WebMockTestCase): |
1311 | def setUp(self): | |
1312 | super().setUp() | |
1313 | ||
1314 | with open("test_samples/test_portfolio.json") as example: | |
1315 | self.json_response = example.read() | |
1316 | ||
1317 | self.wm.get(market.Portfolio.URL, text=self.json_response) | |
1318 | ||
1319 | @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") | |
40d0fa27 IB |
1320 | @mock.patch.object(market.Portfolio, "store_cryptoportfolio") |
1321 | def test_get_cryptoportfolio(self, store_cryptoportfolio, parse_cryptoportfolio): | |
c682bdf4 IB |
1322 | with self.subTest(parallel=False): |
1323 | self.wm.get(market.Portfolio.URL, [ | |
1324 | {"text":'{ "foo": "bar" }', "status_code": 200}, | |
1325 | {"text": "System Error", "status_code": 500}, | |
1326 | {"exc": requests.exceptions.ConnectTimeout}, | |
1327 | ]) | |
1328 | market.Portfolio.get_cryptoportfolio() | |
1329 | self.assertIn("foo", market.Portfolio.data.get()) | |
1330 | self.assertEqual("bar", market.Portfolio.data.get()["foo"]) | |
1331 | self.assertTrue(self.wm.called) | |
1332 | self.assertEqual(1, self.wm.call_count) | |
1333 | market.Portfolio.report.log_error.assert_not_called() | |
1334 | market.Portfolio.report.log_http_request.assert_called_once() | |
1335 | parse_cryptoportfolio.assert_called_once_with() | |
40d0fa27 | 1336 | store_cryptoportfolio.assert_called_once_with() |
c682bdf4 IB |
1337 | market.Portfolio.report.log_http_request.reset_mock() |
1338 | parse_cryptoportfolio.reset_mock() | |
40d0fa27 | 1339 | store_cryptoportfolio.reset_mock() |
c682bdf4 IB |
1340 | market.Portfolio.data = store.LockedVar(None) |
1341 | ||
1342 | market.Portfolio.get_cryptoportfolio() | |
1343 | self.assertIsNone(market.Portfolio.data.get()) | |
1344 | self.assertEqual(2, self.wm.call_count) | |
1345 | parse_cryptoportfolio.assert_not_called() | |
40d0fa27 | 1346 | store_cryptoportfolio.assert_not_called() |
c682bdf4 IB |
1347 | market.Portfolio.report.log_error.assert_not_called() |
1348 | market.Portfolio.report.log_http_request.assert_called_once() | |
1349 | market.Portfolio.report.log_http_request.reset_mock() | |
1350 | parse_cryptoportfolio.reset_mock() | |
40d0fa27 | 1351 | store_cryptoportfolio.reset_mock() |
c682bdf4 IB |
1352 | |
1353 | market.Portfolio.data = store.LockedVar("Foo") | |
1354 | market.Portfolio.get_cryptoportfolio() | |
1355 | self.assertEqual(2, self.wm.call_count) | |
1356 | parse_cryptoportfolio.assert_not_called() | |
40d0fa27 | 1357 | store_cryptoportfolio.assert_not_called() |
c682bdf4 IB |
1358 | |
1359 | market.Portfolio.get_cryptoportfolio(refetch=True) | |
1360 | self.assertEqual("Foo", market.Portfolio.data.get()) | |
1361 | self.assertEqual(3, self.wm.call_count) | |
1362 | market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", | |
1363 | exception=mock.ANY) | |
1364 | market.Portfolio.report.log_http_request.assert_not_called() | |
1365 | with self.subTest(parallel=True): | |
1366 | with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ | |
1367 | mock.patch.object(market.Portfolio, "notify_and_wait") as notify: | |
1368 | with self.subTest(worker=True): | |
1369 | market.Portfolio.data = store.LockedVar(None) | |
1370 | market.Portfolio.worker = mock.Mock() | |
1371 | is_worker.return_value = True | |
1372 | self.wm.get(market.Portfolio.URL, [ | |
1373 | {"text":'{ "foo": "bar" }', "status_code": 200}, | |
1374 | ]) | |
1375 | market.Portfolio.get_cryptoportfolio() | |
1376 | self.assertIn("foo", market.Portfolio.data.get()) | |
1377 | parse_cryptoportfolio.reset_mock() | |
40d0fa27 | 1378 | store_cryptoportfolio.reset_mock() |
c682bdf4 IB |
1379 | with self.subTest(worker=False): |
1380 | market.Portfolio.data = store.LockedVar(None) | |
1381 | market.Portfolio.worker = mock.Mock() | |
1382 | is_worker.return_value = False | |
1383 | market.Portfolio.get_cryptoportfolio() | |
1384 | notify.assert_called_once_with() | |
1385 | parse_cryptoportfolio.assert_not_called() | |
40d0fa27 | 1386 | store_cryptoportfolio.assert_not_called() |
c682bdf4 IB |
1387 | |
1388 | def test_parse_cryptoportfolio(self): | |
1389 | with self.subTest(description="Normal case"): | |
1390 | market.Portfolio.data = store.LockedVar(store.json.loads( | |
1391 | self.json_response, parse_int=D, parse_float=D)) | |
1392 | market.Portfolio.parse_cryptoportfolio() | |
1393 | ||
1394 | self.assertListEqual( | |
1395 | ["medium", "high"], | |
1396 | list(market.Portfolio.liquidities.get().keys())) | |
1397 | ||
1398 | liquidities = market.Portfolio.liquidities.get() | |
1399 | self.assertEqual(10, len(liquidities["medium"].keys())) | |
1400 | self.assertEqual(10, len(liquidities["high"].keys())) | |
1401 | ||
1402 | expected = { | |
1403 | 'BTC': (D("0.2857"), "long"), | |
1404 | 'DGB': (D("0.1015"), "long"), | |
1405 | 'DOGE': (D("0.1805"), "long"), | |
1406 | 'SC': (D("0.0623"), "long"), | |
1407 | 'ZEC': (D("0.3701"), "long"), | |
1408 | } | |
e7d7c0e5 | 1409 | date = portfolio.datetime.datetime(2018, 1, 8) |
c682bdf4 IB |
1410 | self.assertDictEqual(expected, liquidities["high"][date]) |
1411 | ||
1412 | expected = { | |
1413 | 'BTC': (D("1.1102e-16"), "long"), | |
1414 | 'ETC': (D("0.1"), "long"), | |
1415 | 'FCT': (D("0.1"), "long"), | |
1416 | 'GAS': (D("0.1"), "long"), | |
1417 | 'NAV': (D("0.1"), "long"), | |
1418 | 'OMG': (D("0.1"), "long"), | |
1419 | 'OMNI': (D("0.1"), "long"), | |
1420 | 'PPC': (D("0.1"), "long"), | |
1421 | 'RIC': (D("0.1"), "long"), | |
1422 | 'VIA': (D("0.1"), "long"), | |
1423 | 'XCP': (D("0.1"), "long"), | |
1424 | } | |
1425 | self.assertDictEqual(expected, liquidities["medium"][date]) | |
e7d7c0e5 | 1426 | self.assertEqual(portfolio.datetime.datetime(2018, 1, 15), market.Portfolio.last_date.get()) |
c682bdf4 IB |
1427 | |
1428 | with self.subTest(description="Missing weight"): | |
1429 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | |
1430 | del(data["portfolio_2"]["weights"]) | |
1431 | market.Portfolio.data = store.LockedVar(data) | |
1432 | ||
1433 | market.Portfolio.parse_cryptoportfolio() | |
1434 | self.assertListEqual( | |
1435 | ["medium", "high"], | |
1436 | list(market.Portfolio.liquidities.get().keys())) | |
1437 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | |
1438 | ||
1439 | with self.subTest(description="All missing weights"): | |
1440 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | |
1441 | del(data["portfolio_1"]["weights"]) | |
1442 | del(data["portfolio_2"]["weights"]) | |
1443 | market.Portfolio.data = store.LockedVar(data) | |
1444 | ||
1445 | market.Portfolio.parse_cryptoportfolio() | |
1446 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | |
1447 | self.assertEqual({}, market.Portfolio.liquidities.get("high")) | |
1448 | self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) | |
1449 | ||
40d0fa27 IB |
1450 | @mock.patch.object(store.dbs, "redis_connected") |
1451 | @mock.patch.object(store.dbs, "redis") | |
1452 | def test_store_cryptoportfolio(self, redis, redis_connected): | |
1453 | store.Portfolio.liquidities = store.LockedVar({ | |
1454 | "medium": { | |
1455 | datetime.datetime(2018,3,1): "medium_2018-03-01", | |
1456 | datetime.datetime(2018,3,8): "medium_2018-03-08", | |
1457 | }, | |
1458 | "high": { | |
1459 | datetime.datetime(2018,3,1): "high_2018-03-01", | |
1460 | datetime.datetime(2018,3,8): "high_2018-03-08", | |
1461 | } | |
1462 | }) | |
1463 | store.Portfolio.last_date = store.LockedVar(datetime.datetime(2018,3,8)) | |
1464 | ||
1465 | with self.subTest(redis_connected=False): | |
1466 | redis_connected.return_value = False | |
1467 | store.Portfolio.store_cryptoportfolio() | |
1468 | redis.set.assert_not_called() | |
1469 | ||
1470 | with self.subTest(redis_connected=True): | |
1471 | redis_connected.return_value = True | |
1472 | store.Portfolio.store_cryptoportfolio() | |
1473 | redis.set.assert_has_calls([ | |
1474 | mock.call("/cryptoportfolio/repartition/latest", '{"medium": "medium_2018-03-08", "high": "high_2018-03-08"}'), | |
1475 | mock.call("/cryptoportfolio/repartition/date", "2018-03-08"), | |
1476 | ]) | |
c682bdf4 | 1477 | |
9b697863 IB |
1478 | @mock.patch.object(store.dbs, "redis_connected") |
1479 | @mock.patch.object(store.dbs, "redis") | |
1480 | def test_retrieve_cryptoportfolio(self, redis, redis_connected): | |
1481 | with self.subTest(redis_connected=False): | |
1482 | redis_connected.return_value = False | |
1483 | store.Portfolio.retrieve_cryptoportfolio() | |
1484 | redis.get.assert_not_called() | |
1485 | self.assertIsNone(store.Portfolio.data.get()) | |
1486 | ||
1487 | with self.subTest(redis_connected=True, value=None): | |
1488 | redis_connected.return_value = True | |
1489 | redis.get.return_value = None | |
1490 | store.Portfolio.retrieve_cryptoportfolio() | |
1491 | self.assertEqual(2, redis.get.call_count) | |
1492 | ||
1493 | redis.reset_mock() | |
1494 | with self.subTest(redis_connected=True, value="present"): | |
1495 | redis_connected.return_value = True | |
1496 | redis.get.side_effect = [ | |
1497 | b'{ "medium": "medium_repartition", "high": "high_repartition" }', | |
1498 | b"2018-03-08" | |
1499 | ] | |
1500 | store.Portfolio.retrieve_cryptoportfolio() | |
1501 | self.assertEqual(2, redis.get.call_count) | |
1502 | self.assertEqual(datetime.datetime(2018,3,8), store.Portfolio.last_date.get()) | |
1503 | self.assertEqual("", store.Portfolio.data.get()) | |
1504 | expected_liquidities = { | |
1505 | 'high': { datetime.datetime(2018, 3, 8): 'high_repartition' }, | |
1506 | 'medium': { datetime.datetime(2018, 3, 8): 'medium_repartition' }, | |
c682bdf4 | 1507 | } |
9b697863 | 1508 | self.assertEqual(expected_liquidities, store.Portfolio.liquidities.get()) |
c682bdf4 | 1509 | |
9b697863 IB |
1510 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") |
1511 | @mock.patch.object(market.Portfolio, "retrieve_cryptoportfolio") | |
1512 | def test_repartition(self, retrieve_cryptoportfolio, get_cryptoportfolio): | |
1513 | with self.subTest(from_cache=False): | |
1514 | market.Portfolio.liquidities = store.LockedVar({ | |
1515 | "medium": { | |
3d6f74ee IB |
1516 | "2018-03-01": ["medium_2018-03-01"], |
1517 | "2018-03-08": ["medium_2018-03-08"], | |
9b697863 IB |
1518 | }, |
1519 | "high": { | |
3d6f74ee IB |
1520 | "2018-03-01": ["high_2018-03-01"], |
1521 | "2018-03-08": ["high_2018-03-08"], | |
9b697863 IB |
1522 | } |
1523 | }) | |
1524 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | |
1525 | ||
3d6f74ee | 1526 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition()) |
9b697863 IB |
1527 | get_cryptoportfolio.assert_called_once_with() |
1528 | retrieve_cryptoportfolio.assert_not_called() | |
3d6f74ee IB |
1529 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition(liquidity="medium")) |
1530 | self.assertEqual(["high_2018-03-08"], market.Portfolio.repartition(liquidity="high")) | |
9b697863 IB |
1531 | |
1532 | retrieve_cryptoportfolio.reset_mock() | |
1533 | get_cryptoportfolio.reset_mock() | |
1534 | ||
1535 | with self.subTest(from_cache=True): | |
3d6f74ee | 1536 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition(from_cache=True)) |
9b697863 IB |
1537 | get_cryptoportfolio.assert_called_once_with() |
1538 | retrieve_cryptoportfolio.assert_called_once_with() | |
c682bdf4 | 1539 | |
4b598ca6 IB |
1540 | retrieve_cryptoportfolio.reset_mock() |
1541 | get_cryptoportfolio.reset_mock() | |
1542 | ||
1543 | with self.subTest("absent liquidities"): | |
1544 | market.Portfolio.last_date = store.LockedVar("2018-03-15") | |
1545 | self.assertIsNone(market.Portfolio.repartition()) | |
1546 | ||
1547 | with self.subTest("no liquidities"): | |
1548 | market.Portfolio.liquidities = store.LockedVar({}) | |
1549 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | |
1550 | self.assertIsNone(market.Portfolio.repartition()) | |
1551 | ||
c682bdf4 IB |
1552 | @mock.patch.object(market.time, "sleep") |
1553 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | |
1554 | def test_wait_for_recent(self, get_cryptoportfolio, sleep): | |
1555 | self.call_count = 0 | |
1556 | def _get(refetch=False): | |
1557 | if self.call_count != 0: | |
1558 | self.assertTrue(refetch) | |
1559 | else: | |
1560 | self.assertFalse(refetch) | |
1561 | self.call_count += 1 | |
e7d7c0e5 IB |
1562 | market.Portfolio.last_date = store.LockedVar(store.datetime.datetime.now()\ |
1563 | - store.datetime.timedelta(10)\ | |
1564 | + store.datetime.timedelta(self.call_count)) | |
c682bdf4 IB |
1565 | get_cryptoportfolio.side_effect = _get |
1566 | ||
1567 | market.Portfolio.wait_for_recent() | |
1568 | sleep.assert_called_with(30) | |
1569 | self.assertEqual(6, sleep.call_count) | |
1570 | self.assertEqual(7, get_cryptoportfolio.call_count) | |
1571 | market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | |
1572 | ||
1573 | sleep.reset_mock() | |
1574 | get_cryptoportfolio.reset_mock() | |
1575 | market.Portfolio.last_date = store.LockedVar(None) | |
1576 | self.call_count = 0 | |
1577 | market.Portfolio.wait_for_recent(delta=15) | |
1578 | sleep.assert_not_called() | |
1579 | self.assertEqual(1, get_cryptoportfolio.call_count) | |
1580 | ||
1581 | sleep.reset_mock() | |
1582 | get_cryptoportfolio.reset_mock() | |
1583 | market.Portfolio.last_date = store.LockedVar(None) | |
1584 | self.call_count = 0 | |
1585 | market.Portfolio.wait_for_recent(delta=1) | |
1586 | sleep.assert_called_with(30) | |
1587 | self.assertEqual(9, sleep.call_count) | |
1588 | self.assertEqual(10, get_cryptoportfolio.call_count) | |
1589 | ||
1590 | def test_is_worker_thread(self): | |
1591 | with self.subTest(worker=None): | |
1592 | self.assertFalse(store.Portfolio.is_worker_thread()) | |
1593 | ||
1594 | with self.subTest(worker="not self"),\ | |
1595 | mock.patch("threading.current_thread") as current_thread: | |
1596 | current = mock.Mock() | |
1597 | current_thread.return_value = current | |
1598 | store.Portfolio.worker = mock.Mock() | |
1599 | self.assertFalse(store.Portfolio.is_worker_thread()) | |
1600 | ||
1601 | with self.subTest(worker="self"),\ | |
1602 | mock.patch("threading.current_thread") as current_thread: | |
1603 | current = mock.Mock() | |
1604 | current_thread.return_value = current | |
1605 | store.Portfolio.worker = current | |
1606 | self.assertTrue(store.Portfolio.is_worker_thread()) | |
1607 | ||
1608 | def test_start_worker(self): | |
1609 | with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: | |
1610 | store.Portfolio.start_worker() | |
1611 | notification.assert_called_once_with(poll=30) | |
1612 | ||
1613 | self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) | |
1614 | self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) | |
1615 | store.Portfolio.report.start_lock.assert_called_once_with() | |
1616 | ||
1617 | self.assertIsNotNone(store.Portfolio.worker) | |
1618 | self.assertIsNotNone(store.Portfolio.worker_notify) | |
1619 | self.assertIsNotNone(store.Portfolio.callback) | |
1620 | self.assertTrue(store.Portfolio.worker_started) | |
1621 | ||
1622 | self.assertFalse(store.Portfolio.worker.is_alive()) | |
e7d7c0e5 IB |
1623 | self.assertEqual(1, threading.active_count()) |
1624 | ||
1625 | def test_stop_worker(self): | |
1626 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | |
1627 | mock.patch.object(store.Portfolio, "report") as report,\ | |
1628 | mock.patch.object(store.time, "sleep") as sleep: | |
1629 | store.Portfolio.start_worker(poll=3) | |
1630 | store.Portfolio.stop_worker() | |
1631 | store.Portfolio.worker.join() | |
1632 | get.assert_not_called() | |
1633 | report.assert_not_called() | |
1634 | sleep.assert_not_called() | |
1635 | self.assertFalse(store.Portfolio.worker.is_alive()) | |
c682bdf4 IB |
1636 | |
1637 | def test_wait_for_notification(self): | |
1638 | with self.assertRaises(RuntimeError): | |
1639 | store.Portfolio.wait_for_notification() | |
1640 | ||
1641 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | |
1642 | mock.patch.object(store.Portfolio, "report") as report,\ | |
1643 | mock.patch.object(store.time, "sleep") as sleep: | |
1644 | store.Portfolio.start_worker(poll=3) | |
1645 | ||
1646 | store.Portfolio.worker_notify.set() | |
1647 | ||
1648 | store.Portfolio.callback.wait() | |
1649 | ||
a0dcf4e0 | 1650 | report.print_log.assert_called_once_with("[Worker] Fetching cryptoportfolio") |
c682bdf4 IB |
1651 | get.assert_called_once_with(refetch=True) |
1652 | sleep.assert_called_once_with(3) | |
1653 | self.assertFalse(store.Portfolio.worker_notify.is_set()) | |
1654 | self.assertTrue(store.Portfolio.worker.is_alive()) | |
1655 | ||
1656 | store.Portfolio.callback.clear() | |
1657 | store.Portfolio.worker_started = False | |
1658 | store.Portfolio.worker_notify.set() | |
e7d7c0e5 | 1659 | store.Portfolio.worker.join() |
c682bdf4 IB |
1660 | self.assertFalse(store.Portfolio.worker.is_alive()) |
1661 | ||
1662 | def test_notify_and_wait(self): | |
1663 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | |
1664 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | |
1665 | store.Portfolio.notify_and_wait() | |
1666 | callback.clear.assert_called_once_with() | |
1667 | worker_notify.set.assert_called_once_with() | |
1668 | callback.wait.assert_called_once_with() | |
1669 | ||
1670 |