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