]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Don't autoprepare the orders when creating trade
[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 = []
089d5d9d 250 self.market = market
dd359bc0
IB
251 assert self.value_from.currency == self.value_to.currency
252 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
253 self.base_currency = self.value_from.currency
254
cfab619d
IB
255 fees_cache = {}
256 @classmethod
257 def fetch_fees(cls, market):
258 if market.__class__ not in cls.fees_cache:
259 cls.fees_cache[market.__class__] = market.fetch_fees()
260 return cls.fees_cache[market.__class__]
261
262 ticker_cache = {}
263 ticker_cache_timestamp = time.time()
264 @classmethod
265 def get_ticker(cls, c1, c2, market, refresh=False):
266 def invert(ticker):
267 return {
268 "inverted": True,
269 "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
270 "original": ticker,
271 }
272 def augment_ticker(ticker):
273 ticker.update({
274 "inverted": False,
275 "average": (ticker["bid"] + ticker["ask"] ) / 2,
276 })
277
278 if time.time() - cls.ticker_cache_timestamp > 5:
279 cls.ticker_cache = {}
280 cls.ticker_cache_timestamp = time.time()
281 elif not refresh:
282 if (c1, c2, market.__class__) in cls.ticker_cache:
283 return cls.ticker_cache[(c1, c2, market.__class__)]
284 if (c2, c1, market.__class__) in cls.ticker_cache:
285 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
286
287 try:
288 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
289 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
290 except ccxt.ExchangeError:
291 try:
292 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
293 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
294 except ccxt.ExchangeError:
295 cls.ticker_cache[(c1, c2, market.__class__)] = None
296 return cls.get_ticker(c1, c2, market)
297
dd359bc0
IB
298 @classmethod
299 def compute_trades(cls, values_in_base, new_repartition, market=None):
300 base_currency = sum(values_in_base.values()).currency
301 for currency in Balance.currencies():
302 if currency == base_currency:
303 continue
304 cls.trades[currency] = cls(
305 values_in_base.get(currency, Amount(base_currency, 0)),
306 new_repartition.get(currency, Amount(base_currency, 0)),
307 currency,
308 market=market
309 )
089d5d9d 310 cls.trades[currency].prepare_order()
dd359bc0
IB
311 return cls.trades
312
313 @property
314 def action(self):
315 if self.value_from == self.value_to:
316 return None
317 if self.base_currency == self.currency:
318 return None
319
320 if self.value_from < self.value_to:
321 return "buy"
322 else:
323 return "sell"
324
cfab619d 325 def order_action(self, inverted):
dd359bc0
IB
326 if self.value_from < self.value_to:
327 return "ask" if not inverted else "bid"
328 else:
329 return "bid" if not inverted else "ask"
330
089d5d9d 331 def prepare_order(self):
dd359bc0
IB
332 if self.action is None:
333 return
334 ticker = self.value_from.ticker
335 inverted = ticker["inverted"]
336
337 if not inverted:
338 value_from = self.value_from.linked_to
089d5d9d 339 value_to = self.value_to.in_currency(self.currency, self.market)
dd359bc0
IB
340 delta = abs(value_to - value_from)
341 currency = self.base_currency
342 else:
cfab619d 343 ticker = ticker["original"]
dd359bc0
IB
344 delta = abs(self.value_to - self.value_from)
345 currency = self.currency
346
cfab619d 347 rate = ticker[self.order_action(inverted)]
dd359bc0 348
cfab619d 349 self.orders.append(Order(self.order_action(inverted), delta, rate, currency))
dd359bc0
IB
350
351 @classmethod
352 def all_orders(cls):
353 return sum(map(lambda v: v.orders, cls.trades.values()), [])
354
355 @classmethod
356 def follow_orders(cls, market):
357 orders = cls.all_orders()
358 finished_orders = []
359 while len(orders) != len(finished_orders):
360 time.sleep(30)
361 for order in orders:
362 if order in finished_orders:
363 continue
364 if order.get_status(market) != "open":
365 finished_orders.append(order)
366 print("finished {}".format(order))
367 print("All orders finished")
368
369 def __repr__(self):
370 return "Trade({} -> {} in {}, {})".format(
371 self.value_from,
372 self.value_to,
373 self.currency,
374 self.action)
375
376class Order:
377 DEBUG = True
378
379 def __init__(self, action, amount, rate, base_currency):
380 self.action = action
381 self.amount = amount
382 self.rate = rate
383 self.base_currency = base_currency
384 self.result = None
385 self.status = "not run"
386
387 def __repr__(self):
388 return "Order({} {} at {} {} [{}])".format(
389 self.action,
390 self.amount,
391 self.rate,
392 self.base_currency,
393 self.status
394 )
395
396 def run(self, market):
397 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
398 amount = self.amount.value
399
400 if self.DEBUG:
401 print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
402 symbol, self.action, amount, self.rate))
403 else:
404 try:
405 self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
406 self.status = "open"
407 except Exception:
408 pass
409
410 def get_status(self, market):
411 # other states are "closed" and "canceled"
412 if self.status == "open":
413 result = market.fetch_order(self.result['id'])
414 self.status = result["status"]
415 return self.status
416
dd359bc0
IB
417def 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
424def 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
432if __name__ == '__main__':
433 print_orders(market)