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