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 __lt__(self
, other
):
145 if self
.currency
!= other
.currency
:
146 raise Exception("Comparing amounts must be done with same currencies")
147 return self
.value
< other
.value
149 def __eq__(self
, other
):
151 return self
.value
== 0
152 if self
.currency
!= other
.currency
:
153 raise Exception("Comparing amounts must be done with same currencies")
154 return self
.value
== other
.value
157 if self
.linked_to
is None:
158 return "{:.8f} {}".format(self
.value
, self
.currency
)
160 return "{:.8f} {} [{}]".format(self
.value
, self
.currency
, self
.linked_to
)
163 if self
.linked_to
is None:
164 return "Amount({:.8f} {})".format(self
.value
, self
.currency
)
166 return "Amount({:.8f} {} -> {})".format(self
.value
, self
.currency
, repr(self
.linked_to
))
171 def __init__(self
, currency
, total_value
, free_value
, used_value
):
172 self
.currency
= currency
173 self
.total
= Amount(currency
, total_value
)
174 self
.free
= Amount(currency
, free_value
)
175 self
.used
= Amount(currency
, used_value
)
178 def from_hash(cls
, currency
, hash_
):
179 return cls(currency
, hash_
["total"], hash_
["free"], hash_
["used"])
182 def in_currency(cls
, other_currency
, market
, compute_value
="average", type="total"):
184 for currency
in cls
.known_balances
:
185 balance
= cls
.known_balances
[currency
]
186 other_currency_amount
= getattr(balance
, type)\
187 .in_currency(other_currency
, market
, compute_value
=compute_value
)
188 amounts
[currency
] = other_currency_amount
193 return cls
.known_balances
.keys()
196 def _fill_balances(cls
, hash_
):
198 if key
in ["info", "free", "used", "total"]:
200 if hash_
[key
]["total"] != 0 or key
in cls
.known_balances
:
201 cls
.known_balances
[key
] = cls
.from_hash(key
, hash_
[key
])
204 def fetch_balances(cls
, market
):
205 cls
._fill
_balances
(market
.fetch_balance())
206 return cls
.known_balances
207 # FIXME:Separate balances per trade type and in position
208 # Need to check how balances in position are represented
212 def dispatch_assets(cls
, amount
, repartition
=None):
213 if repartition
is None:
214 repartition
= Portfolio
.repartition()
215 sum_ratio
= sum([v
[0] for k
, v
in repartition
.items()])
217 for currency
, (ptt
, trade_type
) in repartition
.items():
218 amounts
[currency
] = ptt
* amount
/ sum_ratio
219 if currency
not in cls
.known_balances
:
220 cls
.known_balances
[currency
] = cls(currency
, 0, 0, 0)
224 def dispatch_trade_types(cls
, repartition
=None):
225 if repartition
is None:
226 repartition
= Portfolio
.repartition()
228 for currency
, (ptt
, trade_type
) in repartition
.items():
229 trade_types
[currency
] = trade_type
231 # FIXME: once we know the repartition and sold everything, we can move
232 # the necessary part to the margin account
235 def prepare_trades(cls
, market
, base_currency
="BTC", compute_value
="average"):
236 cls
.fetch_balances(market
)
237 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
238 total_base_value
= sum(values_in_base
.values())
239 new_repartition
= cls
.dispatch_assets(total_base_value
)
240 trade_types
= cls
.dispatch_trade_types()
241 # Recompute it in case we have new currencies
242 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
243 Trade
.compute_trades(values_in_base
, new_repartition
, trade_types
, market
=market
)
246 def update_trades(cls
, market
, base_currency
="BTC", compute_value
="average", only
=None):
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 trade_types
= cls
.dispatch_trade_types()
252 Trade
.compute_trades(values_in_base
, new_repartition
, trade_types
, only
=only
, market
=market
)
255 def prepare_trades_to_sell_all(cls
, market
, base_currency
="BTC", compute_value
="average"):
256 cls
.fetch_balances(market
)
257 values_in_base
= cls
.in_currency(base_currency
, market
, compute_value
=compute_value
)
258 total_base_value
= sum(values_in_base
.values())
259 new_repartition
= cls
.dispatch_assets(total_base_value
, repartition
={ base_currency: (1, "long") }
)
260 trade_types
= cls
.dispatch_trade_types()
261 Trade
.compute_trades(values_in_base
, new_repartition
, trade_types
, market
=market
)
264 return "Balance({} [{}/{}/{}])".format(self
.currency
, str(self
.free
), str(self
.used
), str(self
.total
))
268 "default": lambda x
, y
: x
[y
],
269 "average": lambda x
, y
: x
["average"],
270 "bid": lambda x
, y
: x
["bid"],
271 "ask": lambda x
, y
: x
["ask"],
277 def __init__(self
, value_from
, value_to
, currency
, trade_type
, market
=None):
278 # We have value_from of currency, and want to finish with value_to of
279 # that currency. value_* may not be in currency's terms
280 self
.currency
= currency
281 self
.value_from
= value_from
282 self
.value_to
= value_to
283 self
.trade_type
= trade_type
286 assert self
.value_from
.currency
== self
.value_to
.currency
287 assert self
.value_from
.linked_to
is not None and self
.value_from
.linked_to
.currency
== self
.currency
288 self
.base_currency
= self
.value_from
.currency
292 def fetch_fees(cls
, market
):
293 if market
.__class
__ not in cls
.fees_cache
:
294 cls
.fees_cache
[market
.__class
__] = market
.fetch_fees()
295 return cls
.fees_cache
[market
.__class
__]
298 ticker_cache_timestamp
= time
.time()
300 def get_ticker(cls
, c1
, c2
, market
, refresh
=False):
304 "average": (1/ticker
["bid"] + 1/ticker
["ask"]) / 2,
307 def augment_ticker(ticker
):
310 "average": (ticker
["bid"] + ticker
["ask"] ) / 2,
313 if time
.time() - cls
.ticker_cache_timestamp
> 5:
314 cls
.ticker_cache
= {}
315 cls
.ticker_cache_timestamp
= time
.time()
317 if (c1
, c2
, market
.__class
__) in cls
.ticker_cache
:
318 return cls
.ticker_cache
[(c1
, c2
, market
.__class
__)]
319 if (c2
, c1
, market
.__class
__) in cls
.ticker_cache
:
320 return invert(cls
.ticker_cache
[(c2
, c1
, market
.__class
__)])
323 cls
.ticker_cache
[(c1
, c2
, market
.__class
__)] = market
.fetch_ticker("{}/{}".format(c1
, c2
))
324 augment_ticker(cls
.ticker_cache
[(c1
, c2
, market
.__class
__)])
325 except ExchangeError
:
327 cls
.ticker_cache
[(c2
, c1
, market
.__class
__)] = market
.fetch_ticker("{}/{}".format(c2
, c1
))
328 augment_ticker(cls
.ticker_cache
[(c2
, c1
, market
.__class
__)])
329 except ExchangeError
:
330 cls
.ticker_cache
[(c1
, c2
, market
.__class
__)] = None
331 return cls
.get_ticker(c1
, c2
, market
)
334 def compute_trades(cls
, values_in_base
, new_repartition
, trade_types
, only
=None, market
=None):
335 base_currency
= sum(values_in_base
.values()).currency
336 for currency
in Balance
.currencies():
337 if currency
== base_currency
:
340 values_in_base
.get(currency
, Amount(base_currency
, 0)),
341 new_repartition
.get(currency
, Amount(base_currency
, 0)),
343 trade_types
.get(currency
, "long"),
346 if only
is None or trade
.action
== only
:
347 cls
.trades
[currency
] = trade
351 def prepare_orders(cls
, only
=None, compute_value
="default"):
352 for currency
, trade
in cls
.trades
.items():
353 if only
is None or trade
.action
== only
:
354 trade
.prepare_order(compute_value
=compute_value
)
358 if self
.value_from
== self
.value_to
:
360 if self
.base_currency
== self
.currency
:
363 if self
.value_from
< self
.value_to
:
368 def order_action(self
, inverted
):
370 if (self
.trade_type
== "short") != ((self
.value_from
< self
.value_to
) != inverted
):
375 def prepare_order(self
, compute_value
="default"):
376 if self
.action
is None:
378 ticker
= Trade
.get_ticker(self
.currency
, self
.base_currency
, self
.market
)
379 inverted
= ticker
["inverted"]
381 ticker
= ticker
["original"]
382 rate
= Trade
.compute_value(ticker
, self
.order_action(inverted
), compute_value
=compute_value
)
385 # FIXME: optimize if value_to == 0 or value_from == 0?)
387 delta_in_base
= abs(self
.value_from
- self
.value_to
)
388 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
391 currency
= self
.base_currency
393 if self
.action
== "sell":
394 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
395 # At rate 1 Foo = 0.1 BTC
396 value_from
= self
.value_from
.linked_to
397 # value_from = 100 FOO
398 value_to
= self
.value_to
.in_currency(self
.currency
, self
.market
, rate
=1/self
.value_from
.rate
)
399 # value_to = 10 FOO (1 BTC * 1/0.1)
400 delta
= abs(value_to
- value_from
)
402 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
404 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
406 delta
= delta_in_base
.in_currency(self
.currency
, self
.market
, rate
=1/rate
)
407 # I want to buy 9 / 0.1 FOO
408 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
410 currency
= self
.currency
412 delta
= delta_in_base
414 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
415 # At rate 1 Foo = 0.1 BTC
416 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
418 # I want to buy 9 / 0.1 FOO
419 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
421 self
.orders
.append(Order(self
.order_action(inverted
), delta
, rate
, currency
, self
.trade_type
, self
.market
))
424 def compute_value(cls
, ticker
, action
, compute_value
="default"):
429 if isinstance(compute_value
, str):
430 compute_value
= Computation
.computations
[compute_value
]
431 return compute_value(ticker
, action
)
434 def all_orders(cls
, state
=None):
435 all_orders
= sum(map(lambda v
: v
.orders
, cls
.trades
.values()), [])
439 return list(filter(lambda o
: o
.status
== state
, all_orders
))
443 for order
in cls
.all_orders(state
="pending"):
447 def follow_orders(cls
, verbose
=True, sleep
=30):
448 orders
= cls
.all_orders()
450 while len(orders
) != len(finished_orders
):
453 if order
in finished_orders
:
455 if order
.get_status() != "open":
456 finished_orders
.append(order
)
458 print("finished {}".format(order
))
460 print("All orders finished")
463 def update_all_orders_status(cls
):
464 for order
in cls
.all_orders(state
="open"):
468 return "Trade({} -> {} in {}, {} {})".format(
476 def print_all_with_order(cls
):
477 for trade
in cls
.trades
.values():
478 trade
.print_with_order()
480 def print_with_order(self
):
482 for order
in self
.orders
:
483 print("\t", order
, sep
="")
486 def __init__(self
, action
, amount
, rate
, base_currency
, trade_type
, market
):
490 self
.base_currency
= base_currency
492 self
.trade_type
= trade_type
494 self
.status
= "pending"
497 return "Order({} {} {} at {} {} [{}])".format(
508 if self
.trade_type
== "long":
515 return self
.status
== "pending"
519 return self
.status
== "closed" or self
.status
== "canceled" or self
.status
== "error"
521 def run(self
, debug
=False):
522 symbol
= "{}/{}".format(self
.amount
.currency
, self
.base_currency
)
523 amount
= round(self
.amount
, self
.market
.order_precision(symbol
)).value
526 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
527 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
530 if self
.action
== "sell" and self
.trade_type
== "short":
531 assert self
.market
.transfer_balance(self
.base_currency
, amount
* self
.rate
, "exchange", "margin")
532 self
.result
= self
.market
.create_order(symbol
, 'limit', self
.action
, amount
, price
=self
.rate
, account
=self
.account
)
534 except Exception as e
:
535 self
.status
= "error"
536 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
537 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
538 self
.error_message
= str("{}: {}".format(e
.__class
__.__name
__, e
))
539 print(self
.error_message
)
541 def get_status(self
):
542 # other states are "closed" and "canceled"
543 if self
.status
== "open":
544 result
= self
.market
.fetch_order(self
.result
['id'])
545 self
.status
= result
["status"]
549 self
.market
.cancel_order(self
.result
['id'])
551 def print_orders(market
, base_currency
="BTC"):
552 Balance
.prepare_trades(market
, base_currency
=base_currency
, compute_value
="average")
553 Trade
.prepare_orders(compute_value
="average")
554 for currency
, balance
in Balance
.known_balances
.items():
556 Trade
.print_all_with_order()
558 def make_orders(market
, base_currency
="BTC"):
559 Balance
.prepare_trades(market
, base_currency
=base_currency
)
560 for currency
, trade
in Trade
.trades
.items():
562 for order
in trade
.orders
:
563 print("\t", order
, sep
="")
566 if __name__
== '__main__':