aboutsummaryrefslogtreecommitdiff
path: root/portfolio.py
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-06 15:42:40 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-06 15:45:49 +0100
commit80cdd672da2f0a4997a792bd1a2de19d4f516e5b (patch)
tree15cf19bbf1a7a904b18531ef929bf0851604a0d0 /portfolio.py
parent006a20846236ad365ec814f848f5fbf7e3dc7d3c (diff)
downloadTrader-80cdd672da2f0a4997a792bd1a2de19d4f516e5b.tar.gz
Trader-80cdd672da2f0a4997a792bd1a2de19d4f516e5b.tar.zst
Trader-80cdd672da2f0a4997a792bd1a2de19d4f516e5b.zip
Implement order following
- Change urllib to requests and use it in tests - Add debug mode for sensitive actions - Follow orders when trade is running - Fetch order from the api and store the mouvements already filled - Start rewriting tests
Diffstat (limited to 'portfolio.py')
-rw-r--r--portfolio.py230
1 files changed, 175 insertions, 55 deletions
diff --git a/portfolio.py b/portfolio.py
index 0ab16fd..b3065b8 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -3,6 +3,11 @@ import time
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4# Put your poloniex api key in market.py 4# Put your poloniex api key in market.py
5from market import market 5from market import market
6from json import JSONDecodeError
7import requests
8
9# FIXME: correctly handle web call timeouts
10
6 11
7class Portfolio: 12class Portfolio:
8 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" 13 URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
@@ -18,20 +23,13 @@ class Portfolio:
18 23
19 @classmethod 24 @classmethod
20 def get_cryptoportfolio(cls): 25 def get_cryptoportfolio(cls):
21 import json
22 import urllib3
23 urllib3.disable_warnings()
24 http = urllib3.PoolManager()
25
26 try: 26 try:
27 r = http.request("GET", cls.URL) 27 r = requests.get(cls.URL)
28 except Exception: 28 except Exception:
29 return None 29 return
30 try: 30 try:
31 cls.data = json.loads(r.data, 31 cls.data = r.json(parse_int=D, parse_float=D)
32 parse_int=D, 32 except JSONDecodeError:
33 parse_float=D)
34 except json.JSONDecodeError:
35 cls.data = None 33 cls.data = None
36 34
37 @classmethod 35 @classmethod
@@ -79,9 +77,6 @@ class Amount:
79 self.ticker = ticker 77 self.ticker = ticker
80 self.rate = rate 78 self.rate = rate
81 79
82 self.ticker_cache = {}
83 self.ticker_cache_timestamp = time.time()
84
85 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"): 80 def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
86 if other_currency == self.currency: 81 if other_currency == self.currency:
87 return self 82 return self
@@ -141,9 +136,6 @@ class Amount:
141 def __truediv__(self, value): 136 def __truediv__(self, value):
142 return self.__floordiv__(value) 137 return self.__floordiv__(value)
143 138
144 def __le__(self, other):
145 return self == other or self < other
146
147 def __lt__(self, other): 139 def __lt__(self, other):
148 if other == 0: 140 if other == 0:
149 return self.value < 0 141 return self.value < 0
@@ -151,6 +143,9 @@ class Amount:
151 raise Exception("Comparing amounts must be done with same currencies") 143 raise Exception("Comparing amounts must be done with same currencies")
152 return self.value < other.value 144 return self.value < other.value
153 145
146 def __le__(self, other):
147 return self == other or self < other
148
154 def __gt__(self, other): 149 def __gt__(self, other):
155 return not self <= other 150 return not self <= other
156 151
@@ -192,9 +187,9 @@ class Balance:
192 "margin_total", "margin_borrowed", "margin_free"]: 187 "margin_total", "margin_borrowed", "margin_free"]:
193 setattr(self, key, Amount(currency, hash_.get(key, 0))) 188 setattr(self, key, Amount(currency, hash_.get(key, 0)))
194 189
195 self.margin_position_type = hash_["margin_position_type"] 190 self.margin_position_type = hash_.get("margin_position_type")
196 191
197 if hash_["margin_borrowed_base_currency"] is not None: 192 if hash_.get("margin_borrowed_base_currency") is not None:
198 base_currency = hash_["margin_borrowed_base_currency"] 193 base_currency = hash_["margin_borrowed_base_currency"]
199 for key in [ 194 for key in [
200 "margin_liquidation_price", 195 "margin_liquidation_price",
@@ -227,7 +222,6 @@ class Balance:
227 cls.known_balances[currency] = cls(currency, balance) 222 cls.known_balances[currency] = cls(currency, balance)
228 return cls.known_balances 223 return cls.known_balances
229 224
230
231 @classmethod 225 @classmethod
232 def dispatch_assets(cls, amount, repartition=None): 226 def dispatch_assets(cls, amount, repartition=None):
233 if repartition is None: 227 if repartition is None:
@@ -239,34 +233,34 @@ class Balance:
239 if trade_type == "short": 233 if trade_type == "short":
240 amounts[currency] = - amounts[currency] 234 amounts[currency] = - amounts[currency]
241 if currency not in cls.known_balances: 235 if currency not in cls.known_balances:
242 cls.known_balances[currency] = cls(currency, 0, 0, 0) 236 cls.known_balances[currency] = cls(currency, {})
243 return amounts 237 return amounts
244 238
245 @classmethod 239 @classmethod
246 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"): 240 def prepare_trades(cls, market, base_currency="BTC", compute_value="average", debug=False):
247 cls.fetch_balances(market) 241 cls.fetch_balances(market)
248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 242 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
249 total_base_value = sum(values_in_base.values()) 243 total_base_value = sum(values_in_base.values())
250 new_repartition = cls.dispatch_assets(total_base_value) 244 new_repartition = cls.dispatch_assets(total_base_value)
251 # Recompute it in case we have new currencies 245 # Recompute it in case we have new currencies
252 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 246 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
253 Trade.compute_trades(values_in_base, new_repartition, market=market) 247 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
254 248
255 @classmethod 249 @classmethod
256 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None): 250 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None, debug=False):
257 cls.fetch_balances(market) 251 cls.fetch_balances(market)
258 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 252 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
259 total_base_value = sum(values_in_base.values()) 253 total_base_value = sum(values_in_base.values())
260 new_repartition = cls.dispatch_assets(total_base_value) 254 new_repartition = cls.dispatch_assets(total_base_value)
261 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market) 255 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug)
262 256
263 @classmethod 257 @classmethod
264 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"): 258 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average", debug=False):
265 cls.fetch_balances(market) 259 cls.fetch_balances(market)
266 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 260 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
267 total_base_value = sum(values_in_base.values()) 261 total_base_value = sum(values_in_base.values())
268 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) 262 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
269 Trade.compute_trades(values_in_base, new_repartition, market=market) 263 Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug)
270 264
271 def __repr__(self): 265 def __repr__(self):
272 if self.exchange_total > 0: 266 if self.exchange_total > 0:
@@ -309,6 +303,7 @@ class Computation:
309 } 303 }
310 304
311class Trade: 305class Trade:
306 debug = False
312 trades = [] 307 trades = []
313 308
314 def __init__(self, value_from, value_to, currency, market=None): 309 def __init__(self, value_from, value_to, currency, market=None):
@@ -370,7 +365,8 @@ class Trade:
370 return cls.get_ticker(c1, c2, market) 365 return cls.get_ticker(c1, c2, market)
371 366
372 @classmethod 367 @classmethod
373 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None): 368 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False):
369 cls.debug = cls.debug or debug
374 base_currency = sum(values_in_base.values()).currency 370 base_currency = sum(values_in_base.values()).currency
375 for currency in Balance.currencies(): 371 for currency in Balance.currencies():
376 if currency == base_currency: 372 if currency == base_currency:
@@ -402,7 +398,7 @@ class Trade:
402 trade.prepare_order(compute_value=compute_value) 398 trade.prepare_order(compute_value=compute_value)
403 399
404 @classmethod 400 @classmethod
405 def move_balances(cls, market, debug=False): 401 def move_balances(cls, market):
406 needed_in_margin = {} 402 needed_in_margin = {}
407 for trade in cls.trades: 403 for trade in cls.trades:
408 if trade.trade_type == "short": 404 if trade.trade_type == "short":
@@ -414,12 +410,12 @@ class Trade:
414 delta = (needed - current_balance).value 410 delta = (needed - current_balance).value
415 # FIXME: don't remove too much if there are open margin position 411 # FIXME: don't remove too much if there are open margin position
416 if delta > 0: 412 if delta > 0:
417 if debug: 413 if cls.debug:
418 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta)) 414 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta))
419 else: 415 else:
420 market.transfer_balance(currency, delta, "exchange", "margin") 416 market.transfer_balance(currency, delta, "exchange", "margin")
421 elif delta < 0: 417 elif delta < 0:
422 if debug: 418 if cls.debug:
423 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta)) 419 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta))
424 else: 420 else:
425 market.transfer_balance(currency, -delta, "margin", "exchange") 421 market.transfer_balance(currency, -delta, "margin", "exchange")
@@ -449,6 +445,37 @@ class Trade:
449 else: 445 else:
450 return "long" 446 return "long"
451 447
448 @property
449 def filled_amount(self):
450 filled_amount = 0
451 for order in self.orders:
452 filled_amount += order.filled_amount
453 return filled_amount
454
455 def update_order(self, order, tick):
456 new_order = None
457 if tick in [0, 1, 3, 4, 6]:
458 print("{}, tick {}, waiting".format(order, tick))
459 elif tick == 2:
460 self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2)
461 new_order = self.orders[-1]
462 print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
463 elif tick ==5:
464 self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
465 new_order = self.orders[-1]
466 print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
467 elif tick >= 7:
468 if tick == 7:
469 print("{}, tick {}, fallbacking to market value".format(order, tick))
470 if (tick - 7) % 3 == 0:
471 self.prepare_order(compute_value="default")
472 new_order = self.orders[-1]
473 print("{}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order))
474
475 if new_order is not None:
476 order.cancel()
477 new_order.run()
478
452 def prepare_order(self, compute_value="default"): 479 def prepare_order(self, compute_value="default"):
453 if self.action is None: 480 if self.action is None:
454 return 481 return
@@ -492,12 +519,20 @@ class Trade:
492 # buy: 519 # buy:
493 # I want to buy 9 / 0.1 FOO 520 # I want to buy 9 / 0.1 FOO
494 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market" 521 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
522 if self.value_to == 0:
523 rate = self.value_from.linked_to.value / self.value_from.value
524 # Recompute the rate to avoid any rounding error
495 525
496 close_if_possible = (self.value_to == 0) 526 close_if_possible = (self.value_to == 0)
497 527
528 if delta <= self.filled_amount:
529 print("Less to do than already filled: {} <= {}".format(delta,
530 self.filled_amount))
531 return
532
498 self.orders.append(Order(self.order_action(inverted), 533 self.orders.append(Order(self.order_action(inverted),
499 delta, rate, currency, self.trade_type, self.market, 534 delta - self.filled_amount, rate, currency, self.trade_type,
500 close_if_possible=close_if_possible)) 535 self.market, self, close_if_possible=close_if_possible))
501 536
502 @classmethod 537 @classmethod
503 def compute_value(cls, ticker, action, compute_value="default"): 538 def compute_value(cls, ticker, action, compute_value="default"):
@@ -523,18 +558,19 @@ class Trade:
523 order.run() 558 order.run()
524 559
525 @classmethod 560 @classmethod
526 def follow_orders(cls, verbose=True, sleep=30): 561 def follow_orders(cls, verbose=True, sleep=None):
527 orders = cls.all_orders() 562 if sleep is None:
528 finished_orders = [] 563 sleep = 7 if cls.debug else 30
529 while len(orders) != len(finished_orders): 564 tick = 0
565 while len(cls.all_orders(state="open")) > 0:
530 time.sleep(sleep) 566 time.sleep(sleep)
531 for order in orders: 567 tick += 1
532 if order in finished_orders: 568 for order in cls.all_orders(state="open"):
533 continue
534 if order.get_status() != "open": 569 if order.get_status() != "open":
535 finished_orders.append(order)
536 if verbose: 570 if verbose:
537 print("finished {}".format(order)) 571 print("finished {}".format(order))
572 else:
573 order.trade.update_order(order, tick)
538 if verbose: 574 if verbose:
539 print("All orders finished") 575 print("All orders finished")
540 576
@@ -562,16 +598,19 @@ class Trade:
562 598
563class Order: 599class Order:
564 def __init__(self, action, amount, rate, base_currency, trade_type, market, 600 def __init__(self, action, amount, rate, base_currency, trade_type, market,
565 close_if_possible=False): 601 trade, close_if_possible=False):
566 self.action = action 602 self.action = action
567 self.amount = amount 603 self.amount = amount
568 self.rate = rate 604 self.rate = rate
569 self.base_currency = base_currency 605 self.base_currency = base_currency
570 self.market = market 606 self.market = market
571 self.trade_type = trade_type 607 self.trade_type = trade_type
572 self.result = None 608 self.results = []
609 self.mouvements = []
573 self.status = "pending" 610 self.status = "pending"
611 self.trade = trade
574 self.close_if_possible = close_if_possible 612 self.close_if_possible = close_if_possible
613 self.debug = trade.debug
575 614
576 def __repr__(self): 615 def __repr__(self):
577 return "Order({} {} {} at {} {} [{}]{})".format( 616 return "Order({} {} {} at {} {} [{}]{})".format(
@@ -599,16 +638,22 @@ class Order:
599 def finished(self): 638 def finished(self):
600 return self.status == "closed" or self.status == "canceled" or self.status == "error" 639 return self.status == "closed" or self.status == "canceled" or self.status == "error"
601 640
602 def run(self, debug=False): 641 @property
642 def id(self):
643 return self.results[0]["id"]
644
645 def run(self):
603 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 646 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
604 amount = round(self.amount, self.market.order_precision(symbol)).value 647 amount = round(self.amount, self.market.order_precision(symbol)).value
605 648
606 if debug: 649 if self.debug:
607 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( 650 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
608 symbol, self.action, amount, self.rate, self.account)) 651 symbol, self.action, amount, self.rate, self.account))
652 self.status = "open"
653 self.results.append({"debug": True, "id": -1})
609 else: 654 else:
610 try: 655 try:
611 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account) 656 self.results.append(self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
612 self.status = "open" 657 self.status = "open"
613 except Exception as e: 658 except Exception as e:
614 self.status = "error" 659 self.status = "error"
@@ -618,22 +663,84 @@ class Order:
618 print(self.error_message) 663 print(self.error_message)
619 664
620 def get_status(self): 665 def get_status(self):
666 if self.debug:
667 return self.status
621 # other states are "closed" and "canceled" 668 # other states are "closed" and "canceled"
622 if self.status == "open": 669 if self.status == "open":
623 result = self.market.fetch_order(self.result['id']) 670 self.fetch()
624 if result["status"] != "open": 671 if self.status != "open":
625 self.mark_finished_order(result["status"]) 672 self.mark_finished_order()
626 return self.status 673 return self.status
627 674
628 def mark_finished_order(self, status): 675 def mark_finished_order(self):
629 if status == "closed": 676 if self.debug:
677 return
678 if self.status == "closed":
630 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: 679 if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
631 self.market.close_margin_position(self.amount.currency, self.base_currency) 680 self.market.close_margin_position(self.amount.currency, self.base_currency)
632 681
682 fetch_cache_timestamp = None
683 def fetch(self, force=False):
684 if self.debug or (not force and self.fetch_cache_timestamp is not None
685 and time.time() - self.fetch_cache_timestamp < 10):
686 return
687 self.fetch_cache_timestamp = time.time()
688
689 self.results.append(self.market.fetch_order(self.id))
690 result = self.results[-1]
633 self.status = result["status"] 691 self.status = result["status"]
692 # Time at which the order started
693 self.timestamp = result["datetime"]
694 self.fetch_mouvements()
695
696 # FIXME: consider open order with dust remaining as closed
697
698 @property
699 def dust_amount_remaining(self):
700 return self.remaining_amount < 0.001
701
702 @property
703 def remaining_amount(self):
704 if self.status == "open":
705 self.fetch()
706 return self.amount - self.filled_amount
707
708 @property
709 def filled_amount(self):
710 if self.status == "open":
711 self.fetch()
712 filled_amount = Amount(self.amount.currency, 0)
713 for mouvement in self.mouvements:
714 filled_amount += mouvement.total
715 return filled_amount
716
717 def fetch_mouvements(self):
718 mouvements = self.market.privatePostReturnOrderTrades({"orderNumber": self.id})
719 self.mouvements = []
720
721 for mouvement_hash in mouvements:
722 self.mouvements.append(Mouvement(self.amount.currency,
723 self.base_currency, mouvement_hash))
634 724
635 def cancel(self): 725 def cancel(self):
726 if self.debug:
727 self.status = "canceled"
728 return
636 self.market.cancel_order(self.result['id']) 729 self.market.cancel_order(self.result['id'])
730 self.fetch()
731
732class Mouvement:
733 def __init__(self, currency, base_currency, hash_):
734 self.currency = currency
735 self.base_currency = base_currency
736 self.id = hash_["id"]
737 self.action = hash_["type"]
738 self.fee_rate = D(hash_["fee"])
739 self.date = datetime.strptime(hash_["date"], '%Y-%m-%d %H:%M:%S')
740 self.rate = D(hash_["rate"])
741 self.total = Amount(currency, hash_["amount"])
742 # rate * total = total_in_base
743 self.total_in_base = Amount(base_currency, hash_["total"])
637 744
638def print_orders(market, base_currency="BTC"): 745def print_orders(market, base_currency="BTC"):
639 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") 746 Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
@@ -650,14 +757,27 @@ def make_orders(market, base_currency="BTC"):
650 print("\t", order, sep="") 757 print("\t", order, sep="")
651 order.run() 758 order.run()
652 759
653def sell_all(market, base_currency="BTC"): 760def process_sell_all_sell(market, base_currency="BTC", debug=False):
654 Balance.prepare_trades_to_sell_all(market) 761 Balance.prepare_trades_to_sell_all(market, debug=debug)
655 Trade.prepare_orders(compute_value="average") 762 Trade.prepare_orders(compute_value="average")
763 print("------------------")
764 for currency, balance in Balance.known_balances.items():
765 print(balance)
766 print("------------------")
767 Trade.print_all_with_order()
768 print("------------------")
656 Trade.run_orders() 769 Trade.run_orders()
657 Trade.follow_orders() 770 Trade.follow_orders()
658 771
659 Balance.update_trades(market, only="acquire") 772def process_sell_all_buy(market, base_currency="BTC", debug=False):
660 Trade.prepare_orders(only="acquire") 773 Balance.prepare_trades(market, debug=debug)
774 Trade.prepare_orders()
775 print("------------------")
776 for currency, balance in Balance.known_balances.items():
777 print(balance)
778 print("------------------")
779 Trade.print_all_with_order()
780 print("------------------")
661 Trade.move_balances(market) 781 Trade.move_balances(market)
662 Trade.run_orders() 782 Trade.run_orders()
663 Trade.follow_orders() 783 Trade.follow_orders()