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