2 from decimal
import Decimal
as D
, ROUND_DOWN
3 # Put your poloniex api key in market.py
4 from json
import JSONDecodeError
9 # FIXME: correctly handle web call timeouts
12 URL
= "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
17 def repartition(cls
, liquidity
="medium"):
18 cls
.parse_cryptoportfolio()
19 liquidities
= cls
.liquidities
[liquidity
]
20 cls
.last_date
= sorted(liquidities
.keys())[-1]
21 return liquidities
[cls
.last_date
]
24 def get_cryptoportfolio(cls
):
26 r
= requests
.get(cls
.URL
)
30 cls
.data
= r
.json(parse_int
=D
, parse_float
=D
)
31 except JSONDecodeError
:
35 def parse_cryptoportfolio(cls
):
37 cls
.get_cryptoportfolio()
39 def filter_weights(weight_hash
):
40 if weight_hash
[1][0] == 0:
42 if weight_hash
[0] == "_row":
47 def clean_weights_(h
):
48 if h
[0].endswith("s"):
49 return [h
[0][0:-1], (h
[1][i
], "short")]
51 return [h
[0], (h
[1][i
], "long")]
54 def parse_weights(portfolio_hash
):
55 weights_hash
= portfolio_hash
["weights"]
57 for i
in range(len(weights_hash
["_row"])):
58 weights
[weights_hash
["_row"][i
]] = dict(filter(
60 map(clean_weights(i
), weights_hash
.items())))
63 high_liquidity
= parse_weights(cls
.data
["portfolio_1"])
64 medium_liquidity
= parse_weights(cls
.data
["portfolio_2"])
67 "medium": medium_liquidity
,
68 "high": high_liquidity
,
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"],
80 def compute_value(cls
, ticker
, action
, compute_value
="default"):
85 if isinstance(compute_value
, str):
86 compute_value
= cls
.computations
[compute_value
]
87 return compute_value(ticker
, action
)
90 def __init__(self
, currency
, value
, linked_to
=None, ticker
=None, rate
=None):
91 self
.currency
= currency
93 self
.linked_to
= linked_to
97 def in_currency(self
, other_currency
, market
, rate
=None, action
=None, compute_value
="average"):
98 if other_currency
== self
.currency
:
106 asset_ticker
= h
.get_ticker(self
.currency
, other_currency
, market
)
107 if asset_ticker
is not None:
108 rate
= Computation
.compute_value(asset_ticker
, action
, compute_value
=compute_value
)
116 raise Exception("This asset is not available in the chosen market")
118 def __round__(self
, n
=8):
119 return Amount(self
.currency
, self
.value
.quantize(D(1)/D(10**n
), rounding
=ROUND_DOWN
))
122 return Amount(self
.currency
, abs(self
.value
))
124 def __add__(self
, other
):
125 if other
.currency
!= self
.currency
and other
.value
* self
.value
!= 0:
126 raise Exception("Summing amounts must be done with same currencies")
127 return Amount(self
.currency
, self
.value
+ other
.value
)
129 def __radd__(self
, other
):
133 return self
.__add
__(other
)
135 def __sub__(self
, other
):
138 if other
.currency
!= self
.currency
and other
.value
* self
.value
!= 0:
139 raise Exception("Summing amounts must be done with same currencies")
140 return Amount(self
.currency
, self
.value
- other
.value
)
142 def __mul__(self
, value
):
143 if not isinstance(value
, (int, float, D
)):
144 raise TypeError("Amount may only be multiplied by numbers")
145 return Amount(self
.currency
, self
.value
* value
)
147 def __rmul__(self
, value
):
148 return self
.__mul
__(value
)
150 def __floordiv__(self
, value
):
151 if not isinstance(value
, (int, float, D
)):
152 raise TypeError("Amount may only be multiplied by integers")
153 return Amount(self
.currency
, self
.value
/ value
)
155 def __truediv__(self
, value
):
156 return self
.__floordiv
__(value
)
158 def __lt__(self
, other
):
160 return self
.value
< 0
161 if self
.currency
!= other
.currency
:
162 raise Exception("Comparing amounts must be done with same currencies")
163 return self
.value
< other
.value
165 def __le__(self
, other
):
166 return self
== other
or self
< other
168 def __gt__(self
, other
):
169 return not self
<= other
171 def __ge__(self
, other
):
172 return not self
< other
174 def __eq__(self
, other
):
176 return self
.value
== 0
177 if self
.currency
!= other
.currency
:
178 raise Exception("Comparing amounts must be done with same currencies")
179 return self
.value
== other
.value
181 def __ne__(self
, other
):
182 return not self
== other
185 return Amount(self
.currency
, - self
.value
)
188 if self
.linked_to
is None:
189 return "{:.8f} {}".format(self
.value
, self
.currency
)
191 return "{:.8f} {} [{}]".format(self
.value
, self
.currency
, self
.linked_to
)
194 if self
.linked_to
is None:
195 return "Amount({:.8f} {})".format(self
.value
, self
.currency
)
197 return "Amount({:.8f} {} -> {})".format(self
.value
, self
.currency
, repr(self
.linked_to
))
201 def __init__(self
, currency
, hash_
):
202 self
.currency
= currency
204 "exchange_total", "exchange_used", "exchange_free",
205 "margin_total", "margin_borrowed", "margin_free"]:
206 setattr(self
, key
, Amount(currency
, hash_
.get(key
, 0)))
208 self
.margin_position_type
= hash_
.get("margin_position_type")
210 if hash_
.get("margin_borrowed_base_currency") is not None:
211 base_currency
= hash_
["margin_borrowed_base_currency"]
213 "margin_liquidation_price",
214 "margin_pending_gain",
215 "margin_lending_fees",
216 "margin_borrowed_base_price"
218 setattr(self
, key
, Amount(base_currency
, hash_
.get(key
, 0)))
221 if self
.exchange_total
> 0:
222 if self
.exchange_free
> 0 and self
.exchange_used
> 0:
223 exchange
= " Exch: [✔{} + ❌{} = {}]".format(str(self
.exchange_free
), str(self
.exchange_used
), str(self
.exchange_total
))
224 elif self
.exchange_free
> 0:
225 exchange
= " Exch: [✔{}]".format(str(self
.exchange_free
))
227 exchange
= " Exch: [❌{}]".format(str(self
.exchange_used
))
231 if self
.margin_total
> 0:
232 if self
.margin_free
!= 0 and self
.margin_borrowed
!= 0:
233 margin
= " Margin: [✔{} + borrowed {} = {}]".format(str(self
.margin_free
), str(self
.margin_borrowed
), str(self
.margin_total
))
234 elif self
.margin_free
!= 0:
235 margin
= " Margin: [✔{}]".format(str(self
.margin_free
))
237 margin
= " Margin: [borrowed {}]".format(str(self
.margin_borrowed
))
238 elif self
.margin_total
< 0:
239 margin
= " Margin: [{} @@ {}/{}]".format(str(self
.margin_total
),
240 str(self
.margin_borrowed_base_price
),
241 str(self
.margin_lending_fees
))
245 if self
.margin_total
!= 0 and self
.exchange_total
!= 0:
246 total
= " Total: [{}]".format(str(self
.total
))
250 return "Balance({}".format(self
.currency
) + "".join([exchange
, margin
, total
]) + ")"
253 def __init__(self
, value_from
, value_to
, currency
, market
=None):
254 # We have value_from of currency, and want to finish with value_to of
255 # that currency. value_* may not be in currency's terms
256 self
.currency
= currency
257 self
.value_from
= value_from
258 self
.value_to
= value_to
261 assert self
.value_from
.currency
== self
.value_to
.currency
262 if self
.value_from
!= 0:
263 assert self
.value_from
.linked_to
is not None and self
.value_from
.linked_to
.currency
== self
.currency
264 elif self
.value_from
.linked_to
is None:
265 self
.value_from
.linked_to
= Amount(self
.currency
, 0)
266 self
.base_currency
= self
.value_from
.currency
270 if self
.value_from
== self
.value_to
:
272 if self
.base_currency
== self
.currency
:
275 if self
.value_from
< self
.value_to
:
280 def order_action(self
, inverted
):
281 if (self
.value_from
< self
.value_to
) != inverted
:
287 def trade_type(self
):
288 if self
.value_from
+ self
.value_to
< 0:
294 def filled_amount(self
):
296 for order
in self
.orders
:
297 filled_amount
+= order
.filled_amount
300 def update_order(self
, order
, tick
):
302 if tick
in [0, 1, 3, 4, 6]:
303 print("{}, tick {}, waiting".format(order
, tick
))
305 self
.prepare_order(compute_value
=lambda x
, y
: (x
[y
] + x
["average"]) / 2)
306 new_order
= self
.orders
[-1]
307 print("{}, tick {}, cancelling and adjusting to {}".format(order
, tick
, new_order
))
309 self
.prepare_order(compute_value
=lambda x
, y
: (x
[y
]*2 + x
["average"]) / 3)
310 new_order
= self
.orders
[-1]
311 print("{}, tick {}, cancelling and adjusting to {}".format(order
, tick
, new_order
))
314 print("{}, tick {}, fallbacking to market value".format(order
, tick
))
315 if (tick
- 7) % 3 == 0:
316 self
.prepare_order(compute_value
="default")
317 new_order
= self
.orders
[-1]
318 print("{}, tick {}, market value, cancelling and adjusting to {}".format(order
, tick
, new_order
))
320 if new_order
is not None:
324 def prepare_order(self
, compute_value
="default"):
325 if self
.action
is None:
327 ticker
= h
.get_ticker(self
.currency
, self
.base_currency
, self
.market
)
328 inverted
= ticker
["inverted"]
330 ticker
= ticker
["original"]
331 rate
= Computation
.compute_value(ticker
, self
.order_action(inverted
), compute_value
=compute_value
)
334 delta_in_base
= abs(self
.value_from
- self
.value_to
)
335 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
338 currency
= self
.base_currency
340 if self
.action
== "dispose":
341 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
342 # At rate 1 Foo = 0.1 BTC
343 value_from
= self
.value_from
.linked_to
344 # value_from = 100 FOO
345 value_to
= self
.value_to
.in_currency(self
.currency
, self
.market
, rate
=1/self
.value_from
.rate
)
346 # value_to = 10 FOO (1 BTC * 1/0.1)
347 delta
= abs(value_to
- value_from
)
349 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
351 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
353 delta
= delta_in_base
.in_currency(self
.currency
, self
.market
, rate
=1/rate
)
354 # I want to buy 9 / 0.1 FOO
355 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
357 currency
= self
.currency
359 delta
= delta_in_base
361 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
362 # At rate 1 Foo = 0.1 BTC
363 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
365 # I want to buy 9 / 0.1 FOO
366 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
367 if self
.value_to
== 0:
368 rate
= self
.value_from
.linked_to
.value
/ self
.value_from
.value
369 # Recompute the rate to avoid any rounding error
371 close_if_possible
= (self
.value_to
== 0)
373 if delta
<= self
.filled_amount
:
374 print("Less to do than already filled: {} <= {}".format(delta
,
378 self
.orders
.append(Order(self
.order_action(inverted
),
379 delta
- self
.filled_amount
, rate
, currency
, self
.trade_type
,
380 self
.market
, self
, close_if_possible
=close_if_possible
))
383 return "Trade({} -> {} in {}, {})".format(
389 def print_with_order(self
):
391 for order
in self
.orders
:
392 print("\t", order
, sep
="")
395 def __init__(self
, action
, amount
, rate
, base_currency
, trade_type
, market
,
396 trade
, close_if_possible
=False):
400 self
.base_currency
= base_currency
402 self
.trade_type
= trade_type
405 self
.status
= "pending"
407 self
.close_if_possible
= close_if_possible
410 return "Order({} {} {} at {} {} [{}]{})".format(
417 " ✂" if self
.close_if_possible
else "",
422 if self
.trade_type
== "long":
429 return self
.status
== "pending"
433 return self
.status
== "closed" or self
.status
== "canceled" or self
.status
== "error"
437 return self
.results
[0]["id"]
440 symbol
= "{}/{}".format(self
.amount
.currency
, self
.base_currency
)
441 amount
= round(self
.amount
, self
.market
.order_precision(symbol
)).value
444 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
445 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
447 self
.results
.append({"debug": True, "id": -1}
)
450 self
.results
.append(self
.market
.create_order(symbol
, 'limit', self
.action
, amount
, price
=self
.rate
, account
=self
.account
))
452 except Exception as e
:
453 self
.status
= "error"
454 print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
455 symbol
, self
.action
, amount
, self
.rate
, self
.account
))
456 self
.error_message
= str("{}: {}".format(e
.__class
__.__name
__, e
))
457 print(self
.error_message
)
459 def get_status(self
):
462 # other states are "closed" and "canceled"
463 if self
.status
== "open":
465 if self
.status
!= "open":
466 self
.mark_finished_order()
469 def mark_finished_order(self
):
472 if self
.status
== "closed":
473 if self
.trade_type
== "short" and self
.action
== "buy" and self
.close_if_possible
:
474 self
.market
.close_margin_position(self
.amount
.currency
, self
.base_currency
)
476 fetch_cache_timestamp
= None
477 def fetch(self
, force
=False):
478 if TradeStore
.debug
or (not force
and self
.fetch_cache_timestamp
is not None
479 and time
.time() - self
.fetch_cache_timestamp
< 10):
481 self
.fetch_cache_timestamp
= time
.time()
483 self
.results
.append(self
.market
.fetch_order(self
.id))
484 result
= self
.results
[-1]
485 self
.status
= result
["status"]
486 # Time at which the order started
487 self
.timestamp
= result
["datetime"]
488 self
.fetch_mouvements()
490 # FIXME: consider open order with dust remaining as closed
493 def dust_amount_remaining(self
):
494 return self
.remaining_amount
< 0.001
497 def remaining_amount(self
):
498 if self
.status
== "open":
500 return self
.amount
- self
.filled_amount
503 def filled_amount(self
):
504 if self
.status
== "open":
506 filled_amount
= Amount(self
.amount
.currency
, 0)
507 for mouvement
in self
.mouvements
:
508 filled_amount
+= mouvement
.total
511 def fetch_mouvements(self
):
512 mouvements
= self
.market
.privatePostReturnOrderTrades({"orderNumber": self.id}
)
515 for mouvement_hash
in mouvements
:
516 self
.mouvements
.append(Mouvement(self
.amount
.currency
,
517 self
.base_currency
, mouvement_hash
))
521 self
.status
= "canceled"
523 self
.market
.cancel_order(self
.result
['id'])
527 def __init__(self
, currency
, base_currency
, hash_
):
528 self
.currency
= currency
529 self
.base_currency
= base_currency
530 self
.id = hash_
["id"]
531 self
.action
= hash_
["type"]
532 self
.fee_rate
= D(hash_
["fee"])
533 self
.date
= datetime
.strptime(hash_
["date"], '%Y-%m-%d %H:%M:%S')
534 self
.rate
= D(hash_
["rate"])
535 self
.total
= Amount(currency
, hash_
["amount"])
536 # rate * total = total_in_base
537 self
.total_in_base
= Amount(base_currency
, hash_
["total"])
539 if __name__
== '__main__':
540 from market
import market
541 h
.print_orders(market
)