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