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