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