]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Fixes after night run
[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
aca4d437 294 assert self.value_from.value * self.value_to.value >= 0
dd359bc0 295 assert self.value_from.currency == self.value_to.currency
006a2084
IB
296 if self.value_from != 0:
297 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
298 elif self.value_from.linked_to is None:
299 self.value_from.linked_to = Amount(self.currency, 0)
dd359bc0
IB
300 self.base_currency = self.value_from.currency
301
aca4d437
IB
302 @property
303 def delta(self):
304 return self.value_to - self.value_from
305
dd359bc0
IB
306 @property
307 def action(self):
308 if self.value_from == self.value_to:
309 return None
310 if self.base_currency == self.currency:
311 return None
312
5a72ded7 313 if abs(self.value_from) < abs(self.value_to):
006a2084 314 return "acquire"
dd359bc0 315 else:
006a2084 316 return "dispose"
dd359bc0 317
cfab619d 318 def order_action(self, inverted):
006a2084 319 if (self.value_from < self.value_to) != inverted:
350ed24d 320 return "buy"
dd359bc0 321 else:
350ed24d 322 return "sell"
dd359bc0 323
006a2084
IB
324 @property
325 def trade_type(self):
326 if self.value_from + self.value_to < 0:
327 return "short"
328 else:
329 return "long"
330
17598517
IB
331 @property
332 def pending(self):
333 return not (self.is_fullfiled or self.closed)
334
335 def close(self):
f9226903
IB
336 for order in self.orders:
337 order.cancel()
17598517
IB
338 self.closed = True
339
aca4d437
IB
340 @property
341 def is_fullfiled(self):
342 return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
343
1aa7d4fa 344 def filled_amount(self, in_base_currency=False):
80cdd672
IB
345 filled_amount = 0
346 for order in self.orders:
1aa7d4fa 347 filled_amount += order.filled_amount(in_base_currency=in_base_currency)
80cdd672
IB
348 return filled_amount
349
350 def update_order(self, order, tick):
aca4d437
IB
351 actions = {
352 0: ["waiting", None],
353 1: ["waiting", None],
354 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
355 3: ["waiting", None],
356 4: ["waiting", None],
357 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
358 6: ["waiting", None],
359 7: ["market_fallback", "default"],
360 }
361
362 if tick in actions:
363 update, compute_value = actions[tick]
364 elif tick % 3 == 1:
365 update = "market_adjust"
366 compute_value = "default"
367 else:
3d0247f9
IB
368 update = "waiting"
369 compute_value = None
aca4d437
IB
370
371 if compute_value is not None:
372 order.cancel()
373 new_order = self.prepare_order(compute_value=compute_value)
374 else:
375 new_order = None
3d0247f9 376
f86ee140 377 self.market.report.log_order(order, tick, update=update,
3d0247f9 378 compute_value=compute_value, new_order=new_order)
80cdd672
IB
379
380 if new_order is not None:
80cdd672 381 new_order.run()
f86ee140 382 self.market.report.log_order(order, tick, new_order=new_order)
80cdd672 383
2033e7fe 384 def prepare_order(self, close_if_possible=None, compute_value="default"):
dd359bc0 385 if self.action is None:
5a72ded7 386 return None
f86ee140 387 ticker = self.market.get_ticker(self.currency, self.base_currency)
dd359bc0 388 inverted = ticker["inverted"]
f2097d71
IB
389 if inverted:
390 ticker = ticker["original"]
6ca5a1ec 391 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
f2097d71 392
97922ff1
IB
393 # FIXME: Dust amount should be removed from there if they werent
394 # honored in other sales
aca4d437 395 delta_in_base = abs(self.delta)
c11e4274 396 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0
IB
397
398 if not inverted:
1aa7d4fa 399 base_currency = self.base_currency
350ed24d 400 # BTC
006a2084 401 if self.action == "dispose":
1aa7d4fa
IB
402 filled = self.filled_amount(in_base_currency=False)
403 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
404 # I have 10 BTC worth of FOO, and I want to sell 9 BTC
405 # worth of it, computed first with rate 10 FOO = 1 BTC.
406 # -> I "sell" "90" FOO at proposed rate "rate".
407
408 delta = delta - filled
409 # I already sold 60 FOO, 30 left
f2097d71 410 else:
1aa7d4fa
IB
411 filled = self.filled_amount(in_base_currency=True)
412 delta = (delta_in_base - filled).in_currency(self.currency, self.market, rate=1/rate)
413 # I want to buy 9 BTC worth of FOO, computed with rate
414 # 10 FOO = 1 BTC
415 # -> I "buy" "9 / rate" FOO at proposed rate "rate"
416
417 # I already bought 3 / rate FOO, 6 / rate left
dd359bc0 418 else:
1aa7d4fa 419 base_currency = self.currency
c11e4274 420 # FOO
1aa7d4fa
IB
421 if self.action == "dispose":
422 filled = self.filled_amount(in_base_currency=True)
423 # Base is FOO
424
425 delta = (delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
426 - filled).in_currency(self.base_currency, self.market, rate=1/rate)
427 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
428 # computed at rate 1 Foo = 0.01 BTC
429 # Computation says I should sell it at 125 FOO / BTC
430 # -> delta_in_base = 9 BTC
431 # -> delta = (9 * 1/0.01 FOO) * 1/125 = 7.2 BTC
432 # Action: "buy" "7.2 BTC" at rate "125" "FOO" on market
433
434 # I already bought 300/125 BTC, only 600/125 left
435 else:
436 filled = self.filled_amount(in_base_currency=False)
437 # Base is FOO
438
439 delta = delta_in_base
440 # I have 1 BTC worth of FOO, and I want to buy 9 BTC worth of it
441 # At rate 100 Foo / BTC
442 # Computation says I should buy it at 125 FOO / BTC
443 # -> delta_in_base = 9 BTC
444 # Action: "sell" "9 BTC" at rate "125" "FOO" on market
445
446 delta = delta - filled
447 # I already sold 4 BTC, only 5 left
dd359bc0 448
2033e7fe
IB
449 if close_if_possible is None:
450 close_if_possible = (self.value_to == 0)
006a2084 451
1aa7d4fa 452 if delta <= 0:
f86ee140 453 self.market.report.log_error("prepare_order", message="Less to do than already filled: {}".format(delta))
5a72ded7 454 return None
80cdd672 455
5a72ded7 456 order = Order(self.order_action(inverted),
1aa7d4fa 457 delta, rate, base_currency, self.trade_type,
5a72ded7
IB
458 self.market, self, close_if_possible=close_if_possible)
459 self.orders.append(order)
460 return order
dd359bc0 461
3d0247f9
IB
462 def as_json(self):
463 return {
464 "action": self.action,
465 "from": self.value_from.as_json()["value"],
466 "to": self.value_to.as_json()["value"],
467 "currency": self.currency,
468 "base_currency": self.base_currency,
469 }
470
dd359bc0 471 def __repr__(self):
f9226903
IB
472 if self.closed and not self.is_fullfiled:
473 closed = " ❌"
474 elif self.is_fullfiled:
475 closed = " ✔"
476 else:
477 closed = ""
478
479 return "Trade({} -> {} in {}, {}{})".format(
dd359bc0
IB
480 self.value_from,
481 self.value_to,
482 self.currency,
f9226903
IB
483 self.action,
484 closed)
dd359bc0 485
3d0247f9 486 def print_with_order(self, ind=""):
f86ee140 487 self.market.report.print_log("{}{}".format(ind, self))
272b3cfb 488 for order in self.orders:
f86ee140 489 self.market.report.print_log("{}\t{}".format(ind, order))
c31df868 490 for mouvement in order.mouvements:
f86ee140 491 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
dd359bc0 492
272b3cfb 493class Order:
006a2084 494 def __init__(self, action, amount, rate, base_currency, trade_type, market,
80cdd672 495 trade, close_if_possible=False):
dd359bc0
IB
496 self.action = action
497 self.amount = amount
498 self.rate = rate
499 self.base_currency = base_currency
a9950fd0 500 self.market = market
350ed24d 501 self.trade_type = trade_type
80cdd672
IB
502 self.results = []
503 self.mouvements = []
a9950fd0 504 self.status = "pending"
80cdd672 505 self.trade = trade
006a2084 506 self.close_if_possible = close_if_possible
5a72ded7 507 self.id = None
f70bb858 508 self.tries = 0
dd359bc0 509
3d0247f9
IB
510 def as_json(self):
511 return {
512 "action": self.action,
513 "trade_type": self.trade_type,
514 "amount": self.amount.as_json()["value"],
515 "currency": self.amount.as_json()["currency"],
516 "base_currency": self.base_currency,
517 "rate": self.rate,
518 "status": self.status,
519 "close_if_possible": self.close_if_possible,
520 "id": self.id,
521 "mouvements": list(map(lambda x: x.as_json(), self.mouvements))
522 }
523
dd359bc0 524 def __repr__(self):
006a2084 525 return "Order({} {} {} at {} {} [{}]{})".format(
dd359bc0 526 self.action,
350ed24d 527 self.trade_type,
dd359bc0
IB
528 self.amount,
529 self.rate,
530 self.base_currency,
006a2084
IB
531 self.status,
532 " ✂" if self.close_if_possible else "",
dd359bc0
IB
533 )
534
350ed24d
IB
535 @property
536 def account(self):
537 if self.trade_type == "long":
538 return "exchange"
539 else:
540 return "margin"
541
5a72ded7
IB
542 @property
543 def open(self):
544 return self.status == "open"
545
a9950fd0
IB
546 @property
547 def pending(self):
548 return self.status == "pending"
549
550 @property
551 def finished(self):
fd8afa51 552 return self.status == "closed" or self.status == "canceled" or self.status == "error"
a9950fd0 553
f70bb858 554 @retry(InsufficientFunds)
80cdd672 555 def run(self):
f70bb858 556 self.tries += 1
dd359bc0 557 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
f86ee140 558 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
dd359bc0 559
f86ee140
IB
560 if self.market.debug:
561 self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
ecba1113 562 symbol, self.action, amount, self.rate, self.account))
80cdd672 563 self.results.append({"debug": True, "id": -1})
dd359bc0 564 else:
f70bb858 565 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
dd359bc0 566 try:
f86ee140 567 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
f70bb858 568 except InvalidOrder:
df9e4e7f
IB
569 # Impossible to honor the order (dust amount)
570 self.status = "closed"
571 self.mark_finished_order()
572 return
f70bb858
IB
573 except InsufficientFunds as e:
574 if self.tries < 5:
575 self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
576 self.amount = self.amount * D("0.99")
577 raise e
578 else:
579 self.market.report.log_error(action, message="Giving up {}".format(self), exception=e)
580 self.status = "error"
581 return
fd8afa51
IB
582 except Exception as e:
583 self.status = "error"
f86ee140 584 self.market.report.log_error(action, exception=e)
5a72ded7
IB
585 return
586 self.id = self.results[0]["id"]
587 self.status = "open"
dd359bc0 588
a9950fd0 589 def get_status(self):
f86ee140
IB
590 if self.market.debug:
591 self.market.report.log_debug_action("Getting {} status".format(self))
80cdd672 592 return self.status
dd359bc0 593 # other states are "closed" and "canceled"
5a72ded7 594 if not self.finished:
80cdd672 595 self.fetch()
5a72ded7 596 if self.finished:
80cdd672 597 self.mark_finished_order()
dd359bc0
IB
598 return self.status
599
80cdd672 600 def mark_finished_order(self):
f86ee140
IB
601 if self.market.debug:
602 self.market.report.log_debug_action("Mark {} as finished".format(self))
80cdd672
IB
603 return
604 if self.status == "closed":
006a2084 605 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
f86ee140 606 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
006a2084 607
aca4d437 608 def fetch(self):
f86ee140
IB
609 if self.market.debug:
610 self.market.report.log_debug_action("Fetching {}".format(self))
3d0247f9 611 return
aca4d437 612 try:
f9226903 613 result = self.market.ccxt.fetch_order(self.id)
aca4d437
IB
614 self.results.append(result)
615 self.status = result["status"]
616 # Time at which the order started
617 self.timestamp = result["datetime"]
618 except OrderNotCached:
619 self.status = "closed_unknown"
5a72ded7 620
80cdd672
IB
621 self.fetch_mouvements()
622
623 # FIXME: consider open order with dust remaining as closed
624
80cdd672 625 def dust_amount_remaining(self):
5a72ded7 626 return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
80cdd672 627
80cdd672 628 def remaining_amount(self):
1aa7d4fa 629 return self.amount - self.filled_amount()
80cdd672 630
1aa7d4fa 631 def filled_amount(self, in_base_currency=False):
80cdd672
IB
632 if self.status == "open":
633 self.fetch()
1aa7d4fa 634 filled_amount = 0
80cdd672 635 for mouvement in self.mouvements:
1aa7d4fa
IB
636 if in_base_currency:
637 filled_amount += mouvement.total_in_base
638 else:
639 filled_amount += mouvement.total
80cdd672
IB
640 return filled_amount
641
642 def fetch_mouvements(self):
df9e4e7f 643 try:
f86ee140 644 mouvements = self.market.ccxt.privatePostReturnOrderTrades({"orderNumber": self.id})
df9e4e7f
IB
645 except ExchangeError:
646 mouvements = []
80cdd672
IB
647 self.mouvements = []
648
649 for mouvement_hash in mouvements:
650 self.mouvements.append(Mouvement(self.amount.currency,
651 self.base_currency, mouvement_hash))
006a2084 652
272b3cfb 653 def cancel(self):
f86ee140
IB
654 if self.market.debug:
655 self.market.report.log_debug_action("Mark {} as cancelled".format(self))
80cdd672
IB
656 self.status = "canceled"
657 return
f9226903
IB
658 if self.open and self.id is not None:
659 try:
660 self.market.ccxt.cancel_order(self.id)
661 except OrderNotFound as e: # Closed inbetween
662 self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e)
663 self.fetch()
80cdd672
IB
664
665class Mouvement:
666 def __init__(self, currency, base_currency, hash_):
667 self.currency = currency
668 self.base_currency = base_currency
df9e4e7f
IB
669 self.id = hash_.get("tradeID")
670 self.action = hash_.get("type")
671 self.fee_rate = D(hash_.get("fee", -1))
672 try:
673 self.date = datetime.strptime(hash_.get("date", ""), '%Y-%m-%d %H:%M:%S')
674 except ValueError:
675 self.date = None
676 self.rate = D(hash_.get("rate", 0))
677 self.total = Amount(currency, hash_.get("amount", 0))
80cdd672 678 # rate * total = total_in_base
df9e4e7f 679 self.total_in_base = Amount(base_currency, hash_.get("total", 0))
272b3cfb 680
3d0247f9
IB
681 def as_json(self):
682 return {
683 "fee_rate": self.fee_rate,
684 "date": self.date,
685 "action": self.action,
686 "total": self.total.value,
687 "currency": self.currency,
688 "total_in_base": self.total_in_base.value,
689 "base_currency": self.base_currency
690 }
691
c31df868
IB
692 def __repr__(self):
693 if self.fee_rate > 0:
694 fee_rate = " fee: {}%".format(self.fee_rate * 100)
695 else:
696 fee_rate = ""
697 if self.date is None:
698 date = "No date"
699 else:
700 date = self.date
701 return "Mouvement({} ; {} {} ({}){})".format(
702 date, self.action, self.total, self.total_in_base,
703 fee_rate)
704