1 from ccxt
import ExchangeError
3 from decimal
import Decimal
as D
, ROUND_DOWN
4 # Put your poloniex api key in market.py
5 from market
import market
8 URL
= "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
13 def repartition(cls
, liquidity
="medium"):
14 cls
.parse_cryptoportfolio()
15 liquidities
= cls
.liquidities
[liquidity
]
16 cls
.last_date
= sorted(liquidities
.keys())[-1]
17 return liquidities
[cls
.last_date
]
20 def get_cryptoportfolio(cls
):
23 urllib3
.disable_warnings()
24 http
= urllib3
.PoolManager()
27 r
= http
.request("GET", cls
.URL
)
31 cls
.data
= json
.loads(r
.data
,
34 except json
.JSONDecodeError
:
38 def parse_cryptoportfolio(cls
):
40 cls
.get_cryptoportfolio()
42 def filter_weights(weight_hash
):
43 if weight_hash
[1][0] == 0:
45 if weight_hash
[0] == "_row":
50 def clean_weights_(h
):
51 if h
[0].endswith("s"):
52 return [h
[0][0:-1], (h
[1][i
], "short")]
54 return [h
[0], (h
[1][i
], "long")]
57 def parse_weights(portfolio_hash
):
58 weights_hash
= portfolio_hash
["weights"]
60 for i
in range(len(weights_hash
["_row"])):
61 weights
[weights_hash
["_row"][i
]] = dict(filter(
63 map(clean_weights(i
), weights_hash
.items())))
66 high_liquidity
= parse_weights(cls
.data
["portfolio_1"])
67 medium_liquidity
= parse_weights(cls
.data
["portfolio_2"])
70 "medium": medium_liquidity
,
71 "high": high_liquidity
,
75 def __init__(self
, currency
, value
, linked_to
=None, ticker
=None, rate
=None):
76 self
.currency
= currency
78 self
.linked_to
= linked_to
82 self
.ticker_cache
= {}
83 self
.ticker_cache_timestamp
= time
.time()
85 def in_currency(self
, other_currency
, market
, rate
=None, action
=None, compute_value
="average"):
86 if other_currency
== self
.currency
:
94 asset_ticker
= Trade
.get_ticker(self
.currency
, other_currency
, market
)
95 if asset_ticker
is not None:
96 rate
= Trade
.compute_value(asset_ticker
, action
, compute_value
=compute_value
)
104 raise Exception("This asset is not available in the chosen market")
106 def __round__(self
, n
=8):
107 return Amount(self
.currency
, self
.value
.quantize(D(1)/D(10**n
), rounding
=ROUND_DOWN
))
110 return Amount(self
.currency
, abs(self
.value
))
112 def __add__(self
, other
):
113 if other
.currency
!= self
.currency
and other
.value
* self
.value
!= 0:
114 raise Exception("Summing amounts must be done with same currencies")
115 return Amount(self
.currency
, self
.value
+ other
.value
)
117 def __radd__(self
, other
):
121 return self
.__add
__(other
)
123 def __sub__(self
, other
):
124 if other
.currency
!= self
.currency
and other
.value
* self
.value
!= 0:
125 raise Exception("Summing amounts must be done with same currencies")
126 return Amount(self
.currency
, self
.value
- other
.value
)
128 def __mul__(self
, value
):
129 if not isinstance(value
, (int, float, D
)):
130 raise TypeError("Amount may only be multiplied by numbers")
131 return Amount(self
.currency
, self
.value
* value
)
133 def __rmul__(self
, value
):
134 return self
.__mul
__(value
)
136 def __floordiv__(self
, value
):
137 if not isinstance(value
, (int, float, D
)):
138 raise TypeError("Amount may only be multiplied by integers")
139 return Amount(self
.currency
, self
.value
/ value
)
141 def __truediv__(self
, value
):
142 return self
.__floordiv
__(value
)
144 def __le__(self
, other
):
145 return self
== other
or self
< other
147 def __lt__(self
, other
):
149 return self
.value
< 0
150 if self
.currency
!= other
.currency
:
151 raise Exception("Comparing amounts must be done with same currencies")
152 return self
.value
< other
.value
154 def __gt__(self
, other
):
155 return not self
<= other
157 def __ge__(self
, other
):
158 return not self
< other
160 def __eq__(self
, other
):
162 return self
.value
== 0
163 if self
.currency
!= other
.currency
:
164 raise Exception("Comparing amounts must be done with same currencies")
165 return self
.value
== other
.value
167 def __ne__(self
, other
):
168 return not self
== other
171 return Amount(self
.currency
, - self
.value
)
174 if self
.linked_to
is None:
175 return "{:.8f} {}".format(self
.value
, self
.currency
)
177 return "{:.8f} {} [{}]".format(self
.value
, self
.currency
, self
.linked_to
)
180 if self
.linked_to
is None:
181 return "Amount({:.8f} {})".format(self
.value
, self
.currency
)
183 return "Amount({:.8f} {} -> {})".format(self
.value
, self
.currency
, repr(self
.linked_to
))
188 def __init__(self
, currency
, hash_
):
189 self
.currency
= currency
191 "exchange_total", "exchange_used", "exchange_free",
192 "margin_total", "margin_borrowed", "margin_free"]:
193 setattr(self
, key
, Amount(currency
, hash_
.get(key
, 0)))
195 self
.margin_position_type
= hash_
["margin_position_type"]
197 if hash_
["margin_borrowed_base_currency"] is not None:
198 base_currency
= hash_
["margin_borrowed_base_currency"]
200 "margin_liquidation_price",
201 "margin_pending_gain",
202 "margin_lending_fees",
203 "margin_borrowed_base_price"
205 setattr(self
, key
, Amount(base_currency
, hash_
[key
]))
208 def in_currency(cls
, other_currency
, market
, compute_value
="average", type="total"):
210 for currency
in cls
.known_balances
:
211 balance
= cls
.known_balances
[currency
]
212 other_currency_amount
= getattr(balance
, type)\
213 .in_currency(other_currency
, market
, compute_value
=compute_value
)
214 amounts
[currency
] = other_currency_amount
219 return cls
.known_balances
.keys()
222 def fetch_balances(cls
, market
):
223 all_balances
= market
.fetch_all_balances()
224 for currency
, balance
in all_balances
.items():
225 if balance
["exchange_total"] != 0 or balance
["margin_total"] != 0 or \
226 currency
in cls
.known_balances
:
227 cls
.known_balances
[currency
] = cls(currency
, balance
)
228 return cls
.known_balances
232 def dispatch_assets(cls
, amount
, repartition
=None):
233 if repartition
is None:
234 repartition
= Portfolio
.repartition()
235 sum_ratio
= sum([v
[0] for k
, v
in repartition
.items()])
237 for currency
, (ptt
, trade_type
) in repartition
.items():
238 amounts
[currency
] = ptt
* amount
/ sum_ratio
239 if trade_type
== "short":
240 amounts
[currency
] = - amounts
[currency
]
241 if currency
not in cls
.known_balances
:
242 cls
.known_balances
[currency
] = cls(currency
, 0, 0, 0)
246 def prepare_trades(cls
, market
, base_currency
="BTC", compute_value
="average"):
247 cls
.fetch_balances(market
)
248 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
249 total_base_value
= sum(values_in_base
.values())
250 new_repartition
= cls
.dispatch_assets(total_base_value
)
251 # Recompute it in case we have new currencies
252 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
253 Trade
.compute_trades(values_in_base
, new_repartition
, market
=market
)
256 def update_trades(cls
, market
, base_currency
="BTC", compute_value
="average", only
=None):
257 cls
.fetch_balances(market
)
258 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
259 total_base_value
= sum(values_in_base
.values())
260 new_repartition
= cls
.dispatch_assets(total_base_value
)
261 Trade
.compute_trades(values_in_base
, new_repartition
, only
=only
, market
=market
)
264 def prepare_trades_to_sell_all(cls
, market
, base_currency
="BTC", compute_value
="average"):
265 cls
.fetch_balances(market
)
266 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
267 total_base_value
= sum(values_in_base
.values())
268 new_repartition
= cls
.dispatch_assets(total_base_value
, repartition
={ base_currency: (1, "long") }
)
269 Trade
.compute_trades(values_in_base
, new_repartition
, market
=market
)
272 if self
.exchange_total
> 0:
273 if self
.exchange_free
> 0 and self
.exchange_used
> 0:
274 exchange
= " Exch: [✔{} + ❌{} = {}]".format(str(self
.exchange_free
), str(self
.exchange_used
), str(self
.exchange_total
))
275 elif self
.exchange_free
> 0:
276 exchange
= " Exch: [✔{}]".format(str(self
.exchange_free
))
278 exchange
= " Exch: [❌{}]".format(str(self
.exchange_used
))
282 if self
.margin_total
> 0:
283 if self
.margin_free
!= 0 and self
.margin_borrowed
!= 0:
284 margin
= " Margin: [✔{} + borrowed {} = {}]".format(str(self
.margin_free
), str(self
.margin_borrowed
), str(self
.margin_total
))
285 elif self
.margin_free
!= 0:
286 margin
= " Margin: [✔{}]".format(str(self
.margin_free
))
288 margin
= " Margin: [borrowed {}]".format(str(self
.margin_borrowed
))
289 elif self
.margin_total
< 0:
290 margin
= " Margin: [{} @@ {}/{}]".format(str(self
.margin_total
),
291 str(self
.margin_borrowed_base_price
),
292 str(self
.margin_lending_fees
))
296 if self
.margin_total
!= 0 and self
.exchange_total
!= 0:
297 total
= " Total: [{}]".format(str(self
.total
))
301 return "Balance({}".format(self
.currency
) + "".join([exchange
, margin
, total
]) + ")"
305 "default": lambda x
, y
: x
[y
],
306 "average": lambda x
, y
: x
["average"],
307 "bid": lambda x
, y
: x
["bid"],
308 "ask": lambda x
, y
: x
["ask"],
314 def __init__(self
, value_from
, value_to
, currency
, market
=None):
315 # We have value_from of currency, and want to finish with value_to of
316 # that currency. value_* may not be in currency's terms
317 self
.currency
= currency
318 self
.value_from
= value_from
319 self
.value_to
= value_to
322 assert self
.value_from
.currency
== self
.value_to
.currency
323 if self
.value_from
!= 0:
324 assert self
.value_from
.linked_to
is not None and self
.value_from
.linked_to
.currency
== self
.currency
325 elif self
.value_from
.linked_to
is None:
326 self
.value_from
.linked_to
= Amount(self
.currency
, 0)
327 self
.base_currency
= self
.value_from
.currency
331 def fetch_fees(cls
, market
):
332 if market
.__class
__ not in cls
.fees_cache
:
333 cls
.fees_cache
[market
.__class
__] = market
.fetch_fees()
334 return cls
.fees_cache
[market
.__class
__]
337 ticker_cache_timestamp
= time
.time()
339 def get_ticker(cls
, c1
, c2
, market
, refresh
=False):
343 "average": (1/ticker
["bid"] + 1/ticker
["ask"]) / 2,
346 def augment_ticker(ticker
):
349 "average": (ticker
["bid"] + ticker
["ask"] ) / 2,
352 if time
.time() - cls
.ticker_cache_timestamp
> 5:
353 cls
.ticker_cache
= {}
354 cls
.ticker_cache_timestamp
= time
.time()
356 if (c1
, c2
, market
.__class
__) in cls
.ticker_cache
:
357 return cls
.ticker_cache
[(c1
, c2
, market
.__class
__)]
358 if (c2
, c1
, market
.__class
__) in cls
.ticker_cache
:
359 return invert(cls
.ticker_cache
[(c2
, c1
, market
.__class
__)])
362 cls
.ticker_cache
[(c1
, c2
, market
.__class
__)] = market
.fetch_ticker("{}/{}".format(c1
, c2
))
363 augment_ticker(cls
.ticker_cache
[(c1
, c2
, market
.__class
__)])
364 except ExchangeError
:
366 cls
.ticker_cache
[(c2
, c1
, market
.__class
__)] = market
.fetch_ticker("{}/{}".format(c2
, c1
))
367 augment_ticker(cls
.ticker_cache
[(c2
, c1
, market
.__class
__)])
368 except ExchangeError
:
369 cls
.ticker_cache
[(c1
, c2
, market
.__class
__)] = None
370 return cls
.get_ticker(c1
, c2
, market
)
373 def compute_trades(cls
, values_in_base
, new_repartition
, only
=None, market
=None):
374 base_currency
= sum(values_in_base
.values()).currency
375 for currency
in Balance
.currencies():
376 if currency
== base_currency
:
378 value_from
= values_in_base
.get(currency
, Amount(base_currency
, 0))
379 value_to
= new_repartition
.get(currency
, Amount(base_currency
, 0))
380 if value_from
.value
* value_to
.value
< 0:
381 trade_1
= cls(value_from
, Amount(base_currency
, 0), currency
, market
=market
)
382 if only
is None or trade_1
.action
== only
:
383 cls
.trades
.append(trade_1
)
384 trade_2
= cls(Amount(base_currency
, 0), value_to
, currency
, market
=market
)
385 if only
is None or trade_2
.action
== only
:
386 cls
.trades
.append(trade_2
)
394 if only
is None or trade
.action
== only
:
395 cls
.trades
.append(trade
)
399 def prepare_orders(cls
, only
=None, compute_value
="default"):
400 for trade
in cls
.trades
:
401 if only
is None or trade
.action
== only
:
402 trade
.prepare_order(compute_value
=compute_value
)
405 def move_balances(cls
, market
, debug
=False):
406 needed_in_margin
= {}
407 for trade
in cls
.trades
:
408 if trade
.trade_type
== "short":
409 if trade
.value_to
.currency
not in needed_in_margin
:
410 needed_in_margin
[trade
.value_to
.currency
] = 0
411 needed_in_margin
[trade
.value_to
.currency
] += abs(trade
.value_to
)
412 for currency
, needed
in needed_in_margin
.items():
413 current_balance
= Balance
.known_balances
[currency
].margin_free
414 delta
= (needed
- current_balance
).value
415 # FIXME: don't remove too much if there are open margin position
418 print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency
, delta
))
420 market
.transfer_balance(currency
, delta
, "exchange", "margin")
423 print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency
, -delta
))
425 market
.transfer_balance(currency
, -delta
, "margin", "exchange")
429 if self
.value_from
== self
.value_to
:
431 if self
.base_currency
== self
.currency
:
434 if self
.value_from
< self
.value_to
:
439 def order_action(self
, inverted
):
440 if (self
.value_from
< self
.value_to
) != inverted
:
446 def trade_type(self
):
447 if self
.value_from
+ self
.value_to
< 0:
452 def prepare_order(self
, compute_value
="default"):
453 if self
.action
is None:
455 ticker
= Trade
.get_ticker(self
.currency
, self
.base_currency
, self
.market
)
456 inverted
= ticker
["inverted"]
458 ticker
= ticker
["original"]
459 rate
= Trade
.compute_value(ticker
, self
.order_action(inverted
), compute_value
=compute_value
)
462 delta_in_base
= abs(self
.value_from
- self
.value_to
)
463 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
466 currency
= self
.base_currency
468 if self
.action
== "dispose":
469 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
470 # At rate 1 Foo = 0.1 BTC
471 value_from
= self
.value_from
.linked_to
472 # value_from = 100 FOO
473 value_to
= self
.value_to
.in_currency(self
.currency
, self
.market
, rate
=1/self
.value_from
.rate
)
474 # value_to = 10 FOO (1 BTC * 1/0.1)
475 delta
= abs(value_to
- value_from
)
477 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
479 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
481 delta
= delta_in_base
.in_currency(self
.currency
, self
.market
, rate
=1/rate
)
482 # I want to buy 9 / 0.1 FOO
483 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
485 currency
= self
.currency
487 delta
= delta_in_base
489 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
490 # At rate 1 Foo = 0.1 BTC
491 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
493 # I want to buy 9 / 0.1 FOO
494 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
496 close_if_possible
= (self
.value_to
== 0)
498 self
.orders
.append(Order(self
.order_action(inverted
),
499 delta
, rate
, currency
, self
.trade_type
, self
.market
,
500 close_if_possible
=close_if_possible
))
503 def compute_value(cls
, ticker
, action
, compute_value
="default"):
508 if isinstance(compute_value
, str):
509 compute_value
= Computation
.computations
[compute_value
]
510 return compute_value(ticker
, action
)
513 def all_orders(cls
, state
=None):
514 all_orders
= sum(map(lambda v
: v
.orders
, cls
.trades
), [])
518 return list(filter(lambda o
: o
.status
== state
, all_orders
))
522 for order
in cls
.all_orders(state
="pending"):
526 def follow_orders(cls
, verbose
=True, sleep
=30):
527 orders
= cls
.all_orders()
529 while len(orders
) != len(finished_orders
):
532 if order
in finished_orders
:
534 if order
.get_status() != "open":
535 finished_orders
.append(order
)
537 print("finished {}".format(order
))
539 print("All orders finished")
542 def update_all_orders_status(cls
):
543 for order
in cls
.all_orders(state
="open"):
547 return "Trade({} -> {} in {}, {})".format(
554 def print_all_with_order(cls
):
555 for trade
in cls
.trades
:
556 trade
.print_with_order()
558 def print_with_order(self
):
560 for order
in self
.orders
:
561 print("\t", order
, sep
="")
564 def __init__(self
, action
, amount
, rate
, base_currency
, trade_type
, market
,
565 close_if_possible
=False):
569 self
.base_currency
= base_currency
571 self
.trade_type
= trade_type
573 self
.status
= "pending"
574 self
.close_if_possible
= close_if_possible
577 return "Order({} {} {} at {} {} [{}]{})".format(
584 " ✂" if self
.close_if_possible
else "",
589 if self
.trade_type
== "long":
596 return self
.status
== "pending"
600 return self
.status
== "closed" or self
.status
== "canceled" or self
.status
== "error"
602 def run(self
, debug
=False):
603 symbol
= "{}/{}".format(self
.amount
.currency
, self
.base_currency
)
604 amount
= round(self
.amount
, self
.market
.order_precision(symbol
)).value
607 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
608 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
611 self
.result
= self
.market
.create_order(symbol
, 'limit', self
.action
, amount
, price
=self
.rate
, account
=self
.account
)
613 except Exception as e
:
614 self
.status
= "error"
615 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
616 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
617 self
.error_message
= str("{}: {}".format(e
.__class
__.__name
__, e
))
618 print(self
.error_message
)
620 def get_status(self
):
621 # other states are "closed" and "canceled"
622 if self
.status
== "open":
623 result
= self
.market
.fetch_order(self
.result
['id'])
624 if result
["status"] != "open":
625 self
.mark_finished_order(result
["status"])
628 def mark_finished_order(self
, status
):
629 if status
== "closed":
630 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
)
633 self
.status
= result
["status"]
636 self
.market
.cancel_order(self
.result
['id'])
638 def print_orders(market
, base_currency
="BTC"):
639 Balance
.prepare_trades(market
, base_currency
=base_currency
, compute_value
="average")
640 Trade
.prepare_orders(compute_value
="average")
641 for currency
, balance
in Balance
.known_balances
.items():
643 Trade
.print_all_with_order()
645 def make_orders(market
, base_currency
="BTC"):
646 Balance
.prepare_trades(market
, base_currency
=base_currency
)
647 for trade
in Trade
.trades
:
649 for order
in trade
.orders
:
650 print("\t", order
, sep
="")
653 def sell_all(market
, base_currency
="BTC"):
654 Balance
.prepare_trades_to_sell_all(market
)
655 Trade
.prepare_orders(compute_value
="average")
657 Trade
.follow_orders()
659 Balance
.update_trades(market
, only
="acquire")
660 Trade
.prepare_orders(only
="acquire")
661 Trade
.move_balances(market
)
663 Trade
.follow_orders()
665 if __name__
== '__main__':