]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - portfolio.py
WIP: handle more balance information
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
1 from ccxt import ExchangeError
2 import time
3 from decimal import Decimal as D, ROUND_DOWN
4 # Put your poloniex api key in market.py
5 from market import market
6
7 class Portfolio:
8 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
9 liquidities = {}
10 data = None
11
12 @classmethod
13 def repartition(cls, liquidity="medium"):
14 cls.parse_cryptoportfolio()
15 liquidities = cls.liquidities[liquidity]
16 cls.last_date = sorted(liquidities.keys())[-1]
17 return liquidities[cls.last_date]
18
19 @classmethod
20 def get_cryptoportfolio(cls):
21 import json
22 import urllib3
23 urllib3.disable_warnings()
24 http = urllib3.PoolManager()
25
26 try:
27 r = http.request("GET", cls.URL)
28 except Exception:
29 return None
30 try:
31 cls.data = json.loads(r.data,
32 parse_int=D,
33 parse_float=D)
34 except json.JSONDecodeError:
35 cls.data = None
36
37 @classmethod
38 def parse_cryptoportfolio(cls):
39 if cls.data is None:
40 cls.get_cryptoportfolio()
41
42 def filter_weights(weight_hash):
43 if weight_hash[1][0] == 0:
44 return False
45 if weight_hash[0] == "_row":
46 return False
47 return True
48
49 def clean_weights(i):
50 def clean_weights_(h):
51 if h[0].endswith("s"):
52 return [h[0][0:-1], (h[1][i], "short")]
53 else:
54 return [h[0], (h[1][i], "long")]
55 return clean_weights_
56
57 def parse_weights(portfolio_hash):
58 weights_hash = portfolio_hash["weights"]
59 weights = {}
60 for i in range(len(weights_hash["_row"])):
61 weights[weights_hash["_row"][i]] = dict(filter(
62 filter_weights,
63 map(clean_weights(i), weights_hash.items())))
64 return weights
65
66 high_liquidity = parse_weights(cls.data["portfolio_1"])
67 medium_liquidity = parse_weights(cls.data["portfolio_2"])
68
69 cls.liquidities = {
70 "medium": medium_liquidity,
71 "high": high_liquidity,
72 }
73
74 class Amount:
75 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
76 self.currency = currency
77 self.value = D(value)
78 self.linked_to = linked_to
79 self.ticker = ticker
80 self.rate = rate
81
82 self.ticker_cache = {}
83 self.ticker_cache_timestamp = time.time()
84
85 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
86 if other_currency == self.currency:
87 return self
88 if rate is not None:
89 return Amount(
90 other_currency,
91 self.value * rate,
92 linked_to=self,
93 rate=rate)
94 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
95 if asset_ticker is not None:
96 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
97 return Amount(
98 other_currency,
99 self.value * rate,
100 linked_to=self,
101 ticker=asset_ticker,
102 rate=rate)
103 else:
104 raise Exception("This asset is not available in the chosen market")
105
106 def __round__(self, n=8):
107 return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN))
108
109 def __abs__(self):
110 return Amount(self.currency, abs(self.value))
111
112 def __add__(self, other):
113 if other.currency != self.currency and other.value * self.value != 0:
114 raise Exception("Summing amounts must be done with same currencies")
115 return Amount(self.currency, self.value + other.value)
116
117 def __radd__(self, other):
118 if other == 0:
119 return self
120 else:
121 return self.__add__(other)
122
123 def __sub__(self, other):
124 if other.currency != self.currency and other.value * self.value != 0:
125 raise Exception("Summing amounts must be done with same currencies")
126 return Amount(self.currency, self.value - other.value)
127
128 def __mul__(self, value):
129 if not isinstance(value, (int, float, D)):
130 raise TypeError("Amount may only be multiplied by numbers")
131 return Amount(self.currency, self.value * value)
132
133 def __rmul__(self, value):
134 return self.__mul__(value)
135
136 def __floordiv__(self, value):
137 if not isinstance(value, (int, float, D)):
138 raise TypeError("Amount may only be multiplied by integers")
139 return Amount(self.currency, self.value / value)
140
141 def __truediv__(self, value):
142 return self.__floordiv__(value)
143
144 def __le__(self, other):
145 return self == other or self < other
146
147 def __lt__(self, other):
148 if other == 0:
149 return self.value < 0
150 if self.currency != other.currency:
151 raise Exception("Comparing amounts must be done with same currencies")
152 return self.value < other.value
153
154 def __gt__(self, other):
155 return not self <= other
156
157 def __ge__(self, other):
158 return not self < other
159
160 def __eq__(self, other):
161 if other == 0:
162 return self.value == 0
163 if self.currency != other.currency:
164 raise Exception("Comparing amounts must be done with same currencies")
165 return self.value == other.value
166
167 def __ne__(self, other):
168 return not self == other
169
170 def __neg__(self):
171 return Amount(self.currency, - self.value)
172
173 def __str__(self):
174 if self.linked_to is None:
175 return "{:.8f} {}".format(self.value, self.currency)
176 else:
177 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
178
179 def __repr__(self):
180 if self.linked_to is None:
181 return "Amount({:.8f} {})".format(self.value, self.currency)
182 else:
183 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
184
185 class Balance:
186 known_balances = {}
187
188 def __init__(self, currency, hash_):
189 self.currency = currency
190 for key in ["total",
191 "exchange_total", "exchange_used", "exchange_free",
192 "margin_total", "margin_borrowed", "margin_free"]:
193 setattr(self, key, Amount(currency, hash_.get(key, 0)))
194
195 self.margin_position_type = hash_["margin_position_type"]
196
197 if hash_["margin_borrowed_base_currency"] is not None:
198 base_currency = hash_["margin_borrowed_base_currency"]
199 for key in [
200 "margin_liquidation_price",
201 "margin_pending_gain",
202 "margin_lending_fees",
203 "margin_borrowed_base_price"
204 ]:
205 setattr(self, key, Amount(base_currency, hash_[key]))
206
207 @classmethod
208 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
209 amounts = {}
210 for currency in cls.known_balances:
211 balance = cls.known_balances[currency]
212 other_currency_amount = getattr(balance, type)\
213 .in_currency(other_currency, market, compute_value=compute_value)
214 amounts[currency] = other_currency_amount
215 return amounts
216
217 @classmethod
218 def currencies(cls):
219 return cls.known_balances.keys()
220
221 @classmethod
222 def fetch_balances(cls, market):
223 all_balances = market.fetch_all_balances()
224 for currency, balance in all_balances.items():
225 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
226 currency in cls.known_balances:
227 cls.known_balances[currency] = cls(currency, balance)
228 return cls.known_balances
229
230
231 @classmethod
232 def dispatch_assets(cls, amount, repartition=None):
233 if repartition is None:
234 repartition = Portfolio.repartition()
235 sum_ratio = sum([v[0] for k, v in repartition.items()])
236 amounts = {}
237 for currency, (ptt, trade_type) in repartition.items():
238 amounts[currency] = ptt * amount / sum_ratio
239 if trade_type == "short":
240 amounts[currency] = - amounts[currency]
241 if currency not in cls.known_balances:
242 cls.known_balances[currency] = cls(currency, 0, 0, 0)
243 return amounts
244
245 @classmethod
246 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
247 cls.fetch_balances(market)
248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
249 total_base_value = sum(values_in_base.values())
250 new_repartition = cls.dispatch_assets(total_base_value)
251 # Recompute it in case we have new currencies
252 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
253 Trade.compute_trades(values_in_base, new_repartition, market=market)
254
255 @classmethod
256 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
257 cls.fetch_balances(market)
258 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
259 total_base_value = sum(values_in_base.values())
260 new_repartition = cls.dispatch_assets(total_base_value)
261 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)
262
263 @classmethod
264 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
265 cls.fetch_balances(market)
266 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
267 total_base_value = sum(values_in_base.values())
268 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
269 Trade.compute_trades(values_in_base, new_repartition, market=market)
270
271 def __repr__(self):
272 if self.exchange_total > 0:
273 if self.exchange_free > 0 and self.exchange_used > 0:
274 exchange = " Exch: [✔{} + ❌{} = {}]".format(str(self.exchange_free), str(self.exchange_used), str(self.exchange_total))
275 elif self.exchange_free > 0:
276 exchange = " Exch: [✔{}]".format(str(self.exchange_free))
277 else:
278 exchange = " Exch: [❌{}]".format(str(self.exchange_used))
279 else:
280 exchange = ""
281
282 if self.margin_total > 0:
283 if self.margin_free != 0 and self.margin_borrowed != 0:
284 margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total))
285 elif self.margin_free != 0:
286 margin = " Margin: [✔{}]".format(str(self.margin_free))
287 else:
288 margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed))
289 elif self.margin_total < 0:
290 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
291 str(self.margin_borrowed_base_price),
292 str(self.margin_lending_fees))
293 else:
294 margin = ""
295
296 if self.margin_total != 0 and self.exchange_total != 0:
297 total = " Total: [{}]".format(str(self.total))
298 else:
299 total = ""
300
301 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")"
302
303 class Computation:
304 computations = {
305 "default": lambda x, y: x[y],
306 "average": lambda x, y: x["average"],
307 "bid": lambda x, y: x["bid"],
308 "ask": lambda x, y: x["ask"],
309 }
310
311 class Trade:
312 trades = []
313
314 def __init__(self, value_from, value_to, currency, market=None):
315 # We have value_from of currency, and want to finish with value_to of
316 # that currency. value_* may not be in currency's terms
317 self.currency = currency
318 self.value_from = value_from
319 self.value_to = value_to
320 self.orders = []
321 self.market = market
322 assert self.value_from.currency == self.value_to.currency
323 if self.value_from != 0:
324 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
325 elif self.value_from.linked_to is None:
326 self.value_from.linked_to = Amount(self.currency, 0)
327 self.base_currency = self.value_from.currency
328
329 fees_cache = {}
330 @classmethod
331 def fetch_fees(cls, market):
332 if market.__class__ not in cls.fees_cache:
333 cls.fees_cache[market.__class__] = market.fetch_fees()
334 return cls.fees_cache[market.__class__]
335
336 ticker_cache = {}
337 ticker_cache_timestamp = time.time()
338 @classmethod
339 def get_ticker(cls, c1, c2, market, refresh=False):
340 def invert(ticker):
341 return {
342 "inverted": True,
343 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
344 "original": ticker,
345 }
346 def augment_ticker(ticker):
347 ticker.update({
348 "inverted": False,
349 "average": (ticker["bid"] + ticker["ask"] ) / 2,
350 })
351
352 if time.time() - cls.ticker_cache_timestamp > 5:
353 cls.ticker_cache = {}
354 cls.ticker_cache_timestamp = time.time()
355 elif not refresh:
356 if (c1, c2, market.__class__) in cls.ticker_cache:
357 return cls.ticker_cache[(c1, c2, market.__class__)]
358 if (c2, c1, market.__class__) in cls.ticker_cache:
359 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
360
361 try:
362 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
363 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
364 except ExchangeError:
365 try:
366 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
367 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
368 except ExchangeError:
369 cls.ticker_cache[(c1, c2, market.__class__)] = None
370 return cls.get_ticker(c1, c2, market)
371
372 @classmethod
373 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
374 base_currency = sum(values_in_base.values()).currency
375 for currency in Balance.currencies():
376 if currency == base_currency:
377 continue
378 value_from = values_in_base.get(currency, Amount(base_currency, 0))
379 value_to = new_repartition.get(currency, Amount(base_currency, 0))
380 if value_from.value * value_to.value < 0:
381 trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market)
382 if only is None or trade_1.action == only:
383 cls.trades.append(trade_1)
384 trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market)
385 if only is None or trade_2.action == only:
386 cls.trades.append(trade_2)
387 else:
388 trade = cls(
389 value_from,
390 value_to,
391 currency,
392 market=market
393 )
394 if only is None or trade.action == only:
395 cls.trades.append(trade)
396 return cls.trades
397
398 @classmethod
399 def prepare_orders(cls, only=None, compute_value="default"):
400 for trade in cls.trades:
401 if only is None or trade.action == only:
402 trade.prepare_order(compute_value=compute_value)
403
404 @classmethod
405 def move_balances(cls, market, debug=False):
406 needed_in_margin = {}
407 for trade in cls.trades:
408 if trade.trade_type == "short":
409 if trade.value_to.currency not in needed_in_margin:
410 needed_in_margin[trade.value_to.currency] = 0
411 needed_in_margin[trade.value_to.currency] += abs(trade.value_to)
412 for currency, needed in needed_in_margin.items():
413 current_balance = Balance.known_balances[currency].margin_free
414 delta = (needed - current_balance).value
415 # FIXME: don't remove too much if there are open margin position
416 if delta > 0:
417 if debug:
418 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta))
419 else:
420 market.transfer_balance(currency, delta, "exchange", "margin")
421 elif delta < 0:
422 if debug:
423 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta))
424 else:
425 market.transfer_balance(currency, -delta, "margin", "exchange")
426
427 @property
428 def action(self):
429 if self.value_from == self.value_to:
430 return None
431 if self.base_currency == self.currency:
432 return None
433
434 if self.value_from < self.value_to:
435 return "acquire"
436 else:
437 return "dispose"
438
439 def order_action(self, inverted):
440 if (self.value_from < self.value_to) != inverted:
441 return "buy"
442 else:
443 return "sell"
444
445 @property
446 def trade_type(self):
447 if self.value_from + self.value_to < 0:
448 return "short"
449 else:
450 return "long"
451
452 def prepare_order(self, compute_value="default"):
453 if self.action is None:
454 return
455 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market)
456 inverted = ticker["inverted"]
457 if inverted:
458 ticker = ticker["original"]
459 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
460 # 0.1
461
462 delta_in_base = abs(self.value_from - self.value_to)
463 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
464
465 if not inverted:
466 currency = self.base_currency
467 # BTC
468 if self.action == "dispose":
469 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
470 # At rate 1 Foo = 0.1 BTC
471 value_from = self.value_from.linked_to
472 # value_from = 100 FOO
473 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
474 # value_to = 10 FOO (1 BTC * 1/0.1)
475 delta = abs(value_to - value_from)
476 # delta = 90 FOO
477 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
478
479 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
480 else:
481 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
482 # I want to buy 9 / 0.1 FOO
483 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
484 else:
485 currency = self.currency
486 # FOO
487 delta = delta_in_base
488 # sell:
489 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
490 # At rate 1 Foo = 0.1 BTC
491 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
492 # buy:
493 # I want to buy 9 / 0.1 FOO
494 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
495
496 close_if_possible = (self.value_to == 0)
497
498 self.orders.append(Order(self.order_action(inverted),
499 delta, rate, currency, self.trade_type, self.market,
500 close_if_possible=close_if_possible))
501
502 @classmethod
503 def compute_value(cls, ticker, action, compute_value="default"):
504 if action == "buy":
505 action = "ask"
506 if action == "sell":
507 action = "bid"
508 if isinstance(compute_value, str):
509 compute_value = Computation.computations[compute_value]
510 return compute_value(ticker, action)
511
512 @classmethod
513 def all_orders(cls, state=None):
514 all_orders = sum(map(lambda v: v.orders, cls.trades), [])
515 if state is None:
516 return all_orders
517 else:
518 return list(filter(lambda o: o.status == state, all_orders))
519
520 @classmethod
521 def run_orders(cls):
522 for order in cls.all_orders(state="pending"):
523 order.run()
524
525 @classmethod
526 def follow_orders(cls, verbose=True, sleep=30):
527 orders = cls.all_orders()
528 finished_orders = []
529 while len(orders) != len(finished_orders):
530 time.sleep(sleep)
531 for order in orders:
532 if order in finished_orders:
533 continue
534 if order.get_status() != "open":
535 finished_orders.append(order)
536 if verbose:
537 print("finished {}".format(order))
538 if verbose:
539 print("All orders finished")
540
541 @classmethod
542 def update_all_orders_status(cls):
543 for order in cls.all_orders(state="open"):
544 order.get_status()
545
546 def __repr__(self):
547 return "Trade({} -> {} in {}, {})".format(
548 self.value_from,
549 self.value_to,
550 self.currency,
551 self.action)
552
553 @classmethod
554 def print_all_with_order(cls):
555 for trade in cls.trades:
556 trade.print_with_order()
557
558 def print_with_order(self):
559 print(self)
560 for order in self.orders:
561 print("\t", order, sep="")
562
563 class Order:
564 def __init__(self, action, amount, rate, base_currency, trade_type, market,
565 close_if_possible=False):
566 self.action = action
567 self.amount = amount
568 self.rate = rate
569 self.base_currency = base_currency
570 self.market = market
571 self.trade_type = trade_type
572 self.result = None
573 self.status = "pending"
574 self.close_if_possible = close_if_possible
575
576 def __repr__(self):
577 return "Order({} {} {} at {} {} [{}]{})".format(
578 self.action,
579 self.trade_type,
580 self.amount,
581 self.rate,
582 self.base_currency,
583 self.status,
584 " ✂" if self.close_if_possible else "",
585 )
586
587 @property
588 def account(self):
589 if self.trade_type == "long":
590 return "exchange"
591 else:
592 return "margin"
593
594 @property
595 def pending(self):
596 return self.status == "pending"
597
598 @property
599 def finished(self):
600 return self.status == "closed" or self.status == "canceled" or self.status == "error"
601
602 def run(self, debug=False):
603 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
604 amount = round(self.amount, self.market.order_precision(symbol)).value
605
606 if debug:
607 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
608 symbol, self.action, amount, self.rate, self.account))
609 else:
610 try:
611 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)
612 self.status = "open"
613 except Exception as e:
614 self.status = "error"
615 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
616 symbol, self.action, amount, self.rate, self.account))
617 self.error_message = str("{}: {}".format(e.__class__.__name__, e))
618 print(self.error_message)
619
620 def get_status(self):
621 # other states are "closed" and "canceled"
622 if self.status == "open":
623 result = self.market.fetch_order(self.result['id'])
624 if result["status"] != "open":
625 self.mark_finished_order(result["status"])
626 return self.status
627
628 def mark_finished_order(self, status):
629 if status == "closed":
630 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
631 self.market.close_margin_position(self.amount.currency, self.base_currency)
632
633 self.status = result["status"]
634
635 def cancel(self):
636 self.market.cancel_order(self.result['id'])
637
638 def print_orders(market, base_currency="BTC"):
639 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
640 Trade.prepare_orders(compute_value="average")
641 for currency, balance in Balance.known_balances.items():
642 print(balance)
643 Trade.print_all_with_order()
644
645 def make_orders(market, base_currency="BTC"):
646 Balance.prepare_trades(market, base_currency=base_currency)
647 for trade in Trade.trades:
648 print(trade)
649 for order in trade.orders:
650 print("\t", order, sep="")
651 order.run()
652
653 def sell_all(market, base_currency="BTC"):
654 Balance.prepare_trades_to_sell_all(market)
655 Trade.prepare_orders(compute_value="average")
656 Trade.run_orders()
657 Trade.follow_orders()
658
659 Balance.update_trades(market, only="acquire")
660 Trade.prepare_orders(only="acquire")
661 Trade.move_balances(market)
662 Trade.run_orders()
663 Trade.follow_orders()
664
665 if __name__ == '__main__':
666 print_orders(market)