]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - portfolio.py
Complete refactor of the script to use classes
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
1 import ccxt
2 import time
3 # Put your poloniex api key in market.py
4 from market import market
5
6 # FIXME: Améliorer le bid/ask
7 # FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition
8 # FIXME: better compute moves to avoid rounding errors
9
10 def static_var(varname, value):
11 def decorate(func):
12 setattr(func, varname, value)
13 return func
14 return decorate
15
16 class Portfolio:
17 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
18 liquidities = {}
19 data = None
20
21 @classmethod
22 def repartition_pertenthousand(cls, liquidity="medium"):
23 cls.parse_cryptoportfolio()
24 liquidities = cls.liquidities[liquidity]
25 last_date = sorted(liquidities.keys())[-1]
26 return liquidities[last_date]
27
28 @classmethod
29 def get_cryptoportfolio(cls):
30 import json
31 import urllib3
32 urllib3.disable_warnings()
33 http = urllib3.PoolManager()
34
35 r = http.request("GET", cls.URL)
36 cls.data = json.loads(r.data)
37
38 @classmethod
39 def parse_cryptoportfolio(cls):
40 if cls.data is None:
41 cls.get_cryptoportfolio()
42
43 def filter_weights(weight_hash):
44 if weight_hash[1] == 0:
45 return False
46 if weight_hash[0] == "_row":
47 return False
48 return True
49
50 def clean_weights(i):
51 def clean_weights_(h):
52 if type(h[1][i]) == str:
53 return [h[0], h[1][i]]
54 else:
55 return [h[0], int(h[1][i] * 10000)]
56 return clean_weights_
57
58 def parse_weights(portfolio_hash):
59 # FIXME: we'll need shorts at some point
60 assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"]))
61 weights_hash = portfolio_hash["weights"]
62 weights = {}
63 for i in range(len(weights_hash["_row"])):
64 weights[weights_hash["_row"][i]] = dict(filter(
65 filter_weights,
66 map(clean_weights(i), weights_hash.items())))
67 return weights
68
69 high_liquidity = parse_weights(cls.data["portfolio_1"])
70 medium_liquidity = parse_weights(cls.data["portfolio_2"])
71
72 cls.liquidities = {
73 "medium": medium_liquidity,
74 "high": high_liquidity,
75 }
76
77 class Amount:
78 MAX_DIGITS = 18
79
80 def __init__(self, currency, value, int_val=None, linked_to=None, ticker=None):
81 self.currency = currency
82 if int_val is None:
83 self._value = int(value * 10**self.MAX_DIGITS)
84 else:
85 self._value = int_val
86 self.linked_to = linked_to
87 self.ticker = ticker
88
89 self.ticker_cache = {}
90 self.ticker_cache_timestamp = time.time()
91
92 @property
93 def value(self):
94 return self._value / 10 ** self.MAX_DIGITS
95
96 def in_currency(self, other_currency, market, action="average"):
97 if other_currency == self.currency:
98 return self
99 asset_ticker = self.get_ticker(other_currency, market)
100 if asset_ticker is not None:
101 return Amount(
102 other_currency,
103 0,
104 int_val=int(self._value * asset_ticker[action]),
105 linked_to=self,
106 ticker=asset_ticker)
107 else:
108 raise Exception("This asset is not available in the chosen market")
109
110 def get_ticker(self, c2, market, refresh=False):
111 c1 = self.currency
112
113 def invert(ticker):
114 return {
115 "inverted": True,
116 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
117 "notInverted": ticker,
118 }
119 def augment_ticker(ticker):
120 ticker.update({
121 "inverted": False,
122 "average": (ticker["bid"] + ticker["ask"] ) / 2,
123 })
124
125 if time.time() - self.ticker_cache_timestamp > 5:
126 self.ticker_cache = {}
127 self.ticker_cache_timestamp = time.time()
128 elif not refresh:
129 if (c1, c2, market.__class__) in self.ticker_cache:
130 return self.ticker_cache[(c1, c2, market.__class__)]
131 if (c2, c1, market.__class__) in self.ticker_cache:
132 return invert(self.ticker_cache[(c2, c1, market.__class__)])
133
134 try:
135 self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
136 augment_ticker(self.ticker_cache[(c1, c2, market.__class__)])
137 except ccxt.ExchangeError:
138 try:
139 self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
140 augment_ticker(self.ticker_cache[(c2, c1, market.__class__)])
141 except ccxt.ExchangeError:
142 self.ticker_cache[(c1, c2, market.__class__)] = None
143 return self.get_ticker(c2, market)
144
145 def __abs__(self):
146 return Amount(self.currency, 0, int_val=abs(self._value))
147
148 def __add__(self, other):
149 if other.currency != self.currency and other._value * self._value != 0:
150 raise Exception("Summing amounts must be done with same currencies")
151 return Amount(self.currency, 0, int_val=self._value + other._value)
152
153 def __radd__(self, other):
154 if other == 0:
155 return self
156 else:
157 return self.__add__(other)
158
159 def __sub__(self, other):
160 if other.currency != self.currency and other._value * self._value != 0:
161 raise Exception("Summing amounts must be done with same currencies")
162 return Amount(self.currency, 0, int_val=self._value - other._value)
163
164 def __int__(self):
165 return self._value
166
167 def __mul__(self, value):
168 if type(value) != int and type(value) != float:
169 raise TypeError("Amount may only be multiplied by numbers")
170 return Amount(self.currency, 0, int_val=(self._value * value))
171
172 def __rmul__(self, value):
173 return self.__mul__(value)
174
175 def __floordiv__(self, value):
176 if type(value) != int:
177 raise TypeError("Amount may only be multiplied by integers")
178 return Amount(self.currency, 0, int_val=(self._value // value))
179
180 def __truediv__(self, value):
181 return self.__floordiv__(value)
182
183 def __lt__(self, other):
184 if self.currency != other.currency:
185 raise Exception("Comparing amounts must be done with same currencies")
186 return self._value < other._value
187
188 def __eq__(self, other):
189 if other == 0:
190 return self._value == 0
191 if self.currency != other.currency:
192 raise Exception("Comparing amounts must be done with same currencies")
193 return self._value == other._value
194
195 def __str__(self):
196 if self.linked_to is None:
197 return "{:.8f} {}".format(self.value, self.currency)
198 else:
199 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
200
201 def __repr__(self):
202 if self.linked_to is None:
203 return "Amount({:.8f} {})".format(self.value, self.currency)
204 else:
205 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
206
207 class Balance:
208 known_balances = {}
209 trades = {}
210
211 def __init__(self, currency, total_value, free_value, used_value):
212 self.currency = currency
213 self.total = Amount(currency, total_value)
214 self.free = Amount(currency, free_value)
215 self.used = Amount(currency, used_value)
216
217 @classmethod
218 def in_currency(cls, other_currency, market, action="average", type="total"):
219 amounts = {}
220 for currency in cls.known_balances:
221 balance = cls.known_balances[currency]
222 other_currency_amount = getattr(balance, type)\
223 .in_currency(other_currency, market, action=action)
224 amounts[currency] = other_currency_amount
225 return amounts
226
227 @classmethod
228 def currencies(cls):
229 return cls.known_balances.keys()
230
231 @classmethod
232 def from_hash(cls, currency, hash_):
233 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
234
235 @classmethod
236 def _fill_balances(cls, hash_):
237 for key in hash_:
238 if key in ["info", "free", "used", "total"]:
239 continue
240 if hash_[key]["total"] > 0:
241 cls.known_balances[key] = cls.from_hash(key, hash_[key])
242
243 @classmethod
244 def fetch_balances(cls, market):
245 cls._fill_balances(market.fetch_balance())
246 return cls.known_balances
247
248 @classmethod
249 def dispatch_assets(cls, amount):
250 repartition_pertenthousand = Portfolio.repartition_pertenthousand()
251 sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
252 amounts = {}
253 for currency, ptt in repartition_pertenthousand.items():
254 amounts[currency] = ptt * amount / sum_pertenthousand
255 if currency not in cls.known_balances:
256 cls.known_balances[currency] = cls(currency, 0, 0, 0)
257 return amounts
258
259 @classmethod
260 def prepare_trades(cls, market, base_currency="BTC"):
261 cls.fetch_balances(market)
262 values_in_base = cls.in_currency(base_currency, market)
263 total_base_value = sum(values_in_base.values())
264 new_repartition = cls.dispatch_assets(total_base_value)
265 Trade.compute_trades(values_in_base, new_repartition, market=market)
266
267 def __int__(self):
268 return int(self.total)
269
270 def __repr__(self):
271 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
272
273 class Trade:
274 trades = {}
275
276 def __init__(self, value_from, value_to, currency, market=None):
277 # We have value_from of currency, and want to finish with value_to of
278 # that currency. value_* may not be in currency's terms
279 self.currency = currency
280 self.value_from = value_from
281 self.value_to = value_to
282 self.orders = []
283 assert self.value_from.currency == self.value_to.currency
284 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
285 self.base_currency = self.value_from.currency
286
287 if market is not None:
288 self.prepare_order(market)
289
290 @classmethod
291 def compute_trades(cls, values_in_base, new_repartition, market=None):
292 base_currency = sum(values_in_base.values()).currency
293 for currency in Balance.currencies():
294 if currency == base_currency:
295 continue
296 cls.trades[currency] = cls(
297 values_in_base.get(currency, Amount(base_currency, 0)),
298 new_repartition.get(currency, Amount(base_currency, 0)),
299 currency,
300 market=market
301 )
302 return cls.trades
303
304 @property
305 def action(self):
306 if self.value_from == self.value_to:
307 return None
308 if self.base_currency == self.currency:
309 return None
310
311 if self.value_from < self.value_to:
312 return "buy"
313 else:
314 return "sell"
315
316 def ticker_action(self, inverted):
317 if self.value_from < self.value_to:
318 return "ask" if not inverted else "bid"
319 else:
320 return "bid" if not inverted else "ask"
321
322 def prepare_order(self, market):
323 if self.action is None:
324 return
325 ticker = self.value_from.ticker
326 inverted = ticker["inverted"]
327
328 if not inverted:
329 value_from = self.value_from.linked_to
330 value_to = self.value_to.in_currency(self.currency, market)
331 delta = abs(value_to - value_from)
332 currency = self.base_currency
333 else:
334 ticker = ticker["notInverted"]
335 delta = abs(self.value_to - self.value_from)
336 currency = self.currency
337
338 rate = ticker[self.ticker_action(inverted)]
339
340 self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency))
341
342 @classmethod
343 def all_orders(cls):
344 return sum(map(lambda v: v.orders, cls.trades.values()), [])
345
346 @classmethod
347 def follow_orders(cls, market):
348 orders = cls.all_orders()
349 finished_orders = []
350 while len(orders) != len(finished_orders):
351 time.sleep(30)
352 for order in orders:
353 if order in finished_orders:
354 continue
355 if order.get_status(market) != "open":
356 finished_orders.append(order)
357 print("finished {}".format(order))
358 print("All orders finished")
359
360 def __repr__(self):
361 return "Trade({} -> {} in {}, {})".format(
362 self.value_from,
363 self.value_to,
364 self.currency,
365 self.action)
366
367 class Order:
368 DEBUG = True
369
370 def __init__(self, action, amount, rate, base_currency):
371 self.action = action
372 self.amount = amount
373 self.rate = rate
374 self.base_currency = base_currency
375 self.result = None
376 self.status = "not run"
377
378 def __repr__(self):
379 return "Order({} {} at {} {} [{}])".format(
380 self.action,
381 self.amount,
382 self.rate,
383 self.base_currency,
384 self.status
385 )
386
387 def run(self, market):
388 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
389 amount = self.amount.value
390
391 if self.DEBUG:
392 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
393 symbol, self.action, amount, self.rate))
394 else:
395 try:
396 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
397 self.status = "open"
398 except Exception:
399 pass
400
401 def get_status(self, market):
402 # other states are "closed" and "canceled"
403 if self.status == "open":
404 result = market.fetch_order(self.result['id'])
405 self.status = result["status"]
406 return self.status
407
408 @static_var("cache", {})
409 def fetch_fees(market):
410 if market.__class__ not in fetch_fees.cache:
411 fetch_fees.cache[market.__class__] = market.fetch_fees()
412 return fetch_fees.cache[market.__class__]
413
414 def print_orders(market, base_currency="BTC"):
415 Balance.prepare_trades(market, base_currency=base_currency)
416 for currency, trade in Trade.trades.items():
417 print(trade)
418 for order in trade.orders:
419 print("\t", order, sep="")
420
421 def make_orders(market, base_currency="BTC"):
422 Balance.prepare_trades(market, base_currency=base_currency)
423 for currency, trade in Trade.trades.items():
424 print(trade)
425 for order in trade.orders:
426 print("\t", order, sep="")
427 order.run(market)
428
429 if __name__ == '__main__':
430 print_orders(market)