]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Eat several positions in the order book after some time spent
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
CommitLineData
e7d7c0e5 1import datetime
f70bb858 2from retry import retry
7f3e7d27 3from decimal import Decimal as D, ROUND_DOWN
b47d7b54 4from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout, InvalidNonce
80cdd672 5
6ca5a1ec 6class Computation:
51bc7cde
IB
7 @staticmethod
8 def eat_several(market):
9 return lambda x, y: market.ccxt.fetch_nth_order_book(x["symbol"], y, 15)
10
6ca5a1ec
IB
11 computations = {
12 "default": lambda x, y: x[y],
13 "average": lambda x, y: x["average"],
14 "bid": lambda x, y: x["bid"],
15 "ask": lambda x, y: x["ask"],
16 }
17
18 @classmethod
19 def compute_value(cls, ticker, action, compute_value="default"):
20 if action == "buy":
21 action = "ask"
22 if action == "sell":
23 action = "bid"
24 if isinstance(compute_value, str):
25 compute_value = cls.computations[compute_value]
26 return compute_value(ticker, action)
27
dd359bc0 28class Amount:
c2644ba8 29 def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
dd359bc0 30 self.currency = currency
5ab23e1c 31 self.value = D(value)
dd359bc0
IB
32 self.linked_to = linked_to
33 self.ticker = ticker
c2644ba8 34 self.rate = rate
dd359bc0 35
c2644ba8 36 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
dd359bc0
IB
37 if other_currency == self.currency:
38 return self
c2644ba8
IB
39 if rate is not None:
40 return Amount(
41 other_currency,
42 self.value * rate,
43 linked_to=self,
44 rate=rate)
f86ee140 45 asset_ticker = market.get_ticker(self.currency, other_currency)
dd359bc0 46 if asset_ticker is not None:
6ca5a1ec 47 rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value)
dd359bc0
IB
48 return Amount(
49 other_currency,
c2644ba8 50 self.value * rate,
dd359bc0 51 linked_to=self,
c2644ba8
IB
52 ticker=asset_ticker,
53 rate=rate)
dd359bc0
IB
54 else:
55 raise Exception("This asset is not available in the chosen market")
56
3d0247f9
IB
57 def as_json(self):
58 return {
59 "currency": self.currency,
60 "value": round(self).value.normalize(),
61 }
62
350ed24d
IB
63 def __round__(self, n=8):
64 return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN))
65
dd359bc0 66 def __abs__(self):
5ab23e1c 67 return Amount(self.currency, abs(self.value))
dd359bc0
IB
68
69 def __add__(self, other):
f320eb8a
IB
70 if other == 0:
71 return self
5ab23e1c 72 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 73 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 74 return Amount(self.currency, self.value + other.value)
dd359bc0
IB
75
76 def __radd__(self, other):
77 if other == 0:
78 return self
79 else:
80 return self.__add__(other)
81
82 def __sub__(self, other):
c51687d2
IB
83 if other == 0:
84 return self
5ab23e1c 85 if other.currency != self.currency and other.value * self.value != 0:
dd359bc0 86 raise Exception("Summing amounts must be done with same currencies")
5ab23e1c 87 return Amount(self.currency, self.value - other.value)
dd359bc0 88
f320eb8a
IB
89 def __rsub__(self, other):
90 if other == 0:
91 return -self
92 else:
93 return -self.__sub__(other)
94
dd359bc0 95 def __mul__(self, value):
77f8a378 96 if not isinstance(value, (int, float, D)):
dd359bc0 97 raise TypeError("Amount may only be multiplied by numbers")
5ab23e1c 98 return Amount(self.currency, self.value * value)
dd359bc0
IB
99
100 def __rmul__(self, value):
101 return self.__mul__(value)
102
103 def __floordiv__(self, value):
77f8a378 104 if not isinstance(value, (int, float, D)):
1aa7d4fa 105 raise TypeError("Amount may only be divided by numbers")
5ab23e1c 106 return Amount(self.currency, self.value / value)
dd359bc0
IB
107
108 def __truediv__(self, value):
109 return self.__floordiv__(value)
110
111 def __lt__(self, other):
006a2084
IB
112 if other == 0:
113 return self.value < 0
dd359bc0
IB
114 if self.currency != other.currency:
115 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 116 return self.value < other.value
dd359bc0 117
80cdd672
IB
118 def __le__(self, other):
119 return self == other or self < other
120
006a2084
IB
121 def __gt__(self, other):
122 return not self <= other
123
124 def __ge__(self, other):
125 return not self < other
126
dd359bc0
IB
127 def __eq__(self, other):
128 if other == 0:
5ab23e1c 129 return self.value == 0
dd359bc0
IB
130 if self.currency != other.currency:
131 raise Exception("Comparing amounts must be done with same currencies")
5ab23e1c 132 return self.value == other.value
dd359bc0 133
006a2084
IB
134 def __ne__(self, other):
135 return not self == other
136
137 def __neg__(self):
138 return Amount(self.currency, - self.value)
139
dd359bc0
IB
140 def __str__(self):
141 if self.linked_to is None:
142 return "{:.8f} {}".format(self.value, self.currency)
143 else:
144 return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
145
146 def __repr__(self):
147 if self.linked_to is None:
148 return "Amount({:.8f} {})".format(self.value, self.currency)
149 else:
150 return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
151
152class Balance:
3d0247f9 153 base_keys = ["total", "exchange_total", "exchange_used",
aca4d437
IB
154 "exchange_free", "margin_total", "margin_in_position",
155 "margin_available", "margin_borrowed", "margin_pending_gain"]
dd359bc0 156
006a2084 157 def __init__(self, currency, hash_):
dd359bc0 158 self.currency = currency
3d0247f9 159 for key in self.base_keys:
006a2084
IB
160 setattr(self, key, Amount(currency, hash_.get(key, 0)))
161
80cdd672 162 self.margin_position_type = hash_.get("margin_position_type")
006a2084 163
80cdd672 164 if hash_.get("margin_borrowed_base_currency") is not None:
006a2084
IB
165 base_currency = hash_["margin_borrowed_base_currency"]
166 for key in [
167 "margin_liquidation_price",
006a2084 168 "margin_lending_fees",
aca4d437 169 "margin_pending_base_gain",
006a2084
IB
170 "margin_borrowed_base_price"
171 ]:
c51687d2 172 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
f2da6589 173
3d0247f9
IB
174 def as_json(self):
175 return dict(map(lambda x: (x, getattr(self, x).as_json()["value"]), self.base_keys))
176
dd359bc0 177 def __repr__(self):
006a2084
IB
178 if self.exchange_total > 0:
179 if self.exchange_free > 0 and self.exchange_used > 0:
180 exchange = " Exch: [✔{} + ❌{} = {}]".format(str(self.exchange_free), str(self.exchange_used), str(self.exchange_total))
181 elif self.exchange_free > 0:
182 exchange = " Exch: [✔{}]".format(str(self.exchange_free))
183 else:
184 exchange = " Exch: [❌{}]".format(str(self.exchange_used))
185 else:
186 exchange = ""
187
188 if self.margin_total > 0:
aca4d437
IB
189 if self.margin_available != 0 and self.margin_in_position != 0:
190 margin = " Margin: [✔{} + ❌{} = {}]".format(str(self.margin_available), str(self.margin_in_position), str(self.margin_total))
191 elif self.margin_available != 0:
192 margin = " Margin: [✔{}]".format(str(self.margin_available))
006a2084 193 else:
aca4d437 194 margin = " Margin: [❌{}]".format(str(self.margin_in_position))
006a2084
IB
195 elif self.margin_total < 0:
196 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
197 str(self.margin_borrowed_base_price),
198 str(self.margin_lending_fees))
199 else:
200 margin = ""
201
202 if self.margin_total != 0 and self.exchange_total != 0:
203 total = " Total: [{}]".format(str(self.total))
204 else:
205 total = ""
206
207 return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")"
dd359bc0
IB
208
209class Trade:
f86ee140 210 def __init__(self, value_from, value_to, currency, market):
dd359bc0
IB
211 # We have value_from of currency, and want to finish with value_to of
212 # that currency. value_* may not be in currency's terms
213 self.currency = currency
214 self.value_from = value_from
215 self.value_to = value_to
216 self.orders = []
089d5d9d 217 self.market = market
17598517 218 self.closed = False
186f7d81 219 self.inverted = None
aca4d437 220 assert self.value_from.value * self.value_to.value >= 0
dd359bc0 221 assert self.value_from.currency == self.value_to.currency
006a2084
IB
222 if self.value_from != 0:
223 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
224 elif self.value_from.linked_to is None:
225 self.value_from.linked_to = Amount(self.currency, 0)
dd359bc0
IB
226 self.base_currency = self.value_from.currency
227
aca4d437
IB
228 @property
229 def delta(self):
230 return self.value_to - self.value_from
231
dd359bc0
IB
232 @property
233 def action(self):
234 if self.value_from == self.value_to:
235 return None
236 if self.base_currency == self.currency:
237 return None
238
5a72ded7 239 if abs(self.value_from) < abs(self.value_to):
006a2084 240 return "acquire"
dd359bc0 241 else:
006a2084 242 return "dispose"
dd359bc0 243
186f7d81
IB
244 def order_action(self):
245 if (self.value_from < self.value_to) != self.inverted:
350ed24d 246 return "buy"
dd359bc0 247 else:
350ed24d 248 return "sell"
dd359bc0 249
006a2084
IB
250 @property
251 def trade_type(self):
252 if self.value_from + self.value_to < 0:
253 return "short"
254 else:
255 return "long"
256
17598517
IB
257 @property
258 def pending(self):
259 return not (self.is_fullfiled or self.closed)
260
261 def close(self):
f9226903
IB
262 for order in self.orders:
263 order.cancel()
17598517
IB
264 self.closed = True
265
aca4d437
IB
266 @property
267 def is_fullfiled(self):
4ae84fb7 268 return abs(self.filled_amount(in_base_currency=(not self.inverted), refetch=True)) >= abs(self.delta)
aca4d437 269
4ae84fb7 270 def filled_amount(self, in_base_currency=False, refetch=False):
80cdd672
IB
271 filled_amount = 0
272 for order in self.orders:
4ae84fb7 273 filled_amount += order.filled_amount(in_base_currency=in_base_currency, refetch=refetch)
80cdd672
IB
274 return filled_amount
275
7ba831c5
IB
276 tick_actions = {
277 0: ["waiting", None],
278 1: ["waiting", None],
279 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
280 3: ["waiting", None],
281 4: ["waiting", None],
282 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
283 6: ["waiting", None],
284 7: ["market_fallback", "default"],
285 }
aca4d437 286
7ba831c5
IB
287 def tick_actions_recreate(self, tick, default="average"):
288 return ([default] + \
289 [ y[1] for x, y in self.tick_actions.items() if x <= tick and y[1] is not None ])[-1]
290
291 def update_order(self, order, tick):
292 if tick in self.tick_actions:
293 update, compute_value = self.tick_actions[tick]
aca4d437 294 elif tick % 3 == 1:
51bc7cde
IB
295 if tick < 20:
296 update = "market_adjust"
297 compute_value = "default"
298 else:
299 update = "market_adjust_eat"
300 compute_value = Computation.eat_several(self.market)
aca4d437 301 else:
3d0247f9
IB
302 update = "waiting"
303 compute_value = None
aca4d437
IB
304
305 if compute_value is not None:
306 order.cancel()
307 new_order = self.prepare_order(compute_value=compute_value)
308 else:
309 new_order = None
3d0247f9 310
f86ee140 311 self.market.report.log_order(order, tick, update=update,
3d0247f9 312 compute_value=compute_value, new_order=new_order)
80cdd672
IB
313
314 if new_order is not None:
80cdd672 315 new_order.run()
f86ee140 316 self.market.report.log_order(order, tick, new_order=new_order)
80cdd672 317
2033e7fe 318 def prepare_order(self, close_if_possible=None, compute_value="default"):
dd359bc0 319 if self.action is None:
5a72ded7 320 return None
f86ee140 321 ticker = self.market.get_ticker(self.currency, self.base_currency)
18de421e
IB
322 if ticker is None:
323 self.market.report.log_error("prepare_order",
324 message="Unknown ticker {}/{}".format(self.currency, self.base_currency))
325 return None
186f7d81
IB
326 self.inverted = ticker["inverted"]
327 if self.inverted:
f2097d71 328 ticker = ticker["original"]
186f7d81 329 rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value)
f2097d71 330
aca4d437 331 delta_in_base = abs(self.delta)
c11e4274 332 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0 333
186f7d81 334 if not self.inverted:
1aa7d4fa 335 base_currency = self.base_currency
350ed24d 336 # BTC
006a2084 337 if self.action == "dispose":
1aa7d4fa
IB
338 filled = self.filled_amount(in_base_currency=False)
339 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
340 # I have 10 BTC worth of FOO, and I want to sell 9 BTC
341 # worth of it, computed first with rate 10 FOO = 1 BTC.
342 # -> I "sell" "90" FOO at proposed rate "rate".
343
344 delta = delta - filled
345 # I already sold 60 FOO, 30 left
f2097d71 346 else:
1aa7d4fa
IB
347 filled = self.filled_amount(in_base_currency=True)
348 delta = (delta_in_base - filled).in_currency(self.currency, self.market, rate=1/rate)
349 # I want to buy 9 BTC worth of FOO, computed with rate
350 # 10 FOO = 1 BTC
351 # -> I "buy" "9 / rate" FOO at proposed rate "rate"
352
353 # I already bought 3 / rate FOO, 6 / rate left
dd359bc0 354 else:
1aa7d4fa 355 base_currency = self.currency
c11e4274 356 # FOO
1aa7d4fa
IB
357 if self.action == "dispose":
358 filled = self.filled_amount(in_base_currency=True)
359 # Base is FOO
360
361 delta = (delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
362 - filled).in_currency(self.base_currency, self.market, rate=1/rate)
363 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
364 # computed at rate 1 Foo = 0.01 BTC
365 # Computation says I should sell it at 125 FOO / BTC
366 # -> delta_in_base = 9 BTC
367 # -> delta = (9 * 1/0.01 FOO) * 1/125 = 7.2 BTC
368 # Action: "buy" "7.2 BTC" at rate "125" "FOO" on market
369
370 # I already bought 300/125 BTC, only 600/125 left
371 else:
372 filled = self.filled_amount(in_base_currency=False)
373 # Base is FOO
374
375 delta = delta_in_base
376 # I have 1 BTC worth of FOO, and I want to buy 9 BTC worth of it
377 # At rate 100 Foo / BTC
378 # Computation says I should buy it at 125 FOO / BTC
379 # -> delta_in_base = 9 BTC
380 # Action: "sell" "9 BTC" at rate "125" "FOO" on market
381
382 delta = delta - filled
383 # I already sold 4 BTC, only 5 left
dd359bc0 384
2033e7fe
IB
385 if close_if_possible is None:
386 close_if_possible = (self.value_to == 0)
006a2084 387
1aa7d4fa 388 if delta <= 0:
f86ee140 389 self.market.report.log_error("prepare_order", message="Less to do than already filled: {}".format(delta))
5a72ded7 390 return None
80cdd672 391
186f7d81 392 order = Order(self.order_action(),
1aa7d4fa 393 delta, rate, base_currency, self.trade_type,
5a72ded7
IB
394 self.market, self, close_if_possible=close_if_possible)
395 self.orders.append(order)
396 return order
dd359bc0 397
3d0247f9
IB
398 def as_json(self):
399 return {
400 "action": self.action,
401 "from": self.value_from.as_json()["value"],
402 "to": self.value_to.as_json()["value"],
403 "currency": self.currency,
404 "base_currency": self.base_currency,
405 }
406
dd359bc0 407 def __repr__(self):
f9226903
IB
408 if self.closed and not self.is_fullfiled:
409 closed = " ❌"
410 elif self.is_fullfiled:
411 closed = " ✔"
412 else:
413 closed = ""
414
415 return "Trade({} -> {} in {}, {}{})".format(
dd359bc0
IB
416 self.value_from,
417 self.value_to,
418 self.currency,
f9226903
IB
419 self.action,
420 closed)
dd359bc0 421
3d0247f9 422 def print_with_order(self, ind=""):
f86ee140 423 self.market.report.print_log("{}{}".format(ind, self))
272b3cfb 424 for order in self.orders:
f86ee140 425 self.market.report.print_log("{}\t{}".format(ind, order))
c31df868 426 for mouvement in order.mouvements:
f86ee140 427 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
dd359bc0 428
7f3e7d27
IB
429class RetryException(Exception):
430 pass
431
272b3cfb 432class Order:
006a2084 433 def __init__(self, action, amount, rate, base_currency, trade_type, market,
80cdd672 434 trade, close_if_possible=False):
dd359bc0
IB
435 self.action = action
436 self.amount = amount
437 self.rate = rate
438 self.base_currency = base_currency
a9950fd0 439 self.market = market
350ed24d 440 self.trade_type = trade_type
80cdd672
IB
441 self.results = []
442 self.mouvements = []
a9950fd0 443 self.status = "pending"
80cdd672 444 self.trade = trade
006a2084 445 self.close_if_possible = close_if_possible
5a72ded7 446 self.id = None
f70bb858 447 self.tries = 0
7f3e7d27 448 self.start_date = None
dd359bc0 449
3d0247f9
IB
450 def as_json(self):
451 return {
452 "action": self.action,
453 "trade_type": self.trade_type,
454 "amount": self.amount.as_json()["value"],
455 "currency": self.amount.as_json()["currency"],
456 "base_currency": self.base_currency,
457 "rate": self.rate,
458 "status": self.status,
459 "close_if_possible": self.close_if_possible,
460 "id": self.id,
461 "mouvements": list(map(lambda x: x.as_json(), self.mouvements))
462 }
463
dd359bc0 464 def __repr__(self):
006a2084 465 return "Order({} {} {} at {} {} [{}]{})".format(
dd359bc0 466 self.action,
350ed24d 467 self.trade_type,
dd359bc0
IB
468 self.amount,
469 self.rate,
470 self.base_currency,
006a2084
IB
471 self.status,
472 " ✂" if self.close_if_possible else "",
dd359bc0
IB
473 )
474
350ed24d
IB
475 @property
476 def account(self):
477 if self.trade_type == "long":
478 return "exchange"
479 else:
480 return "margin"
481
5a72ded7
IB
482 @property
483 def open(self):
484 return self.status == "open"
485
a9950fd0
IB
486 @property
487 def pending(self):
488 return self.status == "pending"
489
490 @property
491 def finished(self):
d8deb0e9 492 return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
a9950fd0 493
b47d7b54 494 @retry((InsufficientFunds, RetryException, InvalidNonce))
80cdd672 495 def run(self):
f70bb858 496 self.tries += 1
dd359bc0 497 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
f86ee140 498 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
dd359bc0 499
7f3e7d27 500 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
f86ee140 501 if self.market.debug:
7f3e7d27 502 self.market.report.log_debug_action(action)
80cdd672 503 self.results.append({"debug": True, "id": -1})
dd359bc0 504 else:
e7d7c0e5 505 self.start_date = datetime.datetime.now()
dd359bc0 506 try:
f86ee140 507 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
f70bb858 508 except InvalidOrder:
df9e4e7f
IB
509 # Impossible to honor the order (dust amount)
510 self.status = "closed"
511 self.mark_finished_order()
512 return
b47d7b54
IB
513 except InvalidNonce as e:
514 if self.tries < 5:
515 self.market.report.log_error(action, message="Retrying after invalid nonce", exception=e)
516 raise e
517 else:
518 self.market.report.log_error(action, message="Giving up {} after invalid nonce".format(self), exception=e)
519 self.status = "error"
520 return
7f3e7d27
IB
521 except RequestTimeout as e:
522 if not self.retrieve_order():
523 if self.tries < 5:
524 self.market.report.log_error(action, message="Retrying after timeout", exception=e)
525 # We make a specific call in case retrieve_order
526 # would raise itself
527 raise RetryException
528 else:
529 self.market.report.log_error(action, message="Giving up {} after timeouts".format(self), exception=e)
530 self.status = "error"
531 return
532 else:
533 self.market.report.log_error(action, message="Timeout, found the order")
f70bb858
IB
534 except InsufficientFunds as e:
535 if self.tries < 5:
536 self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
537 self.amount = self.amount * D("0.99")
538 raise e
539 else:
540 self.market.report.log_error(action, message="Giving up {}".format(self), exception=e)
541 self.status = "error"
542 return
fd8afa51
IB
543 except Exception as e:
544 self.status = "error"
f86ee140 545 self.market.report.log_error(action, exception=e)
5a72ded7
IB
546 return
547 self.id = self.results[0]["id"]
548 self.status = "open"
dd359bc0 549
a9950fd0 550 def get_status(self):
f86ee140
IB
551 if self.market.debug:
552 self.market.report.log_debug_action("Getting {} status".format(self))
80cdd672 553 return self.status
dd359bc0 554 # other states are "closed" and "canceled"
5a72ded7 555 if not self.finished:
80cdd672 556 self.fetch()
dd359bc0
IB
557 return self.status
558
7ba831c5 559 def mark_disappeared_order(self):
5542e9e3 560 if self.status.startswith("closed") and \
7ba831c5
IB
561 len(self.mouvements) > 0 and \
562 self.mouvements[-1].total_in_base == 0:
5542e9e3 563 self.status = "error_disappeared"
5542e9e3 564
80cdd672 565 def mark_finished_order(self):
d8deb0e9 566 if self.status.startswith("closed") and self.market.debug:
f86ee140 567 self.market.report.log_debug_action("Mark {} as finished".format(self))
80cdd672 568 return
d8deb0e9 569 if self.status.startswith("closed"):
006a2084 570 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
f86ee140 571 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
006a2084 572
aca4d437 573 def fetch(self):
f86ee140
IB
574 if self.market.debug:
575 self.market.report.log_debug_action("Fetching {}".format(self))
3d0247f9 576 return
aca4d437 577 try:
f9226903 578 result = self.market.ccxt.fetch_order(self.id)
aca4d437
IB
579 self.results.append(result)
580 self.status = result["status"]
581 # Time at which the order started
582 self.timestamp = result["datetime"]
583 except OrderNotCached:
584 self.status = "closed_unknown"
5a72ded7 585
80cdd672
IB
586 self.fetch_mouvements()
587
7ba831c5 588 self.mark_disappeared_order()
1902674c 589 self.mark_dust_amount_remaining_order()
d8deb0e9 590 self.mark_finished_order()
80cdd672 591
1902674c 592 def mark_dust_amount_remaining_order(self):
e24df7cf 593 if self.status == "open" and self.market.ccxt.is_dust_trade(self.remaining_amount().value, self.rate):
1902674c 594 self.status = "closed_dust_remaining"
80cdd672 595
4ae84fb7
IB
596 def remaining_amount(self, refetch=False):
597 return self.amount - self.filled_amount(refetch=refetch)
80cdd672 598
4ae84fb7
IB
599 def filled_amount(self, in_base_currency=False, refetch=False):
600 if refetch and self.status == "open":
80cdd672 601 self.fetch()
1aa7d4fa 602 filled_amount = 0
80cdd672 603 for mouvement in self.mouvements:
1aa7d4fa
IB
604 if in_base_currency:
605 filled_amount += mouvement.total_in_base
606 else:
607 filled_amount += mouvement.total
80cdd672
IB
608 return filled_amount
609
610 def fetch_mouvements(self):
df9e4e7f 611 try:
f86ee140 612 mouvements = self.market.ccxt.privatePostReturnOrderTrades({"orderNumber": self.id})
df9e4e7f
IB
613 except ExchangeError:
614 mouvements = []
80cdd672
IB
615 self.mouvements = []
616
617 for mouvement_hash in mouvements:
618 self.mouvements.append(Mouvement(self.amount.currency,
619 self.base_currency, mouvement_hash))
7ba831c5 620 self.mouvements.sort(key= lambda x: x.date)
006a2084 621
272b3cfb 622 def cancel(self):
f86ee140
IB
623 if self.market.debug:
624 self.market.report.log_debug_action("Mark {} as cancelled".format(self))
80cdd672
IB
625 self.status = "canceled"
626 return
37b64a97 627 if (self.status == "closed_dust_remaining" or self.open) and self.id is not None:
f9226903
IB
628 try:
629 self.market.ccxt.cancel_order(self.id)
630 except OrderNotFound as e: # Closed inbetween
631 self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e)
632 self.fetch()
80cdd672 633
7f3e7d27
IB
634 def retrieve_order(self):
635 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
636 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
637 start_timestamp = self.start_date.timestamp() - 5
638
639 similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp)
640 for order in similar_open_orders:
641 if (order["info"]["margin"] == 1 and self.account == "exchange") or\
642 (order["info"]["margin"] != 1 and self.account == "margin"):
643 i_m_tested = True # coverage bug ?!
644 continue
645 if order["info"]["side"] != self.action:
646 continue
647 amount_diff = round(
648 abs(D(order["info"]["startingAmount"]) - amount),
649 self.market.ccxt.order_precision(symbol))
650 rate_diff = round(
651 abs(D(order["info"]["rate"]) - self.rate),
652 self.market.ccxt.order_precision(symbol))
653 if amount_diff != 0 or rate_diff != 0:
654 continue
655 self.results.append({"id": order["id"]})
656 return True
657
658 similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp)
7f3e7d27
IB
659 for order_id in sorted(list(map(lambda x: x["order"], similar_trades))):
660 trades = list(filter(lambda x: x["order"] == order_id, similar_trades))
661 if any(x["timestamp"] < start_timestamp for x in trades):
662 continue
663 if any(x["side"] != self.action for x in trades):
664 continue
665 if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\
666 any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades):
667 continue
668 trade_sum = sum(D(x["info"]["amount"]) for x in trades)
669 amount_diff = round(abs(trade_sum - amount),
670 self.market.ccxt.order_precision(symbol))
671 if amount_diff != 0:
672 continue
673 if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\
674 (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)):
675 continue
676 self.results.append({"id": order_id})
677 return True
678
679 return False
680
80cdd672
IB
681class Mouvement:
682 def __init__(self, currency, base_currency, hash_):
683 self.currency = currency
684 self.base_currency = base_currency
df9e4e7f
IB
685 self.id = hash_.get("tradeID")
686 self.action = hash_.get("type")
687 self.fee_rate = D(hash_.get("fee", -1))
688 try:
e7d7c0e5 689 self.date = datetime.datetime.strptime(hash_.get("date", ""), '%Y-%m-%d %H:%M:%S')
df9e4e7f
IB
690 except ValueError:
691 self.date = None
692 self.rate = D(hash_.get("rate", 0))
693 self.total = Amount(currency, hash_.get("amount", 0))
80cdd672 694 # rate * total = total_in_base
df9e4e7f 695 self.total_in_base = Amount(base_currency, hash_.get("total", 0))
272b3cfb 696
3d0247f9
IB
697 def as_json(self):
698 return {
699 "fee_rate": self.fee_rate,
700 "date": self.date,
701 "action": self.action,
702 "total": self.total.value,
703 "currency": self.currency,
704 "total_in_base": self.total_in_base.value,
705 "base_currency": self.base_currency
706 }
707
c31df868
IB
708 def __repr__(self):
709 if self.fee_rate > 0:
710 fee_rate = " fee: {}%".format(self.fee_rate * 100)
711 else:
712 fee_rate = ""
713 if self.date is None:
714 date = "No date"
715 else:
716 date = self.date
717 return "Mouvement({} ; {} {} ({}){})".format(
718 date, self.action, self.total, self.total_in_base,
719 fee_rate)
720