aboutsummaryrefslogtreecommitdiff
path: root/portfolio.py
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-10 13:52:46 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-10 15:25:27 +0100
commit6ca5a1ec669593fa915a2824efca068c975f9caa (patch)
tree375b3f21b4f228eafea7fae364caa2e7bfb8422b /portfolio.py
parentc51687d2b0cbad5460d8424f550014502d84696e (diff)
downloadTrader-6ca5a1ec669593fa915a2824efca068c975f9caa.tar.gz
Trader-6ca5a1ec669593fa915a2824efca068c975f9caa.tar.zst
Trader-6ca5a1ec669593fa915a2824efca068c975f9caa.zip
Separate store and add helper
Diffstat (limited to 'portfolio.py')
-rw-r--r--portfolio.py309
1 files changed, 31 insertions, 278 deletions
diff --git a/portfolio.py b/portfolio.py
index 45fbef9..efd9b84 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -1,14 +1,13 @@
1from ccxt import ExchangeError
2import time 1import time
3from decimal import Decimal as D, ROUND_DOWN 2from decimal import Decimal as D, ROUND_DOWN
4# Put your poloniex api key in market.py 3# Put your poloniex api key in market.py
5from market import market
6from json import JSONDecodeError 4from json import JSONDecodeError
7import requests 5import requests
6import helper as h
7from store import *
8 8
9# FIXME: correctly handle web call timeouts 9# FIXME: correctly handle web call timeouts
10 10
11
12class Portfolio: 11class Portfolio:
13 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" 12 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
14 liquidities = {} 13 liquidities = {}
@@ -69,6 +68,24 @@ class Portfolio:
69 "high": high_liquidity, 68 "high": high_liquidity,
70 } 69 }
71 70
71class Computation:
72 computations = {
73 "default": lambda x, y: x[y],
74 "average": lambda x, y: x["average"],
75 "bid": lambda x, y: x["bid"],
76 "ask": lambda x, y: x["ask"],
77 }
78
79 @classmethod
80 def compute_value(cls, ticker, action, compute_value="default"):
81 if action == "buy":
82 action = "ask"
83 if action == "sell":
84 action = "bid"
85 if isinstance(compute_value, str):
86 compute_value = cls.computations[compute_value]
87 return compute_value(ticker, action)
88
72class Amount: 89class Amount:
73 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): 90 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
74 self.currency = currency 91 self.currency = currency
@@ -86,9 +103,9 @@ class Amount:
86 self.value * rate, 103 self.value * rate,
87 linked_to=self, 104 linked_to=self,
88 rate=rate) 105 rate=rate)
89 asset_ticker = Trade.get_ticker(self.currency, other_currency, market) 106 asset_ticker = h.get_ticker(self.currency, other_currency, market)
90 if asset_ticker is not None: 107 if asset_ticker is not None:
91 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value) 108 rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value)
92 return Amount( 109 return Amount(
93 other_currency, 110 other_currency,
94 self.value * rate, 111 self.value * rate,
@@ -180,7 +197,6 @@ class Amount:
180 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) 197 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
181 198
182class Balance: 199class Balance:
183 known_balances = {}
184 200
185 def __init__(self, currency, hash_): 201 def __init__(self, currency, hash_):
186 self.currency = currency 202 self.currency = currency
@@ -201,69 +217,6 @@ class Balance:
201 ]: 217 ]:
202 setattr(self, key, Amount(base_currency, hash_.get(key, 0))) 218 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
203 219
204 @classmethod
205 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
206 amounts = {}
207 for currency in cls.known_balances:
208 balance = cls.known_balances[currency]
209 other_currency_amount = getattr(balance, type)\
210 .in_currency(other_currency, market, compute_value=compute_value)
211 amounts[currency] = other_currency_amount
212 return amounts
213
214 @classmethod
215 def currencies(cls):
216 return cls.known_balances.keys()
217
218 @classmethod
219 def fetch_balances(cls, market):
220 all_balances = market.fetch_all_balances()
221 for currency, balance in all_balances.items():
222 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
223 currency in cls.known_balances:
224 cls.known_balances[currency] = cls(currency, balance)
225 return cls.known_balances
226
227 @classmethod
228 def dispatch_assets(cls, amount, repartition=None):
229 if repartition is None:
230 repartition = Portfolio.repartition()
231 sum_ratio = sum([v[0] for k, v in repartition.items()])
232 amounts = {}
233 for currency, (ptt, trade_type) in repartition.items():
234 amounts[currency] = ptt * amount / sum_ratio
235 if trade_type == "short":
236 amounts[currency] = - amounts[currency]
237 if currency not in cls.known_balances:
238 cls.known_balances[currency] = cls(currency, {})
239 return amounts
240
241 @classmethod
242 def prepare_trades(cls, market, base_currency="BTC", compute_value="average", debug=False):
243 cls.fetch_balances(market)
244 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
245 total_base_value = sum(values_in_base.values())
246 new_repartition = cls.dispatch_assets(total_base_value)
247 # Recompute it in case we have new currencies
248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
249 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
250
251 @classmethod
252 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None, debug=False):
253 cls.fetch_balances(market)
254 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
255 total_base_value = sum(values_in_base.values())
256 new_repartition = cls.dispatch_assets(total_base_value)
257 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug)
258
259 @classmethod
260 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average", debug=False):
261 cls.fetch_balances(market)
262 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
263 total_base_value = sum(values_in_base.values())
264 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
265 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
266
267 def __repr__(self): 220 def __repr__(self):
268 if self.exchange_total > 0: 221 if self.exchange_total > 0:
269 if self.exchange_free > 0 and self.exchange_used > 0: 222 if self.exchange_free > 0 and self.exchange_used > 0:
@@ -296,18 +249,7 @@ class Balance:
296 249
297 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" 250 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")"
298 251
299class Computation:
300 computations = {
301 "default": lambda x, y: x[y],
302 "average": lambda x, y: x["average"],
303 "bid": lambda x, y: x["bid"],
304 "ask": lambda x, y: x["ask"],
305 }
306
307class Trade: 252class Trade:
308 debug = False
309 trades = []
310
311 def __init__(self, value_from, value_to, currency, market=None): 253 def __init__(self, value_from, value_to, currency, market=None):
312 # We have value_from of currency, and want to finish with value_to of 254 # We have value_from of currency, and want to finish with value_to of
313 # that currency. value_* may not be in currency's terms 255 # that currency. value_* may not be in currency's terms
@@ -323,105 +265,6 @@ class Trade:
323 self.value_from.linked_to = Amount(self.currency, 0) 265 self.value_from.linked_to = Amount(self.currency, 0)
324 self.base_currency = self.value_from.currency 266 self.base_currency = self.value_from.currency
325 267
326 fees_cache = {}
327 @classmethod
328 def fetch_fees(cls, market):
329 if market.__class__ not in cls.fees_cache:
330 cls.fees_cache[market.__class__] = market.fetch_fees()
331 return cls.fees_cache[market.__class__]
332
333 ticker_cache = {}
334 ticker_cache_timestamp = time.time()
335 @classmethod
336 def get_ticker(cls, c1, c2, market, refresh=False):
337 def invert(ticker):
338 return {
339 "inverted": True,
340 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
341 "original": ticker,
342 }
343 def augment_ticker(ticker):
344 ticker.update({
345 "inverted": False,
346 "average": (ticker["bid"] + ticker["ask"] ) / 2,
347 })
348
349 if time.time() - cls.ticker_cache_timestamp > 5:
350 cls.ticker_cache = {}
351 cls.ticker_cache_timestamp = time.time()
352 elif not refresh:
353 if (c1, c2, market.__class__) in cls.ticker_cache:
354 return cls.ticker_cache[(c1, c2, market.__class__)]
355 if (c2, c1, market.__class__) in cls.ticker_cache:
356 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
357
358 try:
359 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
360 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
361 except ExchangeError:
362 try:
363 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
364 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
365 except ExchangeError:
366 cls.ticker_cache[(c1, c2, market.__class__)] = None
367 return cls.get_ticker(c1, c2, market)
368
369 @classmethod
370 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False):
371 cls.debug = cls.debug or debug
372 base_currency = sum(values_in_base.values()).currency
373 for currency in Balance.currencies():
374 if currency == base_currency:
375 continue
376 value_from = values_in_base.get(currency, Amount(base_currency, 0))
377 value_to = new_repartition.get(currency, Amount(base_currency, 0))
378 if value_from.value * value_to.value < 0:
379 trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market)
380 if only is None or trade_1.action == only:
381 cls.trades.append(trade_1)
382 trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market)
383 if only is None or trade_2.action == only:
384 cls.trades.append(trade_2)
385 else:
386 trade = cls(
387 value_from,
388 value_to,
389 currency,
390 market=market
391 )
392 if only is None or trade.action == only:
393 cls.trades.append(trade)
394 return cls.trades
395
396 @classmethod
397 def prepare_orders(cls, only=None, compute_value="default"):
398 for trade in cls.trades:
399 if only is None or trade.action == only:
400 trade.prepare_order(compute_value=compute_value)
401
402 @classmethod
403 def move_balances(cls, market):
404 needed_in_margin = {}
405 for trade in cls.trades:
406 if trade.trade_type == "short":
407 if trade.value_to.currency not in needed_in_margin:
408 needed_in_margin[trade.value_to.currency] = 0
409 needed_in_margin[trade.value_to.currency] += abs(trade.value_to)
410 for currency, needed in needed_in_margin.items():
411 current_balance = Balance.known_balances[currency].margin_free
412 delta = (needed - current_balance).value
413 # FIXME: don't remove too much if there are open margin position
414 if delta > 0:
415 if cls.debug:
416 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta))
417 else:
418 market.transfer_balance(currency, delta, "exchange", "margin")
419 elif delta < 0:
420 if cls.debug:
421 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta))
422 else:
423 market.transfer_balance(currency, -delta, "margin", "exchange")
424
425 @property 268 @property
426 def action(self): 269 def action(self):
427 if self.value_from == self.value_to: 270 if self.value_from == self.value_to:
@@ -481,11 +324,11 @@ class Trade:
481 def prepare_order(self, compute_value="default"): 324 def prepare_order(self, compute_value="default"):
482 if self.action is None: 325 if self.action is None:
483 return 326 return
484 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market) 327 ticker = h.get_ticker(self.currency, self.base_currency, self.market)
485 inverted = ticker["inverted"] 328 inverted = ticker["inverted"]
486 if inverted: 329 if inverted:
487 ticker = ticker["original"] 330 ticker = ticker["original"]
488 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 331 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
489 # 0.1 332 # 0.1
490 333
491 delta_in_base = abs(self.value_from - self.value_to) 334 delta_in_base = abs(self.value_from - self.value_to)
@@ -536,51 +379,6 @@ class Trade:
536 delta - self.filled_amount, rate, currency, self.trade_type, 379 delta - self.filled_amount, rate, currency, self.trade_type,
537 self.market, self, close_if_possible=close_if_possible)) 380 self.market, self, close_if_possible=close_if_possible))
538 381
539 @classmethod
540 def compute_value(cls, ticker, action, compute_value="default"):
541 if action == "buy":
542 action = "ask"
543 if action == "sell":
544 action = "bid"
545 if isinstance(compute_value, str):
546 compute_value = Computation.computations[compute_value]
547 return compute_value(ticker, action)
548
549 @classmethod
550 def all_orders(cls, state=None):
551 all_orders = sum(map(lambda v: v.orders, cls.trades), [])
552 if state is None:
553 return all_orders
554 else:
555 return list(filter(lambda o: o.status == state, all_orders))
556
557 @classmethod
558 def run_orders(cls):
559 for order in cls.all_orders(state="pending"):
560 order.run()
561
562 @classmethod
563 def follow_orders(cls, verbose=True, sleep=None):
564 if sleep is None:
565 sleep = 7 if cls.debug else 30
566 tick = 0
567 while len(cls.all_orders(state="open")) > 0:
568 time.sleep(sleep)
569 tick += 1
570 for order in cls.all_orders(state="open"):
571 if order.get_status() != "open":
572 if verbose:
573 print("finished {}".format(order))
574 else:
575 order.trade.update_order(order, tick)
576 if verbose:
577 print("All orders finished")
578
579 @classmethod
580 def update_all_orders_status(cls):
581 for order in cls.all_orders(state="open"):
582 order.get_status()
583
584 def __repr__(self): 382 def __repr__(self):
585 return "Trade({} -> {} in {}, {})".format( 383 return "Trade({} -> {} in {}, {})".format(
586 self.value_from, 384 self.value_from,
@@ -588,11 +386,6 @@ class Trade:
588 self.currency, 386 self.currency,
589 self.action) 387 self.action)
590 388
591 @classmethod
592 def print_all_with_order(cls):
593 for trade in cls.trades:
594 trade.print_with_order()
595
596 def print_with_order(self): 389 def print_with_order(self):
597 print(self) 390 print(self)
598 for order in self.orders: 391 for order in self.orders:
@@ -612,7 +405,6 @@ class Order:
612 self.status = "pending" 405 self.status = "pending"
613 self.trade = trade 406 self.trade = trade
614 self.close_if_possible = close_if_possible 407 self.close_if_possible = close_if_possible
615 self.debug = trade.debug
616 408
617 def __repr__(self): 409 def __repr__(self):
618 return "Order({} {} {} at {} {} [{}]{})".format( 410 return "Order({} {} {} at {} {} [{}]{})".format(
@@ -648,7 +440,7 @@ class Order:
648 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 440 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
649 amount = round(self.amount, self.market.order_precision(symbol)).value 441 amount = round(self.amount, self.market.order_precision(symbol)).value
650 442
651 if self.debug: 443 if TradeStore.debug:
652 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( 444 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
653 symbol, self.action, amount, self.rate, self.account)) 445 symbol, self.action, amount, self.rate, self.account))
654 self.status = "open" 446 self.status = "open"
@@ -665,7 +457,7 @@ class Order:
665 print(self.error_message) 457 print(self.error_message)
666 458
667 def get_status(self): 459 def get_status(self):
668 if self.debug: 460 if TradeStore.debug:
669 return self.status 461 return self.status
670 # other states are "closed" and "canceled" 462 # other states are "closed" and "canceled"
671 if self.status == "open": 463 if self.status == "open":
@@ -675,7 +467,7 @@ class Order:
675 return self.status 467 return self.status
676 468
677 def mark_finished_order(self): 469 def mark_finished_order(self):
678 if self.debug: 470 if TradeStore.debug:
679 return 471 return
680 if self.status == "closed": 472 if self.status == "closed":
681 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: 473 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
@@ -683,7 +475,7 @@ class Order:
683 475
684 fetch_cache_timestamp = None 476 fetch_cache_timestamp = None
685 def fetch(self, force=False): 477 def fetch(self, force=False):
686 if self.debug or (not force and self.fetch_cache_timestamp is not None 478 if TradeStore.debug or (not force and self.fetch_cache_timestamp is not None
687 and time.time() - self.fetch_cache_timestamp < 10): 479 and time.time() - self.fetch_cache_timestamp < 10):
688 return 480 return
689 self.fetch_cache_timestamp = time.time() 481 self.fetch_cache_timestamp = time.time()
@@ -725,7 +517,7 @@ class Order:
725 self.base_currency, mouvement_hash)) 517 self.base_currency, mouvement_hash))
726 518
727 def cancel(self): 519 def cancel(self):
728 if self.debug: 520 if TradeStore.debug:
729 self.status = "canceled" 521 self.status = "canceled"
730 return 522 return
731 self.market.cancel_order(self.result['id']) 523 self.market.cancel_order(self.result['id'])
@@ -744,45 +536,6 @@ class Mouvement:
744 # rate * total = total_in_base 536 # rate * total = total_in_base
745 self.total_in_base = Amount(base_currency, hash_["total"]) 537 self.total_in_base = Amount(base_currency, hash_["total"])
746 538
747def print_orders(market, base_currency="BTC"):
748 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
749 Trade.prepare_orders(compute_value="average")
750 for currency, balance in Balance.known_balances.items():
751 print(balance)
752 Trade.print_all_with_order()
753
754def make_orders(market, base_currency="BTC"):
755 Balance.prepare_trades(market, base_currency=base_currency)
756 for trade in Trade.trades:
757 print(trade)
758 for order in trade.orders:
759 print("\t", order, sep="")
760 order.run()
761
762def process_sell_all_sell(market, base_currency="BTC", debug=False):
763 Balance.prepare_trades_to_sell_all(market, debug=debug)
764 Trade.prepare_orders(compute_value="average")
765 print("------------------")
766 for currency, balance in Balance.known_balances.items():
767 print(balance)
768 print("------------------")
769 Trade.print_all_with_order()
770 print("------------------")
771 Trade.run_orders()
772 Trade.follow_orders()
773
774def process_sell_all_buy(market, base_currency="BTC", debug=False):
775 Balance.prepare_trades(market, debug=debug)
776 Trade.prepare_orders()
777 print("------------------")
778 for currency, balance in Balance.known_balances.items():
779 print(balance)
780 print("------------------")
781 Trade.print_all_with_order()
782 print("------------------")
783 Trade.move_balances(market)
784 Trade.run_orders()
785 Trade.follow_orders()
786
787if __name__ == '__main__': 539if __name__ == '__main__':
788 print_orders(market) 540 from market import market
541 h.print_orders(market)