diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-10 13:52:46 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-10 15:25:27 +0100 |
commit | 6ca5a1ec669593fa915a2824efca068c975f9caa (patch) | |
tree | 375b3f21b4f228eafea7fae364caa2e7bfb8422b | |
parent | c51687d2b0cbad5460d8424f550014502d84696e (diff) | |
download | Trader-6ca5a1ec669593fa915a2824efca068c975f9caa.tar.gz Trader-6ca5a1ec669593fa915a2824efca068c975f9caa.tar.zst Trader-6ca5a1ec669593fa915a2824efca068c975f9caa.zip |
Separate store and add helper
-rw-r--r-- | helper.py | 151 | ||||
-rw-r--r-- | portfolio.py | 309 | ||||
-rw-r--r-- | store.py | 102 | ||||
-rw-r--r-- | test.py | 668 |
4 files changed, 621 insertions, 609 deletions
diff --git a/helper.py b/helper.py new file mode 100644 index 0000000..8a29f40 --- /dev/null +++ b/helper.py | |||
@@ -0,0 +1,151 @@ | |||
1 | import time | ||
2 | from ccxt import ExchangeError | ||
3 | from store import * | ||
4 | |||
5 | def move_balances(market, debug=False): | ||
6 | needed_in_margin = {} | ||
7 | for trade in TradeStore.all: | ||
8 | if trade.trade_type == "short": | ||
9 | if trade.value_to.currency not in needed_in_margin: | ||
10 | needed_in_margin[trade.value_to.currency] = 0 | ||
11 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | ||
12 | for currency, needed in needed_in_margin.items(): | ||
13 | current_balance = BalanceStore.all[currency].margin_free | ||
14 | delta = (needed - current_balance).value | ||
15 | # FIXME: don't remove too much if there are open margin position | ||
16 | if delta > 0: | ||
17 | if debug: | ||
18 | print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta)) | ||
19 | else: | ||
20 | market.transfer_balance(currency, delta, "exchange", "margin") | ||
21 | elif delta < 0: | ||
22 | if debug: | ||
23 | print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta)) | ||
24 | else: | ||
25 | market.transfer_balance(currency, -delta, "margin", "exchange") | ||
26 | |||
27 | BalanceStore.fetch_balances(market) | ||
28 | |||
29 | ticker_cache = {} | ||
30 | ticker_cache_timestamp = time.time() | ||
31 | def get_ticker(c1, c2, market, refresh=False): | ||
32 | global ticker_cache, ticker_cache_timestamp | ||
33 | def invert(ticker): | ||
34 | return { | ||
35 | "inverted": True, | ||
36 | "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2, | ||
37 | "original": ticker, | ||
38 | } | ||
39 | def augment_ticker(ticker): | ||
40 | ticker.update({ | ||
41 | "inverted": False, | ||
42 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | ||
43 | }) | ||
44 | |||
45 | if time.time() - ticker_cache_timestamp > 5: | ||
46 | ticker_cache = {} | ||
47 | ticker_cache_timestamp = time.time() | ||
48 | elif not refresh: | ||
49 | if (c1, c2, market.__class__) in ticker_cache: | ||
50 | return ticker_cache[(c1, c2, market.__class__)] | ||
51 | if (c2, c1, market.__class__) in ticker_cache: | ||
52 | return invert(ticker_cache[(c2, c1, market.__class__)]) | ||
53 | |||
54 | try: | ||
55 | ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | ||
56 | augment_ticker(ticker_cache[(c1, c2, market.__class__)]) | ||
57 | except ExchangeError: | ||
58 | try: | ||
59 | ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | ||
60 | augment_ticker(ticker_cache[(c2, c1, market.__class__)]) | ||
61 | except ExchangeError: | ||
62 | ticker_cache[(c1, c2, market.__class__)] = None | ||
63 | return get_ticker(c1, c2, market) | ||
64 | |||
65 | fees_cache = {} | ||
66 | def fetch_fees(market): | ||
67 | global fees_cache | ||
68 | if market.__class__ not in fees_cache: | ||
69 | fees_cache[market.__class__] = market.fetch_fees() | ||
70 | return fees_cache[market.__class__] | ||
71 | |||
72 | def prepare_trades(market, base_currency="BTC", compute_value="average", debug=False): | ||
73 | BalanceStore.fetch_balances(market) | ||
74 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
75 | total_base_value = sum(values_in_base.values()) | ||
76 | new_repartition = BalanceStore.dispatch_assets(total_base_value) | ||
77 | # Recompute it in case we have new currencies | ||
78 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
79 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
80 | |||
81 | def update_trades(market, base_currency="BTC", compute_value="average", only=None, debug=False): | ||
82 | BalanceStore.fetch_balances(market) | ||
83 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
84 | total_base_value = sum(values_in_base.values()) | ||
85 | new_repartition = BalanceStore.dispatch_assets(total_base_value) | ||
86 | TradeStore.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) | ||
87 | |||
88 | def prepare_trades_to_sell_all(market, base_currency="BTC", compute_value="average", debug=False): | ||
89 | BalanceStore.fetch_balances(market) | ||
90 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
91 | total_base_value = sum(values_in_base.values()) | ||
92 | new_repartition = BalanceStore.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | ||
93 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
94 | |||
95 | def follow_orders(verbose=True, sleep=None): | ||
96 | if sleep is None: | ||
97 | sleep = 7 if TradeStore.debug else 30 | ||
98 | tick = 0 | ||
99 | while len(TradeStore.all_orders(state="open")) > 0: | ||
100 | time.sleep(sleep) | ||
101 | tick += 1 | ||
102 | for order in TradeStore.all_orders(state="open"): | ||
103 | if order.get_status() != "open": | ||
104 | if verbose: | ||
105 | print("finished {}".format(order)) | ||
106 | else: | ||
107 | order.trade.update_order(order, tick) | ||
108 | if verbose: | ||
109 | print("All orders finished") | ||
110 | |||
111 | def print_orders(market, base_currency="BTC"): | ||
112 | prepare_trades(market, base_currency=base_currency, compute_value="average") | ||
113 | TradeStore.prepare_orders(compute_value="average") | ||
114 | for currency, balance in BalanceStore.all.items(): | ||
115 | print(balance) | ||
116 | TradeStore.print_all_with_order() | ||
117 | |||
118 | def make_orders(market, base_currency="BTC"): | ||
119 | prepare_trades(market, base_currency=base_currency) | ||
120 | for trade in TradeStore.all: | ||
121 | print(trade) | ||
122 | for order in trade.orders: | ||
123 | print("\t", order, sep="") | ||
124 | order.run() | ||
125 | |||
126 | def process_sell_all_sell(market, base_currency="BTC", debug=False): | ||
127 | prepare_trades_to_sell_all(market, debug=debug) | ||
128 | TradeStore.prepare_orders(compute_value="average") | ||
129 | print("------------------") | ||
130 | for currency, balance in BalanceStore.all.items(): | ||
131 | print(balance) | ||
132 | print("------------------") | ||
133 | TradeStore.print_all_with_order() | ||
134 | print("------------------") | ||
135 | TradeStore.run_orders() | ||
136 | follow_orders() | ||
137 | |||
138 | def process_sell_all_buy(market, base_currency="BTC", debug=False): | ||
139 | prepare_trades(market, debug=debug) | ||
140 | TradeStore.prepare_orders() | ||
141 | print("------------------") | ||
142 | for currency, balance in BalanceStore.all.items(): | ||
143 | print(balance) | ||
144 | print("------------------") | ||
145 | TradeStore.print_all_with_order() | ||
146 | print("------------------") | ||
147 | move_balances(market, debug=debug) | ||
148 | TradeStore.run_orders() | ||
149 | follow_orders() | ||
150 | |||
151 | |||
diff --git a/portfolio.py b/portfolio.py index 45fbef9..efd9b84 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,14 +1,13 @@ | |||
1 | from ccxt import ExchangeError | ||
2 | import time | 1 | import time |
3 | from decimal import Decimal as D, ROUND_DOWN | 2 | from decimal import Decimal as D, ROUND_DOWN |
4 | # Put your poloniex api key in market.py | 3 | # Put your poloniex api key in market.py |
5 | from market import market | ||
6 | from json import JSONDecodeError | 4 | from json import JSONDecodeError |
7 | import requests | 5 | import requests |
6 | import helper as h | ||
7 | from store import * | ||
8 | 8 | ||
9 | # FIXME: correctly handle web call timeouts | 9 | # FIXME: correctly handle web call timeouts |
10 | 10 | ||
11 | |||
12 | class Portfolio: | 11 | class Portfolio: |
13 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | 12 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" |
14 | liquidities = {} | 13 | liquidities = {} |
@@ -69,6 +68,24 @@ class Portfolio: | |||
69 | "high": high_liquidity, | 68 | "high": high_liquidity, |
70 | } | 69 | } |
71 | 70 | ||
71 | class Computation: | ||
72 | computations = { | ||
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"], | ||
77 | } | ||
78 | |||
79 | @classmethod | ||
80 | def compute_value(cls, ticker, action, compute_value="default"): | ||
81 | if action == "buy": | ||
82 | action = "ask" | ||
83 | if action == "sell": | ||
84 | action = "bid" | ||
85 | if isinstance(compute_value, str): | ||
86 | compute_value = cls.computations[compute_value] | ||
87 | return compute_value(ticker, action) | ||
88 | |||
72 | class Amount: | 89 | class Amount: |
73 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): | 90 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): |
74 | self.currency = currency | 91 | self.currency = currency |
@@ -86,9 +103,9 @@ class Amount: | |||
86 | self.value * rate, | 103 | self.value * rate, |
87 | linked_to=self, | 104 | linked_to=self, |
88 | rate=rate) | 105 | rate=rate) |
89 | asset_ticker = Trade.get_ticker(self.currency, other_currency, market) | 106 | asset_ticker = h.get_ticker(self.currency, other_currency, market) |
90 | if asset_ticker is not None: | 107 | if asset_ticker is not None: |
91 | rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value) | 108 | rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value) |
92 | return Amount( | 109 | return Amount( |
93 | other_currency, | 110 | other_currency, |
94 | self.value * rate, | 111 | self.value * rate, |
@@ -180,7 +197,6 @@ class Amount: | |||
180 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) | 197 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) |
181 | 198 | ||
182 | class Balance: | 199 | class Balance: |
183 | known_balances = {} | ||
184 | 200 | ||
185 | def __init__(self, currency, hash_): | 201 | def __init__(self, currency, hash_): |
186 | self.currency = currency | 202 | self.currency = currency |
@@ -201,69 +217,6 @@ class Balance: | |||
201 | ]: | 217 | ]: |
202 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) | 218 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) |
203 | 219 | ||
204 | @classmethod | ||
205 | def in_currency(cls, other_currency, market, compute_value="average", type="total"): | ||
206 | amounts = {} | ||
207 | for currency in cls.known_balances: | ||
208 | balance = cls.known_balances[currency] | ||
209 | other_currency_amount = getattr(balance, type)\ | ||
210 | .in_currency(other_currency, market, compute_value=compute_value) | ||
211 | amounts[currency] = other_currency_amount | ||
212 | return amounts | ||
213 | |||
214 | @classmethod | ||
215 | def currencies(cls): | ||
216 | return cls.known_balances.keys() | ||
217 | |||
218 | @classmethod | ||
219 | def fetch_balances(cls, market): | ||
220 | all_balances = market.fetch_all_balances() | ||
221 | for currency, balance in all_balances.items(): | ||
222 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ | ||
223 | currency in cls.known_balances: | ||
224 | cls.known_balances[currency] = cls(currency, balance) | ||
225 | return cls.known_balances | ||
226 | |||
227 | @classmethod | ||
228 | def dispatch_assets(cls, amount, repartition=None): | ||
229 | if repartition is None: | ||
230 | repartition = Portfolio.repartition() | ||
231 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | ||
232 | amounts = {} | ||
233 | for currency, (ptt, trade_type) in repartition.items(): | ||
234 | amounts[currency] = ptt * amount / sum_ratio | ||
235 | if trade_type == "short": | ||
236 | amounts[currency] = - amounts[currency] | ||
237 | if currency not in cls.known_balances: | ||
238 | cls.known_balances[currency] = cls(currency, {}) | ||
239 | return amounts | ||
240 | |||
241 | @classmethod | ||
242 | def prepare_trades(cls, market, base_currency="BTC", compute_value="average", debug=False): | ||
243 | cls.fetch_balances(market) | ||
244 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
245 | total_base_value = sum(values_in_base.values()) | ||
246 | new_repartition = cls.dispatch_assets(total_base_value) | ||
247 | # Recompute it in case we have new currencies | ||
248 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
249 | Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
250 | |||
251 | @classmethod | ||
252 | def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None, debug=False): | ||
253 | cls.fetch_balances(market) | ||
254 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
255 | total_base_value = sum(values_in_base.values()) | ||
256 | new_repartition = cls.dispatch_assets(total_base_value) | ||
257 | Trade.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) | ||
258 | |||
259 | @classmethod | ||
260 | def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average", debug=False): | ||
261 | cls.fetch_balances(market) | ||
262 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
263 | total_base_value = sum(values_in_base.values()) | ||
264 | new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | ||
265 | Trade.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
266 | |||
267 | def __repr__(self): | 220 | def __repr__(self): |
268 | if self.exchange_total > 0: | 221 | if self.exchange_total > 0: |
269 | if self.exchange_free > 0 and self.exchange_used > 0: | 222 | if self.exchange_free > 0 and self.exchange_used > 0: |
@@ -296,18 +249,7 @@ class Balance: | |||
296 | 249 | ||
297 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" | 250 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" |
298 | 251 | ||
299 | class Computation: | ||
300 | computations = { | ||
301 | "default": lambda x, y: x[y], | ||
302 | "average": lambda x, y: x["average"], | ||
303 | "bid": lambda x, y: x["bid"], | ||
304 | "ask": lambda x, y: x["ask"], | ||
305 | } | ||
306 | |||
307 | class Trade: | 252 | class Trade: |
308 | debug = False | ||
309 | trades = [] | ||
310 | |||
311 | def __init__(self, value_from, value_to, currency, market=None): | 253 | def __init__(self, value_from, value_to, currency, market=None): |
312 | # We have value_from of currency, and want to finish with value_to of | 254 | # We have value_from of currency, and want to finish with value_to of |
313 | # that currency. value_* may not be in currency's terms | 255 | # that currency. value_* may not be in currency's terms |
@@ -323,105 +265,6 @@ class Trade: | |||
323 | self.value_from.linked_to = Amount(self.currency, 0) | 265 | self.value_from.linked_to = Amount(self.currency, 0) |
324 | self.base_currency = self.value_from.currency | 266 | self.base_currency = self.value_from.currency |
325 | 267 | ||
326 | fees_cache = {} | ||
327 | @classmethod | ||
328 | def fetch_fees(cls, market): | ||
329 | if market.__class__ not in cls.fees_cache: | ||
330 | cls.fees_cache[market.__class__] = market.fetch_fees() | ||
331 | return cls.fees_cache[market.__class__] | ||
332 | |||
333 | ticker_cache = {} | ||
334 | ticker_cache_timestamp = time.time() | ||
335 | @classmethod | ||
336 | def get_ticker(cls, c1, c2, market, refresh=False): | ||
337 | def invert(ticker): | ||
338 | return { | ||
339 | "inverted": True, | ||
340 | "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2, | ||
341 | "original": ticker, | ||
342 | } | ||
343 | def augment_ticker(ticker): | ||
344 | ticker.update({ | ||
345 | "inverted": False, | ||
346 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | ||
347 | }) | ||
348 | |||
349 | if time.time() - cls.ticker_cache_timestamp > 5: | ||
350 | cls.ticker_cache = {} | ||
351 | cls.ticker_cache_timestamp = time.time() | ||
352 | elif not refresh: | ||
353 | if (c1, c2, market.__class__) in cls.ticker_cache: | ||
354 | return cls.ticker_cache[(c1, c2, market.__class__)] | ||
355 | if (c2, c1, market.__class__) in cls.ticker_cache: | ||
356 | return invert(cls.ticker_cache[(c2, c1, market.__class__)]) | ||
357 | |||
358 | try: | ||
359 | cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | ||
360 | augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)]) | ||
361 | except ExchangeError: | ||
362 | try: | ||
363 | cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | ||
364 | augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)]) | ||
365 | except ExchangeError: | ||
366 | cls.ticker_cache[(c1, c2, market.__class__)] = None | ||
367 | return cls.get_ticker(c1, c2, market) | ||
368 | |||
369 | @classmethod | ||
370 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): | ||
371 | cls.debug = cls.debug or debug | ||
372 | base_currency = sum(values_in_base.values()).currency | ||
373 | for currency in Balance.currencies(): | ||
374 | if currency == base_currency: | ||
375 | continue | ||
376 | value_from = values_in_base.get(currency, Amount(base_currency, 0)) | ||
377 | value_to = new_repartition.get(currency, Amount(base_currency, 0)) | ||
378 | if value_from.value * value_to.value < 0: | ||
379 | trade_1 = cls(value_from, Amount(base_currency, 0), currency, market=market) | ||
380 | if only is None or trade_1.action == only: | ||
381 | cls.trades.append(trade_1) | ||
382 | trade_2 = cls(Amount(base_currency, 0), value_to, currency, market=market) | ||
383 | if only is None or trade_2.action == only: | ||
384 | cls.trades.append(trade_2) | ||
385 | else: | ||
386 | trade = cls( | ||
387 | value_from, | ||
388 | value_to, | ||
389 | currency, | ||
390 | market=market | ||
391 | ) | ||
392 | if only is None or trade.action == only: | ||
393 | cls.trades.append(trade) | ||
394 | return cls.trades | ||
395 | |||
396 | @classmethod | ||
397 | def prepare_orders(cls, only=None, compute_value="default"): | ||
398 | for trade in cls.trades: | ||
399 | if only is None or trade.action == only: | ||
400 | trade.prepare_order(compute_value=compute_value) | ||
401 | |||
402 | @classmethod | ||
403 | def move_balances(cls, market): | ||
404 | needed_in_margin = {} | ||
405 | for trade in cls.trades: | ||
406 | if trade.trade_type == "short": | ||
407 | if trade.value_to.currency not in needed_in_margin: | ||
408 | needed_in_margin[trade.value_to.currency] = 0 | ||
409 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | ||
410 | for currency, needed in needed_in_margin.items(): | ||
411 | current_balance = Balance.known_balances[currency].margin_free | ||
412 | delta = (needed - current_balance).value | ||
413 | # FIXME: don't remove too much if there are open margin position | ||
414 | if delta > 0: | ||
415 | if cls.debug: | ||
416 | print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta)) | ||
417 | else: | ||
418 | market.transfer_balance(currency, delta, "exchange", "margin") | ||
419 | elif delta < 0: | ||
420 | if cls.debug: | ||
421 | print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta)) | ||
422 | else: | ||
423 | market.transfer_balance(currency, -delta, "margin", "exchange") | ||
424 | |||
425 | @property | 268 | @property |
426 | def action(self): | 269 | def action(self): |
427 | if self.value_from == self.value_to: | 270 | if self.value_from == self.value_to: |
@@ -481,11 +324,11 @@ class Trade: | |||
481 | def prepare_order(self, compute_value="default"): | 324 | def prepare_order(self, compute_value="default"): |
482 | if self.action is None: | 325 | if self.action is None: |
483 | return | 326 | return |
484 | ticker = Trade.get_ticker(self.currency, self.base_currency, self.market) | 327 | ticker = h.get_ticker(self.currency, self.base_currency, self.market) |
485 | inverted = ticker["inverted"] | 328 | inverted = ticker["inverted"] |
486 | if inverted: | 329 | if inverted: |
487 | ticker = ticker["original"] | 330 | ticker = ticker["original"] |
488 | rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) | 331 | rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) |
489 | # 0.1 | 332 | # 0.1 |
490 | 333 | ||
491 | delta_in_base = abs(self.value_from - self.value_to) | 334 | delta_in_base = abs(self.value_from - self.value_to) |
@@ -536,51 +379,6 @@ class Trade: | |||
536 | delta - self.filled_amount, rate, currency, self.trade_type, | 379 | delta - self.filled_amount, rate, currency, self.trade_type, |
537 | self.market, self, close_if_possible=close_if_possible)) | 380 | self.market, self, close_if_possible=close_if_possible)) |
538 | 381 | ||
539 | @classmethod | ||
540 | def compute_value(cls, ticker, action, compute_value="default"): | ||
541 | if action == "buy": | ||
542 | action = "ask" | ||
543 | if action == "sell": | ||
544 | action = "bid" | ||
545 | if isinstance(compute_value, str): | ||
546 | compute_value = Computation.computations[compute_value] | ||
547 | return compute_value(ticker, action) | ||
548 | |||
549 | @classmethod | ||
550 | def all_orders(cls, state=None): | ||
551 | all_orders = sum(map(lambda v: v.orders, cls.trades), []) | ||
552 | if state is None: | ||
553 | return all_orders | ||
554 | else: | ||
555 | return list(filter(lambda o: o.status == state, all_orders)) | ||
556 | |||
557 | @classmethod | ||
558 | def run_orders(cls): | ||
559 | for order in cls.all_orders(state="pending"): | ||
560 | order.run() | ||
561 | |||
562 | @classmethod | ||
563 | def follow_orders(cls, verbose=True, sleep=None): | ||
564 | if sleep is None: | ||
565 | sleep = 7 if cls.debug else 30 | ||
566 | tick = 0 | ||
567 | while len(cls.all_orders(state="open")) > 0: | ||
568 | time.sleep(sleep) | ||
569 | tick += 1 | ||
570 | for order in cls.all_orders(state="open"): | ||
571 | if order.get_status() != "open": | ||
572 | if verbose: | ||
573 | print("finished {}".format(order)) | ||
574 | else: | ||
575 | order.trade.update_order(order, tick) | ||
576 | if verbose: | ||
577 | print("All orders finished") | ||
578 | |||
579 | @classmethod | ||
580 | def update_all_orders_status(cls): | ||
581 | for order in cls.all_orders(state="open"): | ||
582 | order.get_status() | ||
583 | |||
584 | def __repr__(self): | 382 | def __repr__(self): |
585 | return "Trade({} -> {} in {}, {})".format( | 383 | return "Trade({} -> {} in {}, {})".format( |
586 | self.value_from, | 384 | self.value_from, |
@@ -588,11 +386,6 @@ class Trade: | |||
588 | self.currency, | 386 | self.currency, |
589 | self.action) | 387 | self.action) |
590 | 388 | ||
591 | @classmethod | ||
592 | def print_all_with_order(cls): | ||
593 | for trade in cls.trades: | ||
594 | trade.print_with_order() | ||
595 | |||
596 | def print_with_order(self): | 389 | def print_with_order(self): |
597 | print(self) | 390 | print(self) |
598 | for order in self.orders: | 391 | for order in self.orders: |
@@ -612,7 +405,6 @@ class Order: | |||
612 | self.status = "pending" | 405 | self.status = "pending" |
613 | self.trade = trade | 406 | self.trade = trade |
614 | self.close_if_possible = close_if_possible | 407 | self.close_if_possible = close_if_possible |
615 | self.debug = trade.debug | ||
616 | 408 | ||
617 | def __repr__(self): | 409 | def __repr__(self): |
618 | return "Order({} {} {} at {} {} [{}]{})".format( | 410 | return "Order({} {} {} at {} {} [{}]{})".format( |
@@ -648,7 +440,7 @@ class Order: | |||
648 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 440 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
649 | amount = round(self.amount, self.market.order_precision(symbol)).value | 441 | amount = round(self.amount, self.market.order_precision(symbol)).value |
650 | 442 | ||
651 | if self.debug: | 443 | if TradeStore.debug: |
652 | print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 444 | print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( |
653 | symbol, self.action, amount, self.rate, self.account)) | 445 | symbol, self.action, amount, self.rate, self.account)) |
654 | self.status = "open" | 446 | self.status = "open" |
@@ -665,7 +457,7 @@ class Order: | |||
665 | print(self.error_message) | 457 | print(self.error_message) |
666 | 458 | ||
667 | def get_status(self): | 459 | def get_status(self): |
668 | if self.debug: | 460 | if TradeStore.debug: |
669 | return self.status | 461 | return self.status |
670 | # other states are "closed" and "canceled" | 462 | # other states are "closed" and "canceled" |
671 | if self.status == "open": | 463 | if self.status == "open": |
@@ -675,7 +467,7 @@ class Order: | |||
675 | return self.status | 467 | return self.status |
676 | 468 | ||
677 | def mark_finished_order(self): | 469 | def mark_finished_order(self): |
678 | if self.debug: | 470 | if TradeStore.debug: |
679 | return | 471 | return |
680 | if self.status == "closed": | 472 | if self.status == "closed": |
681 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: | 473 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: |
@@ -683,7 +475,7 @@ class Order: | |||
683 | 475 | ||
684 | fetch_cache_timestamp = None | 476 | fetch_cache_timestamp = None |
685 | def fetch(self, force=False): | 477 | def fetch(self, force=False): |
686 | if self.debug or (not force and self.fetch_cache_timestamp is not None | 478 | if TradeStore.debug or (not force and self.fetch_cache_timestamp is not None |
687 | and time.time() - self.fetch_cache_timestamp < 10): | 479 | and time.time() - self.fetch_cache_timestamp < 10): |
688 | return | 480 | return |
689 | self.fetch_cache_timestamp = time.time() | 481 | self.fetch_cache_timestamp = time.time() |
@@ -725,7 +517,7 @@ class Order: | |||
725 | self.base_currency, mouvement_hash)) | 517 | self.base_currency, mouvement_hash)) |
726 | 518 | ||
727 | def cancel(self): | 519 | def cancel(self): |
728 | if self.debug: | 520 | if TradeStore.debug: |
729 | self.status = "canceled" | 521 | self.status = "canceled" |
730 | return | 522 | return |
731 | self.market.cancel_order(self.result['id']) | 523 | self.market.cancel_order(self.result['id']) |
@@ -744,45 +536,6 @@ class Mouvement: | |||
744 | # rate * total = total_in_base | 536 | # rate * total = total_in_base |
745 | self.total_in_base = Amount(base_currency, hash_["total"]) | 537 | self.total_in_base = Amount(base_currency, hash_["total"]) |
746 | 538 | ||
747 | def print_orders(market, base_currency="BTC"): | ||
748 | Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") | ||
749 | Trade.prepare_orders(compute_value="average") | ||
750 | for currency, balance in Balance.known_balances.items(): | ||
751 | print(balance) | ||
752 | Trade.print_all_with_order() | ||
753 | |||
754 | def make_orders(market, base_currency="BTC"): | ||
755 | Balance.prepare_trades(market, base_currency=base_currency) | ||
756 | for trade in Trade.trades: | ||
757 | print(trade) | ||
758 | for order in trade.orders: | ||
759 | print("\t", order, sep="") | ||
760 | order.run() | ||
761 | |||
762 | def process_sell_all_sell(market, base_currency="BTC", debug=False): | ||
763 | Balance.prepare_trades_to_sell_all(market, debug=debug) | ||
764 | Trade.prepare_orders(compute_value="average") | ||
765 | print("------------------") | ||
766 | for currency, balance in Balance.known_balances.items(): | ||
767 | print(balance) | ||
768 | print("------------------") | ||
769 | Trade.print_all_with_order() | ||
770 | print("------------------") | ||
771 | Trade.run_orders() | ||
772 | Trade.follow_orders() | ||
773 | |||
774 | def process_sell_all_buy(market, base_currency="BTC", debug=False): | ||
775 | Balance.prepare_trades(market, debug=debug) | ||
776 | Trade.prepare_orders() | ||
777 | print("------------------") | ||
778 | for currency, balance in Balance.known_balances.items(): | ||
779 | print(balance) | ||
780 | print("------------------") | ||
781 | Trade.print_all_with_order() | ||
782 | print("------------------") | ||
783 | Trade.move_balances(market) | ||
784 | Trade.run_orders() | ||
785 | Trade.follow_orders() | ||
786 | |||
787 | if __name__ == '__main__': | 539 | if __name__ == '__main__': |
788 | print_orders(market) | 540 | from market import market |
541 | h.print_orders(market) | ||
diff --git a/store.py b/store.py new file mode 100644 index 0000000..4e46878 --- /dev/null +++ b/store.py | |||
@@ -0,0 +1,102 @@ | |||
1 | import portfolio | ||
2 | |||
3 | __all__ = ["BalanceStore", "TradeStore"] | ||
4 | |||
5 | class BalanceStore: | ||
6 | all = {} | ||
7 | |||
8 | @classmethod | ||
9 | def currencies(cls): | ||
10 | return cls.all.keys() | ||
11 | |||
12 | @classmethod | ||
13 | def in_currency(cls, other_currency, market, compute_value="average", type="total"): | ||
14 | amounts = {} | ||
15 | for currency, balance in cls.all.items(): | ||
16 | other_currency_amount = getattr(balance, type)\ | ||
17 | .in_currency(other_currency, market, compute_value=compute_value) | ||
18 | amounts[currency] = other_currency_amount | ||
19 | return amounts | ||
20 | |||
21 | @classmethod | ||
22 | def fetch_balances(cls, market): | ||
23 | all_balances = market.fetch_all_balances() | ||
24 | for currency, balance in all_balances.items(): | ||
25 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ | ||
26 | currency in cls.all: | ||
27 | cls.all[currency] = portfolio.Balance(currency, balance) | ||
28 | |||
29 | @classmethod | ||
30 | def dispatch_assets(cls, amount, repartition=None): | ||
31 | if repartition is None: | ||
32 | repartition = portfolio.Portfolio.repartition() | ||
33 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | ||
34 | amounts = {} | ||
35 | for currency, (ptt, trade_type) in repartition.items(): | ||
36 | amounts[currency] = ptt * amount / sum_ratio | ||
37 | if trade_type == "short": | ||
38 | amounts[currency] = - amounts[currency] | ||
39 | if currency not in BalanceStore.all: | ||
40 | cls.all[currency] = portfolio.Balance(currency, {}) | ||
41 | return amounts | ||
42 | |||
43 | class TradeStore: | ||
44 | all = [] | ||
45 | debug = False | ||
46 | |||
47 | @classmethod | ||
48 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): | ||
49 | cls.debug = cls.debug or debug | ||
50 | base_currency = sum(values_in_base.values()).currency | ||
51 | for currency in BalanceStore.currencies(): | ||
52 | if currency == base_currency: | ||
53 | continue | ||
54 | value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0)) | ||
55 | value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) | ||
56 | if value_from.value * value_to.value < 0: | ||
57 | trade_1 = portfolio.Trade(value_from, portfolio.Amount(base_currency, 0), currency, market=market) | ||
58 | if only is None or trade_1.action == only: | ||
59 | cls.all.append(trade_1) | ||
60 | trade_2 = portfolio.Trade(portfolio.Amount(base_currency, 0), value_to, currency, market=market) | ||
61 | if only is None or trade_2.action == only: | ||
62 | cls.all.append(trade_2) | ||
63 | else: | ||
64 | trade = portfolio.Trade( | ||
65 | value_from, | ||
66 | value_to, | ||
67 | currency, | ||
68 | market=market | ||
69 | ) | ||
70 | if only is None or trade.action == only: | ||
71 | cls.all.append(trade) | ||
72 | |||
73 | @classmethod | ||
74 | def prepare_orders(cls, only=None, compute_value="default"): | ||
75 | for trade in cls.all: | ||
76 | if only is None or trade.action == only: | ||
77 | trade.prepare_order(compute_value=compute_value) | ||
78 | |||
79 | @classmethod | ||
80 | def print_all_with_order(cls): | ||
81 | for trade in cls.all: | ||
82 | trade.print_with_order() | ||
83 | |||
84 | @classmethod | ||
85 | def run_orders(cls): | ||
86 | for order in cls.all_orders(state="pending"): | ||
87 | order.run() | ||
88 | |||
89 | @classmethod | ||
90 | def all_orders(cls, state=None): | ||
91 | all_orders = sum(map(lambda v: v.orders, cls.all), []) | ||
92 | if state is None: | ||
93 | return all_orders | ||
94 | else: | ||
95 | return list(filter(lambda o: o.status == state, all_orders)) | ||
96 | |||
97 | @classmethod | ||
98 | def update_all_orders_status(cls): | ||
99 | for order in cls.all_orders(state="open"): | ||
100 | order.get_status() | ||
101 | |||
102 | |||
@@ -5,6 +5,7 @@ from unittest import mock | |||
5 | import requests | 5 | import requests |
6 | import requests_mock | 6 | import requests_mock |
7 | from io import StringIO | 7 | from io import StringIO |
8 | import helper | ||
8 | 9 | ||
9 | class WebMockTestCase(unittest.TestCase): | 10 | class WebMockTestCase(unittest.TestCase): |
10 | import time | 11 | import time |
@@ -15,16 +16,18 @@ class WebMockTestCase(unittest.TestCase): | |||
15 | self.wm.start() | 16 | self.wm.start() |
16 | 17 | ||
17 | self.patchers = [ | 18 | self.patchers = [ |
18 | mock.patch.multiple(portfolio.Balance, known_balances={}), | 19 | mock.patch.multiple(portfolio.BalanceStore, |
20 | all={},), | ||
21 | mock.patch.multiple(portfolio.TradeStore, | ||
22 | all=[], | ||
23 | debug=False), | ||
19 | mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={}), | 24 | mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={}), |
20 | mock.patch.multiple(portfolio.Trade, | ||
21 | ticker_cache={}, | ||
22 | ticker_cache_timestamp=self.time.time(), | ||
23 | fees_cache={}, | ||
24 | debug=False, | ||
25 | trades=[]), | ||
26 | mock.patch.multiple(portfolio.Computation, | 25 | mock.patch.multiple(portfolio.Computation, |
27 | computations=portfolio.Computation.computations) | 26 | computations=portfolio.Computation.computations), |
27 | mock.patch.multiple(helper, | ||
28 | fees_cache={}, | ||
29 | ticker_cache={}, | ||
30 | ticker_cache_timestamp=self.time.time()), | ||
28 | ] | 31 | ] |
29 | for patcher in self.patchers: | 32 | for patcher in self.patchers: |
30 | patcher.start() | 33 | patcher.start() |
@@ -149,12 +152,12 @@ class AmountTest(WebMockTestCase): | |||
149 | self.assertEqual(amount, amount.in_currency("ETC", None)) | 152 | self.assertEqual(amount, amount.in_currency("ETC", None)) |
150 | 153 | ||
151 | ticker_mock = unittest.mock.Mock() | 154 | ticker_mock = unittest.mock.Mock() |
152 | with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): | 155 | with mock.patch.object(helper, 'get_ticker', new=ticker_mock): |
153 | ticker_mock.return_value = None | 156 | ticker_mock.return_value = None |
154 | 157 | ||
155 | self.assertRaises(Exception, amount.in_currency, "ETH", None) | 158 | self.assertRaises(Exception, amount.in_currency, "ETH", None) |
156 | 159 | ||
157 | with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): | 160 | with mock.patch.object(helper, 'get_ticker', new=ticker_mock): |
158 | ticker_mock.return_value = { | 161 | ticker_mock.return_value = { |
159 | "bid": D("0.2"), | 162 | "bid": D("0.2"), |
160 | "ask": D("0.4"), | 163 | "ask": D("0.4"), |
@@ -361,37 +364,6 @@ class AmountTest(WebMockTestCase): | |||
361 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) | 364 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) |
362 | 365 | ||
363 | class BalanceTest(WebMockTestCase): | 366 | class BalanceTest(WebMockTestCase): |
364 | def setUp(self): | ||
365 | super(BalanceTest, self).setUp() | ||
366 | |||
367 | self.fetch_balance = { | ||
368 | "ETC": { | ||
369 | "exchange_free": 0, | ||
370 | "exchange_used": 0, | ||
371 | "exchange_total": 0, | ||
372 | "margin_total": 0, | ||
373 | }, | ||
374 | "USDT": { | ||
375 | "exchange_free": D("6.0"), | ||
376 | "exchange_used": D("1.2"), | ||
377 | "exchange_total": D("7.2"), | ||
378 | "margin_total": 0, | ||
379 | }, | ||
380 | "XVG": { | ||
381 | "exchange_free": 16, | ||
382 | "exchange_used": 0, | ||
383 | "exchange_total": 16, | ||
384 | "margin_total": 0, | ||
385 | }, | ||
386 | "XMR": { | ||
387 | "exchange_free": 0, | ||
388 | "exchange_used": 0, | ||
389 | "exchange_total": 0, | ||
390 | "margin_total": D("-1.0"), | ||
391 | "margin_free": 0, | ||
392 | }, | ||
393 | } | ||
394 | |||
395 | def test_values(self): | 367 | def test_values(self): |
396 | balance = portfolio.Balance("BTC", { | 368 | balance = portfolio.Balance("BTC", { |
397 | "exchange_total": "0.65", | 369 | "exchange_total": "0.65", |
@@ -426,91 +398,100 @@ class BalanceTest(WebMockTestCase): | |||
426 | self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value) | 398 | self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value) |
427 | self.assertEqual("USDT", balance.margin_lending_fees.currency) | 399 | self.assertEqual("USDT", balance.margin_lending_fees.currency) |
428 | 400 | ||
429 | @mock.patch.object(portfolio.Trade, "get_ticker") | 401 | def test__repr(self): |
430 | def test_in_currency(self, get_ticker): | 402 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])", |
431 | portfolio.Balance.known_balances = { | 403 | repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 }))) |
432 | "BTC": portfolio.Balance("BTC", { | 404 | balance = portfolio.Balance("BTX", { "exchange_total": 3, |
433 | "total": "0.65", | 405 | "exchange_used": 1, "exchange_free": 2 }) |
434 | "exchange_total":"0.65", | 406 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance)) |
435 | "exchange_free": "0.35", | ||
436 | "exchange_used": "0.30"}), | ||
437 | "ETH": portfolio.Balance("ETH", { | ||
438 | "total": 3, | ||
439 | "exchange_total": 3, | ||
440 | "exchange_free": 3, | ||
441 | "exchange_used": 0}), | ||
442 | } | ||
443 | market = mock.Mock() | ||
444 | get_ticker.return_value = { | ||
445 | "bid": D("0.09"), | ||
446 | "ask": D("0.11"), | ||
447 | "average": D("0.1"), | ||
448 | } | ||
449 | 407 | ||
450 | amounts = portfolio.Balance.in_currency("BTC", market) | 408 | balance = portfolio.Balance("BTX", { "margin_total": 3, |
451 | self.assertEqual("BTC", amounts["ETH"].currency) | 409 | "margin_borrowed": 1, "margin_free": 2 }) |
452 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 410 | self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance)) |
453 | self.assertEqual(D("0.30"), amounts["ETH"].value) | ||
454 | 411 | ||
455 | amounts = portfolio.Balance.in_currency("BTC", market, compute_value="bid") | 412 | balance = portfolio.Balance("BTX", { "margin_total": -3, |
456 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 413 | "margin_borrowed_base_price": D("0.1"), |
457 | self.assertEqual(D("0.27"), amounts["ETH"].value) | 414 | "margin_borrowed_base_currency": "BTC", |
415 | "margin_lending_fees": D("0.002") }) | ||
416 | self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance)) | ||
458 | 417 | ||
459 | amounts = portfolio.Balance.in_currency("BTC", market, compute_value="bid", type="exchange_used") | 418 | class HelperTest(WebMockTestCase): |
460 | self.assertEqual(D("0.30"), amounts["BTC"].value) | 419 | def test_get_ticker(self): |
461 | self.assertEqual(0, amounts["ETH"].value) | 420 | market = mock.Mock() |
421 | market.fetch_ticker.side_effect = [ | ||
422 | { "bid": 1, "ask": 3 }, | ||
423 | helper.ExchangeError("foo"), | ||
424 | { "bid": 10, "ask": 40 }, | ||
425 | helper.ExchangeError("foo"), | ||
426 | helper.ExchangeError("foo"), | ||
427 | ] | ||
462 | 428 | ||
463 | def test_currencies(self): | 429 | ticker = helper.get_ticker("ETH", "ETC", market) |
464 | portfolio.Balance.known_balances = { | 430 | market.fetch_ticker.assert_called_with("ETH/ETC") |
465 | "BTC": portfolio.Balance("BTC", { | 431 | self.assertEqual(1, ticker["bid"]) |
466 | "total": "0.65", | 432 | self.assertEqual(3, ticker["ask"]) |
467 | "exchange_total":"0.65", | 433 | self.assertEqual(2, ticker["average"]) |
468 | "exchange_free": "0.35", | 434 | self.assertFalse(ticker["inverted"]) |
469 | "exchange_used": "0.30"}), | ||
470 | "ETH": portfolio.Balance("ETH", { | ||
471 | "total": 3, | ||
472 | "exchange_total": 3, | ||
473 | "exchange_free": 3, | ||
474 | "exchange_used": 0}), | ||
475 | } | ||
476 | self.assertListEqual(["BTC", "ETH"], list(portfolio.Balance.currencies())) | ||
477 | 435 | ||
478 | @mock.patch.object(portfolio.market, "fetch_all_balances") | 436 | ticker = helper.get_ticker("ETH", "XVG", market) |
479 | def test_fetch_balances(self, fetch_all_balances): | 437 | self.assertEqual(0.0625, ticker["average"]) |
480 | fetch_all_balances.return_value = self.fetch_balance | 438 | self.assertTrue(ticker["inverted"]) |
439 | self.assertIn("original", ticker) | ||
440 | self.assertEqual(10, ticker["original"]["bid"]) | ||
481 | 441 | ||
482 | portfolio.Balance.fetch_balances(portfolio.market) | 442 | ticker = helper.get_ticker("XVG", "XMR", market) |
483 | self.assertNotIn("ETC", portfolio.Balance.currencies()) | 443 | self.assertIsNone(ticker) |
484 | self.assertListEqual(["USDT", "XVG", "XMR"], list(portfolio.Balance.currencies())) | ||
485 | 444 | ||
486 | portfolio.Balance.known_balances["ETC"] = portfolio.Balance("ETC", { | 445 | market.fetch_ticker.assert_has_calls([ |
487 | "exchange_total": "1", "exchange_free": "0", | 446 | mock.call("ETH/ETC"), |
488 | "exchange_used": "1" }) | 447 | mock.call("ETH/XVG"), |
489 | portfolio.Balance.fetch_balances(portfolio.market) | 448 | mock.call("XVG/ETH"), |
490 | self.assertEqual(0, portfolio.Balance.known_balances["ETC"].total) | 449 | mock.call("XVG/XMR"), |
491 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.Balance.currencies())) | 450 | mock.call("XMR/XVG"), |
451 | ]) | ||
492 | 452 | ||
493 | @mock.patch.object(portfolio.Portfolio, "repartition") | 453 | market2 = mock.Mock() |
494 | @mock.patch.object(portfolio.market, "fetch_all_balances") | 454 | market2.fetch_ticker.side_effect = [ |
495 | def test_dispatch_assets(self, fetch_all_balances, repartition): | 455 | { "bid": 1, "ask": 3 }, |
496 | fetch_all_balances.return_value = self.fetch_balance | 456 | { "bid": 1.2, "ask": 3.5 }, |
497 | portfolio.Balance.fetch_balances(portfolio.market) | 457 | ] |
458 | ticker1 = helper.get_ticker("ETH", "ETC", market2) | ||
459 | ticker2 = helper.get_ticker("ETH", "ETC", market2) | ||
460 | ticker3 = helper.get_ticker("ETC", "ETH", market2) | ||
461 | market2.fetch_ticker.assert_called_once_with("ETH/ETC") | ||
462 | self.assertEqual(1, ticker1["bid"]) | ||
463 | self.assertDictEqual(ticker1, ticker2) | ||
464 | self.assertDictEqual(ticker1, ticker3["original"]) | ||
498 | 465 | ||
499 | self.assertNotIn("XEM", portfolio.Balance.currencies()) | 466 | ticker4 = helper.get_ticker("ETH", "ETC", market2, refresh=True) |
467 | ticker5 = helper.get_ticker("ETH", "ETC", market2) | ||
468 | self.assertEqual(1.2, ticker4["bid"]) | ||
469 | self.assertDictEqual(ticker4, ticker5) | ||
500 | 470 | ||
501 | repartition.return_value = { | 471 | market3 = mock.Mock() |
502 | "XEM": (D("0.75"), "long"), | 472 | market3.fetch_ticker.side_effect = [ |
503 | "BTC": (D("0.26"), "long"), | 473 | { "bid": 1, "ask": 3 }, |
504 | } | 474 | { "bid": 1.2, "ask": 3.5 }, |
475 | ] | ||
476 | ticker6 = helper.get_ticker("ETH", "ETC", market3) | ||
477 | helper.ticker_cache_timestamp -= 4 | ||
478 | ticker7 = helper.get_ticker("ETH", "ETC", market3) | ||
479 | helper.ticker_cache_timestamp -= 2 | ||
480 | ticker8 = helper.get_ticker("ETH", "ETC", market3) | ||
481 | self.assertDictEqual(ticker6, ticker7) | ||
482 | self.assertEqual(1.2, ticker8["bid"]) | ||
505 | 483 | ||
506 | amounts = portfolio.Balance.dispatch_assets(portfolio.Amount("BTC", "10.1")) | 484 | def test_fetch_fees(self): |
507 | self.assertIn("XEM", portfolio.Balance.currencies()) | 485 | market = mock.Mock() |
508 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 486 | market.fetch_fees.return_value = "Foo" |
509 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 487 | self.assertEqual("Foo", helper.fetch_fees(market)) |
488 | market.fetch_fees.assert_called_once() | ||
489 | self.assertEqual("Foo", helper.fetch_fees(market)) | ||
490 | market.fetch_fees.assert_called_once() | ||
510 | 491 | ||
511 | @mock.patch.object(portfolio.Portfolio, "repartition") | 492 | @mock.patch.object(portfolio.Portfolio, "repartition") |
512 | @mock.patch.object(portfolio.Trade, "get_ticker") | 493 | @mock.patch.object(helper, "get_ticker") |
513 | @mock.patch.object(portfolio.Trade, "compute_trades") | 494 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
514 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 495 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
515 | repartition.return_value = { | 496 | repartition.return_value = { |
516 | "XEM": (D("0.75"), "long"), | 497 | "XEM": (D("0.75"), "long"), |
@@ -541,7 +522,7 @@ class BalanceTest(WebMockTestCase): | |||
541 | "total": D("10000.0") | 522 | "total": D("10000.0") |
542 | }, | 523 | }, |
543 | } | 524 | } |
544 | portfolio.Balance.prepare_trades(market) | 525 | helper.prepare_trades(market) |
545 | compute_trades.assert_called() | 526 | compute_trades.assert_called() |
546 | 527 | ||
547 | call = compute_trades.call_args | 528 | call = compute_trades.call_args |
@@ -552,8 +533,8 @@ class BalanceTest(WebMockTestCase): | |||
552 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 533 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
553 | 534 | ||
554 | @mock.patch.object(portfolio.Portfolio, "repartition") | 535 | @mock.patch.object(portfolio.Portfolio, "repartition") |
555 | @mock.patch.object(portfolio.Trade, "get_ticker") | 536 | @mock.patch.object(helper, "get_ticker") |
556 | @mock.patch.object(portfolio.Trade, "compute_trades") | 537 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
557 | def test_update_trades(self, compute_trades, get_ticker, repartition): | 538 | def test_update_trades(self, compute_trades, get_ticker, repartition): |
558 | repartition.return_value = { | 539 | repartition.return_value = { |
559 | "XEM": (D("0.75"), "long"), | 540 | "XEM": (D("0.75"), "long"), |
@@ -584,7 +565,7 @@ class BalanceTest(WebMockTestCase): | |||
584 | "total": D("10000.0") | 565 | "total": D("10000.0") |
585 | }, | 566 | }, |
586 | } | 567 | } |
587 | portfolio.Balance.update_trades(market) | 568 | helper.update_trades(market) |
588 | compute_trades.assert_called() | 569 | compute_trades.assert_called() |
589 | 570 | ||
590 | call = compute_trades.call_args | 571 | call = compute_trades.call_args |
@@ -595,8 +576,8 @@ class BalanceTest(WebMockTestCase): | |||
595 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 576 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
596 | 577 | ||
597 | @mock.patch.object(portfolio.Portfolio, "repartition") | 578 | @mock.patch.object(portfolio.Portfolio, "repartition") |
598 | @mock.patch.object(portfolio.Trade, "get_ticker") | 579 | @mock.patch.object(helper, "get_ticker") |
599 | @mock.patch.object(portfolio.Trade, "compute_trades") | 580 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
600 | def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition): | 581 | def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition): |
601 | def _get_ticker(c1, c2, market): | 582 | def _get_ticker(c1, c2, market): |
602 | if c1 == "USDT" and c2 == "BTC": | 583 | if c1 == "USDT" and c2 == "BTC": |
@@ -621,7 +602,7 @@ class BalanceTest(WebMockTestCase): | |||
621 | "total": D("10000.0") | 602 | "total": D("10000.0") |
622 | }, | 603 | }, |
623 | } | 604 | } |
624 | portfolio.Balance.prepare_trades_to_sell_all(market) | 605 | helper.prepare_trades_to_sell_all(market) |
625 | repartition.assert_not_called() | 606 | repartition.assert_not_called() |
626 | compute_trades.assert_called() | 607 | compute_trades.assert_called() |
627 | 608 | ||
@@ -631,89 +612,243 @@ class BalanceTest(WebMockTestCase): | |||
631 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 612 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
632 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) | 613 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) |
633 | 614 | ||
634 | def test__repr(self): | 615 | @unittest.skip("TODO") |
635 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])", | 616 | def test_follow_orders(self): |
636 | repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 }))) | 617 | pass |
637 | balance = portfolio.Balance("BTX", { "exchange_total": 3, | ||
638 | "exchange_used": 1, "exchange_free": 2 }) | ||
639 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance)) | ||
640 | 618 | ||
641 | balance = portfolio.Balance("BTX", { "margin_total": 3, | ||
642 | "margin_borrowed": 1, "margin_free": 2 }) | ||
643 | self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance)) | ||
644 | 619 | ||
645 | balance = portfolio.Balance("BTX", { "margin_total": -3, | 620 | class TradeStoreTest(WebMockTestCase): |
646 | "margin_borrowed_base_price": D("0.1"), | 621 | @unittest.skip("TODO") |
647 | "margin_borrowed_base_currency": "BTC", | 622 | def test_compute_trades(self): |
648 | "margin_lending_fees": D("0.002") }) | 623 | pass |
649 | self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance)) | ||
650 | 624 | ||
651 | class TradeTest(WebMockTestCase): | 625 | def test_prepare_orders(self): |
626 | trade_mock1 = mock.Mock() | ||
627 | trade_mock2 = mock.Mock() | ||
652 | 628 | ||
653 | def test_get_ticker(self): | 629 | portfolio.TradeStore.all.append(trade_mock1) |
630 | portfolio.TradeStore.all.append(trade_mock2) | ||
631 | |||
632 | portfolio.TradeStore.prepare_orders() | ||
633 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | ||
634 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
635 | |||
636 | portfolio.TradeStore.prepare_orders(compute_value="bla") | ||
637 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | ||
638 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | ||
639 | |||
640 | trade_mock1.prepare_order.reset_mock() | ||
641 | trade_mock2.prepare_order.reset_mock() | ||
642 | |||
643 | trade_mock1.action = "foo" | ||
644 | trade_mock2.action = "bar" | ||
645 | portfolio.TradeStore.prepare_orders(only="bar") | ||
646 | trade_mock1.prepare_order.assert_not_called() | ||
647 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
648 | |||
649 | def test_print_all_with_order(self): | ||
650 | trade_mock1 = mock.Mock() | ||
651 | trade_mock2 = mock.Mock() | ||
652 | trade_mock3 = mock.Mock() | ||
653 | portfolio.TradeStore.all = [trade_mock1, trade_mock2, trade_mock3] | ||
654 | |||
655 | portfolio.TradeStore.print_all_with_order() | ||
656 | |||
657 | trade_mock1.print_with_order.assert_called() | ||
658 | trade_mock2.print_with_order.assert_called() | ||
659 | trade_mock3.print_with_order.assert_called() | ||
660 | |||
661 | @mock.patch.object(portfolio.TradeStore, "all_orders") | ||
662 | def test_run_orders(self, all_orders): | ||
663 | order_mock1 = mock.Mock() | ||
664 | order_mock2 = mock.Mock() | ||
665 | order_mock3 = mock.Mock() | ||
666 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
667 | portfolio.TradeStore.run_orders() | ||
668 | all_orders.assert_called_with(state="pending") | ||
669 | |||
670 | order_mock1.run.assert_called() | ||
671 | order_mock2.run.assert_called() | ||
672 | order_mock3.run.assert_called() | ||
673 | |||
674 | def test_all_orders(self): | ||
675 | trade_mock1 = mock.Mock() | ||
676 | trade_mock2 = mock.Mock() | ||
677 | |||
678 | order_mock1 = mock.Mock() | ||
679 | order_mock2 = mock.Mock() | ||
680 | order_mock3 = mock.Mock() | ||
681 | |||
682 | trade_mock1.orders = [order_mock1, order_mock2] | ||
683 | trade_mock2.orders = [order_mock3] | ||
684 | |||
685 | order_mock1.status = "pending" | ||
686 | order_mock2.status = "open" | ||
687 | order_mock3.status = "open" | ||
688 | |||
689 | portfolio.TradeStore.all.append(trade_mock1) | ||
690 | portfolio.TradeStore.all.append(trade_mock2) | ||
691 | |||
692 | orders = portfolio.TradeStore.all_orders() | ||
693 | self.assertEqual(3, len(orders)) | ||
694 | |||
695 | open_orders = portfolio.TradeStore.all_orders(state="open") | ||
696 | self.assertEqual(2, len(open_orders)) | ||
697 | self.assertEqual([order_mock2, order_mock3], open_orders) | ||
698 | |||
699 | @mock.patch.object(portfolio.TradeStore, "all_orders") | ||
700 | def test_update_all_orders_status(self, all_orders): | ||
701 | order_mock1 = mock.Mock() | ||
702 | order_mock2 = mock.Mock() | ||
703 | order_mock3 = mock.Mock() | ||
704 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
705 | portfolio.TradeStore.update_all_orders_status() | ||
706 | all_orders.assert_called_with(state="open") | ||
707 | |||
708 | order_mock1.get_status.assert_called() | ||
709 | order_mock2.get_status.assert_called() | ||
710 | order_mock3.get_status.assert_called() | ||
711 | |||
712 | |||
713 | class BalanceStoreTest(WebMockTestCase): | ||
714 | def setUp(self): | ||
715 | super(BalanceStoreTest, self).setUp() | ||
716 | |||
717 | self.fetch_balance = { | ||
718 | "ETC": { | ||
719 | "exchange_free": 0, | ||
720 | "exchange_used": 0, | ||
721 | "exchange_total": 0, | ||
722 | "margin_total": 0, | ||
723 | }, | ||
724 | "USDT": { | ||
725 | "exchange_free": D("6.0"), | ||
726 | "exchange_used": D("1.2"), | ||
727 | "exchange_total": D("7.2"), | ||
728 | "margin_total": 0, | ||
729 | }, | ||
730 | "XVG": { | ||
731 | "exchange_free": 16, | ||
732 | "exchange_used": 0, | ||
733 | "exchange_total": 16, | ||
734 | "margin_total": 0, | ||
735 | }, | ||
736 | "XMR": { | ||
737 | "exchange_free": 0, | ||
738 | "exchange_used": 0, | ||
739 | "exchange_total": 0, | ||
740 | "margin_total": D("-1.0"), | ||
741 | "margin_free": 0, | ||
742 | }, | ||
743 | } | ||
744 | |||
745 | @mock.patch.object(helper, "get_ticker") | ||
746 | def test_in_currency(self, get_ticker): | ||
747 | portfolio.BalanceStore.all = { | ||
748 | "BTC": portfolio.Balance("BTC", { | ||
749 | "total": "0.65", | ||
750 | "exchange_total":"0.65", | ||
751 | "exchange_free": "0.35", | ||
752 | "exchange_used": "0.30"}), | ||
753 | "ETH": portfolio.Balance("ETH", { | ||
754 | "total": 3, | ||
755 | "exchange_total": 3, | ||
756 | "exchange_free": 3, | ||
757 | "exchange_used": 0}), | ||
758 | } | ||
654 | market = mock.Mock() | 759 | market = mock.Mock() |
655 | market.fetch_ticker.side_effect = [ | 760 | get_ticker.return_value = { |
656 | { "bid": 1, "ask": 3 }, | 761 | "bid": D("0.09"), |
657 | portfolio.ExchangeError("foo"), | 762 | "ask": D("0.11"), |
658 | { "bid": 10, "ask": 40 }, | 763 | "average": D("0.1"), |
659 | portfolio.ExchangeError("foo"), | 764 | } |
660 | portfolio.ExchangeError("foo"), | ||
661 | ] | ||
662 | 765 | ||
663 | ticker = portfolio.Trade.get_ticker("ETH", "ETC", market) | 766 | amounts = portfolio.BalanceStore.in_currency("BTC", market) |
664 | market.fetch_ticker.assert_called_with("ETH/ETC") | 767 | self.assertEqual("BTC", amounts["ETH"].currency) |
665 | self.assertEqual(1, ticker["bid"]) | 768 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
666 | self.assertEqual(3, ticker["ask"]) | 769 | self.assertEqual(D("0.30"), amounts["ETH"].value) |
667 | self.assertEqual(2, ticker["average"]) | ||
668 | self.assertFalse(ticker["inverted"]) | ||
669 | 770 | ||
670 | ticker = portfolio.Trade.get_ticker("ETH", "XVG", market) | 771 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid") |
671 | self.assertEqual(0.0625, ticker["average"]) | 772 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
672 | self.assertTrue(ticker["inverted"]) | 773 | self.assertEqual(D("0.27"), amounts["ETH"].value) |
673 | self.assertIn("original", ticker) | ||
674 | self.assertEqual(10, ticker["original"]["bid"]) | ||
675 | 774 | ||
676 | ticker = portfolio.Trade.get_ticker("XVG", "XMR", market) | 775 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid", type="exchange_used") |
677 | self.assertIsNone(ticker) | 776 | self.assertEqual(D("0.30"), amounts["BTC"].value) |
777 | self.assertEqual(0, amounts["ETH"].value) | ||
678 | 778 | ||
679 | market.fetch_ticker.assert_has_calls([ | 779 | def test_fetch_balances(self): |
680 | mock.call("ETH/ETC"), | 780 | market = mock.Mock() |
681 | mock.call("ETH/XVG"), | 781 | market.fetch_all_balances.return_value = self.fetch_balance |
682 | mock.call("XVG/ETH"), | ||
683 | mock.call("XVG/XMR"), | ||
684 | mock.call("XMR/XVG"), | ||
685 | ]) | ||
686 | 782 | ||
687 | market2 = mock.Mock() | 783 | portfolio.BalanceStore.fetch_balances(market) |
688 | market2.fetch_ticker.side_effect = [ | 784 | self.assertNotIn("ETC", portfolio.BalanceStore.currencies()) |
689 | { "bid": 1, "ask": 3 }, | 785 | self.assertListEqual(["USDT", "XVG", "XMR"], list(portfolio.BalanceStore.currencies())) |
690 | { "bid": 1.2, "ask": 3.5 }, | ||
691 | ] | ||
692 | ticker1 = portfolio.Trade.get_ticker("ETH", "ETC", market2) | ||
693 | ticker2 = portfolio.Trade.get_ticker("ETH", "ETC", market2) | ||
694 | ticker3 = portfolio.Trade.get_ticker("ETC", "ETH", market2) | ||
695 | market2.fetch_ticker.assert_called_once_with("ETH/ETC") | ||
696 | self.assertEqual(1, ticker1["bid"]) | ||
697 | self.assertDictEqual(ticker1, ticker2) | ||
698 | self.assertDictEqual(ticker1, ticker3["original"]) | ||
699 | 786 | ||
700 | ticker4 = portfolio.Trade.get_ticker("ETH", "ETC", market2, refresh=True) | 787 | portfolio.BalanceStore.all["ETC"] = portfolio.Balance("ETC", { |
701 | ticker5 = portfolio.Trade.get_ticker("ETH", "ETC", market2) | 788 | "exchange_total": "1", "exchange_free": "0", |
702 | self.assertEqual(1.2, ticker4["bid"]) | 789 | "exchange_used": "1" }) |
703 | self.assertDictEqual(ticker4, ticker5) | 790 | portfolio.BalanceStore.fetch_balances(market) |
791 | self.assertEqual(0, portfolio.BalanceStore.all["ETC"].total) | ||
792 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.BalanceStore.currencies())) | ||
704 | 793 | ||
705 | market3 = mock.Mock() | 794 | @mock.patch.object(portfolio.Portfolio, "repartition") |
706 | market3.fetch_ticker.side_effect = [ | 795 | def test_dispatch_assets(self, repartition): |
707 | { "bid": 1, "ask": 3 }, | 796 | market = mock.Mock() |
708 | { "bid": 1.2, "ask": 3.5 }, | 797 | market.fetch_all_balances.return_value = self.fetch_balance |
709 | ] | 798 | portfolio.BalanceStore.fetch_balances(market) |
710 | ticker6 = portfolio.Trade.get_ticker("ETH", "ETC", market3) | 799 | |
711 | portfolio.Trade.ticker_cache_timestamp -= 4 | 800 | self.assertNotIn("XEM", portfolio.BalanceStore.currencies()) |
712 | ticker7 = portfolio.Trade.get_ticker("ETH", "ETC", market3) | 801 | |
713 | portfolio.Trade.ticker_cache_timestamp -= 2 | 802 | repartition.return_value = { |
714 | ticker8 = portfolio.Trade.get_ticker("ETH", "ETC", market3) | 803 | "XEM": (D("0.75"), "long"), |
715 | self.assertDictEqual(ticker6, ticker7) | 804 | "BTC": (D("0.26"), "long"), |
716 | self.assertEqual(1.2, ticker8["bid"]) | 805 | } |
806 | |||
807 | amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "10.1")) | ||
808 | self.assertIn("XEM", portfolio.BalanceStore.currencies()) | ||
809 | self.assertEqual(D("2.6"), amounts["BTC"].value) | ||
810 | self.assertEqual(D("7.5"), amounts["XEM"].value) | ||
811 | |||
812 | def test_currencies(self): | ||
813 | portfolio.BalanceStore.all = { | ||
814 | "BTC": portfolio.Balance("BTC", { | ||
815 | "total": "0.65", | ||
816 | "exchange_total":"0.65", | ||
817 | "exchange_free": "0.35", | ||
818 | "exchange_used": "0.30"}), | ||
819 | "ETH": portfolio.Balance("ETH", { | ||
820 | "total": 3, | ||
821 | "exchange_total": 3, | ||
822 | "exchange_free": 3, | ||
823 | "exchange_used": 0}), | ||
824 | } | ||
825 | self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies())) | ||
826 | |||
827 | class ComputationTest(WebMockTestCase): | ||
828 | def test_compute_value(self): | ||
829 | compute = mock.Mock() | ||
830 | portfolio.Computation.compute_value("foo", "buy", compute_value=compute) | ||
831 | compute.assert_called_with("foo", "ask") | ||
832 | |||
833 | compute.reset_mock() | ||
834 | portfolio.Computation.compute_value("foo", "sell", compute_value=compute) | ||
835 | compute.assert_called_with("foo", "bid") | ||
836 | |||
837 | compute.reset_mock() | ||
838 | portfolio.Computation.compute_value("foo", "ask", compute_value=compute) | ||
839 | compute.assert_called_with("foo", "ask") | ||
840 | |||
841 | compute.reset_mock() | ||
842 | portfolio.Computation.compute_value("foo", "bid", compute_value=compute) | ||
843 | compute.assert_called_with("foo", "bid") | ||
844 | |||
845 | compute.reset_mock() | ||
846 | portfolio.Computation.computations["test"] = compute | ||
847 | portfolio.Computation.compute_value("foo", "bid", compute_value="test") | ||
848 | compute.assert_called_with("foo", "bid") | ||
849 | |||
850 | |||
851 | class TradeTest(WebMockTestCase): | ||
717 | 852 | ||
718 | def test_values_assertion(self): | 853 | def test_values_assertion(self): |
719 | value_from = portfolio.Amount("BTC", "1.0") | 854 | value_from = portfolio.Amount("BTC", "1.0") |
@@ -736,14 +871,6 @@ class TradeTest(WebMockTestCase): | |||
736 | trade = portfolio.Trade(value_from, value_to, "ETH") | 871 | trade = portfolio.Trade(value_from, value_to, "ETH") |
737 | self.assertEqual(0, trade.value_from.linked_to) | 872 | self.assertEqual(0, trade.value_from.linked_to) |
738 | 873 | ||
739 | def test_fetch_fees(self): | ||
740 | market = mock.Mock() | ||
741 | market.fetch_fees.return_value = "Foo" | ||
742 | self.assertEqual("Foo", portfolio.Trade.fetch_fees(market)) | ||
743 | market.fetch_fees.assert_called_once() | ||
744 | self.assertEqual("Foo", portfolio.Trade.fetch_fees(market)) | ||
745 | market.fetch_fees.assert_called_once() | ||
746 | |||
747 | def test_action(self): | 874 | def test_action(self): |
748 | value_from = portfolio.Amount("BTC", "1.0") | 875 | value_from = portfolio.Amount("BTC", "1.0") |
749 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 876 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
@@ -821,34 +948,6 @@ class TradeTest(WebMockTestCase): | |||
821 | 948 | ||
822 | self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount) | 949 | self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount) |
823 | 950 | ||
824 | def test_prepare_orders(self): | ||
825 | trade_mock1 = mock.Mock() | ||
826 | trade_mock2 = mock.Mock() | ||
827 | |||
828 | portfolio.Trade.trades.append(trade_mock1) | ||
829 | portfolio.Trade.trades.append(trade_mock2) | ||
830 | |||
831 | portfolio.Trade.prepare_orders() | ||
832 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | ||
833 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
834 | |||
835 | portfolio.Trade.prepare_orders(compute_value="bla") | ||
836 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | ||
837 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | ||
838 | |||
839 | trade_mock1.prepare_order.reset_mock() | ||
840 | trade_mock2.prepare_order.reset_mock() | ||
841 | |||
842 | trade_mock1.action = "foo" | ||
843 | trade_mock2.action = "bar" | ||
844 | portfolio.Trade.prepare_orders(only="bar") | ||
845 | trade_mock1.prepare_order.assert_not_called() | ||
846 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | ||
847 | |||
848 | @unittest.skip("TODO") | ||
849 | def test_compute_trades(self): | ||
850 | pass | ||
851 | |||
852 | @unittest.skip("TODO") | 951 | @unittest.skip("TODO") |
853 | def test_prepare_order(self): | 952 | def test_prepare_order(self): |
854 | pass | 953 | pass |
@@ -857,77 +956,6 @@ class TradeTest(WebMockTestCase): | |||
857 | def test_update_order(self): | 956 | def test_update_order(self): |
858 | pass | 957 | pass |
859 | 958 | ||
860 | @unittest.skip("TODO") | ||
861 | def test_follow_orders(self): | ||
862 | pass | ||
863 | |||
864 | @unittest.skip("TODO") | ||
865 | def test_move_balances(self): | ||
866 | pass | ||
867 | |||
868 | def test_all_orders(self): | ||
869 | trade_mock1 = mock.Mock() | ||
870 | trade_mock2 = mock.Mock() | ||
871 | |||
872 | order_mock1 = mock.Mock() | ||
873 | order_mock2 = mock.Mock() | ||
874 | order_mock3 = mock.Mock() | ||
875 | |||
876 | trade_mock1.orders = [order_mock1, order_mock2] | ||
877 | trade_mock2.orders = [order_mock3] | ||
878 | |||
879 | order_mock1.status = "pending" | ||
880 | order_mock2.status = "open" | ||
881 | order_mock3.status = "open" | ||
882 | |||
883 | portfolio.Trade.trades.append(trade_mock1) | ||
884 | portfolio.Trade.trades.append(trade_mock2) | ||
885 | |||
886 | orders = portfolio.Trade.all_orders() | ||
887 | self.assertEqual(3, len(orders)) | ||
888 | |||
889 | open_orders = portfolio.Trade.all_orders(state="open") | ||
890 | self.assertEqual(2, len(open_orders)) | ||
891 | self.assertEqual([order_mock2, order_mock3], open_orders) | ||
892 | |||
893 | @mock.patch.object(portfolio.Trade, "all_orders") | ||
894 | def test_run_orders(self, all_orders): | ||
895 | order_mock1 = mock.Mock() | ||
896 | order_mock2 = mock.Mock() | ||
897 | order_mock3 = mock.Mock() | ||
898 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
899 | portfolio.Trade.run_orders() | ||
900 | all_orders.assert_called_with(state="pending") | ||
901 | |||
902 | order_mock1.run.assert_called() | ||
903 | order_mock2.run.assert_called() | ||
904 | order_mock3.run.assert_called() | ||
905 | |||
906 | @mock.patch.object(portfolio.Trade, "all_orders") | ||
907 | def test_update_all_orders_status(self, all_orders): | ||
908 | order_mock1 = mock.Mock() | ||
909 | order_mock2 = mock.Mock() | ||
910 | order_mock3 = mock.Mock() | ||
911 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
912 | portfolio.Trade.update_all_orders_status() | ||
913 | all_orders.assert_called_with(state="open") | ||
914 | |||
915 | order_mock1.get_status.assert_called() | ||
916 | order_mock2.get_status.assert_called() | ||
917 | order_mock3.get_status.assert_called() | ||
918 | |||
919 | def test_print_all_with_order(self): | ||
920 | trade_mock1 = mock.Mock() | ||
921 | trade_mock2 = mock.Mock() | ||
922 | trade_mock3 = mock.Mock() | ||
923 | portfolio.Trade.trades = [trade_mock1, trade_mock2, trade_mock3] | ||
924 | |||
925 | portfolio.Trade.print_all_with_order() | ||
926 | |||
927 | trade_mock1.print_with_order.assert_called() | ||
928 | trade_mock2.print_with_order.assert_called() | ||
929 | trade_mock3.print_with_order.assert_called() | ||
930 | |||
931 | @mock.patch('sys.stdout', new_callable=StringIO) | 959 | @mock.patch('sys.stdout', new_callable=StringIO) |
932 | def test_print_with_order(self, mock_stdout): | 960 | def test_print_with_order(self, mock_stdout): |
933 | value_from = portfolio.Amount("BTC", "0.5") | 961 | value_from = portfolio.Amount("BTC", "0.5") |
@@ -951,28 +979,6 @@ class TradeTest(WebMockTestCase): | |||
951 | self.assertEqual("\tMock 1", out[1]) | 979 | self.assertEqual("\tMock 1", out[1]) |
952 | self.assertEqual("\tMock 2", out[2]) | 980 | self.assertEqual("\tMock 2", out[2]) |
953 | 981 | ||
954 | def test_compute_value(self): | ||
955 | compute = mock.Mock() | ||
956 | portfolio.Trade.compute_value("foo", "buy", compute_value=compute) | ||
957 | compute.assert_called_with("foo", "ask") | ||
958 | |||
959 | compute.reset_mock() | ||
960 | portfolio.Trade.compute_value("foo", "sell", compute_value=compute) | ||
961 | compute.assert_called_with("foo", "bid") | ||
962 | |||
963 | compute.reset_mock() | ||
964 | portfolio.Trade.compute_value("foo", "ask", compute_value=compute) | ||
965 | compute.assert_called_with("foo", "ask") | ||
966 | |||
967 | compute.reset_mock() | ||
968 | portfolio.Trade.compute_value("foo", "bid", compute_value=compute) | ||
969 | compute.assert_called_with("foo", "bid") | ||
970 | |||
971 | compute.reset_mock() | ||
972 | portfolio.Computation.computations["test"] = compute | ||
973 | portfolio.Trade.compute_value("foo", "bid", compute_value="test") | ||
974 | compute.assert_called_with("foo", "bid") | ||
975 | |||
976 | def test__repr(self): | 982 | def test__repr(self): |
977 | value_from = portfolio.Amount("BTC", "0.5") | 983 | value_from = portfolio.Amount("BTC", "0.5") |
978 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 984 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
@@ -1045,7 +1051,7 @@ class AcceptanceTest(WebMockTestCase): | |||
1045 | "ask": D("0.0012") | 1051 | "ask": D("0.0012") |
1046 | } | 1052 | } |
1047 | if symbol == "USDT/BTC": | 1053 | if symbol == "USDT/BTC": |
1048 | raise portfolio.ExchangeError | 1054 | raise helper.ExchangeError |
1049 | if symbol == "BTC/USDT": | 1055 | if symbol == "BTC/USDT": |
1050 | return { | 1056 | return { |
1051 | "symbol": "BTC/USDT", | 1057 | "symbol": "BTC/USDT", |
@@ -1059,15 +1065,15 @@ class AcceptanceTest(WebMockTestCase): | |||
1059 | market.fetch_ticker.side_effect = fetch_ticker | 1065 | market.fetch_ticker.side_effect = fetch_ticker |
1060 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 1066 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): |
1061 | # Action 1 | 1067 | # Action 1 |
1062 | portfolio.Balance.prepare_trades(market) | 1068 | helper.prepare_trades(market) |
1063 | 1069 | ||
1064 | balances = portfolio.Balance.known_balances | 1070 | balances = portfolio.BalanceStore.all |
1065 | self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total) | 1071 | self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total) |
1066 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) | 1072 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) |
1067 | self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total) | 1073 | self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total) |
1068 | 1074 | ||
1069 | 1075 | ||
1070 | trades = portfolio.Trade.trades | 1076 | trades = TradeStore.all |
1071 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from) | 1077 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from) |
1072 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to) | 1078 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to) |
1073 | self.assertEqual("dispose", trades[0].action) | 1079 | self.assertEqual("dispose", trades[0].action) |
@@ -1123,7 +1129,7 @@ class AcceptanceTest(WebMockTestCase): | |||
1123 | market.order_precision.return_value = 8 | 1129 | market.order_precision.return_value = 8 |
1124 | 1130 | ||
1125 | # Action 3 | 1131 | # Action 3 |
1126 | portfolio.Trade.run_orders() | 1132 | portfolio.TradeStore.run_orders() |
1127 | 1133 | ||
1128 | self.assertEqual("open", all_orders[0].status) | 1134 | self.assertEqual("open", all_orders[0].status) |
1129 | self.assertEqual("open", all_orders[1].status) | 1135 | self.assertEqual("open", all_orders[1].status) |
@@ -1131,7 +1137,7 @@ class AcceptanceTest(WebMockTestCase): | |||
1131 | market.fetch_order.return_value = { "status": "closed" } | 1137 | market.fetch_order.return_value = { "status": "closed" } |
1132 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 1138 | with mock.patch.object(portfolio.time, "sleep") as sleep: |
1133 | # Action 4 | 1139 | # Action 4 |
1134 | portfolio.Trade.follow_orders(verbose=False) | 1140 | helper.follow_orders(verbose=False) |
1135 | 1141 | ||
1136 | sleep.assert_called_with(30) | 1142 | sleep.assert_called_with(30) |
1137 | 1143 | ||
@@ -1164,16 +1170,16 @@ class AcceptanceTest(WebMockTestCase): | |||
1164 | 1170 | ||
1165 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 1171 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): |
1166 | # Action 5 | 1172 | # Action 5 |
1167 | portfolio.Balance.update_trades(market, only="buy", compute_value="average") | 1173 | helper.update_trades(market, only="buy", compute_value="average") |
1168 | 1174 | ||
1169 | balances = portfolio.Balance.known_balances | 1175 | balances = portfolio.BalanceStore.all |
1170 | self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total) | 1176 | self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total) |
1171 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) | 1177 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) |
1172 | self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total) | 1178 | self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total) |
1173 | self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total) | 1179 | self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total) |
1174 | 1180 | ||
1175 | 1181 | ||
1176 | trades = portfolio.Trade.trades | 1182 | trades = TradeStore.all |
1177 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from) | 1183 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from) |
1178 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to) | 1184 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to) |
1179 | self.assertEqual("sell", trades["ETH"].action) | 1185 | self.assertEqual("sell", trades["ETH"].action) |
@@ -1232,11 +1238,11 @@ class AcceptanceTest(WebMockTestCase): | |||
1232 | 1238 | ||
1233 | # Action 7 | 1239 | # Action 7 |
1234 | # TODO | 1240 | # TODO |
1235 | # portfolio.Trade.run_orders() | 1241 | # portfolio.TradeStore.run_orders() |
1236 | 1242 | ||
1237 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 1243 | with mock.patch.object(portfolio.time, "sleep") as sleep: |
1238 | # Action 8 | 1244 | # Action 8 |
1239 | portfolio.Trade.follow_orders(verbose=False) | 1245 | helper.follow_orders(verbose=False) |
1240 | 1246 | ||
1241 | sleep.assert_called_with(30) | 1247 | sleep.assert_called_with(30) |
1242 | 1248 | ||