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