diff options
-rw-r--r-- | market.py | 17 | ||||
-rw-r--r-- | store.py | 45 | ||||
-rw-r--r-- | tests/test_market.py | 32 | ||||
-rw-r--r-- | tests/test_store.py | 186 |
4 files changed, 250 insertions, 30 deletions
@@ -220,23 +220,20 @@ class Market: | |||
220 | compute_value=compute_value, only=only, | 220 | compute_value=compute_value, only=only, |
221 | repartition=repartition, available_balance_only=available_balance_only) | 221 | repartition=repartition, available_balance_only=available_balance_only) |
222 | 222 | ||
223 | values_in_base = self.balances.in_currency(base_currency, | ||
224 | compute_value=compute_value) | ||
225 | if available_balance_only: | 223 | if available_balance_only: |
226 | balance = self.balances.all.get(base_currency) | 224 | repartition, total_base_value, values_in_base = self.balances.available_balances_for_repartition( |
227 | if balance is None: | 225 | base_currency=base_currency, liquidity=liquidity, |
228 | total_base_value = portfolio.Amount(base_currency, 0) | 226 | repartition=repartition, compute_value=compute_value) |
229 | else: | ||
230 | total_base_value = balance.exchange_free + balance.margin_available | ||
231 | else: | 227 | else: |
228 | values_in_base = self.balances.in_currency(base_currency, | ||
229 | compute_value=compute_value) | ||
232 | total_base_value = sum(values_in_base.values()) | 230 | total_base_value = sum(values_in_base.values()) |
233 | new_repartition = self.balances.dispatch_assets(total_base_value, | 231 | new_repartition = self.balances.dispatch_assets(total_base_value, |
234 | liquidity=liquidity, repartition=repartition) | 232 | liquidity=liquidity, repartition=repartition) |
235 | if available_balance_only: | 233 | if available_balance_only: |
236 | for currency, amount in values_in_base.items(): | 234 | for currency, amount in values_in_base.items(): |
237 | if currency != base_currency: | 235 | if currency != base_currency and currency not in new_repartition: |
238 | new_repartition.setdefault(currency, portfolio.Amount(base_currency, 0)) | 236 | new_repartition[currency] = amount |
239 | new_repartition[currency] += amount | ||
240 | 237 | ||
241 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 238 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
242 | 239 | ||
@@ -325,6 +325,49 @@ class BalanceStore: | |||
325 | else: | 325 | else: |
326 | self.market.report.log_balances(tag=tag, checkpoint=checkpoint) | 326 | self.market.report.log_balances(tag=tag, checkpoint=checkpoint) |
327 | 327 | ||
328 | def available_balances_for_repartition(self, | ||
329 | compute_value="average", base_currency="BTC", | ||
330 | liquidity="medium", repartition=None): | ||
331 | if repartition is None: | ||
332 | repartition = Portfolio.repartition(liquidity=liquidity) | ||
333 | base_currency_balance = self.all.get(base_currency) | ||
334 | |||
335 | if base_currency_balance is None: | ||
336 | total_base_value = portfolio.Amount(base_currency, 0) | ||
337 | else: | ||
338 | total_base_value = base_currency_balance.exchange_free + \ | ||
339 | base_currency_balance.margin_available - \ | ||
340 | base_currency_balance.margin_in_position | ||
341 | |||
342 | amount_in_position = {} | ||
343 | |||
344 | # Compute balances already in the target position | ||
345 | for currency, (ptt, trade_type) in repartition.items(): | ||
346 | amount_in_position[currency] = portfolio.Amount(base_currency, 0) | ||
347 | balance = self.all.get(currency) | ||
348 | if currency != base_currency and balance is not None: | ||
349 | if trade_type == "short": | ||
350 | amount = balance.margin_borrowed | ||
351 | else: | ||
352 | amount = balance.exchange_free + balance.exchange_used | ||
353 | amount_in_position[currency] = amount.in_currency(base_currency, | ||
354 | self.market, compute_value=compute_value) | ||
355 | total_base_value += amount_in_position[currency] | ||
356 | |||
357 | # recursively delete more-than-filled positions from the wanted | ||
358 | # repartition | ||
359 | did_delete = True | ||
360 | while did_delete: | ||
361 | did_delete = False | ||
362 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | ||
363 | current_base_value = total_base_value | ||
364 | for currency, (ptt, trade_type) in repartition.copy().items(): | ||
365 | if amount_in_position[currency] > current_base_value * ptt / sum_ratio: | ||
366 | did_delete = True | ||
367 | del(repartition[currency]) | ||
368 | total_base_value -= amount_in_position[currency] | ||
369 | return repartition, total_base_value, amount_in_position | ||
370 | |||
328 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): | 371 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): |
329 | if repartition is None: | 372 | if repartition is None: |
330 | repartition = Portfolio.repartition(liquidity=liquidity) | 373 | repartition = Portfolio.repartition(liquidity=liquidity) |
@@ -521,7 +564,7 @@ class Portfolio: | |||
521 | cls.retrieve_cryptoportfolio() | 564 | cls.retrieve_cryptoportfolio() |
522 | cls.get_cryptoportfolio() | 565 | cls.get_cryptoportfolio() |
523 | liquidities = cls.liquidities.get(liquidity) | 566 | liquidities = cls.liquidities.get(liquidity) |
524 | return liquidities[cls.last_date.get()] | 567 | return liquidities[cls.last_date.get()].copy() |
525 | 568 | ||
526 | @classmethod | 569 | @classmethod |
527 | def get_cryptoportfolio(cls, refetch=False): | 570 | def get_cryptoportfolio(cls, refetch=False): |
diff --git a/tests/test_market.py b/tests/test_market.py index c89025b..07188ac 100644 --- a/tests/test_market.py +++ b/tests/test_market.py | |||
@@ -186,14 +186,17 @@ class MarketTest(WebMockTestCase): | |||
186 | return { "average": D("0.000001") } | 186 | return { "average": D("0.000001") } |
187 | if c1 == "ETH" and c2 == "BTC": | 187 | if c1 == "ETH" and c2 == "BTC": |
188 | return { "average": D("0.1") } | 188 | return { "average": D("0.1") } |
189 | if c1 == "FOO" and c2 == "BTC": | ||
190 | return { "average": D("0.1") } | ||
189 | self.fail("Should not be called with {}, {}".format(c1, c2)) | 191 | self.fail("Should not be called with {}, {}".format(c1, c2)) |
190 | get_ticker.side_effect = _get_ticker | 192 | get_ticker.side_effect = _get_ticker |
191 | 193 | ||
192 | repartition.return_value = { | 194 | repartition.return_value = { |
193 | "DOGE": (D("0.25"), "short"), | 195 | "DOGE": (D("0.20"), "short"), |
194 | "BTC": (D("0.25"), "long"), | 196 | "BTC": (D("0.20"), "long"), |
195 | "ETH": (D("0.25"), "long"), | 197 | "ETH": (D("0.20"), "long"), |
196 | "XMR": (D("0.25"), "long"), | 198 | "XMR": (D("0.20"), "long"), |
199 | "FOO": (D("0.20"), "long"), | ||
197 | } | 200 | } |
198 | m = market.Market(self.ccxt, self.market_args()) | 201 | m = market.Market(self.ccxt, self.market_args()) |
199 | self.ccxt.fetch_all_balances.return_value = { | 202 | self.ccxt.fetch_all_balances.return_value = { |
@@ -210,12 +213,12 @@ class MarketTest(WebMockTestCase): | |||
210 | "total": D("5.0") | 213 | "total": D("5.0") |
211 | }, | 214 | }, |
212 | "BTC": { | 215 | "BTC": { |
213 | "exchange_free": D("0.075"), | 216 | "exchange_free": D("0.065"), |
214 | "exchange_used": D("0.02"), | 217 | "exchange_used": D("0.02"), |
215 | "exchange_total": D("0.095"), | 218 | "exchange_total": D("0.085"), |
216 | "margin_available": D("0.025"), | 219 | "margin_available": D("0.035"), |
217 | "margin_in_position": D("0.01"), | 220 | "margin_in_position": D("0.01"), |
218 | "margin_total": D("0.035"), | 221 | "margin_total": D("0.045"), |
219 | "total": D("0.13") | 222 | "total": D("0.13") |
220 | }, | 223 | }, |
221 | "ETH": { | 224 | "ETH": { |
@@ -224,6 +227,12 @@ class MarketTest(WebMockTestCase): | |||
224 | "exchange_total": D("1.0"), | 227 | "exchange_total": D("1.0"), |
225 | "total": D("1.0") | 228 | "total": D("1.0") |
226 | }, | 229 | }, |
230 | "FOO": { | ||
231 | "exchange_free": D("0.1"), | ||
232 | "exchange_used": D("0.0"), | ||
233 | "exchange_total": D("0.1"), | ||
234 | "total": D("0.1"), | ||
235 | }, | ||
227 | } | 236 | } |
228 | 237 | ||
229 | m.balances.fetch_balances(tag="tag") | 238 | m.balances.fetch_balances(tag="tag") |
@@ -236,12 +245,13 @@ class MarketTest(WebMockTestCase): | |||
236 | 245 | ||
237 | self.assertEqual(portfolio.Amount("BTC", "-0.025"), | 246 | self.assertEqual(portfolio.Amount("BTC", "-0.025"), |
238 | new_repartition["DOGE"] - values_in_base["DOGE"]) | 247 | new_repartition["DOGE"] - values_in_base["DOGE"]) |
239 | self.assertEqual(portfolio.Amount("BTC", "0.025"), | ||
240 | new_repartition["ETH"] - values_in_base["ETH"]) | ||
241 | self.assertEqual(0, | 248 | self.assertEqual(0, |
242 | new_repartition["ZRC"] - values_in_base["ZRC"]) | 249 | new_repartition["ETH"] - values_in_base["ETH"]) |
250 | self.assertIsNone(new_repartition.get("ZRC")) | ||
243 | self.assertEqual(portfolio.Amount("BTC", "0.025"), | 251 | self.assertEqual(portfolio.Amount("BTC", "0.025"), |
244 | new_repartition["XMR"]) | 252 | new_repartition["XMR"]) |
253 | self.assertEqual(portfolio.Amount("BTC", "0.015"), | ||
254 | new_repartition["FOO"] - values_in_base["FOO"]) | ||
245 | 255 | ||
246 | compute_trades.reset_mock() | 256 | compute_trades.reset_mock() |
247 | with self.subTest(available_balance_only=True, balance=0),\ | 257 | with self.subTest(available_balance_only=True, balance=0),\ |
diff --git a/tests/test_store.py b/tests/test_store.py index d7620a0..1a722b5 100644 --- a/tests/test_store.py +++ b/tests/test_store.py | |||
@@ -434,6 +434,176 @@ class BalanceStoreTest(WebMockTestCase): | |||
434 | self.assertListEqual(["XVG", "XMR", "USDT"], list(balance_store.currencies())) | 434 | self.assertListEqual(["XVG", "XMR", "USDT"], list(balance_store.currencies())) |
435 | 435 | ||
436 | @mock.patch.object(market.Portfolio, "repartition") | 436 | @mock.patch.object(market.Portfolio, "repartition") |
437 | def test_available_balances_for_repartition(self, repartition): | ||
438 | with self.subTest(available_balance_only=True): | ||
439 | def _get_ticker(c1, c2): | ||
440 | if c1 == "ZRC" and c2 == "BTC": | ||
441 | return { "average": D("0.0001") } | ||
442 | if c1 == "DOGE" and c2 == "BTC": | ||
443 | return { "average": D("0.000001") } | ||
444 | if c1 == "ETH" and c2 == "BTC": | ||
445 | return { "average": D("0.1") } | ||
446 | if c1 == "FOO" and c2 == "BTC": | ||
447 | return { "average": D("0.1") } | ||
448 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
449 | self.m.get_ticker.side_effect = _get_ticker | ||
450 | |||
451 | repartition.return_value = { | ||
452 | "DOGE": (D("0.20"), "short"), | ||
453 | "BTC": (D("0.20"), "long"), | ||
454 | "ETH": (D("0.20"), "long"), | ||
455 | "XMR": (D("0.20"), "long"), | ||
456 | "FOO": (D("0.20"), "long"), | ||
457 | } | ||
458 | self.m.ccxt.fetch_all_balances.return_value = { | ||
459 | "ZRC": { | ||
460 | "exchange_free": D("2.0"), | ||
461 | "exchange_used": D("0.0"), | ||
462 | "exchange_total": D("2.0"), | ||
463 | "total": D("2.0") | ||
464 | }, | ||
465 | "DOGE": { | ||
466 | "exchange_free": D("5.0"), | ||
467 | "exchange_used": D("0.0"), | ||
468 | "exchange_total": D("5.0"), | ||
469 | "total": D("5.0") | ||
470 | }, | ||
471 | "BTC": { | ||
472 | "exchange_free": D("0.065"), | ||
473 | "exchange_used": D("0.02"), | ||
474 | "exchange_total": D("0.085"), | ||
475 | "margin_available": D("0.035"), | ||
476 | "margin_in_position": D("0.01"), | ||
477 | "margin_total": D("0.045"), | ||
478 | "total": D("0.13") | ||
479 | }, | ||
480 | "ETH": { | ||
481 | "exchange_free": D("1.0"), | ||
482 | "exchange_used": D("0.0"), | ||
483 | "exchange_total": D("1.0"), | ||
484 | "total": D("1.0") | ||
485 | }, | ||
486 | "FOO": { | ||
487 | "exchange_free": D("0.1"), | ||
488 | "exchange_used": D("0.0"), | ||
489 | "exchange_total": D("0.1"), | ||
490 | "total": D("0.1"), | ||
491 | }, | ||
492 | } | ||
493 | |||
494 | balance_store = market.BalanceStore(self.m) | ||
495 | balance_store.fetch_balances() | ||
496 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition() | ||
497 | repartition.assert_called_with(liquidity="medium") | ||
498 | self.assertEqual((D("0.20"), "short"), _repartition["DOGE"]) | ||
499 | self.assertEqual((D("0.20"), "long"), _repartition["BTC"]) | ||
500 | self.assertEqual((D("0.20"), "long"), _repartition["XMR"]) | ||
501 | self.assertEqual((D("0.20"), "long"), _repartition["FOO"]) | ||
502 | self.assertIsNone(_repartition.get("ETH")) | ||
503 | self.assertEqual(portfolio.Amount("BTC", "0.1"), total_base_value) | ||
504 | self.assertEqual(0, amount_in_position["DOGE"]) | ||
505 | self.assertEqual(0, amount_in_position["BTC"]) | ||
506 | self.assertEqual(0, amount_in_position["XMR"]) | ||
507 | self.assertEqual(portfolio.Amount("BTC", "0.1"), amount_in_position["ETH"]) | ||
508 | self.assertEqual(portfolio.Amount("BTC", "0.01"), amount_in_position["FOO"]) | ||
509 | |||
510 | with self.subTest(available_balance_only=True, balance=0): | ||
511 | def _get_ticker(c1, c2): | ||
512 | if c1 == "ETH" and c2 == "BTC": | ||
513 | return { "average": D("0.1") } | ||
514 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
515 | self.m.get_ticker.side_effect = _get_ticker | ||
516 | |||
517 | repartition.return_value = { | ||
518 | "BTC": (D("0.5"), "long"), | ||
519 | "ETH": (D("0.5"), "long"), | ||
520 | } | ||
521 | self.m.ccxt.fetch_all_balances.return_value = { | ||
522 | "ETH": { | ||
523 | "exchange_free": D("1.0"), | ||
524 | "exchange_used": D("0.0"), | ||
525 | "exchange_total": D("1.0"), | ||
526 | "total": D("1.0") | ||
527 | }, | ||
528 | } | ||
529 | |||
530 | balance_store = market.BalanceStore(self.m) | ||
531 | balance_store.fetch_balances() | ||
532 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(liquidity="high") | ||
533 | |||
534 | repartition.assert_called_with(liquidity="high") | ||
535 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | ||
536 | self.assertIsNone(_repartition.get("ETH")) | ||
537 | self.assertEqual(0, total_base_value) | ||
538 | self.assertEqual(0, amount_in_position["BTC"]) | ||
539 | self.assertEqual(0, amount_in_position["BTC"]) | ||
540 | |||
541 | repartition.reset_mock() | ||
542 | with self.subTest(available_balance_only=True, balance=0, | ||
543 | repartition="present"): | ||
544 | def _get_ticker(c1, c2): | ||
545 | if c1 == "ETH" and c2 == "BTC": | ||
546 | return { "average": D("0.1") } | ||
547 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
548 | self.m.get_ticker.side_effect = _get_ticker | ||
549 | |||
550 | _repartition = { | ||
551 | "BTC": (D("0.5"), "long"), | ||
552 | "ETH": (D("0.5"), "long"), | ||
553 | } | ||
554 | self.m.ccxt.fetch_all_balances.return_value = { | ||
555 | "ETH": { | ||
556 | "exchange_free": D("1.0"), | ||
557 | "exchange_used": D("0.0"), | ||
558 | "exchange_total": D("1.0"), | ||
559 | "total": D("1.0") | ||
560 | }, | ||
561 | } | ||
562 | |||
563 | balance_store = market.BalanceStore(self.m) | ||
564 | balance_store.fetch_balances() | ||
565 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(repartition=_repartition) | ||
566 | repartition.assert_not_called() | ||
567 | |||
568 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | ||
569 | self.assertIsNone(_repartition.get("ETH")) | ||
570 | self.assertEqual(0, total_base_value) | ||
571 | self.assertEqual(0, amount_in_position["BTC"]) | ||
572 | self.assertEqual(portfolio.Amount("BTC", "0.1"), amount_in_position["ETH"]) | ||
573 | |||
574 | repartition.reset_mock() | ||
575 | with self.subTest(available_balance_only=True, balance=0, | ||
576 | repartition="present", base_currency="ETH"): | ||
577 | def _get_ticker(c1, c2): | ||
578 | if c1 == "ETH" and c2 == "BTC": | ||
579 | return { "average": D("0.1") } | ||
580 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
581 | self.m.get_ticker.side_effect = _get_ticker | ||
582 | |||
583 | _repartition = { | ||
584 | "BTC": (D("0.5"), "long"), | ||
585 | "ETH": (D("0.5"), "long"), | ||
586 | } | ||
587 | self.m.ccxt.fetch_all_balances.return_value = { | ||
588 | "ETH": { | ||
589 | "exchange_free": D("1.0"), | ||
590 | "exchange_used": D("0.0"), | ||
591 | "exchange_total": D("1.0"), | ||
592 | "total": D("1.0") | ||
593 | }, | ||
594 | } | ||
595 | |||
596 | balance_store = market.BalanceStore(self.m) | ||
597 | balance_store.fetch_balances() | ||
598 | _repartition, total_base_value, amount_in_position = balance_store.available_balances_for_repartition(repartition=_repartition, base_currency="ETH") | ||
599 | |||
600 | self.assertEqual((D("0.5"), "long"), _repartition["BTC"]) | ||
601 | self.assertEqual((D("0.5"), "long"), _repartition["ETH"]) | ||
602 | self.assertEqual(portfolio.Amount("ETH", 1), total_base_value) | ||
603 | self.assertEqual(0, amount_in_position["BTC"]) | ||
604 | self.assertEqual(0, amount_in_position["ETH"]) | ||
605 | |||
606 | @mock.patch.object(market.Portfolio, "repartition") | ||
437 | def test_dispatch_assets(self, repartition): | 607 | def test_dispatch_assets(self, repartition): |
438 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | 608 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
439 | 609 | ||
@@ -1343,27 +1513,27 @@ class PortfolioTest(WebMockTestCase): | |||
1343 | with self.subTest(from_cache=False): | 1513 | with self.subTest(from_cache=False): |
1344 | market.Portfolio.liquidities = store.LockedVar({ | 1514 | market.Portfolio.liquidities = store.LockedVar({ |
1345 | "medium": { | 1515 | "medium": { |
1346 | "2018-03-01": "medium_2018-03-01", | 1516 | "2018-03-01": ["medium_2018-03-01"], |
1347 | "2018-03-08": "medium_2018-03-08", | 1517 | "2018-03-08": ["medium_2018-03-08"], |
1348 | }, | 1518 | }, |
1349 | "high": { | 1519 | "high": { |
1350 | "2018-03-01": "high_2018-03-01", | 1520 | "2018-03-01": ["high_2018-03-01"], |
1351 | "2018-03-08": "high_2018-03-08", | 1521 | "2018-03-08": ["high_2018-03-08"], |
1352 | } | 1522 | } |
1353 | }) | 1523 | }) |
1354 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | 1524 | market.Portfolio.last_date = store.LockedVar("2018-03-08") |
1355 | 1525 | ||
1356 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) | 1526 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition()) |
1357 | get_cryptoportfolio.assert_called_once_with() | 1527 | get_cryptoportfolio.assert_called_once_with() |
1358 | retrieve_cryptoportfolio.assert_not_called() | 1528 | retrieve_cryptoportfolio.assert_not_called() |
1359 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium")) | 1529 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition(liquidity="medium")) |
1360 | self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high")) | 1530 | self.assertEqual(["high_2018-03-08"], market.Portfolio.repartition(liquidity="high")) |
1361 | 1531 | ||
1362 | retrieve_cryptoportfolio.reset_mock() | 1532 | retrieve_cryptoportfolio.reset_mock() |
1363 | get_cryptoportfolio.reset_mock() | 1533 | get_cryptoportfolio.reset_mock() |
1364 | 1534 | ||
1365 | with self.subTest(from_cache=True): | 1535 | with self.subTest(from_cache=True): |
1366 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(from_cache=True)) | 1536 | self.assertEqual(["medium_2018-03-08"], market.Portfolio.repartition(from_cache=True)) |
1367 | get_cryptoportfolio.assert_called_once_with() | 1537 | get_cryptoportfolio.assert_called_once_with() |
1368 | retrieve_cryptoportfolio.assert_called_once_with() | 1538 | retrieve_cryptoportfolio.assert_called_once_with() |
1369 | 1539 | ||