]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Add compute value lambdas for currency conversion
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
dd359bc0
IB
1import ccxt
2import time
5ab23e1c 3from decimal import Decimal as D
dd359bc0
IB
4# Put your poloniex api key in market.py
5from 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
dd359bc0
IB
11class 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
183a53e3
IB
30 try:
31 r = http.request("GET", cls.URL)
32 except Exception:
33 return
34 try:
5ab23e1c
IB
35 cls.data = json.loads(r.data,
36 parse_int=D,
37 parse_float=D)
183a53e3
IB
38 except json.JSONDecodeError:
39 cls.data = None
dd359bc0
IB
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
80class Amount:
81 MAX_DIGITS = 18
82
5ab23e1c 83 def __init__(self, currency, value, linked_to=None, ticker=None):
dd359bc0 84 self.currency = currency
5ab23e1c 85 self.value = D(value)
dd359bc0
IB
86 self.linked_to = linked_to
87 self.ticker = ticker
88
89 self.ticker_cache = {}
90 self.ticker_cache_timestamp = time.time()
91
deb8924c 92 def in_currency(self, other_currency, market, action=None, compute_value="average"):
dd359bc0
IB
93 if other_currency == self.currency:
94 return self
cfab619d 95 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
dd359bc0
IB
96 if asset_ticker is not None:
97 return Amount(
98 other_currency,
deb8924c 99 self.value * Trade.compute_value(asset_ticker, action, compute_value=compute_value),
dd359bc0
IB
100 linked_to=self,
101 ticker=asset_ticker)
102 else:
103 raise Exception("This asset is not available in the chosen market")
104
dd359bc0 105 def __abs__(self):
5ab23e1c 106 return Amount(self.currency, abs(self.value))
dd359bc0
IB
107
108 def __add__(self, other):
5ab23e1c 109 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 110 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 111 return Amount(self.currency, self.value + other.value)
dd359bc0
IB
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):
5ab23e1c 120 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 121 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 122 return Amount(self.currency, self.value - other.value)
dd359bc0
IB
123
124 def __mul__(self, value):
5ab23e1c 125 if type(value) != int and type(value) != float and type(value) != D:
dd359bc0 126 raise TypeError("Amount may only be multiplied by numbers")
5ab23e1c 127 return Amount(self.currency, self.value * value)
dd359bc0
IB
128
129 def __rmul__(self, value):
130 return self.__mul__(value)
131
132 def __floordiv__(self, value):
5ab23e1c 133 if type(value) != int and type(value) != float and type(value) != D:
dd359bc0 134 raise TypeError("Amount may only be multiplied by integers")
5ab23e1c 135 return Amount(self.currency, self.value / value)
dd359bc0
IB
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")
5ab23e1c 143 return self.value < other.value
dd359bc0
IB
144
145 def __eq__(self, other):
146 if other == 0:
5ab23e1c 147 return self.value == 0
dd359bc0
IB
148 if self.currency != other.currency:
149 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 150 return self.value == other.value
dd359bc0
IB
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
164class Balance:
165 known_balances = {}
dd359bc0
IB
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
f2da6589
IB
173 @classmethod
174 def from_hash(cls, currency, hash_):
175 return cls(currency, hash_["total"], hash_["free"], hash_["used"])
176
dd359bc0 177 @classmethod
deb8924c 178 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
dd359bc0
IB
179 amounts = {}
180 for currency in cls.known_balances:
181 balance = cls.known_balances[currency]
182 other_currency_amount = getattr(balance, type)\
deb8924c 183 .in_currency(other_currency, market, compute_value=compute_value)
dd359bc0
IB
184 amounts[currency] = other_currency_amount
185 return amounts
186
187 @classmethod
188 def currencies(cls):
189 return cls.known_balances.keys()
190
dd359bc0
IB
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
deb8924c 216 def prepare_trades(cls, market, base_currency="BTC", compute_value=None):
dd359bc0
IB
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)
deb8924c
IB
221 # Recompute it in case we have new currencies
222 values_in_base = cls.in_currency(base_currency, market)
223 Trade.compute_trades(values_in_base, new_repartition, market=market, compute_value=compute_value)
dd359bc0 224
dd359bc0
IB
225 def __repr__(self):
226 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
227
deb8924c
IB
228class Computation:
229 def average_inverse(ticker, action):
230 if ticker["inverted"]:
231 return 1/ticker["original"]["average"]
232 else:
233 return ticker["average"]
234
235 computations = {
236 "default": lambda x, y: x[y],
237 "average_inverse": average_inverse,
238 "average": lambda x, y: x["average"],
239 "bid": lambda x, y: x["bid"],
240 "ask": lambda x, y: x["ask"],
241 }
242
243
dd359bc0
IB
244class Trade:
245 trades = {}
246
247 def __init__(self, value_from, value_to, currency, market=None):
248 # We have value_from of currency, and want to finish with value_to of
249 # that currency. value_* may not be in currency's terms
250 self.currency = currency
251 self.value_from = value_from
252 self.value_to = value_to
253 self.orders = []
089d5d9d 254 self.market = market
dd359bc0
IB
255 assert self.value_from.currency == self.value_to.currency
256 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
257 self.base_currency = self.value_from.currency
258
cfab619d
IB
259 fees_cache = {}
260 @classmethod
261 def fetch_fees(cls, market):
262 if market.__class__ not in cls.fees_cache:
263 cls.fees_cache[market.__class__] = market.fetch_fees()
264 return cls.fees_cache[market.__class__]
265
266 ticker_cache = {}
267 ticker_cache_timestamp = time.time()
268 @classmethod
269 def get_ticker(cls, c1, c2, market, refresh=False):
270 def invert(ticker):
271 return {
272 "inverted": True,
5ab23e1c 273 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
cfab619d
IB
274 "original": ticker,
275 }
276 def augment_ticker(ticker):
277 ticker.update({
278 "inverted": False,
279 "average": (ticker["bid"] + ticker["ask"] ) / 2,
280 })
281
282 if time.time() - cls.ticker_cache_timestamp > 5:
283 cls.ticker_cache = {}
284 cls.ticker_cache_timestamp = time.time()
285 elif not refresh:
286 if (c1, c2, market.__class__) in cls.ticker_cache:
287 return cls.ticker_cache[(c1, c2, market.__class__)]
288 if (c2, c1, market.__class__) in cls.ticker_cache:
289 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
290
291 try:
292 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
293 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
294 except ccxt.ExchangeError:
295 try:
296 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
297 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
298 except ccxt.ExchangeError:
299 cls.ticker_cache[(c1, c2, market.__class__)] = None
300 return cls.get_ticker(c1, c2, market)
301
dd359bc0 302 @classmethod
deb8924c 303 def compute_trades(cls, values_in_base, new_repartition, market=None, compute_value=None):
dd359bc0
IB
304 base_currency = sum(values_in_base.values()).currency
305 for currency in Balance.currencies():
306 if currency == base_currency:
307 continue
308 cls.trades[currency] = cls(
309 values_in_base.get(currency, Amount(base_currency, 0)),
310 new_repartition.get(currency, Amount(base_currency, 0)),
311 currency,
312 market=market
313 )
deb8924c
IB
314 if compute_value is not None:
315 cls.trades[currency].prepare_order(compute_value=compute_value)
dd359bc0
IB
316 return cls.trades
317
318 @property
319 def action(self):
320 if self.value_from == self.value_to:
321 return None
322 if self.base_currency == self.currency:
323 return None
324
325 if self.value_from < self.value_to:
326 return "buy"
327 else:
328 return "sell"
329
cfab619d 330 def order_action(self, inverted):
dd359bc0
IB
331 if self.value_from < self.value_to:
332 return "ask" if not inverted else "bid"
333 else:
334 return "bid" if not inverted else "ask"
335
deb8924c 336 def prepare_order(self, compute_value="default"):
dd359bc0
IB
337 if self.action is None:
338 return
339 ticker = self.value_from.ticker
340 inverted = ticker["inverted"]
341
342 if not inverted:
343 value_from = self.value_from.linked_to
deb8924c
IB
344 # The ticker will most probably be inverted, but we want the average
345 # of the initial value
346 value_to = self.value_to.in_currency(self.currency, self.market, compute_value="average_inverse")
dd359bc0
IB
347 delta = abs(value_to - value_from)
348 currency = self.base_currency
349 else:
cfab619d 350 ticker = ticker["original"]
dd359bc0
IB
351 delta = abs(self.value_to - self.value_from)
352 currency = self.currency
353
deb8924c 354 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
dd359bc0 355
cfab619d 356 self.orders.append(Order(self.order_action(inverted), delta, rate, currency))
dd359bc0 357
deb8924c
IB
358 @classmethod
359 def compute_value(cls, ticker, action, compute_value="default"):
360 if type(compute_value) == str:
361 compute_value = Computation.computations[compute_value]
362 return compute_value(ticker, action)
363
dd359bc0
IB
364 @classmethod
365 def all_orders(cls):
366 return sum(map(lambda v: v.orders, cls.trades.values()), [])
367
368 @classmethod
369 def follow_orders(cls, market):
370 orders = cls.all_orders()
371 finished_orders = []
372 while len(orders) != len(finished_orders):
373 time.sleep(30)
374 for order in orders:
375 if order in finished_orders:
376 continue
377 if order.get_status(market) != "open":
378 finished_orders.append(order)
379 print("finished {}".format(order))
380 print("All orders finished")
381
382 def __repr__(self):
383 return "Trade({} -> {} in {}, {})".format(
384 self.value_from,
385 self.value_to,
386 self.currency,
387 self.action)
388
389class Order:
390 DEBUG = True
391
392 def __init__(self, action, amount, rate, base_currency):
393 self.action = action
394 self.amount = amount
395 self.rate = rate
396 self.base_currency = base_currency
397 self.result = None
398 self.status = "not run"
399
400 def __repr__(self):
401 return "Order({} {} at {} {} [{}])".format(
402 self.action,
403 self.amount,
404 self.rate,
405 self.base_currency,
406 self.status
407 )
408
409 def run(self, market):
410 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
411 amount = self.amount.value
412
413 if self.DEBUG:
414 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
415 symbol, self.action, amount, self.rate))
416 else:
417 try:
418 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
419 self.status = "open"
420 except Exception:
421 pass
422
423 def get_status(self, market):
424 # other states are "closed" and "canceled"
425 if self.status == "open":
426 result = market.fetch_order(self.result['id'])
427 self.status = result["status"]
428 return self.status
429
dd359bc0 430def print_orders(market, base_currency="BTC"):
deb8924c 431 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
5ab23e1c
IB
432 for currency, balance in Balance.known_balances.items():
433 print(balance)
dd359bc0
IB
434 for currency, trade in Trade.trades.items():
435 print(trade)
436 for order in trade.orders:
437 print("\t", order, sep="")
438
439def make_orders(market, base_currency="BTC"):
440 Balance.prepare_trades(market, base_currency=base_currency)
441 for currency, trade in Trade.trades.items():
442 print(trade)
443 for order in trade.orders:
444 print("\t", order, sep="")
445 order.run(market)
446
447if __name__ == '__main__':
448 print_orders(market)