aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-06-11 00:38:37 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-06-11 00:38:37 +0200
commit3d6f74ee1a8b061e4b274dad70125ab6388f4d83 (patch)
treecd718866b15b6ff8de3d6b95b6e156ae25c17c20
parente24df7cffc01937a211db2d29f44bccc3d740bd5 (diff)
downloadTrader-3d6f74ee1a8b061e4b274dad70125ab6388f4d83.tar.gz
Trader-3d6f74ee1a8b061e4b274dad70125ab6388f4d83.tar.zst
Trader-3d6f74ee1a8b061e4b274dad70125ab6388f4d83.zip
Fix available balance when buying
-rw-r--r--market.py17
-rw-r--r--store.py45
-rw-r--r--tests/test_market.py32
-rw-r--r--tests/test_store.py186
4 files changed, 250 insertions, 30 deletions
diff --git a/market.py b/market.py
index 9550b77..d7b05ce 100644
--- a/market.py
+++ b/market.py
@@ -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
diff --git a/store.py b/store.py
index 32c4121..1a1ed76 100644
--- a/store.py
+++ b/store.py
@@ -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