]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
review
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
dd359bc0
IB
1import ccxt
2import time
3# Put your poloniex api key in market.py
4from 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
10def static_var(varname, value):
11 def decorate(func):
12 setattr(func, varname, value)
13 return func
14 return decorate
15
16class 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
77class 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 {
e39768da 115 # COMMENT: c'est pas dangereux de faire l'invert comme ca ? il peut pas y avoir d'asymétrie ?
116 # et je pense que ca simplifierai le code de pas avoir le inverted. Il me semble qu'il est là
117 # juste pour une histoire de cache.
dd359bc0 118 "inverted": True,
e39768da 119 # COMMENT: j'ai pas compris pourquoi tu prenais l'average ?
dd359bc0
IB
120 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
121 "notInverted": ticker,
122 }
123 def augment_ticker(ticker):
124 ticker.update({
125 "inverted": False,
126 "average": (ticker["bid"] + ticker["ask"] ) / 2,
127 })
128
129 if time.time() - self.ticker_cache_timestamp > 5:
130 self.ticker_cache = {}
131 self.ticker_cache_timestamp = time.time()
132 elif not refresh:
133 if (c1, c2, market.__class__) in self.ticker_cache:
134 return self.ticker_cache[(c1, c2, market.__class__)]
135 if (c2, c1, market.__class__) in self.ticker_cache:
136 return invert(self.ticker_cache[(c2, c1, market.__class__)])
137
138 try:
139 self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
140 augment_ticker(self.ticker_cache[(c1, c2, market.__class__)])
141 except ccxt.ExchangeError:
142 try:
143 self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
144 augment_ticker(self.ticker_cache[(c2, c1, market.__class__)])
145 except ccxt.ExchangeError:
146 self.ticker_cache[(c1, c2, market.__class__)] = None
147 return self.get_ticker(c2, market)
148
149 def __abs__(self):
150 return Amount(self.currency, 0, int_val=abs(self._value))
151
152 def __add__(self, other):
153 if other.currency != self.currency and other._value * self._value != 0:
154 raise Exception("Summing amounts must be done with same currencies")
155 return Amount(self.currency, 0, int_val=self._value + other._value)
156
157 def __radd__(self, other):
158 if other == 0:
159 return self
160 else:
161 return self.__add__(other)
162
163 def __sub__(self, other):
164 if other.currency != self.currency and other._value * self._value != 0:
165 raise Exception("Summing amounts must be done with same currencies")
166 return Amount(self.currency, 0, int_val=self._value - other._value)
167
168 def __int__(self):
169 return self._value
170
171 def __mul__(self, value):
172 if type(value) != int and type(value) != float:
173 raise TypeError("Amount may only be multiplied by numbers")
174 return Amount(self.currency, 0, int_val=(self._value * value))
175
176 def __rmul__(self, value):
177 return self.__mul__(value)
178
179 def __floordiv__(self, value):
180 if type(value) != int:
181 raise TypeError("Amount may only be multiplied by integers")
182 return Amount(self.currency, 0, int_val=(self._value // value))
183
184 def __truediv__(self, value):
185 return self.__floordiv__(value)
186
187 def __lt__(self, other):
188 if self.currency != other.currency:
189 raise Exception("Comparing amounts must be done with same currencies")
190 return self._value < other._value
191
192 def __eq__(self, other):
193 if other == 0:
194 return self._value == 0
195 if self.currency != other.currency:
196 raise Exception("Comparing amounts must be done with same currencies")
197 return self._value == other._value
198
199 def __str__(self):
200 if self.linked_to is None:
201 return "{:.8f} {}".format(self.value, self.currency)
202 else:
203 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
204
205 def __repr__(self):
206 if self.linked_to is None:
207 return "Amount({:.8f} {})".format(self.value, self.currency)
208 else:
209 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
210
211class Balance:
212 known_balances = {}
213 trades = {}
214
215 def __init__(self, currency, total_value, free_value, used_value):
216 self.currency = currency
217 self.total = Amount(currency, total_value)
218 self.free = Amount(currency, free_value)
219 self.used = Amount(currency, used_value)
220
221 @classmethod
222 def in_currency(cls, other_currency, market, action="average", type="total"):
223 amounts = {}
224 for currency in cls.known_balances:
225 balance = cls.known_balances[currency]
226 other_currency_amount = getattr(balance, type)\
227 .in_currency(other_currency, market, action=action)
228 amounts[currency] = other_currency_amount
229 return amounts
230
231 @classmethod
232 def currencies(cls):
233 return cls.known_balances.keys()
234
235 @classmethod
236 def from_hash(cls, currency, hash_):
237 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
238
239 @classmethod
240 def _fill_balances(cls, hash_):
241 for key in hash_:
242 if key in ["info", "free", "used", "total"]:
243 continue
244 if hash_[key]["total"] > 0:
245 cls.known_balances[key] = cls.from_hash(key, hash_[key])
246
247 @classmethod
248 def fetch_balances(cls, market):
249 cls._fill_balances(market.fetch_balance())
250 return cls.known_balances
251
252 @classmethod
253 def dispatch_assets(cls, amount):
254 repartition_pertenthousand = Portfolio.repartition_pertenthousand()
255 sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
256 amounts = {}
257 for currency, ptt in repartition_pertenthousand.items():
258 amounts[currency] = ptt * amount / sum_pertenthousand
259 if currency not in cls.known_balances:
260 cls.known_balances[currency] = cls(currency, 0, 0, 0)
261 return amounts
262
263 @classmethod
264 def prepare_trades(cls, market, base_currency="BTC"):
265 cls.fetch_balances(market)
266 values_in_base = cls.in_currency(base_currency, market)
267 total_base_value = sum(values_in_base.values())
268 new_repartition = cls.dispatch_assets(total_base_value)
269 Trade.compute_trades(values_in_base, new_repartition, market=market)
270
271 def __int__(self):
272 return int(self.total)
273
274 def __repr__(self):
275 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
276
277class Trade:
278 trades = {}
279
280 def __init__(self, value_from, value_to, currency, market=None):
281 # We have value_from of currency, and want to finish with value_to of
282 # that currency. value_* may not be in currency's terms
283 self.currency = currency
284 self.value_from = value_from
285 self.value_to = value_to
286 self.orders = []
287 assert self.value_from.currency == self.value_to.currency
288 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
289 self.base_currency = self.value_from.currency
290
291 if market is not None:
292 self.prepare_order(market)
293
294 @classmethod
295 def compute_trades(cls, values_in_base, new_repartition, market=None):
296 base_currency = sum(values_in_base.values()).currency
297 for currency in Balance.currencies():
298 if currency == base_currency:
299 continue
300 cls.trades[currency] = cls(
301 values_in_base.get(currency, Amount(base_currency, 0)),
302 new_repartition.get(currency, Amount(base_currency, 0)),
303 currency,
304 market=market
305 )
306 return cls.trades
307
308 @property
309 def action(self):
310 if self.value_from == self.value_to:
311 return None
312 if self.base_currency == self.currency:
313 return None
314
315 if self.value_from < self.value_to:
316 return "buy"
317 else:
318 return "sell"
319
320 def ticker_action(self, inverted):
321 if self.value_from < self.value_to:
322 return "ask" if not inverted else "bid"
323 else:
324 return "bid" if not inverted else "ask"
325
326 def prepare_order(self, market):
327 if self.action is None:
328 return
329 ticker = self.value_from.ticker
330 inverted = ticker["inverted"]
331
332 if not inverted:
333 value_from = self.value_from.linked_to
334 value_to = self.value_to.in_currency(self.currency, market)
335 delta = abs(value_to - value_from)
336 currency = self.base_currency
337 else:
338 ticker = ticker["notInverted"]
339 delta = abs(self.value_to - self.value_from)
340 currency = self.currency
341
342 rate = ticker[self.ticker_action(inverted)]
343
344 self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency))
345
346 @classmethod
347 def all_orders(cls):
348 return sum(map(lambda v: v.orders, cls.trades.values()), [])
349
350 @classmethod
351 def follow_orders(cls, market):
352 orders = cls.all_orders()
353 finished_orders = []
354 while len(orders) != len(finished_orders):
355 time.sleep(30)
356 for order in orders:
357 if order in finished_orders:
358 continue
359 if order.get_status(market) != "open":
360 finished_orders.append(order)
361 print("finished {}".format(order))
362 print("All orders finished")
363
364 def __repr__(self):
365 return "Trade({} -> {} in {}, {})".format(
366 self.value_from,
367 self.value_to,
368 self.currency,
369 self.action)
370
371class Order:
372 DEBUG = True
373
374 def __init__(self, action, amount, rate, base_currency):
375 self.action = action
376 self.amount = amount
377 self.rate = rate
378 self.base_currency = base_currency
379 self.result = None
380 self.status = "not run"
381
382 def __repr__(self):
383 return "Order({} {} at {} {} [{}])".format(
384 self.action,
385 self.amount,
386 self.rate,
387 self.base_currency,
388 self.status
389 )
390
391 def run(self, market):
392 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
393 amount = self.amount.value
394
395 if self.DEBUG:
396 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
397 symbol, self.action, amount, self.rate))
398 else:
399 try:
400 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
401 self.status = "open"
402 except Exception:
403 pass
404
405 def get_status(self, market):
406 # other states are "closed" and "canceled"
407 if self.status == "open":
408 result = market.fetch_order(self.result['id'])
409 self.status = result["status"]
410 return self.status
411
412@static_var("cache", {})
413def fetch_fees(market):
414 if market.__class__ not in fetch_fees.cache:
415 fetch_fees.cache[market.__class__] = market.fetch_fees()
416 return fetch_fees.cache[market.__class__]
417
418def print_orders(market, base_currency="BTC"):
419 Balance.prepare_trades(market, base_currency=base_currency)
420 for currency, trade in Trade.trades.items():
421 print(trade)
422 for order in trade.orders:
423 print("\t", order, sep="")
424
425def make_orders(market, base_currency="BTC"):
426 Balance.prepare_trades(market, base_currency=base_currency)
427 for currency, trade in Trade.trades.items():
428 print(trade)
429 for order in trade.orders:
430 print("\t", order, sep="")
431 order.run(market)
432
433if __name__ == '__main__':
434 print_orders(market)