diff options
Diffstat (limited to 'portfolio.py')
-rw-r--r-- | portfolio.py | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/portfolio.py b/portfolio.py new file mode 100644 index 0000000..507f796 --- /dev/null +++ b/portfolio.py | |||
@@ -0,0 +1,430 @@ | |||
1 | import ccxt | ||
2 | import time | ||
3 | # Put your poloniex api key in market.py | ||
4 | from market import market | ||
5 | |||
6 | # FIXME: Améliorer le bid/ask | ||
7 | # FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition | ||
8 | # FIXME: better compute moves to avoid rounding errors | ||
9 | |||
10 | def static_var(varname, value): | ||
11 | def decorate(func): | ||
12 | setattr(func, varname, value) | ||
13 | return func | ||
14 | return decorate | ||
15 | |||
16 | class Portfolio: | ||
17 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | ||
18 | liquidities = {} | ||
19 | data = None | ||
20 | |||
21 | @classmethod | ||
22 | def repartition_pertenthousand(cls, liquidity="medium"): | ||
23 | cls.parse_cryptoportfolio() | ||
24 | liquidities = cls.liquidities[liquidity] | ||
25 | last_date = sorted(liquidities.keys())[-1] | ||
26 | return liquidities[last_date] | ||
27 | |||
28 | @classmethod | ||
29 | def get_cryptoportfolio(cls): | ||
30 | import json | ||
31 | import urllib3 | ||
32 | urllib3.disable_warnings() | ||
33 | http = urllib3.PoolManager() | ||
34 | |||
35 | r = http.request("GET", cls.URL) | ||
36 | cls.data = json.loads(r.data) | ||
37 | |||
38 | @classmethod | ||
39 | def parse_cryptoportfolio(cls): | ||
40 | if cls.data is None: | ||
41 | cls.get_cryptoportfolio() | ||
42 | |||
43 | def filter_weights(weight_hash): | ||
44 | if weight_hash[1] == 0: | ||
45 | return False | ||
46 | if weight_hash[0] == "_row": | ||
47 | return False | ||
48 | return True | ||
49 | |||
50 | def clean_weights(i): | ||
51 | def clean_weights_(h): | ||
52 | if type(h[1][i]) == str: | ||
53 | return [h[0], h[1][i]] | ||
54 | else: | ||
55 | return [h[0], int(h[1][i] * 10000)] | ||
56 | return clean_weights_ | ||
57 | |||
58 | def parse_weights(portfolio_hash): | ||
59 | # FIXME: we'll need shorts at some point | ||
60 | assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"])) | ||
61 | weights_hash = portfolio_hash["weights"] | ||
62 | weights = {} | ||
63 | for i in range(len(weights_hash["_row"])): | ||
64 | weights[weights_hash["_row"][i]] = dict(filter( | ||
65 | filter_weights, | ||
66 | map(clean_weights(i), weights_hash.items()))) | ||
67 | return weights | ||
68 | |||
69 | high_liquidity = parse_weights(cls.data["portfolio_1"]) | ||
70 | medium_liquidity = parse_weights(cls.data["portfolio_2"]) | ||
71 | |||
72 | cls.liquidities = { | ||
73 | "medium": medium_liquidity, | ||
74 | "high": high_liquidity, | ||
75 | } | ||
76 | |||
77 | class Amount: | ||
78 | MAX_DIGITS = 18 | ||
79 | |||
80 | def __init__(self, currency, value, int_val=None, linked_to=None, ticker=None): | ||
81 | self.currency = currency | ||
82 | if int_val is None: | ||
83 | self._value = int(value * 10**self.MAX_DIGITS) | ||
84 | else: | ||
85 | self._value = int_val | ||
86 | self.linked_to = linked_to | ||
87 | self.ticker = ticker | ||
88 | |||
89 | self.ticker_cache = {} | ||
90 | self.ticker_cache_timestamp = time.time() | ||
91 | |||
92 | @property | ||
93 | def value(self): | ||
94 | return self._value / 10 ** self.MAX_DIGITS | ||
95 | |||
96 | def in_currency(self, other_currency, market, action="average"): | ||
97 | if other_currency == self.currency: | ||
98 | return self | ||
99 | asset_ticker = self.get_ticker(other_currency, market) | ||
100 | if asset_ticker is not None: | ||
101 | return Amount( | ||
102 | other_currency, | ||
103 | 0, | ||
104 | int_val=int(self._value * asset_ticker[action]), | ||
105 | linked_to=self, | ||
106 | ticker=asset_ticker) | ||
107 | else: | ||
108 | raise Exception("This asset is not available in the chosen market") | ||
109 | |||
110 | def get_ticker(self, c2, market, refresh=False): | ||
111 | c1 = self.currency | ||
112 | |||
113 | def invert(ticker): | ||
114 | return { | ||
115 | "inverted": True, | ||
116 | "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2, | ||
117 | "notInverted": ticker, | ||
118 | } | ||
119 | def augment_ticker(ticker): | ||
120 | ticker.update({ | ||
121 | "inverted": False, | ||
122 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | ||
123 | }) | ||
124 | |||
125 | if time.time() - self.ticker_cache_timestamp > 5: | ||
126 | self.ticker_cache = {} | ||
127 | self.ticker_cache_timestamp = time.time() | ||
128 | elif not refresh: | ||
129 | if (c1, c2, market.__class__) in self.ticker_cache: | ||
130 | return self.ticker_cache[(c1, c2, market.__class__)] | ||
131 | if (c2, c1, market.__class__) in self.ticker_cache: | ||
132 | return invert(self.ticker_cache[(c2, c1, market.__class__)]) | ||
133 | |||
134 | try: | ||
135 | self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | ||
136 | augment_ticker(self.ticker_cache[(c1, c2, market.__class__)]) | ||
137 | except ccxt.ExchangeError: | ||
138 | try: | ||
139 | self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | ||
140 | augment_ticker(self.ticker_cache[(c2, c1, market.__class__)]) | ||
141 | except ccxt.ExchangeError: | ||
142 | self.ticker_cache[(c1, c2, market.__class__)] = None | ||
143 | return self.get_ticker(c2, market) | ||
144 | |||
145 | def __abs__(self): | ||
146 | return Amount(self.currency, 0, int_val=abs(self._value)) | ||
147 | |||
148 | def __add__(self, other): | ||
149 | if other.currency != self.currency and other._value * self._value != 0: | ||
150 | raise Exception("Summing amounts must be done with same currencies") | ||
151 | return Amount(self.currency, 0, int_val=self._value + other._value) | ||
152 | |||
153 | def __radd__(self, other): | ||
154 | if other == 0: | ||
155 | return self | ||
156 | else: | ||
157 | return self.__add__(other) | ||
158 | |||
159 | def __sub__(self, other): | ||
160 | if other.currency != self.currency and other._value * self._value != 0: | ||
161 | raise Exception("Summing amounts must be done with same currencies") | ||
162 | return Amount(self.currency, 0, int_val=self._value - other._value) | ||
163 | |||
164 | def __int__(self): | ||
165 | return self._value | ||
166 | |||
167 | def __mul__(self, value): | ||
168 | if type(value) != int and type(value) != float: | ||
169 | raise TypeError("Amount may only be multiplied by numbers") | ||
170 | return Amount(self.currency, 0, int_val=(self._value * value)) | ||
171 | |||
172 | def __rmul__(self, value): | ||
173 | return self.__mul__(value) | ||
174 | |||
175 | def __floordiv__(self, value): | ||
176 | if type(value) != int: | ||
177 | raise TypeError("Amount may only be multiplied by integers") | ||
178 | return Amount(self.currency, 0, int_val=(self._value // value)) | ||
179 | |||
180 | def __truediv__(self, value): | ||
181 | return self.__floordiv__(value) | ||
182 | |||
183 | def __lt__(self, other): | ||
184 | if self.currency != other.currency: | ||
185 | raise Exception("Comparing amounts must be done with same currencies") | ||
186 | return self._value < other._value | ||
187 | |||
188 | def __eq__(self, other): | ||
189 | if other == 0: | ||
190 | return self._value == 0 | ||
191 | if self.currency != other.currency: | ||
192 | raise Exception("Comparing amounts must be done with same currencies") | ||
193 | return self._value == other._value | ||
194 | |||
195 | def __str__(self): | ||
196 | if self.linked_to is None: | ||
197 | return "{:.8f} {}".format(self.value, self.currency) | ||
198 | else: | ||
199 | return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to) | ||
200 | |||
201 | def __repr__(self): | ||
202 | if self.linked_to is None: | ||
203 | return "Amount({:.8f} {})".format(self.value, self.currency) | ||
204 | else: | ||
205 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) | ||
206 | |||
207 | class Balance: | ||
208 | known_balances = {} | ||
209 | trades = {} | ||
210 | |||
211 | def __init__(self, currency, total_value, free_value, used_value): | ||
212 | self.currency = currency | ||
213 | self.total = Amount(currency, total_value) | ||
214 | self.free = Amount(currency, free_value) | ||
215 | self.used = Amount(currency, used_value) | ||
216 | |||
217 | @classmethod | ||
218 | def in_currency(cls, other_currency, market, action="average", type="total"): | ||
219 | amounts = {} | ||
220 | for currency in cls.known_balances: | ||
221 | balance = cls.known_balances[currency] | ||
222 | other_currency_amount = getattr(balance, type)\ | ||
223 | .in_currency(other_currency, market, action=action) | ||
224 | amounts[currency] = other_currency_amount | ||
225 | return amounts | ||
226 | |||
227 | @classmethod | ||
228 | def currencies(cls): | ||
229 | return cls.known_balances.keys() | ||
230 | |||
231 | @classmethod | ||
232 | def from_hash(cls, currency, hash_): | ||
233 | return cls(currency, hash_["total"], hash_["free"], hash_["used"]) | ||
234 | |||
235 | @classmethod | ||
236 | def _fill_balances(cls, hash_): | ||
237 | for key in hash_: | ||
238 | if key in ["info", "free", "used", "total"]: | ||
239 | continue | ||
240 | if hash_[key]["total"] > 0: | ||
241 | cls.known_balances[key] = cls.from_hash(key, hash_[key]) | ||
242 | |||
243 | @classmethod | ||
244 | def fetch_balances(cls, market): | ||
245 | cls._fill_balances(market.fetch_balance()) | ||
246 | return cls.known_balances | ||
247 | |||
248 | @classmethod | ||
249 | def dispatch_assets(cls, amount): | ||
250 | repartition_pertenthousand = Portfolio.repartition_pertenthousand() | ||
251 | sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()]) | ||
252 | amounts = {} | ||
253 | for currency, ptt in repartition_pertenthousand.items(): | ||
254 | amounts[currency] = ptt * amount / sum_pertenthousand | ||
255 | if currency not in cls.known_balances: | ||
256 | cls.known_balances[currency] = cls(currency, 0, 0, 0) | ||
257 | return amounts | ||
258 | |||
259 | @classmethod | ||
260 | def prepare_trades(cls, market, base_currency="BTC"): | ||
261 | cls.fetch_balances(market) | ||
262 | values_in_base = cls.in_currency(base_currency, market) | ||
263 | total_base_value = sum(values_in_base.values()) | ||
264 | new_repartition = cls.dispatch_assets(total_base_value) | ||
265 | Trade.compute_trades(values_in_base, new_repartition, market=market) | ||
266 | |||
267 | def __int__(self): | ||
268 | return int(self.total) | ||
269 | |||
270 | def __repr__(self): | ||
271 | return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) | ||
272 | |||
273 | class Trade: | ||
274 | trades = {} | ||
275 | |||
276 | def __init__(self, value_from, value_to, currency, market=None): | ||
277 | # We have value_from of currency, and want to finish with value_to of | ||
278 | # that currency. value_* may not be in currency's terms | ||
279 | self.currency = currency | ||
280 | self.value_from = value_from | ||
281 | self.value_to = value_to | ||
282 | self.orders = [] | ||
283 | assert self.value_from.currency == self.value_to.currency | ||
284 | assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency | ||
285 | self.base_currency = self.value_from.currency | ||
286 | |||
287 | if market is not None: | ||
288 | self.prepare_order(market) | ||
289 | |||
290 | @classmethod | ||
291 | def compute_trades(cls, values_in_base, new_repartition, market=None): | ||
292 | base_currency = sum(values_in_base.values()).currency | ||
293 | for currency in Balance.currencies(): | ||
294 | if currency == base_currency: | ||
295 | continue | ||
296 | cls.trades[currency] = cls( | ||
297 | values_in_base.get(currency, Amount(base_currency, 0)), | ||
298 | new_repartition.get(currency, Amount(base_currency, 0)), | ||
299 | currency, | ||
300 | market=market | ||
301 | ) | ||
302 | return cls.trades | ||
303 | |||
304 | @property | ||
305 | def action(self): | ||
306 | if self.value_from == self.value_to: | ||
307 | return None | ||
308 | if self.base_currency == self.currency: | ||
309 | return None | ||
310 | |||
311 | if self.value_from < self.value_to: | ||
312 | return "buy" | ||
313 | else: | ||
314 | return "sell" | ||
315 | |||
316 | def ticker_action(self, inverted): | ||
317 | if self.value_from < self.value_to: | ||
318 | return "ask" if not inverted else "bid" | ||
319 | else: | ||
320 | return "bid" if not inverted else "ask" | ||
321 | |||
322 | def prepare_order(self, market): | ||
323 | if self.action is None: | ||
324 | return | ||
325 | ticker = self.value_from.ticker | ||
326 | inverted = ticker["inverted"] | ||
327 | |||
328 | if not inverted: | ||
329 | value_from = self.value_from.linked_to | ||
330 | value_to = self.value_to.in_currency(self.currency, market) | ||
331 | delta = abs(value_to - value_from) | ||
332 | currency = self.base_currency | ||
333 | else: | ||
334 | ticker = ticker["notInverted"] | ||
335 | delta = abs(self.value_to - self.value_from) | ||
336 | currency = self.currency | ||
337 | |||
338 | rate = ticker[self.ticker_action(inverted)] | ||
339 | |||
340 | self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency)) | ||
341 | |||
342 | @classmethod | ||
343 | def all_orders(cls): | ||
344 | return sum(map(lambda v: v.orders, cls.trades.values()), []) | ||
345 | |||
346 | @classmethod | ||
347 | def follow_orders(cls, market): | ||
348 | orders = cls.all_orders() | ||
349 | finished_orders = [] | ||
350 | while len(orders) != len(finished_orders): | ||
351 | time.sleep(30) | ||
352 | for order in orders: | ||
353 | if order in finished_orders: | ||
354 | continue | ||
355 | if order.get_status(market) != "open": | ||
356 | finished_orders.append(order) | ||
357 | print("finished {}".format(order)) | ||
358 | print("All orders finished") | ||
359 | |||
360 | def __repr__(self): | ||
361 | return "Trade({} -> {} in {}, {})".format( | ||
362 | self.value_from, | ||
363 | self.value_to, | ||
364 | self.currency, | ||
365 | self.action) | ||
366 | |||
367 | class Order: | ||
368 | DEBUG = True | ||
369 | |||
370 | def __init__(self, action, amount, rate, base_currency): | ||
371 | self.action = action | ||
372 | self.amount = amount | ||
373 | self.rate = rate | ||
374 | self.base_currency = base_currency | ||
375 | self.result = None | ||
376 | self.status = "not run" | ||
377 | |||
378 | def __repr__(self): | ||
379 | return "Order({} {} at {} {} [{}])".format( | ||
380 | self.action, | ||
381 | self.amount, | ||
382 | self.rate, | ||
383 | self.base_currency, | ||
384 | self.status | ||
385 | ) | ||
386 | |||
387 | def run(self, market): | ||
388 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | ||
389 | amount = self.amount.value | ||
390 | |||
391 | if self.DEBUG: | ||
392 | print("market.create_order('{}', 'limit', '{}', {}, price={})".format( | ||
393 | symbol, self.action, amount, self.rate)) | ||
394 | else: | ||
395 | try: | ||
396 | self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate) | ||
397 | self.status = "open" | ||
398 | except Exception: | ||
399 | pass | ||
400 | |||
401 | def get_status(self, market): | ||
402 | # other states are "closed" and "canceled" | ||
403 | if self.status == "open": | ||
404 | result = market.fetch_order(self.result['id']) | ||
405 | self.status = result["status"] | ||
406 | return self.status | ||
407 | |||
408 | @static_var("cache", {}) | ||
409 | def fetch_fees(market): | ||
410 | if market.__class__ not in fetch_fees.cache: | ||
411 | fetch_fees.cache[market.__class__] = market.fetch_fees() | ||
412 | return fetch_fees.cache[market.__class__] | ||
413 | |||
414 | def print_orders(market, base_currency="BTC"): | ||
415 | Balance.prepare_trades(market, base_currency=base_currency) | ||
416 | for currency, trade in Trade.trades.items(): | ||
417 | print(trade) | ||
418 | for order in trade.orders: | ||
419 | print("\t", order, sep="") | ||
420 | |||
421 | def make_orders(market, base_currency="BTC"): | ||
422 | Balance.prepare_trades(market, base_currency=base_currency) | ||
423 | for currency, trade in Trade.trades.items(): | ||
424 | print(trade) | ||
425 | for order in trade.orders: | ||
426 | print("\t", order, sep="") | ||
427 | order.run(market) | ||
428 | |||
429 | if __name__ == '__main__': | ||
430 | print_orders(market) | ||