diff options
Diffstat (limited to 'test.py')
-rw-r--r-- | test.py | 1257 |
1 files changed, 943 insertions, 314 deletions
@@ -7,7 +7,8 @@ from unittest import mock | |||
7 | import requests | 7 | import requests |
8 | import requests_mock | 8 | import requests_mock |
9 | from io import StringIO | 9 | from io import StringIO |
10 | import portfolio, helper, market | 10 | import threading |
11 | import portfolio, market, main, store | ||
11 | 12 | ||
12 | limits = ["acceptance", "unit"] | 13 | limits = ["acceptance", "unit"] |
13 | for test_type in limits: | 14 | for test_type in limits: |
@@ -32,7 +33,15 @@ class WebMockTestCase(unittest.TestCase): | |||
32 | self.m.debug = False | 33 | self.m.debug = False |
33 | 34 | ||
34 | self.patchers = [ | 35 | self.patchers = [ |
35 | mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), | 36 | mock.patch.multiple(market.Portfolio, |
37 | data=store.LockedVar(None), | ||
38 | liquidities=store.LockedVar({}), | ||
39 | last_date=store.LockedVar(None), | ||
40 | report=mock.Mock(), | ||
41 | worker=None, | ||
42 | worker_notify=None, | ||
43 | worker_started=False, | ||
44 | callback=None), | ||
36 | mock.patch.multiple(portfolio.Computation, | 45 | mock.patch.multiple(portfolio.Computation, |
37 | computations=portfolio.Computation.computations), | 46 | computations=portfolio.Computation.computations), |
38 | ] | 47 | ] |
@@ -126,174 +135,632 @@ class poloniexETest(unittest.TestCase): | |||
126 | } | 135 | } |
127 | self.assertEqual(expected, self.s.margin_summary()) | 136 | self.assertEqual(expected, self.s.margin_summary()) |
128 | 137 | ||
138 | def test_create_order(self): | ||
139 | with mock.patch.object(self.s, "create_exchange_order") as exchange,\ | ||
140 | mock.patch.object(self.s, "create_margin_order") as margin: | ||
141 | with self.subTest(account="unspecified"): | ||
142 | self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params") | ||
143 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
144 | margin.assert_not_called() | ||
145 | exchange.reset_mock() | ||
146 | margin.reset_mock() | ||
147 | |||
148 | with self.subTest(account="exchange"): | ||
149 | self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params") | ||
150 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
151 | margin.assert_not_called() | ||
152 | exchange.reset_mock() | ||
153 | margin.reset_mock() | ||
154 | |||
155 | with self.subTest(account="margin"): | ||
156 | self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params") | ||
157 | margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params") | ||
158 | exchange.assert_not_called() | ||
159 | exchange.reset_mock() | ||
160 | margin.reset_mock() | ||
161 | |||
162 | with self.subTest(account="unknown"), self.assertRaises(NotImplementedError): | ||
163 | self.s.create_order("symbol", "type", "side", "amount", account="unknown") | ||
164 | |||
165 | def test_parse_ticker(self): | ||
166 | ticker = { | ||
167 | "high24hr": "12", | ||
168 | "low24hr": "10", | ||
169 | "highestBid": "10.5", | ||
170 | "lowestAsk": "11.5", | ||
171 | "last": "11", | ||
172 | "percentChange": "0.1", | ||
173 | "quoteVolume": "10", | ||
174 | "baseVolume": "20" | ||
175 | } | ||
176 | market = { | ||
177 | "symbol": "BTC/ETC" | ||
178 | } | ||
179 | with mock.patch.object(self.s, "milliseconds") as ms: | ||
180 | ms.return_value = 1520292715123 | ||
181 | result = self.s.parse_ticker(ticker, market) | ||
182 | |||
183 | expected = { | ||
184 | "symbol": "BTC/ETC", | ||
185 | "timestamp": 1520292715123, | ||
186 | "datetime": "2018-03-05T23:31:55.123Z", | ||
187 | "high": D("12"), | ||
188 | "low": D("10"), | ||
189 | "bid": D("10.5"), | ||
190 | "ask": D("11.5"), | ||
191 | "vwap": None, | ||
192 | "open": None, | ||
193 | "close": None, | ||
194 | "first": None, | ||
195 | "last": D("11"), | ||
196 | "change": D("0.1"), | ||
197 | "percentage": None, | ||
198 | "average": None, | ||
199 | "baseVolume": D("10"), | ||
200 | "quoteVolume": D("20"), | ||
201 | "info": ticker | ||
202 | } | ||
203 | self.assertEqual(expected, result) | ||
204 | |||
205 | def test_fetch_margin_balance(self): | ||
206 | with mock.patch.object(self.s, "privatePostGetMarginPosition") as get_margin_position: | ||
207 | get_margin_position.return_value = { | ||
208 | "BTC_DASH": { | ||
209 | "amount": "-0.1", | ||
210 | "basePrice": "0.06818560", | ||
211 | "lendingFees": "0.00000001", | ||
212 | "liquidationPrice": "0.15107132", | ||
213 | "pl": "-0.00000371", | ||
214 | "total": "0.00681856", | ||
215 | "type": "short" | ||
216 | }, | ||
217 | "BTC_ETC": { | ||
218 | "amount": "-0.6", | ||
219 | "basePrice": "0.1", | ||
220 | "lendingFees": "0.00000001", | ||
221 | "liquidationPrice": "0.6", | ||
222 | "pl": "0.00000371", | ||
223 | "total": "0.06", | ||
224 | "type": "short" | ||
225 | }, | ||
226 | "BTC_ETH": { | ||
227 | "amount": "0", | ||
228 | "basePrice": "0", | ||
229 | "lendingFees": "0", | ||
230 | "liquidationPrice": "-1", | ||
231 | "pl": "0", | ||
232 | "total": "0", | ||
233 | "type": "none" | ||
234 | } | ||
235 | } | ||
236 | balances = self.s.fetch_margin_balance() | ||
237 | self.assertEqual(2, len(balances)) | ||
238 | expected = { | ||
239 | "DASH": { | ||
240 | "amount": D("-0.1"), | ||
241 | "borrowedPrice": D("0.06818560"), | ||
242 | "lendingFees": D("1E-8"), | ||
243 | "pl": D("-0.00000371"), | ||
244 | "liquidationPrice": D("0.15107132"), | ||
245 | "type": "short", | ||
246 | "total": D("0.00681856"), | ||
247 | "baseCurrency": "BTC" | ||
248 | }, | ||
249 | "ETC": { | ||
250 | "amount": D("-0.6"), | ||
251 | "borrowedPrice": D("0.1"), | ||
252 | "lendingFees": D("1E-8"), | ||
253 | "pl": D("0.00000371"), | ||
254 | "liquidationPrice": D("0.6"), | ||
255 | "type": "short", | ||
256 | "total": D("0.06"), | ||
257 | "baseCurrency": "BTC" | ||
258 | } | ||
259 | } | ||
260 | self.assertEqual(expected, balances) | ||
261 | |||
262 | def test_sum(self): | ||
263 | self.assertEqual(D("1.1"), self.s.sum(D("1"), D("0.1"))) | ||
264 | |||
265 | def test_fetch_balance(self): | ||
266 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
267 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balances,\ | ||
268 | mock.patch.object(self.s, "common_currency_code") as ccc: | ||
269 | ccc.side_effect = ["ETH", "BTC", "DASH"] | ||
270 | balances.return_value = { | ||
271 | "ETH": { | ||
272 | "available": "10", | ||
273 | "onOrders": "1", | ||
274 | }, | ||
275 | "BTC": { | ||
276 | "available": "1", | ||
277 | "onOrders": "0", | ||
278 | }, | ||
279 | "DASH": { | ||
280 | "available": "0", | ||
281 | "onOrders": "3" | ||
282 | } | ||
283 | } | ||
284 | |||
285 | expected = { | ||
286 | "info": { | ||
287 | "ETH": {"available": "10", "onOrders": "1"}, | ||
288 | "BTC": {"available": "1", "onOrders": "0"}, | ||
289 | "DASH": {"available": "0", "onOrders": "3"} | ||
290 | }, | ||
291 | "ETH": {"free": D("10"), "used": D("1"), "total": D("11")}, | ||
292 | "BTC": {"free": D("1"), "used": D("0"), "total": D("1")}, | ||
293 | "DASH": {"free": D("0"), "used": D("3"), "total": D("3")}, | ||
294 | "free": {"ETH": D("10"), "BTC": D("1"), "DASH": D("0")}, | ||
295 | "used": {"ETH": D("1"), "BTC": D("0"), "DASH": D("3")}, | ||
296 | "total": {"ETH": D("11"), "BTC": D("1"), "DASH": D("3")} | ||
297 | } | ||
298 | result = self.s.fetch_balance() | ||
299 | load_markets.assert_called_once() | ||
300 | self.assertEqual(expected, result) | ||
301 | |||
302 | def test_fetch_balance_per_type(self): | ||
303 | with mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balances: | ||
304 | balances.return_value = { | ||
305 | "exchange": { | ||
306 | "BLK": "159.83673869", | ||
307 | "BTC": "0.00005959", | ||
308 | "USDT": "0.00002625", | ||
309 | "XMR": "0.18719303" | ||
310 | }, | ||
311 | "margin": { | ||
312 | "BTC": "0.03019227" | ||
313 | } | ||
314 | } | ||
315 | expected = { | ||
316 | "info": { | ||
317 | "exchange": { | ||
318 | "BLK": "159.83673869", | ||
319 | "BTC": "0.00005959", | ||
320 | "USDT": "0.00002625", | ||
321 | "XMR": "0.18719303" | ||
322 | }, | ||
323 | "margin": { | ||
324 | "BTC": "0.03019227" | ||
325 | } | ||
326 | }, | ||
327 | "exchange": { | ||
328 | "BLK": D("159.83673869"), | ||
329 | "BTC": D("0.00005959"), | ||
330 | "USDT": D("0.00002625"), | ||
331 | "XMR": D("0.18719303") | ||
332 | }, | ||
333 | "margin": {"BTC": D("0.03019227")}, | ||
334 | "BLK": {"exchange": D("159.83673869")}, | ||
335 | "BTC": {"exchange": D("0.00005959"), "margin": D("0.03019227")}, | ||
336 | "USDT": {"exchange": D("0.00002625")}, | ||
337 | "XMR": {"exchange": D("0.18719303")} | ||
338 | } | ||
339 | result = self.s.fetch_balance_per_type() | ||
340 | self.assertEqual(expected, result) | ||
341 | |||
342 | def test_fetch_all_balances(self): | ||
343 | import json | ||
344 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
345 | mock.patch.object(self.s, "privatePostGetMarginPosition") as margin_balance,\ | ||
346 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balance,\ | ||
347 | mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balance_per_type: | ||
348 | |||
349 | with open("test_samples/poloniexETest.test_fetch_all_balances.1.json") as f: | ||
350 | balance.return_value = json.load(f) | ||
351 | with open("test_samples/poloniexETest.test_fetch_all_balances.2.json") as f: | ||
352 | margin_balance.return_value = json.load(f) | ||
353 | with open("test_samples/poloniexETest.test_fetch_all_balances.3.json") as f: | ||
354 | balance_per_type.return_value = json.load(f) | ||
355 | |||
356 | result = self.s.fetch_all_balances() | ||
357 | expected_doge = { | ||
358 | "total": D("-12779.79821852"), | ||
359 | "exchange_used": D("0E-8"), | ||
360 | "exchange_total": D("0E-8"), | ||
361 | "exchange_free": D("0E-8"), | ||
362 | "margin_available": 0, | ||
363 | "margin_in_position": 0, | ||
364 | "margin_borrowed": D("12779.79821852"), | ||
365 | "margin_total": D("-12779.79821852"), | ||
366 | "margin_pending_gain": 0, | ||
367 | "margin_lending_fees": D("-9E-8"), | ||
368 | "margin_pending_base_gain": D("0.00024059"), | ||
369 | "margin_position_type": "short", | ||
370 | "margin_liquidation_price": D("0.00000246"), | ||
371 | "margin_borrowed_base_price": D("0.00599149"), | ||
372 | "margin_borrowed_base_currency": "BTC" | ||
373 | } | ||
374 | expected_btc = {"total": D("0.05432165"), | ||
375 | "exchange_used": D("0E-8"), | ||
376 | "exchange_total": D("0.00005959"), | ||
377 | "exchange_free": D("0.00005959"), | ||
378 | "margin_available": D("0.03019227"), | ||
379 | "margin_in_position": D("0.02406979"), | ||
380 | "margin_borrowed": 0, | ||
381 | "margin_total": D("0.05426206"), | ||
382 | "margin_pending_gain": D("0.00093955"), | ||
383 | "margin_lending_fees": 0, | ||
384 | "margin_pending_base_gain": 0, | ||
385 | "margin_position_type": None, | ||
386 | "margin_liquidation_price": 0, | ||
387 | "margin_borrowed_base_price": 0, | ||
388 | "margin_borrowed_base_currency": None | ||
389 | } | ||
390 | expected_xmr = {"total": D("0.18719303"), | ||
391 | "exchange_used": D("0E-8"), | ||
392 | "exchange_total": D("0.18719303"), | ||
393 | "exchange_free": D("0.18719303"), | ||
394 | "margin_available": 0, | ||
395 | "margin_in_position": 0, | ||
396 | "margin_borrowed": 0, | ||
397 | "margin_total": 0, | ||
398 | "margin_pending_gain": 0, | ||
399 | "margin_lending_fees": 0, | ||
400 | "margin_pending_base_gain": 0, | ||
401 | "margin_position_type": None, | ||
402 | "margin_liquidation_price": 0, | ||
403 | "margin_borrowed_base_price": 0, | ||
404 | "margin_borrowed_base_currency": None | ||
405 | } | ||
406 | self.assertEqual(expected_xmr, result["XMR"]) | ||
407 | self.assertEqual(expected_doge, result["DOGE"]) | ||
408 | self.assertEqual(expected_btc, result["BTC"]) | ||
409 | |||
410 | def test_create_margin_order(self): | ||
411 | with self.assertRaises(market.ExchangeError): | ||
412 | self.s.create_margin_order("FOO", "market", "buy", "10") | ||
413 | |||
414 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
415 | mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ | ||
416 | mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ | ||
417 | mock.patch.object(self.s, "market") as market_mock,\ | ||
418 | mock.patch.object(self.s, "price_to_precision") as ptp,\ | ||
419 | mock.patch.object(self.s, "amount_to_precision") as atp: | ||
420 | |||
421 | margin_buy.return_value = { | ||
422 | "orderNumber": 123 | ||
423 | } | ||
424 | margin_sell.return_value = { | ||
425 | "orderNumber": 456 | ||
426 | } | ||
427 | market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" } | ||
428 | ptp.return_value = D("0.1") | ||
429 | atp.return_value = D("12") | ||
430 | |||
431 | order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1") | ||
432 | self.assertEqual(123, order["id"]) | ||
433 | margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | ||
434 | margin_sell.assert_not_called() | ||
435 | margin_buy.reset_mock() | ||
436 | margin_sell.reset_mock() | ||
437 | |||
438 | order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1") | ||
439 | self.assertEqual(456, order["id"]) | ||
440 | margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) | ||
441 | margin_buy.assert_not_called() | ||
442 | |||
443 | def test_create_exchange_order(self): | ||
444 | with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order: | ||
445 | self.s.create_order("symbol", "type", "side", "amount", price="price", params="params") | ||
446 | |||
447 | create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
448 | |||
129 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 449 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
130 | class PortfolioTest(WebMockTestCase): | 450 | class NoopLockTest(unittest.TestCase): |
131 | def fill_data(self): | 451 | def test_with(self): |
132 | if self.json_response is not None: | 452 | noop_lock = store.NoopLock() |
133 | portfolio.Portfolio.data = self.json_response | 453 | with noop_lock: |
454 | self.assertTrue(True) | ||
455 | |||
456 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
457 | class LockedVar(unittest.TestCase): | ||
458 | |||
459 | def test_values(self): | ||
460 | locked_var = store.LockedVar("Foo") | ||
461 | self.assertIsInstance(locked_var.lock, store.NoopLock) | ||
462 | self.assertEqual("Foo", locked_var.val) | ||
463 | |||
464 | def test_get(self): | ||
465 | with self.subTest(desc="Normal case"): | ||
466 | locked_var = store.LockedVar("Foo") | ||
467 | self.assertEqual("Foo", locked_var.get()) | ||
468 | with self.subTest(desc="Dict"): | ||
469 | locked_var = store.LockedVar({"foo": "bar"}) | ||
470 | self.assertEqual({"foo": "bar"}, locked_var.get()) | ||
471 | self.assertEqual("bar", locked_var.get("foo")) | ||
472 | self.assertIsNone(locked_var.get("other")) | ||
473 | |||
474 | def test_set(self): | ||
475 | locked_var = store.LockedVar("Foo") | ||
476 | locked_var.set("Bar") | ||
477 | self.assertEqual("Bar", locked_var.get()) | ||
478 | |||
479 | def test__getattr(self): | ||
480 | dummy = type('Dummy', (object,), {})() | ||
481 | dummy.attribute = "Hey" | ||
482 | |||
483 | locked_var = store.LockedVar(dummy) | ||
484 | self.assertEqual("Hey", locked_var.attribute) | ||
485 | with self.assertRaises(AttributeError): | ||
486 | locked_var.other | ||
487 | |||
488 | def test_start_lock(self): | ||
489 | locked_var = store.LockedVar("Foo") | ||
490 | locked_var.start_lock() | ||
491 | self.assertEqual("lock", locked_var.lock.__class__.__name__) | ||
492 | |||
493 | thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) | ||
494 | thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) | ||
495 | thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) | ||
496 | |||
497 | with locked_var.lock: | ||
498 | thread1.start() | ||
499 | thread2.start() | ||
500 | thread3.start() | ||
501 | |||
502 | self.assertEqual("Foo", locked_var.val) | ||
503 | thread1.join() | ||
504 | thread2.join() | ||
505 | thread3.join() | ||
506 | self.assertEqual("Bar", locked_var.get()[0:3]) | ||
507 | |||
508 | def test_wait_for_notification(self): | ||
509 | with self.assertRaises(RuntimeError): | ||
510 | store.Portfolio.wait_for_notification() | ||
511 | |||
512 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
513 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
514 | mock.patch.object(store.time, "sleep") as sleep: | ||
515 | store.Portfolio.start_worker(poll=3) | ||
516 | |||
517 | store.Portfolio.worker_notify.set() | ||
518 | |||
519 | store.Portfolio.callback.wait() | ||
520 | |||
521 | report.print_log.assert_called_once_with("Fetching cryptoportfolio") | ||
522 | get.assert_called_once_with(refetch=True) | ||
523 | sleep.assert_called_once_with(3) | ||
524 | self.assertFalse(store.Portfolio.worker_notify.is_set()) | ||
525 | self.assertTrue(store.Portfolio.worker.is_alive()) | ||
526 | |||
527 | store.Portfolio.callback.clear() | ||
528 | store.Portfolio.worker_started = False | ||
529 | store.Portfolio.worker_notify.set() | ||
530 | store.Portfolio.callback.wait() | ||
531 | |||
532 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
533 | |||
534 | def test_notify_and_wait(self): | ||
535 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | ||
536 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | ||
537 | store.Portfolio.notify_and_wait() | ||
538 | callback.clear.assert_called_once_with() | ||
539 | worker_notify.set.assert_called_once_with() | ||
540 | callback.wait.assert_called_once_with() | ||
134 | 541 | ||
542 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
543 | class PortfolioTest(WebMockTestCase): | ||
135 | def setUp(self): | 544 | def setUp(self): |
136 | super(PortfolioTest, self).setUp() | 545 | super(PortfolioTest, self).setUp() |
137 | 546 | ||
138 | with open("test_portfolio.json") as example: | 547 | with open("test_samples/test_portfolio.json") as example: |
139 | self.json_response = example.read() | 548 | self.json_response = example.read() |
140 | 549 | ||
141 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) | 550 | self.wm.get(market.Portfolio.URL, text=self.json_response) |
142 | 551 | ||
143 | def test_get_cryptoportfolio(self): | 552 | @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") |
144 | self.wm.get(portfolio.Portfolio.URL, [ | 553 | def test_get_cryptoportfolio(self, parse_cryptoportfolio): |
145 | {"text":'{ "foo": "bar" }', "status_code": 200}, | 554 | with self.subTest(parallel=False): |
146 | {"text": "System Error", "status_code": 500}, | 555 | self.wm.get(market.Portfolio.URL, [ |
147 | {"exc": requests.exceptions.ConnectTimeout}, | 556 | {"text":'{ "foo": "bar" }', "status_code": 200}, |
148 | ]) | 557 | {"text": "System Error", "status_code": 500}, |
149 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 558 | {"exc": requests.exceptions.ConnectTimeout}, |
150 | self.assertIn("foo", portfolio.Portfolio.data) | 559 | ]) |
151 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) | 560 | market.Portfolio.get_cryptoportfolio() |
152 | self.assertTrue(self.wm.called) | 561 | self.assertIn("foo", market.Portfolio.data.get()) |
153 | self.assertEqual(1, self.wm.call_count) | 562 | self.assertEqual("bar", market.Portfolio.data.get()["foo"]) |
154 | self.m.report.log_error.assert_not_called() | 563 | self.assertTrue(self.wm.called) |
155 | self.m.report.log_http_request.assert_called_once() | 564 | self.assertEqual(1, self.wm.call_count) |
156 | self.m.report.log_http_request.reset_mock() | 565 | market.Portfolio.report.log_error.assert_not_called() |
157 | 566 | market.Portfolio.report.log_http_request.assert_called_once() | |
158 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 567 | parse_cryptoportfolio.assert_called_once_with() |
159 | self.assertIsNone(portfolio.Portfolio.data) | 568 | market.Portfolio.report.log_http_request.reset_mock() |
160 | self.assertEqual(2, self.wm.call_count) | 569 | parse_cryptoportfolio.reset_mock() |
161 | self.m.report.log_error.assert_not_called() | 570 | market.Portfolio.data = store.LockedVar(None) |
162 | self.m.report.log_http_request.assert_called_once() | 571 | |
163 | self.m.report.log_http_request.reset_mock() | 572 | market.Portfolio.get_cryptoportfolio() |
164 | 573 | self.assertIsNone(market.Portfolio.data.get()) | |
165 | 574 | self.assertEqual(2, self.wm.call_count) | |
166 | portfolio.Portfolio.data = "Foo" | 575 | parse_cryptoportfolio.assert_not_called() |
167 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 576 | market.Portfolio.report.log_error.assert_not_called() |
168 | self.assertEqual("Foo", portfolio.Portfolio.data) | 577 | market.Portfolio.report.log_http_request.assert_called_once() |
169 | self.assertEqual(3, self.wm.call_count) | 578 | market.Portfolio.report.log_http_request.reset_mock() |
170 | self.m.report.log_error.assert_called_once_with("get_cryptoportfolio", | 579 | parse_cryptoportfolio.reset_mock() |
171 | exception=mock.ANY) | 580 | |
172 | self.m.report.log_http_request.assert_not_called() | 581 | market.Portfolio.data = store.LockedVar("Foo") |
582 | market.Portfolio.get_cryptoportfolio() | ||
583 | self.assertEqual(2, self.wm.call_count) | ||
584 | parse_cryptoportfolio.assert_not_called() | ||
585 | |||
586 | market.Portfolio.get_cryptoportfolio(refetch=True) | ||
587 | self.assertEqual("Foo", market.Portfolio.data.get()) | ||
588 | self.assertEqual(3, self.wm.call_count) | ||
589 | market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", | ||
590 | exception=mock.ANY) | ||
591 | market.Portfolio.report.log_http_request.assert_not_called() | ||
592 | with self.subTest(parallel=True): | ||
593 | with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ | ||
594 | mock.patch.object(market.Portfolio, "notify_and_wait") as notify: | ||
595 | with self.subTest(worker=True): | ||
596 | market.Portfolio.data = store.LockedVar(None) | ||
597 | market.Portfolio.worker = mock.Mock() | ||
598 | is_worker.return_value = True | ||
599 | self.wm.get(market.Portfolio.URL, [ | ||
600 | {"text":'{ "foo": "bar" }', "status_code": 200}, | ||
601 | ]) | ||
602 | market.Portfolio.get_cryptoportfolio() | ||
603 | self.assertIn("foo", market.Portfolio.data.get()) | ||
604 | parse_cryptoportfolio.reset_mock() | ||
605 | with self.subTest(worker=False): | ||
606 | market.Portfolio.data = store.LockedVar(None) | ||
607 | market.Portfolio.worker = mock.Mock() | ||
608 | is_worker.return_value = False | ||
609 | market.Portfolio.get_cryptoportfolio() | ||
610 | notify.assert_called_once_with() | ||
611 | parse_cryptoportfolio.assert_not_called() | ||
173 | 612 | ||
174 | def test_parse_cryptoportfolio(self): | 613 | def test_parse_cryptoportfolio(self): |
175 | portfolio.Portfolio.parse_cryptoportfolio(self.m) | 614 | with self.subTest(description="Normal case"): |
176 | 615 | market.Portfolio.data = store.LockedVar(store.json.loads( | |
177 | self.assertListEqual( | 616 | self.json_response, parse_int=D, parse_float=D)) |
178 | ["medium", "high"], | 617 | market.Portfolio.parse_cryptoportfolio() |
179 | list(portfolio.Portfolio.liquidities.keys())) | ||
180 | |||
181 | liquidities = portfolio.Portfolio.liquidities | ||
182 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
183 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
184 | |||
185 | expected = { | ||
186 | 'BTC': (D("0.2857"), "long"), | ||
187 | 'DGB': (D("0.1015"), "long"), | ||
188 | 'DOGE': (D("0.1805"), "long"), | ||
189 | 'SC': (D("0.0623"), "long"), | ||
190 | 'ZEC': (D("0.3701"), "long"), | ||
191 | } | ||
192 | date = portfolio.datetime(2018, 1, 8) | ||
193 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
194 | |||
195 | expected = { | ||
196 | 'BTC': (D("1.1102e-16"), "long"), | ||
197 | 'ETC': (D("0.1"), "long"), | ||
198 | 'FCT': (D("0.1"), "long"), | ||
199 | 'GAS': (D("0.1"), "long"), | ||
200 | 'NAV': (D("0.1"), "long"), | ||
201 | 'OMG': (D("0.1"), "long"), | ||
202 | 'OMNI': (D("0.1"), "long"), | ||
203 | 'PPC': (D("0.1"), "long"), | ||
204 | 'RIC': (D("0.1"), "long"), | ||
205 | 'VIA': (D("0.1"), "long"), | ||
206 | 'XCP': (D("0.1"), "long"), | ||
207 | } | ||
208 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
209 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) | ||
210 | |||
211 | self.m.report.log_http_request.assert_called_once_with("GET", | ||
212 | portfolio.Portfolio.URL, None, mock.ANY, mock.ANY) | ||
213 | self.m.report.log_http_request.reset_mock() | ||
214 | |||
215 | # It doesn't refetch the data when available | ||
216 | portfolio.Portfolio.parse_cryptoportfolio(self.m) | ||
217 | self.m.report.log_http_request.assert_not_called() | ||
218 | |||
219 | self.assertEqual(1, self.wm.call_count) | ||
220 | |||
221 | portfolio.Portfolio.parse_cryptoportfolio(self.m, refetch=True) | ||
222 | self.assertEqual(2, self.wm.call_count) | ||
223 | self.m.report.log_http_request.assert_called_once() | ||
224 | |||
225 | def test_repartition(self): | ||
226 | expected_medium = { | ||
227 | 'BTC': (D("1.1102e-16"), "long"), | ||
228 | 'USDT': (D("0.1"), "long"), | ||
229 | 'ETC': (D("0.1"), "long"), | ||
230 | 'FCT': (D("0.1"), "long"), | ||
231 | 'OMG': (D("0.1"), "long"), | ||
232 | 'STEEM': (D("0.1"), "long"), | ||
233 | 'STRAT': (D("0.1"), "long"), | ||
234 | 'XEM': (D("0.1"), "long"), | ||
235 | 'XMR': (D("0.1"), "long"), | ||
236 | 'XVC': (D("0.1"), "long"), | ||
237 | 'ZRX': (D("0.1"), "long"), | ||
238 | } | ||
239 | expected_high = { | ||
240 | 'USDT': (D("0.1226"), "long"), | ||
241 | 'BTC': (D("0.1429"), "long"), | ||
242 | 'ETC': (D("0.1127"), "long"), | ||
243 | 'ETH': (D("0.1569"), "long"), | ||
244 | 'FCT': (D("0.3341"), "long"), | ||
245 | 'GAS': (D("0.1308"), "long"), | ||
246 | } | ||
247 | 618 | ||
248 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m)) | 619 | self.assertListEqual( |
249 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium")) | 620 | ["medium", "high"], |
250 | self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high")) | 621 | list(market.Portfolio.liquidities.get().keys())) |
251 | 622 | ||
252 | self.assertEqual(1, self.wm.call_count) | 623 | liquidities = market.Portfolio.liquidities.get() |
624 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
625 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
253 | 626 | ||
254 | portfolio.Portfolio.repartition(self.m) | 627 | expected = { |
255 | self.assertEqual(1, self.wm.call_count) | 628 | 'BTC': (D("0.2857"), "long"), |
629 | 'DGB': (D("0.1015"), "long"), | ||
630 | 'DOGE': (D("0.1805"), "long"), | ||
631 | 'SC': (D("0.0623"), "long"), | ||
632 | 'ZEC': (D("0.3701"), "long"), | ||
633 | } | ||
634 | date = portfolio.datetime(2018, 1, 8) | ||
635 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
256 | 636 | ||
257 | portfolio.Portfolio.repartition(self.m, refetch=True) | 637 | expected = { |
258 | self.assertEqual(2, self.wm.call_count) | 638 | 'BTC': (D("1.1102e-16"), "long"), |
259 | self.m.report.log_http_request.assert_called() | 639 | 'ETC': (D("0.1"), "long"), |
260 | self.assertEqual(2, self.m.report.log_http_request.call_count) | 640 | 'FCT': (D("0.1"), "long"), |
641 | 'GAS': (D("0.1"), "long"), | ||
642 | 'NAV': (D("0.1"), "long"), | ||
643 | 'OMG': (D("0.1"), "long"), | ||
644 | 'OMNI': (D("0.1"), "long"), | ||
645 | 'PPC': (D("0.1"), "long"), | ||
646 | 'RIC': (D("0.1"), "long"), | ||
647 | 'VIA': (D("0.1"), "long"), | ||
648 | 'XCP': (D("0.1"), "long"), | ||
649 | } | ||
650 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
651 | self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date.get()) | ||
652 | |||
653 | with self.subTest(description="Missing weight"): | ||
654 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
655 | del(data["portfolio_2"]["weights"]) | ||
656 | market.Portfolio.data = store.LockedVar(data) | ||
657 | |||
658 | market.Portfolio.parse_cryptoportfolio() | ||
659 | self.assertListEqual( | ||
660 | ["medium", "high"], | ||
661 | list(market.Portfolio.liquidities.get().keys())) | ||
662 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
663 | |||
664 | with self.subTest(description="All missing weights"): | ||
665 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
666 | del(data["portfolio_1"]["weights"]) | ||
667 | del(data["portfolio_2"]["weights"]) | ||
668 | market.Portfolio.data = store.LockedVar(data) | ||
669 | |||
670 | market.Portfolio.parse_cryptoportfolio() | ||
671 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
672 | self.assertEqual({}, market.Portfolio.liquidities.get("high")) | ||
673 | self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) | ||
674 | |||
675 | |||
676 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
677 | def test_repartition(self, get_cryptoportfolio): | ||
678 | market.Portfolio.liquidities = store.LockedVar({ | ||
679 | "medium": { | ||
680 | "2018-03-01": "medium_2018-03-01", | ||
681 | "2018-03-08": "medium_2018-03-08", | ||
682 | }, | ||
683 | "high": { | ||
684 | "2018-03-01": "high_2018-03-01", | ||
685 | "2018-03-08": "high_2018-03-08", | ||
686 | } | ||
687 | }) | ||
688 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | ||
261 | 689 | ||
262 | @mock.patch.object(portfolio.time, "sleep") | 690 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) |
263 | @mock.patch.object(portfolio.Portfolio, "repartition") | 691 | get_cryptoportfolio.assert_called_once_with() |
264 | def test_wait_for_recent(self, repartition, sleep): | 692 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium")) |
693 | self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high")) | ||
694 | |||
695 | @mock.patch.object(market.time, "sleep") | ||
696 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
697 | def test_wait_for_recent(self, get_cryptoportfolio, sleep): | ||
265 | self.call_count = 0 | 698 | self.call_count = 0 |
266 | def _repartition(market, refetch): | 699 | def _get(refetch=False): |
267 | self.assertEqual(self.m, market) | 700 | if self.call_count != 0: |
268 | self.assertTrue(refetch) | 701 | self.assertTrue(refetch) |
702 | else: | ||
703 | self.assertFalse(refetch) | ||
269 | self.call_count += 1 | 704 | self.call_count += 1 |
270 | portfolio.Portfolio.last_date = portfolio.datetime.now()\ | 705 | market.Portfolio.last_date = store.LockedVar(store.datetime.now()\ |
271 | - portfolio.timedelta(10)\ | 706 | - store.timedelta(10)\ |
272 | + portfolio.timedelta(self.call_count) | 707 | + store.timedelta(self.call_count)) |
273 | repartition.side_effect = _repartition | 708 | get_cryptoportfolio.side_effect = _get |
274 | 709 | ||
275 | portfolio.Portfolio.wait_for_recent(self.m) | 710 | market.Portfolio.wait_for_recent() |
276 | sleep.assert_called_with(30) | 711 | sleep.assert_called_with(30) |
277 | self.assertEqual(6, sleep.call_count) | 712 | self.assertEqual(6, sleep.call_count) |
278 | self.assertEqual(7, repartition.call_count) | 713 | self.assertEqual(7, get_cryptoportfolio.call_count) |
279 | self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | 714 | market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") |
280 | 715 | ||
281 | sleep.reset_mock() | 716 | sleep.reset_mock() |
282 | repartition.reset_mock() | 717 | get_cryptoportfolio.reset_mock() |
283 | portfolio.Portfolio.last_date = None | 718 | market.Portfolio.last_date = store.LockedVar(None) |
284 | self.call_count = 0 | 719 | self.call_count = 0 |
285 | portfolio.Portfolio.wait_for_recent(self.m, delta=15) | 720 | market.Portfolio.wait_for_recent(delta=15) |
286 | sleep.assert_not_called() | 721 | sleep.assert_not_called() |
287 | self.assertEqual(1, repartition.call_count) | 722 | self.assertEqual(1, get_cryptoportfolio.call_count) |
288 | 723 | ||
289 | sleep.reset_mock() | 724 | sleep.reset_mock() |
290 | repartition.reset_mock() | 725 | get_cryptoportfolio.reset_mock() |
291 | portfolio.Portfolio.last_date = None | 726 | market.Portfolio.last_date = store.LockedVar(None) |
292 | self.call_count = 0 | 727 | self.call_count = 0 |
293 | portfolio.Portfolio.wait_for_recent(self.m, delta=1) | 728 | market.Portfolio.wait_for_recent(delta=1) |
294 | sleep.assert_called_with(30) | 729 | sleep.assert_called_with(30) |
295 | self.assertEqual(9, sleep.call_count) | 730 | self.assertEqual(9, sleep.call_count) |
296 | self.assertEqual(10, repartition.call_count) | 731 | self.assertEqual(10, get_cryptoportfolio.call_count) |
732 | |||
733 | def test_is_worker_thread(self): | ||
734 | with self.subTest(worker=None): | ||
735 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
736 | |||
737 | with self.subTest(worker="not self"),\ | ||
738 | mock.patch("threading.current_thread") as current_thread: | ||
739 | current = mock.Mock() | ||
740 | current_thread.return_value = current | ||
741 | store.Portfolio.worker = mock.Mock() | ||
742 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
743 | |||
744 | with self.subTest(worker="self"),\ | ||
745 | mock.patch("threading.current_thread") as current_thread: | ||
746 | current = mock.Mock() | ||
747 | current_thread.return_value = current | ||
748 | store.Portfolio.worker = current | ||
749 | self.assertTrue(store.Portfolio.is_worker_thread()) | ||
750 | |||
751 | def test_start_worker(self): | ||
752 | with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: | ||
753 | store.Portfolio.start_worker() | ||
754 | notification.assert_called_once_with(poll=30) | ||
755 | |||
756 | self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) | ||
757 | self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) | ||
758 | store.Portfolio.report.start_lock.assert_called_once_with() | ||
759 | |||
760 | self.assertIsNotNone(store.Portfolio.worker) | ||
761 | self.assertIsNotNone(store.Portfolio.worker_notify) | ||
762 | self.assertIsNotNone(store.Portfolio.callback) | ||
763 | self.assertTrue(store.Portfolio.worker_started) | ||
297 | 764 | ||
298 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 765 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
299 | class AmountTest(WebMockTestCase): | 766 | class AmountTest(WebMockTestCase): |
@@ -736,7 +1203,7 @@ class MarketTest(WebMockTestCase): | |||
736 | self.assertEqual("Foo", m.fetch_fees()) | 1203 | self.assertEqual("Foo", m.fetch_fees()) |
737 | self.ccxt.fetch_fees.assert_not_called() | 1204 | self.ccxt.fetch_fees.assert_not_called() |
738 | 1205 | ||
739 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1206 | @mock.patch.object(market.Portfolio, "repartition") |
740 | @mock.patch.object(market.Market, "get_ticker") | 1207 | @mock.patch.object(market.Market, "get_ticker") |
741 | @mock.patch.object(market.TradeStore, "compute_trades") | 1208 | @mock.patch.object(market.TradeStore, "compute_trades") |
742 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 1209 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
@@ -787,7 +1254,7 @@ class MarketTest(WebMockTestCase): | |||
787 | m.report.log_balances.assert_called_once_with(tag="tag") | 1254 | m.report.log_balances.assert_called_once_with(tag="tag") |
788 | 1255 | ||
789 | 1256 | ||
790 | @mock.patch.object(portfolio.time, "sleep") | 1257 | @mock.patch.object(market.time, "sleep") |
791 | @mock.patch.object(market.TradeStore, "all_orders") | 1258 | @mock.patch.object(market.TradeStore, "all_orders") |
792 | def test_follow_orders(self, all_orders, time_mock): | 1259 | def test_follow_orders(self, all_orders, time_mock): |
793 | for debug, sleep in [ | 1260 | for debug, sleep in [ |
@@ -907,7 +1374,172 @@ class MarketTest(WebMockTestCase): | |||
907 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 1374 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
908 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") | 1375 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") |
909 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") | 1376 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") |
910 | 1377 | ||
1378 | def test_store_report(self): | ||
1379 | |||
1380 | file_open = mock.mock_open() | ||
1381 | m = market.Market(self.ccxt, user_id=1) | ||
1382 | with self.subTest(file=None),\ | ||
1383 | mock.patch.object(m, "report") as report,\ | ||
1384 | mock.patch("market.open", file_open): | ||
1385 | m.store_report() | ||
1386 | report.merge.assert_called_with(store.Portfolio.report) | ||
1387 | file_open.assert_not_called() | ||
1388 | |||
1389 | report.reset_mock() | ||
1390 | file_open = mock.mock_open() | ||
1391 | m = market.Market(self.ccxt, report_path="present", user_id=1) | ||
1392 | with self.subTest(file="present"),\ | ||
1393 | mock.patch("market.open", file_open),\ | ||
1394 | mock.patch.object(m, "report") as report,\ | ||
1395 | mock.patch.object(market, "datetime") as time_mock: | ||
1396 | |||
1397 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1398 | report.to_json.return_value = "json_content" | ||
1399 | |||
1400 | m.store_report() | ||
1401 | |||
1402 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
1403 | file_open().write.assert_called_once_with("json_content") | ||
1404 | m.report.to_json.assert_called_once_with() | ||
1405 | report.merge.assert_called_with(store.Portfolio.report) | ||
1406 | |||
1407 | report.reset_mock() | ||
1408 | |||
1409 | m = market.Market(self.ccxt, report_path="error", user_id=1) | ||
1410 | with self.subTest(file="error"),\ | ||
1411 | mock.patch("market.open") as file_open,\ | ||
1412 | mock.patch.object(m, "report") as report,\ | ||
1413 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
1414 | file_open.side_effect = FileNotFoundError | ||
1415 | |||
1416 | m.store_report() | ||
1417 | |||
1418 | report.merge.assert_called_with(store.Portfolio.report) | ||
1419 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
1420 | |||
1421 | def test_print_orders(self): | ||
1422 | m = market.Market(self.ccxt) | ||
1423 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1424 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1425 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | ||
1426 | mock.patch.object(m.trades, "prepare_orders") as prepare_orders: | ||
1427 | m.print_orders() | ||
1428 | |||
1429 | log_stage.assert_called_with("print_orders") | ||
1430 | fetch_balances.assert_called_with(tag="print_orders") | ||
1431 | prepare_trades.assert_called_with(base_currency="BTC", | ||
1432 | compute_value="average") | ||
1433 | prepare_orders.assert_called_with(compute_value="average") | ||
1434 | |||
1435 | def test_print_balances(self): | ||
1436 | m = market.Market(self.ccxt) | ||
1437 | |||
1438 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | ||
1439 | mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1440 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1441 | mock.patch.object(m.report, "print_log") as print_log: | ||
1442 | |||
1443 | in_currency.return_value = { | ||
1444 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
1445 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
1446 | } | ||
1447 | |||
1448 | m.print_balances() | ||
1449 | |||
1450 | log_stage.assert_called_once_with("print_balances") | ||
1451 | fetch_balances.assert_called_with() | ||
1452 | print_log.assert_has_calls([ | ||
1453 | mock.call("total:"), | ||
1454 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
1455 | ]) | ||
1456 | |||
1457 | @mock.patch("market.Processor.process") | ||
1458 | @mock.patch("market.ReportStore.log_error") | ||
1459 | @mock.patch("market.Market.store_report") | ||
1460 | def test_process(self, store_report, log_error, process): | ||
1461 | m = market.Market(self.ccxt) | ||
1462 | with self.subTest(before=False, after=False): | ||
1463 | m.process(None) | ||
1464 | |||
1465 | process.assert_not_called() | ||
1466 | store_report.assert_called_once() | ||
1467 | log_error.assert_not_called() | ||
1468 | |||
1469 | process.reset_mock() | ||
1470 | log_error.reset_mock() | ||
1471 | store_report.reset_mock() | ||
1472 | with self.subTest(before=True, after=False): | ||
1473 | m.process(None, before=True) | ||
1474 | |||
1475 | process.assert_called_once_with("sell_all", steps="before") | ||
1476 | store_report.assert_called_once() | ||
1477 | log_error.assert_not_called() | ||
1478 | |||
1479 | process.reset_mock() | ||
1480 | log_error.reset_mock() | ||
1481 | store_report.reset_mock() | ||
1482 | with self.subTest(before=False, after=True): | ||
1483 | m.process(None, after=True) | ||
1484 | |||
1485 | process.assert_called_once_with("sell_all", steps="after") | ||
1486 | store_report.assert_called_once() | ||
1487 | log_error.assert_not_called() | ||
1488 | |||
1489 | process.reset_mock() | ||
1490 | log_error.reset_mock() | ||
1491 | store_report.reset_mock() | ||
1492 | with self.subTest(before=True, after=True): | ||
1493 | m.process(None, before=True, after=True) | ||
1494 | |||
1495 | process.assert_has_calls([ | ||
1496 | mock.call("sell_all", steps="before"), | ||
1497 | mock.call("sell_all", steps="after"), | ||
1498 | ]) | ||
1499 | store_report.assert_called_once() | ||
1500 | log_error.assert_not_called() | ||
1501 | |||
1502 | process.reset_mock() | ||
1503 | log_error.reset_mock() | ||
1504 | store_report.reset_mock() | ||
1505 | with self.subTest(action="print_balances"),\ | ||
1506 | mock.patch.object(m, "print_balances") as print_balances: | ||
1507 | m.process(["print_balances"]) | ||
1508 | |||
1509 | process.assert_not_called() | ||
1510 | log_error.assert_not_called() | ||
1511 | store_report.assert_called_once() | ||
1512 | print_balances.assert_called_once_with() | ||
1513 | |||
1514 | log_error.reset_mock() | ||
1515 | store_report.reset_mock() | ||
1516 | with self.subTest(action="print_orders"),\ | ||
1517 | mock.patch.object(m, "print_orders") as print_orders,\ | ||
1518 | mock.patch.object(m, "print_balances") as print_balances: | ||
1519 | m.process(["print_orders", "print_balances"]) | ||
1520 | |||
1521 | process.assert_not_called() | ||
1522 | log_error.assert_not_called() | ||
1523 | store_report.assert_called_once() | ||
1524 | print_orders.assert_called_once_with() | ||
1525 | print_balances.assert_called_once_with() | ||
1526 | |||
1527 | log_error.reset_mock() | ||
1528 | store_report.reset_mock() | ||
1529 | with self.subTest(action="unknown"): | ||
1530 | m.process(["unknown"]) | ||
1531 | log_error.assert_called_once_with("market_process", message="Unknown action unknown") | ||
1532 | store_report.assert_called_once() | ||
1533 | |||
1534 | log_error.reset_mock() | ||
1535 | store_report.reset_mock() | ||
1536 | with self.subTest(unhandled_exception=True): | ||
1537 | process.side_effect = Exception("bouh") | ||
1538 | |||
1539 | m.process(None, before=True) | ||
1540 | log_error.assert_called_with("market_process", exception=mock.ANY) | ||
1541 | store_report.assert_called_once() | ||
1542 | |||
911 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1543 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
912 | class TradeStoreTest(WebMockTestCase): | 1544 | class TradeStoreTest(WebMockTestCase): |
913 | def test_compute_trades(self): | 1545 | def test_compute_trades(self): |
@@ -1226,7 +1858,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1226 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) | 1858 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) |
1227 | self.m.report.log_balances.assert_called_with(tag="foo") | 1859 | self.m.report.log_balances.assert_called_with(tag="foo") |
1228 | 1860 | ||
1229 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1861 | @mock.patch.object(market.Portfolio, "repartition") |
1230 | def test_dispatch_assets(self, repartition): | 1862 | def test_dispatch_assets(self, repartition): |
1231 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | 1863 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
1232 | 1864 | ||
@@ -1243,7 +1875,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1243 | repartition.return_value = repartition_hash | 1875 | repartition.return_value = repartition_hash |
1244 | 1876 | ||
1245 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) | 1877 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) |
1246 | repartition.assert_called_with(self.m, liquidity="medium") | 1878 | repartition.assert_called_with(liquidity="medium") |
1247 | self.assertIn("XEM", balance_store.currencies()) | 1879 | self.assertIn("XEM", balance_store.currencies()) |
1248 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 1880 | self.assertEqual(D("2.6"), amounts["BTC"].value) |
1249 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 1881 | self.assertEqual(D("7.5"), amounts["XEM"].value) |
@@ -2334,6 +2966,19 @@ class ReportStoreTest(WebMockTestCase): | |||
2334 | report_store.set_verbose(False) | 2966 | report_store.set_verbose(False) |
2335 | self.assertFalse(report_store.verbose_print) | 2967 | self.assertFalse(report_store.verbose_print) |
2336 | 2968 | ||
2969 | def test_merge(self): | ||
2970 | report_store1 = market.ReportStore(self.m, verbose_print=False) | ||
2971 | report_store2 = market.ReportStore(None, verbose_print=False) | ||
2972 | |||
2973 | report_store2.log_stage("1") | ||
2974 | report_store1.log_stage("2") | ||
2975 | report_store2.log_stage("3") | ||
2976 | |||
2977 | report_store1.merge(report_store2) | ||
2978 | |||
2979 | self.assertEqual(3, len(report_store1.logs)) | ||
2980 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | ||
2981 | |||
2337 | def test_print_log(self): | 2982 | def test_print_log(self): |
2338 | report_store = market.ReportStore(self.m) | 2983 | report_store = market.ReportStore(self.m) |
2339 | with self.subTest(verbose=True),\ | 2984 | with self.subTest(verbose=True),\ |
@@ -2752,7 +3397,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2752 | }) | 3397 | }) |
2753 | 3398 | ||
2754 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3399 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2755 | class HelperTest(WebMockTestCase): | 3400 | class MainTest(WebMockTestCase): |
2756 | def test_make_order(self): | 3401 | def test_make_order(self): |
2757 | self.m.get_ticker.return_value = { | 3402 | self.m.get_ticker.return_value = { |
2758 | "inverted": False, | 3403 | "inverted": False, |
@@ -2762,7 +3407,7 @@ class HelperTest(WebMockTestCase): | |||
2762 | } | 3407 | } |
2763 | 3408 | ||
2764 | with self.subTest(description="nominal case"): | 3409 | with self.subTest(description="nominal case"): |
2765 | helper.make_order(self.m, 10, "ETH") | 3410 | main.make_order(self.m, 10, "ETH") |
2766 | 3411 | ||
2767 | self.m.report.log_stage.assert_has_calls([ | 3412 | self.m.report.log_stage.assert_has_calls([ |
2768 | mock.call("make_order_begin"), | 3413 | mock.call("make_order_begin"), |
@@ -2787,7 +3432,7 @@ class HelperTest(WebMockTestCase): | |||
2787 | 3432 | ||
2788 | self.m.reset_mock() | 3433 | self.m.reset_mock() |
2789 | with self.subTest(compute_value="default"): | 3434 | with self.subTest(compute_value="default"): |
2790 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3435 | main.make_order(self.m, 10, "ETH", action="dispose", |
2791 | compute_value="ask") | 3436 | compute_value="ask") |
2792 | 3437 | ||
2793 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3438 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
@@ -2796,7 +3441,7 @@ class HelperTest(WebMockTestCase): | |||
2796 | 3441 | ||
2797 | self.m.reset_mock() | 3442 | self.m.reset_mock() |
2798 | with self.subTest(follow=False): | 3443 | with self.subTest(follow=False): |
2799 | result = helper.make_order(self.m, 10, "ETH", follow=False) | 3444 | result = main.make_order(self.m, 10, "ETH", follow=False) |
2800 | 3445 | ||
2801 | self.m.report.log_stage.assert_has_calls([ | 3446 | self.m.report.log_stage.assert_has_calls([ |
2802 | mock.call("make_order_begin"), | 3447 | mock.call("make_order_begin"), |
@@ -2816,7 +3461,7 @@ class HelperTest(WebMockTestCase): | |||
2816 | 3461 | ||
2817 | self.m.reset_mock() | 3462 | self.m.reset_mock() |
2818 | with self.subTest(base_currency="USDT"): | 3463 | with self.subTest(base_currency="USDT"): |
2819 | helper.make_order(self.m, 1, "BTC", base_currency="USDT") | 3464 | main.make_order(self.m, 1, "BTC", base_currency="USDT") |
2820 | 3465 | ||
2821 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3466 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2822 | self.assertEqual("BTC", trade.currency) | 3467 | self.assertEqual("BTC", trade.currency) |
@@ -2824,14 +3469,14 @@ class HelperTest(WebMockTestCase): | |||
2824 | 3469 | ||
2825 | self.m.reset_mock() | 3470 | self.m.reset_mock() |
2826 | with self.subTest(close_if_possible=True): | 3471 | with self.subTest(close_if_possible=True): |
2827 | helper.make_order(self.m, 10, "ETH", close_if_possible=True) | 3472 | main.make_order(self.m, 10, "ETH", close_if_possible=True) |
2828 | 3473 | ||
2829 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3474 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2830 | self.assertEqual(True, trade.orders[0].close_if_possible) | 3475 | self.assertEqual(True, trade.orders[0].close_if_possible) |
2831 | 3476 | ||
2832 | self.m.reset_mock() | 3477 | self.m.reset_mock() |
2833 | with self.subTest(action="dispose"): | 3478 | with self.subTest(action="dispose"): |
2834 | helper.make_order(self.m, 10, "ETH", action="dispose") | 3479 | main.make_order(self.m, 10, "ETH", action="dispose") |
2835 | 3480 | ||
2836 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3481 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2837 | self.assertEqual(0, trade.value_to) | 3482 | self.assertEqual(0, trade.value_to) |
@@ -2841,19 +3486,19 @@ class HelperTest(WebMockTestCase): | |||
2841 | 3486 | ||
2842 | self.m.reset_mock() | 3487 | self.m.reset_mock() |
2843 | with self.subTest(compute_value="default"): | 3488 | with self.subTest(compute_value="default"): |
2844 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3489 | main.make_order(self.m, 10, "ETH", action="dispose", |
2845 | compute_value="bid") | 3490 | compute_value="bid") |
2846 | 3491 | ||
2847 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3492 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2848 | self.assertEqual(D("0.9"), trade.value_from.value) | 3493 | self.assertEqual(D("0.9"), trade.value_from.value) |
2849 | 3494 | ||
2850 | def test_user_market(self): | 3495 | def test_get_user_market(self): |
2851 | with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ | 3496 | with mock.patch("main.fetch_markets") as main_fetch_markets,\ |
2852 | mock.patch("helper.main_parse_config") as main_parse_config: | 3497 | mock.patch("main.parse_config") as main_parse_config: |
2853 | with self.subTest(debug=False): | 3498 | with self.subTest(debug=False): |
2854 | main_parse_config.return_value = ["pg_config", "report_path"] | 3499 | main_parse_config.return_value = ["pg_config", "report_path"] |
2855 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3500 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2856 | m = helper.get_user_market("config_path.ini", 1) | 3501 | m = main.get_user_market("config_path.ini", 1) |
2857 | 3502 | ||
2858 | self.assertIsInstance(m, market.Market) | 3503 | self.assertIsInstance(m, market.Market) |
2859 | self.assertFalse(m.debug) | 3504 | self.assertFalse(m.debug) |
@@ -2861,141 +3506,100 @@ class HelperTest(WebMockTestCase): | |||
2861 | with self.subTest(debug=True): | 3506 | with self.subTest(debug=True): |
2862 | main_parse_config.return_value = ["pg_config", "report_path"] | 3507 | main_parse_config.return_value = ["pg_config", "report_path"] |
2863 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3508 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2864 | m = helper.get_user_market("config_path.ini", 1, debug=True) | 3509 | m = main.get_user_market("config_path.ini", 1, debug=True) |
2865 | 3510 | ||
2866 | self.assertIsInstance(m, market.Market) | 3511 | self.assertIsInstance(m, market.Market) |
2867 | self.assertTrue(m.debug) | 3512 | self.assertTrue(m.debug) |
2868 | 3513 | ||
2869 | def test_main_store_report(self): | 3514 | def test_process(self): |
2870 | file_open = mock.mock_open() | 3515 | with mock.patch("market.Market") as market_mock,\ |
2871 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | ||
2872 | helper.main_store_report(None, 1, self.m) | ||
2873 | file_open.assert_not_called() | ||
2874 | |||
2875 | file_open = mock.mock_open() | ||
2876 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2877 | mock.patch.object(helper, "datetime") as time_mock: | ||
2878 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2879 | self.m.report.to_json.return_value = "json_content" | ||
2880 | |||
2881 | helper.main_store_report("present", 1, self.m) | ||
2882 | |||
2883 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2884 | file_open().write.assert_called_once_with("json_content") | ||
2885 | self.m.report.to_json.assert_called_once_with() | ||
2886 | |||
2887 | with self.subTest(file="error"),\ | ||
2888 | mock.patch("helper.open") as file_open,\ | ||
2889 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3516 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2890 | file_open.side_effect = FileNotFoundError | ||
2891 | 3517 | ||
2892 | helper.main_store_report("error", 1, self.m) | 3518 | args_mock = mock.Mock() |
2893 | 3519 | args_mock.action = "action" | |
2894 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 3520 | args_mock.config = "config" |
2895 | 3521 | args_mock.user = "user" | |
2896 | @mock.patch("helper.Processor.process") | 3522 | args_mock.debug = "debug" |
2897 | def test_main_process_market(self, process): | 3523 | args_mock.before = "before" |
2898 | with self.subTest(before=False, after=False): | 3524 | args_mock.after = "after" |
2899 | m = mock.Mock() | 3525 | self.assertEqual("", stdout_mock.getvalue()) |
2900 | helper.main_process_market(m, None) | ||
2901 | |||
2902 | process.assert_not_called() | ||
2903 | |||
2904 | process.reset_mock() | ||
2905 | with self.subTest(before=True, after=False): | ||
2906 | helper.main_process_market(m, None, before=True) | ||
2907 | |||
2908 | process.assert_called_once_with("sell_all", steps="before") | ||
2909 | |||
2910 | process.reset_mock() | ||
2911 | with self.subTest(before=False, after=True): | ||
2912 | helper.main_process_market(m, None, after=True) | ||
2913 | |||
2914 | process.assert_called_once_with("sell_all", steps="after") | ||
2915 | 3526 | ||
2916 | process.reset_mock() | 3527 | main.process("config", 1, "report_path", args_mock) |
2917 | with self.subTest(before=True, after=True): | ||
2918 | helper.main_process_market(m, None, before=True, after=True) | ||
2919 | 3528 | ||
2920 | process.assert_has_calls([ | 3529 | market_mock.from_config.assert_has_calls([ |
2921 | mock.call("sell_all", steps="before"), | 3530 | mock.call("config", debug="debug", user_id=1, report_path="report_path"), |
2922 | mock.call("sell_all", steps="after"), | 3531 | mock.call().process("action", before="before", after="after"), |
2923 | ]) | 3532 | ]) |
2924 | 3533 | ||
2925 | process.reset_mock() | 3534 | with self.subTest(exception=True): |
2926 | with self.subTest(action="print_balances"),\ | 3535 | market_mock.from_config.side_effect = Exception("boo") |
2927 | mock.patch("helper.print_balances") as print_balances: | 3536 | main.process("config", 1, "report_path", args_mock) |
2928 | helper.main_process_market("user", ["print_balances"]) | 3537 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) |
2929 | |||
2930 | process.assert_not_called() | ||
2931 | print_balances.assert_called_once_with("user") | ||
2932 | |||
2933 | with self.subTest(action="print_orders"),\ | ||
2934 | mock.patch("helper.print_orders") as print_orders,\ | ||
2935 | mock.patch("helper.print_balances") as print_balances: | ||
2936 | helper.main_process_market("user", ["print_orders", "print_balances"]) | ||
2937 | |||
2938 | process.assert_not_called() | ||
2939 | print_orders.assert_called_once_with("user") | ||
2940 | print_balances.assert_called_once_with("user") | ||
2941 | |||
2942 | with self.subTest(action="unknown"),\ | ||
2943 | self.assertRaises(NotImplementedError): | ||
2944 | helper.main_process_market("user", ["unknown"]) | ||
2945 | 3538 | ||
2946 | @mock.patch.object(helper, "psycopg2") | 3539 | def test_main(self): |
2947 | def test_fetch_markets(self, psycopg2): | 3540 | with self.subTest(parallel=False): |
2948 | connect_mock = mock.Mock() | 3541 | with mock.patch("main.parse_args") as parse_args,\ |
2949 | cursor_mock = mock.MagicMock() | 3542 | mock.patch("main.parse_config") as parse_config,\ |
2950 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | 3543 | mock.patch("main.fetch_markets") as fetch_markets,\ |
2951 | 3544 | mock.patch("main.process") as process: | |
2952 | connect_mock.cursor.return_value = cursor_mock | ||
2953 | psycopg2.connect.return_value = connect_mock | ||
2954 | |||
2955 | with self.subTest(user=None): | ||
2956 | rows = list(helper.main_fetch_markets({"foo": "bar"}, None)) | ||
2957 | |||
2958 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2959 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
2960 | |||
2961 | self.assertEqual(["row_1", "row_2"], rows) | ||
2962 | |||
2963 | psycopg2.connect.reset_mock() | ||
2964 | cursor_mock.execute.reset_mock() | ||
2965 | with self.subTest(user=1): | ||
2966 | rows = list(helper.main_fetch_markets({"foo": "bar"}, 1)) | ||
2967 | 3545 | ||
2968 | psycopg2.connect.assert_called_once_with(foo="bar") | 3546 | args_mock = mock.Mock() |
2969 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | 3547 | args_mock.parallel = False |
3548 | args_mock.config = "config" | ||
3549 | args_mock.user = "user" | ||
3550 | parse_args.return_value = args_mock | ||
2970 | 3551 | ||
2971 | self.assertEqual(["row_1", "row_2"], rows) | 3552 | parse_config.return_value = ["pg_config", "report_path"] |
2972 | 3553 | ||
2973 | @mock.patch.object(helper.sys, "exit") | 3554 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] |
2974 | def test_main_parse_args(self, exit): | ||
2975 | with self.subTest(config="config.ini"): | ||
2976 | args = helper.main_parse_args([]) | ||
2977 | self.assertEqual("config.ini", args.config) | ||
2978 | self.assertFalse(args.before) | ||
2979 | self.assertFalse(args.after) | ||
2980 | self.assertFalse(args.debug) | ||
2981 | 3555 | ||
2982 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | 3556 | main.main(["Foo", "Bar"]) |
2983 | self.assertTrue(args.before) | ||
2984 | self.assertTrue(args.after) | ||
2985 | self.assertTrue(args.debug) | ||
2986 | 3557 | ||
2987 | exit.assert_not_called() | 3558 | parse_args.assert_called_with(["Foo", "Bar"]) |
3559 | parse_config.assert_called_with("config") | ||
3560 | fetch_markets.assert_called_with("pg_config", "user") | ||
2988 | 3561 | ||
2989 | with self.subTest(config="inexistant"),\ | 3562 | self.assertEqual(2, process.call_count) |
2990 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3563 | process.assert_has_calls([ |
2991 | args = helper.main_parse_args(["--config", "foo.bar"]) | 3564 | mock.call("config1", 1, "report_path", args_mock), |
2992 | exit.assert_called_once_with(1) | 3565 | mock.call("config2", 2, "report_path", args_mock), |
2993 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | 3566 | ]) |
3567 | with self.subTest(parallel=True): | ||
3568 | with mock.patch("main.parse_args") as parse_args,\ | ||
3569 | mock.patch("main.parse_config") as parse_config,\ | ||
3570 | mock.patch("main.fetch_markets") as fetch_markets,\ | ||
3571 | mock.patch("main.process") as process,\ | ||
3572 | mock.patch("store.Portfolio.start_worker") as start: | ||
3573 | |||
3574 | args_mock = mock.Mock() | ||
3575 | args_mock.parallel = True | ||
3576 | args_mock.config = "config" | ||
3577 | args_mock.user = "user" | ||
3578 | parse_args.return_value = args_mock | ||
3579 | |||
3580 | parse_config.return_value = ["pg_config", "report_path"] | ||
3581 | |||
3582 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] | ||
3583 | |||
3584 | main.main(["Foo", "Bar"]) | ||
3585 | |||
3586 | parse_args.assert_called_with(["Foo", "Bar"]) | ||
3587 | parse_config.assert_called_with("config") | ||
3588 | fetch_markets.assert_called_with("pg_config", "user") | ||
3589 | |||
3590 | start.assert_called_once_with() | ||
3591 | self.assertEqual(2, process.call_count) | ||
3592 | process.assert_has_calls([ | ||
3593 | mock.call.__bool__(), | ||
3594 | mock.call("config1", 1, "report_path", args_mock), | ||
3595 | mock.call.__bool__(), | ||
3596 | mock.call("config2", 2, "report_path", args_mock), | ||
3597 | ]) | ||
2994 | 3598 | ||
2995 | @mock.patch.object(helper.sys, "exit") | 3599 | @mock.patch.object(main.sys, "exit") |
2996 | @mock.patch("helper.configparser") | 3600 | @mock.patch("main.configparser") |
2997 | @mock.patch("helper.os") | 3601 | @mock.patch("main.os") |
2998 | def test_main_parse_config(self, os, configparser, exit): | 3602 | def test_parse_config(self, os, configparser, exit): |
2999 | with self.subTest(pg_config=True, report_path=None): | 3603 | with self.subTest(pg_config=True, report_path=None): |
3000 | config_mock = mock.MagicMock() | 3604 | config_mock = mock.MagicMock() |
3001 | configparser.ConfigParser.return_value = config_mock | 3605 | configparser.ConfigParser.return_value = config_mock |
@@ -3005,7 +3609,7 @@ class HelperTest(WebMockTestCase): | |||
3005 | config_mock.__contains__.side_effect = config | 3609 | config_mock.__contains__.side_effect = config |
3006 | config_mock.__getitem__.return_value = "pg_config" | 3610 | config_mock.__getitem__.return_value = "pg_config" |
3007 | 3611 | ||
3008 | result = helper.main_parse_config("configfile") | 3612 | result = main.parse_config("configfile") |
3009 | 3613 | ||
3010 | config_mock.read.assert_called_with("configfile") | 3614 | config_mock.read.assert_called_with("configfile") |
3011 | 3615 | ||
@@ -3023,7 +3627,7 @@ class HelperTest(WebMockTestCase): | |||
3023 | ] | 3627 | ] |
3024 | 3628 | ||
3025 | os.path.exists.return_value = False | 3629 | os.path.exists.return_value = False |
3026 | result = helper.main_parse_config("configfile") | 3630 | result = main.parse_config("configfile") |
3027 | 3631 | ||
3028 | config_mock.read.assert_called_with("configfile") | 3632 | config_mock.read.assert_called_with("configfile") |
3029 | self.assertEqual(["pg_config", "report_path"], result) | 3633 | self.assertEqual(["pg_config", "report_path"], result) |
@@ -3034,46 +3638,71 @@ class HelperTest(WebMockTestCase): | |||
3034 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3638 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3035 | config_mock = mock.MagicMock() | 3639 | config_mock = mock.MagicMock() |
3036 | configparser.ConfigParser.return_value = config_mock | 3640 | configparser.ConfigParser.return_value = config_mock |
3037 | result = helper.main_parse_config("configfile") | 3641 | result = main.parse_config("configfile") |
3038 | 3642 | ||
3039 | config_mock.read.assert_called_with("configfile") | 3643 | config_mock.read.assert_called_with("configfile") |
3040 | exit.assert_called_once_with(1) | 3644 | exit.assert_called_once_with(1) |
3041 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | 3645 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) |
3042 | 3646 | ||
3647 | @mock.patch.object(main.sys, "exit") | ||
3648 | def test_parse_args(self, exit): | ||
3649 | with self.subTest(config="config.ini"): | ||
3650 | args = main.parse_args([]) | ||
3651 | self.assertEqual("config.ini", args.config) | ||
3652 | self.assertFalse(args.before) | ||
3653 | self.assertFalse(args.after) | ||
3654 | self.assertFalse(args.debug) | ||
3043 | 3655 | ||
3044 | def test_print_orders(self): | 3656 | args = main.parse_args(["--before", "--after", "--debug"]) |
3045 | helper.print_orders(self.m) | 3657 | self.assertTrue(args.before) |
3658 | self.assertTrue(args.after) | ||
3659 | self.assertTrue(args.debug) | ||
3046 | 3660 | ||
3047 | self.m.report.log_stage.assert_called_with("print_orders") | 3661 | exit.assert_not_called() |
3048 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | 3662 | |
3049 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | 3663 | with self.subTest(config="inexistant"),\ |
3050 | compute_value="average") | 3664 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3051 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 3665 | args = main.parse_args(["--config", "foo.bar"]) |
3666 | exit.assert_called_once_with(1) | ||
3667 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
3668 | |||
3669 | @mock.patch.object(main, "psycopg2") | ||
3670 | def test_fetch_markets(self, psycopg2): | ||
3671 | connect_mock = mock.Mock() | ||
3672 | cursor_mock = mock.MagicMock() | ||
3673 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
3674 | |||
3675 | connect_mock.cursor.return_value = cursor_mock | ||
3676 | psycopg2.connect.return_value = connect_mock | ||
3677 | |||
3678 | with self.subTest(user=None): | ||
3679 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | ||
3680 | |||
3681 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3682 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
3683 | |||
3684 | self.assertEqual(["row_1", "row_2"], rows) | ||
3685 | |||
3686 | psycopg2.connect.reset_mock() | ||
3687 | cursor_mock.execute.reset_mock() | ||
3688 | with self.subTest(user=1): | ||
3689 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | ||
3690 | |||
3691 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3692 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
3693 | |||
3694 | self.assertEqual(["row_1", "row_2"], rows) | ||
3052 | 3695 | ||
3053 | def test_print_balances(self): | ||
3054 | self.m.balances.in_currency.return_value = { | ||
3055 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
3056 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
3057 | } | ||
3058 | |||
3059 | helper.print_balances(self.m) | ||
3060 | |||
3061 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
3062 | self.m.balances.fetch_balances.assert_called_with() | ||
3063 | self.m.report.print_log.assert_has_calls([ | ||
3064 | mock.call("total:"), | ||
3065 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
3066 | ]) | ||
3067 | 3696 | ||
3068 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3697 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3069 | class ProcessorTest(WebMockTestCase): | 3698 | class ProcessorTest(WebMockTestCase): |
3070 | def test_values(self): | 3699 | def test_values(self): |
3071 | processor = helper.Processor(self.m) | 3700 | processor = market.Processor(self.m) |
3072 | 3701 | ||
3073 | self.assertEqual(self.m, processor.market) | 3702 | self.assertEqual(self.m, processor.market) |
3074 | 3703 | ||
3075 | def test_run_action(self): | 3704 | def test_run_action(self): |
3076 | processor = helper.Processor(self.m) | 3705 | processor = market.Processor(self.m) |
3077 | 3706 | ||
3078 | with mock.patch.object(processor, "parse_args") as parse_args: | 3707 | with mock.patch.object(processor, "parse_args") as parse_args: |
3079 | method_mock = mock.Mock() | 3708 | method_mock = mock.Mock() |
@@ -3087,10 +3716,10 @@ class ProcessorTest(WebMockTestCase): | |||
3087 | 3716 | ||
3088 | processor.run_action("wait_for_recent", "bar", "baz") | 3717 | processor.run_action("wait_for_recent", "bar", "baz") |
3089 | 3718 | ||
3090 | method_mock.assert_called_with(self.m, foo="bar") | 3719 | method_mock.assert_called_with(foo="bar") |
3091 | 3720 | ||
3092 | def test_select_step(self): | 3721 | def test_select_step(self): |
3093 | processor = helper.Processor(self.m) | 3722 | processor = market.Processor(self.m) |
3094 | 3723 | ||
3095 | scenario = processor.scenarios["sell_all"] | 3724 | scenario = processor.scenarios["sell_all"] |
3096 | 3725 | ||
@@ -3103,9 +3732,9 @@ class ProcessorTest(WebMockTestCase): | |||
3103 | with self.assertRaises(TypeError): | 3732 | with self.assertRaises(TypeError): |
3104 | processor.select_steps(scenario, ["wait"]) | 3733 | processor.select_steps(scenario, ["wait"]) |
3105 | 3734 | ||
3106 | @mock.patch("helper.Processor.process_step") | 3735 | @mock.patch("market.Processor.process_step") |
3107 | def test_process(self, process_step): | 3736 | def test_process(self, process_step): |
3108 | processor = helper.Processor(self.m) | 3737 | processor = market.Processor(self.m) |
3109 | 3738 | ||
3110 | processor.process("sell_all", foo="bar") | 3739 | processor.process("sell_all", foo="bar") |
3111 | self.assertEqual(3, process_step.call_count) | 3740 | self.assertEqual(3, process_step.call_count) |
@@ -3126,11 +3755,11 @@ class ProcessorTest(WebMockTestCase): | |||
3126 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 3755 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3127 | m = market.Market(ccxt) | 3756 | m = market.Market(ccxt) |
3128 | 3757 | ||
3129 | processor = helper.Processor(m) | 3758 | processor = market.Processor(m) |
3130 | 3759 | ||
3131 | method, arguments = processor.method_arguments("wait_for_recent") | 3760 | method, arguments = processor.method_arguments("wait_for_recent") |
3132 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) | 3761 | self.assertEqual(market.Portfolio.wait_for_recent, method) |
3133 | self.assertEqual(["delta"], arguments) | 3762 | self.assertEqual(["delta", "poll"], arguments) |
3134 | 3763 | ||
3135 | method, arguments = processor.method_arguments("prepare_trades") | 3764 | method, arguments = processor.method_arguments("prepare_trades") |
3136 | self.assertEqual(m.prepare_trades, method) | 3765 | self.assertEqual(m.prepare_trades, method) |
@@ -3152,7 +3781,7 @@ class ProcessorTest(WebMockTestCase): | |||
3152 | self.assertEqual(m.trades.close_trades, method) | 3781 | self.assertEqual(m.trades.close_trades, method) |
3153 | 3782 | ||
3154 | def test_process_step(self): | 3783 | def test_process_step(self): |
3155 | processor = helper.Processor(self.m) | 3784 | processor = market.Processor(self.m) |
3156 | 3785 | ||
3157 | with mock.patch.object(processor, "run_action") as run_action: | 3786 | with mock.patch.object(processor, "run_action") as run_action: |
3158 | step = processor.scenarios["sell_needed"][1] | 3787 | step = processor.scenarios["sell_needed"][1] |
@@ -3186,7 +3815,7 @@ class ProcessorTest(WebMockTestCase): | |||
3186 | self.m.balances.fetch_balances.assert_not_called() | 3815 | self.m.balances.fetch_balances.assert_not_called() |
3187 | 3816 | ||
3188 | def test_parse_args(self): | 3817 | def test_parse_args(self): |
3189 | processor = helper.Processor(self.m) | 3818 | processor = market.Processor(self.m) |
3190 | 3819 | ||
3191 | with mock.patch.object(processor, "method_arguments") as method_arguments: | 3820 | with mock.patch.object(processor, "method_arguments") as method_arguments: |
3192 | method_mock = mock.Mock() | 3821 | method_mock = mock.Mock() |
@@ -3312,7 +3941,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3312 | market = mock.Mock() | 3941 | market = mock.Mock() |
3313 | market.fetch_all_balances.return_value = fetch_balance | 3942 | market.fetch_all_balances.return_value = fetch_balance |
3314 | market.fetch_ticker.side_effect = fetch_ticker | 3943 | market.fetch_ticker.side_effect = fetch_ticker |
3315 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 3944 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3316 | # Action 1 | 3945 | # Action 1 |
3317 | helper.prepare_trades(market) | 3946 | helper.prepare_trades(market) |
3318 | 3947 | ||
@@ -3391,7 +4020,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3391 | "amount": "10", "total": "1" | 4020 | "amount": "10", "total": "1" |
3392 | } | 4021 | } |
3393 | ] | 4022 | ] |
3394 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4023 | with mock.patch.object(market.time, "sleep") as sleep: |
3395 | # Action 4 | 4024 | # Action 4 |
3396 | helper.follow_orders(verbose=False) | 4025 | helper.follow_orders(verbose=False) |
3397 | 4026 | ||
@@ -3432,7 +4061,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3432 | } | 4061 | } |
3433 | market.fetch_all_balances.return_value = fetch_balance | 4062 | market.fetch_all_balances.return_value = fetch_balance |
3434 | 4063 | ||
3435 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 4064 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3436 | # Action 5 | 4065 | # Action 5 |
3437 | helper.prepare_trades(market, only="acquire", compute_value="average") | 4066 | helper.prepare_trades(market, only="acquire", compute_value="average") |
3438 | 4067 | ||
@@ -3504,7 +4133,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3504 | # TODO | 4133 | # TODO |
3505 | # portfolio.TradeStore.run_orders() | 4134 | # portfolio.TradeStore.run_orders() |
3506 | 4135 | ||
3507 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4136 | with mock.patch.object(market.time, "sleep") as sleep: |
3508 | # Action 8 | 4137 | # Action 8 |
3509 | helper.follow_orders(verbose=False) | 4138 | helper.follow_orders(verbose=False) |
3510 | 4139 | ||