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) |
@@ -2372,6 +3004,19 @@ class ReportStoreTest(WebMockTestCase): | |||
2372 | report_store.set_verbose(False) | 3004 | report_store.set_verbose(False) |
2373 | self.assertFalse(report_store.verbose_print) | 3005 | self.assertFalse(report_store.verbose_print) |
2374 | 3006 | ||
3007 | def test_merge(self): | ||
3008 | report_store1 = market.ReportStore(self.m, verbose_print=False) | ||
3009 | report_store2 = market.ReportStore(None, verbose_print=False) | ||
3010 | |||
3011 | report_store2.log_stage("1") | ||
3012 | report_store1.log_stage("2") | ||
3013 | report_store2.log_stage("3") | ||
3014 | |||
3015 | report_store1.merge(report_store2) | ||
3016 | |||
3017 | self.assertEqual(3, len(report_store1.logs)) | ||
3018 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | ||
3019 | |||
2375 | def test_print_log(self): | 3020 | def test_print_log(self): |
2376 | report_store = market.ReportStore(self.m) | 3021 | report_store = market.ReportStore(self.m) |
2377 | with self.subTest(verbose=True),\ | 3022 | with self.subTest(verbose=True),\ |
@@ -2790,7 +3435,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2790 | }) | 3435 | }) |
2791 | 3436 | ||
2792 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3437 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2793 | class HelperTest(WebMockTestCase): | 3438 | class MainTest(WebMockTestCase): |
2794 | def test_make_order(self): | 3439 | def test_make_order(self): |
2795 | self.m.get_ticker.return_value = { | 3440 | self.m.get_ticker.return_value = { |
2796 | "inverted": False, | 3441 | "inverted": False, |
@@ -2800,7 +3445,7 @@ class HelperTest(WebMockTestCase): | |||
2800 | } | 3445 | } |
2801 | 3446 | ||
2802 | with self.subTest(description="nominal case"): | 3447 | with self.subTest(description="nominal case"): |
2803 | helper.make_order(self.m, 10, "ETH") | 3448 | main.make_order(self.m, 10, "ETH") |
2804 | 3449 | ||
2805 | self.m.report.log_stage.assert_has_calls([ | 3450 | self.m.report.log_stage.assert_has_calls([ |
2806 | mock.call("make_order_begin"), | 3451 | mock.call("make_order_begin"), |
@@ -2825,7 +3470,7 @@ class HelperTest(WebMockTestCase): | |||
2825 | 3470 | ||
2826 | self.m.reset_mock() | 3471 | self.m.reset_mock() |
2827 | with self.subTest(compute_value="default"): | 3472 | with self.subTest(compute_value="default"): |
2828 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3473 | main.make_order(self.m, 10, "ETH", action="dispose", |
2829 | compute_value="ask") | 3474 | compute_value="ask") |
2830 | 3475 | ||
2831 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3476 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
@@ -2834,7 +3479,7 @@ class HelperTest(WebMockTestCase): | |||
2834 | 3479 | ||
2835 | self.m.reset_mock() | 3480 | self.m.reset_mock() |
2836 | with self.subTest(follow=False): | 3481 | with self.subTest(follow=False): |
2837 | result = helper.make_order(self.m, 10, "ETH", follow=False) | 3482 | result = main.make_order(self.m, 10, "ETH", follow=False) |
2838 | 3483 | ||
2839 | self.m.report.log_stage.assert_has_calls([ | 3484 | self.m.report.log_stage.assert_has_calls([ |
2840 | mock.call("make_order_begin"), | 3485 | mock.call("make_order_begin"), |
@@ -2854,7 +3499,7 @@ class HelperTest(WebMockTestCase): | |||
2854 | 3499 | ||
2855 | self.m.reset_mock() | 3500 | self.m.reset_mock() |
2856 | with self.subTest(base_currency="USDT"): | 3501 | with self.subTest(base_currency="USDT"): |
2857 | helper.make_order(self.m, 1, "BTC", base_currency="USDT") | 3502 | main.make_order(self.m, 1, "BTC", base_currency="USDT") |
2858 | 3503 | ||
2859 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3504 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2860 | self.assertEqual("BTC", trade.currency) | 3505 | self.assertEqual("BTC", trade.currency) |
@@ -2862,14 +3507,14 @@ class HelperTest(WebMockTestCase): | |||
2862 | 3507 | ||
2863 | self.m.reset_mock() | 3508 | self.m.reset_mock() |
2864 | with self.subTest(close_if_possible=True): | 3509 | with self.subTest(close_if_possible=True): |
2865 | helper.make_order(self.m, 10, "ETH", close_if_possible=True) | 3510 | main.make_order(self.m, 10, "ETH", close_if_possible=True) |
2866 | 3511 | ||
2867 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3512 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2868 | self.assertEqual(True, trade.orders[0].close_if_possible) | 3513 | self.assertEqual(True, trade.orders[0].close_if_possible) |
2869 | 3514 | ||
2870 | self.m.reset_mock() | 3515 | self.m.reset_mock() |
2871 | with self.subTest(action="dispose"): | 3516 | with self.subTest(action="dispose"): |
2872 | helper.make_order(self.m, 10, "ETH", action="dispose") | 3517 | main.make_order(self.m, 10, "ETH", action="dispose") |
2873 | 3518 | ||
2874 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3519 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2875 | self.assertEqual(0, trade.value_to) | 3520 | self.assertEqual(0, trade.value_to) |
@@ -2879,19 +3524,19 @@ class HelperTest(WebMockTestCase): | |||
2879 | 3524 | ||
2880 | self.m.reset_mock() | 3525 | self.m.reset_mock() |
2881 | with self.subTest(compute_value="default"): | 3526 | with self.subTest(compute_value="default"): |
2882 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3527 | main.make_order(self.m, 10, "ETH", action="dispose", |
2883 | compute_value="bid") | 3528 | compute_value="bid") |
2884 | 3529 | ||
2885 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3530 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2886 | self.assertEqual(D("0.9"), trade.value_from.value) | 3531 | self.assertEqual(D("0.9"), trade.value_from.value) |
2887 | 3532 | ||
2888 | def test_user_market(self): | 3533 | def test_get_user_market(self): |
2889 | with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ | 3534 | with mock.patch("main.fetch_markets") as main_fetch_markets,\ |
2890 | mock.patch("helper.main_parse_config") as main_parse_config: | 3535 | mock.patch("main.parse_config") as main_parse_config: |
2891 | with self.subTest(debug=False): | 3536 | with self.subTest(debug=False): |
2892 | main_parse_config.return_value = ["pg_config", "report_path"] | 3537 | main_parse_config.return_value = ["pg_config", "report_path"] |
2893 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3538 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2894 | m = helper.get_user_market("config_path.ini", 1) | 3539 | m = main.get_user_market("config_path.ini", 1) |
2895 | 3540 | ||
2896 | self.assertIsInstance(m, market.Market) | 3541 | self.assertIsInstance(m, market.Market) |
2897 | self.assertFalse(m.debug) | 3542 | self.assertFalse(m.debug) |
@@ -2899,141 +3544,100 @@ class HelperTest(WebMockTestCase): | |||
2899 | with self.subTest(debug=True): | 3544 | with self.subTest(debug=True): |
2900 | main_parse_config.return_value = ["pg_config", "report_path"] | 3545 | main_parse_config.return_value = ["pg_config", "report_path"] |
2901 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3546 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2902 | m = helper.get_user_market("config_path.ini", 1, debug=True) | 3547 | m = main.get_user_market("config_path.ini", 1, debug=True) |
2903 | 3548 | ||
2904 | self.assertIsInstance(m, market.Market) | 3549 | self.assertIsInstance(m, market.Market) |
2905 | self.assertTrue(m.debug) | 3550 | self.assertTrue(m.debug) |
2906 | 3551 | ||
2907 | def test_main_store_report(self): | 3552 | def test_process(self): |
2908 | file_open = mock.mock_open() | 3553 | with mock.patch("market.Market") as market_mock,\ |
2909 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | ||
2910 | helper.main_store_report(None, 1, self.m) | ||
2911 | file_open.assert_not_called() | ||
2912 | |||
2913 | file_open = mock.mock_open() | ||
2914 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2915 | mock.patch.object(helper, "datetime") as time_mock: | ||
2916 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2917 | self.m.report.to_json.return_value = "json_content" | ||
2918 | |||
2919 | helper.main_store_report("present", 1, self.m) | ||
2920 | |||
2921 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2922 | file_open().write.assert_called_once_with("json_content") | ||
2923 | self.m.report.to_json.assert_called_once_with() | ||
2924 | |||
2925 | with self.subTest(file="error"),\ | ||
2926 | mock.patch("helper.open") as file_open,\ | ||
2927 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3554 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2928 | file_open.side_effect = FileNotFoundError | ||
2929 | 3555 | ||
2930 | helper.main_store_report("error", 1, self.m) | 3556 | args_mock = mock.Mock() |
2931 | 3557 | args_mock.action = "action" | |
2932 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 3558 | args_mock.config = "config" |
2933 | 3559 | args_mock.user = "user" | |
2934 | @mock.patch("helper.Processor.process") | 3560 | args_mock.debug = "debug" |
2935 | def test_main_process_market(self, process): | 3561 | args_mock.before = "before" |
2936 | with self.subTest(before=False, after=False): | 3562 | args_mock.after = "after" |
2937 | m = mock.Mock() | 3563 | self.assertEqual("", stdout_mock.getvalue()) |
2938 | helper.main_process_market(m, None) | ||
2939 | |||
2940 | process.assert_not_called() | ||
2941 | |||
2942 | process.reset_mock() | ||
2943 | with self.subTest(before=True, after=False): | ||
2944 | helper.main_process_market(m, None, before=True) | ||
2945 | |||
2946 | process.assert_called_once_with("sell_all", steps="before") | ||
2947 | |||
2948 | process.reset_mock() | ||
2949 | with self.subTest(before=False, after=True): | ||
2950 | helper.main_process_market(m, None, after=True) | ||
2951 | |||
2952 | process.assert_called_once_with("sell_all", steps="after") | ||
2953 | 3564 | ||
2954 | process.reset_mock() | 3565 | main.process("config", 1, "report_path", args_mock) |
2955 | with self.subTest(before=True, after=True): | ||
2956 | helper.main_process_market(m, None, before=True, after=True) | ||
2957 | 3566 | ||
2958 | process.assert_has_calls([ | 3567 | market_mock.from_config.assert_has_calls([ |
2959 | mock.call("sell_all", steps="before"), | 3568 | mock.call("config", debug="debug", user_id=1, report_path="report_path"), |
2960 | mock.call("sell_all", steps="after"), | 3569 | mock.call().process("action", before="before", after="after"), |
2961 | ]) | 3570 | ]) |
2962 | 3571 | ||
2963 | process.reset_mock() | 3572 | with self.subTest(exception=True): |
2964 | with self.subTest(action="print_balances"),\ | 3573 | market_mock.from_config.side_effect = Exception("boo") |
2965 | mock.patch("helper.print_balances") as print_balances: | 3574 | main.process("config", 1, "report_path", args_mock) |
2966 | helper.main_process_market("user", ["print_balances"]) | 3575 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) |
2967 | |||
2968 | process.assert_not_called() | ||
2969 | print_balances.assert_called_once_with("user") | ||
2970 | |||
2971 | with self.subTest(action="print_orders"),\ | ||
2972 | mock.patch("helper.print_orders") as print_orders,\ | ||
2973 | mock.patch("helper.print_balances") as print_balances: | ||
2974 | helper.main_process_market("user", ["print_orders", "print_balances"]) | ||
2975 | |||
2976 | process.assert_not_called() | ||
2977 | print_orders.assert_called_once_with("user") | ||
2978 | print_balances.assert_called_once_with("user") | ||
2979 | |||
2980 | with self.subTest(action="unknown"),\ | ||
2981 | self.assertRaises(NotImplementedError): | ||
2982 | helper.main_process_market("user", ["unknown"]) | ||
2983 | 3576 | ||
2984 | @mock.patch.object(helper, "psycopg2") | 3577 | def test_main(self): |
2985 | def test_fetch_markets(self, psycopg2): | 3578 | with self.subTest(parallel=False): |
2986 | connect_mock = mock.Mock() | 3579 | with mock.patch("main.parse_args") as parse_args,\ |
2987 | cursor_mock = mock.MagicMock() | 3580 | mock.patch("main.parse_config") as parse_config,\ |
2988 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | 3581 | mock.patch("main.fetch_markets") as fetch_markets,\ |
2989 | 3582 | mock.patch("main.process") as process: | |
2990 | connect_mock.cursor.return_value = cursor_mock | ||
2991 | psycopg2.connect.return_value = connect_mock | ||
2992 | |||
2993 | with self.subTest(user=None): | ||
2994 | rows = list(helper.main_fetch_markets({"foo": "bar"}, None)) | ||
2995 | |||
2996 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2997 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
2998 | |||
2999 | self.assertEqual(["row_1", "row_2"], rows) | ||
3000 | |||
3001 | psycopg2.connect.reset_mock() | ||
3002 | cursor_mock.execute.reset_mock() | ||
3003 | with self.subTest(user=1): | ||
3004 | rows = list(helper.main_fetch_markets({"foo": "bar"}, 1)) | ||
3005 | 3583 | ||
3006 | psycopg2.connect.assert_called_once_with(foo="bar") | 3584 | args_mock = mock.Mock() |
3007 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | 3585 | args_mock.parallel = False |
3586 | args_mock.config = "config" | ||
3587 | args_mock.user = "user" | ||
3588 | parse_args.return_value = args_mock | ||
3008 | 3589 | ||
3009 | self.assertEqual(["row_1", "row_2"], rows) | 3590 | parse_config.return_value = ["pg_config", "report_path"] |
3010 | 3591 | ||
3011 | @mock.patch.object(helper.sys, "exit") | 3592 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] |
3012 | def test_main_parse_args(self, exit): | ||
3013 | with self.subTest(config="config.ini"): | ||
3014 | args = helper.main_parse_args([]) | ||
3015 | self.assertEqual("config.ini", args.config) | ||
3016 | self.assertFalse(args.before) | ||
3017 | self.assertFalse(args.after) | ||
3018 | self.assertFalse(args.debug) | ||
3019 | 3593 | ||
3020 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | 3594 | main.main(["Foo", "Bar"]) |
3021 | self.assertTrue(args.before) | ||
3022 | self.assertTrue(args.after) | ||
3023 | self.assertTrue(args.debug) | ||
3024 | 3595 | ||
3025 | exit.assert_not_called() | 3596 | parse_args.assert_called_with(["Foo", "Bar"]) |
3597 | parse_config.assert_called_with("config") | ||
3598 | fetch_markets.assert_called_with("pg_config", "user") | ||
3026 | 3599 | ||
3027 | with self.subTest(config="inexistant"),\ | 3600 | self.assertEqual(2, process.call_count) |
3028 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3601 | process.assert_has_calls([ |
3029 | args = helper.main_parse_args(["--config", "foo.bar"]) | 3602 | mock.call("config1", 1, "report_path", args_mock), |
3030 | exit.assert_called_once_with(1) | 3603 | mock.call("config2", 2, "report_path", args_mock), |
3031 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | 3604 | ]) |
3605 | with self.subTest(parallel=True): | ||
3606 | with mock.patch("main.parse_args") as parse_args,\ | ||
3607 | mock.patch("main.parse_config") as parse_config,\ | ||
3608 | mock.patch("main.fetch_markets") as fetch_markets,\ | ||
3609 | mock.patch("main.process") as process,\ | ||
3610 | mock.patch("store.Portfolio.start_worker") as start: | ||
3611 | |||
3612 | args_mock = mock.Mock() | ||
3613 | args_mock.parallel = True | ||
3614 | args_mock.config = "config" | ||
3615 | args_mock.user = "user" | ||
3616 | parse_args.return_value = args_mock | ||
3617 | |||
3618 | parse_config.return_value = ["pg_config", "report_path"] | ||
3619 | |||
3620 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] | ||
3621 | |||
3622 | main.main(["Foo", "Bar"]) | ||
3623 | |||
3624 | parse_args.assert_called_with(["Foo", "Bar"]) | ||
3625 | parse_config.assert_called_with("config") | ||
3626 | fetch_markets.assert_called_with("pg_config", "user") | ||
3627 | |||
3628 | start.assert_called_once_with() | ||
3629 | self.assertEqual(2, process.call_count) | ||
3630 | process.assert_has_calls([ | ||
3631 | mock.call.__bool__(), | ||
3632 | mock.call("config1", 1, "report_path", args_mock), | ||
3633 | mock.call.__bool__(), | ||
3634 | mock.call("config2", 2, "report_path", args_mock), | ||
3635 | ]) | ||
3032 | 3636 | ||
3033 | @mock.patch.object(helper.sys, "exit") | 3637 | @mock.patch.object(main.sys, "exit") |
3034 | @mock.patch("helper.configparser") | 3638 | @mock.patch("main.configparser") |
3035 | @mock.patch("helper.os") | 3639 | @mock.patch("main.os") |
3036 | def test_main_parse_config(self, os, configparser, exit): | 3640 | def test_parse_config(self, os, configparser, exit): |
3037 | with self.subTest(pg_config=True, report_path=None): | 3641 | with self.subTest(pg_config=True, report_path=None): |
3038 | config_mock = mock.MagicMock() | 3642 | config_mock = mock.MagicMock() |
3039 | configparser.ConfigParser.return_value = config_mock | 3643 | configparser.ConfigParser.return_value = config_mock |
@@ -3043,7 +3647,7 @@ class HelperTest(WebMockTestCase): | |||
3043 | config_mock.__contains__.side_effect = config | 3647 | config_mock.__contains__.side_effect = config |
3044 | config_mock.__getitem__.return_value = "pg_config" | 3648 | config_mock.__getitem__.return_value = "pg_config" |
3045 | 3649 | ||
3046 | result = helper.main_parse_config("configfile") | 3650 | result = main.parse_config("configfile") |
3047 | 3651 | ||
3048 | config_mock.read.assert_called_with("configfile") | 3652 | config_mock.read.assert_called_with("configfile") |
3049 | 3653 | ||
@@ -3061,7 +3665,7 @@ class HelperTest(WebMockTestCase): | |||
3061 | ] | 3665 | ] |
3062 | 3666 | ||
3063 | os.path.exists.return_value = False | 3667 | os.path.exists.return_value = False |
3064 | result = helper.main_parse_config("configfile") | 3668 | result = main.parse_config("configfile") |
3065 | 3669 | ||
3066 | config_mock.read.assert_called_with("configfile") | 3670 | config_mock.read.assert_called_with("configfile") |
3067 | self.assertEqual(["pg_config", "report_path"], result) | 3671 | self.assertEqual(["pg_config", "report_path"], result) |
@@ -3072,46 +3676,71 @@ class HelperTest(WebMockTestCase): | |||
3072 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3676 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3073 | config_mock = mock.MagicMock() | 3677 | config_mock = mock.MagicMock() |
3074 | configparser.ConfigParser.return_value = config_mock | 3678 | configparser.ConfigParser.return_value = config_mock |
3075 | result = helper.main_parse_config("configfile") | 3679 | result = main.parse_config("configfile") |
3076 | 3680 | ||
3077 | config_mock.read.assert_called_with("configfile") | 3681 | config_mock.read.assert_called_with("configfile") |
3078 | exit.assert_called_once_with(1) | 3682 | exit.assert_called_once_with(1) |
3079 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | 3683 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) |
3080 | 3684 | ||
3685 | @mock.patch.object(main.sys, "exit") | ||
3686 | def test_parse_args(self, exit): | ||
3687 | with self.subTest(config="config.ini"): | ||
3688 | args = main.parse_args([]) | ||
3689 | self.assertEqual("config.ini", args.config) | ||
3690 | self.assertFalse(args.before) | ||
3691 | self.assertFalse(args.after) | ||
3692 | self.assertFalse(args.debug) | ||
3081 | 3693 | ||
3082 | def test_print_orders(self): | 3694 | args = main.parse_args(["--before", "--after", "--debug"]) |
3083 | helper.print_orders(self.m) | 3695 | self.assertTrue(args.before) |
3696 | self.assertTrue(args.after) | ||
3697 | self.assertTrue(args.debug) | ||
3084 | 3698 | ||
3085 | self.m.report.log_stage.assert_called_with("print_orders") | 3699 | exit.assert_not_called() |
3086 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | 3700 | |
3087 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | 3701 | with self.subTest(config="inexistant"),\ |
3088 | compute_value="average") | 3702 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3089 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 3703 | args = main.parse_args(["--config", "foo.bar"]) |
3704 | exit.assert_called_once_with(1) | ||
3705 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
3706 | |||
3707 | @mock.patch.object(main, "psycopg2") | ||
3708 | def test_fetch_markets(self, psycopg2): | ||
3709 | connect_mock = mock.Mock() | ||
3710 | cursor_mock = mock.MagicMock() | ||
3711 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
3712 | |||
3713 | connect_mock.cursor.return_value = cursor_mock | ||
3714 | psycopg2.connect.return_value = connect_mock | ||
3715 | |||
3716 | with self.subTest(user=None): | ||
3717 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | ||
3718 | |||
3719 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3720 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
3721 | |||
3722 | self.assertEqual(["row_1", "row_2"], rows) | ||
3723 | |||
3724 | psycopg2.connect.reset_mock() | ||
3725 | cursor_mock.execute.reset_mock() | ||
3726 | with self.subTest(user=1): | ||
3727 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | ||
3728 | |||
3729 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3730 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
3731 | |||
3732 | self.assertEqual(["row_1", "row_2"], rows) | ||
3090 | 3733 | ||
3091 | def test_print_balances(self): | ||
3092 | self.m.balances.in_currency.return_value = { | ||
3093 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
3094 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
3095 | } | ||
3096 | |||
3097 | helper.print_balances(self.m) | ||
3098 | |||
3099 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
3100 | self.m.balances.fetch_balances.assert_called_with() | ||
3101 | self.m.report.print_log.assert_has_calls([ | ||
3102 | mock.call("total:"), | ||
3103 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
3104 | ]) | ||
3105 | 3734 | ||
3106 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3735 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3107 | class ProcessorTest(WebMockTestCase): | 3736 | class ProcessorTest(WebMockTestCase): |
3108 | def test_values(self): | 3737 | def test_values(self): |
3109 | processor = helper.Processor(self.m) | 3738 | processor = market.Processor(self.m) |
3110 | 3739 | ||
3111 | self.assertEqual(self.m, processor.market) | 3740 | self.assertEqual(self.m, processor.market) |
3112 | 3741 | ||
3113 | def test_run_action(self): | 3742 | def test_run_action(self): |
3114 | processor = helper.Processor(self.m) | 3743 | processor = market.Processor(self.m) |
3115 | 3744 | ||
3116 | with mock.patch.object(processor, "parse_args") as parse_args: | 3745 | with mock.patch.object(processor, "parse_args") as parse_args: |
3117 | method_mock = mock.Mock() | 3746 | method_mock = mock.Mock() |
@@ -3125,10 +3754,10 @@ class ProcessorTest(WebMockTestCase): | |||
3125 | 3754 | ||
3126 | processor.run_action("wait_for_recent", "bar", "baz") | 3755 | processor.run_action("wait_for_recent", "bar", "baz") |
3127 | 3756 | ||
3128 | method_mock.assert_called_with(self.m, foo="bar") | 3757 | method_mock.assert_called_with(foo="bar") |
3129 | 3758 | ||
3130 | def test_select_step(self): | 3759 | def test_select_step(self): |
3131 | processor = helper.Processor(self.m) | 3760 | processor = market.Processor(self.m) |
3132 | 3761 | ||
3133 | scenario = processor.scenarios["sell_all"] | 3762 | scenario = processor.scenarios["sell_all"] |
3134 | 3763 | ||
@@ -3141,9 +3770,9 @@ class ProcessorTest(WebMockTestCase): | |||
3141 | with self.assertRaises(TypeError): | 3770 | with self.assertRaises(TypeError): |
3142 | processor.select_steps(scenario, ["wait"]) | 3771 | processor.select_steps(scenario, ["wait"]) |
3143 | 3772 | ||
3144 | @mock.patch("helper.Processor.process_step") | 3773 | @mock.patch("market.Processor.process_step") |
3145 | def test_process(self, process_step): | 3774 | def test_process(self, process_step): |
3146 | processor = helper.Processor(self.m) | 3775 | processor = market.Processor(self.m) |
3147 | 3776 | ||
3148 | processor.process("sell_all", foo="bar") | 3777 | processor.process("sell_all", foo="bar") |
3149 | self.assertEqual(3, process_step.call_count) | 3778 | self.assertEqual(3, process_step.call_count) |
@@ -3164,11 +3793,11 @@ class ProcessorTest(WebMockTestCase): | |||
3164 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 3793 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3165 | m = market.Market(ccxt) | 3794 | m = market.Market(ccxt) |
3166 | 3795 | ||
3167 | processor = helper.Processor(m) | 3796 | processor = market.Processor(m) |
3168 | 3797 | ||
3169 | method, arguments = processor.method_arguments("wait_for_recent") | 3798 | method, arguments = processor.method_arguments("wait_for_recent") |
3170 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) | 3799 | self.assertEqual(market.Portfolio.wait_for_recent, method) |
3171 | self.assertEqual(["delta"], arguments) | 3800 | self.assertEqual(["delta", "poll"], arguments) |
3172 | 3801 | ||
3173 | method, arguments = processor.method_arguments("prepare_trades") | 3802 | method, arguments = processor.method_arguments("prepare_trades") |
3174 | self.assertEqual(m.prepare_trades, method) | 3803 | self.assertEqual(m.prepare_trades, method) |
@@ -3190,7 +3819,7 @@ class ProcessorTest(WebMockTestCase): | |||
3190 | self.assertEqual(m.trades.close_trades, method) | 3819 | self.assertEqual(m.trades.close_trades, method) |
3191 | 3820 | ||
3192 | def test_process_step(self): | 3821 | def test_process_step(self): |
3193 | processor = helper.Processor(self.m) | 3822 | processor = market.Processor(self.m) |
3194 | 3823 | ||
3195 | with mock.patch.object(processor, "run_action") as run_action: | 3824 | with mock.patch.object(processor, "run_action") as run_action: |
3196 | step = processor.scenarios["sell_needed"][1] | 3825 | step = processor.scenarios["sell_needed"][1] |
@@ -3224,7 +3853,7 @@ class ProcessorTest(WebMockTestCase): | |||
3224 | self.m.balances.fetch_balances.assert_not_called() | 3853 | self.m.balances.fetch_balances.assert_not_called() |
3225 | 3854 | ||
3226 | def test_parse_args(self): | 3855 | def test_parse_args(self): |
3227 | processor = helper.Processor(self.m) | 3856 | processor = market.Processor(self.m) |
3228 | 3857 | ||
3229 | with mock.patch.object(processor, "method_arguments") as method_arguments: | 3858 | with mock.patch.object(processor, "method_arguments") as method_arguments: |
3230 | method_mock = mock.Mock() | 3859 | method_mock = mock.Mock() |
@@ -3350,7 +3979,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3350 | market = mock.Mock() | 3979 | market = mock.Mock() |
3351 | market.fetch_all_balances.return_value = fetch_balance | 3980 | market.fetch_all_balances.return_value = fetch_balance |
3352 | market.fetch_ticker.side_effect = fetch_ticker | 3981 | market.fetch_ticker.side_effect = fetch_ticker |
3353 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 3982 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3354 | # Action 1 | 3983 | # Action 1 |
3355 | helper.prepare_trades(market) | 3984 | helper.prepare_trades(market) |
3356 | 3985 | ||
@@ -3429,7 +4058,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3429 | "amount": "10", "total": "1" | 4058 | "amount": "10", "total": "1" |
3430 | } | 4059 | } |
3431 | ] | 4060 | ] |
3432 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4061 | with mock.patch.object(market.time, "sleep") as sleep: |
3433 | # Action 4 | 4062 | # Action 4 |
3434 | helper.follow_orders(verbose=False) | 4063 | helper.follow_orders(verbose=False) |
3435 | 4064 | ||
@@ -3470,7 +4099,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3470 | } | 4099 | } |
3471 | market.fetch_all_balances.return_value = fetch_balance | 4100 | market.fetch_all_balances.return_value = fetch_balance |
3472 | 4101 | ||
3473 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 4102 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3474 | # Action 5 | 4103 | # Action 5 |
3475 | helper.prepare_trades(market, only="acquire", compute_value="average") | 4104 | helper.prepare_trades(market, only="acquire", compute_value="average") |
3476 | 4105 | ||
@@ -3542,7 +4171,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3542 | # TODO | 4171 | # TODO |
3543 | # portfolio.TradeStore.run_orders() | 4172 | # portfolio.TradeStore.run_orders() |
3544 | 4173 | ||
3545 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4174 | with mock.patch.object(market.time, "sleep") as sleep: |
3546 | # Action 8 | 4175 | # Action 8 |
3547 | helper.follow_orders(verbose=False) | 4176 | helper.follow_orders(verbose=False) |
3548 | 4177 | ||