diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-24 19:53:58 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-24 19:53:58 +0100 |
commit | 3d0247f944d7510943dfaa64eeb0e15a43b6c989 (patch) | |
tree | 2695181c77e444d52bca5de47b6c99b9b6c1ed32 | |
parent | c31df868c655612b8387a25111e69882f0fe6344 (diff) | |
download | Trader-3d0247f944d7510943dfaa64eeb0e15a43b6c989.tar.gz Trader-3d0247f944d7510943dfaa64eeb0e15a43b6c989.tar.zst Trader-3d0247f944d7510943dfaa64eeb0e15a43b6c989.zip |
Add report store to store messages and logs
-rw-r--r-- | helper.py | 81 | ||||
-rw-r--r-- | portfolio.py | 107 | ||||
-rw-r--r-- | store.py | 224 | ||||
-rw-r--r-- | test.py | 850 |
4 files changed, 1041 insertions, 221 deletions
@@ -4,6 +4,8 @@ from store import * | |||
4 | 4 | ||
5 | def move_balances(market, debug=False): | 5 | def move_balances(market, debug=False): |
6 | needed_in_margin = {} | 6 | needed_in_margin = {} |
7 | moving_to_margin = {} | ||
8 | |||
7 | for currency in BalanceStore.all: | 9 | for currency in BalanceStore.all: |
8 | if BalanceStore.all[currency].margin_free != 0: | 10 | if BalanceStore.all[currency].margin_free != 0: |
9 | needed_in_margin[currency] = 0 | 11 | needed_in_margin[currency] = 0 |
@@ -14,18 +16,16 @@ def move_balances(market, debug=False): | |||
14 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | 16 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) |
15 | for currency, needed in needed_in_margin.items(): | 17 | for currency, needed in needed_in_margin.items(): |
16 | current_balance = BalanceStore.all[currency].margin_free | 18 | current_balance = BalanceStore.all[currency].margin_free |
17 | delta = (needed - current_balance).value | 19 | moving_to_margin[currency] = (needed - current_balance) |
18 | # FIXME: don't remove too much if there are open margin position | 20 | delta = moving_to_margin[currency].value |
21 | if debug: | ||
22 | ReportStore.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) | ||
23 | continue | ||
19 | if delta > 0: | 24 | if delta > 0: |
20 | if debug: | 25 | market.transfer_balance(currency, delta, "exchange", "margin") |
21 | print("market.transfer_balance({}, {}, 'exchange', 'margin')".format(currency, delta)) | ||
22 | else: | ||
23 | market.transfer_balance(currency, delta, "exchange", "margin") | ||
24 | elif delta < 0: | 26 | elif delta < 0: |
25 | if debug: | 27 | market.transfer_balance(currency, -delta, "margin", "exchange") |
26 | print("market.transfer_balance({}, {}, 'margin', 'exchange')".format(currency, -delta)) | 28 | ReportStore.log_move_balances(needed_in_margin, moving_to_margin, debug) |
27 | else: | ||
28 | market.transfer_balance(currency, -delta, "margin", "exchange") | ||
29 | 29 | ||
30 | BalanceStore.fetch_balances(market) | 30 | BalanceStore.fetch_balances(market) |
31 | 31 | ||
@@ -73,6 +73,7 @@ def fetch_fees(market): | |||
73 | return fees_cache[market.__class__] | 73 | return fees_cache[market.__class__] |
74 | 74 | ||
75 | def prepare_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", debug=False): | 75 | def prepare_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", debug=False): |
76 | ReportStore.log_stage("prepare_trades") | ||
76 | BalanceStore.fetch_balances(market) | 77 | BalanceStore.fetch_balances(market) |
77 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | 78 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) |
78 | total_base_value = sum(values_in_base.values()) | 79 | total_base_value = sum(values_in_base.values()) |
@@ -82,6 +83,7 @@ def prepare_trades(market, base_currency="BTC", liquidity="medium", compute_valu | |||
82 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | 83 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) |
83 | 84 | ||
84 | def update_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", only=None, debug=False): | 85 | def update_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", only=None, debug=False): |
86 | ReportStore.log_stage("update_trades") | ||
85 | BalanceStore.fetch_balances(market) | 87 | BalanceStore.fetch_balances(market) |
86 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | 88 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) |
87 | total_base_value = sum(values_in_base.values()) | 89 | total_base_value = sum(values_in_base.values()) |
@@ -89,91 +91,76 @@ def update_trades(market, base_currency="BTC", liquidity="medium", compute_value | |||
89 | TradeStore.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) | 91 | TradeStore.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) |
90 | 92 | ||
91 | def prepare_trades_to_sell_all(market, base_currency="BTC", compute_value="average", debug=False): | 93 | def prepare_trades_to_sell_all(market, base_currency="BTC", compute_value="average", debug=False): |
94 | ReportStore.log_stage("prepare_trades_to_sell_all") | ||
92 | BalanceStore.fetch_balances(market) | 95 | BalanceStore.fetch_balances(market) |
93 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | 96 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) |
94 | total_base_value = sum(values_in_base.values()) | 97 | total_base_value = sum(values_in_base.values()) |
95 | new_repartition = BalanceStore.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | 98 | new_repartition = BalanceStore.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) |
96 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | 99 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) |
97 | 100 | ||
98 | def follow_orders(verbose=True, sleep=None): | 101 | def follow_orders(sleep=None): |
99 | if sleep is None: | 102 | if sleep is None: |
100 | sleep = 7 if TradeStore.debug else 30 | 103 | sleep = 7 if TradeStore.debug else 30 |
104 | if TradeStore.debug: | ||
105 | ReportStore.log_debug_action("Set follow_orders tick to {}s".format(sleep)) | ||
101 | tick = 0 | 106 | tick = 0 |
107 | ReportStore.log_stage("follow_orders_begin") | ||
102 | while len(TradeStore.all_orders(state="open")) > 0: | 108 | while len(TradeStore.all_orders(state="open")) > 0: |
103 | time.sleep(sleep) | 109 | time.sleep(sleep) |
104 | tick += 1 | 110 | tick += 1 |
105 | for order in TradeStore.all_orders(state="open"): | 111 | open_orders = TradeStore.all_orders(state="open") |
112 | ReportStore.log_stage("follow_orders_tick_{}".format(tick)) | ||
113 | ReportStore.log_orders(open_orders, tick=tick) | ||
114 | for order in open_orders: | ||
106 | if order.get_status() != "open": | 115 | if order.get_status() != "open": |
107 | if verbose: | 116 | ReportStore.log_order(order, tick, finished=True) |
108 | print("finished {}".format(order)) | ||
109 | else: | 117 | else: |
110 | order.trade.update_order(order, tick) | 118 | order.trade.update_order(order, tick) |
111 | if verbose: | 119 | ReportStore.log_stage("follow_orders_end") |
112 | print("All orders finished") | ||
113 | 120 | ||
114 | def print_orders(market, base_currency="BTC"): | 121 | def print_orders(market, base_currency="BTC"): |
122 | ReportStore.log_stage("print_orders") | ||
115 | prepare_trades(market, base_currency=base_currency, compute_value="average", debug=True) | 123 | prepare_trades(market, base_currency=base_currency, compute_value="average", debug=True) |
116 | TradeStore.prepare_orders(compute_value="average") | 124 | TradeStore.prepare_orders(compute_value="average") |
117 | for currency, balance in BalanceStore.all.items(): | ||
118 | print(balance) | ||
119 | TradeStore.print_all_with_order() | ||
120 | 125 | ||
121 | def print_balances(market, base_currency="BTC"): | 126 | def print_balances(market, base_currency="BTC"): |
122 | BalanceStore.fetch_balances(market) | 127 | BalanceStore.fetch_balances(market) |
123 | for currency, balance in BalanceStore.all.items(): | ||
124 | print(balance) | ||
125 | if base_currency is not None: | 128 | if base_currency is not None: |
126 | print("total:") | 129 | ReportStore.print_log("total:") |
127 | print(sum(BalanceStore.in_currency(base_currency, market).values())) | 130 | ReportStore.print_log(sum(BalanceStore.in_currency(base_currency, market).values())) |
128 | 131 | ||
129 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC", debug=False): | 132 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC", debug=False): |
133 | ReportStore.log_stage("process_sell_needed__1_sell_begin") | ||
130 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) | 134 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) |
131 | TradeStore.prepare_orders(compute_value="average", only="dispose") | 135 | TradeStore.prepare_orders(compute_value="average", only="dispose") |
132 | print("------------------") | ||
133 | for currency, balance in BalanceStore.all.items(): | ||
134 | print(balance) | ||
135 | print("------------------") | ||
136 | TradeStore.print_all_with_order() | ||
137 | print("------------------") | ||
138 | TradeStore.run_orders() | 136 | TradeStore.run_orders() |
139 | follow_orders() | 137 | follow_orders() |
138 | ReportStore.log_stage("process_sell_needed__1_sell_end") | ||
140 | 139 | ||
141 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC", debug=False): | 140 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC", debug=False): |
141 | ReportStore.log_stage("process_sell_needed__2_buy_begin") | ||
142 | update_trades(market, base_currency=base_currency, liquidity=liquidity, debug=debug, only="acquire") | 142 | update_trades(market, base_currency=base_currency, liquidity=liquidity, debug=debug, only="acquire") |
143 | TradeStore.prepare_orders(compute_value="average", only="acquire") | 143 | TradeStore.prepare_orders(compute_value="average", only="acquire") |
144 | print("------------------") | ||
145 | for currency, balance in BalanceStore.all.items(): | ||
146 | print(balance) | ||
147 | print("------------------") | ||
148 | TradeStore.print_all_with_order() | ||
149 | print("------------------") | ||
150 | move_balances(market, debug=debug) | 144 | move_balances(market, debug=debug) |
151 | TradeStore.run_orders() | 145 | TradeStore.run_orders() |
152 | follow_orders() | 146 | follow_orders() |
147 | ReportStore.log_stage("process_sell_needed__2_buy_end") | ||
153 | 148 | ||
154 | def process_sell_all__1_all_sell(market, base_currency="BTC", debug=False, liquidity="medium"): | 149 | def process_sell_all__1_all_sell(market, base_currency="BTC", debug=False, liquidity="medium"): |
150 | ReportStore.log_stage("process_sell_all__1_all_sell_begin") | ||
155 | prepare_trades_to_sell_all(market, base_currency=base_currency, debug=debug) | 151 | prepare_trades_to_sell_all(market, base_currency=base_currency, debug=debug) |
156 | TradeStore.prepare_orders(compute_value="average") | 152 | TradeStore.prepare_orders(compute_value="average") |
157 | print("------------------") | ||
158 | for currency, balance in BalanceStore.all.items(): | ||
159 | print(balance) | ||
160 | print("------------------") | ||
161 | TradeStore.print_all_with_order() | ||
162 | print("------------------") | ||
163 | TradeStore.run_orders() | 153 | TradeStore.run_orders() |
164 | follow_orders() | 154 | follow_orders() |
155 | ReportStore.log_stage("process_sell_all__1_all_sell_end") | ||
165 | 156 | ||
166 | def process_sell_all__2_all_buy(market, base_currency="BTC", debug=False, liquidity="medium"): | 157 | def process_sell_all__2_all_buy(market, base_currency="BTC", debug=False, liquidity="medium"): |
158 | ReportStore.log_stage("process_sell_all__2_all_buy_begin") | ||
167 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) | 159 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) |
168 | TradeStore.prepare_orders(compute_value="average") | 160 | TradeStore.prepare_orders(compute_value="average") |
169 | print("------------------") | ||
170 | for currency, balance in BalanceStore.all.items(): | ||
171 | print(balance) | ||
172 | print("------------------") | ||
173 | TradeStore.print_all_with_order() | ||
174 | print("------------------") | ||
175 | move_balances(market, debug=debug) | 161 | move_balances(market, debug=debug) |
176 | TradeStore.run_orders() | 162 | TradeStore.run_orders() |
177 | follow_orders() | 163 | follow_orders() |
164 | ReportStore.log_stage("process_sell_all__2_all_buy_end") | ||
178 | 165 | ||
179 | 166 | ||
diff --git a/portfolio.py b/portfolio.py index c3809f0..f9423b9 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta | |||
3 | from decimal import Decimal as D, ROUND_DOWN | 3 | from decimal import Decimal as D, ROUND_DOWN |
4 | # Put your poloniex api key in market.py | 4 | # Put your poloniex api key in market.py |
5 | from json import JSONDecodeError | 5 | from json import JSONDecodeError |
6 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | ||
6 | from ccxt import ExchangeError, ExchangeNotAvailable | 7 | from ccxt import ExchangeError, ExchangeNotAvailable |
7 | import requests | 8 | import requests |
8 | import helper as h | 9 | import helper as h |
@@ -33,11 +34,14 @@ class Portfolio: | |||
33 | def get_cryptoportfolio(cls): | 34 | def get_cryptoportfolio(cls): |
34 | try: | 35 | try: |
35 | r = requests.get(cls.URL) | 36 | r = requests.get(cls.URL) |
36 | except Exception: | 37 | ReportStore.log_http_request(r.request.method, |
38 | r.request.url, r.request.body, r.request.headers, r) | ||
39 | except Exception as e: | ||
40 | ReportStore.log_error("get_cryptoportfolio", exception=e) | ||
37 | return | 41 | return |
38 | try: | 42 | try: |
39 | cls.data = r.json(parse_int=D, parse_float=D) | 43 | cls.data = r.json(parse_int=D, parse_float=D) |
40 | except JSONDecodeError: | 44 | except (JSONDecodeError, SimpleJSONDecodeError): |
41 | cls.data = None | 45 | cls.data = None |
42 | 46 | ||
43 | @classmethod | 47 | @classmethod |
@@ -126,6 +130,12 @@ class Amount: | |||
126 | else: | 130 | else: |
127 | raise Exception("This asset is not available in the chosen market") | 131 | raise Exception("This asset is not available in the chosen market") |
128 | 132 | ||
133 | def as_json(self): | ||
134 | return { | ||
135 | "currency": self.currency, | ||
136 | "value": round(self).value.normalize(), | ||
137 | } | ||
138 | |||
129 | def __round__(self, n=8): | 139 | def __round__(self, n=8): |
130 | return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN)) | 140 | return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN)) |
131 | 141 | ||
@@ -216,12 +226,13 @@ class Amount: | |||
216 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) | 226 | return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to)) |
217 | 227 | ||
218 | class Balance: | 228 | class Balance: |
229 | base_keys = ["total", "exchange_total", "exchange_used", | ||
230 | "exchange_free", "margin_total", "margin_borrowed", | ||
231 | "margin_free"] | ||
219 | 232 | ||
220 | def __init__(self, currency, hash_): | 233 | def __init__(self, currency, hash_): |
221 | self.currency = currency | 234 | self.currency = currency |
222 | for key in ["total", | 235 | for key in self.base_keys: |
223 | "exchange_total", "exchange_used", "exchange_free", | ||
224 | "margin_total", "margin_borrowed", "margin_free"]: | ||
225 | setattr(self, key, Amount(currency, hash_.get(key, 0))) | 236 | setattr(self, key, Amount(currency, hash_.get(key, 0))) |
226 | 237 | ||
227 | self.margin_position_type = hash_.get("margin_position_type") | 238 | self.margin_position_type = hash_.get("margin_position_type") |
@@ -236,6 +247,9 @@ class Balance: | |||
236 | ]: | 247 | ]: |
237 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) | 248 | setattr(self, key, Amount(base_currency, hash_.get(key, 0))) |
238 | 249 | ||
250 | def as_json(self): | ||
251 | return dict(map(lambda x: (x, getattr(self, x).as_json()["value"]), self.base_keys)) | ||
252 | |||
239 | def __repr__(self): | 253 | def __repr__(self): |
240 | if self.exchange_total > 0: | 254 | if self.exchange_total > 0: |
241 | if self.exchange_free > 0 and self.exchange_used > 0: | 255 | if self.exchange_free > 0 and self.exchange_used > 0: |
@@ -318,23 +332,34 @@ class Trade: | |||
318 | def update_order(self, order, tick): | 332 | def update_order(self, order, tick): |
319 | new_order = None | 333 | new_order = None |
320 | if tick in [0, 1, 3, 4, 6]: | 334 | if tick in [0, 1, 3, 4, 6]: |
321 | print("{}, tick {}, waiting".format(order, tick)) | 335 | update = "waiting" |
336 | compute_value = None | ||
322 | elif tick == 2: | 337 | elif tick == 2: |
338 | update = "adjusting" | ||
339 | compute_value = 'lambda x, y: (x[y] + x["average"]) / 2' | ||
323 | new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2) | 340 | new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2) |
324 | print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order)) | ||
325 | elif tick ==5: | 341 | elif tick ==5: |
342 | update = "adjusting" | ||
343 | compute_value = 'lambda x, y: (x[y]*2 + x["average"]) / 3' | ||
326 | new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3) | 344 | new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3) |
327 | print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order)) | ||
328 | elif tick >= 7: | 345 | elif tick >= 7: |
329 | if tick == 7: | ||
330 | print("{}, tick {}, fallbacking to market value".format(order, tick)) | ||
331 | if (tick - 7) % 3 == 0: | 346 | if (tick - 7) % 3 == 0: |
332 | new_order = self.prepare_order(compute_value="default") | 347 | new_order = self.prepare_order(compute_value="default") |
333 | print("{}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order)) | 348 | update = "market_adjust" |
349 | compute_value = "default" | ||
350 | else: | ||
351 | update = "waiting" | ||
352 | compute_value = None | ||
353 | if tick == 7: | ||
354 | update = "market_fallback" | ||
355 | |||
356 | ReportStore.log_order(order, tick, update=update, | ||
357 | compute_value=compute_value, new_order=new_order) | ||
334 | 358 | ||
335 | if new_order is not None: | 359 | if new_order is not None: |
336 | order.cancel() | 360 | order.cancel() |
337 | new_order.run() | 361 | new_order.run() |
362 | ReportStore.log_order(order, tick, new_order=new_order) | ||
338 | 363 | ||
339 | def prepare_order(self, compute_value="default"): | 364 | def prepare_order(self, compute_value="default"): |
340 | if self.action is None: | 365 | if self.action is None: |
@@ -405,7 +430,7 @@ class Trade: | |||
405 | close_if_possible = (self.value_to == 0) | 430 | close_if_possible = (self.value_to == 0) |
406 | 431 | ||
407 | if delta <= 0: | 432 | if delta <= 0: |
408 | print("Less to do than already filled: {}".format(delta)) | 433 | ReportStore.log_error("prepare_order", message="Less to do than already filled: {}".format(delta)) |
409 | return None | 434 | return None |
410 | 435 | ||
411 | order = Order(self.order_action(inverted), | 436 | order = Order(self.order_action(inverted), |
@@ -414,6 +439,15 @@ class Trade: | |||
414 | self.orders.append(order) | 439 | self.orders.append(order) |
415 | return order | 440 | return order |
416 | 441 | ||
442 | def as_json(self): | ||
443 | return { | ||
444 | "action": self.action, | ||
445 | "from": self.value_from.as_json()["value"], | ||
446 | "to": self.value_to.as_json()["value"], | ||
447 | "currency": self.currency, | ||
448 | "base_currency": self.base_currency, | ||
449 | } | ||
450 | |||
417 | def __repr__(self): | 451 | def __repr__(self): |
418 | return "Trade({} -> {} in {}, {})".format( | 452 | return "Trade({} -> {} in {}, {})".format( |
419 | self.value_from, | 453 | self.value_from, |
@@ -421,12 +455,12 @@ class Trade: | |||
421 | self.currency, | 455 | self.currency, |
422 | self.action) | 456 | self.action) |
423 | 457 | ||
424 | def print_with_order(self): | 458 | def print_with_order(self, ind=""): |
425 | print(self) | 459 | ReportStore.print_log("{}{}".format(ind, self)) |
426 | for order in self.orders: | 460 | for order in self.orders: |
427 | print("\t", order, sep="") | 461 | ReportStore.print_log("{}\t{}".format(ind, order)) |
428 | for mouvement in order.mouvements: | 462 | for mouvement in order.mouvements: |
429 | print("\t\t", mouvement, sep="") | 463 | ReportStore.print_log("{}\t\t{}".format(ind, mouvement)) |
430 | 464 | ||
431 | class Order: | 465 | class Order: |
432 | def __init__(self, action, amount, rate, base_currency, trade_type, market, | 466 | def __init__(self, action, amount, rate, base_currency, trade_type, market, |
@@ -445,6 +479,20 @@ class Order: | |||
445 | self.id = None | 479 | self.id = None |
446 | self.fetch_cache_timestamp = None | 480 | self.fetch_cache_timestamp = None |
447 | 481 | ||
482 | def as_json(self): | ||
483 | return { | ||
484 | "action": self.action, | ||
485 | "trade_type": self.trade_type, | ||
486 | "amount": self.amount.as_json()["value"], | ||
487 | "currency": self.amount.as_json()["currency"], | ||
488 | "base_currency": self.base_currency, | ||
489 | "rate": self.rate, | ||
490 | "status": self.status, | ||
491 | "close_if_possible": self.close_if_possible, | ||
492 | "id": self.id, | ||
493 | "mouvements": list(map(lambda x: x.as_json(), self.mouvements)) | ||
494 | } | ||
495 | |||
448 | def __repr__(self): | 496 | def __repr__(self): |
449 | return "Order({} {} {} at {} {} [{}]{})".format( | 497 | return "Order({} {} {} at {} {} [{}]{})".format( |
450 | self.action, | 498 | self.action, |
@@ -480,7 +528,7 @@ class Order: | |||
480 | amount = round(self.amount, self.market.order_precision(symbol)).value | 528 | amount = round(self.amount, self.market.order_precision(symbol)).value |
481 | 529 | ||
482 | if TradeStore.debug: | 530 | if TradeStore.debug: |
483 | print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 531 | ReportStore.log_debug_action("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( |
484 | symbol, self.action, amount, self.rate, self.account)) | 532 | symbol, self.action, amount, self.rate, self.account)) |
485 | self.results.append({"debug": True, "id": -1}) | 533 | self.results.append({"debug": True, "id": -1}) |
486 | else: | 534 | else: |
@@ -493,16 +541,15 @@ class Order: | |||
493 | return | 541 | return |
494 | except Exception as e: | 542 | except Exception as e: |
495 | self.status = "error" | 543 | self.status = "error" |
496 | print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 544 | action = "market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) |
497 | symbol, self.action, amount, self.rate, self.account)) | 545 | ReportStore.log_error(action, exception=e) |
498 | self.error_message = str("{}: {}".format(e.__class__.__name__, e)) | ||
499 | print(self.error_message) | ||
500 | return | 546 | return |
501 | self.id = self.results[0]["id"] | 547 | self.id = self.results[0]["id"] |
502 | self.status = "open" | 548 | self.status = "open" |
503 | 549 | ||
504 | def get_status(self): | 550 | def get_status(self): |
505 | if TradeStore.debug: | 551 | if TradeStore.debug: |
552 | ReportStore.log_debug_action("Getting {} status".format(self)) | ||
506 | return self.status | 553 | return self.status |
507 | # other states are "closed" and "canceled" | 554 | # other states are "closed" and "canceled" |
508 | if not self.finished: | 555 | if not self.finished: |
@@ -513,13 +560,17 @@ class Order: | |||
513 | 560 | ||
514 | def mark_finished_order(self): | 561 | def mark_finished_order(self): |
515 | if TradeStore.debug: | 562 | if TradeStore.debug: |
563 | ReportStore.log_debug_action("Mark {} as finished".format(self)) | ||
516 | return | 564 | return |
517 | if self.status == "closed": | 565 | if self.status == "closed": |
518 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: | 566 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: |
519 | self.market.close_margin_position(self.amount.currency, self.base_currency) | 567 | self.market.close_margin_position(self.amount.currency, self.base_currency) |
520 | 568 | ||
521 | def fetch(self, force=False): | 569 | def fetch(self, force=False): |
522 | if TradeStore.debug or (not force and self.fetch_cache_timestamp is not None | 570 | if TradeStore.debug: |
571 | ReportStore.log_debug_action("Fetching {}".format(self)) | ||
572 | return | ||
573 | if (not force and self.fetch_cache_timestamp is not None | ||
523 | and time.time() - self.fetch_cache_timestamp < 10): | 574 | and time.time() - self.fetch_cache_timestamp < 10): |
524 | return | 575 | return |
525 | self.fetch_cache_timestamp = time.time() | 576 | self.fetch_cache_timestamp = time.time() |
@@ -566,6 +617,7 @@ class Order: | |||
566 | 617 | ||
567 | def cancel(self): | 618 | def cancel(self): |
568 | if TradeStore.debug: | 619 | if TradeStore.debug: |
620 | ReportStore.log_debug_action("Mark {} as cancelled".format(self)) | ||
569 | self.status = "canceled" | 621 | self.status = "canceled" |
570 | return | 622 | return |
571 | self.market.cancel_order(self.id) | 623 | self.market.cancel_order(self.id) |
@@ -587,6 +639,17 @@ class Mouvement: | |||
587 | # rate * total = total_in_base | 639 | # rate * total = total_in_base |
588 | self.total_in_base = Amount(base_currency, hash_.get("total", 0)) | 640 | self.total_in_base = Amount(base_currency, hash_.get("total", 0)) |
589 | 641 | ||
642 | def as_json(self): | ||
643 | return { | ||
644 | "fee_rate": self.fee_rate, | ||
645 | "date": self.date, | ||
646 | "action": self.action, | ||
647 | "total": self.total.value, | ||
648 | "currency": self.currency, | ||
649 | "total_in_base": self.total_in_base.value, | ||
650 | "base_currency": self.base_currency | ||
651 | } | ||
652 | |||
590 | def __repr__(self): | 653 | def __repr__(self): |
591 | if self.fee_rate > 0: | 654 | if self.fee_rate > 0: |
592 | fee_rate = " fee: {}%".format(self.fee_rate * 100) | 655 | fee_rate = " fee: {}%".format(self.fee_rate * 100) |
@@ -1,6 +1,181 @@ | |||
1 | import portfolio | 1 | import portfolio |
2 | import simplejson as json | ||
3 | from decimal import Decimal as D, ROUND_DOWN | ||
4 | from datetime import date, datetime | ||
2 | 5 | ||
3 | __all__ = ["BalanceStore", "TradeStore"] | 6 | __all__ = ["BalanceStore", "ReportStore", "TradeStore"] |
7 | |||
8 | class ReportStore: | ||
9 | logs = [] | ||
10 | verbose_print = True | ||
11 | |||
12 | @classmethod | ||
13 | def print_log(cls, message): | ||
14 | message = str(message) | ||
15 | if cls.verbose_print: | ||
16 | print(message) | ||
17 | |||
18 | @classmethod | ||
19 | def add_log(cls, hash_): | ||
20 | hash_["date"] = datetime.now() | ||
21 | cls.logs.append(hash_) | ||
22 | |||
23 | @classmethod | ||
24 | def to_json(cls): | ||
25 | def default_json_serial(obj): | ||
26 | if isinstance(obj, (datetime, date)): | ||
27 | return obj.isoformat() | ||
28 | raise TypeError ("Type %s not serializable" % type(obj)) | ||
29 | return json.dumps(cls.logs, default=default_json_serial) | ||
30 | |||
31 | @classmethod | ||
32 | def set_verbose(cls, verbose_print): | ||
33 | cls.verbose_print = verbose_print | ||
34 | |||
35 | @classmethod | ||
36 | def log_stage(cls, stage): | ||
37 | cls.print_log("-" * (len(stage) + 8)) | ||
38 | cls.print_log("[Stage] {}".format(stage)) | ||
39 | |||
40 | cls.add_log({ | ||
41 | "type": "stage", | ||
42 | "stage": stage, | ||
43 | }) | ||
44 | |||
45 | @classmethod | ||
46 | def log_balances(cls, market): | ||
47 | cls.print_log("[Balance]") | ||
48 | for currency, balance in BalanceStore.all.items(): | ||
49 | cls.print_log("\t{}".format(balance)) | ||
50 | |||
51 | cls.add_log({ | ||
52 | "type": "balance", | ||
53 | "balances": BalanceStore.as_json() | ||
54 | }) | ||
55 | |||
56 | @classmethod | ||
57 | def log_tickers(cls, market, amounts, other_currency, | ||
58 | compute_value, type): | ||
59 | values = {} | ||
60 | rates = {} | ||
61 | for currency, amount in amounts.items(): | ||
62 | values[currency] = amount.as_json()["value"] | ||
63 | rates[currency] = amount.rate | ||
64 | cls.add_log({ | ||
65 | "type": "tickers", | ||
66 | "compute_value": compute_value, | ||
67 | "balance_type": type, | ||
68 | "currency": other_currency, | ||
69 | "balances": values, | ||
70 | "rates": rates, | ||
71 | "total": sum(amounts.values()).as_json()["value"] | ||
72 | }) | ||
73 | |||
74 | @classmethod | ||
75 | def log_dispatch(cls, amount, amounts, liquidity, repartition): | ||
76 | cls.add_log({ | ||
77 | "type": "dispatch", | ||
78 | "liquidity": liquidity, | ||
79 | "repartition_ratio": repartition, | ||
80 | "total_amount": amount.as_json(), | ||
81 | "repartition": { k: v.as_json()["value"] for k, v in amounts.items() } | ||
82 | }) | ||
83 | |||
84 | @classmethod | ||
85 | def log_trades(cls, matching_and_trades, only, debug): | ||
86 | trades = [] | ||
87 | for matching, trade in matching_and_trades: | ||
88 | trade_json = trade.as_json() | ||
89 | trade_json["skipped"] = not matching | ||
90 | trades.append(trade_json) | ||
91 | |||
92 | cls.add_log({ | ||
93 | "type": "trades", | ||
94 | "only": only, | ||
95 | "debug": debug, | ||
96 | "trades": trades | ||
97 | }) | ||
98 | |||
99 | @classmethod | ||
100 | def log_orders(cls, orders, tick=None, only=None, compute_value=None): | ||
101 | cls.print_log("[Orders]") | ||
102 | TradeStore.print_all_with_order(ind="\t") | ||
103 | cls.add_log({ | ||
104 | "type": "orders", | ||
105 | "only": only, | ||
106 | "compute_value": compute_value, | ||
107 | "tick": tick, | ||
108 | "orders": [order.as_json() for order in orders if order is not None] | ||
109 | }) | ||
110 | |||
111 | @classmethod | ||
112 | def log_order(cls, order, tick, finished=False, update=None, | ||
113 | new_order=None, compute_value=None): | ||
114 | if finished: | ||
115 | cls.print_log("[Order] Finished {}".format(order)) | ||
116 | elif update == "waiting": | ||
117 | cls.print_log("[Order] {}, tick {}, waiting".format(order, tick)) | ||
118 | elif update == "adjusting": | ||
119 | cls.print_log("[Order] {}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order)) | ||
120 | elif update == "market_fallback": | ||
121 | cls.print_log("[Order] {}, tick {}, fallbacking to market value".format(order, tick)) | ||
122 | elif update == "market_adjust": | ||
123 | cls.print_log("[Order] {}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order)) | ||
124 | |||
125 | cls.add_log({ | ||
126 | "type": "order", | ||
127 | "tick": tick, | ||
128 | "update": update, | ||
129 | "order": order.as_json(), | ||
130 | "compute_value": compute_value, | ||
131 | "new_order": new_order.as_json() if new_order is not None else None | ||
132 | }) | ||
133 | |||
134 | @classmethod | ||
135 | def log_move_balances(cls, needed, moving, debug): | ||
136 | cls.add_log({ | ||
137 | "type": "move_balances", | ||
138 | "debug": debug, | ||
139 | "needed": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in needed.items() }, | ||
140 | "moving": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in moving.items() }, | ||
141 | }) | ||
142 | |||
143 | @classmethod | ||
144 | def log_http_request(cls, method, url, body, headers, response): | ||
145 | cls.add_log({ | ||
146 | "type": "http_request", | ||
147 | "method": method, | ||
148 | "url": url, | ||
149 | "body": body, | ||
150 | "headers": headers, | ||
151 | "status": response.status_code, | ||
152 | "response": response.text | ||
153 | }) | ||
154 | |||
155 | @classmethod | ||
156 | def log_error(cls, action, message=None, exception=None): | ||
157 | cls.print_log("[Error] {}".format(action)) | ||
158 | if exception is not None: | ||
159 | cls.print_log(str("\t{}: {}".format(exception.__class__.__name__, exception))) | ||
160 | if message is not None: | ||
161 | cls.print_log("\t{}".format(message)) | ||
162 | |||
163 | cls.add_log({ | ||
164 | "type": "error", | ||
165 | "action": action, | ||
166 | "exception_class": exception.__class__.__name__ if exception is not None else None, | ||
167 | "exception_message": str(exception) if exception is not None else None, | ||
168 | "message": message, | ||
169 | }) | ||
170 | |||
171 | @classmethod | ||
172 | def log_debug_action(cls, action): | ||
173 | cls.print_log("[Debug] {}".format(action)) | ||
174 | |||
175 | cls.add_log({ | ||
176 | "type": "debug_action", | ||
177 | "action": action, | ||
178 | }) | ||
4 | 179 | ||
5 | class BalanceStore: | 180 | class BalanceStore: |
6 | all = {} | 181 | all = {} |
@@ -16,6 +191,8 @@ class BalanceStore: | |||
16 | other_currency_amount = getattr(balance, type)\ | 191 | other_currency_amount = getattr(balance, type)\ |
17 | .in_currency(other_currency, market, compute_value=compute_value) | 192 | .in_currency(other_currency, market, compute_value=compute_value) |
18 | amounts[currency] = other_currency_amount | 193 | amounts[currency] = other_currency_amount |
194 | ReportStore.log_tickers(market, amounts, other_currency, | ||
195 | compute_value, type) | ||
19 | return amounts | 196 | return amounts |
20 | 197 | ||
21 | @classmethod | 198 | @classmethod |
@@ -25,6 +202,7 @@ class BalanceStore: | |||
25 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ | 202 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ |
26 | currency in cls.all: | 203 | currency in cls.all: |
27 | cls.all[currency] = portfolio.Balance(currency, balance) | 204 | cls.all[currency] = portfolio.Balance(currency, balance) |
205 | ReportStore.log_balances(market) | ||
28 | 206 | ||
29 | @classmethod | 207 | @classmethod |
30 | def dispatch_assets(cls, amount, liquidity="medium", repartition=None): | 208 | def dispatch_assets(cls, amount, liquidity="medium", repartition=None): |
@@ -38,14 +216,20 @@ class BalanceStore: | |||
38 | amounts[currency] = - amounts[currency] | 216 | amounts[currency] = - amounts[currency] |
39 | if currency not in BalanceStore.all: | 217 | if currency not in BalanceStore.all: |
40 | cls.all[currency] = portfolio.Balance(currency, {}) | 218 | cls.all[currency] = portfolio.Balance(currency, {}) |
219 | ReportStore.log_dispatch(amount, amounts, liquidity, repartition) | ||
41 | return amounts | 220 | return amounts |
42 | 221 | ||
222 | @classmethod | ||
223 | def as_json(cls): | ||
224 | return { k: v.as_json() for k, v in cls.all.items() } | ||
225 | |||
43 | class TradeStore: | 226 | class TradeStore: |
44 | all = [] | 227 | all = [] |
45 | debug = False | 228 | debug = False |
46 | 229 | ||
47 | @classmethod | 230 | @classmethod |
48 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): | 231 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): |
232 | computed_trades = [] | ||
49 | cls.debug = cls.debug or debug | 233 | cls.debug = cls.debug or debug |
50 | base_currency = sum(values_in_base.values()).currency | 234 | base_currency = sum(values_in_base.values()).currency |
51 | for currency in BalanceStore.currencies(): | 235 | for currency in BalanceStore.currencies(): |
@@ -55,41 +239,49 @@ class TradeStore: | |||
55 | value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) | 239 | value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) |
56 | 240 | ||
57 | if value_from.value * value_to.value < 0: | 241 | if value_from.value * value_to.value < 0: |
58 | cls.add_trade_if_matching( | 242 | computed_trades.append(cls.trade_if_matching( |
59 | value_from, portfolio.Amount(base_currency, 0), | 243 | value_from, portfolio.Amount(base_currency, 0), |
60 | currency, only=only, market=market) | 244 | currency, only=only, market=market)) |
61 | cls.add_trade_if_matching( | 245 | computed_trades.append(cls.trade_if_matching( |
62 | portfolio.Amount(base_currency, 0), value_to, | 246 | portfolio.Amount(base_currency, 0), value_to, |
63 | currency, only=only, market=market) | 247 | currency, only=only, market=market)) |
64 | else: | 248 | else: |
65 | cls.add_trade_if_matching(value_from, value_to, | 249 | computed_trades.append(cls.trade_if_matching( |
66 | currency, only=only, market=market) | 250 | value_from, value_to, |
251 | currency, only=only, market=market)) | ||
252 | for matching, trade in computed_trades: | ||
253 | if matching: | ||
254 | cls.all.append(trade) | ||
255 | ReportStore.log_trades(computed_trades, only, cls.debug) | ||
67 | 256 | ||
68 | @classmethod | 257 | @classmethod |
69 | def add_trade_if_matching(cls, value_from, value_to, currency, | 258 | def trade_if_matching(cls, value_from, value_to, currency, |
70 | only=None, market=None): | 259 | only=None, market=None): |
71 | trade = portfolio.Trade(value_from, value_to, currency, | 260 | trade = portfolio.Trade(value_from, value_to, currency, |
72 | market=market) | 261 | market=market) |
73 | if only is None or trade.action == only: | 262 | matching = only is None or trade.action == only |
74 | cls.all.append(trade) | 263 | return [matching, trade] |
75 | return True | ||
76 | return False | ||
77 | 264 | ||
78 | @classmethod | 265 | @classmethod |
79 | def prepare_orders(cls, only=None, compute_value="default"): | 266 | def prepare_orders(cls, only=None, compute_value="default"): |
267 | orders = [] | ||
80 | for trade in cls.all: | 268 | for trade in cls.all: |
81 | if only is None or trade.action == only: | 269 | if only is None or trade.action == only: |
82 | trade.prepare_order(compute_value=compute_value) | 270 | orders.append(trade.prepare_order(compute_value=compute_value)) |
271 | ReportStore.log_orders(orders, only, compute_value) | ||
83 | 272 | ||
84 | @classmethod | 273 | @classmethod |
85 | def print_all_with_order(cls): | 274 | def print_all_with_order(cls, ind=""): |
86 | for trade in cls.all: | 275 | for trade in cls.all: |
87 | trade.print_with_order() | 276 | trade.print_with_order(ind=ind) |
88 | 277 | ||
89 | @classmethod | 278 | @classmethod |
90 | def run_orders(cls): | 279 | def run_orders(cls): |
91 | for order in cls.all_orders(state="pending"): | 280 | orders = cls.all_orders(state="pending") |
281 | for order in orders: | ||
92 | order.run() | 282 | order.run() |
283 | ReportStore.log_stage("run_orders") | ||
284 | ReportStore.log_orders(orders) | ||
93 | 285 | ||
94 | @classmethod | 286 | @classmethod |
95 | def all_orders(cls, state=None): | 287 | def all_orders(cls, state=None): |
@@ -27,6 +27,8 @@ class WebMockTestCase(unittest.TestCase): | |||
27 | self.wm.start() | 27 | self.wm.start() |
28 | 28 | ||
29 | self.patchers = [ | 29 | self.patchers = [ |
30 | mock.patch.multiple(portfolio.ReportStore, | ||
31 | logs=[], verbose_print=True), | ||
30 | mock.patch.multiple(portfolio.BalanceStore, | 32 | mock.patch.multiple(portfolio.BalanceStore, |
31 | all={},), | 33 | all={},), |
32 | mock.patch.multiple(portfolio.TradeStore, | 34 | mock.patch.multiple(portfolio.TradeStore, |
@@ -63,7 +65,8 @@ class PortfolioTest(WebMockTestCase): | |||
63 | 65 | ||
64 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) | 66 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) |
65 | 67 | ||
66 | def test_get_cryptoportfolio(self): | 68 | @mock.patch("portfolio.ReportStore") |
69 | def test_get_cryptoportfolio(self, report_store): | ||
67 | self.wm.get(portfolio.Portfolio.URL, [ | 70 | self.wm.get(portfolio.Portfolio.URL, [ |
68 | {"text":'{ "foo": "bar" }', "status_code": 200}, | 71 | {"text":'{ "foo": "bar" }', "status_code": 200}, |
69 | {"text": "System Error", "status_code": 500}, | 72 | {"text": "System Error", "status_code": 500}, |
@@ -74,17 +77,28 @@ class PortfolioTest(WebMockTestCase): | |||
74 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) | 77 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) |
75 | self.assertTrue(self.wm.called) | 78 | self.assertTrue(self.wm.called) |
76 | self.assertEqual(1, self.wm.call_count) | 79 | self.assertEqual(1, self.wm.call_count) |
80 | report_store.log_error.assert_not_called() | ||
81 | report_store.log_http_request.assert_called_once() | ||
82 | report_store.log_http_request.reset_mock() | ||
77 | 83 | ||
78 | portfolio.Portfolio.get_cryptoportfolio() | 84 | portfolio.Portfolio.get_cryptoportfolio() |
79 | self.assertIsNone(portfolio.Portfolio.data) | 85 | self.assertIsNone(portfolio.Portfolio.data) |
80 | self.assertEqual(2, self.wm.call_count) | 86 | self.assertEqual(2, self.wm.call_count) |
87 | report_store.log_error.assert_not_called() | ||
88 | report_store.log_http_request.assert_called_once() | ||
89 | report_store.log_http_request.reset_mock() | ||
90 | |||
81 | 91 | ||
82 | portfolio.Portfolio.data = "Foo" | 92 | portfolio.Portfolio.data = "Foo" |
83 | portfolio.Portfolio.get_cryptoportfolio() | 93 | portfolio.Portfolio.get_cryptoportfolio() |
84 | self.assertEqual("Foo", portfolio.Portfolio.data) | 94 | self.assertEqual("Foo", portfolio.Portfolio.data) |
85 | self.assertEqual(3, self.wm.call_count) | 95 | self.assertEqual(3, self.wm.call_count) |
96 | report_store.log_error.assert_called_once_with("get_cryptoportfolio", | ||
97 | exception=mock.ANY) | ||
98 | report_store.log_http_request.assert_not_called() | ||
86 | 99 | ||
87 | def test_parse_cryptoportfolio(self): | 100 | @mock.patch("portfolio.ReportStore") |
101 | def test_parse_cryptoportfolio(self, report_store): | ||
88 | portfolio.Portfolio.parse_cryptoportfolio() | 102 | portfolio.Portfolio.parse_cryptoportfolio() |
89 | 103 | ||
90 | self.assertListEqual( | 104 | self.assertListEqual( |
@@ -121,15 +135,22 @@ class PortfolioTest(WebMockTestCase): | |||
121 | self.assertDictEqual(expected, liquidities["medium"][date]) | 135 | self.assertDictEqual(expected, liquidities["medium"][date]) |
122 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) | 136 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) |
123 | 137 | ||
138 | report_store.log_http_request.assert_called_once_with("GET", | ||
139 | portfolio.Portfolio.URL, None, mock.ANY, mock.ANY) | ||
140 | report_store.log_http_request.reset_mock() | ||
141 | |||
124 | # It doesn't refetch the data when available | 142 | # It doesn't refetch the data when available |
125 | portfolio.Portfolio.parse_cryptoportfolio() | 143 | portfolio.Portfolio.parse_cryptoportfolio() |
144 | report_store.log_http_request.assert_not_called() | ||
126 | 145 | ||
127 | self.assertEqual(1, self.wm.call_count) | 146 | self.assertEqual(1, self.wm.call_count) |
128 | 147 | ||
129 | portfolio.Portfolio.parse_cryptoportfolio(refetch=True) | 148 | portfolio.Portfolio.parse_cryptoportfolio(refetch=True) |
130 | self.assertEqual(2, self.wm.call_count) | 149 | self.assertEqual(2, self.wm.call_count) |
150 | report_store.log_http_request.assert_called_once() | ||
131 | 151 | ||
132 | def test_repartition(self): | 152 | @mock.patch("portfolio.ReportStore") |
153 | def test_repartition(self, report_store): | ||
133 | expected_medium = { | 154 | expected_medium = { |
134 | 'BTC': (D("1.1102e-16"), "long"), | 155 | 'BTC': (D("1.1102e-16"), "long"), |
135 | 'USDT': (D("0.1"), "long"), | 156 | 'USDT': (D("0.1"), "long"), |
@@ -163,6 +184,8 @@ class PortfolioTest(WebMockTestCase): | |||
163 | 184 | ||
164 | portfolio.Portfolio.repartition(refetch=True) | 185 | portfolio.Portfolio.repartition(refetch=True) |
165 | self.assertEqual(2, self.wm.call_count) | 186 | self.assertEqual(2, self.wm.call_count) |
187 | report_store.log_http_request.assert_called() | ||
188 | self.assertEqual(2, report_store.log_http_request.call_count) | ||
166 | 189 | ||
167 | @mock.patch.object(portfolio.time, "sleep") | 190 | @mock.patch.object(portfolio.time, "sleep") |
168 | @mock.patch.object(portfolio.Portfolio, "repartition") | 191 | @mock.patch.object(portfolio.Portfolio, "repartition") |
@@ -434,6 +457,17 @@ class AmountTest(WebMockTestCase): | |||
434 | amount2.linked_to = amount3 | 457 | amount2.linked_to = amount3 |
435 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) | 458 | self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) |
436 | 459 | ||
460 | def test_as_json(self): | ||
461 | amount = portfolio.Amount("BTX", 32) | ||
462 | self.assertEqual({"currency": "BTX", "value": D("32")}, amount.as_json()) | ||
463 | |||
464 | amount = portfolio.Amount("BTX", "1E-10") | ||
465 | self.assertEqual({"currency": "BTX", "value": D("0")}, amount.as_json()) | ||
466 | |||
467 | amount = portfolio.Amount("BTX", "1E-5") | ||
468 | self.assertEqual({"currency": "BTX", "value": D("0.00001")}, amount.as_json()) | ||
469 | self.assertEqual("0.00001", str(amount.as_json()["value"])) | ||
470 | |||
437 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 471 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
438 | class BalanceTest(WebMockTestCase): | 472 | class BalanceTest(WebMockTestCase): |
439 | def test_values(self): | 473 | def test_values(self): |
@@ -497,6 +531,18 @@ class BalanceTest(WebMockTestCase): | |||
497 | "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2}) | 531 | "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2}) |
498 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance)) | 532 | self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance)) |
499 | 533 | ||
534 | def test_as_json(self): | ||
535 | balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 }) | ||
536 | as_json = balance.as_json() | ||
537 | self.assertEqual(set(portfolio.Balance.base_keys), set(as_json.keys())) | ||
538 | self.assertEqual(D(0), as_json["total"]) | ||
539 | self.assertEqual(D(2), as_json["exchange_total"]) | ||
540 | self.assertEqual(D(2), as_json["exchange_free"]) | ||
541 | self.assertEqual(D(0), as_json["exchange_used"]) | ||
542 | self.assertEqual(D(0), as_json["margin_total"]) | ||
543 | self.assertEqual(D(0), as_json["margin_free"]) | ||
544 | self.assertEqual(D(0), as_json["margin_borrowed"]) | ||
545 | |||
500 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 546 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
501 | class HelperTest(WebMockTestCase): | 547 | class HelperTest(WebMockTestCase): |
502 | def test_get_ticker(self): | 548 | def test_get_ticker(self): |
@@ -575,7 +621,9 @@ class HelperTest(WebMockTestCase): | |||
575 | @mock.patch.object(portfolio.Portfolio, "repartition") | 621 | @mock.patch.object(portfolio.Portfolio, "repartition") |
576 | @mock.patch.object(helper, "get_ticker") | 622 | @mock.patch.object(helper, "get_ticker") |
577 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 623 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
578 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 624 | @mock.patch("store.ReportStore") |
625 | @mock.patch("helper.ReportStore") | ||
626 | def test_prepare_trades(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
579 | repartition.return_value = { | 627 | repartition.return_value = { |
580 | "XEM": (D("0.75"), "long"), | 628 | "XEM": (D("0.75"), "long"), |
581 | "BTC": (D("0.25"), "long"), | 629 | "BTC": (D("0.25"), "long"), |
@@ -614,11 +662,15 @@ class HelperTest(WebMockTestCase): | |||
614 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 662 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
615 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) | 663 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) |
616 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 664 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
665 | report_store_h.log_stage.assert_called_once_with("prepare_trades") | ||
666 | report_store.log_balances.assert_called_once_with(market) | ||
617 | 667 | ||
618 | @mock.patch.object(portfolio.Portfolio, "repartition") | 668 | @mock.patch.object(portfolio.Portfolio, "repartition") |
619 | @mock.patch.object(helper, "get_ticker") | 669 | @mock.patch.object(helper, "get_ticker") |
620 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 670 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
621 | def test_update_trades(self, compute_trades, get_ticker, repartition): | 671 | @mock.patch("store.ReportStore") |
672 | @mock.patch("helper.ReportStore") | ||
673 | def test_update_trades(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
622 | repartition.return_value = { | 674 | repartition.return_value = { |
623 | "XEM": (D("0.75"), "long"), | 675 | "XEM": (D("0.75"), "long"), |
624 | "BTC": (D("0.25"), "long"), | 676 | "BTC": (D("0.25"), "long"), |
@@ -657,11 +709,15 @@ class HelperTest(WebMockTestCase): | |||
657 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 709 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
658 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) | 710 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) |
659 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 711 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
712 | report_store_h.log_stage.assert_called_once_with("update_trades") | ||
713 | report_store.log_balances.assert_called_once_with(market) | ||
660 | 714 | ||
661 | @mock.patch.object(portfolio.Portfolio, "repartition") | 715 | @mock.patch.object(portfolio.Portfolio, "repartition") |
662 | @mock.patch.object(helper, "get_ticker") | 716 | @mock.patch.object(helper, "get_ticker") |
663 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 717 | @mock.patch.object(portfolio.TradeStore, "compute_trades") |
664 | def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition): | 718 | @mock.patch("store.ReportStore") |
719 | @mock.patch("helper.ReportStore") | ||
720 | def test_prepare_trades_to_sell_all(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
665 | def _get_ticker(c1, c2, market): | 721 | def _get_ticker(c1, c2, market): |
666 | if c1 == "USDT" and c2 == "BTC": | 722 | if c1 == "USDT" and c2 == "BTC": |
667 | return { "average": D("0.0001") } | 723 | return { "average": D("0.0001") } |
@@ -694,16 +750,17 @@ class HelperTest(WebMockTestCase): | |||
694 | self.assertEqual(1, call[0][0]["USDT"].value) | 750 | self.assertEqual(1, call[0][0]["USDT"].value) |
695 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 751 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
696 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) | 752 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) |
753 | report_store_h.log_stage.assert_called_once_with("prepare_trades_to_sell_all") | ||
754 | report_store.log_balances.assert_called_once_with(market) | ||
697 | 755 | ||
698 | @mock.patch.object(portfolio.time, "sleep") | 756 | @mock.patch.object(portfolio.time, "sleep") |
699 | @mock.patch.object(portfolio.TradeStore, "all_orders") | 757 | @mock.patch.object(portfolio.TradeStore, "all_orders") |
700 | def test_follow_orders(self, all_orders, time_mock): | 758 | def test_follow_orders(self, all_orders, time_mock): |
701 | for verbose, debug, sleep in [ | 759 | for debug, sleep in [ |
702 | (True, False, None), (False, False, None), | 760 | (False, None), (True, None), |
703 | (True, True, None), (True, False, 12), | 761 | (False, 12), (True, 12)]: |
704 | (True, True, 12)]: | 762 | with self.subTest(sleep=sleep, debug=debug), \ |
705 | with self.subTest(sleep=sleep, debug=debug, verbose=verbose), \ | 763 | mock.patch("helper.ReportStore") as report_store: |
706 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
707 | portfolio.TradeStore.debug = debug | 764 | portfolio.TradeStore.debug = debug |
708 | order_mock1 = mock.Mock() | 765 | order_mock1 = mock.Mock() |
709 | order_mock2 = mock.Mock() | 766 | order_mock2 = mock.Mock() |
@@ -729,7 +786,7 @@ class HelperTest(WebMockTestCase): | |||
729 | order_mock2.trade = mock.Mock() | 786 | order_mock2.trade = mock.Mock() |
730 | order_mock3.trade = mock.Mock() | 787 | order_mock3.trade = mock.Mock() |
731 | 788 | ||
732 | helper.follow_orders(verbose=verbose, sleep=sleep) | 789 | helper.follow_orders(sleep=sleep) |
733 | 790 | ||
734 | order_mock1.trade.update_order.assert_any_call(order_mock1, 1) | 791 | order_mock1.trade.update_order.assert_any_call(order_mock1, 1) |
735 | order_mock1.trade.update_order.assert_any_call(order_mock1, 2) | 792 | order_mock1.trade.update_order.assert_any_call(order_mock1, 2) |
@@ -743,25 +800,43 @@ class HelperTest(WebMockTestCase): | |||
743 | order_mock3.trade.update_order.assert_any_call(order_mock3, 2) | 800 | order_mock3.trade.update_order.assert_any_call(order_mock3, 2) |
744 | self.assertEqual(1, order_mock3.trade.update_order.call_count) | 801 | self.assertEqual(1, order_mock3.trade.update_order.call_count) |
745 | self.assertEqual(2, order_mock3.get_status.call_count) | 802 | self.assertEqual(2, order_mock3.get_status.call_count) |
803 | report_store.log_stage.assert_called() | ||
804 | calls = [ | ||
805 | mock.call("follow_orders_begin"), | ||
806 | mock.call("follow_orders_tick_1"), | ||
807 | mock.call("follow_orders_tick_2"), | ||
808 | mock.call("follow_orders_tick_3"), | ||
809 | mock.call("follow_orders_end"), | ||
810 | ] | ||
811 | report_store.log_stage.assert_has_calls(calls) | ||
812 | report_store.log_orders.assert_called() | ||
813 | self.assertEqual(3, report_store.log_orders.call_count) | ||
814 | calls = [ | ||
815 | mock.call([order_mock1, order_mock2], tick=1), | ||
816 | mock.call([order_mock1, order_mock3], tick=2), | ||
817 | mock.call([order_mock1, order_mock3], tick=3), | ||
818 | ] | ||
819 | report_store.log_orders.assert_has_calls(calls) | ||
820 | calls = [ | ||
821 | mock.call(order_mock1, 3, finished=True), | ||
822 | mock.call(order_mock3, 3, finished=True), | ||
823 | ] | ||
824 | report_store.log_order.assert_has_calls(calls) | ||
746 | 825 | ||
747 | if sleep is None: | 826 | if sleep is None: |
748 | if debug: | 827 | if debug: |
828 | report_store.log_debug_action.assert_called_with("Set follow_orders tick to 7s") | ||
749 | time_mock.assert_called_with(7) | 829 | time_mock.assert_called_with(7) |
750 | else: | 830 | else: |
751 | time_mock.assert_called_with(30) | 831 | time_mock.assert_called_with(30) |
752 | else: | 832 | else: |
753 | time_mock.assert_called_with(sleep) | 833 | time_mock.assert_called_with(sleep) |
754 | 834 | ||
755 | if verbose: | ||
756 | self.assertNotEqual("", stdout_mock.getvalue()) | ||
757 | else: | ||
758 | self.assertEqual("", stdout_mock.getvalue()) | ||
759 | |||
760 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | 835 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") |
761 | def test_move_balance(self, fetch_balances): | 836 | def test_move_balance(self, fetch_balances): |
762 | for debug in [True, False]: | 837 | for debug in [True, False]: |
763 | with self.subTest(debug=debug),\ | 838 | with self.subTest(debug=debug),\ |
764 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 839 | mock.patch("helper.ReportStore") as report_store: |
765 | value_from = portfolio.Amount("BTC", "1.0") | 840 | value_from = portfolio.Amount("BTC", "1.0") |
766 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 841 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
767 | value_to = portfolio.Amount("BTC", "10.0") | 842 | value_to = portfolio.Amount("BTC", "10.0") |
@@ -788,8 +863,11 @@ class HelperTest(WebMockTestCase): | |||
788 | helper.move_balances(market, debug=debug) | 863 | helper.move_balances(market, debug=debug) |
789 | 864 | ||
790 | fetch_balances.assert_called_with(market) | 865 | fetch_balances.assert_called_with(market) |
866 | report_store.log_move_balances.assert_called_once() | ||
867 | |||
791 | if debug: | 868 | if debug: |
792 | self.assertRegex(stdout_mock.getvalue(), "market.transfer_balance") | 869 | report_store.log_debug_action.assert_called() |
870 | self.assertEqual(3, report_store.log_debug_action.call_count) | ||
793 | else: | 871 | else: |
794 | market.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 872 | market.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
795 | market.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange") | 873 | market.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange") |
@@ -797,9 +875,8 @@ class HelperTest(WebMockTestCase): | |||
797 | 875 | ||
798 | @mock.patch.object(helper, "prepare_trades") | 876 | @mock.patch.object(helper, "prepare_trades") |
799 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | 877 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") |
800 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | 878 | @mock.patch.object(portfolio.ReportStore, "log_stage") |
801 | @mock.patch('sys.stdout', new_callable=StringIO) | 879 | def test_print_orders(self, log_stage, prepare_orders, prepare_trades): |
802 | def test_print_orders(self, stdout_mock, print_all_with_order, prepare_orders, prepare_trades): | ||
803 | market = mock.Mock() | 880 | market = mock.Mock() |
804 | portfolio.BalanceStore.all = { | 881 | portfolio.BalanceStore.all = { |
805 | "BTC": portfolio.Balance("BTC", { | 882 | "BTC": portfolio.Balance("BTC", { |
@@ -817,13 +894,12 @@ class HelperTest(WebMockTestCase): | |||
817 | prepare_trades.assert_called_with(market, base_currency="BTC", | 894 | prepare_trades.assert_called_with(market, base_currency="BTC", |
818 | compute_value="average", debug=True) | 895 | compute_value="average", debug=True) |
819 | prepare_orders.assert_called_with(compute_value="average") | 896 | prepare_orders.assert_called_with(compute_value="average") |
820 | print_all_with_order.assert_called() | 897 | log_stage.assert_called_with("print_orders") |
821 | self.assertRegex(stdout_mock.getvalue(), "Balance") | ||
822 | 898 | ||
823 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | 899 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") |
824 | @mock.patch.object(portfolio.BalanceStore, "in_currency") | 900 | @mock.patch.object(portfolio.BalanceStore, "in_currency") |
825 | @mock.patch('sys.stdout', new_callable=StringIO) | 901 | @mock.patch.object(helper.ReportStore, "print_log") |
826 | def test_print_balances(self, stdout_mock, in_currency, fetch_balances): | 902 | def test_print_balances(self, print_log, in_currency, fetch_balances): |
827 | market = mock.Mock() | 903 | market = mock.Mock() |
828 | portfolio.BalanceStore.all = { | 904 | portfolio.BalanceStore.all = { |
829 | "BTC": portfolio.Balance("BTC", { | 905 | "BTC": portfolio.Balance("BTC", { |
@@ -843,18 +919,18 @@ class HelperTest(WebMockTestCase): | |||
843 | } | 919 | } |
844 | helper.print_balances(market) | 920 | helper.print_balances(market) |
845 | fetch_balances.assert_called_with(market) | 921 | fetch_balances.assert_called_with(market) |
846 | self.assertRegex(stdout_mock.getvalue(), "Balance") | 922 | print_log.assert_has_calls([ |
847 | self.assertRegex(stdout_mock.getvalue(), "0.95000000 BTC") | 923 | mock.call("total:"), |
924 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
925 | ]) | ||
848 | 926 | ||
849 | @mock.patch.object(helper, "prepare_trades") | 927 | @mock.patch.object(helper, "prepare_trades") |
850 | @mock.patch.object(helper, "follow_orders") | 928 | @mock.patch.object(helper, "follow_orders") |
851 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | 929 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") |
852 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | ||
853 | @mock.patch.object(portfolio.TradeStore, "run_orders") | 930 | @mock.patch.object(portfolio.TradeStore, "run_orders") |
854 | @mock.patch('sys.stdout', new_callable=StringIO) | 931 | @mock.patch.object(portfolio.ReportStore, "log_stage") |
855 | def test_process_sell_needed__1_sell(self, stdout_mock, run_orders, | 932 | def test_process_sell_needed__1_sell(self, log_stage, run_orders, |
856 | print_all_with_order, prepare_orders, follow_orders, | 933 | prepare_orders, follow_orders, prepare_trades): |
857 | prepare_trades): | ||
858 | market = mock.Mock() | 934 | market = mock.Mock() |
859 | portfolio.BalanceStore.all = { | 935 | portfolio.BalanceStore.all = { |
860 | "BTC": portfolio.Balance("BTC", { | 936 | "BTC": portfolio.Balance("BTC", { |
@@ -873,20 +949,18 @@ class HelperTest(WebMockTestCase): | |||
873 | liquidity="medium", debug=False) | 949 | liquidity="medium", debug=False) |
874 | prepare_orders.assert_called_with(compute_value="average", | 950 | prepare_orders.assert_called_with(compute_value="average", |
875 | only="dispose") | 951 | only="dispose") |
876 | print_all_with_order.assert_called() | ||
877 | run_orders.assert_called() | 952 | run_orders.assert_called() |
878 | follow_orders.assert_called() | 953 | follow_orders.assert_called() |
879 | self.assertRegex(stdout_mock.getvalue(), "Balance") | 954 | log_stage.assert_called_with("process_sell_needed__1_sell_end") |
880 | 955 | ||
881 | @mock.patch.object(helper, "update_trades") | 956 | @mock.patch.object(helper, "update_trades") |
882 | @mock.patch.object(helper, "follow_orders") | 957 | @mock.patch.object(helper, "follow_orders") |
883 | @mock.patch.object(helper, "move_balances") | 958 | @mock.patch.object(helper, "move_balances") |
884 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | 959 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") |
885 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | ||
886 | @mock.patch.object(portfolio.TradeStore, "run_orders") | 960 | @mock.patch.object(portfolio.TradeStore, "run_orders") |
887 | @mock.patch('sys.stdout', new_callable=StringIO) | 961 | @mock.patch.object(portfolio.ReportStore, "log_stage") |
888 | def test_process_sell_needed__2_buy(self, stdout_mock, run_orders, | 962 | def test_process_sell_needed__2_buy(self, log_stage, run_orders, |
889 | print_all_with_order, prepare_orders, move_balances, | 963 | prepare_orders, move_balances, |
890 | follow_orders, update_trades): | 964 | follow_orders, update_trades): |
891 | market = mock.Mock() | 965 | market = mock.Mock() |
892 | portfolio.BalanceStore.all = { | 966 | portfolio.BalanceStore.all = { |
@@ -906,21 +980,18 @@ class HelperTest(WebMockTestCase): | |||
906 | debug=False, liquidity="medium", only="acquire") | 980 | debug=False, liquidity="medium", only="acquire") |
907 | prepare_orders.assert_called_with(compute_value="average", | 981 | prepare_orders.assert_called_with(compute_value="average", |
908 | only="acquire") | 982 | only="acquire") |
909 | print_all_with_order.assert_called() | ||
910 | move_balances.assert_called_with(market, debug=False) | 983 | move_balances.assert_called_with(market, debug=False) |
911 | run_orders.assert_called() | 984 | run_orders.assert_called() |
912 | follow_orders.assert_called() | 985 | follow_orders.assert_called() |
913 | self.assertRegex(stdout_mock.getvalue(), "Balance") | 986 | log_stage.assert_called_with("process_sell_needed__2_buy_end") |
914 | 987 | ||
915 | @mock.patch.object(helper, "prepare_trades_to_sell_all") | 988 | @mock.patch.object(helper, "prepare_trades_to_sell_all") |
916 | @mock.patch.object(helper, "follow_orders") | 989 | @mock.patch.object(helper, "follow_orders") |
917 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | 990 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") |
918 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | ||
919 | @mock.patch.object(portfolio.TradeStore, "run_orders") | 991 | @mock.patch.object(portfolio.TradeStore, "run_orders") |
920 | @mock.patch('sys.stdout', new_callable=StringIO) | 992 | @mock.patch.object(portfolio.ReportStore, "log_stage") |
921 | def test_process_sell_all__1_sell(self, stdout_mock, run_orders, | 993 | def test_process_sell_all__1_sell(self, log_stage, run_orders, |
922 | print_all_with_order, prepare_orders, follow_orders, | 994 | prepare_orders, follow_orders, prepare_trades_to_sell_all): |
923 | prepare_trades_to_sell_all): | ||
924 | market = mock.Mock() | 995 | market = mock.Mock() |
925 | portfolio.BalanceStore.all = { | 996 | portfolio.BalanceStore.all = { |
926 | "BTC": portfolio.Balance("BTC", { | 997 | "BTC": portfolio.Balance("BTC", { |
@@ -938,21 +1009,19 @@ class HelperTest(WebMockTestCase): | |||
938 | prepare_trades_to_sell_all.assert_called_with(market, base_currency="BTC", | 1009 | prepare_trades_to_sell_all.assert_called_with(market, base_currency="BTC", |
939 | debug=False) | 1010 | debug=False) |
940 | prepare_orders.assert_called_with(compute_value="average") | 1011 | prepare_orders.assert_called_with(compute_value="average") |
941 | print_all_with_order.assert_called() | ||
942 | run_orders.assert_called() | 1012 | run_orders.assert_called() |
943 | follow_orders.assert_called() | 1013 | follow_orders.assert_called() |
944 | self.assertRegex(stdout_mock.getvalue(), "Balance") | 1014 | log_stage.assert_called_with("process_sell_all__1_all_sell_end") |
945 | 1015 | ||
946 | @mock.patch.object(helper, "prepare_trades") | 1016 | @mock.patch.object(helper, "prepare_trades") |
947 | @mock.patch.object(helper, "follow_orders") | 1017 | @mock.patch.object(helper, "follow_orders") |
948 | @mock.patch.object(helper, "move_balances") | 1018 | @mock.patch.object(helper, "move_balances") |
949 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | 1019 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") |
950 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | ||
951 | @mock.patch.object(portfolio.TradeStore, "run_orders") | 1020 | @mock.patch.object(portfolio.TradeStore, "run_orders") |
952 | @mock.patch('sys.stdout', new_callable=StringIO) | 1021 | @mock.patch.object(portfolio.ReportStore, "log_stage") |
953 | def test_process_sell_all__2_all_buy(self, stdout_mock, run_orders, | 1022 | def test_process_sell_all__2_all_buy(self, log_stage, run_orders, |
954 | print_all_with_order, prepare_orders, move_balances, | 1023 | prepare_orders, move_balances, follow_orders, |
955 | follow_orders, prepare_trades): | 1024 | prepare_trades): |
956 | market = mock.Mock() | 1025 | market = mock.Mock() |
957 | portfolio.BalanceStore.all = { | 1026 | portfolio.BalanceStore.all = { |
958 | "BTC": portfolio.Balance("BTC", { | 1027 | "BTC": portfolio.Balance("BTC", { |
@@ -970,18 +1039,18 @@ class HelperTest(WebMockTestCase): | |||
970 | prepare_trades.assert_called_with(market, base_currency="BTC", | 1039 | prepare_trades.assert_called_with(market, base_currency="BTC", |
971 | liquidity="medium", debug=False) | 1040 | liquidity="medium", debug=False) |
972 | prepare_orders.assert_called_with(compute_value="average") | 1041 | prepare_orders.assert_called_with(compute_value="average") |
973 | print_all_with_order.assert_called() | ||
974 | move_balances.assert_called_with(market, debug=False) | 1042 | move_balances.assert_called_with(market, debug=False) |
975 | run_orders.assert_called() | 1043 | run_orders.assert_called() |
976 | follow_orders.assert_called() | 1044 | follow_orders.assert_called() |
977 | self.assertRegex(stdout_mock.getvalue(), "Balance") | 1045 | log_stage.assert_called_with("process_sell_all__2_all_buy_end") |
978 | 1046 | ||
979 | 1047 | ||
980 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1048 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
981 | class TradeStoreTest(WebMockTestCase): | 1049 | class TradeStoreTest(WebMockTestCase): |
982 | @mock.patch.object(portfolio.BalanceStore, "currencies") | 1050 | @mock.patch.object(portfolio.BalanceStore, "currencies") |
983 | @mock.patch.object(portfolio.TradeStore, "add_trade_if_matching") | 1051 | @mock.patch.object(portfolio.TradeStore, "trade_if_matching") |
984 | def test_compute_trades(self, add_trade_if_matching, currencies): | 1052 | @mock.patch.object(portfolio.ReportStore, "log_trades") |
1053 | def test_compute_trades(self, log_trades, trade_if_matching, currencies): | ||
985 | currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] | 1054 | currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] |
986 | 1055 | ||
987 | values_in_base = { | 1056 | values_in_base = { |
@@ -996,96 +1065,89 @@ class TradeStoreTest(WebMockTestCase): | |||
996 | "BTC": portfolio.Amount("BTC", D("0.4")), | 1065 | "BTC": portfolio.Amount("BTC", D("0.4")), |
997 | "ETH": portfolio.Amount("BTC", D("0.3")), | 1066 | "ETH": portfolio.Amount("BTC", D("0.3")), |
998 | } | 1067 | } |
1068 | side_effect = [ | ||
1069 | (True, 1), | ||
1070 | (False, 2), | ||
1071 | (False, 3), | ||
1072 | (True, 4), | ||
1073 | (True, 5) | ||
1074 | ] | ||
1075 | trade_if_matching.side_effect = side_effect | ||
999 | 1076 | ||
1000 | portfolio.TradeStore.compute_trades(values_in_base, | 1077 | portfolio.TradeStore.compute_trades(values_in_base, |
1001 | new_repartition, only="only", market="market") | 1078 | new_repartition, only="only", market="market") |
1002 | 1079 | ||
1003 | self.assertEqual(5, add_trade_if_matching.call_count) | 1080 | self.assertEqual(5, trade_if_matching.call_count) |
1004 | add_trade_if_matching.assert_any_call( | 1081 | self.assertEqual(3, len(portfolio.TradeStore.all)) |
1005 | portfolio.Amount("BTC", D("0.9")), | 1082 | self.assertEqual([1, 4, 5], portfolio.TradeStore.all) |
1006 | portfolio.Amount("BTC", 0), | 1083 | log_trades.assert_called_with(side_effect, "only", False) |
1007 | "XMR", only="only", market="market" | ||
1008 | ) | ||
1009 | add_trade_if_matching.assert_any_call( | ||
1010 | portfolio.Amount("BTC", D("0.4")), | ||
1011 | portfolio.Amount("BTC", D("0.5")), | ||
1012 | "DASH", only="only", market="market" | ||
1013 | ) | ||
1014 | add_trade_if_matching.assert_any_call( | ||
1015 | portfolio.Amount("BTC", D("-0.5")), | ||
1016 | portfolio.Amount("BTC", D("0")), | ||
1017 | "XVG", only="only", market="market" | ||
1018 | ) | ||
1019 | add_trade_if_matching.assert_any_call( | ||
1020 | portfolio.Amount("BTC", D("0")), | ||
1021 | portfolio.Amount("BTC", D("0.1")), | ||
1022 | "XVG", only="only", market="market" | ||
1023 | ) | ||
1024 | add_trade_if_matching.assert_any_call( | ||
1025 | portfolio.Amount("BTC", D("0")), | ||
1026 | portfolio.Amount("BTC", D("0.3")), | ||
1027 | "ETH", only="only", market="market" | ||
1028 | ) | ||
1029 | 1084 | ||
1030 | def test_add_trade_if_matching(self): | 1085 | def test_trade_if_matching(self): |
1031 | result = portfolio.TradeStore.add_trade_if_matching( | 1086 | result = portfolio.TradeStore.trade_if_matching( |
1032 | portfolio.Amount("BTC", D("0")), | 1087 | portfolio.Amount("BTC", D("0")), |
1033 | portfolio.Amount("BTC", D("0.3")), | 1088 | portfolio.Amount("BTC", D("0.3")), |
1034 | "ETH", only="nope", market="market" | 1089 | "ETH", only="nope", market="market" |
1035 | ) | 1090 | ) |
1036 | self.assertEqual(0, len(portfolio.TradeStore.all)) | 1091 | self.assertEqual(False, result[0]) |
1037 | self.assertEqual(False, result) | 1092 | self.assertIsInstance(result[1], portfolio.Trade) |
1038 | 1093 | ||
1039 | portfolio.TradeStore.all = [] | 1094 | portfolio.TradeStore.all = [] |
1040 | result = portfolio.TradeStore.add_trade_if_matching( | 1095 | result = portfolio.TradeStore.trade_if_matching( |
1041 | portfolio.Amount("BTC", D("0")), | 1096 | portfolio.Amount("BTC", D("0")), |
1042 | portfolio.Amount("BTC", D("0.3")), | 1097 | portfolio.Amount("BTC", D("0.3")), |
1043 | "ETH", only=None, market="market" | 1098 | "ETH", only=None, market="market" |
1044 | ) | 1099 | ) |
1045 | self.assertEqual(1, len(portfolio.TradeStore.all)) | 1100 | self.assertEqual(True, result[0]) |
1046 | self.assertEqual(True, result) | ||
1047 | 1101 | ||
1048 | portfolio.TradeStore.all = [] | 1102 | portfolio.TradeStore.all = [] |
1049 | result = portfolio.TradeStore.add_trade_if_matching( | 1103 | result = portfolio.TradeStore.trade_if_matching( |
1050 | portfolio.Amount("BTC", D("0")), | 1104 | portfolio.Amount("BTC", D("0")), |
1051 | portfolio.Amount("BTC", D("0.3")), | 1105 | portfolio.Amount("BTC", D("0.3")), |
1052 | "ETH", only="acquire", market="market" | 1106 | "ETH", only="acquire", market="market" |
1053 | ) | 1107 | ) |
1054 | self.assertEqual(1, len(portfolio.TradeStore.all)) | 1108 | self.assertEqual(True, result[0]) |
1055 | self.assertEqual(True, result) | ||
1056 | 1109 | ||
1057 | portfolio.TradeStore.all = [] | 1110 | portfolio.TradeStore.all = [] |
1058 | result = portfolio.TradeStore.add_trade_if_matching( | 1111 | result = portfolio.TradeStore.trade_if_matching( |
1059 | portfolio.Amount("BTC", D("0")), | 1112 | portfolio.Amount("BTC", D("0")), |
1060 | portfolio.Amount("BTC", D("0.3")), | 1113 | portfolio.Amount("BTC", D("0.3")), |
1061 | "ETH", only="dispose", market="market" | 1114 | "ETH", only="dispose", market="market" |
1062 | ) | 1115 | ) |
1063 | self.assertEqual(0, len(portfolio.TradeStore.all)) | 1116 | self.assertEqual(False, result[0]) |
1064 | self.assertEqual(False, result) | ||
1065 | 1117 | ||
1066 | def test_prepare_orders(self): | 1118 | @mock.patch.object(portfolio.ReportStore, "log_orders") |
1119 | def test_prepare_orders(self, log_orders): | ||
1067 | trade_mock1 = mock.Mock() | 1120 | trade_mock1 = mock.Mock() |
1068 | trade_mock2 = mock.Mock() | 1121 | trade_mock2 = mock.Mock() |
1069 | 1122 | ||
1123 | trade_mock1.prepare_order.return_value = 1 | ||
1124 | trade_mock2.prepare_order.return_value = 2 | ||
1125 | |||
1070 | portfolio.TradeStore.all.append(trade_mock1) | 1126 | portfolio.TradeStore.all.append(trade_mock1) |
1071 | portfolio.TradeStore.all.append(trade_mock2) | 1127 | portfolio.TradeStore.all.append(trade_mock2) |
1072 | 1128 | ||
1073 | portfolio.TradeStore.prepare_orders() | 1129 | portfolio.TradeStore.prepare_orders() |
1074 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | 1130 | trade_mock1.prepare_order.assert_called_with(compute_value="default") |
1075 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | 1131 | trade_mock2.prepare_order.assert_called_with(compute_value="default") |
1132 | log_orders.assert_called_once_with([1, 2], None, "default") | ||
1133 | |||
1134 | log_orders.reset_mock() | ||
1076 | 1135 | ||
1077 | portfolio.TradeStore.prepare_orders(compute_value="bla") | 1136 | portfolio.TradeStore.prepare_orders(compute_value="bla") |
1078 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | 1137 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") |
1079 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | 1138 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") |
1139 | log_orders.assert_called_once_with([1, 2], None, "bla") | ||
1080 | 1140 | ||
1081 | trade_mock1.prepare_order.reset_mock() | 1141 | trade_mock1.prepare_order.reset_mock() |
1082 | trade_mock2.prepare_order.reset_mock() | 1142 | trade_mock2.prepare_order.reset_mock() |
1143 | log_orders.reset_mock() | ||
1083 | 1144 | ||
1084 | trade_mock1.action = "foo" | 1145 | trade_mock1.action = "foo" |
1085 | trade_mock2.action = "bar" | 1146 | trade_mock2.action = "bar" |
1086 | portfolio.TradeStore.prepare_orders(only="bar") | 1147 | portfolio.TradeStore.prepare_orders(only="bar") |
1087 | trade_mock1.prepare_order.assert_not_called() | 1148 | trade_mock1.prepare_order.assert_not_called() |
1088 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | 1149 | trade_mock2.prepare_order.assert_called_with(compute_value="default") |
1150 | log_orders.assert_called_once_with([2], "bar", "default") | ||
1089 | 1151 | ||
1090 | def test_print_all_with_order(self): | 1152 | def test_print_all_with_order(self): |
1091 | trade_mock1 = mock.Mock() | 1153 | trade_mock1 = mock.Mock() |
@@ -1099,8 +1161,10 @@ class TradeStoreTest(WebMockTestCase): | |||
1099 | trade_mock2.print_with_order.assert_called() | 1161 | trade_mock2.print_with_order.assert_called() |
1100 | trade_mock3.print_with_order.assert_called() | 1162 | trade_mock3.print_with_order.assert_called() |
1101 | 1163 | ||
1164 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
1165 | @mock.patch.object(portfolio.ReportStore, "log_orders") | ||
1102 | @mock.patch.object(portfolio.TradeStore, "all_orders") | 1166 | @mock.patch.object(portfolio.TradeStore, "all_orders") |
1103 | def test_run_orders(self, all_orders): | 1167 | def test_run_orders(self, all_orders, log_orders, log_stage): |
1104 | order_mock1 = mock.Mock() | 1168 | order_mock1 = mock.Mock() |
1105 | order_mock2 = mock.Mock() | 1169 | order_mock2 = mock.Mock() |
1106 | order_mock3 = mock.Mock() | 1170 | order_mock3 = mock.Mock() |
@@ -1112,6 +1176,10 @@ class TradeStoreTest(WebMockTestCase): | |||
1112 | order_mock2.run.assert_called() | 1176 | order_mock2.run.assert_called() |
1113 | order_mock3.run.assert_called() | 1177 | order_mock3.run.assert_called() |
1114 | 1178 | ||
1179 | log_stage.assert_called_with("run_orders") | ||
1180 | log_orders.assert_called_with([order_mock1, order_mock2, | ||
1181 | order_mock3]) | ||
1182 | |||
1115 | def test_all_orders(self): | 1183 | def test_all_orders(self): |
1116 | trade_mock1 = mock.Mock() | 1184 | trade_mock1 = mock.Mock() |
1117 | trade_mock2 = mock.Mock() | 1185 | trade_mock2 = mock.Mock() |
@@ -1150,6 +1218,7 @@ class TradeStoreTest(WebMockTestCase): | |||
1150 | order_mock2.get_status.assert_called() | 1218 | order_mock2.get_status.assert_called() |
1151 | order_mock3.get_status.assert_called() | 1219 | order_mock3.get_status.assert_called() |
1152 | 1220 | ||
1221 | |||
1153 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1222 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
1154 | class BalanceStoreTest(WebMockTestCase): | 1223 | class BalanceStoreTest(WebMockTestCase): |
1155 | def setUp(self): | 1224 | def setUp(self): |
@@ -1184,7 +1253,8 @@ class BalanceStoreTest(WebMockTestCase): | |||
1184 | } | 1253 | } |
1185 | 1254 | ||
1186 | @mock.patch.object(helper, "get_ticker") | 1255 | @mock.patch.object(helper, "get_ticker") |
1187 | def test_in_currency(self, get_ticker): | 1256 | @mock.patch("portfolio.ReportStore.log_tickers") |
1257 | def test_in_currency(self, log_tickers, get_ticker): | ||
1188 | portfolio.BalanceStore.all = { | 1258 | portfolio.BalanceStore.all = { |
1189 | "BTC": portfolio.Balance("BTC", { | 1259 | "BTC": portfolio.Balance("BTC", { |
1190 | "total": "0.65", | 1260 | "total": "0.65", |
@@ -1208,16 +1278,26 @@ class BalanceStoreTest(WebMockTestCase): | |||
1208 | self.assertEqual("BTC", amounts["ETH"].currency) | 1278 | self.assertEqual("BTC", amounts["ETH"].currency) |
1209 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 1279 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
1210 | self.assertEqual(D("0.30"), amounts["ETH"].value) | 1280 | self.assertEqual(D("0.30"), amounts["ETH"].value) |
1281 | log_tickers.assert_called_once_with(market, amounts, "BTC", | ||
1282 | "average", "total") | ||
1283 | log_tickers.reset_mock() | ||
1211 | 1284 | ||
1212 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid") | 1285 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid") |
1213 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 1286 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
1214 | self.assertEqual(D("0.27"), amounts["ETH"].value) | 1287 | self.assertEqual(D("0.27"), amounts["ETH"].value) |
1288 | log_tickers.assert_called_once_with(market, amounts, "BTC", | ||
1289 | "bid", "total") | ||
1290 | log_tickers.reset_mock() | ||
1215 | 1291 | ||
1216 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid", type="exchange_used") | 1292 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid", type="exchange_used") |
1217 | self.assertEqual(D("0.30"), amounts["BTC"].value) | 1293 | self.assertEqual(D("0.30"), amounts["BTC"].value) |
1218 | self.assertEqual(0, amounts["ETH"].value) | 1294 | self.assertEqual(0, amounts["ETH"].value) |
1295 | log_tickers.assert_called_once_with(market, amounts, "BTC", | ||
1296 | "bid", "exchange_used") | ||
1297 | log_tickers.reset_mock() | ||
1219 | 1298 | ||
1220 | def test_fetch_balances(self): | 1299 | @mock.patch.object(portfolio.ReportStore, "log_balances") |
1300 | def test_fetch_balances(self, log_balances): | ||
1221 | market = mock.Mock() | 1301 | market = mock.Mock() |
1222 | market.fetch_all_balances.return_value = self.fetch_balance | 1302 | market.fetch_all_balances.return_value = self.fetch_balance |
1223 | 1303 | ||
@@ -1231,20 +1311,24 @@ class BalanceStoreTest(WebMockTestCase): | |||
1231 | portfolio.BalanceStore.fetch_balances(market) | 1311 | portfolio.BalanceStore.fetch_balances(market) |
1232 | self.assertEqual(0, portfolio.BalanceStore.all["ETC"].total) | 1312 | self.assertEqual(0, portfolio.BalanceStore.all["ETC"].total) |
1233 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.BalanceStore.currencies())) | 1313 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.BalanceStore.currencies())) |
1314 | log_balances.assert_called_with(market) | ||
1234 | 1315 | ||
1235 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1316 | @mock.patch.object(portfolio.Portfolio, "repartition") |
1236 | def test_dispatch_assets(self, repartition): | 1317 | @mock.patch.object(portfolio.ReportStore, "log_balances") |
1318 | @mock.patch("store.ReportStore.log_dispatch") | ||
1319 | def test_dispatch_assets(self, log_dispatch, log_balances, repartition): | ||
1237 | market = mock.Mock() | 1320 | market = mock.Mock() |
1238 | market.fetch_all_balances.return_value = self.fetch_balance | 1321 | market.fetch_all_balances.return_value = self.fetch_balance |
1239 | portfolio.BalanceStore.fetch_balances(market) | 1322 | portfolio.BalanceStore.fetch_balances(market) |
1240 | 1323 | ||
1241 | self.assertNotIn("XEM", portfolio.BalanceStore.currencies()) | 1324 | self.assertNotIn("XEM", portfolio.BalanceStore.currencies()) |
1242 | 1325 | ||
1243 | repartition.return_value = { | 1326 | repartition_hash = { |
1244 | "XEM": (D("0.75"), "long"), | 1327 | "XEM": (D("0.75"), "long"), |
1245 | "BTC": (D("0.26"), "long"), | 1328 | "BTC": (D("0.26"), "long"), |
1246 | "DASH": (D("0.10"), "short"), | 1329 | "DASH": (D("0.10"), "short"), |
1247 | } | 1330 | } |
1331 | repartition.return_value = repartition_hash | ||
1248 | 1332 | ||
1249 | amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1")) | 1333 | amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1")) |
1250 | repartition.assert_called_with(liquidity="medium") | 1334 | repartition.assert_called_with(liquidity="medium") |
@@ -1252,6 +1336,9 @@ class BalanceStoreTest(WebMockTestCase): | |||
1252 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 1336 | self.assertEqual(D("2.6"), amounts["BTC"].value) |
1253 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 1337 | self.assertEqual(D("7.5"), amounts["XEM"].value) |
1254 | self.assertEqual(D("-1.0"), amounts["DASH"].value) | 1338 | self.assertEqual(D("-1.0"), amounts["DASH"].value) |
1339 | log_balances.assert_called_with(market) | ||
1340 | log_dispatch.assert_called_once_with(portfolio.Amount("BTC", | ||
1341 | "11.1"), amounts, "medium", repartition_hash) | ||
1255 | 1342 | ||
1256 | def test_currencies(self): | 1343 | def test_currencies(self): |
1257 | portfolio.BalanceStore.all = { | 1344 | portfolio.BalanceStore.all = { |
@@ -1268,6 +1355,23 @@ class BalanceStoreTest(WebMockTestCase): | |||
1268 | } | 1355 | } |
1269 | self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies())) | 1356 | self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies())) |
1270 | 1357 | ||
1358 | def test_as_json(self): | ||
1359 | balance_mock1 = mock.Mock() | ||
1360 | balance_mock1.as_json.return_value = 1 | ||
1361 | |||
1362 | balance_mock2 = mock.Mock() | ||
1363 | balance_mock2.as_json.return_value = 2 | ||
1364 | |||
1365 | portfolio.BalanceStore.all = { | ||
1366 | "BTC": balance_mock1, | ||
1367 | "ETH": balance_mock2, | ||
1368 | } | ||
1369 | |||
1370 | as_json = portfolio.BalanceStore.as_json() | ||
1371 | self.assertEqual(1, as_json["BTC"]) | ||
1372 | self.assertEqual(2, as_json["ETH"]) | ||
1373 | |||
1374 | |||
1271 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1375 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
1272 | class ComputationTest(WebMockTestCase): | 1376 | class ComputationTest(WebMockTestCase): |
1273 | def test_compute_value(self): | 1377 | def test_compute_value(self): |
@@ -1426,7 +1530,8 @@ class TradeTest(WebMockTestCase): | |||
1426 | Order.assert_not_called() | 1530 | Order.assert_not_called() |
1427 | 1531 | ||
1428 | get_ticker.return_value = { "inverted": False } | 1532 | get_ticker.return_value = { "inverted": False } |
1429 | with self.subTest(desc="Already filled"), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1533 | with self.subTest(desc="Already filled"),\ |
1534 | mock.patch("portfolio.ReportStore") as report_store: | ||
1430 | filled_amount.return_value = portfolio.Amount("FOO", "100") | 1535 | filled_amount.return_value = portfolio.Amount("FOO", "100") |
1431 | compute_value.return_value = D("0.125") | 1536 | compute_value.return_value = D("0.125") |
1432 | 1537 | ||
@@ -1441,7 +1546,7 @@ class TradeTest(WebMockTestCase): | |||
1441 | filled_amount.assert_called_with(in_base_currency=False) | 1546 | filled_amount.assert_called_with(in_base_currency=False) |
1442 | compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default") | 1547 | compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default") |
1443 | self.assertEqual(0, len(trade.orders)) | 1548 | self.assertEqual(0, len(trade.orders)) |
1444 | self.assertRegex(stdout_mock.getvalue(), "Less to do than already filled: ") | 1549 | report_store.log_error.assert_called_with("prepare_order", message=mock.ANY) |
1445 | Order.assert_not_called() | 1550 | Order.assert_not_called() |
1446 | 1551 | ||
1447 | with self.subTest(action="dispose", inverted=False): | 1552 | with self.subTest(action="dispose", inverted=False): |
@@ -1554,77 +1659,110 @@ class TradeTest(WebMockTestCase): | |||
1554 | prepare_order.return_value = new_order_mock | 1659 | prepare_order.return_value = new_order_mock |
1555 | 1660 | ||
1556 | for i in [0, 1, 3, 4, 6]: | 1661 | for i in [0, 1, 3, 4, 6]: |
1557 | with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1662 | with self.subTest(tick=i),\ |
1663 | mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | ||
1558 | trade.update_order(order_mock, i) | 1664 | trade.update_order(order_mock, i) |
1559 | order_mock.cancel.assert_not_called() | 1665 | order_mock.cancel.assert_not_called() |
1560 | new_order_mock.run.assert_not_called() | 1666 | new_order_mock.run.assert_not_called() |
1561 | self.assertRegex(stdout_mock.getvalue(), "tick {}, waiting".format(i)) | 1667 | log_order.assert_called_once_with(order_mock, i, |
1668 | update="waiting", compute_value=None, new_order=None) | ||
1562 | 1669 | ||
1563 | order_mock.reset_mock() | 1670 | order_mock.reset_mock() |
1564 | new_order_mock.reset_mock() | 1671 | new_order_mock.reset_mock() |
1565 | trade.orders = [] | 1672 | trade.orders = [] |
1566 | 1673 | ||
1567 | with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1674 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: |
1568 | trade.update_order(order_mock, 2) | 1675 | trade.update_order(order_mock, 2) |
1569 | order_mock.cancel.assert_called() | 1676 | order_mock.cancel.assert_called() |
1570 | new_order_mock.run.assert_called() | 1677 | new_order_mock.run.assert_called() |
1571 | prepare_order.assert_called() | 1678 | prepare_order.assert_called() |
1572 | self.assertRegex(stdout_mock.getvalue(), "tick 2, cancelling and adjusting") | 1679 | log_order.assert_called() |
1680 | self.assertEqual(2, log_order.call_count) | ||
1681 | calls = [ | ||
1682 | mock.call(order_mock, 2, update="adjusting", | ||
1683 | compute_value='lambda x, y: (x[y] + x["average"]) / 2', | ||
1684 | new_order=new_order_mock), | ||
1685 | mock.call(order_mock, 2, new_order=new_order_mock), | ||
1686 | ] | ||
1687 | log_order.assert_has_calls(calls) | ||
1573 | 1688 | ||
1574 | order_mock.reset_mock() | 1689 | order_mock.reset_mock() |
1575 | new_order_mock.reset_mock() | 1690 | new_order_mock.reset_mock() |
1576 | trade.orders = [] | 1691 | trade.orders = [] |
1577 | 1692 | ||
1578 | with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1693 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: |
1579 | trade.update_order(order_mock, 5) | 1694 | trade.update_order(order_mock, 5) |
1580 | order_mock.cancel.assert_called() | 1695 | order_mock.cancel.assert_called() |
1581 | new_order_mock.run.assert_called() | 1696 | new_order_mock.run.assert_called() |
1582 | prepare_order.assert_called() | 1697 | prepare_order.assert_called() |
1583 | self.assertRegex(stdout_mock.getvalue(), "tick 5, cancelling and adjusting") | 1698 | self.assertEqual(2, log_order.call_count) |
1699 | log_order.assert_called() | ||
1700 | calls = [ | ||
1701 | mock.call(order_mock, 5, update="adjusting", | ||
1702 | compute_value='lambda x, y: (x[y]*2 + x["average"]) / 3', | ||
1703 | new_order=new_order_mock), | ||
1704 | mock.call(order_mock, 5, new_order=new_order_mock), | ||
1705 | ] | ||
1706 | log_order.assert_has_calls(calls) | ||
1584 | 1707 | ||
1585 | order_mock.reset_mock() | 1708 | order_mock.reset_mock() |
1586 | new_order_mock.reset_mock() | 1709 | new_order_mock.reset_mock() |
1587 | trade.orders = [] | 1710 | trade.orders = [] |
1588 | 1711 | ||
1589 | with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1712 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: |
1590 | trade.update_order(order_mock, 7) | 1713 | trade.update_order(order_mock, 7) |
1591 | order_mock.cancel.assert_called() | 1714 | order_mock.cancel.assert_called() |
1592 | new_order_mock.run.assert_called() | 1715 | new_order_mock.run.assert_called() |
1593 | prepare_order.assert_called_with(compute_value="default") | 1716 | prepare_order.assert_called_with(compute_value="default") |
1594 | self.assertRegex(stdout_mock.getvalue(), "tick 7, fallbacking to market value") | 1717 | log_order.assert_called() |
1595 | self.assertRegex(stdout_mock.getvalue(), "tick 7, market value, cancelling and adjusting to") | 1718 | self.assertEqual(2, log_order.call_count) |
1719 | calls = [ | ||
1720 | mock.call(order_mock, 7, update="market_fallback", | ||
1721 | compute_value='default', | ||
1722 | new_order=new_order_mock), | ||
1723 | mock.call(order_mock, 7, new_order=new_order_mock), | ||
1724 | ] | ||
1725 | log_order.assert_has_calls(calls) | ||
1596 | 1726 | ||
1597 | order_mock.reset_mock() | 1727 | order_mock.reset_mock() |
1598 | new_order_mock.reset_mock() | 1728 | new_order_mock.reset_mock() |
1599 | trade.orders = [] | 1729 | trade.orders = [] |
1600 | 1730 | ||
1601 | for i in [10, 13, 16]: | 1731 | for i in [10, 13, 16]: |
1602 | with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1732 | with self.subTest(tick=i), mock.patch.object(portfolio.ReportStore, "log_order") as log_order: |
1603 | trade.update_order(order_mock, i) | 1733 | trade.update_order(order_mock, i) |
1604 | order_mock.cancel.assert_called() | 1734 | order_mock.cancel.assert_called() |
1605 | new_order_mock.run.assert_called() | 1735 | new_order_mock.run.assert_called() |
1606 | prepare_order.assert_called_with(compute_value="default") | 1736 | prepare_order.assert_called_with(compute_value="default") |
1607 | self.assertNotRegex(stdout_mock.getvalue(), "tick {}, fallbacking to market value".format(i)) | 1737 | log_order.assert_called() |
1608 | self.assertRegex(stdout_mock.getvalue(), "tick {}, market value, cancelling and adjusting to".format(i)) | 1738 | self.assertEqual(2, log_order.call_count) |
1739 | calls = [ | ||
1740 | mock.call(order_mock, i, update="market_adjust", | ||
1741 | compute_value='default', | ||
1742 | new_order=new_order_mock), | ||
1743 | mock.call(order_mock, i, new_order=new_order_mock), | ||
1744 | ] | ||
1745 | log_order.assert_has_calls(calls) | ||
1609 | 1746 | ||
1610 | order_mock.reset_mock() | 1747 | order_mock.reset_mock() |
1611 | new_order_mock.reset_mock() | 1748 | new_order_mock.reset_mock() |
1612 | trade.orders = [] | 1749 | trade.orders = [] |
1613 | 1750 | ||
1614 | for i in [8, 9, 11, 12]: | 1751 | for i in [8, 9, 11, 12]: |
1615 | with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1752 | with self.subTest(tick=i), mock.patch.object(portfolio.ReportStore, "log_order") as log_order: |
1616 | trade.update_order(order_mock, i) | 1753 | trade.update_order(order_mock, i) |
1617 | order_mock.cancel.assert_not_called() | 1754 | order_mock.cancel.assert_not_called() |
1618 | new_order_mock.run.assert_not_called() | 1755 | new_order_mock.run.assert_not_called() |
1619 | self.assertEqual("", stdout_mock.getvalue()) | 1756 | log_order.assert_called_once_with(order_mock, i, update="waiting", |
1757 | compute_value=None, new_order=None) | ||
1620 | 1758 | ||
1621 | order_mock.reset_mock() | 1759 | order_mock.reset_mock() |
1622 | new_order_mock.reset_mock() | 1760 | new_order_mock.reset_mock() |
1623 | trade.orders = [] | 1761 | trade.orders = [] |
1624 | 1762 | ||
1625 | 1763 | ||
1626 | @mock.patch('sys.stdout', new_callable=StringIO) | 1764 | @mock.patch.object(portfolio.ReportStore, "print_log") |
1627 | def test_print_with_order(self, mock_stdout): | 1765 | def test_print_with_order(self, print_log): |
1628 | value_from = portfolio.Amount("BTC", "0.5") | 1766 | value_from = portfolio.Amount("BTC", "0.5") |
1629 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1767 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1630 | value_to = portfolio.Amount("BTC", "1.0") | 1768 | value_to = portfolio.Amount("BTC", "1.0") |
@@ -1651,12 +1789,13 @@ class TradeTest(WebMockTestCase): | |||
1651 | 1789 | ||
1652 | trade.print_with_order() | 1790 | trade.print_with_order() |
1653 | 1791 | ||
1654 | out = mock_stdout.getvalue().split("\n") | 1792 | print_log.assert_called() |
1655 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", out[0]) | 1793 | calls = print_log.mock_calls |
1656 | self.assertEqual("\tMock 1", out[1]) | 1794 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0])) |
1657 | self.assertEqual("\tMock 2", out[2]) | 1795 | self.assertEqual("\tMock 1", str(calls[1][1][0])) |
1658 | self.assertEqual("\t\tMouvement 1", out[3]) | 1796 | self.assertEqual("\tMock 2", str(calls[2][1][0])) |
1659 | self.assertEqual("\t\tMouvement 2", out[4]) | 1797 | self.assertEqual("\t\tMouvement 1", str(calls[3][1][0])) |
1798 | self.assertEqual("\t\tMouvement 2", str(calls[4][1][0])) | ||
1660 | 1799 | ||
1661 | def test__repr(self): | 1800 | def test__repr(self): |
1662 | value_from = portfolio.Amount("BTC", "0.5") | 1801 | value_from = portfolio.Amount("BTC", "0.5") |
@@ -1666,6 +1805,19 @@ class TradeTest(WebMockTestCase): | |||
1666 | 1805 | ||
1667 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade)) | 1806 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade)) |
1668 | 1807 | ||
1808 | def test_as_json(self): | ||
1809 | value_from = portfolio.Amount("BTC", "0.5") | ||
1810 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | ||
1811 | value_to = portfolio.Amount("BTC", "1.0") | ||
1812 | trade = portfolio.Trade(value_from, value_to, "ETH") | ||
1813 | |||
1814 | as_json = trade.as_json() | ||
1815 | self.assertEqual("acquire", as_json["action"]) | ||
1816 | self.assertEqual(D("0.5"), as_json["from"]) | ||
1817 | self.assertEqual(D("1.0"), as_json["to"]) | ||
1818 | self.assertEqual("ETH", as_json["currency"]) | ||
1819 | self.assertEqual("BTC", as_json["base_currency"]) | ||
1820 | |||
1669 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1821 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
1670 | class OrderTest(WebMockTestCase): | 1822 | class OrderTest(WebMockTestCase): |
1671 | def test_values(self): | 1823 | def test_values(self): |
@@ -1693,6 +1845,27 @@ class OrderTest(WebMockTestCase): | |||
1693 | close_if_possible=True) | 1845 | close_if_possible=True) |
1694 | self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending] ✂)", repr(order)) | 1846 | self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending] ✂)", repr(order)) |
1695 | 1847 | ||
1848 | def test_as_json(self): | ||
1849 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | ||
1850 | D("0.1"), "BTC", "long", "market", "trade") | ||
1851 | mouvement_mock1 = mock.Mock() | ||
1852 | mouvement_mock1.as_json.return_value = 1 | ||
1853 | mouvement_mock2 = mock.Mock() | ||
1854 | mouvement_mock2.as_json.return_value = 2 | ||
1855 | |||
1856 | order.mouvements = [mouvement_mock1, mouvement_mock2] | ||
1857 | as_json = order.as_json() | ||
1858 | self.assertEqual("buy", as_json["action"]) | ||
1859 | self.assertEqual("long", as_json["trade_type"]) | ||
1860 | self.assertEqual(10, as_json["amount"]) | ||
1861 | self.assertEqual("ETH", as_json["currency"]) | ||
1862 | self.assertEqual("BTC", as_json["base_currency"]) | ||
1863 | self.assertEqual(D("0.1"), as_json["rate"]) | ||
1864 | self.assertEqual("pending", as_json["status"]) | ||
1865 | self.assertEqual(False, as_json["close_if_possible"]) | ||
1866 | self.assertIsNone(as_json["id"]) | ||
1867 | self.assertEqual([1, 2], as_json["mouvements"]) | ||
1868 | |||
1696 | def test_account(self): | 1869 | def test_account(self): |
1697 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1870 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1698 | D("0.1"), "BTC", "long", "market", "trade") | 1871 | D("0.1"), "BTC", "long", "market", "trade") |
@@ -1728,7 +1901,8 @@ class OrderTest(WebMockTestCase): | |||
1728 | self.assertTrue(order.finished) | 1901 | self.assertTrue(order.finished) |
1729 | 1902 | ||
1730 | @mock.patch.object(portfolio.Order, "fetch") | 1903 | @mock.patch.object(portfolio.Order, "fetch") |
1731 | def test_cancel(self, fetch): | 1904 | @mock.patch("portfolio.ReportStore") |
1905 | def test_cancel(self, report_store, fetch): | ||
1732 | market = mock.Mock() | 1906 | market = mock.Mock() |
1733 | portfolio.TradeStore.debug = True | 1907 | portfolio.TradeStore.debug = True |
1734 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1908 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -1737,6 +1911,8 @@ class OrderTest(WebMockTestCase): | |||
1737 | 1911 | ||
1738 | order.cancel() | 1912 | order.cancel() |
1739 | market.cancel_order.assert_not_called() | 1913 | market.cancel_order.assert_not_called() |
1914 | report_store.log_debug_action.assert_called_once() | ||
1915 | report_store.log_debug_action.reset_mock() | ||
1740 | self.assertEqual("canceled", order.status) | 1916 | self.assertEqual("canceled", order.status) |
1741 | 1917 | ||
1742 | portfolio.TradeStore.debug = False | 1918 | portfolio.TradeStore.debug = False |
@@ -1748,6 +1924,7 @@ class OrderTest(WebMockTestCase): | |||
1748 | order.cancel() | 1924 | order.cancel() |
1749 | market.cancel_order.assert_called_with(42) | 1925 | market.cancel_order.assert_called_with(42) |
1750 | fetch.assert_called_once() | 1926 | fetch.assert_called_once() |
1927 | report_store.log_debug_action.assert_not_called() | ||
1751 | 1928 | ||
1752 | def test_dust_amount_remaining(self): | 1929 | def test_dust_amount_remaining(self): |
1753 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1930 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -1824,7 +2001,8 @@ class OrderTest(WebMockTestCase): | |||
1824 | order.fetch_mouvements() | 2001 | order.fetch_mouvements() |
1825 | self.assertEqual(0, len(order.mouvements)) | 2002 | self.assertEqual(0, len(order.mouvements)) |
1826 | 2003 | ||
1827 | def test_mark_finished_order(self): | 2004 | @mock.patch("portfolio.ReportStore") |
2005 | def test_mark_finished_order(self, report_store): | ||
1828 | market = mock.Mock() | 2006 | market = mock.Mock() |
1829 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2007 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1830 | D("0.1"), "BTC", "short", market, "trade", | 2008 | D("0.1"), "BTC", "short", market, "trade", |
@@ -1870,9 +2048,11 @@ class OrderTest(WebMockTestCase): | |||
1870 | 2048 | ||
1871 | order.mark_finished_order() | 2049 | order.mark_finished_order() |
1872 | market.close_margin_position.assert_not_called() | 2050 | market.close_margin_position.assert_not_called() |
2051 | report_store.log_debug_action.assert_called_once() | ||
1873 | 2052 | ||
1874 | @mock.patch.object(portfolio.Order, "fetch_mouvements") | 2053 | @mock.patch.object(portfolio.Order, "fetch_mouvements") |
1875 | def test_fetch(self, fetch_mouvements): | 2054 | @mock.patch("portfolio.ReportStore") |
2055 | def test_fetch(self, report_store, fetch_mouvements): | ||
1876 | time = self.time.time() | 2056 | time = self.time.time() |
1877 | with mock.patch.object(portfolio.time, "time") as time_mock: | 2057 | with mock.patch.object(portfolio.time, "time") as time_mock: |
1878 | market = mock.Mock() | 2058 | market = mock.Mock() |
@@ -1883,10 +2063,14 @@ class OrderTest(WebMockTestCase): | |||
1883 | portfolio.TradeStore.debug = True | 2063 | portfolio.TradeStore.debug = True |
1884 | order.fetch() | 2064 | order.fetch() |
1885 | time_mock.assert_not_called() | 2065 | time_mock.assert_not_called() |
2066 | report_store.log_debug_action.assert_called_once() | ||
2067 | report_store.log_debug_action.reset_mock() | ||
1886 | order.fetch(force=True) | 2068 | order.fetch(force=True) |
1887 | time_mock.assert_not_called() | 2069 | time_mock.assert_not_called() |
1888 | market.fetch_order.assert_not_called() | 2070 | market.fetch_order.assert_not_called() |
1889 | fetch_mouvements.assert_not_called() | 2071 | fetch_mouvements.assert_not_called() |
2072 | report_store.log_debug_action.assert_called_once() | ||
2073 | report_store.log_debug_action.reset_mock() | ||
1890 | self.assertIsNone(order.fetch_cache_timestamp) | 2074 | self.assertIsNone(order.fetch_cache_timestamp) |
1891 | 2075 | ||
1892 | with self.subTest(debug=False): | 2076 | with self.subTest(debug=False): |
@@ -1924,16 +2108,19 @@ class OrderTest(WebMockTestCase): | |||
1924 | order.fetch() | 2108 | order.fetch() |
1925 | market.fetch_order.assert_called_once() | 2109 | market.fetch_order.assert_called_once() |
1926 | fetch_mouvements.assert_called_once() | 2110 | fetch_mouvements.assert_called_once() |
2111 | report_store.log_debug_action.assert_not_called() | ||
1927 | 2112 | ||
1928 | @mock.patch.object(portfolio.Order, "fetch") | 2113 | @mock.patch.object(portfolio.Order, "fetch") |
1929 | @mock.patch.object(portfolio.Order, "mark_finished_order") | 2114 | @mock.patch.object(portfolio.Order, "mark_finished_order") |
1930 | def test_get_status(self, mark_finished_order, fetch): | 2115 | @mock.patch("portfolio.ReportStore") |
2116 | def test_get_status(self, report_store, mark_finished_order, fetch): | ||
1931 | with self.subTest(debug=True): | 2117 | with self.subTest(debug=True): |
1932 | portfolio.TradeStore.debug = True | 2118 | portfolio.TradeStore.debug = True |
1933 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2119 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1934 | D("0.1"), "BTC", "long", "market", "trade") | 2120 | D("0.1"), "BTC", "long", "market", "trade") |
1935 | self.assertEqual("pending", order.get_status()) | 2121 | self.assertEqual("pending", order.get_status()) |
1936 | fetch.assert_not_called() | 2122 | fetch.assert_not_called() |
2123 | report_store.log_debug_action.assert_called_once() | ||
1937 | 2124 | ||
1938 | with self.subTest(debug=False, finished=False): | 2125 | with self.subTest(debug=False, finished=False): |
1939 | portfolio.TradeStore.debug = False | 2126 | portfolio.TradeStore.debug = False |
@@ -1968,13 +2155,13 @@ class OrderTest(WebMockTestCase): | |||
1968 | 2155 | ||
1969 | market.order_precision.return_value = 4 | 2156 | market.order_precision.return_value = 4 |
1970 | with self.subTest(debug=True),\ | 2157 | with self.subTest(debug=True),\ |
1971 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 2158 | mock.patch('portfolio.ReportStore') as report_store: |
1972 | portfolio.TradeStore.debug = True | 2159 | portfolio.TradeStore.debug = True |
1973 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2160 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1974 | D("0.1"), "BTC", "long", market, "trade") | 2161 | D("0.1"), "BTC", "long", market, "trade") |
1975 | order.run() | 2162 | order.run() |
1976 | market.create_order.assert_not_called() | 2163 | market.create_order.assert_not_called() |
1977 | self.assertEqual("market.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)\n", stdout_mock.getvalue()) | 2164 | report_store.log_debug_action.assert_called_with("market.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)") |
1978 | self.assertEqual("open", order.status) | 2165 | self.assertEqual("open", order.status) |
1979 | self.assertEqual(1, len(order.results)) | 2166 | self.assertEqual(1, len(order.results)) |
1980 | self.assertEqual(-1, order.id) | 2167 | self.assertEqual(-1, order.id) |
@@ -1992,7 +2179,7 @@ class OrderTest(WebMockTestCase): | |||
1992 | 2179 | ||
1993 | market.create_order.reset_mock() | 2180 | market.create_order.reset_mock() |
1994 | with self.subTest(exception=True),\ | 2181 | with self.subTest(exception=True),\ |
1995 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 2182 | mock.patch('portfolio.ReportStore') as report_store: |
1996 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2183 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1997 | D("0.1"), "BTC", "long", market, "trade") | 2184 | D("0.1"), "BTC", "long", market, "trade") |
1998 | market.create_order.side_effect = Exception("bouh") | 2185 | market.create_order.side_effect = Exception("bouh") |
@@ -2000,8 +2187,7 @@ class OrderTest(WebMockTestCase): | |||
2000 | market.create_order.assert_called_once() | 2187 | market.create_order.assert_called_once() |
2001 | self.assertEqual(0, len(order.results)) | 2188 | self.assertEqual(0, len(order.results)) |
2002 | self.assertEqual("error", order.status) | 2189 | self.assertEqual("error", order.status) |
2003 | self.assertRegex(stdout_mock.getvalue(), "error when running market.create_order") | 2190 | report_store.log_error.assert_called_once() |
2004 | self.assertRegex(stdout_mock.getvalue(), "Exception: bouh") | ||
2005 | 2191 | ||
2006 | market.create_order.reset_mock() | 2192 | market.create_order.reset_mock() |
2007 | with self.subTest(dust_amount_exception=True),\ | 2193 | with self.subTest(dust_amount_exception=True),\ |
@@ -2050,6 +2236,7 @@ class MouvementTest(WebMockTestCase): | |||
2050 | "amount": "10", "total": "1" | 2236 | "amount": "10", "total": "1" |
2051 | }) | 2237 | }) |
2052 | self.assertEqual("Mouvement(2017-12-30 12:00:12 ; buy 10.00000000 ETH (1.00000000 BTC) fee: 0.1500%)", repr(mouvement)) | 2238 | self.assertEqual("Mouvement(2017-12-30 12:00:12 ; buy 10.00000000 ETH (1.00000000 BTC) fee: 0.1500%)", repr(mouvement)) |
2239 | |||
2053 | mouvement = portfolio.Mouvement("ETH", "BTC", { | 2240 | mouvement = portfolio.Mouvement("ETH", "BTC", { |
2054 | "tradeID": 42, "type": "buy", | 2241 | "tradeID": 42, "type": "buy", |
2055 | "date": "garbage", "rate": "0.1", | 2242 | "date": "garbage", "rate": "0.1", |
@@ -2057,10 +2244,401 @@ class MouvementTest(WebMockTestCase): | |||
2057 | }) | 2244 | }) |
2058 | self.assertEqual("Mouvement(No date ; buy 10.00000000 ETH (1.00000000 BTC))", repr(mouvement)) | 2245 | self.assertEqual("Mouvement(No date ; buy 10.00000000 ETH (1.00000000 BTC))", repr(mouvement)) |
2059 | 2246 | ||
2247 | def test_as_json(self): | ||
2248 | mouvement = portfolio.Mouvement("ETH", "BTC", { | ||
2249 | "tradeID": 42, "type": "buy", "fee": "0.0015", | ||
2250 | "date": "2017-12-30 12:00:12", "rate": "0.1", | ||
2251 | "amount": "10", "total": "1" | ||
2252 | }) | ||
2253 | as_json = mouvement.as_json() | ||
2254 | |||
2255 | self.assertEqual(D("0.0015"), as_json["fee_rate"]) | ||
2256 | self.assertEqual(portfolio.datetime(2017, 12, 30, 12, 0, 12), as_json["date"]) | ||
2257 | self.assertEqual("buy", as_json["action"]) | ||
2258 | self.assertEqual(D("10"), as_json["total"]) | ||
2259 | self.assertEqual(D("1"), as_json["total_in_base"]) | ||
2260 | self.assertEqual("BTC", as_json["base_currency"]) | ||
2261 | self.assertEqual("ETH", as_json["currency"]) | ||
2262 | |||
2263 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
2264 | class ReportStoreTest(WebMockTestCase): | ||
2265 | def test_add_log(self): | ||
2266 | portfolio.ReportStore.add_log({"foo": "bar"}) | ||
2267 | |||
2268 | self.assertEqual({"foo": "bar", "date": mock.ANY}, portfolio.ReportStore.logs[0]) | ||
2269 | |||
2270 | def test_set_verbose(self): | ||
2271 | with self.subTest(verbose=True): | ||
2272 | portfolio.ReportStore.set_verbose(True) | ||
2273 | self.assertTrue(portfolio.ReportStore.verbose_print) | ||
2274 | |||
2275 | with self.subTest(verbose=False): | ||
2276 | portfolio.ReportStore.set_verbose(False) | ||
2277 | self.assertFalse(portfolio.ReportStore.verbose_print) | ||
2278 | |||
2279 | def test_print_log(self): | ||
2280 | with self.subTest(verbose=True),\ | ||
2281 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
2282 | portfolio.ReportStore.set_verbose(True) | ||
2283 | portfolio.ReportStore.print_log("Coucou") | ||
2284 | portfolio.ReportStore.print_log(portfolio.Amount("BTC", 1)) | ||
2285 | self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n") | ||
2286 | |||
2287 | with self.subTest(verbose=False),\ | ||
2288 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
2289 | portfolio.ReportStore.set_verbose(False) | ||
2290 | portfolio.ReportStore.print_log("Coucou") | ||
2291 | portfolio.ReportStore.print_log(portfolio.Amount("BTC", 1)) | ||
2292 | self.assertEqual(stdout_mock.getvalue(), "") | ||
2293 | |||
2294 | def test_to_json(self): | ||
2295 | portfolio.ReportStore.logs.append({"foo": "bar"}) | ||
2296 | self.assertEqual('[{"foo": "bar"}]', portfolio.ReportStore.to_json()) | ||
2297 | portfolio.ReportStore.logs.append({"date": portfolio.datetime(2018, 2, 24)}) | ||
2298 | self.assertEqual('[{"foo": "bar"}, {"date": "2018-02-24T00:00:00"}]', portfolio.ReportStore.to_json()) | ||
2299 | portfolio.ReportStore.logs.append({"amount": portfolio.Amount("BTC", 1)}) | ||
2300 | with self.assertRaises(TypeError): | ||
2301 | portfolio.ReportStore.to_json() | ||
2302 | |||
2303 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2304 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2305 | def test_log_stage(self, add_log, print_log): | ||
2306 | portfolio.ReportStore.log_stage("foo") | ||
2307 | print_log.assert_has_calls([ | ||
2308 | mock.call("-----------"), | ||
2309 | mock.call("[Stage] foo"), | ||
2310 | ]) | ||
2311 | add_log.assert_called_once_with({'type': 'stage', 'stage': 'foo'}) | ||
2312 | |||
2313 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2314 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2315 | @mock.patch("store.BalanceStore") | ||
2316 | def test_log_balances(self, balance_store, add_log, print_log): | ||
2317 | balance_store.as_json.return_value = "json" | ||
2318 | balance_store.all = { "FOO": "bar", "BAR": "baz" } | ||
2319 | |||
2320 | portfolio.ReportStore.log_balances("market") | ||
2321 | print_log.assert_has_calls([ | ||
2322 | mock.call("[Balance]"), | ||
2323 | mock.call("\tbar"), | ||
2324 | mock.call("\tbaz"), | ||
2325 | ]) | ||
2326 | add_log.assert_called_once_with({'type': 'balance', 'balances': 'json'}) | ||
2327 | |||
2328 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2329 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2330 | def test_log_tickers(self, add_log, print_log): | ||
2331 | market = mock.Mock() | ||
2332 | amounts = { | ||
2333 | "BTC": portfolio.Amount("BTC", 10), | ||
2334 | "ETH": portfolio.Amount("BTC", D("0.3")) | ||
2335 | } | ||
2336 | amounts["ETH"].rate = D("0.1") | ||
2337 | |||
2338 | portfolio.ReportStore.log_tickers(market, amounts, "BTC", "default", "total") | ||
2339 | print_log.assert_not_called() | ||
2340 | add_log.assert_called_once_with({ | ||
2341 | 'type': 'tickers', | ||
2342 | 'compute_value': 'default', | ||
2343 | 'balance_type': 'total', | ||
2344 | 'currency': 'BTC', | ||
2345 | 'balances': { | ||
2346 | 'BTC': D('10'), | ||
2347 | 'ETH': D('0.3') | ||
2348 | }, | ||
2349 | 'rates': { | ||
2350 | 'BTC': None, | ||
2351 | 'ETH': D('0.1') | ||
2352 | }, | ||
2353 | 'total': D('10.3') | ||
2354 | }) | ||
2355 | |||
2356 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2357 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2358 | def test_log_dispatch(self, add_log, print_log): | ||
2359 | amount = portfolio.Amount("BTC", "10.3") | ||
2360 | amounts = { | ||
2361 | "BTC": portfolio.Amount("BTC", 10), | ||
2362 | "ETH": portfolio.Amount("BTC", D("0.3")) | ||
2363 | } | ||
2364 | portfolio.ReportStore.log_dispatch(amount, amounts, "medium", "repartition") | ||
2365 | print_log.assert_not_called() | ||
2366 | add_log.assert_called_once_with({ | ||
2367 | 'type': 'dispatch', | ||
2368 | 'liquidity': 'medium', | ||
2369 | 'repartition_ratio': 'repartition', | ||
2370 | 'total_amount': { | ||
2371 | 'currency': 'BTC', | ||
2372 | 'value': D('10.3') | ||
2373 | }, | ||
2374 | 'repartition': { | ||
2375 | 'BTC': D('10'), | ||
2376 | 'ETH': D('0.3') | ||
2377 | } | ||
2378 | }) | ||
2379 | |||
2380 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2381 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2382 | def test_log_trades(self, add_log, print_log): | ||
2383 | trade_mock1 = mock.Mock() | ||
2384 | trade_mock2 = mock.Mock() | ||
2385 | trade_mock1.as_json.return_value = { "trade": "1" } | ||
2386 | trade_mock2.as_json.return_value = { "trade": "2" } | ||
2387 | |||
2388 | matching_and_trades = [ | ||
2389 | (True, trade_mock1), | ||
2390 | (False, trade_mock2), | ||
2391 | ] | ||
2392 | portfolio.ReportStore.log_trades(matching_and_trades, "only", "debug") | ||
2393 | |||
2394 | print_log.assert_not_called() | ||
2395 | add_log.assert_called_with({ | ||
2396 | 'type': 'trades', | ||
2397 | 'only': 'only', | ||
2398 | 'debug': 'debug', | ||
2399 | 'trades': [ | ||
2400 | {'trade': '1', 'skipped': False}, | ||
2401 | {'trade': '2', 'skipped': True} | ||
2402 | ] | ||
2403 | }) | ||
2404 | |||
2405 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2406 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2407 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | ||
2408 | def test_log_orders(self, print_all_with_order, add_log, print_log): | ||
2409 | order_mock1 = mock.Mock() | ||
2410 | order_mock2 = mock.Mock() | ||
2411 | |||
2412 | order_mock1.as_json.return_value = "order1" | ||
2413 | order_mock2.as_json.return_value = "order2" | ||
2414 | |||
2415 | orders = [order_mock1, order_mock2] | ||
2416 | |||
2417 | portfolio.ReportStore.log_orders(orders, tick="tick", | ||
2418 | only="only", compute_value="compute_value") | ||
2419 | |||
2420 | print_log.assert_called_once_with("[Orders]") | ||
2421 | print_all_with_order.assert_called_once_with(ind="\t") | ||
2422 | |||
2423 | add_log.assert_called_with({ | ||
2424 | 'type': 'orders', | ||
2425 | 'only': 'only', | ||
2426 | 'compute_value': 'compute_value', | ||
2427 | 'tick': 'tick', | ||
2428 | 'orders': ['order1', 'order2'] | ||
2429 | }) | ||
2430 | |||
2431 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2432 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2433 | def test_log_order(self, add_log, print_log): | ||
2434 | order_mock = mock.Mock() | ||
2435 | order_mock.as_json.return_value = "order" | ||
2436 | new_order_mock = mock.Mock() | ||
2437 | new_order_mock.as_json.return_value = "new_order" | ||
2438 | order_mock.__repr__ = mock.Mock() | ||
2439 | order_mock.__repr__.return_value = "Order Mock" | ||
2440 | new_order_mock.__repr__ = mock.Mock() | ||
2441 | new_order_mock.__repr__.return_value = "New order Mock" | ||
2442 | |||
2443 | with self.subTest(finished=True): | ||
2444 | portfolio.ReportStore.log_order(order_mock, 1, finished=True) | ||
2445 | print_log.assert_called_once_with("[Order] Finished Order Mock") | ||
2446 | add_log.assert_called_once_with({ | ||
2447 | 'type': 'order', | ||
2448 | 'tick': 1, | ||
2449 | 'update': None, | ||
2450 | 'order': 'order', | ||
2451 | 'compute_value': None, | ||
2452 | 'new_order': None | ||
2453 | }) | ||
2454 | |||
2455 | add_log.reset_mock() | ||
2456 | print_log.reset_mock() | ||
2457 | |||
2458 | with self.subTest(update="waiting"): | ||
2459 | portfolio.ReportStore.log_order(order_mock, 1, update="waiting") | ||
2460 | print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting") | ||
2461 | add_log.assert_called_once_with({ | ||
2462 | 'type': 'order', | ||
2463 | 'tick': 1, | ||
2464 | 'update': 'waiting', | ||
2465 | 'order': 'order', | ||
2466 | 'compute_value': None, | ||
2467 | 'new_order': None | ||
2468 | }) | ||
2469 | |||
2470 | add_log.reset_mock() | ||
2471 | print_log.reset_mock() | ||
2472 | with self.subTest(update="adjusting"): | ||
2473 | portfolio.ReportStore.log_order(order_mock, 3, | ||
2474 | update="adjusting", new_order=new_order_mock, | ||
2475 | compute_value="default") | ||
2476 | print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") | ||
2477 | add_log.assert_called_once_with({ | ||
2478 | 'type': 'order', | ||
2479 | 'tick': 3, | ||
2480 | 'update': 'adjusting', | ||
2481 | 'order': 'order', | ||
2482 | 'compute_value': "default", | ||
2483 | 'new_order': 'new_order' | ||
2484 | }) | ||
2485 | |||
2486 | add_log.reset_mock() | ||
2487 | print_log.reset_mock() | ||
2488 | with self.subTest(update="market_fallback"): | ||
2489 | portfolio.ReportStore.log_order(order_mock, 7, | ||
2490 | update="market_fallback", new_order=new_order_mock) | ||
2491 | print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value") | ||
2492 | add_log.assert_called_once_with({ | ||
2493 | 'type': 'order', | ||
2494 | 'tick': 7, | ||
2495 | 'update': 'market_fallback', | ||
2496 | 'order': 'order', | ||
2497 | 'compute_value': None, | ||
2498 | 'new_order': 'new_order' | ||
2499 | }) | ||
2500 | |||
2501 | add_log.reset_mock() | ||
2502 | print_log.reset_mock() | ||
2503 | with self.subTest(update="market_adjusting"): | ||
2504 | portfolio.ReportStore.log_order(order_mock, 17, | ||
2505 | update="market_adjust", new_order=new_order_mock) | ||
2506 | print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock") | ||
2507 | add_log.assert_called_once_with({ | ||
2508 | 'type': 'order', | ||
2509 | 'tick': 17, | ||
2510 | 'update': 'market_adjust', | ||
2511 | 'order': 'order', | ||
2512 | 'compute_value': None, | ||
2513 | 'new_order': 'new_order' | ||
2514 | }) | ||
2515 | |||
2516 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2517 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2518 | def test_log_move_balances(self, add_log, print_log): | ||
2519 | needed = { | ||
2520 | "BTC": portfolio.Amount("BTC", 10), | ||
2521 | "USDT": 1 | ||
2522 | } | ||
2523 | moving = { | ||
2524 | "BTC": portfolio.Amount("BTC", 3), | ||
2525 | "USDT": -2 | ||
2526 | } | ||
2527 | portfolio.ReportStore.log_move_balances(needed, moving, True) | ||
2528 | print_log.assert_not_called() | ||
2529 | add_log.assert_called_once_with({ | ||
2530 | 'type': 'move_balances', | ||
2531 | 'debug': True, | ||
2532 | 'needed': { | ||
2533 | 'BTC': D('10'), | ||
2534 | 'USDT': 1 | ||
2535 | }, | ||
2536 | 'moving': { | ||
2537 | 'BTC': D('3'), | ||
2538 | 'USDT': -2 | ||
2539 | } | ||
2540 | }) | ||
2541 | |||
2542 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2543 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2544 | def test_log_http_request(self, add_log, print_log): | ||
2545 | response = mock.Mock() | ||
2546 | response.status_code = 200 | ||
2547 | response.text = "Hey" | ||
2548 | |||
2549 | portfolio.ReportStore.log_http_request("method", "url", "body", | ||
2550 | "headers", response) | ||
2551 | print_log.assert_not_called() | ||
2552 | add_log.assert_called_once_with({ | ||
2553 | 'type': 'http_request', | ||
2554 | 'method': 'method', | ||
2555 | 'url': 'url', | ||
2556 | 'body': 'body', | ||
2557 | 'headers': 'headers', | ||
2558 | 'status': 200, | ||
2559 | 'response': 'Hey' | ||
2560 | }) | ||
2561 | |||
2562 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2563 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2564 | def test_log_error(self, add_log, print_log): | ||
2565 | with self.subTest(message=None, exception=None): | ||
2566 | portfolio.ReportStore.log_error("action") | ||
2567 | print_log.assert_called_once_with("[Error] action") | ||
2568 | add_log.assert_called_once_with({ | ||
2569 | 'type': 'error', | ||
2570 | 'action': 'action', | ||
2571 | 'exception_class': None, | ||
2572 | 'exception_message': None, | ||
2573 | 'message': None | ||
2574 | }) | ||
2575 | |||
2576 | print_log.reset_mock() | ||
2577 | add_log.reset_mock() | ||
2578 | with self.subTest(message="Hey", exception=None): | ||
2579 | portfolio.ReportStore.log_error("action", message="Hey") | ||
2580 | print_log.assert_has_calls([ | ||
2581 | mock.call("[Error] action"), | ||
2582 | mock.call("\tHey") | ||
2583 | ]) | ||
2584 | add_log.assert_called_once_with({ | ||
2585 | 'type': 'error', | ||
2586 | 'action': 'action', | ||
2587 | 'exception_class': None, | ||
2588 | 'exception_message': None, | ||
2589 | 'message': "Hey" | ||
2590 | }) | ||
2591 | |||
2592 | print_log.reset_mock() | ||
2593 | add_log.reset_mock() | ||
2594 | with self.subTest(message=None, exception=Exception("bouh")): | ||
2595 | portfolio.ReportStore.log_error("action", exception=Exception("bouh")) | ||
2596 | print_log.assert_has_calls([ | ||
2597 | mock.call("[Error] action"), | ||
2598 | mock.call("\tException: bouh") | ||
2599 | ]) | ||
2600 | add_log.assert_called_once_with({ | ||
2601 | 'type': 'error', | ||
2602 | 'action': 'action', | ||
2603 | 'exception_class': "Exception", | ||
2604 | 'exception_message': "bouh", | ||
2605 | 'message': None | ||
2606 | }) | ||
2607 | |||
2608 | print_log.reset_mock() | ||
2609 | add_log.reset_mock() | ||
2610 | with self.subTest(message="Hey", exception=Exception("bouh")): | ||
2611 | portfolio.ReportStore.log_error("action", message="Hey", exception=Exception("bouh")) | ||
2612 | print_log.assert_has_calls([ | ||
2613 | mock.call("[Error] action"), | ||
2614 | mock.call("\tException: bouh"), | ||
2615 | mock.call("\tHey") | ||
2616 | ]) | ||
2617 | add_log.assert_called_once_with({ | ||
2618 | 'type': 'error', | ||
2619 | 'action': 'action', | ||
2620 | 'exception_class': "Exception", | ||
2621 | 'exception_message': "bouh", | ||
2622 | 'message': "Hey" | ||
2623 | }) | ||
2624 | |||
2625 | @mock.patch.object(portfolio.ReportStore, "print_log") | ||
2626 | @mock.patch.object(portfolio.ReportStore, "add_log") | ||
2627 | def test_log_debug_action(self, add_log, print_log): | ||
2628 | portfolio.ReportStore.log_debug_action("Hey") | ||
2629 | |||
2630 | print_log.assert_called_once_with("[Debug] Hey") | ||
2631 | add_log.assert_called_once_with({ | ||
2632 | 'type': 'debug_action', | ||
2633 | 'action': 'Hey' | ||
2634 | }) | ||
2635 | |||
2060 | @unittest.skipUnless("acceptance" in limits, "Acceptance skipped") | 2636 | @unittest.skipUnless("acceptance" in limits, "Acceptance skipped") |
2061 | class AcceptanceTest(WebMockTestCase): | 2637 | class AcceptanceTest(WebMockTestCase): |
2062 | @unittest.expectedFailure | 2638 | @unittest.expectedFailure |
2063 | def test_success_sell_only_necessary(self): | 2639 | def test_success_sell_only_necessary(self): |
2640 | # FIXME: catch stdout | ||
2641 | portfolio.ReportStore.verbose_print = False | ||
2064 | fetch_balance = { | 2642 | fetch_balance = { |
2065 | "ETH": { | 2643 | "ETH": { |
2066 | "exchange_free": D("1.0"), | 2644 | "exchange_free": D("1.0"), |