]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Write some tests for Portfolio class
[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
183a53e3
IB
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
dd359bc0
IB
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
83class 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
213class 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 in_currency(cls, other_currency, market, action="average", type="total"):
225 amounts = {}
226 for currency in cls.known_balances:
227 balance = cls.known_balances[currency]
228 other_currency_amount = getattr(balance, type)\
229 .in_currency(other_currency, market, action=action)
230 amounts[currency] = other_currency_amount
231 return amounts
232
233 @classmethod
234 def currencies(cls):
235 return cls.known_balances.keys()
236
237 @classmethod
238 def from_hash(cls, currency, hash_):
239 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
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 __int__(self):
274 return int(self.total)
275
276 def __repr__(self):
277 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
278
279class Trade:
280 trades = {}
281
282 def __init__(self, value_from, value_to, currency, market=None):
283 # We have value_from of currency, and want to finish with value_to of
284 # that currency. value_* may not be in currency's terms
285 self.currency = currency
286 self.value_from = value_from
287 self.value_to = value_to
288 self.orders = []
289 assert self.value_from.currency == self.value_to.currency
290 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
291 self.base_currency = self.value_from.currency
292
293 if market is not None:
294 self.prepare_order(market)
295
296 @classmethod
297 def compute_trades(cls, values_in_base, new_repartition, market=None):
298 base_currency = sum(values_in_base.values()).currency
299 for currency in Balance.currencies():
300 if currency == base_currency:
301 continue
302 cls.trades[currency] = cls(
303 values_in_base.get(currency, Amount(base_currency, 0)),
304 new_repartition.get(currency, Amount(base_currency, 0)),
305 currency,
306 market=market
307 )
308 return cls.trades
309
310 @property
311 def action(self):
312 if self.value_from == self.value_to:
313 return None
314 if self.base_currency == self.currency:
315 return None
316
317 if self.value_from < self.value_to:
318 return "buy"
319 else:
320 return "sell"
321
322 def ticker_action(self, inverted):
323 if self.value_from < self.value_to:
324 return "ask" if not inverted else "bid"
325 else:
326 return "bid" if not inverted else "ask"
327
328 def prepare_order(self, market):
329 if self.action is None:
330 return
331 ticker = self.value_from.ticker
332 inverted = ticker["inverted"]
333
334 if not inverted:
335 value_from = self.value_from.linked_to
336 value_to = self.value_to.in_currency(self.currency, market)
337 delta = abs(value_to - value_from)
338 currency = self.base_currency
339 else:
340 ticker = ticker["notInverted"]
341 delta = abs(self.value_to - self.value_from)
342 currency = self.currency
343
344 rate = ticker[self.ticker_action(inverted)]
345
346 self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency))
347
348 @classmethod
349 def all_orders(cls):
350 return sum(map(lambda v: v.orders, cls.trades.values()), [])
351
352 @classmethod
353 def follow_orders(cls, market):
354 orders = cls.all_orders()
355 finished_orders = []
356 while len(orders) != len(finished_orders):
357 time.sleep(30)
358 for order in orders:
359 if order in finished_orders:
360 continue
361 if order.get_status(market) != "open":
362 finished_orders.append(order)
363 print("finished {}".format(order))
364 print("All orders finished")
365
366 def __repr__(self):
367 return "Trade({} -> {} in {}, {})".format(
368 self.value_from,
369 self.value_to,
370 self.currency,
371 self.action)
372
373class Order:
374 DEBUG = True
375
376 def __init__(self, action, amount, rate, base_currency):
377 self.action = action
378 self.amount = amount
379 self.rate = rate
380 self.base_currency = base_currency
381 self.result = None
382 self.status = "not run"
383
384 def __repr__(self):
385 return "Order({} {} at {} {} [{}])".format(
386 self.action,
387 self.amount,
388 self.rate,
389 self.base_currency,
390 self.status
391 )
392
393 def run(self, market):
394 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
395 amount = self.amount.value
396
397 if self.DEBUG:
398 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
399 symbol, self.action, amount, self.rate))
400 else:
401 try:
402 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
403 self.status = "open"
404 except Exception:
405 pass
406
407 def get_status(self, market):
408 # other states are "closed" and "canceled"
409 if self.status == "open":
410 result = market.fetch_order(self.result['id'])
411 self.status = result["status"]
412 return self.status
413
414@static_var("cache", {})
415def fetch_fees(market):
416 if market.__class__ not in fetch_fees.cache:
417 fetch_fees.cache[market.__class__] = market.fetch_fees()
418 return fetch_fees.cache[market.__class__]
419
420def print_orders(market, base_currency="BTC"):
421 Balance.prepare_trades(market, base_currency=base_currency)
422 for currency, trade in Trade.trades.items():
423 print(trade)
424 for order in trade.orders:
425 print("\t", order, sep="")
426
427def make_orders(market, base_currency="BTC"):
428 Balance.prepare_trades(market, base_currency=base_currency)
429 for currency, trade in Trade.trades.items():
430 print(trade)
431 for order in trade.orders:
432 print("\t", order, sep="")
433 order.run(market)
434
435if __name__ == '__main__':
436 print_orders(market)