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