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