aboutsummaryrefslogtreecommitdiff
path: root/portfolio.py
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 22:04:02 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 22:04:02 +0200
commitbfe841c557094afad2db0d2c63deadeea4ba63c6 (patch)
treedfade60890ffe5529dc80fec0b23702b97ea0383 /portfolio.py
parentbd7ba362442f27fe3f53729a0040f2473b85a068 (diff)
parentd004a2a5e15a78991870dcb90cd6db63ab40a4e6 (diff)
downloadTrader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.gz
Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.zst
Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.zip
Merge branch 'dev'v1.0
Diffstat (limited to 'portfolio.py')
-rw-r--r--portfolio.py158
1 files changed, 72 insertions, 86 deletions
diff --git a/portfolio.py b/portfolio.py
index ed50b57..9c58676 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -1,86 +1,7 @@
1import time 1from datetime import datetime
2from datetime import datetime, timedelta
3from decimal import Decimal as D, ROUND_DOWN
4from json import JSONDecodeError
5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound
7from retry import retry 2from retry import retry
8import requests 3from decimal import Decimal as D, ROUND_DOWN
9 4from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout
10# FIXME: correctly handle web call timeouts
11
12class Portfolio:
13 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
14 liquidities = {}
15 data = None
16 last_date = None
17
18 @classmethod
19 def wait_for_recent(cls, market, delta=4):
20 cls.repartition(market, refetch=True)
21 while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta):
22 time.sleep(30)
23 market.report.print_log("Attempt to fetch up-to-date cryptoportfolio")
24 cls.repartition(market, refetch=True)
25
26 @classmethod
27 def repartition(cls, market, liquidity="medium", refetch=False):
28 cls.parse_cryptoportfolio(market, refetch=refetch)
29 liquidities = cls.liquidities[liquidity]
30 return liquidities[cls.last_date]
31
32 @classmethod
33 def get_cryptoportfolio(cls, market):
34 try:
35 r = requests.get(cls.URL)
36 market.report.log_http_request(r.request.method,
37 r.request.url, r.request.body, r.request.headers, r)
38 except Exception as e:
39 market.report.log_error("get_cryptoportfolio", exception=e)
40 return
41 try:
42 cls.data = r.json(parse_int=D, parse_float=D)
43 except (JSONDecodeError, SimpleJSONDecodeError):
44 cls.data = None
45
46 @classmethod
47 def parse_cryptoportfolio(cls, market, refetch=False):
48 if refetch or cls.data is None:
49 cls.get_cryptoportfolio(market)
50
51 def filter_weights(weight_hash):
52 if weight_hash[1][0] == 0:
53 return False
54 if weight_hash[0] == "_row":
55 return False
56 return True
57
58 def clean_weights(i):
59 def clean_weights_(h):
60 if h[0].endswith("s"):
61 return [h[0][0:-1], (h[1][i], "short")]
62 else:
63 return [h[0], (h[1][i], "long")]
64 return clean_weights_
65
66 def parse_weights(portfolio_hash):
67 weights_hash = portfolio_hash["weights"]
68 weights = {}
69 for i in range(len(weights_hash["_row"])):
70 date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d")
71 weights[date] = dict(filter(
72 filter_weights,
73 map(clean_weights(i), weights_hash.items())))
74 return weights
75
76 high_liquidity = parse_weights(cls.data["portfolio_1"])
77 medium_liquidity = parse_weights(cls.data["portfolio_2"])
78
79 cls.liquidities = {
80 "medium": medium_liquidity,
81 "high": high_liquidity,
82 }
83 cls.last_date = max(max(medium_liquidity.keys()), max(high_liquidity.keys()))
84 5
85class Computation: 6class Computation:
86 computations = { 7 computations = {
@@ -491,6 +412,9 @@ class Trade:
491 for mouvement in order.mouvements: 412 for mouvement in order.mouvements:
492 self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) 413 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
493 414
415class RetryException(Exception):
416 pass
417
494class Order: 418class Order:
495 def __init__(self, action, amount, rate, base_currency, trade_type, market, 419 def __init__(self, action, amount, rate, base_currency, trade_type, market,
496 trade, close_if_possible=False): 420 trade, close_if_possible=False):
@@ -507,6 +431,7 @@ class Order:
507 self.close_if_possible = close_if_possible 431 self.close_if_possible = close_if_possible
508 self.id = None 432 self.id = None
509 self.tries = 0 433 self.tries = 0
434 self.start_date = None
510 435
511 def as_json(self): 436 def as_json(self):
512 return { 437 return {
@@ -552,18 +477,18 @@ class Order:
552 def finished(self): 477 def finished(self):
553 return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" 478 return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
554 479
555 @retry(InsufficientFunds) 480 @retry((InsufficientFunds, RetryException))
556 def run(self): 481 def run(self):
557 self.tries += 1 482 self.tries += 1
558 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 483 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
559 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value 484 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
560 485
486 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
561 if self.market.debug: 487 if self.market.debug:
562 self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( 488 self.market.report.log_debug_action(action)
563 symbol, self.action, amount, self.rate, self.account))
564 self.results.append({"debug": True, "id": -1}) 489 self.results.append({"debug": True, "id": -1})
565 else: 490 else:
566 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) 491 self.start_date = datetime.now()
567 try: 492 try:
568 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) 493 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
569 except InvalidOrder: 494 except InvalidOrder:
@@ -571,6 +496,19 @@ class Order:
571 self.status = "closed" 496 self.status = "closed"
572 self.mark_finished_order() 497 self.mark_finished_order()
573 return 498 return
499 except RequestTimeout as e:
500 if not self.retrieve_order():
501 if self.tries < 5:
502 self.market.report.log_error(action, message="Retrying after timeout", exception=e)
503 # We make a specific call in case retrieve_order
504 # would raise itself
505 raise RetryException
506 else:
507 self.market.report.log_error(action, message="Giving up {} after timeouts".format(self), exception=e)
508 self.status = "error"
509 return
510 else:
511 self.market.report.log_error(action, message="Timeout, found the order")
574 except InsufficientFunds as e: 512 except InsufficientFunds as e:
575 if self.tries < 5: 513 if self.tries < 5:
576 self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) 514 self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
@@ -662,6 +600,54 @@ class Order:
662 self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e) 600 self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e)
663 self.fetch() 601 self.fetch()
664 602
603 def retrieve_order(self):
604 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
605 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
606 start_timestamp = self.start_date.timestamp() - 5
607
608 similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp)
609 for order in similar_open_orders:
610 if (order["info"]["margin"] == 1 and self.account == "exchange") or\
611 (order["info"]["margin"] != 1 and self.account == "margin"):
612 i_m_tested = True # coverage bug ?!
613 continue
614 if order["info"]["side"] != self.action:
615 continue
616 amount_diff = round(
617 abs(D(order["info"]["startingAmount"]) - amount),
618 self.market.ccxt.order_precision(symbol))
619 rate_diff = round(
620 abs(D(order["info"]["rate"]) - self.rate),
621 self.market.ccxt.order_precision(symbol))
622 if amount_diff != 0 or rate_diff != 0:
623 continue
624 self.results.append({"id": order["id"]})
625 return True
626
627 similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp)
628 # FIXME: use set instead of sorted(list(...))
629 for order_id in sorted(list(map(lambda x: x["order"], similar_trades))):
630 trades = list(filter(lambda x: x["order"] == order_id, similar_trades))
631 if any(x["timestamp"] < start_timestamp for x in trades):
632 continue
633 if any(x["side"] != self.action for x in trades):
634 continue
635 if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\
636 any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades):
637 continue
638 trade_sum = sum(D(x["info"]["amount"]) for x in trades)
639 amount_diff = round(abs(trade_sum - amount),
640 self.market.ccxt.order_precision(symbol))
641 if amount_diff != 0:
642 continue
643 if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\
644 (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)):
645 continue
646 self.results.append({"id": order_id})
647 return True
648
649 return False
650
665class Mouvement: 651class Mouvement:
666 def __init__(self, currency, base_currency, hash_): 652 def __init__(self, currency, base_currency, hash_):
667 self.currency = currency 653 self.currency = currency