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