aboutsummaryrefslogtreecommitdiff
path: root/portfolio.py
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-01 13:14:41 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-01 13:14:41 +0100
commitaca4d4372553110ab5d76740ff536de83d5617b2 (patch)
treea9bfdca4226daf422273da97a9e139721469c9f1 /portfolio.py
parent2033e7fef780298be2ec15455a0ec1d26515de55 (diff)
downloadTrader-aca4d4372553110ab5d76740ff536de83d5617b2.tar.gz
Trader-aca4d4372553110ab5d76740ff536de83d5617b2.tar.zst
Trader-aca4d4372553110ab5d76740ff536de83d5617b2.zip
Various fixes/improvementsv0.3
- Use pending gains to compute the move_balance - Use ttl_cache for tickers
Diffstat (limited to 'portfolio.py')
-rw-r--r--portfolio.py99
1 files changed, 52 insertions, 47 deletions
diff --git a/portfolio.py b/portfolio.py
index eb3390e..b77850b 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4from json import JSONDecodeError 4from json import JSONDecodeError
5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError 5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder 6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached
7from retry import retry 7from retry import retry
8import requests 8import requests
9 9
@@ -226,8 +226,8 @@ class Amount:
226 226
227class Balance: 227class Balance:
228 base_keys = ["total", "exchange_total", "exchange_used", 228 base_keys = ["total", "exchange_total", "exchange_used",
229 "exchange_free", "margin_total", "margin_borrowed", 229 "exchange_free", "margin_total", "margin_in_position",
230 "margin_free"] 230 "margin_available", "margin_borrowed", "margin_pending_gain"]
231 231
232 def __init__(self, currency, hash_): 232 def __init__(self, currency, hash_):
233 self.currency = currency 233 self.currency = currency
@@ -240,8 +240,8 @@ class Balance:
240 base_currency = hash_["margin_borrowed_base_currency"] 240 base_currency = hash_["margin_borrowed_base_currency"]
241 for key in [ 241 for key in [
242 "margin_liquidation_price", 242 "margin_liquidation_price",
243 "margin_pending_gain",
244 "margin_lending_fees", 243 "margin_lending_fees",
244 "margin_pending_base_gain",
245 "margin_borrowed_base_price" 245 "margin_borrowed_base_price"
246 ]: 246 ]:
247 setattr(self, key, Amount(base_currency, hash_.get(key, 0))) 247 setattr(self, key, Amount(base_currency, hash_.get(key, 0)))
@@ -261,12 +261,12 @@ class Balance:
261 exchange = "" 261 exchange = ""
262 262
263 if self.margin_total > 0: 263 if self.margin_total > 0:
264 if self.margin_free != 0 and self.margin_borrowed != 0: 264 if self.margin_available != 0 and self.margin_in_position != 0:
265 margin = " Margin: [✔{} + borrowed {} = {}]".format(str(self.margin_free), str(self.margin_borrowed), str(self.margin_total)) 265 margin = " Margin: [✔{} + ❌{} = {}]".format(str(self.margin_available), str(self.margin_in_position), str(self.margin_total))
266 elif self.margin_free != 0: 266 elif self.margin_available != 0:
267 margin = " Margin: [✔{}]".format(str(self.margin_free)) 267 margin = " Margin: [✔{}]".format(str(self.margin_available))
268 else: 268 else:
269 margin = " Margin: [borrowed {}]".format(str(self.margin_borrowed)) 269 margin = " Margin: [{}]".format(str(self.margin_in_position))
270 elif self.margin_total < 0: 270 elif self.margin_total < 0:
271 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total), 271 margin = " Margin: [{} @@ {}/{}]".format(str(self.margin_total),
272 str(self.margin_borrowed_base_price), 272 str(self.margin_borrowed_base_price),
@@ -290,6 +290,7 @@ class Trade:
290 self.value_to = value_to 290 self.value_to = value_to
291 self.orders = [] 291 self.orders = []
292 self.market = market 292 self.market = market
293 assert self.value_from.value * self.value_to.value >= 0
293 assert self.value_from.currency == self.value_to.currency 294 assert self.value_from.currency == self.value_to.currency
294 if self.value_from != 0: 295 if self.value_from != 0:
295 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency 296 assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
@@ -298,6 +299,10 @@ class Trade:
298 self.base_currency = self.value_from.currency 299 self.base_currency = self.value_from.currency
299 300
300 @property 301 @property
302 def delta(self):
303 return self.value_to - self.value_from
304
305 @property
301 def action(self): 306 def action(self):
302 if self.value_from == self.value_to: 307 if self.value_from == self.value_to:
303 return None 308 return None
@@ -322,6 +327,10 @@ class Trade:
322 else: 327 else:
323 return "long" 328 return "long"
324 329
330 @property
331 def is_fullfiled(self):
332 return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
333
325 def filled_amount(self, in_base_currency=False): 334 def filled_amount(self, in_base_currency=False):
326 filled_amount = 0 335 filled_amount = 0
327 for order in self.orders: 336 for order in self.orders:
@@ -329,34 +338,36 @@ class Trade:
329 return filled_amount 338 return filled_amount
330 339
331 def update_order(self, order, tick): 340 def update_order(self, order, tick):
332 new_order = None 341 actions = {
333 if tick in [0, 1, 3, 4, 6]: 342 0: ["waiting", None],
343 1: ["waiting", None],
344 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
345 3: ["waiting", None],
346 4: ["waiting", None],
347 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
348 6: ["waiting", None],
349 7: ["market_fallback", "default"],
350 }
351
352 if tick in actions:
353 update, compute_value = actions[tick]
354 elif tick % 3 == 1:
355 update = "market_adjust"
356 compute_value = "default"
357 else:
334 update = "waiting" 358 update = "waiting"
335 compute_value = None 359 compute_value = None
336 elif tick == 2: 360
337 update = "adjusting" 361 if compute_value is not None:
338 compute_value = 'lambda x, y: (x[y] + x["average"]) / 2' 362 order.cancel()
339 new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2) 363 new_order = self.prepare_order(compute_value=compute_value)
340 elif tick ==5: 364 else:
341 update = "adjusting" 365 new_order = None
342 compute_value = 'lambda x, y: (x[y]*2 + x["average"]) / 3'
343 new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
344 elif tick >= 7:
345 if (tick - 7) % 3 == 0:
346 new_order = self.prepare_order(compute_value="default")
347 update = "market_adjust"
348 compute_value = "default"
349 else:
350 update = "waiting"
351 compute_value = None
352 if tick == 7:
353 update = "market_fallback"
354 366
355 self.market.report.log_order(order, tick, update=update, 367 self.market.report.log_order(order, tick, update=update,
356 compute_value=compute_value, new_order=new_order) 368 compute_value=compute_value, new_order=new_order)
357 369
358 if new_order is not None: 370 if new_order is not None:
359 order.cancel()
360 new_order.run() 371 new_order.run()
361 self.market.report.log_order(order, tick, new_order=new_order) 372 self.market.report.log_order(order, tick, new_order=new_order)
362 373
@@ -369,10 +380,9 @@ class Trade:
369 ticker = ticker["original"] 380 ticker = ticker["original"]
370 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 381 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
371 382
372 #TODO: store when the order is considered filled
373 # FIXME: Dust amount should be removed from there if they werent 383 # FIXME: Dust amount should be removed from there if they werent
374 # honored in other sales 384 # honored in other sales
375 delta_in_base = abs(self.value_from - self.value_to) 385 delta_in_base = abs(self.delta)
376 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 386 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
377 387
378 if not inverted: 388 if not inverted:
@@ -477,7 +487,6 @@ class Order:
477 self.trade = trade 487 self.trade = trade
478 self.close_if_possible = close_if_possible 488 self.close_if_possible = close_if_possible
479 self.id = None 489 self.id = None
480 self.fetch_cache_timestamp = None
481 self.tries = 0 490 self.tries = 0
482 491
483 def as_json(self): 492 def as_json(self):
@@ -578,21 +587,19 @@ class Order:
578 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: 587 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
579 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency) 588 self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
580 589
581 def fetch(self, force=False): 590 def fetch(self):
582 if self.market.debug: 591 if self.market.debug:
583 self.market.report.log_debug_action("Fetching {}".format(self)) 592 self.market.report.log_debug_action("Fetching {}".format(self))
584 return 593 return
585 if (not force and self.fetch_cache_timestamp is not None 594 try:
586 and time.time() - self.fetch_cache_timestamp < 10): 595 result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency)
587 return 596 self.results.append(result)
588 self.fetch_cache_timestamp = time.time() 597 self.status = result["status"]
589 598 # Time at which the order started
590 result = self.market.ccxt.fetch_order(self.id, symbol=self.amount.currency) 599 self.timestamp = result["datetime"]
591 self.results.append(result) 600 except OrderNotCached:
601 self.status = "closed_unknown"
592 602
593 self.status = result["status"]
594 # Time at which the order started
595 self.timestamp = result["datetime"]
596 self.fetch_mouvements() 603 self.fetch_mouvements()
597 604
598 # FIXME: consider open order with dust remaining as closed 605 # FIXME: consider open order with dust remaining as closed
@@ -601,8 +608,6 @@ class Order:
601 return self.remaining_amount() < Amount(self.amount.currency, D("0.001")) 608 return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
602 609
603 def remaining_amount(self): 610 def remaining_amount(self):
604 if self.status == "open":
605 self.fetch()
606 return self.amount - self.filled_amount() 611 return self.amount - self.filled_amount()
607 612
608 def filled_amount(self, in_base_currency=False): 613 def filled_amount(self, in_base_currency=False):
@@ -633,7 +638,7 @@ class Order:
633 self.status = "canceled" 638 self.status = "canceled"
634 return 639 return
635 self.market.ccxt.cancel_order(self.id) 640 self.market.ccxt.cancel_order(self.id)
636 self.fetch(force=True) 641 self.fetch()
637 642
638class Mouvement: 643class Mouvement:
639 def __init__(self, currency, base_currency, hash_): 644 def __init__(self, currency, base_currency, hash_):