]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - portfolio.py
Move Portfolio to store and cleanup methods
[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
aca4d437 217 assert self.value_from.value * self.value_to.value >= 0
dd359bc0 218 assert self.value_from.currency == self.value_to.currency
006a2084
IB
219 if self.value_from != 0:
220 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
221 elif self.value_from.linked_to is None:
222 self.value_from.linked_to = Amount(self.currency, 0)
dd359bc0
IB
223 self.base_currency = self.value_from.currency
224
aca4d437
IB
225 @property
226 def delta(self):
227 return self.value_to - self.value_from
228
dd359bc0
IB
229 @property
230 def action(self):
231 if self.value_from == self.value_to:
232 return None
233 if self.base_currency == self.currency:
234 return None
235
5a72ded7 236 if abs(self.value_from) < abs(self.value_to):
006a2084 237 return "acquire"
dd359bc0 238 else:
006a2084 239 return "dispose"
dd359bc0 240
cfab619d 241 def order_action(self, inverted):
006a2084 242 if (self.value_from < self.value_to) != inverted:
350ed24d 243 return "buy"
dd359bc0 244 else:
350ed24d 245 return "sell"
dd359bc0 246
006a2084
IB
247 @property
248 def trade_type(self):
249 if self.value_from + self.value_to < 0:
250 return "short"
251 else:
252 return "long"
253
17598517
IB
254 @property
255 def pending(self):
256 return not (self.is_fullfiled or self.closed)
257
258 def close(self):
f9226903
IB
259 for order in self.orders:
260 order.cancel()
17598517
IB
261 self.closed = True
262
aca4d437
IB
263 @property
264 def is_fullfiled(self):
265 return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
266
1aa7d4fa 267 def filled_amount(self, in_base_currency=False):
80cdd672
IB
268 filled_amount = 0
269 for order in self.orders:
1aa7d4fa 270 filled_amount += order.filled_amount(in_base_currency=in_base_currency)
80cdd672
IB
271 return filled_amount
272
273 def update_order(self, order, tick):
aca4d437
IB
274 actions = {
275 0: ["waiting", None],
276 1: ["waiting", None],
277 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
278 3: ["waiting", None],
279 4: ["waiting", None],
280 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
281 6: ["waiting", None],
282 7: ["market_fallback", "default"],
283 }
284
285 if tick in actions:
286 update, compute_value = actions[tick]
287 elif tick % 3 == 1:
288 update = "market_adjust"
289 compute_value = "default"
290 else:
3d0247f9
IB
291 update = "waiting"
292 compute_value = None
aca4d437
IB
293
294 if compute_value is not None:
295 order.cancel()
296 new_order = self.prepare_order(compute_value=compute_value)
297 else:
298 new_order = None
3d0247f9 299
f86ee140 300 self.market.report.log_order(order, tick, update=update,
3d0247f9 301 compute_value=compute_value, new_order=new_order)
80cdd672
IB
302
303 if new_order is not None:
80cdd672 304 new_order.run()
f86ee140 305 self.market.report.log_order(order, tick, new_order=new_order)
80cdd672 306
2033e7fe 307 def prepare_order(self, close_if_possible=None, compute_value="default"):
dd359bc0 308 if self.action is None:
5a72ded7 309 return None
f86ee140 310 ticker = self.market.get_ticker(self.currency, self.base_currency)
dd359bc0 311 inverted = ticker["inverted"]
f2097d71
IB
312 if inverted:
313 ticker = ticker["original"]
6ca5a1ec 314 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
f2097d71 315
97922ff1
IB
316 # FIXME: Dust amount should be removed from there if they werent
317 # honored in other sales
aca4d437 318 delta_in_base = abs(self.delta)
c11e4274 319 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
dd359bc0
IB
320
321 if not inverted:
1aa7d4fa 322 base_currency = self.base_currency
350ed24d 323 # BTC
006a2084 324 if self.action == "dispose":
1aa7d4fa
IB
325 filled = self.filled_amount(in_base_currency=False)
326 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
327 # I have 10 BTC worth of FOO, and I want to sell 9 BTC
328 # worth of it, computed first with rate 10 FOO = 1 BTC.
329 # -> I "sell" "90" FOO at proposed rate "rate".
330
331 delta = delta - filled
332 # I already sold 60 FOO, 30 left
f2097d71 333 else:
1aa7d4fa
IB
334 filled = self.filled_amount(in_base_currency=True)
335 delta = (delta_in_base - filled).in_currency(self.currency, self.market, rate=1/rate)
336 # I want to buy 9 BTC worth of FOO, computed with rate
337 # 10 FOO = 1 BTC
338 # -> I "buy" "9 / rate" FOO at proposed rate "rate"
339
340 # I already bought 3 / rate FOO, 6 / rate left
dd359bc0 341 else:
1aa7d4fa 342 base_currency = self.currency
c11e4274 343 # FOO
1aa7d4fa
IB
344 if self.action == "dispose":
345 filled = self.filled_amount(in_base_currency=True)
346 # Base is FOO
347
348 delta = (delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
349 - filled).in_currency(self.base_currency, self.market, rate=1/rate)
350 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
351 # computed at rate 1 Foo = 0.01 BTC
352 # Computation says I should sell it at 125 FOO / BTC
353 # -> delta_in_base = 9 BTC
354 # -> delta = (9 * 1/0.01 FOO) * 1/125 = 7.2 BTC
355 # Action: "buy" "7.2 BTC" at rate "125" "FOO" on market
356
357 # I already bought 300/125 BTC, only 600/125 left
358 else:
359 filled = self.filled_amount(in_base_currency=False)
360 # Base is FOO
361
362 delta = delta_in_base
363 # I have 1 BTC worth of FOO, and I want to buy 9 BTC worth of it
364 # At rate 100 Foo / BTC
365 # Computation says I should buy it at 125 FOO / BTC
366 # -> delta_in_base = 9 BTC
367 # Action: "sell" "9 BTC" at rate "125" "FOO" on market
368
369 delta = delta - filled
370 # I already sold 4 BTC, only 5 left
dd359bc0 371
2033e7fe
IB
372 if close_if_possible is None:
373 close_if_possible = (self.value_to == 0)
006a2084 374
1aa7d4fa 375 if delta <= 0:
f86ee140 376 self.market.report.log_error("prepare_order", message="Less to do than already filled: {}".format(delta))
5a72ded7 377 return None
80cdd672 378
5a72ded7 379 order = Order(self.order_action(inverted),
1aa7d4fa 380 delta, rate, base_currency, self.trade_type,
5a72ded7
IB
381 self.market, self, close_if_possible=close_if_possible)
382 self.orders.append(order)
383 return order
dd359bc0 384
3d0247f9
IB
385 def as_json(self):
386 return {
387 "action": self.action,
388 "from": self.value_from.as_json()["value"],
389 "to": self.value_to.as_json()["value"],
390 "currency": self.currency,
391 "base_currency": self.base_currency,
392 }
393
dd359bc0 394 def __repr__(self):
f9226903
IB
395 if self.closed and not self.is_fullfiled:
396 closed = " ❌"
397 elif self.is_fullfiled:
398 closed = " ✔"
399 else:
400 closed = ""
401
402 return "Trade({} -> {} in {}, {}{})".format(
dd359bc0
IB
403 self.value_from,
404 self.value_to,
405 self.currency,
f9226903
IB
406 self.action,
407 closed)
dd359bc0 408
3d0247f9 409 def print_with_order(self, ind=""):
f86ee140 410 self.market.report.print_log("{}{}".format(ind, self))
272b3cfb 411 for order in self.orders:
f86ee140 412 self.market.report.print_log("{}\t{}".format(ind, order))
c31df868 413 for mouvement in order.mouvements:
f86ee140 414 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
dd359bc0 415
272b3cfb 416class Order:
006a2084 417 def __init__(self, action, amount, rate, base_currency, trade_type, market,
80cdd672 418 trade, close_if_possible=False):
dd359bc0
IB
419 self.action = action
420 self.amount = amount
421 self.rate = rate
422 self.base_currency = base_currency
a9950fd0 423 self.market = market
350ed24d 424 self.trade_type = trade_type
80cdd672
IB
425 self.results = []
426 self.mouvements = []
a9950fd0 427 self.status = "pending"
80cdd672 428 self.trade = trade
006a2084 429 self.close_if_possible = close_if_possible
5a72ded7 430 self.id = None
f70bb858 431 self.tries = 0
dd359bc0 432
3d0247f9
IB
433 def as_json(self):
434 return {
435 "action": self.action,
436 "trade_type": self.trade_type,
437 "amount": self.amount.as_json()["value"],
438 "currency": self.amount.as_json()["currency"],
439 "base_currency": self.base_currency,
440 "rate": self.rate,
441 "status": self.status,
442 "close_if_possible": self.close_if_possible,
443 "id": self.id,
444 "mouvements": list(map(lambda x: x.as_json(), self.mouvements))
445 }
446
dd359bc0 447 def __repr__(self):
006a2084 448 return "Order({} {} {} at {} {} [{}]{})".format(
dd359bc0 449 self.action,
350ed24d 450 self.trade_type,
dd359bc0
IB
451 self.amount,
452 self.rate,
453 self.base_currency,
006a2084
IB
454 self.status,
455 " ✂" if self.close_if_possible else "",
dd359bc0
IB
456 )
457
350ed24d
IB
458 @property
459 def account(self):
460 if self.trade_type == "long":
461 return "exchange"
462 else:
463 return "margin"
464
5a72ded7
IB
465 @property
466 def open(self):
467 return self.status == "open"
468
a9950fd0
IB
469 @property
470 def pending(self):
471 return self.status == "pending"
472
473 @property
474 def finished(self):
fd8afa51 475 return self.status == "closed" or self.status == "canceled" or self.status == "error"
a9950fd0 476
f70bb858 477 @retry(InsufficientFunds)
80cdd672 478 def run(self):
f70bb858 479 self.tries += 1
dd359bc0 480 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
f86ee140 481 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
dd359bc0 482
f86ee140
IB
483 if self.market.debug:
484 self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
ecba1113 485 symbol, self.action, amount, self.rate, self.account))
80cdd672 486 self.results.append({"debug": True, "id": -1})
dd359bc0 487 else:
f70bb858 488 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
dd359bc0 489 try:
f86ee140 490 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
f70bb858 491 except InvalidOrder:
df9e4e7f
IB
492 # Impossible to honor the order (dust amount)
493 self.status = "closed"
494 self.mark_finished_order()
495 return
f70bb858
IB
496 except InsufficientFunds as e:
497 if self.tries < 5:
498 self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
499 self.amount = self.amount * D("0.99")
500 raise e
501 else:
502 self.market.report.log_error(action, message="Giving up {}".format(self), exception=e)
503 self.status = "error"
504 return
fd8afa51
IB
505 except Exception as e:
506 self.status = "error"
f86ee140 507 self.market.report.log_error(action, exception=e)
5a72ded7
IB
508 return
509 self.id = self.results[0]["id"]
510 self.status = "open"
dd359bc0 511
a9950fd0 512 def get_status(self):
f86ee140
IB
513 if self.market.debug:
514 self.market.report.log_debug_action("Getting {} status".format(self))
80cdd672 515 return self.status
dd359bc0 516 # other states are "closed" and "canceled"
5a72ded7 517 if not self.finished:
80cdd672 518 self.fetch()
5a72ded7 519 if self.finished:
80cdd672 520 self.mark_finished_order()
dd359bc0
IB
521 return self.status
522
80cdd672 523 def mark_finished_order(self):
f86ee140
IB
524 if self.market.debug:
525 self.market.report.log_debug_action("Mark {} as finished".format(self))
80cdd672
IB
526 return
527 if self.status == "closed":
006a2084 528 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
f86ee140 529 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
006a2084 530
aca4d437 531 def fetch(self):
f86ee140
IB
532 if self.market.debug:
533 self.market.report.log_debug_action("Fetching {}".format(self))
3d0247f9 534 return
aca4d437 535 try:
f9226903 536 result = self.market.ccxt.fetch_order(self.id)
aca4d437
IB
537 self.results.append(result)
538 self.status = result["status"]
539 # Time at which the order started
540 self.timestamp = result["datetime"]
541 except OrderNotCached:
542 self.status = "closed_unknown"
5a72ded7 543
80cdd672
IB
544 self.fetch_mouvements()
545
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