]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Fix Amount.__sub__ not working with 0
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
e0b14bcc 1from ccxt import ExchangeError
dd359bc0 2import time
350ed24d 3from decimal import Decimal as D, ROUND_DOWN
dd359bc0
IB
4# Put your poloniex api key in market.py
5from market import market
80cdd672
IB
6from json import JSONDecodeError
7import requests
8
9# FIXME: correctly handle web call timeouts
10
dd359bc0 11
dd359bc0
IB
12class Portfolio:
13 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
14 liquidities = {}
15 data = None
16
17 @classmethod
350ed24d 18 def repartition(cls, liquidity="medium"):
dd359bc0
IB
19 cls.parse_cryptoportfolio()
20 liquidities = cls.liquidities[liquidity]
c11e4274
IB
21 cls.last_date = sorted(liquidities.keys())[-1]
22 return liquidities[cls.last_date]
dd359bc0
IB
23
24 @classmethod
25 def get_cryptoportfolio(cls):
183a53e3 26 try:
80cdd672 27 r = requests.get(cls.URL)
183a53e3 28 except Exception:
80cdd672 29 return
183a53e3 30 try:
80cdd672
IB
31 cls.data = r.json(parse_int=D, parse_float=D)
32 except JSONDecodeError:
183a53e3 33 cls.data = None
dd359bc0
IB
34
35 @classmethod
36 def parse_cryptoportfolio(cls):
37 if cls.data is None:
38 cls.get_cryptoportfolio()
39
40 def filter_weights(weight_hash):
350ed24d 41 if weight_hash[1][0] == 0:
dd359bc0
IB
42 return False
43 if weight_hash[0] == "_row":
44 return False
45 return True
46
47 def clean_weights(i):
48 def clean_weights_(h):
350ed24d
IB
49 if h[0].endswith("s"):
50 return [h[0][0:-1], (h[1][i], "short")]
dd359bc0 51 else:
350ed24d 52 return [h[0], (h[1][i], "long")]
dd359bc0
IB
53 return clean_weights_
54
55 def parse_weights(portfolio_hash):
dd359bc0
IB
56 weights_hash = portfolio_hash["weights"]
57 weights = {}
58 for i in range(len(weights_hash["_row"])):
59 weights[weights_hash["_row"][i]] = dict(filter(
60 filter_weights,
61 map(clean_weights(i), weights_hash.items())))
62 return weights
63
64 high_liquidity = parse_weights(cls.data["portfolio_1"])
65 medium_liquidity = parse_weights(cls.data["portfolio_2"])
66
67 cls.liquidities = {
68 "medium": medium_liquidity,
69 "high": high_liquidity,
70 }
71
72class Amount:
c2644ba8 73 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
dd359bc0 74 self.currency = currency
5ab23e1c 75 self.value = D(value)
dd359bc0
IB
76 self.linked_to = linked_to
77 self.ticker = ticker
c2644ba8 78 self.rate = rate
dd359bc0 79
c2644ba8 80 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
dd359bc0
IB
81 if other_currency == self.currency:
82 return self
c2644ba8
IB
83 if rate is not None:
84 return Amount(
85 other_currency,
86 self.value * rate,
87 linked_to=self,
88 rate=rate)
cfab619d 89 asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
dd359bc0 90 if asset_ticker is not None:
c2644ba8 91 rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
dd359bc0
IB
92 return Amount(
93 other_currency,
c2644ba8 94 self.value * rate,
dd359bc0 95 linked_to=self,
c2644ba8
IB
96 ticker=asset_ticker,
97 rate=rate)
dd359bc0
IB
98 else:
99 raise Exception("This asset is not available in the chosen market")
100
350ed24d
IB
101 def __round__(self, n=8):
102 return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN))
103
dd359bc0 104 def __abs__(self):
5ab23e1c 105 return Amount(self.currency, abs(self.value))
dd359bc0
IB
106
107 def __add__(self, other):
5ab23e1c 108 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 109 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 110 return Amount(self.currency, self.value + other.value)
dd359bc0
IB
111
112 def __radd__(self, other):
113 if other == 0:
114 return self
115 else:
116 return self.__add__(other)
117
118 def __sub__(self, other):
c51687d2
IB
119 if other == 0:
120 return self
5ab23e1c 121 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 122 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 123 return Amount(self.currency, self.value - other.value)
dd359bc0
IB
124
125 def __mul__(self, value):
77f8a378 126 if not isinstance(value, (int, float, D)):
dd359bc0 127 raise TypeError("Amount may only be multiplied by numbers")
5ab23e1c 128 return Amount(self.currency, self.value * value)
dd359bc0
IB
129
130 def __rmul__(self, value):
131 return self.__mul__(value)
132
133 def __floordiv__(self, value):
77f8a378 134 if not isinstance(value, (int, float, D)):
dd359bc0 135 raise TypeError("Amount may only be multiplied by integers")
5ab23e1c 136 return Amount(self.currency, self.value / value)
dd359bc0
IB
137
138 def __truediv__(self, value):
139 return self.__floordiv__(value)
140
141 def __lt__(self, other):
006a2084
IB
142 if other == 0:
143 return self.value < 0
dd359bc0
IB
144 if self.currency != other.currency:
145 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 146 return self.value < other.value
dd359bc0 147
80cdd672
IB
148 def __le__(self, other):
149 return self == other or self < other
150
006a2084
IB
151 def __gt__(self, other):
152 return not self <= other
153
154 def __ge__(self, other):
155 return not self < other
156
dd359bc0
IB
157 def __eq__(self, other):
158 if other == 0:
5ab23e1c 159 return self.value == 0
dd359bc0
IB
160 if self.currency != other.currency:
161 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 162 return self.value == other.value
dd359bc0 163
006a2084
IB
164 def __ne__(self, other):
165 return not self == other
166
167 def __neg__(self):
168 return Amount(self.currency, - self.value)
169
dd359bc0
IB
170 def __str__(self):
171 if self.linked_to is None:
172 return "{:.8f} {}".format(self.value, self.currency)
173 else:
174 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
175
176 def __repr__(self):
177 if self.linked_to is None:
178 return "Amount({:.8f} {})".format(self.value, self.currency)
179 else:
180 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
181
182class Balance:
183 known_balances = {}
dd359bc0 184
006a2084 185 def __init__(self, currency, hash_):
dd359bc0 186 self.currency = currency
006a2084
IB
187 for key in ["total",
188 "exchange_total", "exchange_used", "exchange_free",
189 "margin_total", "margin_borrowed", "margin_free"]:
190 setattr(self, key, Amount(currency, hash_.get(key, 0)))
191
80cdd672 192 self.margin_position_type = hash_.get("margin_position_type")
006a2084 193
80cdd672 194 if hash_.get("margin_borrowed_base_currency") is not None:
006a2084
IB
195 base_currency = hash_["margin_borrowed_base_currency"]
196 for key in [
197 "margin_liquidation_price",
198 "margin_pending_gain",
199 "margin_lending_fees",
200 "margin_borrowed_base_price"
201 ]:
c51687d2 202 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
f2da6589 203
dd359bc0 204 @classmethod
deb8924c 205 def in_currency(cls, other_currency, market, compute_value="average", type="total"):
dd359bc0
IB
206 amounts = {}
207 for currency in cls.known_balances:
208 balance = cls.known_balances[currency]
209 other_currency_amount = getattr(balance, type)\
deb8924c 210 .in_currency(other_currency, market, compute_value=compute_value)
dd359bc0
IB
211 amounts[currency] = other_currency_amount
212 return amounts
213
214 @classmethod
215 def currencies(cls):
216 return cls.known_balances.keys()
217
dd359bc0
IB
218 @classmethod
219 def fetch_balances(cls, market):
006a2084
IB
220 all_balances = market.fetch_all_balances()
221 for currency, balance in all_balances.items():
222 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
223 currency in cls.known_balances:
224 cls.known_balances[currency] = cls(currency, balance)
dd359bc0 225 return cls.known_balances
350ed24d 226
dd359bc0 227 @classmethod
7ab23e29
IB
228 def dispatch_assets(cls, amount, repartition=None):
229 if repartition is None:
350ed24d
IB
230 repartition = Portfolio.repartition()
231 sum_ratio = sum([v[0] for k, v in repartition.items()])
dd359bc0 232 amounts = {}
350ed24d
IB
233 for currency, (ptt, trade_type) in repartition.items():
234 amounts[currency] = ptt * amount / sum_ratio
006a2084
IB
235 if trade_type == "short":
236 amounts[currency] = - amounts[currency]
dd359bc0 237 if currency not in cls.known_balances:
80cdd672 238 cls.known_balances[currency] = cls(currency, {})
dd359bc0
IB
239 return amounts
240
241 @classmethod
80cdd672 242 def prepare_trades(cls, market, base_currency="BTC", compute_value="average", debug=False):
dd359bc0 243 cls.fetch_balances(market)
a9950fd0 244 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
dd359bc0
IB
245 total_base_value = sum(values_in_base.values())
246 new_repartition = cls.dispatch_assets(total_base_value)
deb8924c 247 # Recompute it in case we have new currencies
a9950fd0 248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
80cdd672 249 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
a9950fd0
IB
250
251 @classmethod
80cdd672 252 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None, debug=False):
a9950fd0
IB
253 cls.fetch_balances(market)
254 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
255 total_base_value = sum(values_in_base.values())
256 new_repartition = cls.dispatch_assets(total_base_value)
80cdd672 257 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug)
dd359bc0 258
7ab23e29 259 @classmethod
80cdd672 260 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average", debug=False):
7ab23e29
IB
261 cls.fetch_balances(market)
262 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
263 total_base_value = sum(values_in_base.values())
350ed24d 264 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
80cdd672 265 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
7ab23e29 266
dd359bc0 267 def __repr__(self):
006a2084
IB
268 if self.exchange_total > 0:
269 if self.exchange_free > 0 and self.exchange_used > 0:
270 exchange = " Exch: [✔{} + ❌{} = {}]".format(str(self.exchange_free), str(self.exchange_used), str(self.exchange_total))
271 elif self.exchange_free > 0:
272 exchange = " Exch: [✔{}]".format(str(self.exchange_free))
273 else:
274 exchange = " Exch: [❌{}]".format(str(self.exchange_used))
275 else:
276 exchange = ""
277
278 if self.margin_total > 0:
279 if self.margin_free != 0 and self.margin_borrowed != 0:
280 margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total))
281 elif self.margin_free != 0:
282 margin = " Margin: [✔{}]".format(str(self.margin_free))
283 else:
284 margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed))
285 elif self.margin_total < 0:
286 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
287 str(self.margin_borrowed_base_price),
288 str(self.margin_lending_fees))
289 else:
290 margin = ""
291
292 if self.margin_total != 0 and self.exchange_total != 0:
293 total = " Total: [{}]".format(str(self.total))
294 else:
295 total = ""
296
297 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")"
dd359bc0 298
deb8924c 299class Computation:
deb8924c
IB
300 computations = {
301 "default": lambda x, y: x[y],
deb8924c
IB
302 "average": lambda x, y: x["average"],
303 "bid": lambda x, y: x["bid"],
304 "ask": lambda x, y: x["ask"],
305 }
306
dd359bc0 307class Trade:
80cdd672 308 debug = False
006a2084 309 trades = []
dd359bc0 310
006a2084 311 def __init__(self, value_from, value_to, currency, market=None):
dd359bc0
IB
312 # We have value_from of currency, and want to finish with value_to of
313 # that currency. value_* may not be in currency's terms
314 self.currency = currency
315 self.value_from = value_from
316 self.value_to = value_to
317 self.orders = []
089d5d9d 318 self.market = market
dd359bc0 319 assert self.value_from.currency == self.value_to.currency
006a2084
IB
320 if self.value_from != 0:
321 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
322 elif self.value_from.linked_to is None:
323 self.value_from.linked_to = Amount(self.currency, 0)
dd359bc0
IB
324 self.base_currency = self.value_from.currency
325
cfab619d
IB
326 fees_cache = {}
327 @classmethod
328 def fetch_fees(cls, market):
329 if market.__class__ not in cls.fees_cache:
330 cls.fees_cache[market.__class__] = market.fetch_fees()
331 return cls.fees_cache[market.__class__]
332
333 ticker_cache = {}
334 ticker_cache_timestamp = time.time()
335 @classmethod
336 def get_ticker(cls, c1, c2, market, refresh=False):
337 def invert(ticker):
338 return {
339 "inverted": True,
5ab23e1c 340 "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
cfab619d
IB
341 "original": ticker,
342 }
343 def augment_ticker(ticker):
344 ticker.update({
345 "inverted": False,
346 "average": (ticker["bid"] + ticker["ask"] ) / 2,
347 })
348
349 if time.time() - cls.ticker_cache_timestamp > 5:
350 cls.ticker_cache = {}
351 cls.ticker_cache_timestamp = time.time()
352 elif not refresh:
353 if (c1, c2, market.__class__) in cls.ticker_cache:
354 return cls.ticker_cache[(c1, c2, market.__class__)]
355 if (c2, c1, market.__class__) in cls.ticker_cache:
356 return invert(cls.ticker_cache[(c2, c1, market.__class__)])
357
358 try:
359 cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
360 augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
e0b14bcc 361 except ExchangeError:
cfab619d
IB
362 try:
363 cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
364 augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
e0b14bcc 365 except ExchangeError:
cfab619d
IB
366 cls.ticker_cache[(c1, c2, market.__class__)] = None
367 return cls.get_ticker(c1, c2, market)
368
dd359bc0 369 @classmethod
80cdd672
IB
370 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False):
371 cls.debug = cls.debug or debug
dd359bc0
IB
372 base_currency = sum(values_in_base.values()).currency
373 for currency in Balance.currencies():
374 if currency == base_currency:
375 continue
006a2084
IB
376 value_from = values_in_base.get(currency, Amount(base_currency, 0))
377 value_to = new_repartition.get(currency, Amount(base_currency, 0))
378 if value_from.value * value_to.value < 0:
379 trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market)
380 if only is None or trade_1.action == only:
381 cls.trades.append(trade_1)
382 trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market)
383 if only is None or trade_2.action == only:
384 cls.trades.append(trade_2)
385 else:
386 trade = cls(
387 value_from,
388 value_to,
389 currency,
390 market=market
391 )
392 if only is None or trade.action == only:
393 cls.trades.append(trade)
dd359bc0
IB
394 return cls.trades
395
a9950fd0
IB
396 @classmethod
397 def prepare_orders(cls, only=None, compute_value="default"):
006a2084 398 for trade in cls.trades:
a9950fd0
IB
399 if only is None or trade.action == only:
400 trade.prepare_order(compute_value=compute_value)
401
006a2084 402 @classmethod
80cdd672 403 def move_balances(cls, market):
006a2084
IB
404 needed_in_margin = {}
405 for trade in cls.trades:
406 if trade.trade_type == "short":
407 if trade.value_to.currency not in needed_in_margin:
408 needed_in_margin[trade.value_to.currency] = 0
409 needed_in_margin[trade.value_to.currency] += abs(trade.value_to)
410 for currency, needed in needed_in_margin.items():
411 current_balance = Balance.known_balances[currency].margin_free
412 delta = (needed - current_balance).value
413 # FIXME: don't remove too much if there are open margin position
414 if delta > 0:
80cdd672 415 if cls.debug:
006a2084
IB
416 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta))
417 else:
418 market.transfer_balance(currency, delta, "exchange", "margin")
419 elif delta < 0:
80cdd672 420 if cls.debug:
006a2084
IB
421 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta))
422 else:
423 market.transfer_balance(currency, -delta, "margin", "exchange")
424
dd359bc0
IB
425 @property
426 def action(self):
427 if self.value_from == self.value_to:
428 return None
429 if self.base_currency == self.currency:
430 return None
431
432 if self.value_from < self.value_to:
006a2084 433 return "acquire"
dd359bc0 434 else:
006a2084 435 return "dispose"
dd359bc0 436
cfab619d 437 def order_action(self, inverted):
006a2084 438 if (self.value_from < self.value_to) != inverted:
350ed24d 439 return "buy"
dd359bc0 440 else:
350ed24d 441 return "sell"
dd359bc0 442
006a2084
IB
443 @property
444 def trade_type(self):
445 if self.value_from + self.value_to < 0:
446 return "short"
447 else:
448 return "long"
449
80cdd672
IB
450 @property
451 def filled_amount(self):
452 filled_amount = 0
453 for order in self.orders:
454 filled_amount += order.filled_amount
455 return filled_amount
456
457 def update_order(self, order, tick):
458 new_order = None
459 if tick in [0, 1, 3, 4, 6]:
460 print("{}, tick {}, waiting".format(order, tick))
461 elif tick == 2:
462 self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2)
463 new_order = self.orders[-1]
464 print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
465 elif tick ==5:
466 self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
467 new_order = self.orders[-1]
468 print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
469 elif tick >= 7:
470 if tick == 7:
471 print("{}, tick {}, fallbacking to market value".format(order, tick))
472 if (tick - 7) % 3 == 0:
473 self.prepare_order(compute_value="default")
474 new_order = self.orders[-1]
475 print("{}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order))
476
477 if new_order is not None:
478 order.cancel()
479 new_order.run()
480
deb8924c 481 def prepare_order(self, compute_value="default"):
dd359bc0
IB
482 if self.action is None:
483 return
350ed24d 484 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market)
dd359bc0 485 inverted = ticker["inverted"]
f2097d71
IB
486 if inverted:
487 ticker = ticker["original"]
488 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
c11e4274 489 # 0.1
f2097d71 490
f2097d71 491 delta_in_base = abs(self.value_from - self.value_to)
c11e4274 492 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0
IB
493
494 if not inverted:
350ed24d
IB
495 currency = self.base_currency
496 # BTC
006a2084 497 if self.action == "dispose":
c11e4274
IB
498 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
499 # At rate 1 Foo = 0.1 BTC
f2097d71 500 value_from = self.value_from.linked_to
c11e4274 501 # value_from = 100 FOO
f2097d71 502 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
c11e4274 503 # value_to = 10 FOO (1 BTC * 1/0.1)
f2097d71 504 delta = abs(value_to - value_from)
c11e4274
IB
505 # delta = 90 FOO
506 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
507
508 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
f2097d71
IB
509 else:
510 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
c11e4274
IB
511 # I want to buy 9 / 0.1 FOO
512 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
dd359bc0 513 else:
dd359bc0 514 currency = self.currency
c11e4274 515 # FOO
350ed24d
IB
516 delta = delta_in_base
517 # sell:
518 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
519 # At rate 1 Foo = 0.1 BTC
520 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
521 # buy:
522 # I want to buy 9 / 0.1 FOO
523 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
80cdd672
IB
524 if self.value_to == 0:
525 rate = self.value_from.linked_to.value / self.value_from.value
526 # Recompute the rate to avoid any rounding error
dd359bc0 527
006a2084
IB
528 close_if_possible = (self.value_to == 0)
529
80cdd672
IB
530 if delta <= self.filled_amount:
531 print("Less to do than already filled: {} <= {}".format(delta,
532 self.filled_amount))
533 return
534
006a2084 535 self.orders.append(Order(self.order_action(inverted),
80cdd672
IB
536 delta - self.filled_amount, rate, currency, self.trade_type,
537 self.market, self, close_if_possible=close_if_possible))
dd359bc0 538
deb8924c
IB
539 @classmethod
540 def compute_value(cls, ticker, action, compute_value="default"):
350ed24d
IB
541 if action == "buy":
542 action = "ask"
543 if action == "sell":
544 action = "bid"
77f8a378 545 if isinstance(compute_value, str):
deb8924c
IB
546 compute_value = Computation.computations[compute_value]
547 return compute_value(ticker, action)
548
dd359bc0 549 @classmethod
a9950fd0 550 def all_orders(cls, state=None):
006a2084 551 all_orders = sum(map(lambda v: v.orders, cls.trades), [])
a9950fd0
IB
552 if state is None:
553 return all_orders
554 else:
555 return list(filter(lambda o: o.status == state, all_orders))
556
557 @classmethod
558 def run_orders(cls):
559 for order in cls.all_orders(state="pending"):
560 order.run()
dd359bc0
IB
561
562 @classmethod
80cdd672
IB
563 def follow_orders(cls, verbose=True, sleep=None):
564 if sleep is None:
565 sleep = 7 if cls.debug else 30
566 tick = 0
567 while len(cls.all_orders(state="open")) > 0:
a9950fd0 568 time.sleep(sleep)
80cdd672
IB
569 tick += 1
570 for order in cls.all_orders(state="open"):
a9950fd0 571 if order.get_status() != "open":
a9950fd0
IB
572 if verbose:
573 print("finished {}".format(order))
80cdd672
IB
574 else:
575 order.trade.update_order(order, tick)
a9950fd0
IB
576 if verbose:
577 print("All orders finished")
dd359bc0 578
272b3cfb
IB
579 @classmethod
580 def update_all_orders_status(cls):
581 for order in cls.all_orders(state="open"):
582 order.get_status()
583
dd359bc0 584 def __repr__(self):
006a2084 585 return "Trade({} -> {} in {}, {})".format(
dd359bc0
IB
586 self.value_from,
587 self.value_to,
588 self.currency,
006a2084 589 self.action)
dd359bc0 590
272b3cfb
IB
591 @classmethod
592 def print_all_with_order(cls):
006a2084 593 for trade in cls.trades:
272b3cfb
IB
594 trade.print_with_order()
595
596 def print_with_order(self):
597 print(self)
598 for order in self.orders:
599 print("\t", order, sep="")
dd359bc0 600
272b3cfb 601class Order:
006a2084 602 def __init__(self, action, amount, rate, base_currency, trade_type, market,
80cdd672 603 trade, close_if_possible=False):
dd359bc0
IB
604 self.action = action
605 self.amount = amount
606 self.rate = rate
607 self.base_currency = base_currency
a9950fd0 608 self.market = market
350ed24d 609 self.trade_type = trade_type
80cdd672
IB
610 self.results = []
611 self.mouvements = []
a9950fd0 612 self.status = "pending"
80cdd672 613 self.trade = trade
006a2084 614 self.close_if_possible = close_if_possible
80cdd672 615 self.debug = trade.debug
dd359bc0
IB
616
617 def __repr__(self):
006a2084 618 return "Order({} {} {} at {} {} [{}]{})".format(
dd359bc0 619 self.action,
350ed24d 620 self.trade_type,
dd359bc0
IB
621 self.amount,
622 self.rate,
623 self.base_currency,
006a2084
IB
624 self.status,
625 " ✂" if self.close_if_possible else "",
dd359bc0
IB
626 )
627
350ed24d
IB
628 @property
629 def account(self):
630 if self.trade_type == "long":
631 return "exchange"
632 else:
633 return "margin"
634
a9950fd0
IB
635 @property
636 def pending(self):
637 return self.status == "pending"
638
639 @property
640 def finished(self):
fd8afa51 641 return self.status == "closed" or self.status == "canceled" or self.status == "error"
a9950fd0 642
80cdd672
IB
643 @property
644 def id(self):
645 return self.results[0]["id"]
646
647 def run(self):
dd359bc0 648 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
350ed24d 649 amount = round(self.amount, self.market.order_precision(symbol)).value
dd359bc0 650
80cdd672 651 if self.debug:
ecba1113
IB
652 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
653 symbol, self.action, amount, self.rate, self.account))
80cdd672
IB
654 self.status = "open"
655 self.results.append({"debug": True, "id": -1})
dd359bc0
IB
656 else:
657 try:
80cdd672 658 self.results.append(self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
dd359bc0 659 self.status = "open"
fd8afa51
IB
660 except Exception as e:
661 self.status = "error"
ecba1113
IB
662 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
663 symbol, self.action, amount, self.rate, self.account))
fd8afa51
IB
664 self.error_message = str("{}: {}".format(e.__class__.__name__, e))
665 print(self.error_message)
dd359bc0 666
a9950fd0 667 def get_status(self):
80cdd672
IB
668 if self.debug:
669 return self.status
dd359bc0
IB
670 # other states are "closed" and "canceled"
671 if self.status == "open":
80cdd672
IB
672 self.fetch()
673 if self.status != "open":
674 self.mark_finished_order()
dd359bc0
IB
675 return self.status
676
80cdd672
IB
677 def mark_finished_order(self):
678 if self.debug:
679 return
680 if self.status == "closed":
006a2084
IB
681 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
682 self.market.close_margin_position(self.amount.currency, self.base_currency)
683
80cdd672
IB
684 fetch_cache_timestamp = None
685 def fetch(self, force=False):
686 if self.debug or (not force and self.fetch_cache_timestamp is not None
687 and time.time() - self.fetch_cache_timestamp < 10):
688 return
689 self.fetch_cache_timestamp = time.time()
690
691 self.results.append(self.market.fetch_order(self.id))
692 result = self.results[-1]
006a2084 693 self.status = result["status"]
80cdd672
IB
694 # Time at which the order started
695 self.timestamp = result["datetime"]
696 self.fetch_mouvements()
697
698 # FIXME: consider open order with dust remaining as closed
699
700 @property
701 def dust_amount_remaining(self):
702 return self.remaining_amount < 0.001
703
704 @property
705 def remaining_amount(self):
706 if self.status == "open":
707 self.fetch()
708 return self.amount - self.filled_amount
709
710 @property
711 def filled_amount(self):
712 if self.status == "open":
713 self.fetch()
714 filled_amount = Amount(self.amount.currency, 0)
715 for mouvement in self.mouvements:
716 filled_amount += mouvement.total
717 return filled_amount
718
719 def fetch_mouvements(self):
720 mouvements = self.market.privatePostReturnOrderTrades({"orderNumber": self.id})
721 self.mouvements = []
722
723 for mouvement_hash in mouvements:
724 self.mouvements.append(Mouvement(self.amount.currency,
725 self.base_currency, mouvement_hash))
006a2084 726
272b3cfb 727 def cancel(self):
80cdd672
IB
728 if self.debug:
729 self.status = "canceled"
730 return
272b3cfb 731 self.market.cancel_order(self.result['id'])
80cdd672
IB
732 self.fetch()
733
734class Mouvement:
735 def __init__(self, currency, base_currency, hash_):
736 self.currency = currency
737 self.base_currency = base_currency
738 self.id = hash_["id"]
739 self.action = hash_["type"]
740 self.fee_rate = D(hash_["fee"])
741 self.date = datetime.strptime(hash_["date"], '%Y-%m-%d %H:%M:%S')
742 self.rate = D(hash_["rate"])
743 self.total = Amount(currency, hash_["amount"])
744 # rate * total = total_in_base
745 self.total_in_base = Amount(base_currency, hash_["total"])
272b3cfb 746
dd359bc0 747def print_orders(market, base_currency="BTC"):
deb8924c 748 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
a9950fd0 749 Trade.prepare_orders(compute_value="average")
5ab23e1c
IB
750 for currency, balance in Balance.known_balances.items():
751 print(balance)
350ed24d 752 Trade.print_all_with_order()
dd359bc0
IB
753
754def make_orders(market, base_currency="BTC"):
755 Balance.prepare_trades(market, base_currency=base_currency)
006a2084 756 for trade in Trade.trades:
dd359bc0
IB
757 print(trade)
758 for order in trade.orders:
759 print("\t", order, sep="")
a9950fd0 760 order.run()
dd359bc0 761
80cdd672
IB
762def process_sell_all_sell(market, base_currency="BTC", debug=False):
763 Balance.prepare_trades_to_sell_all(market, debug=debug)
006a2084 764 Trade.prepare_orders(compute_value="average")
80cdd672
IB
765 print("------------------")
766 for currency, balance in Balance.known_balances.items():
767 print(balance)
768 print("------------------")
769 Trade.print_all_with_order()
770 print("------------------")
006a2084
IB
771 Trade.run_orders()
772 Trade.follow_orders()
773
80cdd672
IB
774def process_sell_all_buy(market, base_currency="BTC", debug=False):
775 Balance.prepare_trades(market, debug=debug)
776 Trade.prepare_orders()
777 print("------------------")
778 for currency, balance in Balance.known_balances.items():
779 print(balance)
780 print("------------------")
781 Trade.print_all_with_order()
782 print("------------------")
006a2084
IB
783 Trade.move_balances(market)
784 Trade.run_orders()
785 Trade.follow_orders()
786
dd359bc0
IB
787if __name__ == '__main__':
788 print_orders(market)