diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-25 22:04:02 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-25 22:04:02 +0200 |
commit | bfe841c557094afad2db0d2c63deadeea4ba63c6 (patch) | |
tree | dfade60890ffe5529dc80fec0b23702b97ea0383 /portfolio.py | |
parent | bd7ba362442f27fe3f53729a0040f2473b85a068 (diff) | |
parent | d004a2a5e15a78991870dcb90cd6db63ab40a4e6 (diff) | |
download | Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.gz Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.zst Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.zip |
Merge branch 'dev'v1.0
Diffstat (limited to 'portfolio.py')
-rw-r--r-- | portfolio.py | 158 |
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 @@ | |||
1 | import time | 1 | from datetime import datetime |
2 | from datetime import datetime, timedelta | ||
3 | from decimal import Decimal as D, ROUND_DOWN | ||
4 | from json import JSONDecodeError | ||
5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | ||
6 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound | ||
7 | from retry import retry | 2 | from retry import retry |
8 | import requests | 3 | from decimal import Decimal as D, ROUND_DOWN |
9 | 4 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout | |
10 | # FIXME: correctly handle web call timeouts | ||
11 | |||
12 | class 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 | ||
85 | class Computation: | 6 | class 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 | ||
415 | class RetryException(Exception): | ||
416 | pass | ||
417 | |||
494 | class Order: | 418 | class 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 | |||
665 | class Mouvement: | 651 | class 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 |