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