diff options
Diffstat (limited to 'portfolio.py')
-rw-r--r-- | portfolio.py | 309 |
1 files changed, 31 insertions, 278 deletions
diff --git a/portfolio.py b/portfolio.py index 45fbef9..efd9b84 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,14 +1,13 @@ | |||
1 | from ccxt import ExchangeError | ||
2 | import time | 1 | import time |
3 | from decimal import Decimal as D, ROUND_DOWN | 2 | from decimal import Decimal as D, ROUND_DOWN |
4 | # Put your poloniex api key in market.py | 3 | # Put your poloniex api key in market.py |
5 | from market import market | ||
6 | from json import JSONDecodeError | 4 | from json import JSONDecodeError |
7 | import requests | 5 | import requests |
6 | import helper as h | ||
7 | from store import * | ||
8 | 8 | ||
9 | # FIXME: correctly handle web call timeouts | 9 | # FIXME: correctly handle web call timeouts |
10 | 10 | ||
11 | |||
12 | class Portfolio: | 11 | class Portfolio: |
13 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | 12 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" |
14 | liquidities = {} | 13 | liquidities = {} |
@@ -69,6 +68,24 @@ class Portfolio: | |||
69 | "high": high_liquidity, | 68 | "high": high_liquidity, |
70 | } | 69 | } |
71 | 70 | ||
71 | class Computation: | ||
72 | computations = { | ||
73 | "default": lambda x, y: x[y], | ||
74 | "average": lambda x, y: x["average"], | ||
75 | "bid": lambda x, y: x["bid"], | ||
76 | "ask": lambda x, y: x["ask"], | ||
77 | } | ||
78 | |||
79 | @classmethod | ||
80 | def compute_value(cls, ticker, action, compute_value="default"): | ||
81 | if action == "buy": | ||
82 | action = "ask" | ||
83 | if action == "sell": | ||
84 | action = "bid" | ||
85 | if isinstance(compute_value, str): | ||
86 | compute_value = cls.computations[compute_value] | ||
87 | return compute_value(ticker, action) | ||
88 | |||
72 | class Amount: | 89 | class Amount: |
73 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): | 90 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): |
74 | self.currency = currency | 91 | self.currency = currency |
@@ -86,9 +103,9 @@ class Amount: | |||
86 | self.value * rate, | 103 | self.value * rate, |
87 | linked_to=self, | 104 | linked_to=self, |
88 | rate=rate) | 105 | rate=rate) |
89 | asset_ticker = Trade.get_ticker(self.currency, other_currency, market) | 106 | asset_ticker = h.get_ticker(self.currency, other_currency, market) |
90 | if asset_ticker is not None: | 107 | if asset_ticker is not None: |
91 | rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value) | 108 | rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value) |
92 | return Amount( | 109 | return Amount( |
93 | other_currency, | 110 | other_currency, |
94 | self.value * rate, | 111 | self.value * rate, |
@@ -180,7 +197,6 @@ class Amount: | |||
180 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) | 197 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) |
181 | 198 | ||
182 | class Balance: | 199 | class Balance: |
183 | known_balances = {} | ||
184 | 200 | ||
185 | def __init__(self, currency, hash_): | 201 | def __init__(self, currency, hash_): |
186 | self.currency = currency | 202 | self.currency = currency |
@@ -201,69 +217,6 @@ class Balance: | |||
201 | ]: | 217 | ]: |
202 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) | 218 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) |
203 | 219 | ||
204 | @classmethod | ||
205 | def in_currency(cls, other_currency, market, compute_value="average", type="total"): | ||
206 | amounts = {} | ||
207 | for currency in cls.known_balances: | ||
208 | balance = cls.known_balances[currency] | ||
209 | other_currency_amount = getattr(balance, type)\ | ||
210 | .in_currency(other_currency, market, compute_value=compute_value) | ||
211 | amounts[currency] = other_currency_amount | ||
212 | return amounts | ||
213 | |||
214 | @classmethod | ||
215 | def currencies(cls): | ||
216 | return cls.known_balances.keys() | ||
217 | |||
218 | @classmethod | ||
219 | def fetch_balances(cls, market): | ||
220 | all_balances = market.fetch_all_balances() | ||
221 | for currency, balance in all_balances.items(): | ||
222 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ | ||
223 | currency in cls.known_balances: | ||
224 | cls.known_balances[currency] = cls(currency, balance) | ||
225 | return cls.known_balances | ||
226 | |||
227 | @classmethod | ||
228 | def dispatch_assets(cls, amount, repartition=None): | ||
229 | if repartition is None: | ||
230 | repartition = Portfolio.repartition() | ||
231 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | ||
232 | amounts = {} | ||
233 | for currency, (ptt, trade_type) in repartition.items(): | ||
234 | amounts[currency] = ptt * amount / sum_ratio | ||
235 | if trade_type == "short": | ||
236 | amounts[currency] = - amounts[currency] | ||
237 | if currency not in cls.known_balances: | ||
238 | cls.known_balances[currency] = cls(currency, {}) | ||
239 | return amounts | ||
240 | |||
241 | @classmethod | ||
242 | def prepare_trades(cls, market, base_currency="BTC", compute_value="average", debug=False): | ||
243 | cls.fetch_balances(market) | ||
244 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
245 | total_base_value = sum(values_in_base.values()) | ||
246 | new_repartition = cls.dispatch_assets(total_base_value) | ||
247 | # Recompute it in case we have new currencies | ||
248 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
249 | Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
250 | |||
251 | @classmethod | ||
252 | def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None, debug=False): | ||
253 | cls.fetch_balances(market) | ||
254 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
255 | total_base_value = sum(values_in_base.values()) | ||
256 | new_repartition = cls.dispatch_assets(total_base_value) | ||
257 | Trade.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) | ||
258 | |||
259 | @classmethod | ||
260 | def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average", debug=False): | ||
261 | cls.fetch_balances(market) | ||
262 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
263 | total_base_value = sum(values_in_base.values()) | ||
264 | new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | ||
265 | Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
266 | |||
267 | def __repr__(self): | 220 | def __repr__(self): |
268 | if self.exchange_total > 0: | 221 | if self.exchange_total > 0: |
269 | if self.exchange_free > 0 and self.exchange_used > 0: | 222 | if self.exchange_free > 0 and self.exchange_used > 0: |
@@ -296,18 +249,7 @@ class Balance: | |||
296 | 249 | ||
297 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" | 250 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" |
298 | 251 | ||
299 | class Computation: | ||
300 | computations = { | ||
301 | "default": lambda x, y: x[y], | ||
302 | "average": lambda x, y: x["average"], | ||
303 | "bid": lambda x, y: x["bid"], | ||
304 | "ask": lambda x, y: x["ask"], | ||
305 | } | ||
306 | |||
307 | class Trade: | 252 | class Trade: |
308 | debug = False | ||
309 | trades = [] | ||
310 | |||
311 | def __init__(self, value_from, value_to, currency, market=None): | 253 | def __init__(self, value_from, value_to, currency, market=None): |
312 | # We have value_from of currency, and want to finish with value_to of | 254 | # We have value_from of currency, and want to finish with value_to of |
313 | # that currency. value_* may not be in currency's terms | 255 | # that currency. value_* may not be in currency's terms |
@@ -323,105 +265,6 @@ class Trade: | |||
323 | self.value_from.linked_to = Amount(self.currency, 0) | 265 | self.value_from.linked_to = Amount(self.currency, 0) |
324 | self.base_currency = self.value_from.currency | 266 | self.base_currency = self.value_from.currency |
325 | 267 | ||
326 | fees_cache = {} | ||
327 | @classmethod | ||
328 | def fetch_fees(cls, market): | ||
329 | if market.__class__ not in cls.fees_cache: | ||
330 | cls.fees_cache[market.__class__] = market.fetch_fees() | ||
331 | return cls.fees_cache[market.__class__] | ||
332 | |||
333 | ticker_cache = {} | ||
334 | ticker_cache_timestamp = time.time() | ||
335 | @classmethod | ||
336 | def get_ticker(cls, c1, c2, market, refresh=False): | ||
337 | def invert(ticker): | ||
338 | return { | ||
339 | "inverted": True, | ||
340 | "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2, | ||
341 | "original": ticker, | ||
342 | } | ||
343 | def augment_ticker(ticker): | ||
344 | ticker.update({ | ||
345 | "inverted": False, | ||
346 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | ||
347 | }) | ||
348 | |||
349 | if time.time() - cls.ticker_cache_timestamp > 5: | ||
350 | cls.ticker_cache = {} | ||
351 | cls.ticker_cache_timestamp = time.time() | ||
352 | elif not refresh: | ||
353 | if (c1, c2, market.__class__) in cls.ticker_cache: | ||
354 | return cls.ticker_cache[(c1, c2, market.__class__)] | ||
355 | if (c2, c1, market.__class__) in cls.ticker_cache: | ||
356 | return invert(cls.ticker_cache[(c2, c1, market.__class__)]) | ||
357 | |||
358 | try: | ||
359 | cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | ||
360 | augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)]) | ||
361 | except ExchangeError: | ||
362 | try: | ||
363 | cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | ||
364 | augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)]) | ||
365 | except ExchangeError: | ||
366 | cls.ticker_cache[(c1, c2, market.__class__)] = None | ||
367 | return cls.get_ticker(c1, c2, market) | ||
368 | |||
369 | @classmethod | ||
370 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): | ||
371 | cls.debug = cls.debug or debug | ||
372 | base_currency = sum(values_in_base.values()).currency | ||
373 | for currency in Balance.currencies(): | ||
374 | if currency == base_currency: | ||
375 | continue | ||
376 | value_from = values_in_base.get(currency, Amount(base_currency, 0)) | ||
377 | value_to = new_repartition.get(currency, Amount(base_currency, 0)) | ||
378 | if value_from.value * value_to.value < 0: | ||
379 | trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market) | ||
380 | if only is None or trade_1.action == only: | ||
381 | cls.trades.append(trade_1) | ||
382 | trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market) | ||
383 | if only is None or trade_2.action == only: | ||
384 | cls.trades.append(trade_2) | ||
385 | else: | ||
386 | trade = cls( | ||
387 | value_from, | ||
388 | value_to, | ||
389 | currency, | ||
390 | market=market | ||
391 | ) | ||
392 | if only is None or trade.action == only: | ||
393 | cls.trades.append(trade) | ||
394 | return cls.trades | ||
395 | |||
396 | @classmethod | ||
397 | def prepare_orders(cls, only=None, compute_value="default"): | ||
398 | for trade in cls.trades: | ||
399 | if only is None or trade.action == only: | ||
400 | trade.prepare_order(compute_value=compute_value) | ||
401 | |||
402 | @classmethod | ||
403 | def move_balances(cls, market): | ||
404 | needed_in_margin = {} | ||
405 | for trade in cls.trades: | ||
406 | if trade.trade_type == "short": | ||
407 | if trade.value_to.currency not in needed_in_margin: | ||
408 | needed_in_margin[trade.value_to.currency] = 0 | ||
409 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | ||
410 | for currency, needed in needed_in_margin.items(): | ||
411 | current_balance = Balance.known_balances[currency].margin_free | ||
412 | delta = (needed - current_balance).value | ||
413 | # FIXME: don't remove too much if there are open margin position | ||
414 | if delta > 0: | ||
415 | if cls.debug: | ||
416 | print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta)) | ||
417 | else: | ||
418 | market.transfer_balance(currency, delta, "exchange", "margin") | ||
419 | elif delta < 0: | ||
420 | if cls.debug: | ||
421 | print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta)) | ||
422 | else: | ||
423 | market.transfer_balance(currency, -delta, "margin", "exchange") | ||
424 | |||
425 | @property | 268 | @property |
426 | def action(self): | 269 | def action(self): |
427 | if self.value_from == self.value_to: | 270 | if self.value_from == self.value_to: |
@@ -481,11 +324,11 @@ class Trade: | |||
481 | def prepare_order(self, compute_value="default"): | 324 | def prepare_order(self, compute_value="default"): |
482 | if self.action is None: | 325 | if self.action is None: |
483 | return | 326 | return |
484 | ticker = Trade.get_ticker(self.currency, self.base_currency, self.market) | 327 | ticker = h.get_ticker(self.currency, self.base_currency, self.market) |
485 | inverted = ticker["inverted"] | 328 | inverted = ticker["inverted"] |
486 | if inverted: | 329 | if inverted: |
487 | ticker = ticker["original"] | 330 | ticker = ticker["original"] |
488 | rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) | 331 | rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) |
489 | # 0.1 | 332 | # 0.1 |
490 | 333 | ||
491 | delta_in_base = abs(self.value_from - self.value_to) | 334 | delta_in_base = abs(self.value_from - self.value_to) |
@@ -536,51 +379,6 @@ class Trade: | |||
536 | delta - self.filled_amount, rate, currency, self.trade_type, | 379 | delta - self.filled_amount, rate, currency, self.trade_type, |
537 | self.market, self, close_if_possible=close_if_possible)) | 380 | self.market, self, close_if_possible=close_if_possible)) |
538 | 381 | ||
539 | @classmethod | ||
540 | def compute_value(cls, ticker, action, compute_value="default"): | ||
541 | if action == "buy": | ||
542 | action = "ask" | ||
543 | if action == "sell": | ||
544 | action = "bid" | ||
545 | if isinstance(compute_value, str): | ||
546 | compute_value = Computation.computations[compute_value] | ||
547 | return compute_value(ticker, action) | ||
548 | |||
549 | @classmethod | ||
550 | def all_orders(cls, state=None): | ||
551 | all_orders = sum(map(lambda v: v.orders, cls.trades), []) | ||
552 | if state is None: | ||
553 | return all_orders | ||
554 | else: | ||
555 | return list(filter(lambda o: o.status == state, all_orders)) | ||
556 | |||
557 | @classmethod | ||
558 | def run_orders(cls): | ||
559 | for order in cls.all_orders(state="pending"): | ||
560 | order.run() | ||
561 | |||
562 | @classmethod | ||
563 | def follow_orders(cls, verbose=True, sleep=None): | ||
564 | if sleep is None: | ||
565 | sleep = 7 if cls.debug else 30 | ||
566 | tick = 0 | ||
567 | while len(cls.all_orders(state="open")) > 0: | ||
568 | time.sleep(sleep) | ||
569 | tick += 1 | ||
570 | for order in cls.all_orders(state="open"): | ||
571 | if order.get_status() != "open": | ||
572 | if verbose: | ||
573 | print("finished {}".format(order)) | ||
574 | else: | ||
575 | order.trade.update_order(order, tick) | ||
576 | if verbose: | ||
577 | print("All orders finished") | ||
578 | |||
579 | @classmethod | ||
580 | def update_all_orders_status(cls): | ||
581 | for order in cls.all_orders(state="open"): | ||
582 | order.get_status() | ||
583 | |||
584 | def __repr__(self): | 382 | def __repr__(self): |
585 | return "Trade({} -> {} in {}, {})".format( | 383 | return "Trade({} -> {} in {}, {})".format( |
586 | self.value_from, | 384 | self.value_from, |
@@ -588,11 +386,6 @@ class Trade: | |||
588 | self.currency, | 386 | self.currency, |
589 | self.action) | 387 | self.action) |
590 | 388 | ||
591 | @classmethod | ||
592 | def print_all_with_order(cls): | ||
593 | for trade in cls.trades: | ||
594 | trade.print_with_order() | ||
595 | |||
596 | def print_with_order(self): | 389 | def print_with_order(self): |
597 | print(self) | 390 | print(self) |
598 | for order in self.orders: | 391 | for order in self.orders: |
@@ -612,7 +405,6 @@ class Order: | |||
612 | self.status = "pending" | 405 | self.status = "pending" |
613 | self.trade = trade | 406 | self.trade = trade |
614 | self.close_if_possible = close_if_possible | 407 | self.close_if_possible = close_if_possible |
615 | self.debug = trade.debug | ||
616 | 408 | ||
617 | def __repr__(self): | 409 | def __repr__(self): |
618 | return "Order({} {} {} at {} {} [{}]{})".format( | 410 | return "Order({} {} {} at {} {} [{}]{})".format( |
@@ -648,7 +440,7 @@ class Order: | |||
648 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 440 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
649 | amount = round(self.amount, self.market.order_precision(symbol)).value | 441 | amount = round(self.amount, self.market.order_precision(symbol)).value |
650 | 442 | ||
651 | if self.debug: | 443 | if TradeStore.debug: |
652 | print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 444 | print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( |
653 | symbol, self.action, amount, self.rate, self.account)) | 445 | symbol, self.action, amount, self.rate, self.account)) |
654 | self.status = "open" | 446 | self.status = "open" |
@@ -665,7 +457,7 @@ class Order: | |||
665 | print(self.error_message) | 457 | print(self.error_message) |
666 | 458 | ||
667 | def get_status(self): | 459 | def get_status(self): |
668 | if self.debug: | 460 | if TradeStore.debug: |
669 | return self.status | 461 | return self.status |
670 | # other states are "closed" and "canceled" | 462 | # other states are "closed" and "canceled" |
671 | if self.status == "open": | 463 | if self.status == "open": |
@@ -675,7 +467,7 @@ class Order: | |||
675 | return self.status | 467 | return self.status |
676 | 468 | ||
677 | def mark_finished_order(self): | 469 | def mark_finished_order(self): |
678 | if self.debug: | 470 | if TradeStore.debug: |
679 | return | 471 | return |
680 | if self.status == "closed": | 472 | if self.status == "closed": |
681 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: | 473 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: |
@@ -683,7 +475,7 @@ class Order: | |||
683 | 475 | ||
684 | fetch_cache_timestamp = None | 476 | fetch_cache_timestamp = None |
685 | def fetch(self, force=False): | 477 | def fetch(self, force=False): |
686 | if self.debug or (not force and self.fetch_cache_timestamp is not None | 478 | if TradeStore.debug or (not force and self.fetch_cache_timestamp is not None |
687 | and time.time() - self.fetch_cache_timestamp < 10): | 479 | and time.time() - self.fetch_cache_timestamp < 10): |
688 | return | 480 | return |
689 | self.fetch_cache_timestamp = time.time() | 481 | self.fetch_cache_timestamp = time.time() |
@@ -725,7 +517,7 @@ class Order: | |||
725 | self.base_currency, mouvement_hash)) | 517 | self.base_currency, mouvement_hash)) |
726 | 518 | ||
727 | def cancel(self): | 519 | def cancel(self): |
728 | if self.debug: | 520 | if TradeStore.debug: |
729 | self.status = "canceled" | 521 | self.status = "canceled" |
730 | return | 522 | return |
731 | self.market.cancel_order(self.result['id']) | 523 | self.market.cancel_order(self.result['id']) |
@@ -744,45 +536,6 @@ class Mouvement: | |||
744 | # rate * total = total_in_base | 536 | # rate * total = total_in_base |
745 | self.total_in_base = Amount(base_currency, hash_["total"]) | 537 | self.total_in_base = Amount(base_currency, hash_["total"]) |
746 | 538 | ||
747 | def print_orders(market, base_currency="BTC"): | ||
748 | Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") | ||
749 | Trade.prepare_orders(compute_value="average") | ||
750 | for currency, balance in Balance.known_balances.items(): | ||
751 | print(balance) | ||
752 | Trade.print_all_with_order() | ||
753 | |||
754 | def make_orders(market, base_currency="BTC"): | ||
755 | Balance.prepare_trades(market, base_currency=base_currency) | ||
756 | for trade in Trade.trades: | ||
757 | print(trade) | ||
758 | for order in trade.orders: | ||
759 | print("\t", order, sep="") | ||
760 | order.run() | ||
761 | |||
762 | def process_sell_all_sell(market, base_currency="BTC", debug=False): | ||
763 | Balance.prepare_trades_to_sell_all(market, debug=debug) | ||
764 | Trade.prepare_orders(compute_value="average") | ||
765 | print("------------------") | ||
766 | for currency, balance in Balance.known_balances.items(): | ||
767 | print(balance) | ||
768 | print("------------------") | ||
769 | Trade.print_all_with_order() | ||
770 | print("------------------") | ||
771 | Trade.run_orders() | ||
772 | Trade.follow_orders() | ||
773 | |||
774 | def process_sell_all_buy(market, base_currency="BTC", debug=False): | ||
775 | Balance.prepare_trades(market, debug=debug) | ||
776 | Trade.prepare_orders() | ||
777 | print("------------------") | ||
778 | for currency, balance in Balance.known_balances.items(): | ||
779 | print(balance) | ||
780 | print("------------------") | ||
781 | Trade.print_all_with_order() | ||
782 | print("------------------") | ||
783 | Trade.move_balances(market) | ||
784 | Trade.run_orders() | ||
785 | Trade.follow_orders() | ||
786 | |||
787 | if __name__ == '__main__': | 539 | if __name__ == '__main__': |
788 | print_orders(market) | 540 | from market import market |
541 | h.print_orders(market) | ||