]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - portfolio.py
Add debug mode parameter rather than global flag
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
1 import ccxt
2 import time
3 from decimal import Decimal as D
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_pertenthousand(cls, liquidity="medium"):
14 cls.parse_cryptoportfolio()
15 liquidities = cls.liquidities[liquidity]
16 last_date = sorted(liquidities.keys())[-1]
17 return liquidities[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
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:
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 type(h[1][i]) == str:
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
76 class Amount:
77 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
78 self.currency = currency
79 self.value = D(value)
80 self.linked_to = linked_to
81 self.ticker = ticker
82 self.rate = rate
83
84 self.ticker_cache = {}
85 self.ticker_cache_timestamp = time.time()
86
87 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
88 if other_currency == self.currency:
89 return self
90 if rate is not None:
91 return Amount(
92 other_currency,
93 self.value * rate,
94 linked_to=self,
95 rate=rate)
96 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
97 if asset_ticker is not None:
98 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
99 return Amount(
100 other_currency,
101 self.value * rate,
102 linked_to=self,
103 ticker=asset_ticker,
104 rate=rate)
105 else:
106 raise Exception("This asset is not available in the chosen market")
107
108 def __abs__(self):
109 return Amount(self.currency, abs(self.value))
110
111 def __add__(self, other):
112 if other.currency != self.currency and other.value * self.value != 0:
113 raise Exception("Summing amounts must be done with same currencies")
114 return Amount(self.currency, self.value + other.value)
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):
123 if other.currency != self.currency and other.value * self.value != 0:
124 raise Exception("Summing amounts must be done with same currencies")
125 return Amount(self.currency, self.value - other.value)
126
127 def __mul__(self, value):
128 if type(value) != int and type(value) != float and type(value) != D:
129 raise TypeError("Amount may only be multiplied by numbers")
130 return Amount(self.currency, self.value * value)
131
132 def __rmul__(self, value):
133 return self.__mul__(value)
134
135 def __floordiv__(self, value):
136 if type(value) != int and type(value) != float and type(value) != D:
137 raise TypeError("Amount may only be multiplied by integers")
138 return Amount(self.currency, self.value / value)
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")
146 return self.value < other.value
147
148 def __eq__(self, other):
149 if other == 0:
150 return self.value == 0
151 if self.currency != other.currency:
152 raise Exception("Comparing amounts must be done with same currencies")
153 return self.value == other.value
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
167 class Balance:
168 known_balances = {}
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
176 @classmethod
177 def from_hash(cls, currency, hash_):
178 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
179
180 @classmethod
181 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
182 amounts = {}
183 for currency in cls.known_balances:
184 balance = cls.known_balances[currency]
185 other_currency_amount = getattr(balance, type)\
186 .in_currency(other_currency, market, compute_value=compute_value)
187 amounts[currency] = other_currency_amount
188 return amounts
189
190 @classmethod
191 def currencies(cls):
192 return cls.known_balances.keys()
193
194 @classmethod
195 def _fill_balances(cls, hash_):
196 for key in hash_:
197 if key in ["info", "free", "used", "total"]:
198 continue
199 if hash_[key]["total"] > 0 or key in cls.known_balances:
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
208 def dispatch_assets(cls, amount):
209 repartition_pertenthousand = Portfolio.repartition_pertenthousand()
210 sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
211 amounts = {}
212 for currency, ptt in repartition_pertenthousand.items():
213 amounts[currency] = ptt * amount / sum_pertenthousand
214 if currency not in cls.known_balances:
215 cls.known_balances[currency] = cls(currency, 0, 0, 0)
216 return amounts
217
218 @classmethod
219 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
220 cls.fetch_balances(market)
221 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
222 total_base_value = sum(values_in_base.values())
223 new_repartition = cls.dispatch_assets(total_base_value)
224 # Recompute it in case we have new currencies
225 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
226 Trade.compute_trades(values_in_base, new_repartition, market=market)
227
228 @classmethod
229 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
230 cls.fetch_balances(market)
231 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
232 total_base_value = sum(values_in_base.values())
233 new_repartition = cls.dispatch_assets(total_base_value)
234 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)
235
236 def __repr__(self):
237 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
238
239 class Computation:
240 computations = {
241 "default": lambda x, y: x[y],
242 "average": lambda x, y: x["average"],
243 "bid": lambda x, y: x["bid"],
244 "ask": lambda x, y: x["ask"],
245 }
246
247
248 class Trade:
249 trades = {}
250
251 def __init__(self, value_from, value_to, currency, market=None):
252 # We have value_from of currency, and want to finish with value_to of
253 # that currency. value_* may not be in currency's terms
254 self.currency = currency
255 self.value_from = value_from
256 self.value_to = value_to
257 self.orders = []
258 self.market = market
259 assert self.value_from.currency == self.value_to.currency
260 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
261 self.base_currency = self.value_from.currency
262
263 fees_cache = {}
264 @classmethod
265 def fetch_fees(cls, market):
266 if market.__class__ not in cls.fees_cache:
267 cls.fees_cache[market.__class__] = market.fetch_fees()
268 return cls.fees_cache[market.__class__]
269
270 ticker_cache = {}
271 ticker_cache_timestamp = time.time()
272 @classmethod
273 def get_ticker(cls, c1, c2, market, refresh=False):
274 def invert(ticker):
275 return {
276 "inverted": True,
277 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
278 "original": ticker,
279 }
280 def augment_ticker(ticker):
281 ticker.update({
282 "inverted": False,
283 "average": (ticker["bid"] + ticker["ask"] ) / 2,
284 })
285
286 if time.time() - cls.ticker_cache_timestamp > 5:
287 cls.ticker_cache = {}
288 cls.ticker_cache_timestamp = time.time()
289 elif not refresh:
290 if (c1, c2, market.__class__) in cls.ticker_cache:
291 return cls.ticker_cache[(c1, c2, market.__class__)]
292 if (c2, c1, market.__class__) in cls.ticker_cache:
293 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
294
295 try:
296 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
297 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
298 except ccxt.ExchangeError:
299 try:
300 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
301 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
302 except ccxt.ExchangeError:
303 cls.ticker_cache[(c1, c2, market.__class__)] = None
304 return cls.get_ticker(c1, c2, market)
305
306 @classmethod
307 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
308 base_currency = sum(values_in_base.values()).currency
309 for currency in Balance.currencies():
310 if currency == base_currency:
311 continue
312 trade = cls(
313 values_in_base.get(currency, Amount(base_currency, 0)),
314 new_repartition.get(currency, Amount(base_currency, 0)),
315 currency,
316 market=market
317 )
318 if only is None or trade.action == only:
319 cls.trades[currency] = trade
320 return cls.trades
321
322 @classmethod
323 def prepare_orders(cls, only=None, compute_value="default"):
324 for currency, trade in cls.trades.items():
325 if only is None or trade.action == only:
326 trade.prepare_order(compute_value=compute_value)
327
328 @property
329 def action(self):
330 if self.value_from == self.value_to:
331 return None
332 if self.base_currency == self.currency:
333 return None
334
335 if self.value_from < self.value_to:
336 return "buy"
337 else:
338 return "sell"
339
340 def order_action(self, inverted):
341 if self.value_from < self.value_to:
342 return "ask" if not inverted else "bid"
343 else:
344 return "bid" if not inverted else "ask"
345
346 def prepare_order(self, compute_value="default"):
347 if self.action is None:
348 return
349 ticker = self.value_from.ticker
350 inverted = ticker["inverted"]
351
352 if not inverted:
353 value_from = self.value_from.linked_to
354 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
355 delta = abs(value_to - value_from)
356 currency = self.base_currency
357 else:
358 ticker = ticker["original"]
359 delta = abs(self.value_to - self.value_from)
360 currency = self.currency
361
362 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
363
364 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market))
365
366 @classmethod
367 def compute_value(cls, ticker, action, compute_value="default"):
368 if type(compute_value) == str:
369 compute_value = Computation.computations[compute_value]
370 return compute_value(ticker, action)
371
372 @classmethod
373 def all_orders(cls, state=None):
374 all_orders = sum(map(lambda v: v.orders, cls.trades.values()), [])
375 if state is None:
376 return all_orders
377 else:
378 return list(filter(lambda o: o.status == state, all_orders))
379
380 @classmethod
381 def run_orders(cls):
382 for order in cls.all_orders(state="pending"):
383 order.run()
384
385 @classmethod
386 def follow_orders(cls, verbose=True, sleep=30):
387 orders = cls.all_orders()
388 finished_orders = []
389 while len(orders) != len(finished_orders):
390 time.sleep(sleep)
391 for order in orders:
392 if order in finished_orders:
393 continue
394 if order.get_status() != "open":
395 finished_orders.append(order)
396 if verbose:
397 print("finished {}".format(order))
398 if verbose:
399 print("All orders finished")
400
401 def __repr__(self):
402 return "Trade({} -> {} in {}, {})".format(
403 self.value_from,
404 self.value_to,
405 self.currency,
406 self.action)
407
408 class Order:
409
410 def __init__(self, action, amount, rate, base_currency, market):
411 self.action = action
412 self.amount = amount
413 self.rate = rate
414 self.base_currency = base_currency
415 self.market = market
416 self.result = None
417 self.status = "pending"
418
419 def __repr__(self):
420 return "Order({} {} at {} {} [{}])".format(
421 self.action,
422 self.amount,
423 self.rate,
424 self.base_currency,
425 self.status
426 )
427
428 @property
429 def pending(self):
430 return self.status == "pending"
431
432 @property
433 def finished(self):
434 return self.status == "closed" or self.status == "canceled"
435
436 def run(self, debug=False):
437 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
438 amount = self.amount.value
439
440 if debug:
441 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
442 symbol, self.action, amount, self.rate))
443 else:
444 try:
445 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
446 self.status = "open"
447 except Exception:
448 pass
449
450 def get_status(self):
451 # other states are "closed" and "canceled"
452 if self.status == "open":
453 result = self.market.fetch_order(self.result['id'])
454 self.status = result["status"]
455 return self.status
456
457 def print_orders(market, base_currency="BTC"):
458 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
459 Trade.prepare_orders(compute_value="average")
460 for currency, balance in Balance.known_balances.items():
461 print(balance)
462 for currency, trade in Trade.trades.items():
463 print(trade)
464 for order in trade.orders:
465 print("\t", order, sep="")
466
467 def make_orders(market, base_currency="BTC"):
468 Balance.prepare_trades(market, base_currency=base_currency)
469 for currency, trade in Trade.trades.items():
470 print(trade)
471 for order in trade.orders:
472 print("\t", order, sep="")
473 order.run()
474
475 if __name__ == '__main__':
476 print_orders(market)