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