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