]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Add test for Trade
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
e0b14bcc 1from ccxt import ExchangeError
dd359bc0 2import time
5ab23e1c 3from decimal import Decimal as D
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
13 def repartition_pertenthousand(cls, liquidity="medium"):
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):
43 if weight_hash[1] == 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):
77f8a378 51 if isinstance(h[1][i], str):
dd359bc0
IB
52 return [h[0], h[1][i]]
53 else:
54 return [h[0], int(h[1][i] * 10000)]
55 return clean_weights_
56
57 def parse_weights(portfolio_hash):
58 # FIXME: we'll need shorts at some point
59 assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"]))
60 weights_hash = portfolio_hash["weights"]
61 weights = {}
62 for i in range(len(weights_hash["_row"])):
63 weights[weights_hash["_row"][i]] = dict(filter(
64 filter_weights,
65 map(clean_weights(i), weights_hash.items())))
66 return weights
67
68 high_liquidity = parse_weights(cls.data["portfolio_1"])
69 medium_liquidity = parse_weights(cls.data["portfolio_2"])
70
71 cls.liquidities = {
72 "medium": medium_liquidity,
73 "high": high_liquidity,
74 }
75
76class Amount:
c2644ba8 77 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
dd359bc0 78 self.currency = currency
5ab23e1c 79 self.value = D(value)
dd359bc0
IB
80 self.linked_to = linked_to
81 self.ticker = ticker
c2644ba8 82 self.rate = rate
dd359bc0
IB
83
84 self.ticker_cache = {}
85 self.ticker_cache_timestamp = time.time()
86
c2644ba8 87 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
dd359bc0
IB
88 if other_currency == self.currency:
89 return self
c2644ba8
IB
90 if rate is not None:
91 return Amount(
92 other_currency,
93 self.value * rate,
94 linked_to=self,
95 rate=rate)
cfab619d 96 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
dd359bc0 97 if asset_ticker is not None:
c2644ba8 98 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
dd359bc0
IB
99 return Amount(
100 other_currency,
c2644ba8 101 self.value * rate,
dd359bc0 102 linked_to=self,
c2644ba8
IB
103 ticker=asset_ticker,
104 rate=rate)
dd359bc0
IB
105 else:
106 raise Exception("This asset is not available in the chosen market")
107
dd359bc0 108 def __abs__(self):
5ab23e1c 109 return Amount(self.currency, abs(self.value))
dd359bc0
IB
110
111 def __add__(self, other):
5ab23e1c 112 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 113 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 114 return Amount(self.currency, self.value + other.value)
dd359bc0
IB
115
116 def __radd__(self, other):
117 if other == 0:
118 return self
119 else:
120 return self.__add__(other)
121
122 def __sub__(self, other):
5ab23e1c 123 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 124 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 125 return Amount(self.currency, self.value - other.value)
dd359bc0
IB
126
127 def __mul__(self, value):
77f8a378 128 if not isinstance(value, (int, float, D)):
dd359bc0 129 raise TypeError("Amount may only be multiplied by numbers")
5ab23e1c 130 return Amount(self.currency, self.value * value)
dd359bc0
IB
131
132 def __rmul__(self, value):
133 return self.__mul__(value)
134
135 def __floordiv__(self, value):
77f8a378 136 if not isinstance(value, (int, float, D)):
dd359bc0 137 raise TypeError("Amount may only be multiplied by integers")
5ab23e1c 138 return Amount(self.currency, self.value / value)
dd359bc0
IB
139
140 def __truediv__(self, value):
141 return self.__floordiv__(value)
142
143 def __lt__(self, other):
144 if self.currency != other.currency:
145 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 146 return self.value < other.value
dd359bc0
IB
147
148 def __eq__(self, other):
149 if other == 0:
5ab23e1c 150 return self.value == 0
dd359bc0
IB
151 if self.currency != other.currency:
152 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 153 return self.value == other.value
dd359bc0
IB
154
155 def __str__(self):
156 if self.linked_to is None:
157 return "{:.8f} {}".format(self.value, self.currency)
158 else:
159 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
160
161 def __repr__(self):
162 if self.linked_to is None:
163 return "Amount({:.8f} {})".format(self.value, self.currency)
164 else:
165 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
166
167class Balance:
168 known_balances = {}
dd359bc0
IB
169
170 def __init__(self, currency, total_value, free_value, used_value):
171 self.currency = currency
172 self.total = Amount(currency, total_value)
173 self.free = Amount(currency, free_value)
174 self.used = Amount(currency, used_value)
175
f2da6589
IB
176 @classmethod
177 def from_hash(cls, currency, hash_):
178 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
179
dd359bc0 180 @classmethod
deb8924c 181 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
dd359bc0
IB
182 amounts = {}
183 for currency in cls.known_balances:
184 balance = cls.known_balances[currency]
185 other_currency_amount = getattr(balance, type)\
deb8924c 186 .in_currency(other_currency, market, compute_value=compute_value)
dd359bc0
IB
187 amounts[currency] = other_currency_amount
188 return amounts
189
190 @classmethod
191 def currencies(cls):
192 return cls.known_balances.keys()
193
dd359bc0
IB
194 @classmethod
195 def _fill_balances(cls, hash_):
196 for key in hash_:
197 if key in ["info", "free", "used", "total"]:
198 continue
a9950fd0 199 if hash_[key]["total"] > 0 or key in cls.known_balances:
dd359bc0
IB
200 cls.known_balances[key] = cls.from_hash(key, hash_[key])
201
202 @classmethod
203 def fetch_balances(cls, market):
204 cls._fill_balances(market.fetch_balance())
205 return cls.known_balances
206
207 @classmethod
7ab23e29
IB
208 def dispatch_assets(cls, amount, repartition=None):
209 if repartition is None:
210 repartition = Portfolio.repartition_pertenthousand()
211 sum_pertenthousand = sum([v for k, v in repartition.items()])
dd359bc0 212 amounts = {}
7ab23e29 213 for currency, ptt in repartition.items():
dd359bc0
IB
214 amounts[currency] = ptt * amount / sum_pertenthousand
215 if currency not in cls.known_balances:
216 cls.known_balances[currency] = cls(currency, 0, 0, 0)
217 return amounts
218
219 @classmethod
a9950fd0 220 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
dd359bc0 221 cls.fetch_balances(market)
a9950fd0 222 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
dd359bc0
IB
223 total_base_value = sum(values_in_base.values())
224 new_repartition = cls.dispatch_assets(total_base_value)
deb8924c 225 # Recompute it in case we have new currencies
a9950fd0
IB
226 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
227 Trade.compute_trades(values_in_base, new_repartition, market=market)
228
229 @classmethod
230 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
231 cls.fetch_balances(market)
232 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
233 total_base_value = sum(values_in_base.values())
234 new_repartition = cls.dispatch_assets(total_base_value)
235 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)
dd359bc0 236
7ab23e29
IB
237 @classmethod
238 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
239 cls.fetch_balances(market)
240 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
241 total_base_value = sum(values_in_base.values())
242 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: 1 })
243 Trade.compute_trades(values_in_base, new_repartition, market=market)
244
dd359bc0
IB
245 def __repr__(self):
246 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
247
deb8924c 248class Computation:
deb8924c
IB
249 computations = {
250 "default": lambda x, y: x[y],
deb8924c
IB
251 "average": lambda x, y: x["average"],
252 "bid": lambda x, y: x["bid"],
253 "ask": lambda x, y: x["ask"],
254 }
255
256
dd359bc0
IB
257class Trade:
258 trades = {}
259
260 def __init__(self, value_from, value_to, currency, market=None):
261 # We have value_from of currency, and want to finish with value_to of
262 # that currency. value_* may not be in currency's terms
263 self.currency = currency
264 self.value_from = value_from
265 self.value_to = value_to
266 self.orders = []
089d5d9d 267 self.market = market
dd359bc0
IB
268 assert self.value_from.currency == self.value_to.currency
269 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
270 self.base_currency = self.value_from.currency
271
cfab619d
IB
272 fees_cache = {}
273 @classmethod
274 def fetch_fees(cls, market):
275 if market.__class__ not in cls.fees_cache:
276 cls.fees_cache[market.__class__] = market.fetch_fees()
277 return cls.fees_cache[market.__class__]
278
279 ticker_cache = {}
280 ticker_cache_timestamp = time.time()
281 @classmethod
282 def get_ticker(cls, c1, c2, market, refresh=False):
283 def invert(ticker):
284 return {
285 "inverted": True,
5ab23e1c 286 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
cfab619d
IB
287 "original": ticker,
288 }
289 def augment_ticker(ticker):
290 ticker.update({
291 "inverted": False,
292 "average": (ticker["bid"] + ticker["ask"] ) / 2,
293 })
294
295 if time.time() - cls.ticker_cache_timestamp > 5:
296 cls.ticker_cache = {}
297 cls.ticker_cache_timestamp = time.time()
298 elif not refresh:
299 if (c1, c2, market.__class__) in cls.ticker_cache:
300 return cls.ticker_cache[(c1, c2, market.__class__)]
301 if (c2, c1, market.__class__) in cls.ticker_cache:
302 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
303
304 try:
305 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
306 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
e0b14bcc 307 except ExchangeError:
cfab619d
IB
308 try:
309 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
310 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
e0b14bcc 311 except ExchangeError:
cfab619d
IB
312 cls.ticker_cache[(c1, c2, market.__class__)] = None
313 return cls.get_ticker(c1, c2, market)
314
dd359bc0 315 @classmethod
a9950fd0 316 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
dd359bc0
IB
317 base_currency = sum(values_in_base.values()).currency
318 for currency in Balance.currencies():
319 if currency == base_currency:
320 continue
a9950fd0 321 trade = cls(
dd359bc0
IB
322 values_in_base.get(currency, Amount(base_currency, 0)),
323 new_repartition.get(currency, Amount(base_currency, 0)),
324 currency,
325 market=market
326 )
a9950fd0
IB
327 if only is None or trade.action == only:
328 cls.trades[currency] = trade
dd359bc0
IB
329 return cls.trades
330
a9950fd0
IB
331 @classmethod
332 def prepare_orders(cls, only=None, compute_value="default"):
333 for currency, trade in cls.trades.items():
334 if only is None or trade.action == only:
335 trade.prepare_order(compute_value=compute_value)
336
dd359bc0
IB
337 @property
338 def action(self):
339 if self.value_from == self.value_to:
340 return None
341 if self.base_currency == self.currency:
342 return None
343
344 if self.value_from < self.value_to:
345 return "buy"
346 else:
347 return "sell"
348
cfab619d 349 def order_action(self, inverted):
dd359bc0 350 if self.value_from < self.value_to:
b83d4897 351 return "buy" if not inverted else "sell"
dd359bc0 352 else:
b83d4897 353 return "sell" if not inverted else "buy"
dd359bc0 354
deb8924c 355 def prepare_order(self, compute_value="default"):
dd359bc0
IB
356 if self.action is None:
357 return
358 ticker = self.value_from.ticker
359 inverted = ticker["inverted"]
f2097d71
IB
360 if inverted:
361 ticker = ticker["original"]
362 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
c11e4274 363 # 0.1
f2097d71 364
f2097d71 365 delta_in_base = abs(self.value_from - self.value_to)
c11e4274 366 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0
IB
367
368 if not inverted:
f2097d71 369 if self.action == "sell":
c11e4274
IB
370 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
371 # At rate 1 Foo = 0.1 BTC
f2097d71 372 value_from = self.value_from.linked_to
c11e4274 373 # value_from = 100 FOO
f2097d71 374 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
c11e4274 375 # value_to = 10 FOO (1 BTC * 1/0.1)
f2097d71 376 delta = abs(value_to - value_from)
c11e4274
IB
377 # delta = 90 FOO
378 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
379
380 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
f2097d71
IB
381 else:
382 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
c11e4274
IB
383 # I want to buy 9 / 0.1 FOO
384 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
385
386 # FIXME: Need to round up to the correct amount of FOO in case
387 # we want to use all BTC
dd359bc0 388 currency = self.base_currency
c11e4274 389 # BTC
dd359bc0 390 else:
f2097d71 391 if self.action == "sell":
c11e4274
IB
392 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
393 # At rate 1 Foo = 0.1 BTC
394 delta = delta_in_base
395 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
396
397 # FIXME: Need to round up to the correct amount of FOO in case
398 # we want to sell all
f2097d71
IB
399 else:
400 delta = delta_in_base
c11e4274
IB
401 # I want to buy 9 / 0.1 FOO
402 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
403
404 # FIXME: Need to round up to the correct amount of FOO in case
405 # we want to use all BTC
406
dd359bc0 407 currency = self.currency
c11e4274 408 # FOO
dd359bc0 409
a9950fd0 410 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market))
dd359bc0 411
deb8924c
IB
412 @classmethod
413 def compute_value(cls, ticker, action, compute_value="default"):
77f8a378 414 if isinstance(compute_value, str):
deb8924c
IB
415 compute_value = Computation.computations[compute_value]
416 return compute_value(ticker, action)
417
dd359bc0 418 @classmethod
a9950fd0
IB
419 def all_orders(cls, state=None):
420 all_orders = sum(map(lambda v: v.orders, cls.trades.values()), [])
421 if state is None:
422 return all_orders
423 else:
424 return list(filter(lambda o: o.status == state, all_orders))
425
426 @classmethod
427 def run_orders(cls):
428 for order in cls.all_orders(state="pending"):
429 order.run()
dd359bc0
IB
430
431 @classmethod
a9950fd0 432 def follow_orders(cls, verbose=True, sleep=30):
dd359bc0
IB
433 orders = cls.all_orders()
434 finished_orders = []
435 while len(orders) != len(finished_orders):
a9950fd0 436 time.sleep(sleep)
dd359bc0
IB
437 for order in orders:
438 if order in finished_orders:
439 continue
a9950fd0 440 if order.get_status() != "open":
dd359bc0 441 finished_orders.append(order)
a9950fd0
IB
442 if verbose:
443 print("finished {}".format(order))
444 if verbose:
445 print("All orders finished")
dd359bc0 446
272b3cfb
IB
447 @classmethod
448 def update_all_orders_status(cls):
449 for order in cls.all_orders(state="open"):
450 order.get_status()
451
dd359bc0
IB
452 def __repr__(self):
453 return "Trade({} -> {} in {}, {})".format(
454 self.value_from,
455 self.value_to,
456 self.currency,
457 self.action)
458
272b3cfb
IB
459 @classmethod
460 def print_all_with_order(cls):
461 for trade in cls.trades.values():
462 trade.print_with_order()
463
464 def print_with_order(self):
465 print(self)
466 for order in self.orders:
467 print("\t", order, sep="")
dd359bc0 468
272b3cfb 469class Order:
a9950fd0 470 def __init__(self, action, amount, rate, base_currency, market):
dd359bc0
IB
471 self.action = action
472 self.amount = amount
473 self.rate = rate
474 self.base_currency = base_currency
a9950fd0 475 self.market = market
dd359bc0 476 self.result = None
a9950fd0 477 self.status = "pending"
dd359bc0
IB
478
479 def __repr__(self):
480 return "Order({} {} at {} {} [{}])".format(
481 self.action,
482 self.amount,
483 self.rate,
484 self.base_currency,
485 self.status
486 )
487
a9950fd0
IB
488 @property
489 def pending(self):
490 return self.status == "pending"
491
492 @property
493 def finished(self):
fd8afa51 494 return self.status == "closed" or self.status == "canceled" or self.status == "error"
a9950fd0 495
643767f1 496 def run(self, debug=False):
dd359bc0
IB
497 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
498 amount = self.amount.value
499
643767f1 500 if debug:
dd359bc0
IB
501 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
502 symbol, self.action, amount, self.rate))
503 else:
504 try:
a9950fd0 505 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
dd359bc0 506 self.status = "open"
fd8afa51
IB
507 except Exception as e:
508 self.status = "error"
509 print("error when running market.create_order('{}', 'limit', '{}', {}, price={})".format(
510 symbol, self.action, amount, self.rate))
511 self.error_message = str("{}: {}".format(e.__class__.__name__, e))
512 print(self.error_message)
dd359bc0 513
a9950fd0 514 def get_status(self):
dd359bc0
IB
515 # other states are "closed" and "canceled"
516 if self.status == "open":
a9950fd0 517 result = self.market.fetch_order(self.result['id'])
dd359bc0
IB
518 self.status = result["status"]
519 return self.status
520
272b3cfb
IB
521 def cancel(self):
522 self.market.cancel_order(self.result['id'])
523
dd359bc0 524def print_orders(market, base_currency="BTC"):
deb8924c 525 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
a9950fd0 526 Trade.prepare_orders(compute_value="average")
5ab23e1c
IB
527 for currency, balance in Balance.known_balances.items():
528 print(balance)
272b3cfb 529 portfolio.Trade.print_all_with_order()
dd359bc0
IB
530
531def make_orders(market, base_currency="BTC"):
532 Balance.prepare_trades(market, base_currency=base_currency)
533 for currency, trade in Trade.trades.items():
534 print(trade)
535 for order in trade.orders:
536 print("\t", order, sep="")
a9950fd0 537 order.run()
dd359bc0
IB
538
539if __name__ == '__main__':
540 print_orders(market)