diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-16 00:44:20 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-16 01:56:04 +0100 |
commit | dd359bc0617a915909efb2ef37048192c0639836 (patch) | |
tree | 0d9eb997f313ab883e584ae7c68f2584deb63710 | |
parent | b2dab3acfb8cb23bdb54298c88aecfb7042fb8d5 (diff) | |
download | Trader-jl/dev.tar.gz Trader-jl/dev.tar.zst Trader-jl/dev.zip |
Complete refactor of the script to use classesjl/dev
-rw-r--r-- | portfolio.py | 430 | ||||
-rw-r--r-- | script.py | 218 | ||||
-rw-r--r-- | test.py | 174 |
3 files changed, 604 insertions, 218 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) | ||
diff --git a/script.py b/script.py deleted file mode 100644 index 187ff73..0000000 --- a/script.py +++ /dev/null | |||
@@ -1,218 +0,0 @@ | |||
1 | import ccxt | ||
2 | # Put your poloniex api key in market.py | ||
3 | from market import market | ||
4 | |||
5 | def static_var(varname, value): | ||
6 | def decorate(func): | ||
7 | setattr(func, varname, value) | ||
8 | return func | ||
9 | return decorate | ||
10 | |||
11 | max_digits = 18 | ||
12 | |||
13 | repartition_pertenthousand = { | ||
14 | "BTC": 2857, | ||
15 | "ZEC": 3701, | ||
16 | "DOGE": 1805, | ||
17 | "DGB": 1015, | ||
18 | "SC": 623, | ||
19 | } | ||
20 | |||
21 | |||
22 | def formatted_price(value): | ||
23 | return round(value / 10**max_digits, 8) | ||
24 | |||
25 | @static_var("cache", {}) | ||
26 | def get_ticker(c1, c2, market): | ||
27 | def invert(ticker): | ||
28 | return { | ||
29 | "inverted": True, | ||
30 | "bid": float(1/ticker["ask"]), | ||
31 | "ask": float(1/ticker["bid"]), | ||
32 | "bidA": float(1/ticker["askA"]), | ||
33 | "askA": float(1/ticker["bidA"]), | ||
34 | "bidE": float(1/ticker["askE"]), | ||
35 | "askE": float(1/ticker["bidE"]), | ||
36 | } | ||
37 | def augment_ticker(ticker): | ||
38 | bid_factor = 1.01 | ||
39 | ask_factor = 0.99 | ||
40 | fees = fetch_fees(market) | ||
41 | # FIXME: need to do better than just a multiplier | ||
42 | ticker.update({ | ||
43 | "inverted": False, | ||
44 | # Adjusted | ||
45 | "bidA": ticker["bid"] * bid_factor, | ||
46 | "askA": ticker["ask"] * ask_factor, | ||
47 | # Expected in the end | ||
48 | "bidE": ticker["bid"] * bid_factor * (1 - fees["maker"]), | ||
49 | "askE": ticker["ask"] * ask_factor * (1 - fees["maker"]), | ||
50 | # fees | ||
51 | "bidF": ticker["bid"] * bid_factor * fees["maker"], | ||
52 | "askF": ticker["ask"] * ask_factor * fees["maker"], | ||
53 | }) | ||
54 | |||
55 | if (c1, c2, market.__class__) in get_ticker.cache: | ||
56 | return get_ticker.cache[(c1, c2, market.__class__)] | ||
57 | if (c2, c1, market.__class__) in get_ticker.cache: | ||
58 | return invert(get_ticker.cache[(c2, c1, market.__class__)]) | ||
59 | |||
60 | try: | ||
61 | get_ticker.cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | ||
62 | augment_ticker(get_ticker.cache[(c1, c2, market.__class__)]) | ||
63 | except ccxt.ExchangeError: | ||
64 | try: | ||
65 | get_ticker.cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | ||
66 | augment_ticker(get_ticker.cache[(c2, c1, market.__class__)]) | ||
67 | except ccxt.ExchangeError: | ||
68 | get_ticker.cache[(c1, c2, market.__class__)] = None | ||
69 | return get_ticker(c1, c2, market) | ||
70 | |||
71 | def fetch_balances(market): | ||
72 | balances = {} | ||
73 | fetched_balance = market.fetch_balance() | ||
74 | for key, value in fetched_balance["total"].items(): | ||
75 | if value > 0: | ||
76 | balances[key] = int(value * 10**max_digits) | ||
77 | return balances | ||
78 | |||
79 | @static_var("cache", {}) | ||
80 | def fetch_fees(market): | ||
81 | if market.__class__ not in fetch_fees.cache: | ||
82 | fetch_fees.cache[market.__class__] = market.fetch_fees() | ||
83 | return fetch_fees.cache[market.__class__] | ||
84 | |||
85 | def assets_value(assets, market, base_currency="BTC"): | ||
86 | repartition_in_base_currency = {} | ||
87 | for currency, asset_value in assets.items(): | ||
88 | if currency == base_currency: | ||
89 | repartition_in_base_currency[currency] = [asset_value, 0] | ||
90 | else: | ||
91 | asset_ticker = get_ticker(currency, base_currency, market) | ||
92 | if asset_ticker is None: | ||
93 | raise Exception("This asset is not available in the chosen market") | ||
94 | repartition_in_base_currency[currency] = [ | ||
95 | int(asset_ticker["bidE"] * asset_value), | ||
96 | int(asset_ticker["bidF"] * asset_value) | ||
97 | ] | ||
98 | |||
99 | return repartition_in_base_currency | ||
100 | |||
101 | def dispatch_assets(base_currency_value, repartition_pertenthousand, market, base_currency="BTC"): | ||
102 | sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()]) | ||
103 | repartition_in_base_currency = {} | ||
104 | for currency, ptt in repartition_pertenthousand.items(): | ||
105 | repartition_in_base_currency[currency] = int(ptt * base_currency_value / sum_pertenthousand) | ||
106 | return repartition_in_base_currency | ||
107 | |||
108 | def compute_moves(current_assets, repartition_pertenthousand, market, no_fees=True, base_currency="BTC"): | ||
109 | value_in_base = assets_value(current_assets, market, base_currency=base_currency) | ||
110 | total_base_value = sum([ v[0] for k, v in value_in_base.items()]) | ||
111 | |||
112 | new_repartition = dispatch_assets(total_base_value, repartition_pertenthousand, market, base_currency=base_currency) | ||
113 | mouvements = {} | ||
114 | |||
115 | if no_fees: | ||
116 | for key in set(value_in_base.keys()).union(set(new_repartition.keys())): | ||
117 | mouvements[key] = value_in_base.get(key, [0, 0])[0] - new_repartition.get(key, 0) | ||
118 | else: | ||
119 | for key in set(value_in_base.keys()).union(set(new_repartition.keys())): | ||
120 | value, fee = value_in_base.get(key, [0, 0]) | ||
121 | mouvements[key] = [value - new_repartition.get(key, 0), fee] | ||
122 | |||
123 | return mouvements | ||
124 | |||
125 | def compute_order(currency, value, market, base_currency="BTC"): | ||
126 | if currency == base_currency or value == 0: | ||
127 | return [None, 0, False] | ||
128 | |||
129 | asset_ticker = get_ticker(currency, base_currency, market) | ||
130 | if asset_ticker["inverted"]: | ||
131 | asset_ticker = get_ticker(base_currency, currency, market) | ||
132 | if value > 0: | ||
133 | rate = asset_ticker["askA"] | ||
134 | return ["buy", rate, True] | ||
135 | else: | ||
136 | rate = asset_ticker["bidA"] | ||
137 | return ["sell", rate, True] | ||
138 | else: | ||
139 | if value > 0: | ||
140 | rate = asset_ticker["bidA"] | ||
141 | return ["sell", rate, False] | ||
142 | else: | ||
143 | rate = asset_ticker["askA"] | ||
144 | return ["buy", rate, False] | ||
145 | |||
146 | def make_order(currency, value, market, base_currency="BTC"): | ||
147 | action, rate, inverted = compute_order(currency, value, market, base_currency=base_currency) | ||
148 | amount = formatted_price(abs(value)) | ||
149 | if not inverted: | ||
150 | symbol = "{}/{}".format(currency, base_currency) | ||
151 | else: | ||
152 | symbol = "{}/{}".format(base_currency, currency) | ||
153 | return market.create_order(symbol, 'limit', action, amount, price=rate) | ||
154 | |||
155 | def make_orders(current_assets, repartition_pertenthousand, market, base_currency="BTC"): | ||
156 | mouvements = compute_moves( | ||
157 | current_assets, | ||
158 | repartition_pertenthousand, | ||
159 | market, | ||
160 | base_currency=base_currency) | ||
161 | |||
162 | results = [] | ||
163 | for currency, value in sorted(mouvements.items(), key=lambda x: x[1]): | ||
164 | # FIXME: wait for sales to finish | ||
165 | results.append(make_order(currency, value, market, base_currency=base_currency)) | ||
166 | return results | ||
167 | |||
168 | def print_assets(assets, indent="", market=None, base_currency="BTC"): | ||
169 | if market is not None: | ||
170 | format_string = "{}{} {} ({} {})" | ||
171 | else: | ||
172 | format_string = "{}{} {}" | ||
173 | base_currency_price = 0 | ||
174 | |||
175 | for currency, value in assets.items(): | ||
176 | if market is not None: | ||
177 | asset_ticker = get_ticker(currency, base_currency, market) | ||
178 | base_currency_price = asset_ticker["bidE"] * value | ||
179 | print(format_string.format( | ||
180 | indent, | ||
181 | formatted_price(value), | ||
182 | currency, | ||
183 | formatted_price(base_currency_price), | ||
184 | base_currency)) | ||
185 | |||
186 | def print_orders(current_assets, repartition_pertenthousand, market, base_currency="BTC"): | ||
187 | mouvements = compute_moves( | ||
188 | current_assets, | ||
189 | repartition_pertenthousand, | ||
190 | market, | ||
191 | no_fees=False, | ||
192 | base_currency=base_currency) | ||
193 | |||
194 | for currency, [value, fee] in mouvements.items(): | ||
195 | action, rate, inverted = compute_order( | ||
196 | currency, | ||
197 | value, | ||
198 | market, | ||
199 | base_currency=base_currency) | ||
200 | if action is not None: | ||
201 | currency_price = int(value / rate) | ||
202 | |||
203 | if not inverted: | ||
204 | c1, c2 = [base_currency, currency] | ||
205 | v1, v2 = [value, currency_price] | ||
206 | else: | ||
207 | c1, c2 = [currency, base_currency] | ||
208 | v1, v2 = [currency_price, value] | ||
209 | |||
210 | print("need to {} {} {}'s worth of {}, i.e. {} {} ( + {} {} fee)".format( | ||
211 | action, | ||
212 | formatted_price(abs(v1)), c1, | ||
213 | c2, | ||
214 | formatted_price(abs(v2)), c2, | ||
215 | formatted_price(fee), c2)) | ||
216 | |||
217 | current_assets = fetch_balances(market) | ||
218 | print_orders(current_assets, repartition_pertenthousand, market) | ||
@@ -0,0 +1,174 @@ | |||
1 | import portfolio | ||
2 | import unittest | ||
3 | from unittest import mock | ||
4 | |||
5 | class AmountTest(unittest.TestCase): | ||
6 | def setUp(self): | ||
7 | super(AmountTest, self).setUp() | ||
8 | |||
9 | def test_values(self): | ||
10 | amount = portfolio.Amount("BTC", 0.65) | ||
11 | self.assertEqual(0.65, amount.value) | ||
12 | self.assertEqual("BTC", amount.currency) | ||
13 | |||
14 | amount = portfolio.Amount("BTC", 10, int_val=2000000000000000) | ||
15 | self.assertEqual(0.002, amount.value) | ||
16 | |||
17 | def test_in_currency(self): | ||
18 | amount = portfolio.Amount("ETC", 10) | ||
19 | |||
20 | self.assertEqual(amount, amount.in_currency("ETC", None)) | ||
21 | |||
22 | ticker_mock = unittest.mock.Mock() | ||
23 | with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): | ||
24 | ticker_mock.return_value = None | ||
25 | portfolio.Amount.get_ticker = ticker_mock | ||
26 | |||
27 | self.assertRaises(Exception, amount.in_currency, "ETH", None) | ||
28 | |||
29 | with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): | ||
30 | ticker_mock.return_value = { | ||
31 | "average": 0.3, | ||
32 | "foo": "bar", | ||
33 | } | ||
34 | converted_amount = amount.in_currency("ETH", None) | ||
35 | |||
36 | self.assertEqual(3.0, converted_amount.value) | ||
37 | self.assertEqual("ETH", converted_amount.currency) | ||
38 | self.assertEqual(amount, converted_amount.linked_to) | ||
39 | self.assertEqual("bar", converted_amount.ticker["foo"]) | ||
40 | |||
41 | @unittest.skip("TODO") | ||
42 | def test_get_ticker(self): | ||
43 | pass | ||
44 | |||
45 | def test__abs(self): | ||
46 | amount = portfolio.Amount("SC", -120) | ||
47 | self.assertEqual(120, abs(amount).value) | ||
48 | self.assertEqual("SC", abs(amount).currency) | ||
49 | |||
50 | amount = portfolio.Amount("SC", 10) | ||
51 | self.assertEqual(10, abs(amount).value) | ||
52 | self.assertEqual("SC", abs(amount).currency) | ||
53 | |||
54 | def test__add(self): | ||
55 | amount1 = portfolio.Amount("XVG", 12.9) | ||
56 | amount2 = portfolio.Amount("XVG", 13.1) | ||
57 | |||
58 | self.assertEqual(26, (amount1 + amount2).value) | ||
59 | self.assertEqual("XVG", (amount1 + amount2).currency) | ||
60 | |||
61 | amount3 = portfolio.Amount("ETH", 1.6) | ||
62 | with self.assertRaises(Exception): | ||
63 | amount1 + amount3 | ||
64 | |||
65 | amount4 = portfolio.Amount("ETH", 0.0) | ||
66 | self.assertEqual(amount1, amount1 + amount4) | ||
67 | |||
68 | def test__radd(self): | ||
69 | amount = portfolio.Amount("XVG", 12.9) | ||
70 | |||
71 | self.assertEqual(amount, 0 + amount) | ||
72 | with self.assertRaises(Exception): | ||
73 | 4 + amount | ||
74 | |||
75 | def test__sub(self): | ||
76 | amount1 = portfolio.Amount("XVG", 13.3) | ||
77 | amount2 = portfolio.Amount("XVG", 13.1) | ||
78 | |||
79 | self.assertEqual(0.2, (amount1 - amount2).value) | ||
80 | self.assertEqual("XVG", (amount1 - amount2).currency) | ||
81 | |||
82 | amount3 = portfolio.Amount("ETH", 1.6) | ||
83 | with self.assertRaises(Exception): | ||
84 | amount1 - amount3 | ||
85 | |||
86 | amount4 = portfolio.Amount("ETH", 0.0) | ||
87 | self.assertEqual(amount1, amount1 - amount4) | ||
88 | |||
89 | def test__int(self): | ||
90 | amount = portfolio.Amount("XMR", 0.1) | ||
91 | self.assertEqual(100000000000000000, int(amount)) | ||
92 | |||
93 | def test__mul(self): | ||
94 | amount = portfolio.Amount("XEM", 11) | ||
95 | |||
96 | self.assertEqual(38.5, (amount * 3.5).value) | ||
97 | self.assertEqual(33, (amount * 3).value) | ||
98 | |||
99 | with self.assertRaises(Exception): | ||
100 | amount * amount | ||
101 | |||
102 | def test__rmul(self): | ||
103 | amount = portfolio.Amount("XEM", 11) | ||
104 | |||
105 | self.assertEqual(38.5, (3.5 * amount).value) | ||
106 | self.assertEqual(33, (3 * amount).value) | ||
107 | |||
108 | def test__floordiv(self): | ||
109 | amount = portfolio.Amount("XEM", 11) | ||
110 | |||
111 | self.assertEqual(5.5, (amount // 2).value) | ||
112 | with self.assertRaises(TypeError): | ||
113 | amount // 2.5 | ||
114 | self.assertEqual(1571428571428571428, (amount // 7)._value) | ||
115 | |||
116 | def test__div(self): | ||
117 | amount = portfolio.Amount("XEM", 11) | ||
118 | |||
119 | with self.assertRaises(TypeError): | ||
120 | amount / 2.5 | ||
121 | self.assertEqual(5.5, (amount / 2).value) | ||
122 | self.assertEqual(1571428571428571428, (amount / 7)._value) | ||
123 | |||
124 | def test__lt(self): | ||
125 | amount1 = portfolio.Amount("BTD", 11.3) | ||
126 | amount2 = portfolio.Amount("BTD", 13.1) | ||
127 | |||
128 | self.assertTrue(amount1 < amount2) | ||
129 | self.assertFalse(amount2 < amount1) | ||
130 | self.assertFalse(amount1 < amount1) | ||
131 | |||
132 | amount3 = portfolio.Amount("BTC", 1.6) | ||
133 | with self.assertRaises(Exception): | ||
134 | amount1 < amount3 | ||
135 | |||
136 | def test__eq(self): | ||
137 | amount1 = portfolio.Amount("BTD", 11.3) | ||
138 | amount2 = portfolio.Amount("BTD", 13.1) | ||
139 | amount3 = portfolio.Amount("BTD", 11.3) | ||
140 | |||
141 | self.assertFalse(amount1 == amount2) | ||
142 | self.assertFalse(amount2 == amount1) | ||
143 | self.assertTrue(amount1 == amount3) | ||
144 | self.assertFalse(amount2 == 0) | ||
145 | |||
146 | amount4 = portfolio.Amount("BTC", 1.6) | ||
147 | with self.assertRaises(Exception): | ||
148 | amount1 == amount4 | ||
149 | |||
150 | amount5 = portfolio.Amount("BTD", 0) | ||
151 | self.assertTrue(amount5 == 0) | ||
152 | |||
153 | def test__str(self): | ||
154 | amount1 = portfolio.Amount("BTX", 32) | ||
155 | self.assertEqual("32.00000000 BTX", str(amount1)) | ||
156 | |||
157 | amount2 = portfolio.Amount("USDT", 12000) | ||
158 | amount1.linked_to = amount2 | ||
159 | self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1)) | ||
160 | |||
161 | def test__repr(self): | ||
162 | amount1 = portfolio.Amount("BTX", 32) | ||
163 | self.assertEqual("Amount(32.00000000 BTX)", repr(amount1)) | ||
164 | |||
165 | amount2 = portfolio.Amount("USDT", 12000) | ||
166 | amount1.linked_to = amount2 | ||
167 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1)) | ||
168 | |||
169 | amount3 = portfolio.Amount("BTC", 0.1) | ||
170 | amount2.linked_to = amount3 | ||
171 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) | ||
172 | |||
173 | if __name__ == '__main__': | ||
174 | unittest.main() | ||