diff options
Diffstat (limited to 'tests/test_store.py')
-rw-r--r-- | tests/test_store.py | 1268 |
1 files changed, 1268 insertions, 0 deletions
diff --git a/tests/test_store.py b/tests/test_store.py new file mode 100644 index 0000000..408c0e8 --- /dev/null +++ b/tests/test_store.py | |||
@@ -0,0 +1,1268 @@ | |||
1 | from .helper import * | ||
2 | import requests | ||
3 | import datetime | ||
4 | import threading | ||
5 | import market, portfolio, store | ||
6 | |||
7 | class NoopLockTest(unittest.TestCase): | ||
8 | def test_with(self): | ||
9 | noop_lock = store.NoopLock() | ||
10 | with noop_lock: | ||
11 | self.assertTrue(True) | ||
12 | |||
13 | class LockedVarTest(unittest.TestCase): | ||
14 | |||
15 | def test_values(self): | ||
16 | locked_var = store.LockedVar("Foo") | ||
17 | self.assertIsInstance(locked_var.lock, store.NoopLock) | ||
18 | self.assertEqual("Foo", locked_var.val) | ||
19 | |||
20 | def test_get(self): | ||
21 | with self.subTest(desc="Normal case"): | ||
22 | locked_var = store.LockedVar("Foo") | ||
23 | self.assertEqual("Foo", locked_var.get()) | ||
24 | with self.subTest(desc="Dict"): | ||
25 | locked_var = store.LockedVar({"foo": "bar"}) | ||
26 | self.assertEqual({"foo": "bar"}, locked_var.get()) | ||
27 | self.assertEqual("bar", locked_var.get("foo")) | ||
28 | self.assertIsNone(locked_var.get("other")) | ||
29 | |||
30 | def test_set(self): | ||
31 | locked_var = store.LockedVar("Foo") | ||
32 | locked_var.set("Bar") | ||
33 | self.assertEqual("Bar", locked_var.get()) | ||
34 | |||
35 | def test__getattr(self): | ||
36 | dummy = type('Dummy', (object,), {})() | ||
37 | dummy.attribute = "Hey" | ||
38 | |||
39 | locked_var = store.LockedVar(dummy) | ||
40 | self.assertEqual("Hey", locked_var.attribute) | ||
41 | with self.assertRaises(AttributeError): | ||
42 | locked_var.other | ||
43 | |||
44 | def test_start_lock(self): | ||
45 | locked_var = store.LockedVar("Foo") | ||
46 | locked_var.start_lock() | ||
47 | self.assertEqual("lock", locked_var.lock.__class__.__name__) | ||
48 | |||
49 | thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) | ||
50 | thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) | ||
51 | thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) | ||
52 | |||
53 | with locked_var.lock: | ||
54 | thread1.start() | ||
55 | thread2.start() | ||
56 | thread3.start() | ||
57 | |||
58 | self.assertEqual("Foo", locked_var.val) | ||
59 | thread1.join() | ||
60 | thread2.join() | ||
61 | thread3.join() | ||
62 | self.assertEqual("Bar", locked_var.get()[0:3]) | ||
63 | |||
64 | class TradeStoreTest(WebMockTestCase): | ||
65 | def test_compute_trades(self): | ||
66 | self.m.balances.currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] | ||
67 | |||
68 | values_in_base = { | ||
69 | "XMR": portfolio.Amount("BTC", D("0.9")), | ||
70 | "DASH": portfolio.Amount("BTC", D("0.4")), | ||
71 | "XVG": portfolio.Amount("BTC", D("-0.5")), | ||
72 | "BTC": portfolio.Amount("BTC", D("0.5")), | ||
73 | } | ||
74 | new_repartition = { | ||
75 | "DASH": portfolio.Amount("BTC", D("0.5")), | ||
76 | "XVG": portfolio.Amount("BTC", D("0.1")), | ||
77 | "BTC": portfolio.Amount("BTC", D("0.4")), | ||
78 | "ETH": portfolio.Amount("BTC", D("0.3")), | ||
79 | } | ||
80 | side_effect = [ | ||
81 | (True, 1), | ||
82 | (False, 2), | ||
83 | (False, 3), | ||
84 | (True, 4), | ||
85 | (True, 5) | ||
86 | ] | ||
87 | |||
88 | with mock.patch.object(market.TradeStore, "trade_if_matching") as trade_if_matching: | ||
89 | trade_store = market.TradeStore(self.m) | ||
90 | trade_if_matching.side_effect = side_effect | ||
91 | |||
92 | trade_store.compute_trades(values_in_base, | ||
93 | new_repartition, only="only") | ||
94 | |||
95 | self.assertEqual(5, trade_if_matching.call_count) | ||
96 | self.assertEqual(3, len(trade_store.all)) | ||
97 | self.assertEqual([1, 4, 5], trade_store.all) | ||
98 | self.m.report.log_trades.assert_called_with(side_effect, "only") | ||
99 | |||
100 | def test_trade_if_matching(self): | ||
101 | |||
102 | with self.subTest(only="nope"): | ||
103 | trade_store = market.TradeStore(self.m) | ||
104 | result = trade_store.trade_if_matching( | ||
105 | portfolio.Amount("BTC", D("0")), | ||
106 | portfolio.Amount("BTC", D("0.3")), | ||
107 | "ETH", only="nope") | ||
108 | self.assertEqual(False, result[0]) | ||
109 | self.assertIsInstance(result[1], portfolio.Trade) | ||
110 | |||
111 | with self.subTest(only=None): | ||
112 | trade_store = market.TradeStore(self.m) | ||
113 | result = trade_store.trade_if_matching( | ||
114 | portfolio.Amount("BTC", D("0")), | ||
115 | portfolio.Amount("BTC", D("0.3")), | ||
116 | "ETH", only=None) | ||
117 | self.assertEqual(True, result[0]) | ||
118 | |||
119 | with self.subTest(only="acquire"): | ||
120 | trade_store = market.TradeStore(self.m) | ||
121 | result = trade_store.trade_if_matching( | ||
122 | portfolio.Amount("BTC", D("0")), | ||
123 | portfolio.Amount("BTC", D("0.3")), | ||
124 | "ETH", only="acquire") | ||
125 | self.assertEqual(True, result[0]) | ||
126 | |||
127 | with self.subTest(only="dispose"): | ||
128 | trade_store = market.TradeStore(self.m) | ||
129 | result = trade_store.trade_if_matching( | ||
130 | portfolio.Amount("BTC", D("0")), | ||
131 | portfolio.Amount("BTC", D("0.3")), | ||
132 | "ETH", only="dispose") | ||
133 | self.assertEqual(False, result[0]) | ||
134 | |||
135 | def test_prepare_orders(self): | ||
136 | trade_store = market.TradeStore(self.m) | ||
137 | |||
138 | trade_mock1 = mock.Mock() | ||
139 | trade_mock2 = mock.Mock() | ||
140 | trade_mock3 = mock.Mock() | ||
141 | |||
142 | trade_mock1.prepare_order.return_value = 1 | ||
143 | trade_mock2.prepare_order.return_value = 2 | ||
144 | trade_mock3.prepare_order.return_value = 3 | ||
145 | |||
146 | trade_mock1.pending = True | ||
147 | trade_mock2.pending = True | ||
148 | trade_mock3.pending = False | ||
149 | |||
150 | trade_store.all.append(trade_mock1) | ||
151 | trade_store.all.append(trade_mock2) | ||
152 | trade_store.all.append(trade_mock3) | ||
153 | |||
154 | trade_store.prepare_orders() | ||
155 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | ||
156 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
157 | trade_mock3.prepare_order.assert_not_called() | ||
158 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "default") | ||
159 | |||
160 | self.m.report.log_orders.reset_mock() | ||
161 | |||
162 | trade_store.prepare_orders(compute_value="bla") | ||
163 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | ||
164 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | ||
165 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "bla") | ||
166 | |||
167 | trade_mock1.prepare_order.reset_mock() | ||
168 | trade_mock2.prepare_order.reset_mock() | ||
169 | self.m.report.log_orders.reset_mock() | ||
170 | |||
171 | trade_mock1.action = "foo" | ||
172 | trade_mock2.action = "bar" | ||
173 | trade_store.prepare_orders(only="bar") | ||
174 | trade_mock1.prepare_order.assert_not_called() | ||
175 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
176 | self.m.report.log_orders.assert_called_once_with([2], "bar", "default") | ||
177 | |||
178 | def test_print_all_with_order(self): | ||
179 | trade_mock1 = mock.Mock() | ||
180 | trade_mock2 = mock.Mock() | ||
181 | trade_mock3 = mock.Mock() | ||
182 | trade_store = market.TradeStore(self.m) | ||
183 | trade_store.all = [trade_mock1, trade_mock2, trade_mock3] | ||
184 | |||
185 | trade_store.print_all_with_order() | ||
186 | |||
187 | trade_mock1.print_with_order.assert_called() | ||
188 | trade_mock2.print_with_order.assert_called() | ||
189 | trade_mock3.print_with_order.assert_called() | ||
190 | |||
191 | def test_run_orders(self): | ||
192 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: | ||
193 | order_mock1 = mock.Mock() | ||
194 | order_mock2 = mock.Mock() | ||
195 | order_mock3 = mock.Mock() | ||
196 | trade_store = market.TradeStore(self.m) | ||
197 | |||
198 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
199 | |||
200 | trade_store.run_orders() | ||
201 | |||
202 | all_orders.assert_called_with(state="pending") | ||
203 | |||
204 | order_mock1.run.assert_called() | ||
205 | order_mock2.run.assert_called() | ||
206 | order_mock3.run.assert_called() | ||
207 | |||
208 | self.m.report.log_stage.assert_called_with("run_orders") | ||
209 | self.m.report.log_orders.assert_called_with([order_mock1, order_mock2, | ||
210 | order_mock3]) | ||
211 | |||
212 | def test_all_orders(self): | ||
213 | trade_mock1 = mock.Mock() | ||
214 | trade_mock2 = mock.Mock() | ||
215 | |||
216 | order_mock1 = mock.Mock() | ||
217 | order_mock2 = mock.Mock() | ||
218 | order_mock3 = mock.Mock() | ||
219 | |||
220 | trade_mock1.orders = [order_mock1, order_mock2] | ||
221 | trade_mock2.orders = [order_mock3] | ||
222 | |||
223 | order_mock1.status = "pending" | ||
224 | order_mock2.status = "open" | ||
225 | order_mock3.status = "open" | ||
226 | |||
227 | trade_store = market.TradeStore(self.m) | ||
228 | trade_store.all.append(trade_mock1) | ||
229 | trade_store.all.append(trade_mock2) | ||
230 | |||
231 | orders = trade_store.all_orders() | ||
232 | self.assertEqual(3, len(orders)) | ||
233 | |||
234 | open_orders = trade_store.all_orders(state="open") | ||
235 | self.assertEqual(2, len(open_orders)) | ||
236 | self.assertEqual([order_mock2, order_mock3], open_orders) | ||
237 | |||
238 | def test_update_all_orders_status(self): | ||
239 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: | ||
240 | order_mock1 = mock.Mock() | ||
241 | order_mock2 = mock.Mock() | ||
242 | order_mock3 = mock.Mock() | ||
243 | |||
244 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
245 | |||
246 | trade_store = market.TradeStore(self.m) | ||
247 | |||
248 | trade_store.update_all_orders_status() | ||
249 | all_orders.assert_called_with(state="open") | ||
250 | |||
251 | order_mock1.get_status.assert_called() | ||
252 | order_mock2.get_status.assert_called() | ||
253 | order_mock3.get_status.assert_called() | ||
254 | |||
255 | def test_close_trades(self): | ||
256 | trade_mock1 = mock.Mock() | ||
257 | trade_mock2 = mock.Mock() | ||
258 | trade_mock3 = mock.Mock() | ||
259 | |||
260 | trade_store = market.TradeStore(self.m) | ||
261 | |||
262 | trade_store.all.append(trade_mock1) | ||
263 | trade_store.all.append(trade_mock2) | ||
264 | trade_store.all.append(trade_mock3) | ||
265 | |||
266 | trade_store.close_trades() | ||
267 | |||
268 | trade_mock1.close.assert_called_once_with() | ||
269 | trade_mock2.close.assert_called_once_with() | ||
270 | trade_mock3.close.assert_called_once_with() | ||
271 | |||
272 | def test_pending(self): | ||
273 | trade_mock1 = mock.Mock() | ||
274 | trade_mock1.pending = True | ||
275 | trade_mock2 = mock.Mock() | ||
276 | trade_mock2.pending = True | ||
277 | trade_mock3 = mock.Mock() | ||
278 | trade_mock3.pending = False | ||
279 | |||
280 | trade_store = market.TradeStore(self.m) | ||
281 | |||
282 | trade_store.all.append(trade_mock1) | ||
283 | trade_store.all.append(trade_mock2) | ||
284 | trade_store.all.append(trade_mock3) | ||
285 | |||
286 | self.assertEqual([trade_mock1, trade_mock2], trade_store.pending) | ||
287 | |||
288 | class BalanceStoreTest(WebMockTestCase): | ||
289 | def setUp(self): | ||
290 | super().setUp() | ||
291 | |||
292 | self.fetch_balance = { | ||
293 | "ETC": { | ||
294 | "exchange_free": 0, | ||
295 | "exchange_used": 0, | ||
296 | "exchange_total": 0, | ||
297 | "margin_total": 0, | ||
298 | }, | ||
299 | "USDT": { | ||
300 | "exchange_free": D("6.0"), | ||
301 | "exchange_used": D("1.2"), | ||
302 | "exchange_total": D("7.2"), | ||
303 | "margin_total": 0, | ||
304 | }, | ||
305 | "XVG": { | ||
306 | "exchange_free": 16, | ||
307 | "exchange_used": 0, | ||
308 | "exchange_total": 16, | ||
309 | "margin_total": 0, | ||
310 | }, | ||
311 | "XMR": { | ||
312 | "exchange_free": 0, | ||
313 | "exchange_used": 0, | ||
314 | "exchange_total": 0, | ||
315 | "margin_total": D("-1.0"), | ||
316 | "margin_free": 0, | ||
317 | }, | ||
318 | } | ||
319 | |||
320 | def test_in_currency(self): | ||
321 | self.m.get_ticker.return_value = { | ||
322 | "bid": D("0.09"), | ||
323 | "ask": D("0.11"), | ||
324 | "average": D("0.1"), | ||
325 | } | ||
326 | |||
327 | balance_store = market.BalanceStore(self.m) | ||
328 | balance_store.all = { | ||
329 | "BTC": portfolio.Balance("BTC", { | ||
330 | "total": "0.65", | ||
331 | "exchange_total":"0.65", | ||
332 | "exchange_free": "0.35", | ||
333 | "exchange_used": "0.30"}), | ||
334 | "ETH": portfolio.Balance("ETH", { | ||
335 | "total": 3, | ||
336 | "exchange_total": 3, | ||
337 | "exchange_free": 3, | ||
338 | "exchange_used": 0}), | ||
339 | } | ||
340 | |||
341 | amounts = balance_store.in_currency("BTC") | ||
342 | self.assertEqual("BTC", amounts["ETH"].currency) | ||
343 | self.assertEqual(D("0.65"), amounts["BTC"].value) | ||
344 | self.assertEqual(D("0.30"), amounts["ETH"].value) | ||
345 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | ||
346 | "average", "total") | ||
347 | self.m.report.log_tickers.reset_mock() | ||
348 | |||
349 | amounts = balance_store.in_currency("BTC", compute_value="bid") | ||
350 | self.assertEqual(D("0.65"), amounts["BTC"].value) | ||
351 | self.assertEqual(D("0.27"), amounts["ETH"].value) | ||
352 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | ||
353 | "bid", "total") | ||
354 | self.m.report.log_tickers.reset_mock() | ||
355 | |||
356 | amounts = balance_store.in_currency("BTC", compute_value="bid", type="exchange_used") | ||
357 | self.assertEqual(D("0.30"), amounts["BTC"].value) | ||
358 | self.assertEqual(0, amounts["ETH"].value) | ||
359 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", | ||
360 | "bid", "exchange_used") | ||
361 | self.m.report.log_tickers.reset_mock() | ||
362 | |||
363 | def test_fetch_balances(self): | ||
364 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | ||
365 | |||
366 | balance_store = market.BalanceStore(self.m) | ||
367 | |||
368 | balance_store.fetch_balances() | ||
369 | self.assertNotIn("ETC", balance_store.currencies()) | ||
370 | self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies())) | ||
371 | |||
372 | balance_store.all["ETC"] = portfolio.Balance("ETC", { | ||
373 | "exchange_total": "1", "exchange_free": "0", | ||
374 | "exchange_used": "1" }) | ||
375 | balance_store.fetch_balances(tag="foo") | ||
376 | self.assertEqual(0, balance_store.all["ETC"].total) | ||
377 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) | ||
378 | self.m.report.log_balances.assert_called_with(tag="foo") | ||
379 | |||
380 | @mock.patch.object(market.Portfolio, "repartition") | ||
381 | def test_dispatch_assets(self, repartition): | ||
382 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | ||
383 | |||
384 | balance_store = market.BalanceStore(self.m) | ||
385 | balance_store.fetch_balances() | ||
386 | |||
387 | self.assertNotIn("XEM", balance_store.currencies()) | ||
388 | |||
389 | repartition_hash = { | ||
390 | "XEM": (D("0.75"), "long"), | ||
391 | "BTC": (D("0.26"), "long"), | ||
392 | "DASH": (D("0.10"), "short"), | ||
393 | } | ||
394 | repartition.return_value = repartition_hash | ||
395 | |||
396 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) | ||
397 | repartition.assert_called_with(liquidity="medium") | ||
398 | self.assertIn("XEM", balance_store.currencies()) | ||
399 | self.assertEqual(D("2.6"), amounts["BTC"].value) | ||
400 | self.assertEqual(D("7.5"), amounts["XEM"].value) | ||
401 | self.assertEqual(D("-1.0"), amounts["DASH"].value) | ||
402 | self.m.report.log_balances.assert_called_with(tag=None) | ||
403 | self.m.report.log_dispatch.assert_called_once_with(portfolio.Amount("BTC", | ||
404 | "11.1"), amounts, "medium", repartition_hash) | ||
405 | |||
406 | def test_currencies(self): | ||
407 | balance_store = market.BalanceStore(self.m) | ||
408 | |||
409 | balance_store.all = { | ||
410 | "BTC": portfolio.Balance("BTC", { | ||
411 | "total": "0.65", | ||
412 | "exchange_total":"0.65", | ||
413 | "exchange_free": "0.35", | ||
414 | "exchange_used": "0.30"}), | ||
415 | "ETH": portfolio.Balance("ETH", { | ||
416 | "total": 3, | ||
417 | "exchange_total": 3, | ||
418 | "exchange_free": 3, | ||
419 | "exchange_used": 0}), | ||
420 | } | ||
421 | self.assertListEqual(["BTC", "ETH"], list(balance_store.currencies())) | ||
422 | |||
423 | def test_as_json(self): | ||
424 | balance_mock1 = mock.Mock() | ||
425 | balance_mock1.as_json.return_value = 1 | ||
426 | |||
427 | balance_mock2 = mock.Mock() | ||
428 | balance_mock2.as_json.return_value = 2 | ||
429 | |||
430 | balance_store = market.BalanceStore(self.m) | ||
431 | balance_store.all = { | ||
432 | "BTC": balance_mock1, | ||
433 | "ETH": balance_mock2, | ||
434 | } | ||
435 | |||
436 | as_json = balance_store.as_json() | ||
437 | self.assertEqual(1, as_json["BTC"]) | ||
438 | self.assertEqual(2, as_json["ETH"]) | ||
439 | |||
440 | class ReportStoreTest(WebMockTestCase): | ||
441 | def test_add_log(self): | ||
442 | with self.subTest(market=self.m): | ||
443 | self.m.user_id = 1 | ||
444 | self.m.market_id = 3 | ||
445 | report_store = market.ReportStore(self.m) | ||
446 | result = report_store.add_log({"foo": "bar"}) | ||
447 | |||
448 | self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": 1, "market_id": 3}, result) | ||
449 | self.assertEqual(result, report_store.logs[0]) | ||
450 | |||
451 | with self.subTest(market=None): | ||
452 | report_store = market.ReportStore(None) | ||
453 | result = report_store.add_log({"foo": "bar"}) | ||
454 | |||
455 | self.assertEqual({"foo": "bar", "date": mock.ANY, "user_id": None, "market_id": None}, result) | ||
456 | |||
457 | def test_set_verbose(self): | ||
458 | report_store = market.ReportStore(self.m) | ||
459 | with self.subTest(verbose=True): | ||
460 | report_store.set_verbose(True) | ||
461 | self.assertTrue(report_store.verbose_print) | ||
462 | |||
463 | with self.subTest(verbose=False): | ||
464 | report_store.set_verbose(False) | ||
465 | self.assertFalse(report_store.verbose_print) | ||
466 | |||
467 | def test_merge(self): | ||
468 | self.m.user_id = 1 | ||
469 | self.m.market_id = 3 | ||
470 | report_store1 = market.ReportStore(self.m, verbose_print=False) | ||
471 | report_store2 = market.ReportStore(None, verbose_print=False) | ||
472 | |||
473 | report_store2.log_stage("1") | ||
474 | report_store1.log_stage("2") | ||
475 | report_store2.log_stage("3") | ||
476 | |||
477 | report_store1.merge(report_store2) | ||
478 | |||
479 | self.assertEqual(3, len(report_store1.logs)) | ||
480 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | ||
481 | self.assertEqual(6, len(report_store1.print_logs)) | ||
482 | |||
483 | def test_print_log(self): | ||
484 | report_store = market.ReportStore(self.m) | ||
485 | with self.subTest(verbose=True),\ | ||
486 | mock.patch.object(store, "datetime") as time_mock,\ | ||
487 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
488 | time_mock.datetime.now.return_value = datetime.datetime(2018, 2, 25, 2, 20, 10) | ||
489 | report_store.set_verbose(True) | ||
490 | report_store.print_log("Coucou") | ||
491 | report_store.print_log(portfolio.Amount("BTC", 1)) | ||
492 | self.assertEqual(stdout_mock.getvalue(), "2018-02-25 02:20:10: Coucou\n2018-02-25 02:20:10: 1.00000000 BTC\n") | ||
493 | |||
494 | with self.subTest(verbose=False),\ | ||
495 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
496 | report_store.set_verbose(False) | ||
497 | report_store.print_log("Coucou") | ||
498 | report_store.print_log(portfolio.Amount("BTC", 1)) | ||
499 | self.assertEqual(stdout_mock.getvalue(), "") | ||
500 | |||
501 | def test_default_json_serial(self): | ||
502 | report_store = market.ReportStore(self.m) | ||
503 | |||
504 | self.assertEqual("2018-02-24T00:00:00", | ||
505 | report_store.default_json_serial(portfolio.datetime.datetime(2018, 2, 24))) | ||
506 | self.assertEqual("1.00000000 BTC", | ||
507 | report_store.default_json_serial(portfolio.Amount("BTC", 1))) | ||
508 | |||
509 | def test_to_json(self): | ||
510 | report_store = market.ReportStore(self.m) | ||
511 | report_store.logs.append({"foo": "bar"}) | ||
512 | self.assertEqual('[\n {\n "foo": "bar"\n }\n]', report_store.to_json()) | ||
513 | report_store.logs.append({"date": portfolio.datetime.datetime(2018, 2, 24)}) | ||
514 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n }\n]', report_store.to_json()) | ||
515 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) | ||
516 | 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()) | ||
517 | |||
518 | def test_to_json_array(self): | ||
519 | report_store = market.ReportStore(self.m) | ||
520 | report_store.logs.append({ | ||
521 | "date": "date1", "type": "type1", "foo": "bar", "bla": "bla" | ||
522 | }) | ||
523 | report_store.logs.append({ | ||
524 | "date": "date2", "type": "type2", "foo": "bar", "bla": "bla" | ||
525 | }) | ||
526 | logs = list(report_store.to_json_array()) | ||
527 | |||
528 | self.assertEqual(2, len(logs)) | ||
529 | self.assertEqual(("date1", "type1", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[0]) | ||
530 | self.assertEqual(("date2", "type2", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[1]) | ||
531 | |||
532 | @mock.patch.object(market.ReportStore, "print_log") | ||
533 | @mock.patch.object(market.ReportStore, "add_log") | ||
534 | def test_log_stage(self, add_log, print_log): | ||
535 | report_store = market.ReportStore(self.m) | ||
536 | c = lambda x: x | ||
537 | report_store.log_stage("foo", bar="baz", c=c, d=portfolio.Amount("BTC", 1)) | ||
538 | print_log.assert_has_calls([ | ||
539 | mock.call("-----------"), | ||
540 | mock.call("[Stage] foo bar=baz, c=c = lambda x: x, d={'currency': 'BTC', 'value': Decimal('1')}"), | ||
541 | ]) | ||
542 | add_log.assert_called_once_with({ | ||
543 | 'type': 'stage', | ||
544 | 'stage': 'foo', | ||
545 | 'args': { | ||
546 | 'bar': 'baz', | ||
547 | 'c': 'c = lambda x: x', | ||
548 | 'd': { | ||
549 | 'currency': 'BTC', | ||
550 | 'value': D('1') | ||
551 | } | ||
552 | } | ||
553 | }) | ||
554 | |||
555 | @mock.patch.object(market.ReportStore, "print_log") | ||
556 | @mock.patch.object(market.ReportStore, "add_log") | ||
557 | def test_log_balances(self, add_log, print_log): | ||
558 | report_store = market.ReportStore(self.m) | ||
559 | self.m.balances.as_json.return_value = "json" | ||
560 | self.m.balances.all = { "FOO": "bar", "BAR": "baz" } | ||
561 | |||
562 | report_store.log_balances(tag="tag") | ||
563 | print_log.assert_has_calls([ | ||
564 | mock.call("[Balance]"), | ||
565 | mock.call("\tbar"), | ||
566 | mock.call("\tbaz"), | ||
567 | ]) | ||
568 | add_log.assert_called_once_with({ | ||
569 | 'type': 'balance', | ||
570 | 'balances': 'json', | ||
571 | 'tag': 'tag' | ||
572 | }) | ||
573 | |||
574 | @mock.patch.object(market.ReportStore, "print_log") | ||
575 | @mock.patch.object(market.ReportStore, "add_log") | ||
576 | def test_log_tickers(self, add_log, print_log): | ||
577 | report_store = market.ReportStore(self.m) | ||
578 | amounts = { | ||
579 | "BTC": portfolio.Amount("BTC", 10), | ||
580 | "ETH": portfolio.Amount("BTC", D("0.3")) | ||
581 | } | ||
582 | amounts["ETH"].rate = D("0.1") | ||
583 | |||
584 | report_store.log_tickers(amounts, "BTC", "default", "total") | ||
585 | print_log.assert_not_called() | ||
586 | add_log.assert_called_once_with({ | ||
587 | 'type': 'tickers', | ||
588 | 'compute_value': 'default', | ||
589 | 'balance_type': 'total', | ||
590 | 'currency': 'BTC', | ||
591 | 'balances': { | ||
592 | 'BTC': D('10'), | ||
593 | 'ETH': D('0.3') | ||
594 | }, | ||
595 | 'rates': { | ||
596 | 'BTC': None, | ||
597 | 'ETH': D('0.1') | ||
598 | }, | ||
599 | 'total': D('10.3') | ||
600 | }) | ||
601 | |||
602 | add_log.reset_mock() | ||
603 | compute_value = lambda x: x["bid"] | ||
604 | report_store.log_tickers(amounts, "BTC", compute_value, "total") | ||
605 | add_log.assert_called_once_with({ | ||
606 | 'type': 'tickers', | ||
607 | 'compute_value': 'compute_value = lambda x: x["bid"]', | ||
608 | 'balance_type': 'total', | ||
609 | 'currency': 'BTC', | ||
610 | 'balances': { | ||
611 | 'BTC': D('10'), | ||
612 | 'ETH': D('0.3') | ||
613 | }, | ||
614 | 'rates': { | ||
615 | 'BTC': None, | ||
616 | 'ETH': D('0.1') | ||
617 | }, | ||
618 | 'total': D('10.3') | ||
619 | }) | ||
620 | |||
621 | @mock.patch.object(market.ReportStore, "print_log") | ||
622 | @mock.patch.object(market.ReportStore, "add_log") | ||
623 | def test_log_dispatch(self, add_log, print_log): | ||
624 | report_store = market.ReportStore(self.m) | ||
625 | amount = portfolio.Amount("BTC", "10.3") | ||
626 | amounts = { | ||
627 | "BTC": portfolio.Amount("BTC", 10), | ||
628 | "ETH": portfolio.Amount("BTC", D("0.3")) | ||
629 | } | ||
630 | report_store.log_dispatch(amount, amounts, "medium", "repartition") | ||
631 | print_log.assert_not_called() | ||
632 | add_log.assert_called_once_with({ | ||
633 | 'type': 'dispatch', | ||
634 | 'liquidity': 'medium', | ||
635 | 'repartition_ratio': 'repartition', | ||
636 | 'total_amount': { | ||
637 | 'currency': 'BTC', | ||
638 | 'value': D('10.3') | ||
639 | }, | ||
640 | 'repartition': { | ||
641 | 'BTC': D('10'), | ||
642 | 'ETH': D('0.3') | ||
643 | } | ||
644 | }) | ||
645 | |||
646 | @mock.patch.object(market.ReportStore, "print_log") | ||
647 | @mock.patch.object(market.ReportStore, "add_log") | ||
648 | def test_log_trades(self, add_log, print_log): | ||
649 | report_store = market.ReportStore(self.m) | ||
650 | trade_mock1 = mock.Mock() | ||
651 | trade_mock2 = mock.Mock() | ||
652 | trade_mock1.as_json.return_value = { "trade": "1" } | ||
653 | trade_mock2.as_json.return_value = { "trade": "2" } | ||
654 | |||
655 | matching_and_trades = [ | ||
656 | (True, trade_mock1), | ||
657 | (False, trade_mock2), | ||
658 | ] | ||
659 | report_store.log_trades(matching_and_trades, "only") | ||
660 | |||
661 | print_log.assert_not_called() | ||
662 | add_log.assert_called_with({ | ||
663 | 'type': 'trades', | ||
664 | 'only': 'only', | ||
665 | 'debug': False, | ||
666 | 'trades': [ | ||
667 | {'trade': '1', 'skipped': False}, | ||
668 | {'trade': '2', 'skipped': True} | ||
669 | ] | ||
670 | }) | ||
671 | |||
672 | @mock.patch.object(market.ReportStore, "print_log") | ||
673 | @mock.patch.object(market.ReportStore, "add_log") | ||
674 | def test_log_orders(self, add_log, print_log): | ||
675 | report_store = market.ReportStore(self.m) | ||
676 | |||
677 | order_mock1 = mock.Mock() | ||
678 | order_mock2 = mock.Mock() | ||
679 | |||
680 | order_mock1.as_json.return_value = "order1" | ||
681 | order_mock2.as_json.return_value = "order2" | ||
682 | |||
683 | orders = [order_mock1, order_mock2] | ||
684 | |||
685 | report_store.log_orders(orders, tick="tick", | ||
686 | only="only", compute_value="compute_value") | ||
687 | |||
688 | print_log.assert_called_once_with("[Orders]") | ||
689 | self.m.trades.print_all_with_order.assert_called_once_with(ind="\t") | ||
690 | |||
691 | add_log.assert_called_with({ | ||
692 | 'type': 'orders', | ||
693 | 'only': 'only', | ||
694 | 'compute_value': 'compute_value', | ||
695 | 'tick': 'tick', | ||
696 | 'orders': ['order1', 'order2'] | ||
697 | }) | ||
698 | |||
699 | add_log.reset_mock() | ||
700 | def compute_value(x, y): | ||
701 | return x[y] | ||
702 | report_store.log_orders(orders, tick="tick", | ||
703 | only="only", compute_value=compute_value) | ||
704 | add_log.assert_called_with({ | ||
705 | 'type': 'orders', | ||
706 | 'only': 'only', | ||
707 | 'compute_value': 'def compute_value(x, y):\n return x[y]', | ||
708 | 'tick': 'tick', | ||
709 | 'orders': ['order1', 'order2'] | ||
710 | }) | ||
711 | |||
712 | |||
713 | @mock.patch.object(market.ReportStore, "print_log") | ||
714 | @mock.patch.object(market.ReportStore, "add_log") | ||
715 | def test_log_order(self, add_log, print_log): | ||
716 | report_store = market.ReportStore(self.m) | ||
717 | order_mock = mock.Mock() | ||
718 | order_mock.as_json.return_value = "order" | ||
719 | new_order_mock = mock.Mock() | ||
720 | new_order_mock.as_json.return_value = "new_order" | ||
721 | order_mock.__repr__ = mock.Mock() | ||
722 | order_mock.__repr__.return_value = "Order Mock" | ||
723 | new_order_mock.__repr__ = mock.Mock() | ||
724 | new_order_mock.__repr__.return_value = "New order Mock" | ||
725 | |||
726 | with self.subTest(finished=True): | ||
727 | report_store.log_order(order_mock, 1, finished=True) | ||
728 | print_log.assert_called_once_with("[Order] Finished Order Mock") | ||
729 | add_log.assert_called_once_with({ | ||
730 | 'type': 'order', | ||
731 | 'tick': 1, | ||
732 | 'update': None, | ||
733 | 'order': 'order', | ||
734 | 'compute_value': None, | ||
735 | 'new_order': None | ||
736 | }) | ||
737 | |||
738 | add_log.reset_mock() | ||
739 | print_log.reset_mock() | ||
740 | |||
741 | with self.subTest(update="waiting"): | ||
742 | report_store.log_order(order_mock, 1, update="waiting") | ||
743 | print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting") | ||
744 | add_log.assert_called_once_with({ | ||
745 | 'type': 'order', | ||
746 | 'tick': 1, | ||
747 | 'update': 'waiting', | ||
748 | 'order': 'order', | ||
749 | 'compute_value': None, | ||
750 | 'new_order': None | ||
751 | }) | ||
752 | |||
753 | add_log.reset_mock() | ||
754 | print_log.reset_mock() | ||
755 | with self.subTest(update="adjusting"): | ||
756 | compute_value = lambda x: (x["bid"] + x["ask"]*2)/3 | ||
757 | report_store.log_order(order_mock, 3, | ||
758 | update="adjusting", new_order=new_order_mock, | ||
759 | compute_value=compute_value) | ||
760 | print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") | ||
761 | add_log.assert_called_once_with({ | ||
762 | 'type': 'order', | ||
763 | 'tick': 3, | ||
764 | 'update': 'adjusting', | ||
765 | 'order': 'order', | ||
766 | 'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3', | ||
767 | 'new_order': 'new_order' | ||
768 | }) | ||
769 | |||
770 | add_log.reset_mock() | ||
771 | print_log.reset_mock() | ||
772 | with self.subTest(update="market_fallback"): | ||
773 | report_store.log_order(order_mock, 7, | ||
774 | update="market_fallback", new_order=new_order_mock) | ||
775 | print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value") | ||
776 | add_log.assert_called_once_with({ | ||
777 | 'type': 'order', | ||
778 | 'tick': 7, | ||
779 | 'update': 'market_fallback', | ||
780 | 'order': 'order', | ||
781 | 'compute_value': None, | ||
782 | 'new_order': 'new_order' | ||
783 | }) | ||
784 | |||
785 | add_log.reset_mock() | ||
786 | print_log.reset_mock() | ||
787 | with self.subTest(update="market_adjusting"): | ||
788 | report_store.log_order(order_mock, 17, | ||
789 | update="market_adjust", new_order=new_order_mock) | ||
790 | print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock") | ||
791 | add_log.assert_called_once_with({ | ||
792 | 'type': 'order', | ||
793 | 'tick': 17, | ||
794 | 'update': 'market_adjust', | ||
795 | 'order': 'order', | ||
796 | 'compute_value': None, | ||
797 | 'new_order': 'new_order' | ||
798 | }) | ||
799 | |||
800 | @mock.patch.object(market.ReportStore, "print_log") | ||
801 | @mock.patch.object(market.ReportStore, "add_log") | ||
802 | def test_log_move_balances(self, add_log, print_log): | ||
803 | report_store = market.ReportStore(self.m) | ||
804 | needed = { | ||
805 | "BTC": portfolio.Amount("BTC", 10), | ||
806 | "USDT": 1 | ||
807 | } | ||
808 | moving = { | ||
809 | "BTC": portfolio.Amount("BTC", 3), | ||
810 | "USDT": -2 | ||
811 | } | ||
812 | report_store.log_move_balances(needed, moving) | ||
813 | print_log.assert_not_called() | ||
814 | add_log.assert_called_once_with({ | ||
815 | 'type': 'move_balances', | ||
816 | 'debug': False, | ||
817 | 'needed': { | ||
818 | 'BTC': D('10'), | ||
819 | 'USDT': 1 | ||
820 | }, | ||
821 | 'moving': { | ||
822 | 'BTC': D('3'), | ||
823 | 'USDT': -2 | ||
824 | } | ||
825 | }) | ||
826 | |||
827 | def test_log_http_request(self): | ||
828 | with mock.patch.object(market.ReportStore, "add_log") as add_log: | ||
829 | report_store = market.ReportStore(self.m) | ||
830 | response = mock.Mock() | ||
831 | response.status_code = 200 | ||
832 | response.text = "Hey" | ||
833 | |||
834 | report_store.log_http_request("method", "url", "body", | ||
835 | "headers", response) | ||
836 | add_log.assert_called_once_with({ | ||
837 | 'type': 'http_request', | ||
838 | 'method': 'method', | ||
839 | 'url': 'url', | ||
840 | 'body': 'body', | ||
841 | 'headers': 'headers', | ||
842 | 'status': 200, | ||
843 | 'response': 'Hey', | ||
844 | 'response_same_as': None, | ||
845 | }) | ||
846 | |||
847 | add_log.reset_mock() | ||
848 | report_store.log_http_request("method", "url", "body", | ||
849 | "headers", ValueError("Foo")) | ||
850 | add_log.assert_called_once_with({ | ||
851 | 'type': 'http_request', | ||
852 | 'method': 'method', | ||
853 | 'url': 'url', | ||
854 | 'body': 'body', | ||
855 | 'headers': 'headers', | ||
856 | 'status': -1, | ||
857 | 'response': None, | ||
858 | 'error': 'ValueError', | ||
859 | 'error_message': 'Foo', | ||
860 | }) | ||
861 | |||
862 | with self.subTest(no_http_dup=True, duplicate=True): | ||
863 | self.m.user_id = 1 | ||
864 | self.m.market_id = 3 | ||
865 | report_store = market.ReportStore(self.m, no_http_dup=True) | ||
866 | original_add_log = report_store.add_log | ||
867 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | ||
868 | report_store.log_http_request("method", "url", "body", | ||
869 | "headers", response) | ||
870 | report_store.log_http_request("method", "url", "body", | ||
871 | "headers", response) | ||
872 | self.assertEqual(2, add_log.call_count) | ||
873 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | ||
874 | self.assertIsNone(add_log.mock_calls[1][1][0]["response"]) | ||
875 | self.assertEqual(add_log.mock_calls[0][1][0]["date"], add_log.mock_calls[1][1][0]["response_same_as"]) | ||
876 | with self.subTest(no_http_dup=True, duplicate=False, case="Different call"): | ||
877 | self.m.user_id = 1 | ||
878 | self.m.market_id = 3 | ||
879 | report_store = market.ReportStore(self.m, no_http_dup=True) | ||
880 | original_add_log = report_store.add_log | ||
881 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | ||
882 | report_store.log_http_request("method", "url", "body", | ||
883 | "headers", response) | ||
884 | report_store.log_http_request("method2", "url", "body", | ||
885 | "headers", response) | ||
886 | self.assertEqual(2, add_log.call_count) | ||
887 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | ||
888 | self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"]) | ||
889 | with self.subTest(no_http_dup=True, duplicate=False, case="Call inbetween"): | ||
890 | self.m.user_id = 1 | ||
891 | self.m.market_id = 3 | ||
892 | report_store = market.ReportStore(self.m, no_http_dup=True) | ||
893 | original_add_log = report_store.add_log | ||
894 | |||
895 | response2 = mock.Mock() | ||
896 | response2.status_code = 200 | ||
897 | response2.text = "Hey there!" | ||
898 | |||
899 | with mock.patch.object(report_store, "add_log", side_effect=original_add_log) as add_log: | ||
900 | report_store.log_http_request("method", "url", "body", | ||
901 | "headers", response) | ||
902 | report_store.log_http_request("method", "url", "body", | ||
903 | "headers", response2) | ||
904 | report_store.log_http_request("method", "url", "body", | ||
905 | "headers", response) | ||
906 | self.assertEqual(3, add_log.call_count) | ||
907 | self.assertIsNone(add_log.mock_calls[0][1][0]["response_same_as"]) | ||
908 | self.assertIsNone(add_log.mock_calls[1][1][0]["response_same_as"]) | ||
909 | self.assertIsNone(add_log.mock_calls[2][1][0]["response_same_as"]) | ||
910 | |||
911 | @mock.patch.object(market.ReportStore, "add_log") | ||
912 | def test_log_market(self, add_log): | ||
913 | report_store = market.ReportStore(self.m) | ||
914 | |||
915 | report_store.log_market(self.market_args(debug=True, quiet=False)) | ||
916 | add_log.assert_called_once_with({ | ||
917 | "type": "market", | ||
918 | "commit": "$Format:%H$", | ||
919 | "args": { "report_path": None, "debug": True, "quiet": False }, | ||
920 | }) | ||
921 | |||
922 | @mock.patch.object(market.ReportStore, "print_log") | ||
923 | @mock.patch.object(market.ReportStore, "add_log") | ||
924 | def test_log_error(self, add_log, print_log): | ||
925 | report_store = market.ReportStore(self.m) | ||
926 | with self.subTest(message=None, exception=None): | ||
927 | report_store.log_error("action") | ||
928 | print_log.assert_called_once_with("[Error] action") | ||
929 | add_log.assert_called_once_with({ | ||
930 | 'type': 'error', | ||
931 | 'action': 'action', | ||
932 | 'exception_class': None, | ||
933 | 'exception_message': None, | ||
934 | 'message': None | ||
935 | }) | ||
936 | |||
937 | print_log.reset_mock() | ||
938 | add_log.reset_mock() | ||
939 | with self.subTest(message="Hey", exception=None): | ||
940 | report_store.log_error("action", message="Hey") | ||
941 | print_log.assert_has_calls([ | ||
942 | mock.call("[Error] action"), | ||
943 | mock.call("\tHey") | ||
944 | ]) | ||
945 | add_log.assert_called_once_with({ | ||
946 | 'type': 'error', | ||
947 | 'action': 'action', | ||
948 | 'exception_class': None, | ||
949 | 'exception_message': None, | ||
950 | 'message': "Hey" | ||
951 | }) | ||
952 | |||
953 | print_log.reset_mock() | ||
954 | add_log.reset_mock() | ||
955 | with self.subTest(message=None, exception=Exception("bouh")): | ||
956 | report_store.log_error("action", exception=Exception("bouh")) | ||
957 | print_log.assert_has_calls([ | ||
958 | mock.call("[Error] action"), | ||
959 | mock.call("\tException: bouh") | ||
960 | ]) | ||
961 | add_log.assert_called_once_with({ | ||
962 | 'type': 'error', | ||
963 | 'action': 'action', | ||
964 | 'exception_class': "Exception", | ||
965 | 'exception_message': "bouh", | ||
966 | 'message': None | ||
967 | }) | ||
968 | |||
969 | print_log.reset_mock() | ||
970 | add_log.reset_mock() | ||
971 | with self.subTest(message="Hey", exception=Exception("bouh")): | ||
972 | report_store.log_error("action", message="Hey", exception=Exception("bouh")) | ||
973 | print_log.assert_has_calls([ | ||
974 | mock.call("[Error] action"), | ||
975 | mock.call("\tException: bouh"), | ||
976 | mock.call("\tHey") | ||
977 | ]) | ||
978 | add_log.assert_called_once_with({ | ||
979 | 'type': 'error', | ||
980 | 'action': 'action', | ||
981 | 'exception_class': "Exception", | ||
982 | 'exception_message': "bouh", | ||
983 | 'message': "Hey" | ||
984 | }) | ||
985 | |||
986 | @mock.patch.object(market.ReportStore, "print_log") | ||
987 | @mock.patch.object(market.ReportStore, "add_log") | ||
988 | def test_log_debug_action(self, add_log, print_log): | ||
989 | report_store = market.ReportStore(self.m) | ||
990 | report_store.log_debug_action("Hey") | ||
991 | |||
992 | print_log.assert_called_once_with("[Debug] Hey") | ||
993 | add_log.assert_called_once_with({ | ||
994 | 'type': 'debug_action', | ||
995 | 'action': 'Hey' | ||
996 | }) | ||
997 | |||
998 | class PortfolioTest(WebMockTestCase): | ||
999 | def setUp(self): | ||
1000 | super().setUp() | ||
1001 | |||
1002 | with open("test_samples/test_portfolio.json") as example: | ||
1003 | self.json_response = example.read() | ||
1004 | |||
1005 | self.wm.get(market.Portfolio.URL, text=self.json_response) | ||
1006 | |||
1007 | @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") | ||
1008 | def test_get_cryptoportfolio(self, parse_cryptoportfolio): | ||
1009 | with self.subTest(parallel=False): | ||
1010 | self.wm.get(market.Portfolio.URL, [ | ||
1011 | {"text":'{ "foo": "bar" }', "status_code": 200}, | ||
1012 | {"text": "System Error", "status_code": 500}, | ||
1013 | {"exc": requests.exceptions.ConnectTimeout}, | ||
1014 | ]) | ||
1015 | market.Portfolio.get_cryptoportfolio() | ||
1016 | self.assertIn("foo", market.Portfolio.data.get()) | ||
1017 | self.assertEqual("bar", market.Portfolio.data.get()["foo"]) | ||
1018 | self.assertTrue(self.wm.called) | ||
1019 | self.assertEqual(1, self.wm.call_count) | ||
1020 | market.Portfolio.report.log_error.assert_not_called() | ||
1021 | market.Portfolio.report.log_http_request.assert_called_once() | ||
1022 | parse_cryptoportfolio.assert_called_once_with() | ||
1023 | market.Portfolio.report.log_http_request.reset_mock() | ||
1024 | parse_cryptoportfolio.reset_mock() | ||
1025 | market.Portfolio.data = store.LockedVar(None) | ||
1026 | |||
1027 | market.Portfolio.get_cryptoportfolio() | ||
1028 | self.assertIsNone(market.Portfolio.data.get()) | ||
1029 | self.assertEqual(2, self.wm.call_count) | ||
1030 | parse_cryptoportfolio.assert_not_called() | ||
1031 | market.Portfolio.report.log_error.assert_not_called() | ||
1032 | market.Portfolio.report.log_http_request.assert_called_once() | ||
1033 | market.Portfolio.report.log_http_request.reset_mock() | ||
1034 | parse_cryptoportfolio.reset_mock() | ||
1035 | |||
1036 | market.Portfolio.data = store.LockedVar("Foo") | ||
1037 | market.Portfolio.get_cryptoportfolio() | ||
1038 | self.assertEqual(2, self.wm.call_count) | ||
1039 | parse_cryptoportfolio.assert_not_called() | ||
1040 | |||
1041 | market.Portfolio.get_cryptoportfolio(refetch=True) | ||
1042 | self.assertEqual("Foo", market.Portfolio.data.get()) | ||
1043 | self.assertEqual(3, self.wm.call_count) | ||
1044 | market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", | ||
1045 | exception=mock.ANY) | ||
1046 | market.Portfolio.report.log_http_request.assert_not_called() | ||
1047 | with self.subTest(parallel=True): | ||
1048 | with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ | ||
1049 | mock.patch.object(market.Portfolio, "notify_and_wait") as notify: | ||
1050 | with self.subTest(worker=True): | ||
1051 | market.Portfolio.data = store.LockedVar(None) | ||
1052 | market.Portfolio.worker = mock.Mock() | ||
1053 | is_worker.return_value = True | ||
1054 | self.wm.get(market.Portfolio.URL, [ | ||
1055 | {"text":'{ "foo": "bar" }', "status_code": 200}, | ||
1056 | ]) | ||
1057 | market.Portfolio.get_cryptoportfolio() | ||
1058 | self.assertIn("foo", market.Portfolio.data.get()) | ||
1059 | parse_cryptoportfolio.reset_mock() | ||
1060 | with self.subTest(worker=False): | ||
1061 | market.Portfolio.data = store.LockedVar(None) | ||
1062 | market.Portfolio.worker = mock.Mock() | ||
1063 | is_worker.return_value = False | ||
1064 | market.Portfolio.get_cryptoportfolio() | ||
1065 | notify.assert_called_once_with() | ||
1066 | parse_cryptoportfolio.assert_not_called() | ||
1067 | |||
1068 | def test_parse_cryptoportfolio(self): | ||
1069 | with self.subTest(description="Normal case"): | ||
1070 | market.Portfolio.data = store.LockedVar(store.json.loads( | ||
1071 | self.json_response, parse_int=D, parse_float=D)) | ||
1072 | market.Portfolio.parse_cryptoportfolio() | ||
1073 | |||
1074 | self.assertListEqual( | ||
1075 | ["medium", "high"], | ||
1076 | list(market.Portfolio.liquidities.get().keys())) | ||
1077 | |||
1078 | liquidities = market.Portfolio.liquidities.get() | ||
1079 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
1080 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
1081 | |||
1082 | expected = { | ||
1083 | 'BTC': (D("0.2857"), "long"), | ||
1084 | 'DGB': (D("0.1015"), "long"), | ||
1085 | 'DOGE': (D("0.1805"), "long"), | ||
1086 | 'SC': (D("0.0623"), "long"), | ||
1087 | 'ZEC': (D("0.3701"), "long"), | ||
1088 | } | ||
1089 | date = portfolio.datetime.datetime(2018, 1, 8) | ||
1090 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
1091 | |||
1092 | expected = { | ||
1093 | 'BTC': (D("1.1102e-16"), "long"), | ||
1094 | 'ETC': (D("0.1"), "long"), | ||
1095 | 'FCT': (D("0.1"), "long"), | ||
1096 | 'GAS': (D("0.1"), "long"), | ||
1097 | 'NAV': (D("0.1"), "long"), | ||
1098 | 'OMG': (D("0.1"), "long"), | ||
1099 | 'OMNI': (D("0.1"), "long"), | ||
1100 | 'PPC': (D("0.1"), "long"), | ||
1101 | 'RIC': (D("0.1"), "long"), | ||
1102 | 'VIA': (D("0.1"), "long"), | ||
1103 | 'XCP': (D("0.1"), "long"), | ||
1104 | } | ||
1105 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
1106 | self.assertEqual(portfolio.datetime.datetime(2018, 1, 15), market.Portfolio.last_date.get()) | ||
1107 | |||
1108 | with self.subTest(description="Missing weight"): | ||
1109 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
1110 | del(data["portfolio_2"]["weights"]) | ||
1111 | market.Portfolio.data = store.LockedVar(data) | ||
1112 | |||
1113 | market.Portfolio.parse_cryptoportfolio() | ||
1114 | self.assertListEqual( | ||
1115 | ["medium", "high"], | ||
1116 | list(market.Portfolio.liquidities.get().keys())) | ||
1117 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
1118 | |||
1119 | with self.subTest(description="All missing weights"): | ||
1120 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
1121 | del(data["portfolio_1"]["weights"]) | ||
1122 | del(data["portfolio_2"]["weights"]) | ||
1123 | market.Portfolio.data = store.LockedVar(data) | ||
1124 | |||
1125 | market.Portfolio.parse_cryptoportfolio() | ||
1126 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
1127 | self.assertEqual({}, market.Portfolio.liquidities.get("high")) | ||
1128 | self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) | ||
1129 | |||
1130 | |||
1131 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
1132 | def test_repartition(self, get_cryptoportfolio): | ||
1133 | market.Portfolio.liquidities = store.LockedVar({ | ||
1134 | "medium": { | ||
1135 | "2018-03-01": "medium_2018-03-01", | ||
1136 | "2018-03-08": "medium_2018-03-08", | ||
1137 | }, | ||
1138 | "high": { | ||
1139 | "2018-03-01": "high_2018-03-01", | ||
1140 | "2018-03-08": "high_2018-03-08", | ||
1141 | } | ||
1142 | }) | ||
1143 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | ||
1144 | |||
1145 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) | ||
1146 | get_cryptoportfolio.assert_called_once_with() | ||
1147 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium")) | ||
1148 | self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high")) | ||
1149 | |||
1150 | @mock.patch.object(market.time, "sleep") | ||
1151 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
1152 | def test_wait_for_recent(self, get_cryptoportfolio, sleep): | ||
1153 | self.call_count = 0 | ||
1154 | def _get(refetch=False): | ||
1155 | if self.call_count != 0: | ||
1156 | self.assertTrue(refetch) | ||
1157 | else: | ||
1158 | self.assertFalse(refetch) | ||
1159 | self.call_count += 1 | ||
1160 | market.Portfolio.last_date = store.LockedVar(store.datetime.datetime.now()\ | ||
1161 | - store.datetime.timedelta(10)\ | ||
1162 | + store.datetime.timedelta(self.call_count)) | ||
1163 | get_cryptoportfolio.side_effect = _get | ||
1164 | |||
1165 | market.Portfolio.wait_for_recent() | ||
1166 | sleep.assert_called_with(30) | ||
1167 | self.assertEqual(6, sleep.call_count) | ||
1168 | self.assertEqual(7, get_cryptoportfolio.call_count) | ||
1169 | market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | ||
1170 | |||
1171 | sleep.reset_mock() | ||
1172 | get_cryptoportfolio.reset_mock() | ||
1173 | market.Portfolio.last_date = store.LockedVar(None) | ||
1174 | self.call_count = 0 | ||
1175 | market.Portfolio.wait_for_recent(delta=15) | ||
1176 | sleep.assert_not_called() | ||
1177 | self.assertEqual(1, get_cryptoportfolio.call_count) | ||
1178 | |||
1179 | sleep.reset_mock() | ||
1180 | get_cryptoportfolio.reset_mock() | ||
1181 | market.Portfolio.last_date = store.LockedVar(None) | ||
1182 | self.call_count = 0 | ||
1183 | market.Portfolio.wait_for_recent(delta=1) | ||
1184 | sleep.assert_called_with(30) | ||
1185 | self.assertEqual(9, sleep.call_count) | ||
1186 | self.assertEqual(10, get_cryptoportfolio.call_count) | ||
1187 | |||
1188 | def test_is_worker_thread(self): | ||
1189 | with self.subTest(worker=None): | ||
1190 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
1191 | |||
1192 | with self.subTest(worker="not self"),\ | ||
1193 | mock.patch("threading.current_thread") as current_thread: | ||
1194 | current = mock.Mock() | ||
1195 | current_thread.return_value = current | ||
1196 | store.Portfolio.worker = mock.Mock() | ||
1197 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
1198 | |||
1199 | with self.subTest(worker="self"),\ | ||
1200 | mock.patch("threading.current_thread") as current_thread: | ||
1201 | current = mock.Mock() | ||
1202 | current_thread.return_value = current | ||
1203 | store.Portfolio.worker = current | ||
1204 | self.assertTrue(store.Portfolio.is_worker_thread()) | ||
1205 | |||
1206 | def test_start_worker(self): | ||
1207 | with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: | ||
1208 | store.Portfolio.start_worker() | ||
1209 | notification.assert_called_once_with(poll=30) | ||
1210 | |||
1211 | self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) | ||
1212 | self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) | ||
1213 | store.Portfolio.report.start_lock.assert_called_once_with() | ||
1214 | |||
1215 | self.assertIsNotNone(store.Portfolio.worker) | ||
1216 | self.assertIsNotNone(store.Portfolio.worker_notify) | ||
1217 | self.assertIsNotNone(store.Portfolio.callback) | ||
1218 | self.assertTrue(store.Portfolio.worker_started) | ||
1219 | |||
1220 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
1221 | self.assertEqual(1, threading.active_count()) | ||
1222 | |||
1223 | def test_stop_worker(self): | ||
1224 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
1225 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
1226 | mock.patch.object(store.time, "sleep") as sleep: | ||
1227 | store.Portfolio.start_worker(poll=3) | ||
1228 | store.Portfolio.stop_worker() | ||
1229 | store.Portfolio.worker.join() | ||
1230 | get.assert_not_called() | ||
1231 | report.assert_not_called() | ||
1232 | sleep.assert_not_called() | ||
1233 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
1234 | |||
1235 | def test_wait_for_notification(self): | ||
1236 | with self.assertRaises(RuntimeError): | ||
1237 | store.Portfolio.wait_for_notification() | ||
1238 | |||
1239 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
1240 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
1241 | mock.patch.object(store.time, "sleep") as sleep: | ||
1242 | store.Portfolio.start_worker(poll=3) | ||
1243 | |||
1244 | store.Portfolio.worker_notify.set() | ||
1245 | |||
1246 | store.Portfolio.callback.wait() | ||
1247 | |||
1248 | report.print_log.assert_called_once_with("Fetching cryptoportfolio") | ||
1249 | get.assert_called_once_with(refetch=True) | ||
1250 | sleep.assert_called_once_with(3) | ||
1251 | self.assertFalse(store.Portfolio.worker_notify.is_set()) | ||
1252 | self.assertTrue(store.Portfolio.worker.is_alive()) | ||
1253 | |||
1254 | store.Portfolio.callback.clear() | ||
1255 | store.Portfolio.worker_started = False | ||
1256 | store.Portfolio.worker_notify.set() | ||
1257 | store.Portfolio.worker.join() | ||
1258 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
1259 | |||
1260 | def test_notify_and_wait(self): | ||
1261 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | ||
1262 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | ||
1263 | store.Portfolio.notify_and_wait() | ||
1264 | callback.clear.assert_called_once_with() | ||
1265 | worker_notify.set.assert_called_once_with() | ||
1266 | callback.wait.assert_called_once_with() | ||
1267 | |||
1268 | |||