]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - portfolio.py
Work in progress to use shorts
[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 __lt__(self, other):
145 if self.currency != other.currency:
146 raise Exception("Comparing amounts must be done with same currencies")
147 return self.value < other.value
148
149 def __eq__(self, other):
150 if other == 0:
151 return self.value == 0
152 if self.currency != other.currency:
153 raise Exception("Comparing amounts must be done with same currencies")
154 return self.value == other.value
155
156 def __str__(self):
157 if self.linked_to is None:
158 return "{:.8f} {}".format(self.value, self.currency)
159 else:
160 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
161
162 def __repr__(self):
163 if self.linked_to is None:
164 return "Amount({:.8f} {})".format(self.value, self.currency)
165 else:
166 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
167
168 class Balance:
169 known_balances = {}
170
171 def __init__(self, currency, total_value, free_value, used_value):
172 self.currency = currency
173 self.total = Amount(currency, total_value)
174 self.free = Amount(currency, free_value)
175 self.used = Amount(currency, used_value)
176
177 @classmethod
178 def from_hash(cls, currency, hash_):
179 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
180
181 @classmethod
182 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
183 amounts = {}
184 for currency in cls.known_balances:
185 balance = cls.known_balances[currency]
186 other_currency_amount = getattr(balance, type)\
187 .in_currency(other_currency, market, compute_value=compute_value)
188 amounts[currency] = other_currency_amount
189 return amounts
190
191 @classmethod
192 def currencies(cls):
193 return cls.known_balances.keys()
194
195 @classmethod
196 def _fill_balances(cls, hash_):
197 for key in hash_:
198 if key in ["info", "free", "used", "total"]:
199 continue
200 if hash_[key]["total"] != 0 or key in cls.known_balances:
201 cls.known_balances[key] = cls.from_hash(key, hash_[key])
202
203 @classmethod
204 def fetch_balances(cls, market):
205 cls._fill_balances(market.fetch_balance())
206 return cls.known_balances
207 # FIXME:Separate balances per trade type and in position
208 # Need to check how balances in position are represented
209
210
211 @classmethod
212 def dispatch_assets(cls, amount, repartition=None):
213 if repartition is None:
214 repartition = Portfolio.repartition()
215 sum_ratio = sum([v[0] for k, v in repartition.items()])
216 amounts = {}
217 for currency, (ptt, trade_type) in repartition.items():
218 amounts[currency] = ptt * amount / sum_ratio
219 if currency not in cls.known_balances:
220 cls.known_balances[currency] = cls(currency, 0, 0, 0)
221 return amounts
222
223 @classmethod
224 def dispatch_trade_types(cls, repartition=None):
225 if repartition is None:
226 repartition = Portfolio.repartition()
227 trade_types = {}
228 for currency, (ptt, trade_type) in repartition.items():
229 trade_types[currency] = trade_type
230 return trade_types
231 # FIXME: once we know the repartition and sold everything, we can move
232 # the necessary part to the margin account
233
234 @classmethod
235 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
236 cls.fetch_balances(market)
237 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
238 total_base_value = sum(values_in_base.values())
239 new_repartition = cls.dispatch_assets(total_base_value)
240 trade_types = cls.dispatch_trade_types()
241 # Recompute it in case we have new currencies
242 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
243 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
244
245 @classmethod
246 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
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 trade_types = cls.dispatch_trade_types()
252 Trade.compute_trades(values_in_base, new_repartition, trade_types, only=only, market=market)
253
254 @classmethod
255 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
256 cls.fetch_balances(market)
257 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
258 total_base_value = sum(values_in_base.values())
259 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
260 trade_types = cls.dispatch_trade_types()
261 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
262
263 def __repr__(self):
264 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
265
266 class Computation:
267 computations = {
268 "default": lambda x, y: x[y],
269 "average": lambda x, y: x["average"],
270 "bid": lambda x, y: x["bid"],
271 "ask": lambda x, y: x["ask"],
272 }
273
274 class Trade:
275 trades = {}
276
277 def __init__(self, value_from, value_to, currency, trade_type, market=None):
278 # We have value_from of currency, and want to finish with value_to of
279 # that currency. value_* may not be in currency's terms
280 self.currency = currency
281 self.value_from = value_from
282 self.value_to = value_to
283 self.trade_type = trade_type
284 self.orders = []
285 self.market = market
286 assert self.value_from.currency == self.value_to.currency
287 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
288 self.base_currency = self.value_from.currency
289
290 fees_cache = {}
291 @classmethod
292 def fetch_fees(cls, market):
293 if market.__class__ not in cls.fees_cache:
294 cls.fees_cache[market.__class__] = market.fetch_fees()
295 return cls.fees_cache[market.__class__]
296
297 ticker_cache = {}
298 ticker_cache_timestamp = time.time()
299 @classmethod
300 def get_ticker(cls, c1, c2, market, refresh=False):
301 def invert(ticker):
302 return {
303 "inverted": True,
304 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
305 "original": ticker,
306 }
307 def augment_ticker(ticker):
308 ticker.update({
309 "inverted": False,
310 "average": (ticker["bid"] + ticker["ask"] ) / 2,
311 })
312
313 if time.time() - cls.ticker_cache_timestamp > 5:
314 cls.ticker_cache = {}
315 cls.ticker_cache_timestamp = time.time()
316 elif not refresh:
317 if (c1, c2, market.__class__) in cls.ticker_cache:
318 return cls.ticker_cache[(c1, c2, market.__class__)]
319 if (c2, c1, market.__class__) in cls.ticker_cache:
320 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
321
322 try:
323 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
324 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
325 except ExchangeError:
326 try:
327 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
328 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
329 except ExchangeError:
330 cls.ticker_cache[(c1, c2, market.__class__)] = None
331 return cls.get_ticker(c1, c2, market)
332
333 @classmethod
334 def compute_trades(cls, values_in_base, new_repartition, trade_types, only=None, market=None):
335 base_currency = sum(values_in_base.values()).currency
336 for currency in Balance.currencies():
337 if currency == base_currency:
338 continue
339 trade = cls(
340 values_in_base.get(currency, Amount(base_currency, 0)),
341 new_repartition.get(currency, Amount(base_currency, 0)),
342 currency,
343 trade_types.get(currency, "long"),
344 market=market
345 )
346 if only is None or trade.action == only:
347 cls.trades[currency] = trade
348 return cls.trades
349
350 @classmethod
351 def prepare_orders(cls, only=None, compute_value="default"):
352 for currency, trade in cls.trades.items():
353 if only is None or trade.action == only:
354 trade.prepare_order(compute_value=compute_value)
355
356 @property
357 def action(self):
358 if self.value_from == self.value_to:
359 return None
360 if self.base_currency == self.currency:
361 return None
362
363 if self.value_from < self.value_to:
364 return "buy"
365 else:
366 return "sell"
367
368 def order_action(self, inverted):
369 # a xor b xor c
370 if (self.trade_type == "short") != ((self.value_from < self.value_to) != inverted):
371 return "buy"
372 else:
373 return "sell"
374
375 def prepare_order(self, compute_value="default"):
376 if self.action is None:
377 return
378 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market)
379 inverted = ticker["inverted"]
380 if inverted:
381 ticker = ticker["original"]
382 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
383 # 0.1
384
385 # FIXME: optimize if value_to == 0 or value_from == 0?)
386
387 delta_in_base = abs(self.value_from - self.value_to)
388 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
389
390 if not inverted:
391 currency = self.base_currency
392 # BTC
393 if self.action == "sell":
394 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
395 # At rate 1 Foo = 0.1 BTC
396 value_from = self.value_from.linked_to
397 # value_from = 100 FOO
398 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
399 # value_to = 10 FOO (1 BTC * 1/0.1)
400 delta = abs(value_to - value_from)
401 # delta = 90 FOO
402 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
403
404 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
405 else:
406 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
407 # I want to buy 9 / 0.1 FOO
408 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
409 else:
410 currency = self.currency
411 # FOO
412 delta = delta_in_base
413 # sell:
414 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
415 # At rate 1 Foo = 0.1 BTC
416 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
417 # buy:
418 # I want to buy 9 / 0.1 FOO
419 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
420
421 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.trade_type, self.market))
422
423 @classmethod
424 def compute_value(cls, ticker, action, compute_value="default"):
425 if action == "buy":
426 action = "ask"
427 if action == "sell":
428 action = "bid"
429 if isinstance(compute_value, str):
430 compute_value = Computation.computations[compute_value]
431 return compute_value(ticker, action)
432
433 @classmethod
434 def all_orders(cls, state=None):
435 all_orders = sum(map(lambda v: v.orders, cls.trades.values()), [])
436 if state is None:
437 return all_orders
438 else:
439 return list(filter(lambda o: o.status == state, all_orders))
440
441 @classmethod
442 def run_orders(cls):
443 for order in cls.all_orders(state="pending"):
444 order.run()
445
446 @classmethod
447 def follow_orders(cls, verbose=True, sleep=30):
448 orders = cls.all_orders()
449 finished_orders = []
450 while len(orders) != len(finished_orders):
451 time.sleep(sleep)
452 for order in orders:
453 if order in finished_orders:
454 continue
455 if order.get_status() != "open":
456 finished_orders.append(order)
457 if verbose:
458 print("finished {}".format(order))
459 if verbose:
460 print("All orders finished")
461
462 @classmethod
463 def update_all_orders_status(cls):
464 for order in cls.all_orders(state="open"):
465 order.get_status()
466
467 def __repr__(self):
468 return "Trade({} -> {} in {}, {} {})".format(
469 self.value_from,
470 self.value_to,
471 self.currency,
472 self.action,
473 self.trade_type)
474
475 @classmethod
476 def print_all_with_order(cls):
477 for trade in cls.trades.values():
478 trade.print_with_order()
479
480 def print_with_order(self):
481 print(self)
482 for order in self.orders:
483 print("\t", order, sep="")
484
485 class Order:
486 def __init__(self, action, amount, rate, base_currency, trade_type, market):
487 self.action = action
488 self.amount = amount
489 self.rate = rate
490 self.base_currency = base_currency
491 self.market = market
492 self.trade_type = trade_type
493 self.result = None
494 self.status = "pending"
495
496 def __repr__(self):
497 return "Order({} {} {} at {} {} [{}])".format(
498 self.action,
499 self.trade_type,
500 self.amount,
501 self.rate,
502 self.base_currency,
503 self.status
504 )
505
506 @property
507 def account(self):
508 if self.trade_type == "long":
509 return "exchange"
510 else:
511 return "margin"
512
513 @property
514 def pending(self):
515 return self.status == "pending"
516
517 @property
518 def finished(self):
519 return self.status == "closed" or self.status == "canceled" or self.status == "error"
520
521 def run(self, debug=False):
522 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
523 amount = round(self.amount, self.market.order_precision(symbol)).value
524
525 if debug:
526 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
527 symbol, self.action, amount, self.rate, self.account))
528 else:
529 try:
530 if self.action == "sell" and self.trade_type == "short":
531 assert self.market.transfer_balance(self.base_currency, amount * self.rate, "exchange", "margin")
532 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)
533 self.status = "open"
534 except Exception as e:
535 self.status = "error"
536 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
537 symbol, self.action, amount, self.rate, self.account))
538 self.error_message = str("{}: {}".format(e.__class__.__name__, e))
539 print(self.error_message)
540
541 def get_status(self):
542 # other states are "closed" and "canceled"
543 if self.status == "open":
544 result = self.market.fetch_order(self.result['id'])
545 self.status = result["status"]
546 return self.status
547
548 def cancel(self):
549 self.market.cancel_order(self.result['id'])
550
551 def print_orders(market, base_currency="BTC"):
552 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
553 Trade.prepare_orders(compute_value="average")
554 for currency, balance in Balance.known_balances.items():
555 print(balance)
556 Trade.print_all_with_order()
557
558 def make_orders(market, base_currency="BTC"):
559 Balance.prepare_trades(market, base_currency=base_currency)
560 for currency, trade in Trade.trades.items():
561 print(trade)
562 for order in trade.orders:
563 print("\t", order, sep="")
564 order.run()
565
566 if __name__ == '__main__':
567 print_orders(market)