]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Work in progress to use shorts
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
e0b14bcc 1from ccxt import ExchangeError
dd359bc0 2import time
350ed24d 3from decimal import Decimal as D, ROUND_DOWN
dd359bc0
IB
4# Put your poloniex api key in market.py
5from market import market
6
dd359bc0
IB
7class Portfolio:
8 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
9 liquidities = {}
10 data = None
11
12 @classmethod
350ed24d 13 def repartition(cls, liquidity="medium"):
dd359bc0
IB
14 cls.parse_cryptoportfolio()
15 liquidities = cls.liquidities[liquidity]
c11e4274
IB
16 cls.last_date = sorted(liquidities.keys())[-1]
17 return liquidities[cls.last_date]
dd359bc0
IB
18
19 @classmethod
20 def get_cryptoportfolio(cls):
21 import json
22 import urllib3
23 urllib3.disable_warnings()
24 http = urllib3.PoolManager()
25
183a53e3
IB
26 try:
27 r = http.request("GET", cls.URL)
28 except Exception:
77f8a378 29 return None
183a53e3 30 try:
5ab23e1c
IB
31 cls.data = json.loads(r.data,
32 parse_int=D,
33 parse_float=D)
183a53e3
IB
34 except json.JSONDecodeError:
35 cls.data = None
dd359bc0
IB
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):
350ed24d 43 if weight_hash[1][0] == 0:
dd359bc0
IB
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):
350ed24d
IB
51 if h[0].endswith("s"):
52 return [h[0][0:-1], (h[1][i], "short")]
dd359bc0 53 else:
350ed24d 54 return [h[0], (h[1][i], "long")]
dd359bc0
IB
55 return clean_weights_
56
57 def parse_weights(portfolio_hash):
dd359bc0
IB
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
74class Amount:
c2644ba8 75 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
dd359bc0 76 self.currency = currency
5ab23e1c 77 self.value = D(value)
dd359bc0
IB
78 self.linked_to = linked_to
79 self.ticker = ticker
c2644ba8 80 self.rate = rate
dd359bc0
IB
81
82 self.ticker_cache = {}
83 self.ticker_cache_timestamp = time.time()
84
c2644ba8 85 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
dd359bc0
IB
86 if other_currency == self.currency:
87 return self
c2644ba8
IB
88 if rate is not None:
89 return Amount(
90 other_currency,
91 self.value * rate,
92 linked_to=self,
93 rate=rate)
cfab619d 94 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
dd359bc0 95 if asset_ticker is not None:
c2644ba8 96 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
dd359bc0
IB
97 return Amount(
98 other_currency,
c2644ba8 99 self.value * rate,
dd359bc0 100 linked_to=self,
c2644ba8
IB
101 ticker=asset_ticker,
102 rate=rate)
dd359bc0
IB
103 else:
104 raise Exception("This asset is not available in the chosen market")
105
350ed24d
IB
106 def __round__(self, n=8):
107 return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN))
108
dd359bc0 109 def __abs__(self):
5ab23e1c 110 return Amount(self.currency, abs(self.value))
dd359bc0
IB
111
112 def __add__(self, other):
5ab23e1c 113 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 114 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 115 return Amount(self.currency, self.value + other.value)
dd359bc0
IB
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):
5ab23e1c 124 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 125 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 126 return Amount(self.currency, self.value - other.value)
dd359bc0
IB
127
128 def __mul__(self, value):
77f8a378 129 if not isinstance(value, (int, float, D)):
dd359bc0 130 raise TypeError("Amount may only be multiplied by numbers")
5ab23e1c 131 return Amount(self.currency, self.value * value)
dd359bc0
IB
132
133 def __rmul__(self, value):
134 return self.__mul__(value)
135
136 def __floordiv__(self, value):
77f8a378 137 if not isinstance(value, (int, float, D)):
dd359bc0 138 raise TypeError("Amount may only be multiplied by integers")
5ab23e1c 139 return Amount(self.currency, self.value / value)
dd359bc0
IB
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")
5ab23e1c 147 return self.value < other.value
dd359bc0
IB
148
149 def __eq__(self, other):
150 if other == 0:
5ab23e1c 151 return self.value == 0
dd359bc0
IB
152 if self.currency != other.currency:
153 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 154 return self.value == other.value
dd359bc0
IB
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
168class Balance:
169 known_balances = {}
dd359bc0
IB
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
f2da6589
IB
177 @classmethod
178 def from_hash(cls, currency, hash_):
179 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
180
dd359bc0 181 @classmethod
deb8924c 182 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
dd359bc0
IB
183 amounts = {}
184 for currency in cls.known_balances:
185 balance = cls.known_balances[currency]
186 other_currency_amount = getattr(balance, type)\
deb8924c 187 .in_currency(other_currency, market, compute_value=compute_value)
dd359bc0
IB
188 amounts[currency] = other_currency_amount
189 return amounts
190
191 @classmethod
192 def currencies(cls):
193 return cls.known_balances.keys()
194
dd359bc0
IB
195 @classmethod
196 def _fill_balances(cls, hash_):
197 for key in hash_:
198 if key in ["info", "free", "used", "total"]:
199 continue
350ed24d 200 if hash_[key]["total"] != 0 or key in cls.known_balances:
dd359bc0
IB
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
350ed24d
IB
207 # FIXME:Separate balances per trade type and in position
208 # Need to check how balances in position are represented
209
dd359bc0
IB
210
211 @classmethod
7ab23e29
IB
212 def dispatch_assets(cls, amount, repartition=None):
213 if repartition is None:
350ed24d
IB
214 repartition = Portfolio.repartition()
215 sum_ratio = sum([v[0] for k, v in repartition.items()])
dd359bc0 216 amounts = {}
350ed24d
IB
217 for currency, (ptt, trade_type) in repartition.items():
218 amounts[currency] = ptt * amount / sum_ratio
dd359bc0
IB
219 if currency not in cls.known_balances:
220 cls.known_balances[currency] = cls(currency, 0, 0, 0)
221 return amounts
222
350ed24d
IB
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
dd359bc0 234 @classmethod
a9950fd0 235 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
dd359bc0 236 cls.fetch_balances(market)
a9950fd0 237 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
dd359bc0
IB
238 total_base_value = sum(values_in_base.values())
239 new_repartition = cls.dispatch_assets(total_base_value)
350ed24d 240 trade_types = cls.dispatch_trade_types()
deb8924c 241 # Recompute it in case we have new currencies
a9950fd0 242 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
350ed24d 243 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
a9950fd0
IB
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)
350ed24d
IB
251 trade_types = cls.dispatch_trade_types()
252 Trade.compute_trades(values_in_base, new_repartition, trade_types, only=only, market=market)
dd359bc0 253
7ab23e29
IB
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())
350ed24d
IB
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)
7ab23e29 262
dd359bc0
IB
263 def __repr__(self):
264 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
265
deb8924c 266class Computation:
deb8924c
IB
267 computations = {
268 "default": lambda x, y: x[y],
deb8924c
IB
269 "average": lambda x, y: x["average"],
270 "bid": lambda x, y: x["bid"],
271 "ask": lambda x, y: x["ask"],
272 }
273
dd359bc0
IB
274class Trade:
275 trades = {}
276
350ed24d 277 def __init__(self, value_from, value_to, currency, trade_type, market=None):
dd359bc0
IB
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
350ed24d 283 self.trade_type = trade_type
dd359bc0 284 self.orders = []
089d5d9d 285 self.market = market
dd359bc0
IB
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
cfab619d
IB
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,
5ab23e1c 304 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
cfab619d
IB
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__)])
e0b14bcc 325 except ExchangeError:
cfab619d
IB
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__)])
e0b14bcc 329 except ExchangeError:
cfab619d
IB
330 cls.ticker_cache[(c1, c2, market.__class__)] = None
331 return cls.get_ticker(c1, c2, market)
332
dd359bc0 333 @classmethod
350ed24d 334 def compute_trades(cls, values_in_base, new_repartition, trade_types, only=None, market=None):
dd359bc0
IB
335 base_currency = sum(values_in_base.values()).currency
336 for currency in Balance.currencies():
337 if currency == base_currency:
338 continue
a9950fd0 339 trade = cls(
dd359bc0
IB
340 values_in_base.get(currency, Amount(base_currency, 0)),
341 new_repartition.get(currency, Amount(base_currency, 0)),
342 currency,
350ed24d 343 trade_types.get(currency, "long"),
dd359bc0
IB
344 market=market
345 )
a9950fd0
IB
346 if only is None or trade.action == only:
347 cls.trades[currency] = trade
dd359bc0
IB
348 return cls.trades
349
a9950fd0
IB
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
dd359bc0
IB
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
cfab619d 368 def order_action(self, inverted):
350ed24d
IB
369 # a xor b xor c
370 if (self.trade_type == "short") != ((self.value_from < self.value_to) != inverted):
371 return "buy"
dd359bc0 372 else:
350ed24d 373 return "sell"
dd359bc0 374
deb8924c 375 def prepare_order(self, compute_value="default"):
dd359bc0
IB
376 if self.action is None:
377 return
350ed24d 378 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market)
dd359bc0 379 inverted = ticker["inverted"]
f2097d71
IB
380 if inverted:
381 ticker = ticker["original"]
382 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
c11e4274 383 # 0.1
f2097d71 384
350ed24d
IB
385 # FIXME: optimize if value_to == 0 or value_from == 0?)
386
f2097d71 387 delta_in_base = abs(self.value_from - self.value_to)
c11e4274 388 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0
IB
389
390 if not inverted:
350ed24d
IB
391 currency = self.base_currency
392 # BTC
f2097d71 393 if self.action == "sell":
c11e4274
IB
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
f2097d71 396 value_from = self.value_from.linked_to
c11e4274 397 # value_from = 100 FOO
f2097d71 398 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
c11e4274 399 # value_to = 10 FOO (1 BTC * 1/0.1)
f2097d71 400 delta = abs(value_to - value_from)
c11e4274
IB
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
f2097d71
IB
405 else:
406 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
c11e4274
IB
407 # I want to buy 9 / 0.1 FOO
408 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
dd359bc0 409 else:
dd359bc0 410 currency = self.currency
c11e4274 411 # FOO
350ed24d
IB
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"
dd359bc0 420
350ed24d 421 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.trade_type, self.market))
dd359bc0 422
deb8924c
IB
423 @classmethod
424 def compute_value(cls, ticker, action, compute_value="default"):
350ed24d
IB
425 if action == "buy":
426 action = "ask"
427 if action == "sell":
428 action = "bid"
77f8a378 429 if isinstance(compute_value, str):
deb8924c
IB
430 compute_value = Computation.computations[compute_value]
431 return compute_value(ticker, action)
432
dd359bc0 433 @classmethod
a9950fd0
IB
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()
dd359bc0
IB
445
446 @classmethod
a9950fd0 447 def follow_orders(cls, verbose=True, sleep=30):
dd359bc0
IB
448 orders = cls.all_orders()
449 finished_orders = []
450 while len(orders) != len(finished_orders):
a9950fd0 451 time.sleep(sleep)
dd359bc0
IB
452 for order in orders:
453 if order in finished_orders:
454 continue
a9950fd0 455 if order.get_status() != "open":
dd359bc0 456 finished_orders.append(order)
a9950fd0
IB
457 if verbose:
458 print("finished {}".format(order))
459 if verbose:
460 print("All orders finished")
dd359bc0 461
272b3cfb
IB
462 @classmethod
463 def update_all_orders_status(cls):
464 for order in cls.all_orders(state="open"):
465 order.get_status()
466
dd359bc0 467 def __repr__(self):
350ed24d 468 return "Trade({} -> {} in {}, {} {})".format(
dd359bc0
IB
469 self.value_from,
470 self.value_to,
471 self.currency,
350ed24d
IB
472 self.action,
473 self.trade_type)
dd359bc0 474
272b3cfb
IB
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="")
dd359bc0 484
272b3cfb 485class Order:
350ed24d 486 def __init__(self, action, amount, rate, base_currency, trade_type, market):
dd359bc0
IB
487 self.action = action
488 self.amount = amount
489 self.rate = rate
490 self.base_currency = base_currency
a9950fd0 491 self.market = market
350ed24d 492 self.trade_type = trade_type
dd359bc0 493 self.result = None
a9950fd0 494 self.status = "pending"
dd359bc0
IB
495
496 def __repr__(self):
350ed24d 497 return "Order({} {} {} at {} {} [{}])".format(
dd359bc0 498 self.action,
350ed24d 499 self.trade_type,
dd359bc0
IB
500 self.amount,
501 self.rate,
502 self.base_currency,
503 self.status
504 )
505
350ed24d
IB
506 @property
507 def account(self):
508 if self.trade_type == "long":
509 return "exchange"
510 else:
511 return "margin"
512
a9950fd0
IB
513 @property
514 def pending(self):
515 return self.status == "pending"
516
517 @property
518 def finished(self):
fd8afa51 519 return self.status == "closed" or self.status == "canceled" or self.status == "error"
a9950fd0 520
643767f1 521 def run(self, debug=False):
dd359bc0 522 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
350ed24d 523 amount = round(self.amount, self.market.order_precision(symbol)).value
dd359bc0 524
643767f1 525 if debug:
ecba1113
IB
526 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
527 symbol, self.action, amount, self.rate, self.account))
dd359bc0
IB
528 else:
529 try:
350ed24d
IB
530 if self.action == "sell" and self.trade_type == "short":
531 assert self.market.transfer_balance(self.base_currency, amount * self.rate, "exchange", "margin")
ecba1113 532 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)
dd359bc0 533 self.status = "open"
fd8afa51
IB
534 except Exception as e:
535 self.status = "error"
ecba1113
IB
536 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
537 symbol, self.action, amount, self.rate, self.account))
fd8afa51
IB
538 self.error_message = str("{}: {}".format(e.__class__.__name__, e))
539 print(self.error_message)
dd359bc0 540
a9950fd0 541 def get_status(self):
dd359bc0
IB
542 # other states are "closed" and "canceled"
543 if self.status == "open":
a9950fd0 544 result = self.market.fetch_order(self.result['id'])
dd359bc0
IB
545 self.status = result["status"]
546 return self.status
547
272b3cfb
IB
548 def cancel(self):
549 self.market.cancel_order(self.result['id'])
550
dd359bc0 551def print_orders(market, base_currency="BTC"):
deb8924c 552 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
a9950fd0 553 Trade.prepare_orders(compute_value="average")
5ab23e1c
IB
554 for currency, balance in Balance.known_balances.items():
555 print(balance)
350ed24d 556 Trade.print_all_with_order()
dd359bc0
IB
557
558def 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="")
a9950fd0 564 order.run()
dd359bc0
IB
565
566if __name__ == '__main__':
567 print_orders(market)