diff options
-rw-r--r-- | helper.py | 295 | ||||
-rw-r--r-- | main.py | 66 | ||||
-rw-r--r-- | market.py | 157 | ||||
-rw-r--r-- | portfolio.py | 81 | ||||
-rw-r--r-- | store.py | 230 | ||||
-rw-r--r-- | test.py | 1730 |
6 files changed, 1337 insertions, 1222 deletions
@@ -1,178 +1,133 @@ | |||
1 | import time | 1 | from datetime import datetime |
2 | from ccxt import ExchangeError | 2 | import argparse |
3 | from store import * | 3 | import configparser |
4 | 4 | import psycopg2 | |
5 | def move_balances(market, debug=False): | 5 | import os |
6 | needed_in_margin = {} | 6 | import sys |
7 | moving_to_margin = {} | 7 | |
8 | 8 | import portfolio | |
9 | for currency in BalanceStore.all: | 9 | |
10 | if BalanceStore.all[currency].margin_free != 0: | 10 | def main_parse_args(argv): |
11 | needed_in_margin[currency] = 0 | 11 | parser = argparse.ArgumentParser( |
12 | for trade in TradeStore.all: | 12 | description="Run the trade bot") |
13 | if trade.value_to.currency not in needed_in_margin: | 13 | |
14 | needed_in_margin[trade.value_to.currency] = 0 | 14 | parser.add_argument("-c", "--config", |
15 | if trade.trade_type == "short": | 15 | default="config.ini", |
16 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | 16 | required=False, |
17 | for currency, needed in needed_in_margin.items(): | 17 | help="Config file to load (default: config.ini)") |
18 | current_balance = BalanceStore.all[currency].margin_free | 18 | parser.add_argument("--before", |
19 | moving_to_margin[currency] = (needed - current_balance) | 19 | default=False, action='store_const', const=True, |
20 | delta = moving_to_margin[currency].value | 20 | help="Run the steps before the cryptoportfolio update") |
21 | if debug: | 21 | parser.add_argument("--after", |
22 | ReportStore.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) | 22 | default=False, action='store_const', const=True, |
23 | continue | 23 | help="Run the steps after the cryptoportfolio update") |
24 | if delta > 0: | 24 | parser.add_argument("--debug", |
25 | market.transfer_balance(currency, delta, "exchange", "margin") | 25 | default=False, action='store_const', const=True, |
26 | elif delta < 0: | 26 | help="Run in debug mode") |
27 | market.transfer_balance(currency, -delta, "margin", "exchange") | 27 | |
28 | ReportStore.log_move_balances(needed_in_margin, moving_to_margin, debug) | 28 | args = parser.parse_args(argv) |
29 | 29 | ||
30 | BalanceStore.fetch_balances(market) | 30 | if not os.path.exists(args.config): |
31 | 31 | print("no config file found, exiting") | |
32 | ticker_cache = {} | 32 | sys.exit(1) |
33 | ticker_cache_timestamp = time.time() | 33 | |
34 | def get_ticker(c1, c2, market, refresh=False): | 34 | return args |
35 | global ticker_cache, ticker_cache_timestamp | 35 | |
36 | def invert(ticker): | 36 | def main_parse_config(config_file): |
37 | return { | 37 | config = configparser.ConfigParser() |
38 | "inverted": True, | 38 | config.read(config_file) |
39 | "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2, | 39 | |
40 | "original": ticker, | 40 | if "postgresql" not in config: |
41 | } | 41 | print("no configuration for postgresql in config file") |
42 | def augment_ticker(ticker): | 42 | sys.exit(1) |
43 | ticker.update({ | 43 | |
44 | "inverted": False, | 44 | if "app" in config and "report_path" in config["app"]: |
45 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | 45 | report_path = config["app"]["report_path"] |
46 | }) | 46 | |
47 | 47 | if not os.path.exists(report_path): | |
48 | if time.time() - ticker_cache_timestamp > 5: | 48 | os.makedirs(report_path) |
49 | ticker_cache = {} | 49 | else: |
50 | ticker_cache_timestamp = time.time() | 50 | report_path = None |
51 | elif not refresh: | 51 | |
52 | if (c1, c2, market.__class__) in ticker_cache: | 52 | return [config["postgresql"], report_path] |
53 | return ticker_cache[(c1, c2, market.__class__)] | 53 | |
54 | if (c2, c1, market.__class__) in ticker_cache: | 54 | def main_fetch_markets(pg_config): |
55 | return invert(ticker_cache[(c2, c1, market.__class__)]) | 55 | connection = psycopg2.connect(**pg_config) |
56 | 56 | cursor = connection.cursor() | |
57 | |||
58 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
59 | |||
60 | for row in cursor: | ||
61 | yield row | ||
62 | |||
63 | def main_process_market(user_market, before=False, after=False): | ||
64 | if before: | ||
65 | process_sell_all__1_all_sell(user_market) | ||
66 | if after: | ||
67 | portfolio.Portfolio.wait_for_recent(user_market) | ||
68 | process_sell_all__2_all_buy(user_market) | ||
69 | |||
70 | def main_store_report(report_path, user_id, user_market): | ||
57 | try: | 71 | try: |
58 | ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) | 72 | if report_path is not None: |
59 | augment_ticker(ticker_cache[(c1, c2, market.__class__)]) | 73 | report_file = "{}/{}_{}.json".format(report_path, datetime.now().isoformat(), user_id) |
60 | except ExchangeError: | 74 | with open(report_file, "w") as f: |
61 | try: | 75 | f.write(user_market.report.to_json()) |
62 | ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) | 76 | except Exception as e: |
63 | augment_ticker(ticker_cache[(c2, c1, market.__class__)]) | 77 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) |
64 | except ExchangeError: | ||
65 | ticker_cache[(c1, c2, market.__class__)] = None | ||
66 | return get_ticker(c1, c2, market) | ||
67 | |||
68 | fees_cache = {} | ||
69 | def fetch_fees(market): | ||
70 | global fees_cache | ||
71 | if market.__class__ not in fees_cache: | ||
72 | fees_cache[market.__class__] = market.fetch_fees() | ||
73 | return fees_cache[market.__class__] | ||
74 | |||
75 | def prepare_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", debug=False): | ||
76 | ReportStore.log_stage("prepare_trades") | ||
77 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
78 | total_base_value = sum(values_in_base.values()) | ||
79 | new_repartition = BalanceStore.dispatch_assets(total_base_value, liquidity=liquidity) | ||
80 | # Recompute it in case we have new currencies | ||
81 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
82 | TradeStore.compute_trades(values_in_base, new_repartition, market=market, debug=debug) | ||
83 | |||
84 | def update_trades(market, base_currency="BTC", liquidity="medium", compute_value="average", only=None, debug=False): | ||
85 | ReportStore.log_stage("update_trades") | ||
86 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
87 | total_base_value = sum(values_in_base.values()) | ||
88 | new_repartition = BalanceStore.dispatch_assets(total_base_value, liquidity=liquidity) | ||
89 | TradeStore.compute_trades(values_in_base, new_repartition, only=only, market=market, debug=debug) | ||
90 | |||
91 | def prepare_trades_to_sell_all(market, base_currency="BTC", compute_value="average", debug=False): | ||
92 | ReportStore.log_stage("prepare_trades_to_sell_all") | ||
93 | values_in_base = BalanceStore.in_currency(base_currency, market, compute_value=compute_value) | ||
94 | total_base_value = sum(values_in_base.values()) | ||
95 | 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) | ||
97 | |||
98 | def follow_orders(sleep=None): | ||
99 | if sleep is None: | ||
100 | sleep = 7 if TradeStore.debug else 30 | ||
101 | if TradeStore.debug: | ||
102 | ReportStore.log_debug_action("Set follow_orders tick to {}s".format(sleep)) | ||
103 | tick = 0 | ||
104 | ReportStore.log_stage("follow_orders_begin") | ||
105 | while len(TradeStore.all_orders(state="open")) > 0: | ||
106 | time.sleep(sleep) | ||
107 | tick += 1 | ||
108 | open_orders = TradeStore.all_orders(state="open") | ||
109 | ReportStore.log_stage("follow_orders_tick_{}".format(tick)) | ||
110 | ReportStore.log_orders(open_orders, tick=tick) | ||
111 | for order in open_orders: | ||
112 | if order.get_status() != "open": | ||
113 | ReportStore.log_order(order, tick, finished=True) | ||
114 | else: | ||
115 | order.trade.update_order(order, tick) | ||
116 | ReportStore.log_stage("follow_orders_end") | ||
117 | 78 | ||
118 | def print_orders(market, base_currency="BTC"): | 79 | def print_orders(market, base_currency="BTC"): |
119 | ReportStore.log_stage("print_orders") | 80 | market.report.log_stage("print_orders") |
120 | BalanceStore.fetch_balances(market, tag="print_orders") | 81 | market.balances.fetch_balances(tag="print_orders") |
121 | prepare_trades(market, base_currency=base_currency, compute_value="average", debug=True) | 82 | market.prepare_trades(base_currency=base_currency, compute_value="average") |
122 | TradeStore.prepare_orders(compute_value="average") | 83 | market.trades.prepare_orders(compute_value="average") |
123 | 84 | ||
124 | def print_balances(market, base_currency="BTC"): | 85 | def print_balances(market, base_currency="BTC"): |
125 | BalanceStore.fetch_balances(market) | 86 | market.balances.fetch_balances() |
126 | if base_currency is not None: | 87 | if base_currency is not None: |
127 | ReportStore.print_log("total:") | 88 | market.report.print_log("total:") |
128 | ReportStore.print_log(sum(BalanceStore.in_currency(base_currency, market).values())) | 89 | market.report.print_log(sum(market.balances.in_currency(base_currency).values())) |
129 | 90 | ||
130 | def reset_all(): | 91 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC"): |
131 | # use them as regular classes, sub-object of market | 92 | market.report.log_stage("process_sell_needed__1_sell_begin") |
132 | ReportStore.logs = [] | 93 | market.balances.fetch_balances(tag="process_sell_needed__1_sell_begin") |
133 | BalanceStore.all = {} | 94 | market.prepare_trades(liquidity=liquidity, base_currency=base_currency) |
134 | TradeStore.all = [] | 95 | market.trades.prepare_orders(compute_value="average", only="dispose") |
135 | 96 | market.trades.run_orders() | |
136 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC", debug=False): | 97 | market.follow_orders() |
137 | ReportStore.log_stage("process_sell_needed__1_sell_begin") | 98 | market.balances.fetch_balances(tag="process_sell_needed__1_sell_end") |
138 | BalanceStore.fetch_balances(market, tag="process_sell_needed__1_sell_begin") | 99 | market.report.log_stage("process_sell_needed__1_sell_end") |
139 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) | 100 | |
140 | TradeStore.prepare_orders(compute_value="average", only="dispose") | 101 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC"): |
141 | TradeStore.run_orders() | 102 | market.report.log_stage("process_sell_needed__2_buy_begin") |
142 | follow_orders() | 103 | market.balances.fetch_balances(tag="process_sell_needed__2_buy_begin") |
143 | BalanceStore.fetch_balances(market, tag="process_sell_needed__1_sell_end") | 104 | market.update_trades(base_currency=base_currency, liquidity=liquidity, only="acquire") |
144 | ReportStore.log_stage("process_sell_needed__1_sell_end") | 105 | market.trades.prepare_orders(compute_value="average", only="acquire") |
145 | 106 | market.move_balances() | |
146 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC", debug=False): | 107 | market.trades.run_orders() |
147 | ReportStore.log_stage("process_sell_needed__2_buy_begin") | 108 | market.follow_orders() |
148 | BalanceStore.fetch_balances(market, tag="process_sell_needed__2_buy_begin") | 109 | market.balances.fetch_balances(tag="process_sell_needed__2_buy_end") |
149 | update_trades(market, base_currency=base_currency, liquidity=liquidity, debug=debug, only="acquire") | 110 | market.report.log_stage("process_sell_needed__2_buy_end") |
150 | TradeStore.prepare_orders(compute_value="average", only="acquire") | 111 | |
151 | move_balances(market, debug=debug) | 112 | def process_sell_all__1_all_sell(market, base_currency="BTC", liquidity="medium"): |
152 | TradeStore.run_orders() | 113 | market.report.log_stage("process_sell_all__1_all_sell_begin") |
153 | follow_orders() | 114 | market.balances.fetch_balances(tag="process_sell_all__1_all_sell_begin") |
154 | BalanceStore.fetch_balances(market, tag="process_sell_needed__2_buy_end") | 115 | market.prepare_trades_to_sell_all(base_currency=base_currency) |
155 | ReportStore.log_stage("process_sell_needed__2_buy_end") | 116 | market.trades.prepare_orders(compute_value="average") |
156 | 117 | market.trades.run_orders() | |
157 | def process_sell_all__1_all_sell(market, base_currency="BTC", debug=False, liquidity="medium"): | 118 | market.follow_orders() |
158 | ReportStore.log_stage("process_sell_all__1_all_sell_begin") | 119 | market.balances.fetch_balances(tag="process_sell_all__1_all_sell_end") |
159 | BalanceStore.fetch_balances(market, tag="process_sell_all__1_all_sell_begin") | 120 | market.report.log_stage("process_sell_all__1_all_sell_end") |
160 | prepare_trades_to_sell_all(market, base_currency=base_currency, debug=debug) | 121 | |
161 | TradeStore.prepare_orders(compute_value="average") | 122 | def process_sell_all__2_all_buy(market, base_currency="BTC", liquidity="medium"): |
162 | TradeStore.run_orders() | 123 | market.report.log_stage("process_sell_all__2_all_buy_begin") |
163 | follow_orders() | 124 | market.balances.fetch_balances(tag="process_sell_all__2_all_buy_begin") |
164 | BalanceStore.fetch_balances(market, tag="process_sell_all__1_all_sell_end") | 125 | market.prepare_trades(liquidity=liquidity, base_currency=base_currency) |
165 | ReportStore.log_stage("process_sell_all__1_all_sell_end") | 126 | market.trades.prepare_orders(compute_value="average") |
166 | 127 | market.move_balances() | |
167 | def process_sell_all__2_all_buy(market, base_currency="BTC", debug=False, liquidity="medium"): | 128 | market.trades.run_orders() |
168 | ReportStore.log_stage("process_sell_all__2_all_buy_begin") | 129 | market.follow_orders() |
169 | BalanceStore.fetch_balances(market, tag="process_sell_all__2_all_buy_begin") | 130 | market.balances.fetch_balances(tag="process_sell_all__2_all_buy_end") |
170 | prepare_trades(market, liquidity=liquidity, base_currency=base_currency, debug=debug) | 131 | market.report.log_stage("process_sell_all__2_all_buy_end") |
171 | TradeStore.prepare_orders(compute_value="average") | ||
172 | move_balances(market, debug=debug) | ||
173 | TradeStore.run_orders() | ||
174 | follow_orders() | ||
175 | BalanceStore.fetch_balances(market, tag="process_sell_all__2_all_buy_end") | ||
176 | ReportStore.log_stage("process_sell_all__2_all_buy_end") | ||
177 | 132 | ||
178 | 133 | ||
@@ -1,64 +1,16 @@ | |||
1 | import os | ||
2 | import sys | 1 | import sys |
3 | import configparser | 2 | import helper, market |
4 | import psycopg2 | ||
5 | import argparse | ||
6 | from datetime import datetime | ||
7 | 3 | ||
8 | import portfolio, market | 4 | args = helper.main_parse_args(sys.argv[1:]) |
9 | 5 | ||
10 | parser = argparse.ArgumentParser( | 6 | pg_config, report_path = helper.main_parse_config(args.config) |
11 | description="Run the trade bot") | ||
12 | 7 | ||
13 | parser.add_argument("-c", "--config", | 8 | for market_config, user_id in helper.main_fetch_markets(pg_config): |
14 | default="config.ini", | ||
15 | required=False, | ||
16 | help="Config file to load (default: config.ini)") | ||
17 | parser.add_argument("--before", | ||
18 | default=False, action='store_const', const=True, | ||
19 | help="Run the steps before the cryptoportfolio update") | ||
20 | parser.add_argument("--after", | ||
21 | default=False, action='store_const', const=True, | ||
22 | help="Run the steps after the cryptoportfolio update") | ||
23 | parser.add_argument("--debug", | ||
24 | default=False, action='store_const', const=True, | ||
25 | help="Run in debug mode") | ||
26 | |||
27 | args = parser.parse_args() | ||
28 | |||
29 | if not os.path.exists(args.config): | ||
30 | print("no config file found, exiting") | ||
31 | sys.exit(1) | ||
32 | |||
33 | config = configparser.ConfigParser() | ||
34 | config.read(args.config) | ||
35 | |||
36 | pg_config = config["postgresql"] | ||
37 | |||
38 | connection = psycopg2.connect(**pg_config) | ||
39 | cursor = connection.cursor() | ||
40 | |||
41 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
42 | |||
43 | report_path = config["app"]["report_path"] | ||
44 | if not os.path.exists(report_path): | ||
45 | os.makedirs(report_path) | ||
46 | |||
47 | for row in cursor: | ||
48 | market_config, user_id = row | ||
49 | try: | 9 | try: |
50 | user_market = market.get_market(market_config) | 10 | market_config["apiKey"] = market_config.pop("key") |
51 | if args.before: | 11 | user_market = market.Market.from_config(market_config, debug=args.debug) |
52 | portfolio.h.process_sell_all__1_all_sell(user_market, debug=args.debug) | 12 | helper.main_process_market(user_market, before=args.before, after=args.after) |
53 | if args.after: | ||
54 | portfolio.Portfolio.wait_for_recent() | ||
55 | portfolio.h.process_sell_all__2_all_buy(user_market, debug=args.debug) | ||
56 | except Exception as e: | 13 | except Exception as e: |
57 | print(e) | 14 | print("{}: {}".format(e.__class__.__name__, e)) |
58 | pass | ||
59 | finally: | 15 | finally: |
60 | report_file = "{}/{}_{}.json".format(report_path, datetime.now().isoformat(), user_id) | 16 | helper.main_store_report(report_path, user_id, user_market) |
61 | with open(report_file, "w") as f: | ||
62 | f.write(portfolio.ReportStore.to_json()) | ||
63 | portfolio.h.reset_all() | ||
64 | |||
@@ -1,17 +1,150 @@ | |||
1 | from ccxt import ExchangeError | ||
1 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
2 | from store import ReportStore | 3 | import time |
4 | from store import * | ||
3 | 5 | ||
4 | def get_market(config): | 6 | class Market: |
5 | market = ccxt.poloniexE(config) | 7 | debug = False |
8 | ccxt = None | ||
9 | report = None | ||
10 | trades = None | ||
11 | balances = None | ||
6 | 12 | ||
7 | # For requests logging | 13 | def __init__(self, ccxt_instance, debug=False): |
8 | market.session.origin_request = market.session.request | 14 | self.debug = debug |
15 | self.ccxt = ccxt_instance | ||
16 | self.ccxt._market = self | ||
17 | self.report = ReportStore(self) | ||
18 | self.trades = TradeStore(self) | ||
19 | self.balances = BalanceStore(self) | ||
20 | |||
21 | @classmethod | ||
22 | def from_config(cls, config, debug=False): | ||
23 | ccxt_instance = ccxt.poloniexE(config) | ||
24 | |||
25 | # For requests logging | ||
26 | ccxt_instance.session.origin_request = ccxt_instance.session.request | ||
27 | ccxt_instance.session._parent = ccxt_instance | ||
28 | |||
29 | def request_wrap(self, *args, **kwargs): | ||
30 | r = self.origin_request(*args, **kwargs) | ||
31 | self._parent._market.report.log_http_request(args[0], | ||
32 | args[1], kwargs["data"], kwargs["headers"], r) | ||
33 | return r | ||
34 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, | ||
35 | ccxt_instance.session.__class__) | ||
36 | |||
37 | return cls(ccxt_instance, debug=debug) | ||
38 | |||
39 | def move_balances(self): | ||
40 | needed_in_margin = {} | ||
41 | moving_to_margin = {} | ||
42 | |||
43 | for currency in self.balances.all: | ||
44 | if self.balances.all[currency].margin_free != 0: | ||
45 | needed_in_margin[currency] = 0 | ||
46 | for trade in self.trades.all: | ||
47 | if trade.value_to.currency not in needed_in_margin: | ||
48 | needed_in_margin[trade.value_to.currency] = 0 | ||
49 | if trade.trade_type == "short": | ||
50 | needed_in_margin[trade.value_to.currency] += abs(trade.value_to) | ||
51 | for currency, needed in needed_in_margin.items(): | ||
52 | current_balance = self.balances.all[currency].margin_free | ||
53 | moving_to_margin[currency] = (needed - current_balance) | ||
54 | delta = moving_to_margin[currency].value | ||
55 | if self.debug: | ||
56 | self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) | ||
57 | continue | ||
58 | if delta > 0: | ||
59 | self.ccxt.transfer_balance(currency, delta, "exchange", "margin") | ||
60 | elif delta < 0: | ||
61 | self.ccxt.transfer_balance(currency, -delta, "margin", "exchange") | ||
62 | self.report.log_move_balances(needed_in_margin, moving_to_margin) | ||
63 | |||
64 | self.balances.fetch_balances() | ||
65 | |||
66 | fees_cache = {} | ||
67 | def fetch_fees(self): | ||
68 | if self.ccxt.__class__ not in self.fees_cache: | ||
69 | self.fees_cache[self.ccxt.__class__] = self.ccxt.fetch_fees() | ||
70 | return self.fees_cache[self.ccxt.__class__] | ||
71 | |||
72 | ticker_cache = {} | ||
73 | ticker_cache_timestamp = time.time() | ||
74 | def get_ticker(self, c1, c2, refresh=False): | ||
75 | def invert(ticker): | ||
76 | return { | ||
77 | "inverted": True, | ||
78 | "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2, | ||
79 | "original": ticker, | ||
80 | } | ||
81 | def augment_ticker(ticker): | ||
82 | ticker.update({ | ||
83 | "inverted": False, | ||
84 | "average": (ticker["bid"] + ticker["ask"] ) / 2, | ||
85 | }) | ||
86 | |||
87 | if time.time() - self.ticker_cache_timestamp > 5: | ||
88 | self.ticker_cache = {} | ||
89 | self.ticker_cache_timestamp = time.time() | ||
90 | elif not refresh: | ||
91 | if (c1, c2, self.ccxt.__class__) in self.ticker_cache: | ||
92 | return self.ticker_cache[(c1, c2, self.ccxt.__class__)] | ||
93 | if (c2, c1, self.ccxt.__class__) in self.ticker_cache: | ||
94 | return invert(self.ticker_cache[(c2, c1, self.ccxt.__class__)]) | ||
95 | |||
96 | try: | ||
97 | self.ticker_cache[(c1, c2, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c1, c2)) | ||
98 | augment_ticker(self.ticker_cache[(c1, c2, self.ccxt.__class__)]) | ||
99 | except ExchangeError: | ||
100 | try: | ||
101 | self.ticker_cache[(c2, c1, self.ccxt.__class__)] = self.ccxt.fetch_ticker("{}/{}".format(c2, c1)) | ||
102 | augment_ticker(self.ticker_cache[(c2, c1, self.ccxt.__class__)]) | ||
103 | except ExchangeError: | ||
104 | self.ticker_cache[(c1, c2, self.ccxt.__class__)] = None | ||
105 | return self.get_ticker(c1, c2) | ||
106 | |||
107 | def follow_orders(self, sleep=None): | ||
108 | if sleep is None: | ||
109 | sleep = 7 if self.debug else 30 | ||
110 | if self.debug: | ||
111 | self.report.log_debug_action("Set follow_orders tick to {}s".format(sleep)) | ||
112 | tick = 0 | ||
113 | self.report.log_stage("follow_orders_begin") | ||
114 | while len(self.trades.all_orders(state="open")) > 0: | ||
115 | time.sleep(sleep) | ||
116 | tick += 1 | ||
117 | open_orders = self.trades.all_orders(state="open") | ||
118 | self.report.log_stage("follow_orders_tick_{}".format(tick)) | ||
119 | self.report.log_orders(open_orders, tick=tick) | ||
120 | for order in open_orders: | ||
121 | if order.get_status() != "open": | ||
122 | self.report.log_order(order, tick, finished=True) | ||
123 | else: | ||
124 | order.trade.update_order(order, tick) | ||
125 | self.report.log_stage("follow_orders_end") | ||
126 | |||
127 | def prepare_trades(self, base_currency="BTC", liquidity="medium", compute_value="average"): | ||
128 | self.report.log_stage("prepare_trades") | ||
129 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | ||
130 | total_base_value = sum(values_in_base.values()) | ||
131 | new_repartition = self.balances.dispatch_assets(total_base_value, liquidity=liquidity) | ||
132 | # Recompute it in case we have new currencies | ||
133 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | ||
134 | self.trades.compute_trades(values_in_base, new_repartition) | ||
135 | |||
136 | def update_trades(self, base_currency="BTC", liquidity="medium", compute_value="average", only=None): | ||
137 | self.report.log_stage("update_trades") | ||
138 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | ||
139 | total_base_value = sum(values_in_base.values()) | ||
140 | new_repartition = self.balances.dispatch_assets(total_base_value, liquidity=liquidity) | ||
141 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | ||
142 | |||
143 | def prepare_trades_to_sell_all(self, base_currency="BTC", compute_value="average"): | ||
144 | self.report.log_stage("prepare_trades_to_sell_all") | ||
145 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | ||
146 | total_base_value = sum(values_in_base.values()) | ||
147 | new_repartition = self.balances.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | ||
148 | self.trades.compute_trades(values_in_base, new_repartition) | ||
9 | 149 | ||
10 | def request_wrap(self, *args, **kwargs): | ||
11 | r = self.origin_request(*args, **kwargs) | ||
12 | ReportStore.log_http_request(args[0], args[1], kwargs["data"], | ||
13 | kwargs["headers"], r) | ||
14 | return r | ||
15 | market.session.request = request_wrap.__get__(market.session, market.session.__class__) | ||
16 | 150 | ||
17 | return market | ||
diff --git a/portfolio.py b/portfolio.py index f9423b9..43a39c4 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,13 +1,10 @@ | |||
1 | import time | 1 | import time |
2 | from datetime import datetime, timedelta | 2 | 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 | ||
5 | from json import JSONDecodeError | 4 | from json import JSONDecodeError |
6 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | 5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError |
7 | from ccxt import ExchangeError, ExchangeNotAvailable | 6 | from ccxt import ExchangeError, ExchangeNotAvailable |
8 | import requests | 7 | import requests |
9 | import helper as h | ||
10 | from store import * | ||
11 | 8 | ||
12 | # FIXME: correctly handle web call timeouts | 9 | # FIXME: correctly handle web call timeouts |
13 | 10 | ||
@@ -18,26 +15,27 @@ class Portfolio: | |||
18 | last_date = None | 15 | last_date = None |
19 | 16 | ||
20 | @classmethod | 17 | @classmethod |
21 | def wait_for_recent(cls, delta=4): | 18 | def wait_for_recent(cls, market, delta=4): |
22 | cls.repartition(refetch=True) | 19 | cls.repartition(market, refetch=True) |
23 | while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta): | 20 | while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta): |
24 | time.sleep(30) | 21 | time.sleep(30) |
25 | cls.repartition(refetch=True) | 22 | market.report.print_log("Attempt to fetch up-to-date cryptoportfolio") |
23 | cls.repartition(market, refetch=True) | ||
26 | 24 | ||
27 | @classmethod | 25 | @classmethod |
28 | def repartition(cls, liquidity="medium", refetch=False): | 26 | def repartition(cls, market, liquidity="medium", refetch=False): |
29 | cls.parse_cryptoportfolio(refetch=refetch) | 27 | cls.parse_cryptoportfolio(market, refetch=refetch) |
30 | liquidities = cls.liquidities[liquidity] | 28 | liquidities = cls.liquidities[liquidity] |
31 | return liquidities[cls.last_date] | 29 | return liquidities[cls.last_date] |
32 | 30 | ||
33 | @classmethod | 31 | @classmethod |
34 | def get_cryptoportfolio(cls): | 32 | def get_cryptoportfolio(cls, market): |
35 | try: | 33 | try: |
36 | r = requests.get(cls.URL) | 34 | r = requests.get(cls.URL) |
37 | ReportStore.log_http_request(r.request.method, | 35 | market.report.log_http_request(r.request.method, |
38 | r.request.url, r.request.body, r.request.headers, r) | 36 | r.request.url, r.request.body, r.request.headers, r) |
39 | except Exception as e: | 37 | except Exception as e: |
40 | ReportStore.log_error("get_cryptoportfolio", exception=e) | 38 | market.report.log_error("get_cryptoportfolio", exception=e) |
41 | return | 39 | return |
42 | try: | 40 | try: |
43 | cls.data = r.json(parse_int=D, parse_float=D) | 41 | cls.data = r.json(parse_int=D, parse_float=D) |
@@ -45,9 +43,9 @@ class Portfolio: | |||
45 | cls.data = None | 43 | cls.data = None |
46 | 44 | ||
47 | @classmethod | 45 | @classmethod |
48 | def parse_cryptoportfolio(cls, refetch=False): | 46 | def parse_cryptoportfolio(cls, market, refetch=False): |
49 | if refetch or cls.data is None: | 47 | if refetch or cls.data is None: |
50 | cls.get_cryptoportfolio() | 48 | cls.get_cryptoportfolio(market) |
51 | 49 | ||
52 | def filter_weights(weight_hash): | 50 | def filter_weights(weight_hash): |
53 | if weight_hash[1][0] == 0: | 51 | if weight_hash[1][0] == 0: |
@@ -118,7 +116,7 @@ class Amount: | |||
118 | self.value * rate, | 116 | self.value * rate, |
119 | linked_to=self, | 117 | linked_to=self, |
120 | rate=rate) | 118 | rate=rate) |
121 | asset_ticker = h.get_ticker(self.currency, other_currency, market) | 119 | asset_ticker = market.get_ticker(self.currency, other_currency) |
122 | if asset_ticker is not None: | 120 | if asset_ticker is not None: |
123 | rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value) | 121 | rate = Computation.compute_value(asset_ticker, action, compute_value=compute_value) |
124 | return Amount( | 122 | return Amount( |
@@ -283,7 +281,7 @@ class Balance: | |||
283 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" | 281 | return "Balance({}".format(self.currency) + "".join([exchange, margin, total]) + ")" |
284 | 282 | ||
285 | class Trade: | 283 | class Trade: |
286 | def __init__(self, value_from, value_to, currency, market=None): | 284 | def __init__(self, value_from, value_to, currency, market): |
287 | # We have value_from of currency, and want to finish with value_to of | 285 | # We have value_from of currency, and want to finish with value_to of |
288 | # that currency. value_* may not be in currency's terms | 286 | # that currency. value_* may not be in currency's terms |
289 | self.currency = currency | 287 | self.currency = currency |
@@ -353,18 +351,18 @@ class Trade: | |||
353 | if tick == 7: | 351 | if tick == 7: |
354 | update = "market_fallback" | 352 | update = "market_fallback" |
355 | 353 | ||
356 | ReportStore.log_order(order, tick, update=update, | 354 | self.market.report.log_order(order, tick, update=update, |
357 | compute_value=compute_value, new_order=new_order) | 355 | compute_value=compute_value, new_order=new_order) |
358 | 356 | ||
359 | if new_order is not None: | 357 | if new_order is not None: |
360 | order.cancel() | 358 | order.cancel() |
361 | new_order.run() | 359 | new_order.run() |
362 | ReportStore.log_order(order, tick, new_order=new_order) | 360 | self.market.report.log_order(order, tick, new_order=new_order) |
363 | 361 | ||
364 | def prepare_order(self, compute_value="default"): | 362 | def prepare_order(self, compute_value="default"): |
365 | if self.action is None: | 363 | if self.action is None: |
366 | return None | 364 | return None |
367 | ticker = h.get_ticker(self.currency, self.base_currency, self.market) | 365 | ticker = self.market.get_ticker(self.currency, self.base_currency) |
368 | inverted = ticker["inverted"] | 366 | inverted = ticker["inverted"] |
369 | if inverted: | 367 | if inverted: |
370 | ticker = ticker["original"] | 368 | ticker = ticker["original"] |
@@ -430,7 +428,7 @@ class Trade: | |||
430 | close_if_possible = (self.value_to == 0) | 428 | close_if_possible = (self.value_to == 0) |
431 | 429 | ||
432 | if delta <= 0: | 430 | if delta <= 0: |
433 | ReportStore.log_error("prepare_order", message="Less to do than already filled: {}".format(delta)) | 431 | self.market.report.log_error("prepare_order", message="Less to do than already filled: {}".format(delta)) |
434 | return None | 432 | return None |
435 | 433 | ||
436 | order = Order(self.order_action(inverted), | 434 | order = Order(self.order_action(inverted), |
@@ -456,11 +454,11 @@ class Trade: | |||
456 | self.action) | 454 | self.action) |
457 | 455 | ||
458 | def print_with_order(self, ind=""): | 456 | def print_with_order(self, ind=""): |
459 | ReportStore.print_log("{}{}".format(ind, self)) | 457 | self.market.report.print_log("{}{}".format(ind, self)) |
460 | for order in self.orders: | 458 | for order in self.orders: |
461 | ReportStore.print_log("{}\t{}".format(ind, order)) | 459 | self.market.report.print_log("{}\t{}".format(ind, order)) |
462 | for mouvement in order.mouvements: | 460 | for mouvement in order.mouvements: |
463 | ReportStore.print_log("{}\t\t{}".format(ind, mouvement)) | 461 | self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) |
464 | 462 | ||
465 | class Order: | 463 | class Order: |
466 | def __init__(self, action, amount, rate, base_currency, trade_type, market, | 464 | def __init__(self, action, amount, rate, base_currency, trade_type, market, |
@@ -525,15 +523,15 @@ class Order: | |||
525 | 523 | ||
526 | def run(self): | 524 | def run(self): |
527 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 525 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
528 | amount = round(self.amount, self.market.order_precision(symbol)).value | 526 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value |
529 | 527 | ||
530 | if TradeStore.debug: | 528 | if self.market.debug: |
531 | ReportStore.log_debug_action("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 529 | self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( |
532 | symbol, self.action, amount, self.rate, self.account)) | 530 | symbol, self.action, amount, self.rate, self.account)) |
533 | self.results.append({"debug": True, "id": -1}) | 531 | self.results.append({"debug": True, "id": -1}) |
534 | else: | 532 | else: |
535 | try: | 533 | try: |
536 | self.results.append(self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) | 534 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) |
537 | except ExchangeNotAvailable: | 535 | except ExchangeNotAvailable: |
538 | # Impossible to honor the order (dust amount) | 536 | # Impossible to honor the order (dust amount) |
539 | self.status = "closed" | 537 | self.status = "closed" |
@@ -541,15 +539,15 @@ class Order: | |||
541 | return | 539 | return |
542 | except Exception as e: | 540 | except Exception as e: |
543 | self.status = "error" | 541 | self.status = "error" |
544 | action = "market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | 542 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) |
545 | ReportStore.log_error(action, exception=e) | 543 | self.market.report.log_error(action, exception=e) |
546 | return | 544 | return |
547 | self.id = self.results[0]["id"] | 545 | self.id = self.results[0]["id"] |
548 | self.status = "open" | 546 | self.status = "open" |
549 | 547 | ||
550 | def get_status(self): | 548 | def get_status(self): |
551 | if TradeStore.debug: | 549 | if self.market.debug: |
552 | ReportStore.log_debug_action("Getting {} status".format(self)) | 550 | self.market.report.log_debug_action("Getting {} status".format(self)) |
553 | return self.status | 551 | return self.status |
554 | # other states are "closed" and "canceled" | 552 | # other states are "closed" and "canceled" |
555 | if not self.finished: | 553 | if not self.finished: |
@@ -559,23 +557,23 @@ class Order: | |||
559 | return self.status | 557 | return self.status |
560 | 558 | ||
561 | def mark_finished_order(self): | 559 | def mark_finished_order(self): |
562 | if TradeStore.debug: | 560 | if self.market.debug: |
563 | ReportStore.log_debug_action("Mark {} as finished".format(self)) | 561 | self.market.report.log_debug_action("Mark {} as finished".format(self)) |
564 | return | 562 | return |
565 | if self.status == "closed": | 563 | if self.status == "closed": |
566 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: | 564 | if self.trade_type == "short" and self.action == "buy" and self.close_if_possible: |
567 | self.market.close_margin_position(self.amount.currency, self.base_currency) | 565 | self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency) |
568 | 566 | ||
569 | def fetch(self, force=False): | 567 | def fetch(self, force=False): |
570 | if TradeStore.debug: | 568 | if self.market.debug: |
571 | ReportStore.log_debug_action("Fetching {}".format(self)) | 569 | self.market.report.log_debug_action("Fetching {}".format(self)) |
572 | return | 570 | return |
573 | if (not force and self.fetch_cache_timestamp is not None | 571 | if (not force and self.fetch_cache_timestamp is not None |
574 | and time.time() - self.fetch_cache_timestamp < 10): | 572 | and time.time() - self.fetch_cache_timestamp < 10): |
575 | return | 573 | return |
576 | self.fetch_cache_timestamp = time.time() | 574 | self.fetch_cache_timestamp = time.time() |
577 | 575 | ||
578 | result = self.market.fetch_order(self.id) | 576 | result = self.market.ccxt.fetch_order(self.id) |
579 | self.results.append(result) | 577 | self.results.append(result) |
580 | 578 | ||
581 | self.status = result["status"] | 579 | self.status = result["status"] |
@@ -606,7 +604,7 @@ class Order: | |||
606 | 604 | ||
607 | def fetch_mouvements(self): | 605 | def fetch_mouvements(self): |
608 | try: | 606 | try: |
609 | mouvements = self.market.privatePostReturnOrderTrades({"orderNumber": self.id}) | 607 | mouvements = self.market.ccxt.privatePostReturnOrderTrades({"orderNumber": self.id}) |
610 | except ExchangeError: | 608 | except ExchangeError: |
611 | mouvements = [] | 609 | mouvements = [] |
612 | self.mouvements = [] | 610 | self.mouvements = [] |
@@ -616,11 +614,11 @@ class Order: | |||
616 | self.base_currency, mouvement_hash)) | 614 | self.base_currency, mouvement_hash)) |
617 | 615 | ||
618 | def cancel(self): | 616 | def cancel(self): |
619 | if TradeStore.debug: | 617 | if self.market.debug: |
620 | ReportStore.log_debug_action("Mark {} as cancelled".format(self)) | 618 | self.market.report.log_debug_action("Mark {} as cancelled".format(self)) |
621 | self.status = "canceled" | 619 | self.status = "canceled" |
622 | return | 620 | return |
623 | self.market.cancel_order(self.id) | 621 | self.market.ccxt.cancel_order(self.id) |
624 | self.fetch() | 622 | self.fetch() |
625 | 623 | ||
626 | class Mouvement: | 624 | class Mouvement: |
@@ -663,6 +661,3 @@ class Mouvement: | |||
663 | date, self.action, self.total, self.total_in_base, | 661 | date, self.action, self.total, self.total_in_base, |
664 | fee_rate) | 662 | fee_rate) |
665 | 663 | ||
666 | if __name__ == '__main__': # pragma: no cover | ||
667 | from market import market | ||
668 | h.print_orders(market) | ||
@@ -6,63 +6,59 @@ from datetime import date, datetime | |||
6 | __all__ = ["BalanceStore", "ReportStore", "TradeStore"] | 6 | __all__ = ["BalanceStore", "ReportStore", "TradeStore"] |
7 | 7 | ||
8 | class ReportStore: | 8 | class ReportStore: |
9 | logs = [] | 9 | def __init__(self, market, verbose_print=True): |
10 | verbose_print = True | 10 | self.market = market |
11 | self.verbose_print = verbose_print | ||
11 | 12 | ||
12 | @classmethod | 13 | self.logs = [] |
13 | def print_log(cls, message): | 14 | |
15 | def print_log(self, message): | ||
14 | message = str(message) | 16 | message = str(message) |
15 | if cls.verbose_print: | 17 | if self.verbose_print: |
16 | print(message) | 18 | print(message) |
17 | 19 | ||
18 | @classmethod | 20 | def add_log(self, hash_): |
19 | def add_log(cls, hash_): | ||
20 | hash_["date"] = datetime.now() | 21 | hash_["date"] = datetime.now() |
21 | cls.logs.append(hash_) | 22 | self.logs.append(hash_) |
22 | 23 | ||
23 | @classmethod | 24 | def to_json(self): |
24 | def to_json(cls): | ||
25 | def default_json_serial(obj): | 25 | def default_json_serial(obj): |
26 | if isinstance(obj, (datetime, date)): | 26 | if isinstance(obj, (datetime, date)): |
27 | return obj.isoformat() | 27 | return obj.isoformat() |
28 | raise TypeError ("Type %s not serializable" % type(obj)) | 28 | raise TypeError ("Type %s not serializable" % type(obj)) |
29 | return json.dumps(cls.logs, default=default_json_serial) | 29 | return json.dumps(self.logs, default=default_json_serial) |
30 | 30 | ||
31 | @classmethod | 31 | def set_verbose(self, verbose_print): |
32 | def set_verbose(cls, verbose_print): | 32 | self.verbose_print = verbose_print |
33 | cls.verbose_print = verbose_print | ||
34 | 33 | ||
35 | @classmethod | 34 | def log_stage(self, stage): |
36 | def log_stage(cls, stage): | 35 | self.print_log("-" * (len(stage) + 8)) |
37 | cls.print_log("-" * (len(stage) + 8)) | 36 | self.print_log("[Stage] {}".format(stage)) |
38 | cls.print_log("[Stage] {}".format(stage)) | ||
39 | 37 | ||
40 | cls.add_log({ | 38 | self.add_log({ |
41 | "type": "stage", | 39 | "type": "stage", |
42 | "stage": stage, | 40 | "stage": stage, |
43 | }) | 41 | }) |
44 | 42 | ||
45 | @classmethod | 43 | def log_balances(self, tag=None): |
46 | def log_balances(cls, market, tag=None): | 44 | self.print_log("[Balance]") |
47 | cls.print_log("[Balance]") | 45 | for currency, balance in self.market.balances.all.items(): |
48 | for currency, balance in BalanceStore.all.items(): | 46 | self.print_log("\t{}".format(balance)) |
49 | cls.print_log("\t{}".format(balance)) | ||
50 | 47 | ||
51 | cls.add_log({ | 48 | self.add_log({ |
52 | "type": "balance", | 49 | "type": "balance", |
53 | "tag": tag, | 50 | "tag": tag, |
54 | "balances": BalanceStore.as_json() | 51 | "balances": self.market.balances.as_json() |
55 | }) | 52 | }) |
56 | 53 | ||
57 | @classmethod | 54 | def log_tickers(self, amounts, other_currency, |
58 | def log_tickers(cls, market, amounts, other_currency, | ||
59 | compute_value, type): | 55 | compute_value, type): |
60 | values = {} | 56 | values = {} |
61 | rates = {} | 57 | rates = {} |
62 | for currency, amount in amounts.items(): | 58 | for currency, amount in amounts.items(): |
63 | values[currency] = amount.as_json()["value"] | 59 | values[currency] = amount.as_json()["value"] |
64 | rates[currency] = amount.rate | 60 | rates[currency] = amount.rate |
65 | cls.add_log({ | 61 | self.add_log({ |
66 | "type": "tickers", | 62 | "type": "tickers", |
67 | "compute_value": compute_value, | 63 | "compute_value": compute_value, |
68 | "balance_type": type, | 64 | "balance_type": type, |
@@ -72,9 +68,8 @@ class ReportStore: | |||
72 | "total": sum(amounts.values()).as_json()["value"] | 68 | "total": sum(amounts.values()).as_json()["value"] |
73 | }) | 69 | }) |
74 | 70 | ||
75 | @classmethod | 71 | def log_dispatch(self, amount, amounts, liquidity, repartition): |
76 | def log_dispatch(cls, amount, amounts, liquidity, repartition): | 72 | self.add_log({ |
77 | cls.add_log({ | ||
78 | "type": "dispatch", | 73 | "type": "dispatch", |
79 | "liquidity": liquidity, | 74 | "liquidity": liquidity, |
80 | "repartition_ratio": repartition, | 75 | "repartition_ratio": repartition, |
@@ -82,26 +77,24 @@ class ReportStore: | |||
82 | "repartition": { k: v.as_json()["value"] for k, v in amounts.items() } | 77 | "repartition": { k: v.as_json()["value"] for k, v in amounts.items() } |
83 | }) | 78 | }) |
84 | 79 | ||
85 | @classmethod | 80 | def log_trades(self, matching_and_trades, only): |
86 | def log_trades(cls, matching_and_trades, only, debug): | ||
87 | trades = [] | 81 | trades = [] |
88 | for matching, trade in matching_and_trades: | 82 | for matching, trade in matching_and_trades: |
89 | trade_json = trade.as_json() | 83 | trade_json = trade.as_json() |
90 | trade_json["skipped"] = not matching | 84 | trade_json["skipped"] = not matching |
91 | trades.append(trade_json) | 85 | trades.append(trade_json) |
92 | 86 | ||
93 | cls.add_log({ | 87 | self.add_log({ |
94 | "type": "trades", | 88 | "type": "trades", |
95 | "only": only, | 89 | "only": only, |
96 | "debug": debug, | 90 | "debug": self.market.debug, |
97 | "trades": trades | 91 | "trades": trades |
98 | }) | 92 | }) |
99 | 93 | ||
100 | @classmethod | 94 | def log_orders(self, orders, tick=None, only=None, compute_value=None): |
101 | def log_orders(cls, orders, tick=None, only=None, compute_value=None): | 95 | self.print_log("[Orders]") |
102 | cls.print_log("[Orders]") | 96 | self.market.trades.print_all_with_order(ind="\t") |
103 | TradeStore.print_all_with_order(ind="\t") | 97 | self.add_log({ |
104 | cls.add_log({ | ||
105 | "type": "orders", | 98 | "type": "orders", |
106 | "only": only, | 99 | "only": only, |
107 | "compute_value": compute_value, | 100 | "compute_value": compute_value, |
@@ -109,21 +102,20 @@ class ReportStore: | |||
109 | "orders": [order.as_json() for order in orders if order is not None] | 102 | "orders": [order.as_json() for order in orders if order is not None] |
110 | }) | 103 | }) |
111 | 104 | ||
112 | @classmethod | 105 | def log_order(self, order, tick, finished=False, update=None, |
113 | def log_order(cls, order, tick, finished=False, update=None, | ||
114 | new_order=None, compute_value=None): | 106 | new_order=None, compute_value=None): |
115 | if finished: | 107 | if finished: |
116 | cls.print_log("[Order] Finished {}".format(order)) | 108 | self.print_log("[Order] Finished {}".format(order)) |
117 | elif update == "waiting": | 109 | elif update == "waiting": |
118 | cls.print_log("[Order] {}, tick {}, waiting".format(order, tick)) | 110 | self.print_log("[Order] {}, tick {}, waiting".format(order, tick)) |
119 | elif update == "adjusting": | 111 | elif update == "adjusting": |
120 | cls.print_log("[Order] {}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order)) | 112 | self.print_log("[Order] {}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order)) |
121 | elif update == "market_fallback": | 113 | elif update == "market_fallback": |
122 | cls.print_log("[Order] {}, tick {}, fallbacking to market value".format(order, tick)) | 114 | self.print_log("[Order] {}, tick {}, fallbacking to market value".format(order, tick)) |
123 | elif update == "market_adjust": | 115 | elif update == "market_adjust": |
124 | cls.print_log("[Order] {}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order)) | 116 | self.print_log("[Order] {}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order)) |
125 | 117 | ||
126 | cls.add_log({ | 118 | self.add_log({ |
127 | "type": "order", | 119 | "type": "order", |
128 | "tick": tick, | 120 | "tick": tick, |
129 | "update": update, | 121 | "update": update, |
@@ -132,18 +124,16 @@ class ReportStore: | |||
132 | "new_order": new_order.as_json() if new_order is not None else None | 124 | "new_order": new_order.as_json() if new_order is not None else None |
133 | }) | 125 | }) |
134 | 126 | ||
135 | @classmethod | 127 | def log_move_balances(self, needed, moving): |
136 | def log_move_balances(cls, needed, moving, debug): | 128 | self.add_log({ |
137 | cls.add_log({ | ||
138 | "type": "move_balances", | 129 | "type": "move_balances", |
139 | "debug": debug, | 130 | "debug": self.market.debug, |
140 | "needed": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in needed.items() }, | 131 | "needed": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in needed.items() }, |
141 | "moving": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in moving.items() }, | 132 | "moving": { k: v.as_json()["value"] if isinstance(v, portfolio.Amount) else v for k, v in moving.items() }, |
142 | }) | 133 | }) |
143 | 134 | ||
144 | @classmethod | 135 | def log_http_request(self, method, url, body, headers, response): |
145 | def log_http_request(cls, method, url, body, headers, response): | 136 | self.add_log({ |
146 | cls.add_log({ | ||
147 | "type": "http_request", | 137 | "type": "http_request", |
148 | "method": method, | 138 | "method": method, |
149 | "url": url, | 139 | "url": url, |
@@ -153,15 +143,14 @@ class ReportStore: | |||
153 | "response": response.text | 143 | "response": response.text |
154 | }) | 144 | }) |
155 | 145 | ||
156 | @classmethod | 146 | def log_error(self, action, message=None, exception=None): |
157 | def log_error(cls, action, message=None, exception=None): | 147 | self.print_log("[Error] {}".format(action)) |
158 | cls.print_log("[Error] {}".format(action)) | ||
159 | if exception is not None: | 148 | if exception is not None: |
160 | cls.print_log(str("\t{}: {}".format(exception.__class__.__name__, exception))) | 149 | self.print_log(str("\t{}: {}".format(exception.__class__.__name__, exception))) |
161 | if message is not None: | 150 | if message is not None: |
162 | cls.print_log("\t{}".format(message)) | 151 | self.print_log("\t{}".format(message)) |
163 | 152 | ||
164 | cls.add_log({ | 153 | self.add_log({ |
165 | "type": "error", | 154 | "type": "error", |
166 | "action": action, | 155 | "action": action, |
167 | "exception_class": exception.__class__.__name__ if exception is not None else None, | 156 | "exception_class": exception.__class__.__name__ if exception is not None else None, |
@@ -169,132 +158,121 @@ class ReportStore: | |||
169 | "message": message, | 158 | "message": message, |
170 | }) | 159 | }) |
171 | 160 | ||
172 | @classmethod | 161 | def log_debug_action(self, action): |
173 | def log_debug_action(cls, action): | 162 | self.print_log("[Debug] {}".format(action)) |
174 | cls.print_log("[Debug] {}".format(action)) | ||
175 | 163 | ||
176 | cls.add_log({ | 164 | self.add_log({ |
177 | "type": "debug_action", | 165 | "type": "debug_action", |
178 | "action": action, | 166 | "action": action, |
179 | }) | 167 | }) |
180 | 168 | ||
181 | class BalanceStore: | 169 | class BalanceStore: |
182 | all = {} | 170 | def __init__(self, market): |
171 | self.market = market | ||
172 | self.all = {} | ||
183 | 173 | ||
184 | @classmethod | 174 | def currencies(self): |
185 | def currencies(cls): | 175 | return self.all.keys() |
186 | return cls.all.keys() | ||
187 | 176 | ||
188 | @classmethod | 177 | def in_currency(self, other_currency, compute_value="average", type="total"): |
189 | def in_currency(cls, other_currency, market, compute_value="average", type="total"): | ||
190 | amounts = {} | 178 | amounts = {} |
191 | for currency, balance in cls.all.items(): | 179 | for currency, balance in self.all.items(): |
192 | other_currency_amount = getattr(balance, type)\ | 180 | other_currency_amount = getattr(balance, type)\ |
193 | .in_currency(other_currency, market, compute_value=compute_value) | 181 | .in_currency(other_currency, self.market, compute_value=compute_value) |
194 | amounts[currency] = other_currency_amount | 182 | amounts[currency] = other_currency_amount |
195 | ReportStore.log_tickers(market, amounts, other_currency, | 183 | self.market.report.log_tickers(amounts, other_currency, |
196 | compute_value, type) | 184 | compute_value, type) |
197 | return amounts | 185 | return amounts |
198 | 186 | ||
199 | @classmethod | 187 | def fetch_balances(self, tag=None): |
200 | def fetch_balances(cls, market, tag=None): | 188 | all_balances = self.market.ccxt.fetch_all_balances() |
201 | all_balances = market.fetch_all_balances() | ||
202 | for currency, balance in all_balances.items(): | 189 | for currency, balance in all_balances.items(): |
203 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ | 190 | if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ |
204 | currency in cls.all: | 191 | currency in self.all: |
205 | cls.all[currency] = portfolio.Balance(currency, balance) | 192 | self.all[currency] = portfolio.Balance(currency, balance) |
206 | ReportStore.log_balances(market, tag=tag) | 193 | self.market.report.log_balances(tag=tag) |
207 | 194 | ||
208 | @classmethod | 195 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): |
209 | def dispatch_assets(cls, amount, liquidity="medium", repartition=None): | ||
210 | if repartition is None: | 196 | if repartition is None: |
211 | repartition = portfolio.Portfolio.repartition(liquidity=liquidity) | 197 | repartition = portfolio.Portfolio.repartition(self.market, liquidity=liquidity) |
212 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | 198 | sum_ratio = sum([v[0] for k, v in repartition.items()]) |
213 | amounts = {} | 199 | amounts = {} |
214 | for currency, (ptt, trade_type) in repartition.items(): | 200 | for currency, (ptt, trade_type) in repartition.items(): |
215 | amounts[currency] = ptt * amount / sum_ratio | 201 | amounts[currency] = ptt * amount / sum_ratio |
216 | if trade_type == "short": | 202 | if trade_type == "short": |
217 | amounts[currency] = - amounts[currency] | 203 | amounts[currency] = - amounts[currency] |
218 | if currency not in BalanceStore.all: | 204 | if currency not in self.all: |
219 | cls.all[currency] = portfolio.Balance(currency, {}) | 205 | self.all[currency] = portfolio.Balance(currency, {}) |
220 | ReportStore.log_dispatch(amount, amounts, liquidity, repartition) | 206 | self.market.report.log_dispatch(amount, amounts, liquidity, repartition) |
221 | return amounts | 207 | return amounts |
222 | 208 | ||
223 | @classmethod | 209 | def as_json(self): |
224 | def as_json(cls): | 210 | return { k: v.as_json() for k, v in self.all.items() } |
225 | return { k: v.as_json() for k, v in cls.all.items() } | ||
226 | 211 | ||
227 | class TradeStore: | 212 | class TradeStore: |
228 | all = [] | 213 | def __init__(self, market): |
229 | debug = False | 214 | self.market = market |
215 | self.all = [] | ||
230 | 216 | ||
231 | @classmethod | 217 | def compute_trades(self, values_in_base, new_repartition, only=None): |
232 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None, debug=False): | ||
233 | computed_trades = [] | 218 | computed_trades = [] |
234 | cls.debug = cls.debug or debug | ||
235 | base_currency = sum(values_in_base.values()).currency | 219 | base_currency = sum(values_in_base.values()).currency |
236 | for currency in BalanceStore.currencies(): | 220 | for currency in self.market.balances.currencies(): |
237 | if currency == base_currency: | 221 | if currency == base_currency: |
238 | continue | 222 | continue |
239 | value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0)) | 223 | value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0)) |
240 | value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) | 224 | value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) |
241 | 225 | ||
242 | if value_from.value * value_to.value < 0: | 226 | if value_from.value * value_to.value < 0: |
243 | computed_trades.append(cls.trade_if_matching( | 227 | computed_trades.append(self.trade_if_matching( |
244 | value_from, portfolio.Amount(base_currency, 0), | 228 | value_from, portfolio.Amount(base_currency, 0), |
245 | currency, only=only, market=market)) | 229 | currency, only=only)) |
246 | computed_trades.append(cls.trade_if_matching( | 230 | computed_trades.append(self.trade_if_matching( |
247 | portfolio.Amount(base_currency, 0), value_to, | 231 | portfolio.Amount(base_currency, 0), value_to, |
248 | currency, only=only, market=market)) | 232 | currency, only=only)) |
249 | else: | 233 | else: |
250 | computed_trades.append(cls.trade_if_matching( | 234 | computed_trades.append(self.trade_if_matching( |
251 | value_from, value_to, | 235 | value_from, value_to, |
252 | currency, only=only, market=market)) | 236 | currency, only=only)) |
253 | for matching, trade in computed_trades: | 237 | for matching, trade in computed_trades: |
254 | if matching: | 238 | if matching: |
255 | cls.all.append(trade) | 239 | self.all.append(trade) |
256 | ReportStore.log_trades(computed_trades, only, cls.debug) | 240 | self.market.report.log_trades(computed_trades, only) |
257 | 241 | ||
258 | @classmethod | 242 | def trade_if_matching(self, value_from, value_to, currency, |
259 | def trade_if_matching(cls, value_from, value_to, currency, | 243 | only=None): |
260 | only=None, market=None): | ||
261 | trade = portfolio.Trade(value_from, value_to, currency, | 244 | trade = portfolio.Trade(value_from, value_to, currency, |
262 | market=market) | 245 | self.market) |
263 | matching = only is None or trade.action == only | 246 | matching = only is None or trade.action == only |
264 | return [matching, trade] | 247 | return [matching, trade] |
265 | 248 | ||
266 | @classmethod | 249 | def prepare_orders(self, only=None, compute_value="default"): |
267 | def prepare_orders(cls, only=None, compute_value="default"): | ||
268 | orders = [] | 250 | orders = [] |
269 | for trade in cls.all: | 251 | for trade in self.all: |
270 | if only is None or trade.action == only: | 252 | if only is None or trade.action == only: |
271 | orders.append(trade.prepare_order(compute_value=compute_value)) | 253 | orders.append(trade.prepare_order(compute_value=compute_value)) |
272 | ReportStore.log_orders(orders, only, compute_value) | 254 | self.market.report.log_orders(orders, only, compute_value) |
273 | 255 | ||
274 | @classmethod | 256 | def print_all_with_order(self, ind=""): |
275 | def print_all_with_order(cls, ind=""): | 257 | for trade in self.all: |
276 | for trade in cls.all: | ||
277 | trade.print_with_order(ind=ind) | 258 | trade.print_with_order(ind=ind) |
278 | 259 | ||
279 | @classmethod | 260 | def run_orders(self): |
280 | def run_orders(cls): | 261 | orders = self.all_orders(state="pending") |
281 | orders = cls.all_orders(state="pending") | ||
282 | for order in orders: | 262 | for order in orders: |
283 | order.run() | 263 | order.run() |
284 | ReportStore.log_stage("run_orders") | 264 | self.market.report.log_stage("run_orders") |
285 | ReportStore.log_orders(orders) | 265 | self.market.report.log_orders(orders) |
286 | 266 | ||
287 | @classmethod | 267 | def all_orders(self, state=None): |
288 | def all_orders(cls, state=None): | 268 | all_orders = sum(map(lambda v: v.orders, self.all), []) |
289 | all_orders = sum(map(lambda v: v.orders, cls.all), []) | ||
290 | if state is None: | 269 | if state is None: |
291 | return all_orders | 270 | return all_orders |
292 | else: | 271 | else: |
293 | return list(filter(lambda o: o.status == state, all_orders)) | 272 | return list(filter(lambda o: o.status == state, all_orders)) |
294 | 273 | ||
295 | @classmethod | 274 | def update_all_orders_status(self): |
296 | def update_all_orders_status(cls): | 275 | for order in self.all_orders(state="open"): |
297 | for order in cls.all_orders(state="open"): | ||
298 | order.get_status() | 276 | order.get_status() |
299 | 277 | ||
300 | 278 | ||
@@ -1,12 +1,13 @@ | |||
1 | import sys | 1 | import sys |
2 | import portfolio | 2 | import portfolio |
3 | import unittest | 3 | import unittest |
4 | import datetime | ||
4 | from decimal import Decimal as D | 5 | from decimal import Decimal as D |
5 | from unittest import mock | 6 | from unittest import mock |
6 | import requests | 7 | import requests |
7 | import requests_mock | 8 | import requests_mock |
8 | from io import StringIO | 9 | from io import StringIO |
9 | import helper | 10 | import portfolio, helper, market |
10 | 11 | ||
11 | limits = ["acceptance", "unit"] | 12 | limits = ["acceptance", "unit"] |
12 | for test_type in limits: | 13 | for test_type in limits: |
@@ -26,18 +27,15 @@ class WebMockTestCase(unittest.TestCase): | |||
26 | self.wm = requests_mock.Mocker() | 27 | self.wm = requests_mock.Mocker() |
27 | self.wm.start() | 28 | self.wm.start() |
28 | 29 | ||
30 | # market | ||
31 | self.m = mock.Mock(name="Market", spec=market.Market) | ||
32 | self.m.debug = False | ||
33 | |||
29 | self.patchers = [ | 34 | self.patchers = [ |
30 | mock.patch.multiple(portfolio.ReportStore, | ||
31 | logs=[], verbose_print=True), | ||
32 | mock.patch.multiple(portfolio.BalanceStore, | ||
33 | all={},), | ||
34 | mock.patch.multiple(portfolio.TradeStore, | ||
35 | all=[], | ||
36 | debug=False), | ||
37 | mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), | 35 | mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), |
38 | mock.patch.multiple(portfolio.Computation, | 36 | mock.patch.multiple(portfolio.Computation, |
39 | computations=portfolio.Computation.computations), | 37 | computations=portfolio.Computation.computations), |
40 | mock.patch.multiple(helper, | 38 | mock.patch.multiple(market.Market, |
41 | fees_cache={}, | 39 | fees_cache={}, |
42 | ticker_cache={}, | 40 | ticker_cache={}, |
43 | ticker_cache_timestamp=self.time.time()), | 41 | ticker_cache_timestamp=self.time.time()), |
@@ -65,41 +63,39 @@ class PortfolioTest(WebMockTestCase): | |||
65 | 63 | ||
66 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) | 64 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) |
67 | 65 | ||
68 | @mock.patch("portfolio.ReportStore") | 66 | def test_get_cryptoportfolio(self): |
69 | def test_get_cryptoportfolio(self, report_store): | ||
70 | self.wm.get(portfolio.Portfolio.URL, [ | 67 | self.wm.get(portfolio.Portfolio.URL, [ |
71 | {"text":'{ "foo": "bar" }', "status_code": 200}, | 68 | {"text":'{ "foo": "bar" }', "status_code": 200}, |
72 | {"text": "System Error", "status_code": 500}, | 69 | {"text": "System Error", "status_code": 500}, |
73 | {"exc": requests.exceptions.ConnectTimeout}, | 70 | {"exc": requests.exceptions.ConnectTimeout}, |
74 | ]) | 71 | ]) |
75 | portfolio.Portfolio.get_cryptoportfolio() | 72 | portfolio.Portfolio.get_cryptoportfolio(self.m) |
76 | self.assertIn("foo", portfolio.Portfolio.data) | 73 | self.assertIn("foo", portfolio.Portfolio.data) |
77 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) | 74 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) |
78 | self.assertTrue(self.wm.called) | 75 | self.assertTrue(self.wm.called) |
79 | self.assertEqual(1, self.wm.call_count) | 76 | self.assertEqual(1, self.wm.call_count) |
80 | report_store.log_error.assert_not_called() | 77 | self.m.report.log_error.assert_not_called() |
81 | report_store.log_http_request.assert_called_once() | 78 | self.m.report.log_http_request.assert_called_once() |
82 | report_store.log_http_request.reset_mock() | 79 | self.m.report.log_http_request.reset_mock() |
83 | 80 | ||
84 | portfolio.Portfolio.get_cryptoportfolio() | 81 | portfolio.Portfolio.get_cryptoportfolio(self.m) |
85 | self.assertIsNone(portfolio.Portfolio.data) | 82 | self.assertIsNone(portfolio.Portfolio.data) |
86 | self.assertEqual(2, self.wm.call_count) | 83 | self.assertEqual(2, self.wm.call_count) |
87 | report_store.log_error.assert_not_called() | 84 | self.m.report.log_error.assert_not_called() |
88 | report_store.log_http_request.assert_called_once() | 85 | self.m.report.log_http_request.assert_called_once() |
89 | report_store.log_http_request.reset_mock() | 86 | self.m.report.log_http_request.reset_mock() |
90 | 87 | ||
91 | 88 | ||
92 | portfolio.Portfolio.data = "Foo" | 89 | portfolio.Portfolio.data = "Foo" |
93 | portfolio.Portfolio.get_cryptoportfolio() | 90 | portfolio.Portfolio.get_cryptoportfolio(self.m) |
94 | self.assertEqual("Foo", portfolio.Portfolio.data) | 91 | self.assertEqual("Foo", portfolio.Portfolio.data) |
95 | self.assertEqual(3, self.wm.call_count) | 92 | self.assertEqual(3, self.wm.call_count) |
96 | report_store.log_error.assert_called_once_with("get_cryptoportfolio", | 93 | self.m.report.log_error.assert_called_once_with("get_cryptoportfolio", |
97 | exception=mock.ANY) | 94 | exception=mock.ANY) |
98 | report_store.log_http_request.assert_not_called() | 95 | self.m.report.log_http_request.assert_not_called() |
99 | 96 | ||
100 | @mock.patch("portfolio.ReportStore") | 97 | def test_parse_cryptoportfolio(self): |
101 | def test_parse_cryptoportfolio(self, report_store): | 98 | portfolio.Portfolio.parse_cryptoportfolio(self.m) |
102 | portfolio.Portfolio.parse_cryptoportfolio() | ||
103 | 99 | ||
104 | self.assertListEqual( | 100 | self.assertListEqual( |
105 | ["medium", "high"], | 101 | ["medium", "high"], |
@@ -135,22 +131,21 @@ class PortfolioTest(WebMockTestCase): | |||
135 | self.assertDictEqual(expected, liquidities["medium"][date]) | 131 | self.assertDictEqual(expected, liquidities["medium"][date]) |
136 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) | 132 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) |
137 | 133 | ||
138 | report_store.log_http_request.assert_called_once_with("GET", | 134 | self.m.report.log_http_request.assert_called_once_with("GET", |
139 | portfolio.Portfolio.URL, None, mock.ANY, mock.ANY) | 135 | portfolio.Portfolio.URL, None, mock.ANY, mock.ANY) |
140 | report_store.log_http_request.reset_mock() | 136 | self.m.report.log_http_request.reset_mock() |
141 | 137 | ||
142 | # It doesn't refetch the data when available | 138 | # It doesn't refetch the data when available |
143 | portfolio.Portfolio.parse_cryptoportfolio() | 139 | portfolio.Portfolio.parse_cryptoportfolio(self.m) |
144 | report_store.log_http_request.assert_not_called() | 140 | self.m.report.log_http_request.assert_not_called() |
145 | 141 | ||
146 | self.assertEqual(1, self.wm.call_count) | 142 | self.assertEqual(1, self.wm.call_count) |
147 | 143 | ||
148 | portfolio.Portfolio.parse_cryptoportfolio(refetch=True) | 144 | portfolio.Portfolio.parse_cryptoportfolio(self.m, refetch=True) |
149 | self.assertEqual(2, self.wm.call_count) | 145 | self.assertEqual(2, self.wm.call_count) |
150 | report_store.log_http_request.assert_called_once() | 146 | self.m.report.log_http_request.assert_called_once() |
151 | 147 | ||
152 | @mock.patch("portfolio.ReportStore") | 148 | def test_repartition(self): |
153 | def test_repartition(self, report_store): | ||
154 | expected_medium = { | 149 | expected_medium = { |
155 | 'BTC': (D("1.1102e-16"), "long"), | 150 | 'BTC': (D("1.1102e-16"), "long"), |
156 | 'USDT': (D("0.1"), "long"), | 151 | 'USDT': (D("0.1"), "long"), |
@@ -173,25 +168,26 @@ class PortfolioTest(WebMockTestCase): | |||
173 | 'GAS': (D("0.1308"), "long"), | 168 | 'GAS': (D("0.1308"), "long"), |
174 | } | 169 | } |
175 | 170 | ||
176 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition()) | 171 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m)) |
177 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium")) | 172 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium")) |
178 | self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high")) | 173 | self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high")) |
179 | 174 | ||
180 | self.assertEqual(1, self.wm.call_count) | 175 | self.assertEqual(1, self.wm.call_count) |
181 | 176 | ||
182 | portfolio.Portfolio.repartition() | 177 | portfolio.Portfolio.repartition(self.m) |
183 | self.assertEqual(1, self.wm.call_count) | 178 | self.assertEqual(1, self.wm.call_count) |
184 | 179 | ||
185 | portfolio.Portfolio.repartition(refetch=True) | 180 | portfolio.Portfolio.repartition(self.m, refetch=True) |
186 | self.assertEqual(2, self.wm.call_count) | 181 | self.assertEqual(2, self.wm.call_count) |
187 | report_store.log_http_request.assert_called() | 182 | self.m.report.log_http_request.assert_called() |
188 | self.assertEqual(2, report_store.log_http_request.call_count) | 183 | self.assertEqual(2, self.m.report.log_http_request.call_count) |
189 | 184 | ||
190 | @mock.patch.object(portfolio.time, "sleep") | 185 | @mock.patch.object(portfolio.time, "sleep") |
191 | @mock.patch.object(portfolio.Portfolio, "repartition") | 186 | @mock.patch.object(portfolio.Portfolio, "repartition") |
192 | def test_wait_for_recent(self, repartition, sleep): | 187 | def test_wait_for_recent(self, repartition, sleep): |
193 | self.call_count = 0 | 188 | self.call_count = 0 |
194 | def _repartition(refetch): | 189 | def _repartition(market, refetch): |
190 | self.assertEqual(self.m, market) | ||
195 | self.assertTrue(refetch) | 191 | self.assertTrue(refetch) |
196 | self.call_count += 1 | 192 | self.call_count += 1 |
197 | portfolio.Portfolio.last_date = portfolio.datetime.now()\ | 193 | portfolio.Portfolio.last_date = portfolio.datetime.now()\ |
@@ -199,16 +195,17 @@ class PortfolioTest(WebMockTestCase): | |||
199 | + portfolio.timedelta(self.call_count) | 195 | + portfolio.timedelta(self.call_count) |
200 | repartition.side_effect = _repartition | 196 | repartition.side_effect = _repartition |
201 | 197 | ||
202 | portfolio.Portfolio.wait_for_recent() | 198 | portfolio.Portfolio.wait_for_recent(self.m) |
203 | sleep.assert_called_with(30) | 199 | sleep.assert_called_with(30) |
204 | self.assertEqual(6, sleep.call_count) | 200 | self.assertEqual(6, sleep.call_count) |
205 | self.assertEqual(7, repartition.call_count) | 201 | self.assertEqual(7, repartition.call_count) |
202 | self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | ||
206 | 203 | ||
207 | sleep.reset_mock() | 204 | sleep.reset_mock() |
208 | repartition.reset_mock() | 205 | repartition.reset_mock() |
209 | portfolio.Portfolio.last_date = None | 206 | portfolio.Portfolio.last_date = None |
210 | self.call_count = 0 | 207 | self.call_count = 0 |
211 | portfolio.Portfolio.wait_for_recent(delta=15) | 208 | portfolio.Portfolio.wait_for_recent(self.m, delta=15) |
212 | sleep.assert_not_called() | 209 | sleep.assert_not_called() |
213 | self.assertEqual(1, repartition.call_count) | 210 | self.assertEqual(1, repartition.call_count) |
214 | 211 | ||
@@ -216,7 +213,7 @@ class PortfolioTest(WebMockTestCase): | |||
216 | repartition.reset_mock() | 213 | repartition.reset_mock() |
217 | portfolio.Portfolio.last_date = None | 214 | portfolio.Portfolio.last_date = None |
218 | self.call_count = 0 | 215 | self.call_count = 0 |
219 | portfolio.Portfolio.wait_for_recent(delta=1) | 216 | portfolio.Portfolio.wait_for_recent(self.m, delta=1) |
220 | sleep.assert_called_with(30) | 217 | sleep.assert_called_with(30) |
221 | self.assertEqual(9, sleep.call_count) | 218 | self.assertEqual(9, sleep.call_count) |
222 | self.assertEqual(10, repartition.call_count) | 219 | self.assertEqual(10, repartition.call_count) |
@@ -231,35 +228,34 @@ class AmountTest(WebMockTestCase): | |||
231 | def test_in_currency(self): | 228 | def test_in_currency(self): |
232 | amount = portfolio.Amount("ETC", 10) | 229 | amount = portfolio.Amount("ETC", 10) |
233 | 230 | ||
234 | self.assertEqual(amount, amount.in_currency("ETC", None)) | 231 | self.assertEqual(amount, amount.in_currency("ETC", self.m)) |
235 | 232 | ||
236 | ticker_mock = unittest.mock.Mock() | 233 | with self.subTest(desc="no ticker for currency"): |
237 | with mock.patch.object(helper, 'get_ticker', new=ticker_mock): | 234 | self.m.get_ticker.return_value = None |
238 | ticker_mock.return_value = None | ||
239 | 235 | ||
240 | self.assertRaises(Exception, amount.in_currency, "ETH", None) | 236 | self.assertRaises(Exception, amount.in_currency, "ETH", self.m) |
241 | 237 | ||
242 | with mock.patch.object(helper, 'get_ticker', new=ticker_mock): | 238 | with self.subTest(desc="nominal case"): |
243 | ticker_mock.return_value = { | 239 | self.m.get_ticker.return_value = { |
244 | "bid": D("0.2"), | 240 | "bid": D("0.2"), |
245 | "ask": D("0.4"), | 241 | "ask": D("0.4"), |
246 | "average": D("0.3"), | 242 | "average": D("0.3"), |
247 | "foo": "bar", | 243 | "foo": "bar", |
248 | } | 244 | } |
249 | converted_amount = amount.in_currency("ETH", None) | 245 | converted_amount = amount.in_currency("ETH", self.m) |
250 | 246 | ||
251 | self.assertEqual(D("3.0"), converted_amount.value) | 247 | self.assertEqual(D("3.0"), converted_amount.value) |
252 | self.assertEqual("ETH", converted_amount.currency) | 248 | self.assertEqual("ETH", converted_amount.currency) |
253 | self.assertEqual(amount, converted_amount.linked_to) | 249 | self.assertEqual(amount, converted_amount.linked_to) |
254 | self.assertEqual("bar", converted_amount.ticker["foo"]) | 250 | self.assertEqual("bar", converted_amount.ticker["foo"]) |
255 | 251 | ||
256 | converted_amount = amount.in_currency("ETH", None, action="bid", compute_value="default") | 252 | converted_amount = amount.in_currency("ETH", self.m, action="bid", compute_value="default") |
257 | self.assertEqual(D("2"), converted_amount.value) | 253 | self.assertEqual(D("2"), converted_amount.value) |
258 | 254 | ||
259 | converted_amount = amount.in_currency("ETH", None, compute_value="ask") | 255 | converted_amount = amount.in_currency("ETH", self.m, compute_value="ask") |
260 | self.assertEqual(D("4"), converted_amount.value) | 256 | self.assertEqual(D("4"), converted_amount.value) |
261 | 257 | ||
262 | converted_amount = amount.in_currency("ETH", None, rate=D("0.02")) | 258 | converted_amount = amount.in_currency("ETH", self.m, rate=D("0.02")) |
263 | self.assertEqual(D("0.2"), converted_amount.value) | 259 | self.assertEqual(D("0.2"), converted_amount.value) |
264 | 260 | ||
265 | def test__round(self): | 261 | def test__round(self): |
@@ -318,7 +314,7 @@ class AmountTest(WebMockTestCase): | |||
318 | with self.assertRaises(Exception): | 314 | with self.assertRaises(Exception): |
319 | 3 - amount | 315 | 3 - amount |
320 | 316 | ||
321 | self.assertEqual(portfolio.Amount("ETH", "-1.6"), -amount) | 317 | self.assertEqual(portfolio.Amount("ETH", "-1.6"), 0-amount) |
322 | 318 | ||
323 | def test__mul(self): | 319 | def test__mul(self): |
324 | amount = portfolio.Amount("XEM", 11) | 320 | amount = portfolio.Amount("XEM", 11) |
@@ -544,34 +540,76 @@ class BalanceTest(WebMockTestCase): | |||
544 | self.assertEqual(D(0), as_json["margin_borrowed"]) | 540 | self.assertEqual(D(0), as_json["margin_borrowed"]) |
545 | 541 | ||
546 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 542 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
547 | class HelperTest(WebMockTestCase): | 543 | class MarketTest(WebMockTestCase): |
544 | def setUp(self): | ||
545 | super(MarketTest, self).setUp() | ||
546 | |||
547 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) | ||
548 | |||
549 | def test_values(self): | ||
550 | m = market.Market(self.ccxt) | ||
551 | |||
552 | self.assertEqual(self.ccxt, m.ccxt) | ||
553 | self.assertFalse(m.debug) | ||
554 | self.assertIsInstance(m.report, market.ReportStore) | ||
555 | self.assertIsInstance(m.trades, market.TradeStore) | ||
556 | self.assertIsInstance(m.balances, market.BalanceStore) | ||
557 | self.assertEqual(m, m.report.market) | ||
558 | self.assertEqual(m, m.trades.market) | ||
559 | self.assertEqual(m, m.balances.market) | ||
560 | self.assertEqual(m, m.ccxt._market) | ||
561 | |||
562 | m = market.Market(self.ccxt, debug=True) | ||
563 | self.assertTrue(m.debug) | ||
564 | |||
565 | m = market.Market(self.ccxt, debug=False) | ||
566 | self.assertFalse(m.debug) | ||
567 | |||
568 | @mock.patch("market.ccxt") | ||
569 | def test_from_config(self, ccxt): | ||
570 | with mock.patch("market.ReportStore"): | ||
571 | ccxt.poloniexE.return_value = self.ccxt | ||
572 | self.ccxt.session.request.return_value = "response" | ||
573 | |||
574 | m = market.Market.from_config("config") | ||
575 | |||
576 | self.assertEqual(self.ccxt, m.ccxt) | ||
577 | |||
578 | self.ccxt.session.request("GET", "URL", data="data", | ||
579 | headers="headers") | ||
580 | m.report.log_http_request.assert_called_with('GET', 'URL', 'data', | ||
581 | 'headers', 'response') | ||
582 | |||
583 | m = market.Market.from_config("config", debug=True) | ||
584 | self.assertEqual(True, m.debug) | ||
585 | |||
548 | def test_get_ticker(self): | 586 | def test_get_ticker(self): |
549 | market = mock.Mock() | 587 | m = market.Market(self.ccxt) |
550 | market.fetch_ticker.side_effect = [ | 588 | self.ccxt.fetch_ticker.side_effect = [ |
551 | { "bid": 1, "ask": 3 }, | 589 | { "bid": 1, "ask": 3 }, |
552 | helper.ExchangeError("foo"), | 590 | market.ExchangeError("foo"), |
553 | { "bid": 10, "ask": 40 }, | 591 | { "bid": 10, "ask": 40 }, |
554 | helper.ExchangeError("foo"), | 592 | market.ExchangeError("foo"), |
555 | helper.ExchangeError("foo"), | 593 | market.ExchangeError("foo"), |
556 | ] | 594 | ] |
557 | 595 | ||
558 | ticker = helper.get_ticker("ETH", "ETC", market) | 596 | ticker = m.get_ticker("ETH", "ETC") |
559 | market.fetch_ticker.assert_called_with("ETH/ETC") | 597 | self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") |
560 | self.assertEqual(1, ticker["bid"]) | 598 | self.assertEqual(1, ticker["bid"]) |
561 | self.assertEqual(3, ticker["ask"]) | 599 | self.assertEqual(3, ticker["ask"]) |
562 | self.assertEqual(2, ticker["average"]) | 600 | self.assertEqual(2, ticker["average"]) |
563 | self.assertFalse(ticker["inverted"]) | 601 | self.assertFalse(ticker["inverted"]) |
564 | 602 | ||
565 | ticker = helper.get_ticker("ETH", "XVG", market) | 603 | ticker = m.get_ticker("ETH", "XVG") |
566 | self.assertEqual(0.0625, ticker["average"]) | 604 | self.assertEqual(0.0625, ticker["average"]) |
567 | self.assertTrue(ticker["inverted"]) | 605 | self.assertTrue(ticker["inverted"]) |
568 | self.assertIn("original", ticker) | 606 | self.assertIn("original", ticker) |
569 | self.assertEqual(10, ticker["original"]["bid"]) | 607 | self.assertEqual(10, ticker["original"]["bid"]) |
570 | 608 | ||
571 | ticker = helper.get_ticker("XVG", "XMR", market) | 609 | ticker = m.get_ticker("XVG", "XMR") |
572 | self.assertIsNone(ticker) | 610 | self.assertIsNone(ticker) |
573 | 611 | ||
574 | market.fetch_ticker.assert_has_calls([ | 612 | self.ccxt.fetch_ticker.assert_has_calls([ |
575 | mock.call("ETH/ETC"), | 613 | mock.call("ETH/ETC"), |
576 | mock.call("ETH/XVG"), | 614 | mock.call("ETH/XVG"), |
577 | mock.call("XVG/ETH"), | 615 | mock.call("XVG/ETH"), |
@@ -579,56 +617,62 @@ class HelperTest(WebMockTestCase): | |||
579 | mock.call("XMR/XVG"), | 617 | mock.call("XMR/XVG"), |
580 | ]) | 618 | ]) |
581 | 619 | ||
582 | market2 = mock.Mock() | 620 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
583 | market2.fetch_ticker.side_effect = [ | 621 | m1b = market.Market(self.ccxt) |
622 | m1b.get_ticker("ETH", "ETC") | ||
623 | self.ccxt.fetch_ticker.assert_not_called() | ||
624 | |||
625 | self.ccxt = mock.Mock(spec=market.ccxt.poloniex) | ||
626 | m2 = market.Market(self.ccxt) | ||
627 | self.ccxt.fetch_ticker.side_effect = [ | ||
584 | { "bid": 1, "ask": 3 }, | 628 | { "bid": 1, "ask": 3 }, |
585 | { "bid": 1.2, "ask": 3.5 }, | 629 | { "bid": 1.2, "ask": 3.5 }, |
586 | ] | 630 | ] |
587 | ticker1 = helper.get_ticker("ETH", "ETC", market2) | 631 | ticker1 = m2.get_ticker("ETH", "ETC") |
588 | ticker2 = helper.get_ticker("ETH", "ETC", market2) | 632 | ticker2 = m2.get_ticker("ETH", "ETC") |
589 | ticker3 = helper.get_ticker("ETC", "ETH", market2) | 633 | ticker3 = m2.get_ticker("ETC", "ETH") |
590 | market2.fetch_ticker.assert_called_once_with("ETH/ETC") | 634 | self.ccxt.fetch_ticker.assert_called_once_with("ETH/ETC") |
591 | self.assertEqual(1, ticker1["bid"]) | 635 | self.assertEqual(1, ticker1["bid"]) |
592 | self.assertDictEqual(ticker1, ticker2) | 636 | self.assertDictEqual(ticker1, ticker2) |
593 | self.assertDictEqual(ticker1, ticker3["original"]) | 637 | self.assertDictEqual(ticker1, ticker3["original"]) |
594 | 638 | ||
595 | ticker4 = helper.get_ticker("ETH", "ETC", market2, refresh=True) | 639 | ticker4 = m2.get_ticker("ETH", "ETC", refresh=True) |
596 | ticker5 = helper.get_ticker("ETH", "ETC", market2) | 640 | ticker5 = m2.get_ticker("ETH", "ETC") |
597 | self.assertEqual(1.2, ticker4["bid"]) | 641 | self.assertEqual(1.2, ticker4["bid"]) |
598 | self.assertDictEqual(ticker4, ticker5) | 642 | self.assertDictEqual(ticker4, ticker5) |
599 | 643 | ||
600 | market3 = mock.Mock() | 644 | self.ccxt = mock.Mock(spec=market.ccxt.binance) |
601 | market3.fetch_ticker.side_effect = [ | 645 | m3 = market.Market(self.ccxt) |
646 | self.ccxt.fetch_ticker.side_effect = [ | ||
602 | { "bid": 1, "ask": 3 }, | 647 | { "bid": 1, "ask": 3 }, |
603 | { "bid": 1.2, "ask": 3.5 }, | 648 | { "bid": 1.2, "ask": 3.5 }, |
604 | ] | 649 | ] |
605 | ticker6 = helper.get_ticker("ETH", "ETC", market3) | 650 | ticker6 = m3.get_ticker("ETH", "ETC") |
606 | helper.ticker_cache_timestamp -= 4 | 651 | m3.ticker_cache_timestamp -= 4 |
607 | ticker7 = helper.get_ticker("ETH", "ETC", market3) | 652 | ticker7 = m3.get_ticker("ETH", "ETC") |
608 | helper.ticker_cache_timestamp -= 2 | 653 | m3.ticker_cache_timestamp -= 2 |
609 | ticker8 = helper.get_ticker("ETH", "ETC", market3) | 654 | ticker8 = m3.get_ticker("ETH", "ETC") |
610 | self.assertDictEqual(ticker6, ticker7) | 655 | self.assertDictEqual(ticker6, ticker7) |
611 | self.assertEqual(1.2, ticker8["bid"]) | 656 | self.assertEqual(1.2, ticker8["bid"]) |
612 | 657 | ||
613 | def test_fetch_fees(self): | 658 | def test_fetch_fees(self): |
614 | market = mock.Mock() | 659 | m = market.Market(self.ccxt) |
615 | market.fetch_fees.return_value = "Foo" | 660 | self.ccxt.fetch_fees.return_value = "Foo" |
616 | self.assertEqual("Foo", helper.fetch_fees(market)) | 661 | self.assertEqual("Foo", m.fetch_fees()) |
617 | market.fetch_fees.assert_called_once() | 662 | self.ccxt.fetch_fees.assert_called_once() |
618 | self.assertEqual("Foo", helper.fetch_fees(market)) | 663 | self.ccxt.reset_mock() |
619 | market.fetch_fees.assert_called_once() | 664 | self.assertEqual("Foo", m.fetch_fees()) |
665 | self.ccxt.fetch_fees.assert_not_called() | ||
620 | 666 | ||
621 | @mock.patch.object(portfolio.Portfolio, "repartition") | 667 | @mock.patch.object(portfolio.Portfolio, "repartition") |
622 | @mock.patch.object(helper, "get_ticker") | 668 | @mock.patch.object(market.Market, "get_ticker") |
623 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 669 | @mock.patch.object(market.TradeStore, "compute_trades") |
624 | @mock.patch("store.ReportStore") | 670 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
625 | @mock.patch("helper.ReportStore") | ||
626 | def test_prepare_trades(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
627 | repartition.return_value = { | 671 | repartition.return_value = { |
628 | "XEM": (D("0.75"), "long"), | 672 | "XEM": (D("0.75"), "long"), |
629 | "BTC": (D("0.25"), "long"), | 673 | "BTC": (D("0.25"), "long"), |
630 | } | 674 | } |
631 | def _get_ticker(c1, c2, market): | 675 | def _get_ticker(c1, c2): |
632 | if c1 == "USDT" and c2 == "BTC": | 676 | if c1 == "USDT" and c2 == "BTC": |
633 | return { "average": D("0.0001") } | 677 | return { "average": D("0.0001") } |
634 | if c1 == "XVG" and c2 == "BTC": | 678 | if c1 == "XVG" and c2 == "BTC": |
@@ -638,46 +682,45 @@ class HelperTest(WebMockTestCase): | |||
638 | self.fail("Should be called with {}, {}".format(c1, c2)) | 682 | self.fail("Should be called with {}, {}".format(c1, c2)) |
639 | get_ticker.side_effect = _get_ticker | 683 | get_ticker.side_effect = _get_ticker |
640 | 684 | ||
641 | market = mock.Mock() | 685 | with mock.patch("market.ReportStore"): |
642 | market.fetch_all_balances.return_value = { | 686 | m = market.Market(self.ccxt) |
643 | "USDT": { | 687 | self.ccxt.fetch_all_balances.return_value = { |
644 | "exchange_free": D("10000.0"), | 688 | "USDT": { |
645 | "exchange_used": D("0.0"), | 689 | "exchange_free": D("10000.0"), |
646 | "exchange_total": D("10000.0"), | 690 | "exchange_used": D("0.0"), |
647 | "total": D("10000.0") | 691 | "exchange_total": D("10000.0"), |
648 | }, | 692 | "total": D("10000.0") |
649 | "XVG": { | 693 | }, |
650 | "exchange_free": D("10000.0"), | 694 | "XVG": { |
651 | "exchange_used": D("0.0"), | 695 | "exchange_free": D("10000.0"), |
652 | "exchange_total": D("10000.0"), | 696 | "exchange_used": D("0.0"), |
653 | "total": D("10000.0") | 697 | "exchange_total": D("10000.0"), |
654 | }, | 698 | "total": D("10000.0") |
655 | } | 699 | }, |
656 | portfolio.BalanceStore.fetch_balances(market, tag="tag") | 700 | } |
657 | 701 | ||
658 | helper.prepare_trades(market) | 702 | m.balances.fetch_balances(tag="tag") |
659 | compute_trades.assert_called() | ||
660 | 703 | ||
661 | call = compute_trades.call_args | 704 | m.prepare_trades() |
662 | self.assertEqual(market, call[1]["market"]) | 705 | compute_trades.assert_called() |
663 | self.assertEqual(1, call[0][0]["USDT"].value) | 706 | |
664 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 707 | call = compute_trades.call_args |
665 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) | 708 | self.assertEqual(1, call[0][0]["USDT"].value) |
666 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 709 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
667 | report_store_h.log_stage.assert_called_once_with("prepare_trades") | 710 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) |
668 | report_store.log_balances.assert_called_once_with(market, tag="tag") | 711 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
712 | m.report.log_stage.assert_called_once_with("prepare_trades") | ||
713 | m.report.log_balances.assert_called_once_with(tag="tag") | ||
669 | 714 | ||
670 | @mock.patch.object(portfolio.Portfolio, "repartition") | 715 | @mock.patch.object(portfolio.Portfolio, "repartition") |
671 | @mock.patch.object(helper, "get_ticker") | 716 | @mock.patch.object(market.Market, "get_ticker") |
672 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 717 | @mock.patch.object(market.TradeStore, "compute_trades") |
673 | @mock.patch("store.ReportStore") | 718 | def test_update_trades(self, compute_trades, get_ticker, repartition): |
674 | @mock.patch("helper.ReportStore") | ||
675 | def test_update_trades(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
676 | repartition.return_value = { | 719 | repartition.return_value = { |
677 | "XEM": (D("0.75"), "long"), | 720 | "XEM": (D("0.75"), "long"), |
678 | "BTC": (D("0.25"), "long"), | 721 | "BTC": (D("0.25"), "long"), |
679 | } | 722 | } |
680 | def _get_ticker(c1, c2, market): | 723 | def _get_ticker(c1, c2): |
681 | if c1 == "USDT" and c2 == "BTC": | 724 | if c1 == "USDT" and c2 == "BTC": |
682 | return { "average": D("0.0001") } | 725 | return { "average": D("0.0001") } |
683 | if c1 == "XVG" and c2 == "BTC": | 726 | if c1 == "XVG" and c2 == "BTC": |
@@ -687,42 +730,41 @@ class HelperTest(WebMockTestCase): | |||
687 | self.fail("Should be called with {}, {}".format(c1, c2)) | 730 | self.fail("Should be called with {}, {}".format(c1, c2)) |
688 | get_ticker.side_effect = _get_ticker | 731 | get_ticker.side_effect = _get_ticker |
689 | 732 | ||
690 | market = mock.Mock() | 733 | with mock.patch("market.ReportStore"): |
691 | market.fetch_all_balances.return_value = { | 734 | m = market.Market(self.ccxt) |
692 | "USDT": { | 735 | self.ccxt.fetch_all_balances.return_value = { |
693 | "exchange_free": D("10000.0"), | 736 | "USDT": { |
694 | "exchange_used": D("0.0"), | 737 | "exchange_free": D("10000.0"), |
695 | "exchange_total": D("10000.0"), | 738 | "exchange_used": D("0.0"), |
696 | "total": D("10000.0") | 739 | "exchange_total": D("10000.0"), |
697 | }, | 740 | "total": D("10000.0") |
698 | "XVG": { | 741 | }, |
699 | "exchange_free": D("10000.0"), | 742 | "XVG": { |
700 | "exchange_used": D("0.0"), | 743 | "exchange_free": D("10000.0"), |
701 | "exchange_total": D("10000.0"), | 744 | "exchange_used": D("0.0"), |
702 | "total": D("10000.0") | 745 | "exchange_total": D("10000.0"), |
703 | }, | 746 | "total": D("10000.0") |
704 | } | 747 | }, |
705 | portfolio.BalanceStore.fetch_balances(market, tag="tag") | 748 | } |
749 | |||
750 | m.balances.fetch_balances(tag="tag") | ||
706 | 751 | ||
707 | helper.update_trades(market) | 752 | m.update_trades() |
708 | compute_trades.assert_called() | 753 | compute_trades.assert_called() |
709 | 754 | ||
710 | call = compute_trades.call_args | 755 | call = compute_trades.call_args |
711 | self.assertEqual(market, call[1]["market"]) | 756 | self.assertEqual(1, call[0][0]["USDT"].value) |
712 | self.assertEqual(1, call[0][0]["USDT"].value) | 757 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
713 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 758 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) |
714 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) | 759 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
715 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 760 | m.report.log_stage.assert_called_once_with("update_trades") |
716 | report_store_h.log_stage.assert_called_once_with("update_trades") | 761 | m.report.log_balances.assert_called_once_with(tag="tag") |
717 | report_store.log_balances.assert_called_once_with(market, tag="tag") | ||
718 | 762 | ||
719 | @mock.patch.object(portfolio.Portfolio, "repartition") | 763 | @mock.patch.object(portfolio.Portfolio, "repartition") |
720 | @mock.patch.object(helper, "get_ticker") | 764 | @mock.patch.object(market.Market, "get_ticker") |
721 | @mock.patch.object(portfolio.TradeStore, "compute_trades") | 765 | @mock.patch.object(market.TradeStore, "compute_trades") |
722 | @mock.patch("store.ReportStore") | 766 | def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition): |
723 | @mock.patch("helper.ReportStore") | 767 | def _get_ticker(c1, c2): |
724 | def test_prepare_trades_to_sell_all(self, report_store_h, report_store, compute_trades, get_ticker, repartition): | ||
725 | def _get_ticker(c1, c2, market): | ||
726 | if c1 == "USDT" and c2 == "BTC": | 768 | if c1 == "USDT" and c2 == "BTC": |
727 | return { "average": D("0.0001") } | 769 | return { "average": D("0.0001") } |
728 | if c1 == "XVG" and c2 == "BTC": | 770 | if c1 == "XVG" and c2 == "BTC": |
@@ -730,44 +772,47 @@ class HelperTest(WebMockTestCase): | |||
730 | self.fail("Should be called with {}, {}".format(c1, c2)) | 772 | self.fail("Should be called with {}, {}".format(c1, c2)) |
731 | get_ticker.side_effect = _get_ticker | 773 | get_ticker.side_effect = _get_ticker |
732 | 774 | ||
733 | market = mock.Mock() | 775 | with mock.patch("market.ReportStore"): |
734 | market.fetch_all_balances.return_value = { | 776 | m = market.Market(self.ccxt) |
735 | "USDT": { | 777 | self.ccxt.fetch_all_balances.return_value = { |
736 | "exchange_free": D("10000.0"), | 778 | "USDT": { |
737 | "exchange_used": D("0.0"), | 779 | "exchange_free": D("10000.0"), |
738 | "exchange_total": D("10000.0"), | 780 | "exchange_used": D("0.0"), |
739 | "total": D("10000.0") | 781 | "exchange_total": D("10000.0"), |
740 | }, | 782 | "total": D("10000.0") |
741 | "XVG": { | 783 | }, |
742 | "exchange_free": D("10000.0"), | 784 | "XVG": { |
743 | "exchange_used": D("0.0"), | 785 | "exchange_free": D("10000.0"), |
744 | "exchange_total": D("10000.0"), | 786 | "exchange_used": D("0.0"), |
745 | "total": D("10000.0") | 787 | "exchange_total": D("10000.0"), |
746 | }, | 788 | "total": D("10000.0") |
747 | } | 789 | }, |
748 | portfolio.BalanceStore.fetch_balances(market, tag="tag") | 790 | } |
791 | |||
792 | m.balances.fetch_balances(tag="tag") | ||
793 | |||
794 | m.prepare_trades_to_sell_all() | ||
749 | 795 | ||
750 | helper.prepare_trades_to_sell_all(market) | 796 | repartition.assert_not_called() |
751 | repartition.assert_not_called() | 797 | compute_trades.assert_called() |
752 | compute_trades.assert_called() | ||
753 | 798 | ||
754 | call = compute_trades.call_args | 799 | call = compute_trades.call_args |
755 | self.assertEqual(market, call[1]["market"]) | 800 | self.assertEqual(1, call[0][0]["USDT"].value) |
756 | self.assertEqual(1, call[0][0]["USDT"].value) | 801 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) |
757 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | 802 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) |
758 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) | 803 | m.report.log_stage.assert_called_once_with("prepare_trades_to_sell_all") |
759 | report_store_h.log_stage.assert_called_once_with("prepare_trades_to_sell_all") | 804 | m.report.log_balances.assert_called_once_with(tag="tag") |
760 | report_store.log_balances.assert_called_once_with(market, tag="tag") | ||
761 | 805 | ||
762 | @mock.patch.object(portfolio.time, "sleep") | 806 | @mock.patch.object(portfolio.time, "sleep") |
763 | @mock.patch.object(portfolio.TradeStore, "all_orders") | 807 | @mock.patch.object(market.TradeStore, "all_orders") |
764 | def test_follow_orders(self, all_orders, time_mock): | 808 | def test_follow_orders(self, all_orders, time_mock): |
765 | for debug, sleep in [ | 809 | for debug, sleep in [ |
766 | (False, None), (True, None), | 810 | (False, None), (True, None), |
767 | (False, 12), (True, 12)]: | 811 | (False, 12), (True, 12)]: |
768 | with self.subTest(sleep=sleep, debug=debug), \ | 812 | with self.subTest(sleep=sleep, debug=debug), \ |
769 | mock.patch("helper.ReportStore") as report_store: | 813 | mock.patch("market.ReportStore"): |
770 | portfolio.TradeStore.debug = debug | 814 | m = market.Market(self.ccxt, debug=debug) |
815 | |||
771 | order_mock1 = mock.Mock() | 816 | order_mock1 = mock.Mock() |
772 | order_mock2 = mock.Mock() | 817 | order_mock2 = mock.Mock() |
773 | order_mock3 = mock.Mock() | 818 | order_mock3 = mock.Mock() |
@@ -792,7 +837,7 @@ class HelperTest(WebMockTestCase): | |||
792 | order_mock2.trade = mock.Mock() | 837 | order_mock2.trade = mock.Mock() |
793 | order_mock3.trade = mock.Mock() | 838 | order_mock3.trade = mock.Mock() |
794 | 839 | ||
795 | helper.follow_orders(sleep=sleep) | 840 | m.follow_orders(sleep=sleep) |
796 | 841 | ||
797 | order_mock1.trade.update_order.assert_any_call(order_mock1, 1) | 842 | order_mock1.trade.update_order.assert_any_call(order_mock1, 1) |
798 | order_mock1.trade.update_order.assert_any_call(order_mock1, 2) | 843 | order_mock1.trade.update_order.assert_any_call(order_mock1, 2) |
@@ -806,7 +851,7 @@ class HelperTest(WebMockTestCase): | |||
806 | order_mock3.trade.update_order.assert_any_call(order_mock3, 2) | 851 | order_mock3.trade.update_order.assert_any_call(order_mock3, 2) |
807 | self.assertEqual(1, order_mock3.trade.update_order.call_count) | 852 | self.assertEqual(1, order_mock3.trade.update_order.call_count) |
808 | self.assertEqual(2, order_mock3.get_status.call_count) | 853 | self.assertEqual(2, order_mock3.get_status.call_count) |
809 | report_store.log_stage.assert_called() | 854 | m.report.log_stage.assert_called() |
810 | calls = [ | 855 | calls = [ |
811 | mock.call("follow_orders_begin"), | 856 | mock.call("follow_orders_begin"), |
812 | mock.call("follow_orders_tick_1"), | 857 | mock.call("follow_orders_tick_1"), |
@@ -814,285 +859,75 @@ class HelperTest(WebMockTestCase): | |||
814 | mock.call("follow_orders_tick_3"), | 859 | mock.call("follow_orders_tick_3"), |
815 | mock.call("follow_orders_end"), | 860 | mock.call("follow_orders_end"), |
816 | ] | 861 | ] |
817 | report_store.log_stage.assert_has_calls(calls) | 862 | m.report.log_stage.assert_has_calls(calls) |
818 | report_store.log_orders.assert_called() | 863 | m.report.log_orders.assert_called() |
819 | self.assertEqual(3, report_store.log_orders.call_count) | 864 | self.assertEqual(3, m.report.log_orders.call_count) |
820 | calls = [ | 865 | calls = [ |
821 | mock.call([order_mock1, order_mock2], tick=1), | 866 | mock.call([order_mock1, order_mock2], tick=1), |
822 | mock.call([order_mock1, order_mock3], tick=2), | 867 | mock.call([order_mock1, order_mock3], tick=2), |
823 | mock.call([order_mock1, order_mock3], tick=3), | 868 | mock.call([order_mock1, order_mock3], tick=3), |
824 | ] | 869 | ] |
825 | report_store.log_orders.assert_has_calls(calls) | 870 | m.report.log_orders.assert_has_calls(calls) |
826 | calls = [ | 871 | calls = [ |
827 | mock.call(order_mock1, 3, finished=True), | 872 | mock.call(order_mock1, 3, finished=True), |
828 | mock.call(order_mock3, 3, finished=True), | 873 | mock.call(order_mock3, 3, finished=True), |
829 | ] | 874 | ] |
830 | report_store.log_order.assert_has_calls(calls) | 875 | m.report.log_order.assert_has_calls(calls) |
831 | 876 | ||
832 | if sleep is None: | 877 | if sleep is None: |
833 | if debug: | 878 | if debug: |
834 | report_store.log_debug_action.assert_called_with("Set follow_orders tick to 7s") | 879 | m.report.log_debug_action.assert_called_with("Set follow_orders tick to 7s") |
835 | time_mock.assert_called_with(7) | 880 | time_mock.assert_called_with(7) |
836 | else: | 881 | else: |
837 | time_mock.assert_called_with(30) | 882 | time_mock.assert_called_with(30) |
838 | else: | 883 | else: |
839 | time_mock.assert_called_with(sleep) | 884 | time_mock.assert_called_with(sleep) |
840 | 885 | ||
841 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | 886 | @mock.patch.object(market.BalanceStore, "fetch_balances") |
842 | def test_move_balance(self, fetch_balances): | 887 | def test_move_balance(self, fetch_balances): |
843 | for debug in [True, False]: | 888 | for debug in [True, False]: |
844 | with self.subTest(debug=debug),\ | 889 | with self.subTest(debug=debug),\ |
845 | mock.patch("helper.ReportStore") as report_store: | 890 | mock.patch("market.ReportStore"): |
891 | m = market.Market(self.ccxt, debug=debug) | ||
892 | |||
846 | value_from = portfolio.Amount("BTC", "1.0") | 893 | value_from = portfolio.Amount("BTC", "1.0") |
847 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 894 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
848 | value_to = portfolio.Amount("BTC", "10.0") | 895 | value_to = portfolio.Amount("BTC", "10.0") |
849 | trade1 = portfolio.Trade(value_from, value_to, "ETH") | 896 | trade1 = portfolio.Trade(value_from, value_to, "ETH", m) |
850 | 897 | ||
851 | value_from = portfolio.Amount("BTC", "0.0") | 898 | value_from = portfolio.Amount("BTC", "0.0") |
852 | value_from.linked_to = portfolio.Amount("ETH", "0.0") | 899 | value_from.linked_to = portfolio.Amount("ETH", "0.0") |
853 | value_to = portfolio.Amount("BTC", "-3.0") | 900 | value_to = portfolio.Amount("BTC", "-3.0") |
854 | trade2 = portfolio.Trade(value_from, value_to, "ETH") | 901 | trade2 = portfolio.Trade(value_from, value_to, "ETH", m) |
855 | 902 | ||
856 | value_from = portfolio.Amount("USDT", "0.0") | 903 | value_from = portfolio.Amount("USDT", "0.0") |
857 | value_from.linked_to = portfolio.Amount("XVG", "0.0") | 904 | value_from.linked_to = portfolio.Amount("XVG", "0.0") |
858 | value_to = portfolio.Amount("USDT", "-50.0") | 905 | value_to = portfolio.Amount("USDT", "-50.0") |
859 | trade3 = portfolio.Trade(value_from, value_to, "XVG") | 906 | trade3 = portfolio.Trade(value_from, value_to, "XVG", m) |
860 | 907 | ||
861 | portfolio.TradeStore.all = [trade1, trade2, trade3] | 908 | m.trades.all = [trade1, trade2, trade3] |
862 | balance1 = portfolio.Balance("BTC", { "margin_free": "0" }) | 909 | balance1 = portfolio.Balance("BTC", { "margin_free": "0" }) |
863 | balance2 = portfolio.Balance("USDT", { "margin_free": "100" }) | 910 | balance2 = portfolio.Balance("USDT", { "margin_free": "100" }) |
864 | balance3 = portfolio.Balance("ETC", { "margin_free": "10" }) | 911 | balance3 = portfolio.Balance("ETC", { "margin_free": "10" }) |
865 | portfolio.BalanceStore.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} | 912 | m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} |
866 | |||
867 | market = mock.Mock() | ||
868 | 913 | ||
869 | helper.move_balances(market, debug=debug) | 914 | m.move_balances() |
870 | 915 | ||
871 | fetch_balances.assert_called_with(market) | 916 | fetch_balances.assert_called_with() |
872 | report_store.log_move_balances.assert_called_once() | 917 | m.report.log_move_balances.assert_called_once() |
873 | 918 | ||
874 | if debug: | 919 | if debug: |
875 | report_store.log_debug_action.assert_called() | 920 | m.report.log_debug_action.assert_called() |
876 | self.assertEqual(3, report_store.log_debug_action.call_count) | 921 | self.assertEqual(3, m.report.log_debug_action.call_count) |
877 | else: | 922 | else: |
878 | market.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 923 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
879 | market.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange") | 924 | self.ccxt.transfer_balance.assert_any_call("USDT", 50, "margin", "exchange") |
880 | market.transfer_balance.assert_any_call("ETC", 10, "margin", "exchange") | 925 | self.ccxt.transfer_balance.assert_any_call("ETC", 10, "margin", "exchange") |
881 | 926 | ||
882 | @mock.patch.object(helper, "prepare_trades") | ||
883 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | ||
884 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
885 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
886 | def test_print_orders(self, log_stage, fetch_balances, prepare_orders, prepare_trades): | ||
887 | market = mock.Mock() | ||
888 | portfolio.BalanceStore.all = { | ||
889 | "BTC": portfolio.Balance("BTC", { | ||
890 | "total": "0.65", | ||
891 | "exchange_total":"0.65", | ||
892 | "exchange_free": "0.35", | ||
893 | "exchange_used": "0.30"}), | ||
894 | "ETH": portfolio.Balance("ETH", { | ||
895 | "total": 3, | ||
896 | "exchange_total": 3, | ||
897 | "exchange_free": 3, | ||
898 | "exchange_used": 0}), | ||
899 | } | ||
900 | |||
901 | helper.print_orders(market) | ||
902 | fetch_balances.assert_called_with(market, tag="print_orders") | ||
903 | prepare_trades.assert_called_with(market, base_currency="BTC", | ||
904 | compute_value="average", debug=True) | ||
905 | prepare_orders.assert_called_with(compute_value="average") | ||
906 | log_stage.assert_called_with("print_orders") | ||
907 | |||
908 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
909 | @mock.patch.object(portfolio.BalanceStore, "in_currency") | ||
910 | @mock.patch.object(helper.ReportStore, "print_log") | ||
911 | def test_print_balances(self, print_log, in_currency, fetch_balances): | ||
912 | market = mock.Mock() | ||
913 | portfolio.BalanceStore.all = { | ||
914 | "BTC": portfolio.Balance("BTC", { | ||
915 | "total": "0.65", | ||
916 | "exchange_total":"0.65", | ||
917 | "exchange_free": "0.35", | ||
918 | "exchange_used": "0.30"}), | ||
919 | "ETH": portfolio.Balance("ETH", { | ||
920 | "total": 3, | ||
921 | "exchange_total": 3, | ||
922 | "exchange_free": 3, | ||
923 | "exchange_used": 0}), | ||
924 | } | ||
925 | in_currency.return_value = { | ||
926 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
927 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
928 | } | ||
929 | helper.print_balances(market) | ||
930 | fetch_balances.assert_called_with(market) | ||
931 | print_log.assert_has_calls([ | ||
932 | mock.call("total:"), | ||
933 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
934 | ]) | ||
935 | |||
936 | @mock.patch.object(helper, "prepare_trades") | ||
937 | @mock.patch.object(helper, "follow_orders") | ||
938 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | ||
939 | @mock.patch.object(portfolio.TradeStore, "run_orders") | ||
940 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
941 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
942 | def test_process_sell_needed__1_sell(self, log_stage, | ||
943 | fetch_balances, run_orders, prepare_orders, follow_orders, | ||
944 | prepare_trades): | ||
945 | market = mock.Mock() | ||
946 | portfolio.BalanceStore.all = { | ||
947 | "BTC": portfolio.Balance("BTC", { | ||
948 | "total": "0.65", | ||
949 | "exchange_total":"0.65", | ||
950 | "exchange_free": "0.35", | ||
951 | "exchange_used": "0.30"}), | ||
952 | "ETH": portfolio.Balance("ETH", { | ||
953 | "total": 3, | ||
954 | "exchange_total": 3, | ||
955 | "exchange_free": 3, | ||
956 | "exchange_used": 0}), | ||
957 | } | ||
958 | helper.process_sell_needed__1_sell(market) | ||
959 | fetch_balances.assert_has_calls([ | ||
960 | mock.call(market, tag="process_sell_needed__1_sell_begin"), | ||
961 | mock.call(market, tag="process_sell_needed__1_sell_end"), | ||
962 | ]) | ||
963 | prepare_trades.assert_called_with(market, base_currency="BTC", | ||
964 | liquidity="medium", debug=False) | ||
965 | prepare_orders.assert_called_with(compute_value="average", | ||
966 | only="dispose") | ||
967 | run_orders.assert_called() | ||
968 | follow_orders.assert_called() | ||
969 | log_stage.assert_called_with("process_sell_needed__1_sell_end") | ||
970 | |||
971 | @mock.patch.object(helper, "update_trades") | ||
972 | @mock.patch.object(helper, "follow_orders") | ||
973 | @mock.patch.object(helper, "move_balances") | ||
974 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | ||
975 | @mock.patch.object(portfolio.TradeStore, "run_orders") | ||
976 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
977 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
978 | def test_process_sell_needed__2_buy(self, log_stage, fetch_balances, | ||
979 | run_orders, prepare_orders, move_balances, follow_orders, | ||
980 | update_trades): | ||
981 | market = mock.Mock() | ||
982 | portfolio.BalanceStore.all = { | ||
983 | "BTC": portfolio.Balance("BTC", { | ||
984 | "total": "0.65", | ||
985 | "exchange_total":"0.65", | ||
986 | "exchange_free": "0.35", | ||
987 | "exchange_used": "0.30"}), | ||
988 | "ETH": portfolio.Balance("ETH", { | ||
989 | "total": 3, | ||
990 | "exchange_total": 3, | ||
991 | "exchange_free": 3, | ||
992 | "exchange_used": 0}), | ||
993 | } | ||
994 | helper.process_sell_needed__2_buy(market) | ||
995 | fetch_balances.assert_has_calls([ | ||
996 | mock.call(market, tag="process_sell_needed__2_buy_begin"), | ||
997 | mock.call(market, tag="process_sell_needed__2_buy_end"), | ||
998 | ]) | ||
999 | update_trades.assert_called_with(market, base_currency="BTC", | ||
1000 | debug=False, liquidity="medium", only="acquire") | ||
1001 | prepare_orders.assert_called_with(compute_value="average", | ||
1002 | only="acquire") | ||
1003 | move_balances.assert_called_with(market, debug=False) | ||
1004 | run_orders.assert_called() | ||
1005 | follow_orders.assert_called() | ||
1006 | log_stage.assert_called_with("process_sell_needed__2_buy_end") | ||
1007 | |||
1008 | @mock.patch.object(helper, "prepare_trades_to_sell_all") | ||
1009 | @mock.patch.object(helper, "follow_orders") | ||
1010 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | ||
1011 | @mock.patch.object(portfolio.TradeStore, "run_orders") | ||
1012 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
1013 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
1014 | def test_process_sell_all__1_sell(self, log_stage, fetch_balances, | ||
1015 | run_orders, prepare_orders, follow_orders, | ||
1016 | prepare_trades_to_sell_all): | ||
1017 | market = mock.Mock() | ||
1018 | portfolio.BalanceStore.all = { | ||
1019 | "BTC": portfolio.Balance("BTC", { | ||
1020 | "total": "0.65", | ||
1021 | "exchange_total":"0.65", | ||
1022 | "exchange_free": "0.35", | ||
1023 | "exchange_used": "0.30"}), | ||
1024 | "ETH": portfolio.Balance("ETH", { | ||
1025 | "total": 3, | ||
1026 | "exchange_total": 3, | ||
1027 | "exchange_free": 3, | ||
1028 | "exchange_used": 0}), | ||
1029 | } | ||
1030 | helper.process_sell_all__1_all_sell(market) | ||
1031 | fetch_balances.assert_has_calls([ | ||
1032 | mock.call(market, tag="process_sell_all__1_all_sell_begin"), | ||
1033 | mock.call(market, tag="process_sell_all__1_all_sell_end"), | ||
1034 | ]) | ||
1035 | prepare_trades_to_sell_all.assert_called_with(market, base_currency="BTC", | ||
1036 | debug=False) | ||
1037 | prepare_orders.assert_called_with(compute_value="average") | ||
1038 | run_orders.assert_called() | ||
1039 | follow_orders.assert_called() | ||
1040 | log_stage.assert_called_with("process_sell_all__1_all_sell_end") | ||
1041 | |||
1042 | @mock.patch.object(helper, "prepare_trades") | ||
1043 | @mock.patch.object(helper, "follow_orders") | ||
1044 | @mock.patch.object(helper, "move_balances") | ||
1045 | @mock.patch.object(portfolio.TradeStore, "prepare_orders") | ||
1046 | @mock.patch.object(portfolio.TradeStore, "run_orders") | ||
1047 | @mock.patch.object(portfolio.BalanceStore, "fetch_balances") | ||
1048 | @mock.patch.object(portfolio.ReportStore, "log_stage") | ||
1049 | def test_process_sell_all__2_all_buy(self, log_stage, | ||
1050 | fetch_balances, run_orders, prepare_orders, move_balances, | ||
1051 | follow_orders, prepare_trades): | ||
1052 | market = mock.Mock() | ||
1053 | portfolio.BalanceStore.all = { | ||
1054 | "BTC": portfolio.Balance("BTC", { | ||
1055 | "total": "0.65", | ||
1056 | "exchange_total":"0.65", | ||
1057 | "exchange_free": "0.35", | ||
1058 | "exchange_used": "0.30"}), | ||
1059 | "ETH": portfolio.Balance("ETH", { | ||
1060 | "total": 3, | ||
1061 | "exchange_total": 3, | ||
1062 | "exchange_free": 3, | ||
1063 | "exchange_used": 0}), | ||
1064 | } | ||
1065 | helper.process_sell_all__2_all_buy(market) | ||
1066 | fetch_balances.assert_has_calls([ | ||
1067 | mock.call(market, tag="process_sell_all__2_all_buy_begin"), | ||
1068 | mock.call(market, tag="process_sell_all__2_all_buy_end"), | ||
1069 | ]) | ||
1070 | prepare_trades.assert_called_with(market, base_currency="BTC", | ||
1071 | liquidity="medium", debug=False) | ||
1072 | prepare_orders.assert_called_with(compute_value="average") | ||
1073 | move_balances.assert_called_with(market, debug=False) | ||
1074 | run_orders.assert_called() | ||
1075 | follow_orders.assert_called() | ||
1076 | log_stage.assert_called_with("process_sell_all__2_all_buy_end") | ||
1077 | |||
1078 | def test_reset_all(self): | ||
1079 | portfolio.BalanceStore.all = { "foo": "bar" } | ||
1080 | portfolio.ReportStore.logs.append("hey") | ||
1081 | portfolio.TradeStore.all.append("bouh") | ||
1082 | |||
1083 | helper.reset_all() | ||
1084 | |||
1085 | self.assertEqual(0, len(portfolio.BalanceStore.all)) | ||
1086 | self.assertEqual(0, len(portfolio.ReportStore.logs)) | ||
1087 | self.assertEqual(0, len(portfolio.TradeStore.all)) | ||
1088 | |||
1089 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 927 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
1090 | class TradeStoreTest(WebMockTestCase): | 928 | class TradeStoreTest(WebMockTestCase): |
1091 | @mock.patch.object(portfolio.BalanceStore, "currencies") | 929 | def test_compute_trades(self): |
1092 | @mock.patch.object(portfolio.TradeStore, "trade_if_matching") | 930 | self.m.balances.currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] |
1093 | @mock.patch.object(portfolio.ReportStore, "log_trades") | ||
1094 | def test_compute_trades(self, log_trades, trade_if_matching, currencies): | ||
1095 | currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"] | ||
1096 | 931 | ||
1097 | values_in_base = { | 932 | values_in_base = { |
1098 | "XMR": portfolio.Amount("BTC", D("0.9")), | 933 | "XMR": portfolio.Amount("BTC", D("0.9")), |
@@ -1113,112 +948,121 @@ class TradeStoreTest(WebMockTestCase): | |||
1113 | (True, 4), | 948 | (True, 4), |
1114 | (True, 5) | 949 | (True, 5) |
1115 | ] | 950 | ] |
1116 | trade_if_matching.side_effect = side_effect | ||
1117 | 951 | ||
1118 | portfolio.TradeStore.compute_trades(values_in_base, | 952 | with mock.patch.object(market.TradeStore, "trade_if_matching") as trade_if_matching: |
1119 | new_repartition, only="only", market="market") | 953 | trade_store = market.TradeStore(self.m) |
954 | trade_if_matching.side_effect = side_effect | ||
1120 | 955 | ||
1121 | self.assertEqual(5, trade_if_matching.call_count) | 956 | trade_store.compute_trades(values_in_base, |
1122 | self.assertEqual(3, len(portfolio.TradeStore.all)) | 957 | new_repartition, only="only") |
1123 | self.assertEqual([1, 4, 5], portfolio.TradeStore.all) | 958 | |
1124 | log_trades.assert_called_with(side_effect, "only", False) | 959 | self.assertEqual(5, trade_if_matching.call_count) |
960 | self.assertEqual(3, len(trade_store.all)) | ||
961 | self.assertEqual([1, 4, 5], trade_store.all) | ||
962 | self.m.report.log_trades.assert_called_with(side_effect, "only") | ||
1125 | 963 | ||
1126 | def test_trade_if_matching(self): | 964 | def test_trade_if_matching(self): |
1127 | result = portfolio.TradeStore.trade_if_matching( | 965 | |
1128 | portfolio.Amount("BTC", D("0")), | 966 | with self.subTest(only="nope"): |
1129 | portfolio.Amount("BTC", D("0.3")), | 967 | trade_store = market.TradeStore(self.m) |
1130 | "ETH", only="nope", market="market" | 968 | result = trade_store.trade_if_matching( |
1131 | ) | 969 | portfolio.Amount("BTC", D("0")), |
1132 | self.assertEqual(False, result[0]) | 970 | portfolio.Amount("BTC", D("0.3")), |
1133 | self.assertIsInstance(result[1], portfolio.Trade) | 971 | "ETH", only="nope") |
1134 | 972 | self.assertEqual(False, result[0]) | |
1135 | portfolio.TradeStore.all = [] | 973 | self.assertIsInstance(result[1], portfolio.Trade) |
1136 | result = portfolio.TradeStore.trade_if_matching( | 974 | |
1137 | portfolio.Amount("BTC", D("0")), | 975 | with self.subTest(only=None): |
1138 | portfolio.Amount("BTC", D("0.3")), | 976 | trade_store = market.TradeStore(self.m) |
1139 | "ETH", only=None, market="market" | 977 | result = trade_store.trade_if_matching( |
1140 | ) | 978 | portfolio.Amount("BTC", D("0")), |
1141 | self.assertEqual(True, result[0]) | 979 | portfolio.Amount("BTC", D("0.3")), |
1142 | 980 | "ETH", only=None) | |
1143 | portfolio.TradeStore.all = [] | 981 | self.assertEqual(True, result[0]) |
1144 | result = portfolio.TradeStore.trade_if_matching( | 982 | |
1145 | portfolio.Amount("BTC", D("0")), | 983 | with self.subTest(only="acquire"): |
1146 | portfolio.Amount("BTC", D("0.3")), | 984 | trade_store = market.TradeStore(self.m) |
1147 | "ETH", only="acquire", market="market" | 985 | result = trade_store.trade_if_matching( |
1148 | ) | 986 | portfolio.Amount("BTC", D("0")), |
1149 | self.assertEqual(True, result[0]) | 987 | portfolio.Amount("BTC", D("0.3")), |
1150 | 988 | "ETH", only="acquire") | |
1151 | portfolio.TradeStore.all = [] | 989 | self.assertEqual(True, result[0]) |
1152 | result = portfolio.TradeStore.trade_if_matching( | 990 | |
1153 | portfolio.Amount("BTC", D("0")), | 991 | with self.subTest(only="dispose"): |
1154 | portfolio.Amount("BTC", D("0.3")), | 992 | trade_store = market.TradeStore(self.m) |
1155 | "ETH", only="dispose", market="market" | 993 | result = trade_store.trade_if_matching( |
1156 | ) | 994 | portfolio.Amount("BTC", D("0")), |
1157 | self.assertEqual(False, result[0]) | 995 | portfolio.Amount("BTC", D("0.3")), |
1158 | 996 | "ETH", only="dispose") | |
1159 | @mock.patch.object(portfolio.ReportStore, "log_orders") | 997 | self.assertEqual(False, result[0]) |
1160 | def test_prepare_orders(self, log_orders): | 998 | |
999 | def test_prepare_orders(self): | ||
1000 | trade_store = market.TradeStore(self.m) | ||
1001 | |||
1161 | trade_mock1 = mock.Mock() | 1002 | trade_mock1 = mock.Mock() |
1162 | trade_mock2 = mock.Mock() | 1003 | trade_mock2 = mock.Mock() |
1163 | 1004 | ||
1164 | trade_mock1.prepare_order.return_value = 1 | 1005 | trade_mock1.prepare_order.return_value = 1 |
1165 | trade_mock2.prepare_order.return_value = 2 | 1006 | trade_mock2.prepare_order.return_value = 2 |
1166 | 1007 | ||
1167 | portfolio.TradeStore.all.append(trade_mock1) | 1008 | trade_store.all.append(trade_mock1) |
1168 | portfolio.TradeStore.all.append(trade_mock2) | 1009 | trade_store.all.append(trade_mock2) |
1169 | 1010 | ||
1170 | portfolio.TradeStore.prepare_orders() | 1011 | trade_store.prepare_orders() |
1171 | trade_mock1.prepare_order.assert_called_with(compute_value="default") | 1012 | trade_mock1.prepare_order.assert_called_with(compute_value="default") |
1172 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | 1013 | trade_mock2.prepare_order.assert_called_with(compute_value="default") |
1173 | log_orders.assert_called_once_with([1, 2], None, "default") | 1014 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "default") |
1174 | 1015 | ||
1175 | log_orders.reset_mock() | 1016 | self.m.report.log_orders.reset_mock() |
1176 | 1017 | ||
1177 | portfolio.TradeStore.prepare_orders(compute_value="bla") | 1018 | trade_store.prepare_orders(compute_value="bla") |
1178 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") | 1019 | trade_mock1.prepare_order.assert_called_with(compute_value="bla") |
1179 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") | 1020 | trade_mock2.prepare_order.assert_called_with(compute_value="bla") |
1180 | log_orders.assert_called_once_with([1, 2], None, "bla") | 1021 | self.m.report.log_orders.assert_called_once_with([1, 2], None, "bla") |
1181 | 1022 | ||
1182 | trade_mock1.prepare_order.reset_mock() | 1023 | trade_mock1.prepare_order.reset_mock() |
1183 | trade_mock2.prepare_order.reset_mock() | 1024 | trade_mock2.prepare_order.reset_mock() |
1184 | log_orders.reset_mock() | 1025 | self.m.report.log_orders.reset_mock() |
1185 | 1026 | ||
1186 | trade_mock1.action = "foo" | 1027 | trade_mock1.action = "foo" |
1187 | trade_mock2.action = "bar" | 1028 | trade_mock2.action = "bar" |
1188 | portfolio.TradeStore.prepare_orders(only="bar") | 1029 | trade_store.prepare_orders(only="bar") |
1189 | trade_mock1.prepare_order.assert_not_called() | 1030 | trade_mock1.prepare_order.assert_not_called() |
1190 | trade_mock2.prepare_order.assert_called_with(compute_value="default") | 1031 | trade_mock2.prepare_order.assert_called_with(compute_value="default") |
1191 | log_orders.assert_called_once_with([2], "bar", "default") | 1032 | self.m.report.log_orders.assert_called_once_with([2], "bar", "default") |
1192 | 1033 | ||
1193 | def test_print_all_with_order(self): | 1034 | def test_print_all_with_order(self): |
1194 | trade_mock1 = mock.Mock() | 1035 | trade_mock1 = mock.Mock() |
1195 | trade_mock2 = mock.Mock() | 1036 | trade_mock2 = mock.Mock() |
1196 | trade_mock3 = mock.Mock() | 1037 | trade_mock3 = mock.Mock() |
1197 | portfolio.TradeStore.all = [trade_mock1, trade_mock2, trade_mock3] | 1038 | trade_store = market.TradeStore(self.m) |
1039 | trade_store.all = [trade_mock1, trade_mock2, trade_mock3] | ||
1198 | 1040 | ||
1199 | portfolio.TradeStore.print_all_with_order() | 1041 | trade_store.print_all_with_order() |
1200 | 1042 | ||
1201 | trade_mock1.print_with_order.assert_called() | 1043 | trade_mock1.print_with_order.assert_called() |
1202 | trade_mock2.print_with_order.assert_called() | 1044 | trade_mock2.print_with_order.assert_called() |
1203 | trade_mock3.print_with_order.assert_called() | 1045 | trade_mock3.print_with_order.assert_called() |
1204 | 1046 | ||
1205 | @mock.patch.object(portfolio.ReportStore, "log_stage") | 1047 | def test_run_orders(self): |
1206 | @mock.patch.object(portfolio.ReportStore, "log_orders") | 1048 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: |
1207 | @mock.patch.object(portfolio.TradeStore, "all_orders") | 1049 | order_mock1 = mock.Mock() |
1208 | def test_run_orders(self, all_orders, log_orders, log_stage): | 1050 | order_mock2 = mock.Mock() |
1209 | order_mock1 = mock.Mock() | 1051 | order_mock3 = mock.Mock() |
1210 | order_mock2 = mock.Mock() | 1052 | trade_store = market.TradeStore(self.m) |
1211 | order_mock3 = mock.Mock() | 1053 | |
1212 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | 1054 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] |
1213 | portfolio.TradeStore.run_orders() | 1055 | |
1214 | all_orders.assert_called_with(state="pending") | 1056 | trade_store.run_orders() |
1057 | |||
1058 | all_orders.assert_called_with(state="pending") | ||
1215 | 1059 | ||
1216 | order_mock1.run.assert_called() | 1060 | order_mock1.run.assert_called() |
1217 | order_mock2.run.assert_called() | 1061 | order_mock2.run.assert_called() |
1218 | order_mock3.run.assert_called() | 1062 | order_mock3.run.assert_called() |
1219 | 1063 | ||
1220 | log_stage.assert_called_with("run_orders") | 1064 | self.m.report.log_stage.assert_called_with("run_orders") |
1221 | log_orders.assert_called_with([order_mock1, order_mock2, | 1065 | self.m.report.log_orders.assert_called_with([order_mock1, order_mock2, |
1222 | order_mock3]) | 1066 | order_mock3]) |
1223 | 1067 | ||
1224 | def test_all_orders(self): | 1068 | def test_all_orders(self): |
@@ -1236,28 +1080,33 @@ class TradeStoreTest(WebMockTestCase): | |||
1236 | order_mock2.status = "open" | 1080 | order_mock2.status = "open" |
1237 | order_mock3.status = "open" | 1081 | order_mock3.status = "open" |
1238 | 1082 | ||
1239 | portfolio.TradeStore.all.append(trade_mock1) | 1083 | trade_store = market.TradeStore(self.m) |
1240 | portfolio.TradeStore.all.append(trade_mock2) | 1084 | trade_store.all.append(trade_mock1) |
1085 | trade_store.all.append(trade_mock2) | ||
1241 | 1086 | ||
1242 | orders = portfolio.TradeStore.all_orders() | 1087 | orders = trade_store.all_orders() |
1243 | self.assertEqual(3, len(orders)) | 1088 | self.assertEqual(3, len(orders)) |
1244 | 1089 | ||
1245 | open_orders = portfolio.TradeStore.all_orders(state="open") | 1090 | open_orders = trade_store.all_orders(state="open") |
1246 | self.assertEqual(2, len(open_orders)) | 1091 | self.assertEqual(2, len(open_orders)) |
1247 | self.assertEqual([order_mock2, order_mock3], open_orders) | 1092 | self.assertEqual([order_mock2, order_mock3], open_orders) |
1248 | 1093 | ||
1249 | @mock.patch.object(portfolio.TradeStore, "all_orders") | 1094 | def test_update_all_orders_status(self): |
1250 | def test_update_all_orders_status(self, all_orders): | 1095 | with mock.patch.object(market.TradeStore, "all_orders") as all_orders: |
1251 | order_mock1 = mock.Mock() | 1096 | order_mock1 = mock.Mock() |
1252 | order_mock2 = mock.Mock() | 1097 | order_mock2 = mock.Mock() |
1253 | order_mock3 = mock.Mock() | 1098 | order_mock3 = mock.Mock() |
1254 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] | ||
1255 | portfolio.TradeStore.update_all_orders_status() | ||
1256 | all_orders.assert_called_with(state="open") | ||
1257 | 1099 | ||
1258 | order_mock1.get_status.assert_called() | 1100 | all_orders.return_value = [order_mock1, order_mock2, order_mock3] |
1259 | order_mock2.get_status.assert_called() | 1101 | |
1260 | order_mock3.get_status.assert_called() | 1102 | trade_store = market.TradeStore(self.m) |
1103 | |||
1104 | trade_store.update_all_orders_status() | ||
1105 | all_orders.assert_called_with(state="open") | ||
1106 | |||
1107 | order_mock1.get_status.assert_called() | ||
1108 | order_mock2.get_status.assert_called() | ||
1109 | order_mock3.get_status.assert_called() | ||
1261 | 1110 | ||
1262 | 1111 | ||
1263 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1112 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
@@ -1293,10 +1142,15 @@ class BalanceStoreTest(WebMockTestCase): | |||
1293 | }, | 1142 | }, |
1294 | } | 1143 | } |
1295 | 1144 | ||
1296 | @mock.patch.object(helper, "get_ticker") | 1145 | def test_in_currency(self): |
1297 | @mock.patch("portfolio.ReportStore.log_tickers") | 1146 | self.m.get_ticker.return_value = { |
1298 | def test_in_currency(self, log_tickers, get_ticker): | 1147 | "bid": D("0.09"), |
1299 | portfolio.BalanceStore.all = { | 1148 | "ask": D("0.11"), |
1149 | "average": D("0.1"), | ||
1150 | } | ||
1151 | |||
1152 | balance_store = market.BalanceStore(self.m) | ||
1153 | balance_store.all = { | ||
1300 | "BTC": portfolio.Balance("BTC", { | 1154 | "BTC": portfolio.Balance("BTC", { |
1301 | "total": "0.65", | 1155 | "total": "0.65", |
1302 | "exchange_total":"0.65", | 1156 | "exchange_total":"0.65", |
@@ -1308,61 +1162,54 @@ class BalanceStoreTest(WebMockTestCase): | |||
1308 | "exchange_free": 3, | 1162 | "exchange_free": 3, |
1309 | "exchange_used": 0}), | 1163 | "exchange_used": 0}), |
1310 | } | 1164 | } |
1311 | market = mock.Mock() | ||
1312 | get_ticker.return_value = { | ||
1313 | "bid": D("0.09"), | ||
1314 | "ask": D("0.11"), | ||
1315 | "average": D("0.1"), | ||
1316 | } | ||
1317 | 1165 | ||
1318 | amounts = portfolio.BalanceStore.in_currency("BTC", market) | 1166 | amounts = balance_store.in_currency("BTC") |
1319 | self.assertEqual("BTC", amounts["ETH"].currency) | 1167 | self.assertEqual("BTC", amounts["ETH"].currency) |
1320 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 1168 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
1321 | self.assertEqual(D("0.30"), amounts["ETH"].value) | 1169 | self.assertEqual(D("0.30"), amounts["ETH"].value) |
1322 | log_tickers.assert_called_once_with(market, amounts, "BTC", | 1170 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", |
1323 | "average", "total") | 1171 | "average", "total") |
1324 | log_tickers.reset_mock() | 1172 | self.m.report.log_tickers.reset_mock() |
1325 | 1173 | ||
1326 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid") | 1174 | amounts = balance_store.in_currency("BTC", compute_value="bid") |
1327 | self.assertEqual(D("0.65"), amounts["BTC"].value) | 1175 | self.assertEqual(D("0.65"), amounts["BTC"].value) |
1328 | self.assertEqual(D("0.27"), amounts["ETH"].value) | 1176 | self.assertEqual(D("0.27"), amounts["ETH"].value) |
1329 | log_tickers.assert_called_once_with(market, amounts, "BTC", | 1177 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", |
1330 | "bid", "total") | 1178 | "bid", "total") |
1331 | log_tickers.reset_mock() | 1179 | self.m.report.log_tickers.reset_mock() |
1332 | 1180 | ||
1333 | amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid", type="exchange_used") | 1181 | amounts = balance_store.in_currency("BTC", compute_value="bid", type="exchange_used") |
1334 | self.assertEqual(D("0.30"), amounts["BTC"].value) | 1182 | self.assertEqual(D("0.30"), amounts["BTC"].value) |
1335 | self.assertEqual(0, amounts["ETH"].value) | 1183 | self.assertEqual(0, amounts["ETH"].value) |
1336 | log_tickers.assert_called_once_with(market, amounts, "BTC", | 1184 | self.m.report.log_tickers.assert_called_once_with(amounts, "BTC", |
1337 | "bid", "exchange_used") | 1185 | "bid", "exchange_used") |
1338 | log_tickers.reset_mock() | 1186 | self.m.report.log_tickers.reset_mock() |
1339 | 1187 | ||
1340 | @mock.patch.object(portfolio.ReportStore, "log_balances") | 1188 | def test_fetch_balances(self): |
1341 | def test_fetch_balances(self, log_balances): | 1189 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
1342 | market = mock.Mock() | ||
1343 | market.fetch_all_balances.return_value = self.fetch_balance | ||
1344 | 1190 | ||
1345 | portfolio.BalanceStore.fetch_balances(market) | 1191 | balance_store = market.BalanceStore(self.m) |
1346 | self.assertNotIn("ETC", portfolio.BalanceStore.currencies()) | ||
1347 | self.assertListEqual(["USDT", "XVG", "XMR"], list(portfolio.BalanceStore.currencies())) | ||
1348 | 1192 | ||
1349 | portfolio.BalanceStore.all["ETC"] = portfolio.Balance("ETC", { | 1193 | balance_store.fetch_balances() |
1194 | self.assertNotIn("ETC", balance_store.currencies()) | ||
1195 | self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies())) | ||
1196 | |||
1197 | balance_store.all["ETC"] = portfolio.Balance("ETC", { | ||
1350 | "exchange_total": "1", "exchange_free": "0", | 1198 | "exchange_total": "1", "exchange_free": "0", |
1351 | "exchange_used": "1" }) | 1199 | "exchange_used": "1" }) |
1352 | portfolio.BalanceStore.fetch_balances(market, tag="foo") | 1200 | balance_store.fetch_balances(tag="foo") |
1353 | self.assertEqual(0, portfolio.BalanceStore.all["ETC"].total) | 1201 | self.assertEqual(0, balance_store.all["ETC"].total) |
1354 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.BalanceStore.currencies())) | 1202 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) |
1355 | log_balances.assert_called_with(market, tag="foo") | 1203 | self.m.report.log_balances.assert_called_with(tag="foo") |
1356 | 1204 | ||
1357 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1205 | @mock.patch.object(portfolio.Portfolio, "repartition") |
1358 | @mock.patch.object(portfolio.ReportStore, "log_balances") | 1206 | def test_dispatch_assets(self, repartition): |
1359 | @mock.patch("store.ReportStore.log_dispatch") | 1207 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
1360 | def test_dispatch_assets(self, log_dispatch, log_balances, repartition): | ||
1361 | market = mock.Mock() | ||
1362 | market.fetch_all_balances.return_value = self.fetch_balance | ||
1363 | portfolio.BalanceStore.fetch_balances(market) | ||
1364 | 1208 | ||
1365 | self.assertNotIn("XEM", portfolio.BalanceStore.currencies()) | 1209 | balance_store = market.BalanceStore(self.m) |
1210 | balance_store.fetch_balances() | ||
1211 | |||
1212 | self.assertNotIn("XEM", balance_store.currencies()) | ||
1366 | 1213 | ||
1367 | repartition_hash = { | 1214 | repartition_hash = { |
1368 | "XEM": (D("0.75"), "long"), | 1215 | "XEM": (D("0.75"), "long"), |
@@ -1371,18 +1218,20 @@ class BalanceStoreTest(WebMockTestCase): | |||
1371 | } | 1218 | } |
1372 | repartition.return_value = repartition_hash | 1219 | repartition.return_value = repartition_hash |
1373 | 1220 | ||
1374 | amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1")) | 1221 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) |
1375 | repartition.assert_called_with(liquidity="medium") | 1222 | repartition.assert_called_with(self.m, liquidity="medium") |
1376 | self.assertIn("XEM", portfolio.BalanceStore.currencies()) | 1223 | self.assertIn("XEM", balance_store.currencies()) |
1377 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 1224 | self.assertEqual(D("2.6"), amounts["BTC"].value) |
1378 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 1225 | self.assertEqual(D("7.5"), amounts["XEM"].value) |
1379 | self.assertEqual(D("-1.0"), amounts["DASH"].value) | 1226 | self.assertEqual(D("-1.0"), amounts["DASH"].value) |
1380 | log_balances.assert_called_with(market, tag=None) | 1227 | self.m.report.log_balances.assert_called_with(tag=None) |
1381 | log_dispatch.assert_called_once_with(portfolio.Amount("BTC", | 1228 | self.m.report.log_dispatch.assert_called_once_with(portfolio.Amount("BTC", |
1382 | "11.1"), amounts, "medium", repartition_hash) | 1229 | "11.1"), amounts, "medium", repartition_hash) |
1383 | 1230 | ||
1384 | def test_currencies(self): | 1231 | def test_currencies(self): |
1385 | portfolio.BalanceStore.all = { | 1232 | balance_store = market.BalanceStore(self.m) |
1233 | |||
1234 | balance_store.all = { | ||
1386 | "BTC": portfolio.Balance("BTC", { | 1235 | "BTC": portfolio.Balance("BTC", { |
1387 | "total": "0.65", | 1236 | "total": "0.65", |
1388 | "exchange_total":"0.65", | 1237 | "exchange_total":"0.65", |
@@ -1394,7 +1243,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1394 | "exchange_free": 3, | 1243 | "exchange_free": 3, |
1395 | "exchange_used": 0}), | 1244 | "exchange_used": 0}), |
1396 | } | 1245 | } |
1397 | self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies())) | 1246 | self.assertListEqual(["BTC", "ETH"], list(balance_store.currencies())) |
1398 | 1247 | ||
1399 | def test_as_json(self): | 1248 | def test_as_json(self): |
1400 | balance_mock1 = mock.Mock() | 1249 | balance_mock1 = mock.Mock() |
@@ -1403,12 +1252,13 @@ class BalanceStoreTest(WebMockTestCase): | |||
1403 | balance_mock2 = mock.Mock() | 1252 | balance_mock2 = mock.Mock() |
1404 | balance_mock2.as_json.return_value = 2 | 1253 | balance_mock2.as_json.return_value = 2 |
1405 | 1254 | ||
1406 | portfolio.BalanceStore.all = { | 1255 | balance_store = market.BalanceStore(self.m) |
1256 | balance_store.all = { | ||
1407 | "BTC": balance_mock1, | 1257 | "BTC": balance_mock1, |
1408 | "ETH": balance_mock2, | 1258 | "ETH": balance_mock2, |
1409 | } | 1259 | } |
1410 | 1260 | ||
1411 | as_json = portfolio.BalanceStore.as_json() | 1261 | as_json = balance_store.as_json() |
1412 | self.assertEqual(1, as_json["BTC"]) | 1262 | self.assertEqual(1, as_json["BTC"]) |
1413 | self.assertEqual(2, as_json["ETH"]) | 1263 | self.assertEqual(2, as_json["ETH"]) |
1414 | 1264 | ||
@@ -1445,49 +1295,50 @@ class TradeTest(WebMockTestCase): | |||
1445 | value_from = portfolio.Amount("BTC", "1.0") | 1295 | value_from = portfolio.Amount("BTC", "1.0") |
1446 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1296 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1447 | value_to = portfolio.Amount("BTC", "1.0") | 1297 | value_to = portfolio.Amount("BTC", "1.0") |
1448 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1298 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1449 | self.assertEqual("BTC", trade.base_currency) | 1299 | self.assertEqual("BTC", trade.base_currency) |
1450 | self.assertEqual("ETH", trade.currency) | 1300 | self.assertEqual("ETH", trade.currency) |
1301 | self.assertEqual(self.m, trade.market) | ||
1451 | 1302 | ||
1452 | with self.assertRaises(AssertionError): | 1303 | with self.assertRaises(AssertionError): |
1453 | portfolio.Trade(value_from, value_to, "ETC") | 1304 | portfolio.Trade(value_from, value_to, "ETC", self.m) |
1454 | with self.assertRaises(AssertionError): | 1305 | with self.assertRaises(AssertionError): |
1455 | value_from.linked_to = None | 1306 | value_from.linked_to = None |
1456 | portfolio.Trade(value_from, value_to, "ETH") | 1307 | portfolio.Trade(value_from, value_to, "ETH", self.m) |
1457 | with self.assertRaises(AssertionError): | 1308 | with self.assertRaises(AssertionError): |
1458 | value_from.currency = "ETH" | 1309 | value_from.currency = "ETH" |
1459 | portfolio.Trade(value_from, value_to, "ETH") | 1310 | portfolio.Trade(value_from, value_to, "ETH", self.m) |
1460 | 1311 | ||
1461 | value_from = portfolio.Amount("BTC", 0) | 1312 | value_from = portfolio.Amount("BTC", 0) |
1462 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1313 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1463 | self.assertEqual(0, trade.value_from.linked_to) | 1314 | self.assertEqual(0, trade.value_from.linked_to) |
1464 | 1315 | ||
1465 | def test_action(self): | 1316 | def test_action(self): |
1466 | value_from = portfolio.Amount("BTC", "1.0") | 1317 | value_from = portfolio.Amount("BTC", "1.0") |
1467 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1318 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1468 | value_to = portfolio.Amount("BTC", "1.0") | 1319 | value_to = portfolio.Amount("BTC", "1.0") |
1469 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1320 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1470 | 1321 | ||
1471 | self.assertIsNone(trade.action) | 1322 | self.assertIsNone(trade.action) |
1472 | 1323 | ||
1473 | value_from = portfolio.Amount("BTC", "1.0") | 1324 | value_from = portfolio.Amount("BTC", "1.0") |
1474 | value_from.linked_to = portfolio.Amount("BTC", "1.0") | 1325 | value_from.linked_to = portfolio.Amount("BTC", "1.0") |
1475 | value_to = portfolio.Amount("BTC", "2.0") | 1326 | value_to = portfolio.Amount("BTC", "2.0") |
1476 | trade = portfolio.Trade(value_from, value_to, "BTC") | 1327 | trade = portfolio.Trade(value_from, value_to, "BTC", self.m) |
1477 | 1328 | ||
1478 | self.assertIsNone(trade.action) | 1329 | self.assertIsNone(trade.action) |
1479 | 1330 | ||
1480 | value_from = portfolio.Amount("BTC", "0.5") | 1331 | value_from = portfolio.Amount("BTC", "0.5") |
1481 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1332 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1482 | value_to = portfolio.Amount("BTC", "1.0") | 1333 | value_to = portfolio.Amount("BTC", "1.0") |
1483 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1334 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1484 | 1335 | ||
1485 | self.assertEqual("acquire", trade.action) | 1336 | self.assertEqual("acquire", trade.action) |
1486 | 1337 | ||
1487 | value_from = portfolio.Amount("BTC", "0") | 1338 | value_from = portfolio.Amount("BTC", "0") |
1488 | value_from.linked_to = portfolio.Amount("ETH", "0") | 1339 | value_from.linked_to = portfolio.Amount("ETH", "0") |
1489 | value_to = portfolio.Amount("BTC", "-1.0") | 1340 | value_to = portfolio.Amount("BTC", "-1.0") |
1490 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1341 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1491 | 1342 | ||
1492 | self.assertEqual("acquire", trade.action) | 1343 | self.assertEqual("acquire", trade.action) |
1493 | 1344 | ||
@@ -1495,7 +1346,7 @@ class TradeTest(WebMockTestCase): | |||
1495 | value_from = portfolio.Amount("BTC", "0.5") | 1346 | value_from = portfolio.Amount("BTC", "0.5") |
1496 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1347 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1497 | value_to = portfolio.Amount("BTC", "1.0") | 1348 | value_to = portfolio.Amount("BTC", "1.0") |
1498 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1349 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1499 | 1350 | ||
1500 | self.assertEqual("buy", trade.order_action(False)) | 1351 | self.assertEqual("buy", trade.order_action(False)) |
1501 | self.assertEqual("sell", trade.order_action(True)) | 1352 | self.assertEqual("sell", trade.order_action(True)) |
@@ -1503,7 +1354,7 @@ class TradeTest(WebMockTestCase): | |||
1503 | value_from = portfolio.Amount("BTC", "0") | 1354 | value_from = portfolio.Amount("BTC", "0") |
1504 | value_from.linked_to = portfolio.Amount("ETH", "0") | 1355 | value_from.linked_to = portfolio.Amount("ETH", "0") |
1505 | value_to = portfolio.Amount("BTC", "-1.0") | 1356 | value_to = portfolio.Amount("BTC", "-1.0") |
1506 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1357 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1507 | 1358 | ||
1508 | self.assertEqual("sell", trade.order_action(False)) | 1359 | self.assertEqual("sell", trade.order_action(False)) |
1509 | self.assertEqual("buy", trade.order_action(True)) | 1360 | self.assertEqual("buy", trade.order_action(True)) |
@@ -1512,14 +1363,14 @@ class TradeTest(WebMockTestCase): | |||
1512 | value_from = portfolio.Amount("BTC", "0.5") | 1363 | value_from = portfolio.Amount("BTC", "0.5") |
1513 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1364 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1514 | value_to = portfolio.Amount("BTC", "1.0") | 1365 | value_to = portfolio.Amount("BTC", "1.0") |
1515 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1366 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1516 | 1367 | ||
1517 | self.assertEqual("long", trade.trade_type) | 1368 | self.assertEqual("long", trade.trade_type) |
1518 | 1369 | ||
1519 | value_from = portfolio.Amount("BTC", "0") | 1370 | value_from = portfolio.Amount("BTC", "0") |
1520 | value_from.linked_to = portfolio.Amount("ETH", "0") | 1371 | value_from.linked_to = portfolio.Amount("ETH", "0") |
1521 | value_to = portfolio.Amount("BTC", "-1.0") | 1372 | value_to = portfolio.Amount("BTC", "-1.0") |
1522 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1373 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1523 | 1374 | ||
1524 | self.assertEqual("short", trade.trade_type) | 1375 | self.assertEqual("short", trade.trade_type) |
1525 | 1376 | ||
@@ -1527,7 +1378,7 @@ class TradeTest(WebMockTestCase): | |||
1527 | value_from = portfolio.Amount("BTC", "0.5") | 1378 | value_from = portfolio.Amount("BTC", "0.5") |
1528 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1379 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1529 | value_to = portfolio.Amount("BTC", "1.0") | 1380 | value_to = portfolio.Amount("BTC", "1.0") |
1530 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1381 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1531 | 1382 | ||
1532 | order1 = mock.Mock() | 1383 | order1 = mock.Mock() |
1533 | order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3") | 1384 | order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3") |
@@ -1549,11 +1400,10 @@ class TradeTest(WebMockTestCase): | |||
1549 | order1.filled_amount.assert_called_with(in_base_currency=True) | 1400 | order1.filled_amount.assert_called_with(in_base_currency=True) |
1550 | order2.filled_amount.assert_called_with(in_base_currency=True) | 1401 | order2.filled_amount.assert_called_with(in_base_currency=True) |
1551 | 1402 | ||
1552 | @mock.patch.object(helper, "get_ticker") | ||
1553 | @mock.patch.object(portfolio.Computation, "compute_value") | 1403 | @mock.patch.object(portfolio.Computation, "compute_value") |
1554 | @mock.patch.object(portfolio.Trade, "filled_amount") | 1404 | @mock.patch.object(portfolio.Trade, "filled_amount") |
1555 | @mock.patch.object(portfolio, "Order") | 1405 | @mock.patch.object(portfolio, "Order") |
1556 | def test_prepare_order(self, Order, filled_amount, compute_value, get_ticker): | 1406 | def test_prepare_order(self, Order, filled_amount, compute_value): |
1557 | Order.return_value = "Order" | 1407 | Order.return_value = "Order" |
1558 | 1408 | ||
1559 | with self.subTest(desc="Nothing to do"): | 1409 | with self.subTest(desc="Nothing to do"): |
@@ -1561,7 +1411,7 @@ class TradeTest(WebMockTestCase): | |||
1561 | value_from.rate = D("0.1") | 1411 | value_from.rate = D("0.1") |
1562 | value_from.linked_to = portfolio.Amount("FOO", "100") | 1412 | value_from.linked_to = portfolio.Amount("FOO", "100") |
1563 | value_to = portfolio.Amount("BTC", "10") | 1413 | value_to = portfolio.Amount("BTC", "10") |
1564 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1414 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1565 | 1415 | ||
1566 | trade.prepare_order() | 1416 | trade.prepare_order() |
1567 | 1417 | ||
@@ -1570,9 +1420,8 @@ class TradeTest(WebMockTestCase): | |||
1570 | self.assertEqual(0, len(trade.orders)) | 1420 | self.assertEqual(0, len(trade.orders)) |
1571 | Order.assert_not_called() | 1421 | Order.assert_not_called() |
1572 | 1422 | ||
1573 | get_ticker.return_value = { "inverted": False } | 1423 | self.m.get_ticker.return_value = { "inverted": False } |
1574 | with self.subTest(desc="Already filled"),\ | 1424 | with self.subTest(desc="Already filled"): |
1575 | mock.patch("portfolio.ReportStore") as report_store: | ||
1576 | filled_amount.return_value = portfolio.Amount("FOO", "100") | 1425 | filled_amount.return_value = portfolio.Amount("FOO", "100") |
1577 | compute_value.return_value = D("0.125") | 1426 | compute_value.return_value = D("0.125") |
1578 | 1427 | ||
@@ -1580,14 +1429,14 @@ class TradeTest(WebMockTestCase): | |||
1580 | value_from.rate = D("0.1") | 1429 | value_from.rate = D("0.1") |
1581 | value_from.linked_to = portfolio.Amount("FOO", "100") | 1430 | value_from.linked_to = portfolio.Amount("FOO", "100") |
1582 | value_to = portfolio.Amount("BTC", "0") | 1431 | value_to = portfolio.Amount("BTC", "0") |
1583 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1432 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1584 | 1433 | ||
1585 | trade.prepare_order() | 1434 | trade.prepare_order() |
1586 | 1435 | ||
1587 | filled_amount.assert_called_with(in_base_currency=False) | 1436 | filled_amount.assert_called_with(in_base_currency=False) |
1588 | compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default") | 1437 | compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default") |
1589 | self.assertEqual(0, len(trade.orders)) | 1438 | self.assertEqual(0, len(trade.orders)) |
1590 | report_store.log_error.assert_called_with("prepare_order", message=mock.ANY) | 1439 | self.m.report.log_error.assert_called_with("prepare_order", message=mock.ANY) |
1591 | Order.assert_not_called() | 1440 | Order.assert_not_called() |
1592 | 1441 | ||
1593 | with self.subTest(action="dispose", inverted=False): | 1442 | with self.subTest(action="dispose", inverted=False): |
@@ -1598,15 +1447,15 @@ class TradeTest(WebMockTestCase): | |||
1598 | value_from.rate = D("0.1") | 1447 | value_from.rate = D("0.1") |
1599 | value_from.linked_to = portfolio.Amount("FOO", "100") | 1448 | value_from.linked_to = portfolio.Amount("FOO", "100") |
1600 | value_to = portfolio.Amount("BTC", "1") | 1449 | value_to = portfolio.Amount("BTC", "1") |
1601 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1450 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1602 | 1451 | ||
1603 | trade.prepare_order() | 1452 | trade.prepare_order() |
1604 | 1453 | ||
1605 | filled_amount.assert_called_with(in_base_currency=False) | 1454 | filled_amount.assert_called_with(in_base_currency=False) |
1606 | compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default") | 1455 | compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default") |
1607 | self.assertEqual(1, len(trade.orders)) | 1456 | self.assertEqual(1, len(trade.orders)) |
1608 | Order.assert_called_with("sell", portfolio.Amount("FOO", 30), | 1457 | Order.assert_called_with("sell", portfolio.Amount("FOO", 30), |
1609 | D("0.125"), "BTC", "long", "market", | 1458 | D("0.125"), "BTC", "long", self.m, |
1610 | trade, close_if_possible=False) | 1459 | trade, close_if_possible=False) |
1611 | 1460 | ||
1612 | with self.subTest(action="acquire", inverted=False): | 1461 | with self.subTest(action="acquire", inverted=False): |
@@ -1617,16 +1466,16 @@ class TradeTest(WebMockTestCase): | |||
1617 | value_from.rate = D("0.1") | 1466 | value_from.rate = D("0.1") |
1618 | value_from.linked_to = portfolio.Amount("FOO", "10") | 1467 | value_from.linked_to = portfolio.Amount("FOO", "10") |
1619 | value_to = portfolio.Amount("BTC", "10") | 1468 | value_to = portfolio.Amount("BTC", "10") |
1620 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1469 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1621 | 1470 | ||
1622 | trade.prepare_order() | 1471 | trade.prepare_order() |
1623 | 1472 | ||
1624 | filled_amount.assert_called_with(in_base_currency=True) | 1473 | filled_amount.assert_called_with(in_base_currency=True) |
1625 | compute_value.assert_called_with(get_ticker.return_value, "buy", compute_value="default") | 1474 | compute_value.assert_called_with(self.m.get_ticker.return_value, "buy", compute_value="default") |
1626 | self.assertEqual(1, len(trade.orders)) | 1475 | self.assertEqual(1, len(trade.orders)) |
1627 | 1476 | ||
1628 | Order.assert_called_with("buy", portfolio.Amount("FOO", 48), | 1477 | Order.assert_called_with("buy", portfolio.Amount("FOO", 48), |
1629 | D("0.125"), "BTC", "long", "market", | 1478 | D("0.125"), "BTC", "long", self.m, |
1630 | trade, close_if_possible=False) | 1479 | trade, close_if_possible=False) |
1631 | 1480 | ||
1632 | with self.subTest(close_if_possible=True): | 1481 | with self.subTest(close_if_possible=True): |
@@ -1637,18 +1486,18 @@ class TradeTest(WebMockTestCase): | |||
1637 | value_from.rate = D("0.1") | 1486 | value_from.rate = D("0.1") |
1638 | value_from.linked_to = portfolio.Amount("FOO", "100") | 1487 | value_from.linked_to = portfolio.Amount("FOO", "100") |
1639 | value_to = portfolio.Amount("BTC", "0") | 1488 | value_to = portfolio.Amount("BTC", "0") |
1640 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1489 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1641 | 1490 | ||
1642 | trade.prepare_order() | 1491 | trade.prepare_order() |
1643 | 1492 | ||
1644 | filled_amount.assert_called_with(in_base_currency=False) | 1493 | filled_amount.assert_called_with(in_base_currency=False) |
1645 | compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default") | 1494 | compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default") |
1646 | self.assertEqual(1, len(trade.orders)) | 1495 | self.assertEqual(1, len(trade.orders)) |
1647 | Order.assert_called_with("sell", portfolio.Amount("FOO", 100), | 1496 | Order.assert_called_with("sell", portfolio.Amount("FOO", 100), |
1648 | D("0.125"), "BTC", "long", "market", | 1497 | D("0.125"), "BTC", "long", self.m, |
1649 | trade, close_if_possible=True) | 1498 | trade, close_if_possible=True) |
1650 | 1499 | ||
1651 | get_ticker.return_value = { "inverted": True, "original": {} } | 1500 | self.m.get_ticker.return_value = { "inverted": True, "original": {} } |
1652 | with self.subTest(action="dispose", inverted=True): | 1501 | with self.subTest(action="dispose", inverted=True): |
1653 | filled_amount.return_value = portfolio.Amount("FOO", "300") | 1502 | filled_amount.return_value = portfolio.Amount("FOO", "300") |
1654 | compute_value.return_value = D("125") | 1503 | compute_value.return_value = D("125") |
@@ -1657,15 +1506,15 @@ class TradeTest(WebMockTestCase): | |||
1657 | value_from.rate = D("0.01") | 1506 | value_from.rate = D("0.01") |
1658 | value_from.linked_to = portfolio.Amount("FOO", "1000") | 1507 | value_from.linked_to = portfolio.Amount("FOO", "1000") |
1659 | value_to = portfolio.Amount("BTC", "1") | 1508 | value_to = portfolio.Amount("BTC", "1") |
1660 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1509 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1661 | 1510 | ||
1662 | trade.prepare_order(compute_value="foo") | 1511 | trade.prepare_order(compute_value="foo") |
1663 | 1512 | ||
1664 | filled_amount.assert_called_with(in_base_currency=True) | 1513 | filled_amount.assert_called_with(in_base_currency=True) |
1665 | compute_value.assert_called_with(get_ticker.return_value["original"], "buy", compute_value="foo") | 1514 | compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "buy", compute_value="foo") |
1666 | self.assertEqual(1, len(trade.orders)) | 1515 | self.assertEqual(1, len(trade.orders)) |
1667 | Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")), | 1516 | Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")), |
1668 | D("125"), "FOO", "long", "market", | 1517 | D("125"), "FOO", "long", self.m, |
1669 | trade, close_if_possible=False) | 1518 | trade, close_if_possible=False) |
1670 | 1519 | ||
1671 | with self.subTest(action="acquire", inverted=True): | 1520 | with self.subTest(action="acquire", inverted=True): |
@@ -1676,15 +1525,15 @@ class TradeTest(WebMockTestCase): | |||
1676 | value_from.rate = D("0.01") | 1525 | value_from.rate = D("0.01") |
1677 | value_from.linked_to = portfolio.Amount("FOO", "100") | 1526 | value_from.linked_to = portfolio.Amount("FOO", "100") |
1678 | value_to = portfolio.Amount("BTC", "10") | 1527 | value_to = portfolio.Amount("BTC", "10") |
1679 | trade = portfolio.Trade(value_from, value_to, "FOO", market="market") | 1528 | trade = portfolio.Trade(value_from, value_to, "FOO", self.m) |
1680 | 1529 | ||
1681 | trade.prepare_order(compute_value="foo") | 1530 | trade.prepare_order(compute_value="foo") |
1682 | 1531 | ||
1683 | filled_amount.assert_called_with(in_base_currency=False) | 1532 | filled_amount.assert_called_with(in_base_currency=False) |
1684 | compute_value.assert_called_with(get_ticker.return_value["original"], "sell", compute_value="foo") | 1533 | compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "sell", compute_value="foo") |
1685 | self.assertEqual(1, len(trade.orders)) | 1534 | self.assertEqual(1, len(trade.orders)) |
1686 | Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")), | 1535 | Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")), |
1687 | D("125"), "FOO", "long", "market", | 1536 | D("125"), "FOO", "long", self.m, |
1688 | trade, close_if_possible=False) | 1537 | trade, close_if_possible=False) |
1689 | 1538 | ||
1690 | 1539 | ||
@@ -1696,118 +1545,119 @@ class TradeTest(WebMockTestCase): | |||
1696 | value_from = portfolio.Amount("BTC", "0.5") | 1545 | value_from = portfolio.Amount("BTC", "0.5") |
1697 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1546 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1698 | value_to = portfolio.Amount("BTC", "1.0") | 1547 | value_to = portfolio.Amount("BTC", "1.0") |
1699 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1548 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1700 | prepare_order.return_value = new_order_mock | 1549 | prepare_order.return_value = new_order_mock |
1701 | 1550 | ||
1702 | for i in [0, 1, 3, 4, 6]: | 1551 | for i in [0, 1, 3, 4, 6]: |
1703 | with self.subTest(tick=i),\ | 1552 | with self.subTest(tick=i): |
1704 | mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | ||
1705 | trade.update_order(order_mock, i) | 1553 | trade.update_order(order_mock, i) |
1706 | order_mock.cancel.assert_not_called() | 1554 | order_mock.cancel.assert_not_called() |
1707 | new_order_mock.run.assert_not_called() | 1555 | new_order_mock.run.assert_not_called() |
1708 | log_order.assert_called_once_with(order_mock, i, | 1556 | self.m.report.log_order.assert_called_once_with(order_mock, i, |
1709 | update="waiting", compute_value=None, new_order=None) | 1557 | update="waiting", compute_value=None, new_order=None) |
1710 | 1558 | ||
1711 | order_mock.reset_mock() | 1559 | order_mock.reset_mock() |
1712 | new_order_mock.reset_mock() | 1560 | new_order_mock.reset_mock() |
1713 | trade.orders = [] | 1561 | trade.orders = [] |
1714 | 1562 | self.m.report.log_order.reset_mock() | |
1715 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | 1563 | |
1716 | trade.update_order(order_mock, 2) | 1564 | trade.update_order(order_mock, 2) |
1717 | order_mock.cancel.assert_called() | 1565 | order_mock.cancel.assert_called() |
1718 | new_order_mock.run.assert_called() | 1566 | new_order_mock.run.assert_called() |
1719 | prepare_order.assert_called() | 1567 | prepare_order.assert_called() |
1720 | log_order.assert_called() | 1568 | self.m.report.log_order.assert_called() |
1721 | self.assertEqual(2, log_order.call_count) | 1569 | self.assertEqual(2, self.m.report.log_order.call_count) |
1722 | calls = [ | 1570 | calls = [ |
1723 | mock.call(order_mock, 2, update="adjusting", | 1571 | mock.call(order_mock, 2, update="adjusting", |
1724 | compute_value='lambda x, y: (x[y] + x["average"]) / 2', | 1572 | compute_value='lambda x, y: (x[y] + x["average"]) / 2', |
1725 | new_order=new_order_mock), | 1573 | new_order=new_order_mock), |
1726 | mock.call(order_mock, 2, new_order=new_order_mock), | 1574 | mock.call(order_mock, 2, new_order=new_order_mock), |
1727 | ] | 1575 | ] |
1728 | log_order.assert_has_calls(calls) | 1576 | self.m.report.log_order.assert_has_calls(calls) |
1729 | 1577 | ||
1730 | order_mock.reset_mock() | 1578 | order_mock.reset_mock() |
1731 | new_order_mock.reset_mock() | 1579 | new_order_mock.reset_mock() |
1732 | trade.orders = [] | 1580 | trade.orders = [] |
1733 | 1581 | self.m.report.log_order.reset_mock() | |
1734 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | 1582 | |
1735 | trade.update_order(order_mock, 5) | 1583 | trade.update_order(order_mock, 5) |
1736 | order_mock.cancel.assert_called() | 1584 | order_mock.cancel.assert_called() |
1737 | new_order_mock.run.assert_called() | 1585 | new_order_mock.run.assert_called() |
1738 | prepare_order.assert_called() | 1586 | prepare_order.assert_called() |
1739 | self.assertEqual(2, log_order.call_count) | 1587 | self.assertEqual(2, self.m.report.log_order.call_count) |
1740 | log_order.assert_called() | 1588 | self.m.report.log_order.assert_called() |
1741 | calls = [ | 1589 | calls = [ |
1742 | mock.call(order_mock, 5, update="adjusting", | 1590 | mock.call(order_mock, 5, update="adjusting", |
1743 | compute_value='lambda x, y: (x[y]*2 + x["average"]) / 3', | 1591 | compute_value='lambda x, y: (x[y]*2 + x["average"]) / 3', |
1744 | new_order=new_order_mock), | 1592 | new_order=new_order_mock), |
1745 | mock.call(order_mock, 5, new_order=new_order_mock), | 1593 | mock.call(order_mock, 5, new_order=new_order_mock), |
1746 | ] | 1594 | ] |
1747 | log_order.assert_has_calls(calls) | 1595 | self.m.report.log_order.assert_has_calls(calls) |
1748 | 1596 | ||
1749 | order_mock.reset_mock() | 1597 | order_mock.reset_mock() |
1750 | new_order_mock.reset_mock() | 1598 | new_order_mock.reset_mock() |
1751 | trade.orders = [] | 1599 | trade.orders = [] |
1752 | 1600 | self.m.report.log_order.reset_mock() | |
1753 | with mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | 1601 | |
1754 | trade.update_order(order_mock, 7) | 1602 | trade.update_order(order_mock, 7) |
1755 | order_mock.cancel.assert_called() | 1603 | order_mock.cancel.assert_called() |
1756 | new_order_mock.run.assert_called() | 1604 | new_order_mock.run.assert_called() |
1757 | prepare_order.assert_called_with(compute_value="default") | 1605 | prepare_order.assert_called_with(compute_value="default") |
1758 | log_order.assert_called() | 1606 | self.m.report.log_order.assert_called() |
1759 | self.assertEqual(2, log_order.call_count) | 1607 | self.assertEqual(2, self.m.report.log_order.call_count) |
1760 | calls = [ | 1608 | calls = [ |
1761 | mock.call(order_mock, 7, update="market_fallback", | 1609 | mock.call(order_mock, 7, update="market_fallback", |
1762 | compute_value='default', | 1610 | compute_value='default', |
1763 | new_order=new_order_mock), | 1611 | new_order=new_order_mock), |
1764 | mock.call(order_mock, 7, new_order=new_order_mock), | 1612 | mock.call(order_mock, 7, new_order=new_order_mock), |
1765 | ] | 1613 | ] |
1766 | log_order.assert_has_calls(calls) | 1614 | self.m.report.log_order.assert_has_calls(calls) |
1767 | 1615 | ||
1768 | order_mock.reset_mock() | 1616 | order_mock.reset_mock() |
1769 | new_order_mock.reset_mock() | 1617 | new_order_mock.reset_mock() |
1770 | trade.orders = [] | 1618 | trade.orders = [] |
1619 | self.m.report.log_order.reset_mock() | ||
1771 | 1620 | ||
1772 | for i in [10, 13, 16]: | 1621 | for i in [10, 13, 16]: |
1773 | with self.subTest(tick=i), mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | 1622 | with self.subTest(tick=i): |
1774 | trade.update_order(order_mock, i) | 1623 | trade.update_order(order_mock, i) |
1775 | order_mock.cancel.assert_called() | 1624 | order_mock.cancel.assert_called() |
1776 | new_order_mock.run.assert_called() | 1625 | new_order_mock.run.assert_called() |
1777 | prepare_order.assert_called_with(compute_value="default") | 1626 | prepare_order.assert_called_with(compute_value="default") |
1778 | log_order.assert_called() | 1627 | self.m.report.log_order.assert_called() |
1779 | self.assertEqual(2, log_order.call_count) | 1628 | self.assertEqual(2, self.m.report.log_order.call_count) |
1780 | calls = [ | 1629 | calls = [ |
1781 | mock.call(order_mock, i, update="market_adjust", | 1630 | mock.call(order_mock, i, update="market_adjust", |
1782 | compute_value='default', | 1631 | compute_value='default', |
1783 | new_order=new_order_mock), | 1632 | new_order=new_order_mock), |
1784 | mock.call(order_mock, i, new_order=new_order_mock), | 1633 | mock.call(order_mock, i, new_order=new_order_mock), |
1785 | ] | 1634 | ] |
1786 | log_order.assert_has_calls(calls) | 1635 | self.m.report.log_order.assert_has_calls(calls) |
1787 | 1636 | ||
1788 | order_mock.reset_mock() | 1637 | order_mock.reset_mock() |
1789 | new_order_mock.reset_mock() | 1638 | new_order_mock.reset_mock() |
1790 | trade.orders = [] | 1639 | trade.orders = [] |
1640 | self.m.report.log_order.reset_mock() | ||
1791 | 1641 | ||
1792 | for i in [8, 9, 11, 12]: | 1642 | for i in [8, 9, 11, 12]: |
1793 | with self.subTest(tick=i), mock.patch.object(portfolio.ReportStore, "log_order") as log_order: | 1643 | with self.subTest(tick=i): |
1794 | trade.update_order(order_mock, i) | 1644 | trade.update_order(order_mock, i) |
1795 | order_mock.cancel.assert_not_called() | 1645 | order_mock.cancel.assert_not_called() |
1796 | new_order_mock.run.assert_not_called() | 1646 | new_order_mock.run.assert_not_called() |
1797 | log_order.assert_called_once_with(order_mock, i, update="waiting", | 1647 | self.m.report.log_order.assert_called_once_with(order_mock, i, update="waiting", |
1798 | compute_value=None, new_order=None) | 1648 | compute_value=None, new_order=None) |
1799 | 1649 | ||
1800 | order_mock.reset_mock() | 1650 | order_mock.reset_mock() |
1801 | new_order_mock.reset_mock() | 1651 | new_order_mock.reset_mock() |
1802 | trade.orders = [] | 1652 | trade.orders = [] |
1653 | self.m.report.log_order.reset_mock() | ||
1803 | 1654 | ||
1804 | 1655 | ||
1805 | @mock.patch.object(portfolio.ReportStore, "print_log") | 1656 | def test_print_with_order(self): |
1806 | def test_print_with_order(self, print_log): | ||
1807 | value_from = portfolio.Amount("BTC", "0.5") | 1657 | value_from = portfolio.Amount("BTC", "0.5") |
1808 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1658 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1809 | value_to = portfolio.Amount("BTC", "1.0") | 1659 | value_to = portfolio.Amount("BTC", "1.0") |
1810 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1660 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1811 | 1661 | ||
1812 | order_mock1 = mock.Mock() | 1662 | order_mock1 = mock.Mock() |
1813 | order_mock1.__repr__ = mock.Mock() | 1663 | order_mock1.__repr__ = mock.Mock() |
@@ -1830,8 +1680,8 @@ class TradeTest(WebMockTestCase): | |||
1830 | 1680 | ||
1831 | trade.print_with_order() | 1681 | trade.print_with_order() |
1832 | 1682 | ||
1833 | print_log.assert_called() | 1683 | self.m.report.print_log.assert_called() |
1834 | calls = print_log.mock_calls | 1684 | calls = self.m.report.print_log.mock_calls |
1835 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0])) | 1685 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0])) |
1836 | self.assertEqual("\tMock 1", str(calls[1][1][0])) | 1686 | self.assertEqual("\tMock 1", str(calls[1][1][0])) |
1837 | self.assertEqual("\tMock 2", str(calls[2][1][0])) | 1687 | self.assertEqual("\tMock 2", str(calls[2][1][0])) |
@@ -1842,7 +1692,7 @@ class TradeTest(WebMockTestCase): | |||
1842 | value_from = portfolio.Amount("BTC", "0.5") | 1692 | value_from = portfolio.Amount("BTC", "0.5") |
1843 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1693 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1844 | value_to = portfolio.Amount("BTC", "1.0") | 1694 | value_to = portfolio.Amount("BTC", "1.0") |
1845 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1695 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1846 | 1696 | ||
1847 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade)) | 1697 | self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade)) |
1848 | 1698 | ||
@@ -1850,7 +1700,7 @@ class TradeTest(WebMockTestCase): | |||
1850 | value_from = portfolio.Amount("BTC", "0.5") | 1700 | value_from = portfolio.Amount("BTC", "0.5") |
1851 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1701 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
1852 | value_to = portfolio.Amount("BTC", "1.0") | 1702 | value_to = portfolio.Amount("BTC", "1.0") |
1853 | trade = portfolio.Trade(value_from, value_to, "ETH") | 1703 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) |
1854 | 1704 | ||
1855 | as_json = trade.as_json() | 1705 | as_json = trade.as_json() |
1856 | self.assertEqual("acquire", as_json["action"]) | 1706 | self.assertEqual("acquire", as_json["action"]) |
@@ -1942,34 +1792,32 @@ class OrderTest(WebMockTestCase): | |||
1942 | self.assertTrue(order.finished) | 1792 | self.assertTrue(order.finished) |
1943 | 1793 | ||
1944 | @mock.patch.object(portfolio.Order, "fetch") | 1794 | @mock.patch.object(portfolio.Order, "fetch") |
1945 | @mock.patch("portfolio.ReportStore") | 1795 | def test_cancel(self, fetch): |
1946 | def test_cancel(self, report_store, fetch): | 1796 | self.m.debug = True |
1947 | market = mock.Mock() | ||
1948 | portfolio.TradeStore.debug = True | ||
1949 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1797 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1950 | D("0.1"), "BTC", "long", market, "trade") | 1798 | D("0.1"), "BTC", "long", self.m, "trade") |
1951 | order.status = "open" | 1799 | order.status = "open" |
1952 | 1800 | ||
1953 | order.cancel() | 1801 | order.cancel() |
1954 | market.cancel_order.assert_not_called() | 1802 | self.m.ccxt.cancel_order.assert_not_called() |
1955 | report_store.log_debug_action.assert_called_once() | 1803 | self.m.report.log_debug_action.assert_called_once() |
1956 | report_store.log_debug_action.reset_mock() | 1804 | self.m.report.log_debug_action.reset_mock() |
1957 | self.assertEqual("canceled", order.status) | 1805 | self.assertEqual("canceled", order.status) |
1958 | 1806 | ||
1959 | portfolio.TradeStore.debug = False | 1807 | self.m.debug = False |
1960 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1808 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1961 | D("0.1"), "BTC", "long", market, "trade") | 1809 | D("0.1"), "BTC", "long", self.m, "trade") |
1962 | order.status = "open" | 1810 | order.status = "open" |
1963 | order.id = 42 | 1811 | order.id = 42 |
1964 | 1812 | ||
1965 | order.cancel() | 1813 | order.cancel() |
1966 | market.cancel_order.assert_called_with(42) | 1814 | self.m.ccxt.cancel_order.assert_called_with(42) |
1967 | fetch.assert_called_once() | 1815 | fetch.assert_called_once() |
1968 | report_store.log_debug_action.assert_not_called() | 1816 | self.m.report.log_debug_action.assert_not_called() |
1969 | 1817 | ||
1970 | def test_dust_amount_remaining(self): | 1818 | def test_dust_amount_remaining(self): |
1971 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1819 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1972 | D("0.1"), "BTC", "long", "market", "trade") | 1820 | D("0.1"), "BTC", "long", self.m, "trade") |
1973 | order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", 1)) | 1821 | order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", 1)) |
1974 | self.assertFalse(order.dust_amount_remaining()) | 1822 | self.assertFalse(order.dust_amount_remaining()) |
1975 | 1823 | ||
@@ -1980,7 +1828,7 @@ class OrderTest(WebMockTestCase): | |||
1980 | @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1)) | 1828 | @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1)) |
1981 | def test_remaining_amount(self, filled_amount, fetch): | 1829 | def test_remaining_amount(self, filled_amount, fetch): |
1982 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1830 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1983 | D("0.1"), "BTC", "long", "market", "trade") | 1831 | D("0.1"), "BTC", "long", self.m, "trade") |
1984 | 1832 | ||
1985 | self.assertEqual(9, order.remaining_amount().value) | 1833 | self.assertEqual(9, order.remaining_amount().value) |
1986 | order.fetch.assert_not_called() | 1834 | order.fetch.assert_not_called() |
@@ -1992,7 +1840,7 @@ class OrderTest(WebMockTestCase): | |||
1992 | @mock.patch.object(portfolio.Order, "fetch") | 1840 | @mock.patch.object(portfolio.Order, "fetch") |
1993 | def test_filled_amount(self, fetch): | 1841 | def test_filled_amount(self, fetch): |
1994 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1842 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
1995 | D("0.1"), "BTC", "long", "market", "trade") | 1843 | D("0.1"), "BTC", "long", self.m, "trade") |
1996 | order.mouvements.append(portfolio.Mouvement("ETH", "BTC", { | 1844 | order.mouvements.append(portfolio.Mouvement("ETH", "BTC", { |
1997 | "tradeID": 42, "type": "buy", "fee": "0.0015", | 1845 | "tradeID": 42, "type": "buy", "fee": "0.0015", |
1998 | "date": "2017-12-30 12:00:12", "rate": "0.1", | 1846 | "date": "2017-12-30 12:00:12", "rate": "0.1", |
@@ -2011,8 +1859,7 @@ class OrderTest(WebMockTestCase): | |||
2011 | self.assertEqual(portfolio.Amount("BTC", "0.7"), order.filled_amount(in_base_currency=True)) | 1859 | self.assertEqual(portfolio.Amount("BTC", "0.7"), order.filled_amount(in_base_currency=True)) |
2012 | 1860 | ||
2013 | def test_fetch_mouvements(self): | 1861 | def test_fetch_mouvements(self): |
2014 | market = mock.Mock() | 1862 | self.m.ccxt.privatePostReturnOrderTrades.return_value = [ |
2015 | market.privatePostReturnOrderTrades.return_value = [ | ||
2016 | { | 1863 | { |
2017 | "tradeID": 42, "type": "buy", "fee": "0.0015", | 1864 | "tradeID": 42, "type": "buy", "fee": "0.0015", |
2018 | "date": "2017-12-30 12:00:12", "rate": "0.1", | 1865 | "date": "2017-12-30 12:00:12", "rate": "0.1", |
@@ -2025,148 +1872,143 @@ class OrderTest(WebMockTestCase): | |||
2025 | } | 1872 | } |
2026 | ] | 1873 | ] |
2027 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1874 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2028 | D("0.1"), "BTC", "long", market, "trade") | 1875 | D("0.1"), "BTC", "long", self.m, "trade") |
2029 | order.id = 12 | 1876 | order.id = 12 |
2030 | order.mouvements = ["Foo", "Bar", "Baz"] | 1877 | order.mouvements = ["Foo", "Bar", "Baz"] |
2031 | 1878 | ||
2032 | order.fetch_mouvements() | 1879 | order.fetch_mouvements() |
2033 | 1880 | ||
2034 | market.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12}) | 1881 | self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12}) |
2035 | self.assertEqual(2, len(order.mouvements)) | 1882 | self.assertEqual(2, len(order.mouvements)) |
2036 | self.assertEqual(42, order.mouvements[0].id) | 1883 | self.assertEqual(42, order.mouvements[0].id) |
2037 | self.assertEqual(43, order.mouvements[1].id) | 1884 | self.assertEqual(43, order.mouvements[1].id) |
2038 | 1885 | ||
2039 | market.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError | 1886 | self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError |
2040 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1887 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2041 | D("0.1"), "BTC", "long", market, "trade") | 1888 | D("0.1"), "BTC", "long", self.m, "trade") |
2042 | order.fetch_mouvements() | 1889 | order.fetch_mouvements() |
2043 | self.assertEqual(0, len(order.mouvements)) | 1890 | self.assertEqual(0, len(order.mouvements)) |
2044 | 1891 | ||
2045 | @mock.patch("portfolio.ReportStore") | 1892 | def test_mark_finished_order(self): |
2046 | def test_mark_finished_order(self, report_store): | ||
2047 | market = mock.Mock() | ||
2048 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1893 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2049 | D("0.1"), "BTC", "short", market, "trade", | 1894 | D("0.1"), "BTC", "short", self.m, "trade", |
2050 | close_if_possible=True) | 1895 | close_if_possible=True) |
2051 | order.status = "closed" | 1896 | order.status = "closed" |
2052 | portfolio.TradeStore.debug = False | 1897 | self.m.debug = False |
2053 | 1898 | ||
2054 | order.mark_finished_order() | 1899 | order.mark_finished_order() |
2055 | market.close_margin_position.assert_called_with("ETH", "BTC") | 1900 | self.m.ccxt.close_margin_position.assert_called_with("ETH", "BTC") |
2056 | market.close_margin_position.reset_mock() | 1901 | self.m.ccxt.close_margin_position.reset_mock() |
2057 | 1902 | ||
2058 | order.status = "open" | 1903 | order.status = "open" |
2059 | order.mark_finished_order() | 1904 | order.mark_finished_order() |
2060 | market.close_margin_position.assert_not_called() | 1905 | self.m.ccxt.close_margin_position.assert_not_called() |
2061 | 1906 | ||
2062 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1907 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2063 | D("0.1"), "BTC", "short", market, "trade", | 1908 | D("0.1"), "BTC", "short", self.m, "trade", |
2064 | close_if_possible=False) | 1909 | close_if_possible=False) |
2065 | order.status = "closed" | 1910 | order.status = "closed" |
2066 | order.mark_finished_order() | 1911 | order.mark_finished_order() |
2067 | market.close_margin_position.assert_not_called() | 1912 | self.m.ccxt.close_margin_position.assert_not_called() |
2068 | 1913 | ||
2069 | order = portfolio.Order("sell", portfolio.Amount("ETH", 10), | 1914 | order = portfolio.Order("sell", portfolio.Amount("ETH", 10), |
2070 | D("0.1"), "BTC", "short", market, "trade", | 1915 | D("0.1"), "BTC", "short", self.m, "trade", |
2071 | close_if_possible=True) | 1916 | close_if_possible=True) |
2072 | order.status = "closed" | 1917 | order.status = "closed" |
2073 | order.mark_finished_order() | 1918 | order.mark_finished_order() |
2074 | market.close_margin_position.assert_not_called() | 1919 | self.m.ccxt.close_margin_position.assert_not_called() |
2075 | 1920 | ||
2076 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1921 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2077 | D("0.1"), "BTC", "long", market, "trade", | 1922 | D("0.1"), "BTC", "long", self.m, "trade", |
2078 | close_if_possible=True) | 1923 | close_if_possible=True) |
2079 | order.status = "closed" | 1924 | order.status = "closed" |
2080 | order.mark_finished_order() | 1925 | order.mark_finished_order() |
2081 | market.close_margin_position.assert_not_called() | 1926 | self.m.ccxt.close_margin_position.assert_not_called() |
2082 | 1927 | ||
2083 | portfolio.TradeStore.debug = True | 1928 | self.m.debug = True |
2084 | 1929 | ||
2085 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1930 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2086 | D("0.1"), "BTC", "short", market, "trade", | 1931 | D("0.1"), "BTC", "short", self.m, "trade", |
2087 | close_if_possible=True) | 1932 | close_if_possible=True) |
2088 | order.status = "closed" | 1933 | order.status = "closed" |
2089 | 1934 | ||
2090 | order.mark_finished_order() | 1935 | order.mark_finished_order() |
2091 | market.close_margin_position.assert_not_called() | 1936 | self.m.ccxt.close_margin_position.assert_not_called() |
2092 | report_store.log_debug_action.assert_called_once() | 1937 | self.m.report.log_debug_action.assert_called_once() |
2093 | 1938 | ||
2094 | @mock.patch.object(portfolio.Order, "fetch_mouvements") | 1939 | @mock.patch.object(portfolio.Order, "fetch_mouvements") |
2095 | @mock.patch("portfolio.ReportStore") | 1940 | def test_fetch(self, fetch_mouvements): |
2096 | def test_fetch(self, report_store, fetch_mouvements): | ||
2097 | time = self.time.time() | 1941 | time = self.time.time() |
2098 | with mock.patch.object(portfolio.time, "time") as time_mock: | 1942 | with mock.patch.object(portfolio.time, "time") as time_mock: |
2099 | market = mock.Mock() | ||
2100 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 1943 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2101 | D("0.1"), "BTC", "long", market, "trade") | 1944 | D("0.1"), "BTC", "long", self.m, "trade") |
2102 | order.id = 45 | 1945 | order.id = 45 |
2103 | with self.subTest(debug=True): | 1946 | with self.subTest(debug=True): |
2104 | portfolio.TradeStore.debug = True | 1947 | self.m.debug = True |
2105 | order.fetch() | 1948 | order.fetch() |
2106 | time_mock.assert_not_called() | 1949 | time_mock.assert_not_called() |
2107 | report_store.log_debug_action.assert_called_once() | 1950 | self.m.report.log_debug_action.assert_called_once() |
2108 | report_store.log_debug_action.reset_mock() | 1951 | self.m.report.log_debug_action.reset_mock() |
2109 | order.fetch(force=True) | 1952 | order.fetch(force=True) |
2110 | time_mock.assert_not_called() | 1953 | time_mock.assert_not_called() |
2111 | market.fetch_order.assert_not_called() | 1954 | self.m.ccxt.fetch_order.assert_not_called() |
2112 | fetch_mouvements.assert_not_called() | 1955 | fetch_mouvements.assert_not_called() |
2113 | report_store.log_debug_action.assert_called_once() | 1956 | self.m.report.log_debug_action.assert_called_once() |
2114 | report_store.log_debug_action.reset_mock() | 1957 | self.m.report.log_debug_action.reset_mock() |
2115 | self.assertIsNone(order.fetch_cache_timestamp) | 1958 | self.assertIsNone(order.fetch_cache_timestamp) |
2116 | 1959 | ||
2117 | with self.subTest(debug=False): | 1960 | with self.subTest(debug=False): |
2118 | portfolio.TradeStore.debug = False | 1961 | self.m.debug = False |
2119 | time_mock.return_value = time | 1962 | time_mock.return_value = time |
2120 | market.fetch_order.return_value = { | 1963 | self.m.ccxt.fetch_order.return_value = { |
2121 | "status": "foo", | 1964 | "status": "foo", |
2122 | "datetime": "timestamp" | 1965 | "datetime": "timestamp" |
2123 | } | 1966 | } |
2124 | order.fetch() | 1967 | order.fetch() |
2125 | 1968 | ||
2126 | market.fetch_order.assert_called_once() | 1969 | self.m.ccxt.fetch_order.assert_called_once() |
2127 | fetch_mouvements.assert_called_once() | 1970 | fetch_mouvements.assert_called_once() |
2128 | self.assertEqual("foo", order.status) | 1971 | self.assertEqual("foo", order.status) |
2129 | self.assertEqual("timestamp", order.timestamp) | 1972 | self.assertEqual("timestamp", order.timestamp) |
2130 | self.assertEqual(time, order.fetch_cache_timestamp) | 1973 | self.assertEqual(time, order.fetch_cache_timestamp) |
2131 | self.assertEqual(1, len(order.results)) | 1974 | self.assertEqual(1, len(order.results)) |
2132 | 1975 | ||
2133 | market.fetch_order.reset_mock() | 1976 | self.m.ccxt.fetch_order.reset_mock() |
2134 | fetch_mouvements.reset_mock() | 1977 | fetch_mouvements.reset_mock() |
2135 | 1978 | ||
2136 | time_mock.return_value = time + 8 | 1979 | time_mock.return_value = time + 8 |
2137 | order.fetch() | 1980 | order.fetch() |
2138 | market.fetch_order.assert_not_called() | 1981 | self.m.ccxt.fetch_order.assert_not_called() |
2139 | fetch_mouvements.assert_not_called() | 1982 | fetch_mouvements.assert_not_called() |
2140 | 1983 | ||
2141 | order.fetch(force=True) | 1984 | order.fetch(force=True) |
2142 | market.fetch_order.assert_called_once() | 1985 | self.m.ccxt.fetch_order.assert_called_once() |
2143 | fetch_mouvements.assert_called_once() | 1986 | fetch_mouvements.assert_called_once() |
2144 | 1987 | ||
2145 | market.fetch_order.reset_mock() | 1988 | self.m.ccxt.fetch_order.reset_mock() |
2146 | fetch_mouvements.reset_mock() | 1989 | fetch_mouvements.reset_mock() |
2147 | 1990 | ||
2148 | time_mock.return_value = time + 19 | 1991 | time_mock.return_value = time + 19 |
2149 | order.fetch() | 1992 | order.fetch() |
2150 | market.fetch_order.assert_called_once() | 1993 | self.m.ccxt.fetch_order.assert_called_once() |
2151 | fetch_mouvements.assert_called_once() | 1994 | fetch_mouvements.assert_called_once() |
2152 | report_store.log_debug_action.assert_not_called() | 1995 | self.m.report.log_debug_action.assert_not_called() |
2153 | 1996 | ||
2154 | @mock.patch.object(portfolio.Order, "fetch") | 1997 | @mock.patch.object(portfolio.Order, "fetch") |
2155 | @mock.patch.object(portfolio.Order, "mark_finished_order") | 1998 | @mock.patch.object(portfolio.Order, "mark_finished_order") |
2156 | @mock.patch("portfolio.ReportStore") | 1999 | def test_get_status(self, mark_finished_order, fetch): |
2157 | def test_get_status(self, report_store, mark_finished_order, fetch): | ||
2158 | with self.subTest(debug=True): | 2000 | with self.subTest(debug=True): |
2159 | portfolio.TradeStore.debug = True | 2001 | self.m.debug = True |
2160 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2002 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2161 | D("0.1"), "BTC", "long", "market", "trade") | 2003 | D("0.1"), "BTC", "long", self.m, "trade") |
2162 | self.assertEqual("pending", order.get_status()) | 2004 | self.assertEqual("pending", order.get_status()) |
2163 | fetch.assert_not_called() | 2005 | fetch.assert_not_called() |
2164 | report_store.log_debug_action.assert_called_once() | 2006 | self.m.report.log_debug_action.assert_called_once() |
2165 | 2007 | ||
2166 | with self.subTest(debug=False, finished=False): | 2008 | with self.subTest(debug=False, finished=False): |
2167 | portfolio.TradeStore.debug = False | 2009 | self.m.debug = False |
2168 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2010 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2169 | D("0.1"), "BTC", "long", "market", "trade") | 2011 | D("0.1"), "BTC", "long", self.m, "trade") |
2170 | def _fetch(order): | 2012 | def _fetch(order): |
2171 | def update_status(): | 2013 | def update_status(): |
2172 | order.status = "open" | 2014 | order.status = "open" |
@@ -2179,9 +2021,9 @@ class OrderTest(WebMockTestCase): | |||
2179 | mark_finished_order.reset_mock() | 2021 | mark_finished_order.reset_mock() |
2180 | fetch.reset_mock() | 2022 | fetch.reset_mock() |
2181 | with self.subTest(debug=False, finished=True): | 2023 | with self.subTest(debug=False, finished=True): |
2182 | portfolio.TradeStore.debug = False | 2024 | self.m.debug = False |
2183 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2025 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2184 | D("0.1"), "BTC", "long", "market", "trade") | 2026 | D("0.1"), "BTC", "long", self.m, "trade") |
2185 | def _fetch(order): | 2027 | def _fetch(order): |
2186 | def update_status(): | 2028 | def update_status(): |
2187 | order.status = "closed" | 2029 | order.status = "closed" |
@@ -2192,52 +2034,48 @@ class OrderTest(WebMockTestCase): | |||
2192 | fetch.assert_called_once() | 2034 | fetch.assert_called_once() |
2193 | 2035 | ||
2194 | def test_run(self): | 2036 | def test_run(self): |
2195 | market = mock.Mock() | 2037 | self.m.ccxt.order_precision.return_value = 4 |
2196 | 2038 | with self.subTest(debug=True): | |
2197 | market.order_precision.return_value = 4 | 2039 | self.m.debug = True |
2198 | with self.subTest(debug=True),\ | ||
2199 | mock.patch('portfolio.ReportStore') as report_store: | ||
2200 | portfolio.TradeStore.debug = True | ||
2201 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2040 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2202 | D("0.1"), "BTC", "long", market, "trade") | 2041 | D("0.1"), "BTC", "long", self.m, "trade") |
2203 | order.run() | 2042 | order.run() |
2204 | market.create_order.assert_not_called() | 2043 | self.m.ccxt.create_order.assert_not_called() |
2205 | report_store.log_debug_action.assert_called_with("market.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)") | 2044 | self.m.report.log_debug_action.assert_called_with("market.ccxt.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)") |
2206 | self.assertEqual("open", order.status) | 2045 | self.assertEqual("open", order.status) |
2207 | self.assertEqual(1, len(order.results)) | 2046 | self.assertEqual(1, len(order.results)) |
2208 | self.assertEqual(-1, order.id) | 2047 | self.assertEqual(-1, order.id) |
2209 | 2048 | ||
2210 | market.create_order.reset_mock() | 2049 | self.m.ccxt.create_order.reset_mock() |
2211 | with self.subTest(debug=False): | 2050 | with self.subTest(debug=False): |
2212 | portfolio.TradeStore.debug = False | 2051 | self.m.debug = False |
2213 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2052 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2214 | D("0.1"), "BTC", "long", market, "trade") | 2053 | D("0.1"), "BTC", "long", self.m, "trade") |
2215 | market.create_order.return_value = { "id": 123 } | 2054 | self.m.ccxt.create_order.return_value = { "id": 123 } |
2216 | order.run() | 2055 | order.run() |
2217 | market.create_order.assert_called_once() | 2056 | self.m.ccxt.create_order.assert_called_once() |
2218 | self.assertEqual(1, len(order.results)) | 2057 | self.assertEqual(1, len(order.results)) |
2219 | self.assertEqual("open", order.status) | 2058 | self.assertEqual("open", order.status) |
2220 | 2059 | ||
2221 | market.create_order.reset_mock() | 2060 | self.m.ccxt.create_order.reset_mock() |
2222 | with self.subTest(exception=True),\ | 2061 | with self.subTest(exception=True): |
2223 | mock.patch('portfolio.ReportStore') as report_store: | ||
2224 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 2062 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
2225 | D("0.1"), "BTC", "long", market, "trade") | 2063 | D("0.1"), "BTC", "long", self.m, "trade") |
2226 | market.create_order.side_effect = Exception("bouh") | 2064 | self.m.ccxt.create_order.side_effect = Exception("bouh") |
2227 | order.run() | 2065 | order.run() |
2228 | market.create_order.assert_called_once() | 2066 | self.m.ccxt.create_order.assert_called_once() |
2229 | self.assertEqual(0, len(order.results)) | 2067 | self.assertEqual(0, len(order.results)) |
2230 | self.assertEqual("error", order.status) | 2068 | self.assertEqual("error", order.status) |
2231 | report_store.log_error.assert_called_once() | 2069 | self.m.report.log_error.assert_called_once() |
2232 | 2070 | ||
2233 | market.create_order.reset_mock() | 2071 | self.m.ccxt.create_order.reset_mock() |
2234 | with self.subTest(dust_amount_exception=True),\ | 2072 | with self.subTest(dust_amount_exception=True),\ |
2235 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: | 2073 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: |
2236 | order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001), | 2074 | order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001), |
2237 | D("0.1"), "BTC", "long", market, "trade") | 2075 | D("0.1"), "BTC", "long", self.m, "trade") |
2238 | market.create_order.side_effect = portfolio.ExchangeNotAvailable | 2076 | self.m.ccxt.create_order.side_effect = portfolio.ExchangeNotAvailable |
2239 | order.run() | 2077 | order.run() |
2240 | market.create_order.assert_called_once() | 2078 | self.m.ccxt.create_order.assert_called_once() |
2241 | self.assertEqual(0, len(order.results)) | 2079 | self.assertEqual(0, len(order.results)) |
2242 | self.assertEqual("closed", order.status) | 2080 | self.assertEqual("closed", order.status) |
2243 | mark_finished_order.assert_called_once() | 2081 | mark_finished_order.assert_called_once() |
@@ -2304,61 +2142,66 @@ class MouvementTest(WebMockTestCase): | |||
2304 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 2142 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2305 | class ReportStoreTest(WebMockTestCase): | 2143 | class ReportStoreTest(WebMockTestCase): |
2306 | def test_add_log(self): | 2144 | def test_add_log(self): |
2307 | portfolio.ReportStore.add_log({"foo": "bar"}) | 2145 | report_store = market.ReportStore(self.m) |
2146 | report_store.add_log({"foo": "bar"}) | ||
2308 | 2147 | ||
2309 | self.assertEqual({"foo": "bar", "date": mock.ANY}, portfolio.ReportStore.logs[0]) | 2148 | self.assertEqual({"foo": "bar", "date": mock.ANY}, report_store.logs[0]) |
2310 | 2149 | ||
2311 | def test_set_verbose(self): | 2150 | def test_set_verbose(self): |
2151 | report_store = market.ReportStore(self.m) | ||
2312 | with self.subTest(verbose=True): | 2152 | with self.subTest(verbose=True): |
2313 | portfolio.ReportStore.set_verbose(True) | 2153 | report_store.set_verbose(True) |
2314 | self.assertTrue(portfolio.ReportStore.verbose_print) | 2154 | self.assertTrue(report_store.verbose_print) |
2315 | 2155 | ||
2316 | with self.subTest(verbose=False): | 2156 | with self.subTest(verbose=False): |
2317 | portfolio.ReportStore.set_verbose(False) | 2157 | report_store.set_verbose(False) |
2318 | self.assertFalse(portfolio.ReportStore.verbose_print) | 2158 | self.assertFalse(report_store.verbose_print) |
2319 | 2159 | ||
2320 | def test_print_log(self): | 2160 | def test_print_log(self): |
2161 | report_store = market.ReportStore(self.m) | ||
2321 | with self.subTest(verbose=True),\ | 2162 | with self.subTest(verbose=True),\ |
2322 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 2163 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2323 | portfolio.ReportStore.set_verbose(True) | 2164 | report_store.set_verbose(True) |
2324 | portfolio.ReportStore.print_log("Coucou") | 2165 | report_store.print_log("Coucou") |
2325 | portfolio.ReportStore.print_log(portfolio.Amount("BTC", 1)) | 2166 | report_store.print_log(portfolio.Amount("BTC", 1)) |
2326 | self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n") | 2167 | self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n") |
2327 | 2168 | ||
2328 | with self.subTest(verbose=False),\ | 2169 | with self.subTest(verbose=False),\ |
2329 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 2170 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2330 | portfolio.ReportStore.set_verbose(False) | 2171 | report_store.set_verbose(False) |
2331 | portfolio.ReportStore.print_log("Coucou") | 2172 | report_store.print_log("Coucou") |
2332 | portfolio.ReportStore.print_log(portfolio.Amount("BTC", 1)) | 2173 | report_store.print_log(portfolio.Amount("BTC", 1)) |
2333 | self.assertEqual(stdout_mock.getvalue(), "") | 2174 | self.assertEqual(stdout_mock.getvalue(), "") |
2334 | 2175 | ||
2335 | def test_to_json(self): | 2176 | def test_to_json(self): |
2336 | portfolio.ReportStore.logs.append({"foo": "bar"}) | 2177 | report_store = market.ReportStore(self.m) |
2337 | self.assertEqual('[{"foo": "bar"}]', portfolio.ReportStore.to_json()) | 2178 | report_store.logs.append({"foo": "bar"}) |
2338 | portfolio.ReportStore.logs.append({"date": portfolio.datetime(2018, 2, 24)}) | 2179 | self.assertEqual('[{"foo": "bar"}]', report_store.to_json()) |
2339 | self.assertEqual('[{"foo": "bar"}, {"date": "2018-02-24T00:00:00"}]', portfolio.ReportStore.to_json()) | 2180 | report_store.logs.append({"date": portfolio.datetime(2018, 2, 24)}) |
2340 | portfolio.ReportStore.logs.append({"amount": portfolio.Amount("BTC", 1)}) | 2181 | self.assertEqual('[{"foo": "bar"}, {"date": "2018-02-24T00:00:00"}]', report_store.to_json()) |
2182 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) | ||
2341 | with self.assertRaises(TypeError): | 2183 | with self.assertRaises(TypeError): |
2342 | portfolio.ReportStore.to_json() | 2184 | report_store.to_json() |
2343 | 2185 | ||
2344 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2186 | @mock.patch.object(market.ReportStore, "print_log") |
2345 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2187 | @mock.patch.object(market.ReportStore, "add_log") |
2346 | def test_log_stage(self, add_log, print_log): | 2188 | def test_log_stage(self, add_log, print_log): |
2347 | portfolio.ReportStore.log_stage("foo") | 2189 | report_store = market.ReportStore(self.m) |
2190 | report_store.log_stage("foo") | ||
2348 | print_log.assert_has_calls([ | 2191 | print_log.assert_has_calls([ |
2349 | mock.call("-----------"), | 2192 | mock.call("-----------"), |
2350 | mock.call("[Stage] foo"), | 2193 | mock.call("[Stage] foo"), |
2351 | ]) | 2194 | ]) |
2352 | add_log.assert_called_once_with({'type': 'stage', 'stage': 'foo'}) | 2195 | add_log.assert_called_once_with({'type': 'stage', 'stage': 'foo'}) |
2353 | 2196 | ||
2354 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2197 | @mock.patch.object(market.ReportStore, "print_log") |
2355 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2198 | @mock.patch.object(market.ReportStore, "add_log") |
2356 | @mock.patch("store.BalanceStore") | 2199 | def test_log_balances(self, add_log, print_log): |
2357 | def test_log_balances(self, balance_store, add_log, print_log): | 2200 | report_store = market.ReportStore(self.m) |
2358 | balance_store.as_json.return_value = "json" | 2201 | self.m.balances.as_json.return_value = "json" |
2359 | balance_store.all = { "FOO": "bar", "BAR": "baz" } | 2202 | self.m.balances.all = { "FOO": "bar", "BAR": "baz" } |
2360 | 2203 | ||
2361 | portfolio.ReportStore.log_balances("market", tag="tag") | 2204 | report_store.log_balances(tag="tag") |
2362 | print_log.assert_has_calls([ | 2205 | print_log.assert_has_calls([ |
2363 | mock.call("[Balance]"), | 2206 | mock.call("[Balance]"), |
2364 | mock.call("\tbar"), | 2207 | mock.call("\tbar"), |
@@ -2370,17 +2213,17 @@ class ReportStoreTest(WebMockTestCase): | |||
2370 | 'tag': 'tag' | 2213 | 'tag': 'tag' |
2371 | }) | 2214 | }) |
2372 | 2215 | ||
2373 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2216 | @mock.patch.object(market.ReportStore, "print_log") |
2374 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2217 | @mock.patch.object(market.ReportStore, "add_log") |
2375 | def test_log_tickers(self, add_log, print_log): | 2218 | def test_log_tickers(self, add_log, print_log): |
2376 | market = mock.Mock() | 2219 | report_store = market.ReportStore(self.m) |
2377 | amounts = { | 2220 | amounts = { |
2378 | "BTC": portfolio.Amount("BTC", 10), | 2221 | "BTC": portfolio.Amount("BTC", 10), |
2379 | "ETH": portfolio.Amount("BTC", D("0.3")) | 2222 | "ETH": portfolio.Amount("BTC", D("0.3")) |
2380 | } | 2223 | } |
2381 | amounts["ETH"].rate = D("0.1") | 2224 | amounts["ETH"].rate = D("0.1") |
2382 | 2225 | ||
2383 | portfolio.ReportStore.log_tickers(market, amounts, "BTC", "default", "total") | 2226 | report_store.log_tickers(amounts, "BTC", "default", "total") |
2384 | print_log.assert_not_called() | 2227 | print_log.assert_not_called() |
2385 | add_log.assert_called_once_with({ | 2228 | add_log.assert_called_once_with({ |
2386 | 'type': 'tickers', | 2229 | 'type': 'tickers', |
@@ -2398,15 +2241,16 @@ class ReportStoreTest(WebMockTestCase): | |||
2398 | 'total': D('10.3') | 2241 | 'total': D('10.3') |
2399 | }) | 2242 | }) |
2400 | 2243 | ||
2401 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2244 | @mock.patch.object(market.ReportStore, "print_log") |
2402 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2245 | @mock.patch.object(market.ReportStore, "add_log") |
2403 | def test_log_dispatch(self, add_log, print_log): | 2246 | def test_log_dispatch(self, add_log, print_log): |
2247 | report_store = market.ReportStore(self.m) | ||
2404 | amount = portfolio.Amount("BTC", "10.3") | 2248 | amount = portfolio.Amount("BTC", "10.3") |
2405 | amounts = { | 2249 | amounts = { |
2406 | "BTC": portfolio.Amount("BTC", 10), | 2250 | "BTC": portfolio.Amount("BTC", 10), |
2407 | "ETH": portfolio.Amount("BTC", D("0.3")) | 2251 | "ETH": portfolio.Amount("BTC", D("0.3")) |
2408 | } | 2252 | } |
2409 | portfolio.ReportStore.log_dispatch(amount, amounts, "medium", "repartition") | 2253 | report_store.log_dispatch(amount, amounts, "medium", "repartition") |
2410 | print_log.assert_not_called() | 2254 | print_log.assert_not_called() |
2411 | add_log.assert_called_once_with({ | 2255 | add_log.assert_called_once_with({ |
2412 | 'type': 'dispatch', | 2256 | 'type': 'dispatch', |
@@ -2422,9 +2266,10 @@ class ReportStoreTest(WebMockTestCase): | |||
2422 | } | 2266 | } |
2423 | }) | 2267 | }) |
2424 | 2268 | ||
2425 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2269 | @mock.patch.object(market.ReportStore, "print_log") |
2426 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2270 | @mock.patch.object(market.ReportStore, "add_log") |
2427 | def test_log_trades(self, add_log, print_log): | 2271 | def test_log_trades(self, add_log, print_log): |
2272 | report_store = market.ReportStore(self.m) | ||
2428 | trade_mock1 = mock.Mock() | 2273 | trade_mock1 = mock.Mock() |
2429 | trade_mock2 = mock.Mock() | 2274 | trade_mock2 = mock.Mock() |
2430 | trade_mock1.as_json.return_value = { "trade": "1" } | 2275 | trade_mock1.as_json.return_value = { "trade": "1" } |
@@ -2434,23 +2279,24 @@ class ReportStoreTest(WebMockTestCase): | |||
2434 | (True, trade_mock1), | 2279 | (True, trade_mock1), |
2435 | (False, trade_mock2), | 2280 | (False, trade_mock2), |
2436 | ] | 2281 | ] |
2437 | portfolio.ReportStore.log_trades(matching_and_trades, "only", "debug") | 2282 | report_store.log_trades(matching_and_trades, "only") |
2438 | 2283 | ||
2439 | print_log.assert_not_called() | 2284 | print_log.assert_not_called() |
2440 | add_log.assert_called_with({ | 2285 | add_log.assert_called_with({ |
2441 | 'type': 'trades', | 2286 | 'type': 'trades', |
2442 | 'only': 'only', | 2287 | 'only': 'only', |
2443 | 'debug': 'debug', | 2288 | 'debug': False, |
2444 | 'trades': [ | 2289 | 'trades': [ |
2445 | {'trade': '1', 'skipped': False}, | 2290 | {'trade': '1', 'skipped': False}, |
2446 | {'trade': '2', 'skipped': True} | 2291 | {'trade': '2', 'skipped': True} |
2447 | ] | 2292 | ] |
2448 | }) | 2293 | }) |
2449 | 2294 | ||
2450 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2295 | @mock.patch.object(market.ReportStore, "print_log") |
2451 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2296 | @mock.patch.object(market.ReportStore, "add_log") |
2452 | @mock.patch.object(portfolio.TradeStore, "print_all_with_order") | 2297 | def test_log_orders(self, add_log, print_log): |
2453 | def test_log_orders(self, print_all_with_order, add_log, print_log): | 2298 | report_store = market.ReportStore(self.m) |
2299 | |||
2454 | order_mock1 = mock.Mock() | 2300 | order_mock1 = mock.Mock() |
2455 | order_mock2 = mock.Mock() | 2301 | order_mock2 = mock.Mock() |
2456 | 2302 | ||
@@ -2459,11 +2305,11 @@ class ReportStoreTest(WebMockTestCase): | |||
2459 | 2305 | ||
2460 | orders = [order_mock1, order_mock2] | 2306 | orders = [order_mock1, order_mock2] |
2461 | 2307 | ||
2462 | portfolio.ReportStore.log_orders(orders, tick="tick", | 2308 | report_store.log_orders(orders, tick="tick", |
2463 | only="only", compute_value="compute_value") | 2309 | only="only", compute_value="compute_value") |
2464 | 2310 | ||
2465 | print_log.assert_called_once_with("[Orders]") | 2311 | print_log.assert_called_once_with("[Orders]") |
2466 | print_all_with_order.assert_called_once_with(ind="\t") | 2312 | self.m.trades.print_all_with_order.assert_called_once_with(ind="\t") |
2467 | 2313 | ||
2468 | add_log.assert_called_with({ | 2314 | add_log.assert_called_with({ |
2469 | 'type': 'orders', | 2315 | 'type': 'orders', |
@@ -2473,9 +2319,10 @@ class ReportStoreTest(WebMockTestCase): | |||
2473 | 'orders': ['order1', 'order2'] | 2319 | 'orders': ['order1', 'order2'] |
2474 | }) | 2320 | }) |
2475 | 2321 | ||
2476 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2322 | @mock.patch.object(market.ReportStore, "print_log") |
2477 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2323 | @mock.patch.object(market.ReportStore, "add_log") |
2478 | def test_log_order(self, add_log, print_log): | 2324 | def test_log_order(self, add_log, print_log): |
2325 | report_store = market.ReportStore(self.m) | ||
2479 | order_mock = mock.Mock() | 2326 | order_mock = mock.Mock() |
2480 | order_mock.as_json.return_value = "order" | 2327 | order_mock.as_json.return_value = "order" |
2481 | new_order_mock = mock.Mock() | 2328 | new_order_mock = mock.Mock() |
@@ -2486,7 +2333,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2486 | new_order_mock.__repr__.return_value = "New order Mock" | 2333 | new_order_mock.__repr__.return_value = "New order Mock" |
2487 | 2334 | ||
2488 | with self.subTest(finished=True): | 2335 | with self.subTest(finished=True): |
2489 | portfolio.ReportStore.log_order(order_mock, 1, finished=True) | 2336 | report_store.log_order(order_mock, 1, finished=True) |
2490 | print_log.assert_called_once_with("[Order] Finished Order Mock") | 2337 | print_log.assert_called_once_with("[Order] Finished Order Mock") |
2491 | add_log.assert_called_once_with({ | 2338 | add_log.assert_called_once_with({ |
2492 | 'type': 'order', | 2339 | 'type': 'order', |
@@ -2501,7 +2348,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2501 | print_log.reset_mock() | 2348 | print_log.reset_mock() |
2502 | 2349 | ||
2503 | with self.subTest(update="waiting"): | 2350 | with self.subTest(update="waiting"): |
2504 | portfolio.ReportStore.log_order(order_mock, 1, update="waiting") | 2351 | report_store.log_order(order_mock, 1, update="waiting") |
2505 | print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting") | 2352 | print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting") |
2506 | add_log.assert_called_once_with({ | 2353 | add_log.assert_called_once_with({ |
2507 | 'type': 'order', | 2354 | 'type': 'order', |
@@ -2515,7 +2362,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2515 | add_log.reset_mock() | 2362 | add_log.reset_mock() |
2516 | print_log.reset_mock() | 2363 | print_log.reset_mock() |
2517 | with self.subTest(update="adjusting"): | 2364 | with self.subTest(update="adjusting"): |
2518 | portfolio.ReportStore.log_order(order_mock, 3, | 2365 | report_store.log_order(order_mock, 3, |
2519 | update="adjusting", new_order=new_order_mock, | 2366 | update="adjusting", new_order=new_order_mock, |
2520 | compute_value="default") | 2367 | compute_value="default") |
2521 | print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") | 2368 | print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock") |
@@ -2531,7 +2378,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2531 | add_log.reset_mock() | 2378 | add_log.reset_mock() |
2532 | print_log.reset_mock() | 2379 | print_log.reset_mock() |
2533 | with self.subTest(update="market_fallback"): | 2380 | with self.subTest(update="market_fallback"): |
2534 | portfolio.ReportStore.log_order(order_mock, 7, | 2381 | report_store.log_order(order_mock, 7, |
2535 | update="market_fallback", new_order=new_order_mock) | 2382 | update="market_fallback", new_order=new_order_mock) |
2536 | print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value") | 2383 | print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value") |
2537 | add_log.assert_called_once_with({ | 2384 | add_log.assert_called_once_with({ |
@@ -2546,7 +2393,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2546 | add_log.reset_mock() | 2393 | add_log.reset_mock() |
2547 | print_log.reset_mock() | 2394 | print_log.reset_mock() |
2548 | with self.subTest(update="market_adjusting"): | 2395 | with self.subTest(update="market_adjusting"): |
2549 | portfolio.ReportStore.log_order(order_mock, 17, | 2396 | report_store.log_order(order_mock, 17, |
2550 | update="market_adjust", new_order=new_order_mock) | 2397 | update="market_adjust", new_order=new_order_mock) |
2551 | print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock") | 2398 | print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock") |
2552 | add_log.assert_called_once_with({ | 2399 | add_log.assert_called_once_with({ |
@@ -2558,9 +2405,10 @@ class ReportStoreTest(WebMockTestCase): | |||
2558 | 'new_order': 'new_order' | 2405 | 'new_order': 'new_order' |
2559 | }) | 2406 | }) |
2560 | 2407 | ||
2561 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2408 | @mock.patch.object(market.ReportStore, "print_log") |
2562 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2409 | @mock.patch.object(market.ReportStore, "add_log") |
2563 | def test_log_move_balances(self, add_log, print_log): | 2410 | def test_log_move_balances(self, add_log, print_log): |
2411 | report_store = market.ReportStore(self.m) | ||
2564 | needed = { | 2412 | needed = { |
2565 | "BTC": portfolio.Amount("BTC", 10), | 2413 | "BTC": portfolio.Amount("BTC", 10), |
2566 | "USDT": 1 | 2414 | "USDT": 1 |
@@ -2569,11 +2417,11 @@ class ReportStoreTest(WebMockTestCase): | |||
2569 | "BTC": portfolio.Amount("BTC", 3), | 2417 | "BTC": portfolio.Amount("BTC", 3), |
2570 | "USDT": -2 | 2418 | "USDT": -2 |
2571 | } | 2419 | } |
2572 | portfolio.ReportStore.log_move_balances(needed, moving, True) | 2420 | report_store.log_move_balances(needed, moving) |
2573 | print_log.assert_not_called() | 2421 | print_log.assert_not_called() |
2574 | add_log.assert_called_once_with({ | 2422 | add_log.assert_called_once_with({ |
2575 | 'type': 'move_balances', | 2423 | 'type': 'move_balances', |
2576 | 'debug': True, | 2424 | 'debug': False, |
2577 | 'needed': { | 2425 | 'needed': { |
2578 | 'BTC': D('10'), | 2426 | 'BTC': D('10'), |
2579 | 'USDT': 1 | 2427 | 'USDT': 1 |
@@ -2584,14 +2432,15 @@ class ReportStoreTest(WebMockTestCase): | |||
2584 | } | 2432 | } |
2585 | }) | 2433 | }) |
2586 | 2434 | ||
2587 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2435 | @mock.patch.object(market.ReportStore, "print_log") |
2588 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2436 | @mock.patch.object(market.ReportStore, "add_log") |
2589 | def test_log_http_request(self, add_log, print_log): | 2437 | def test_log_http_request(self, add_log, print_log): |
2438 | report_store = market.ReportStore(self.m) | ||
2590 | response = mock.Mock() | 2439 | response = mock.Mock() |
2591 | response.status_code = 200 | 2440 | response.status_code = 200 |
2592 | response.text = "Hey" | 2441 | response.text = "Hey" |
2593 | 2442 | ||
2594 | portfolio.ReportStore.log_http_request("method", "url", "body", | 2443 | report_store.log_http_request("method", "url", "body", |
2595 | "headers", response) | 2444 | "headers", response) |
2596 | print_log.assert_not_called() | 2445 | print_log.assert_not_called() |
2597 | add_log.assert_called_once_with({ | 2446 | add_log.assert_called_once_with({ |
@@ -2604,11 +2453,12 @@ class ReportStoreTest(WebMockTestCase): | |||
2604 | 'response': 'Hey' | 2453 | 'response': 'Hey' |
2605 | }) | 2454 | }) |
2606 | 2455 | ||
2607 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2456 | @mock.patch.object(market.ReportStore, "print_log") |
2608 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2457 | @mock.patch.object(market.ReportStore, "add_log") |
2609 | def test_log_error(self, add_log, print_log): | 2458 | def test_log_error(self, add_log, print_log): |
2459 | report_store = market.ReportStore(self.m) | ||
2610 | with self.subTest(message=None, exception=None): | 2460 | with self.subTest(message=None, exception=None): |
2611 | portfolio.ReportStore.log_error("action") | 2461 | report_store.log_error("action") |
2612 | print_log.assert_called_once_with("[Error] action") | 2462 | print_log.assert_called_once_with("[Error] action") |
2613 | add_log.assert_called_once_with({ | 2463 | add_log.assert_called_once_with({ |
2614 | 'type': 'error', | 2464 | 'type': 'error', |
@@ -2621,7 +2471,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2621 | print_log.reset_mock() | 2471 | print_log.reset_mock() |
2622 | add_log.reset_mock() | 2472 | add_log.reset_mock() |
2623 | with self.subTest(message="Hey", exception=None): | 2473 | with self.subTest(message="Hey", exception=None): |
2624 | portfolio.ReportStore.log_error("action", message="Hey") | 2474 | report_store.log_error("action", message="Hey") |
2625 | print_log.assert_has_calls([ | 2475 | print_log.assert_has_calls([ |
2626 | mock.call("[Error] action"), | 2476 | mock.call("[Error] action"), |
2627 | mock.call("\tHey") | 2477 | mock.call("\tHey") |
@@ -2637,7 +2487,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2637 | print_log.reset_mock() | 2487 | print_log.reset_mock() |
2638 | add_log.reset_mock() | 2488 | add_log.reset_mock() |
2639 | with self.subTest(message=None, exception=Exception("bouh")): | 2489 | with self.subTest(message=None, exception=Exception("bouh")): |
2640 | portfolio.ReportStore.log_error("action", exception=Exception("bouh")) | 2490 | report_store.log_error("action", exception=Exception("bouh")) |
2641 | print_log.assert_has_calls([ | 2491 | print_log.assert_has_calls([ |
2642 | mock.call("[Error] action"), | 2492 | mock.call("[Error] action"), |
2643 | mock.call("\tException: bouh") | 2493 | mock.call("\tException: bouh") |
@@ -2653,7 +2503,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2653 | print_log.reset_mock() | 2503 | print_log.reset_mock() |
2654 | add_log.reset_mock() | 2504 | add_log.reset_mock() |
2655 | with self.subTest(message="Hey", exception=Exception("bouh")): | 2505 | with self.subTest(message="Hey", exception=Exception("bouh")): |
2656 | portfolio.ReportStore.log_error("action", message="Hey", exception=Exception("bouh")) | 2506 | report_store.log_error("action", message="Hey", exception=Exception("bouh")) |
2657 | print_log.assert_has_calls([ | 2507 | print_log.assert_has_calls([ |
2658 | mock.call("[Error] action"), | 2508 | mock.call("[Error] action"), |
2659 | mock.call("\tException: bouh"), | 2509 | mock.call("\tException: bouh"), |
@@ -2667,10 +2517,11 @@ class ReportStoreTest(WebMockTestCase): | |||
2667 | 'message': "Hey" | 2517 | 'message': "Hey" |
2668 | }) | 2518 | }) |
2669 | 2519 | ||
2670 | @mock.patch.object(portfolio.ReportStore, "print_log") | 2520 | @mock.patch.object(market.ReportStore, "print_log") |
2671 | @mock.patch.object(portfolio.ReportStore, "add_log") | 2521 | @mock.patch.object(market.ReportStore, "add_log") |
2672 | def test_log_debug_action(self, add_log, print_log): | 2522 | def test_log_debug_action(self, add_log, print_log): |
2673 | portfolio.ReportStore.log_debug_action("Hey") | 2523 | report_store = market.ReportStore(self.m) |
2524 | report_store.log_debug_action("Hey") | ||
2674 | 2525 | ||
2675 | print_log.assert_called_once_with("[Debug] Hey") | 2526 | print_log.assert_called_once_with("[Debug] Hey") |
2676 | add_log.assert_called_once_with({ | 2527 | add_log.assert_called_once_with({ |
@@ -2678,12 +2529,263 @@ class ReportStoreTest(WebMockTestCase): | |||
2678 | 'action': 'Hey' | 2529 | 'action': 'Hey' |
2679 | }) | 2530 | }) |
2680 | 2531 | ||
2532 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
2533 | class HelperTest(WebMockTestCase): | ||
2534 | def test_main_store_report(self): | ||
2535 | file_open = mock.mock_open() | ||
2536 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | ||
2537 | helper.main_store_report(None, 1, self.m) | ||
2538 | file_open.assert_not_called() | ||
2539 | |||
2540 | file_open = mock.mock_open() | ||
2541 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2542 | mock.patch.object(helper, "datetime") as time_mock: | ||
2543 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2544 | self.m.report.to_json.return_value = "json_content" | ||
2545 | |||
2546 | helper.main_store_report("present", 1, self.m) | ||
2547 | |||
2548 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2549 | file_open().write.assert_called_once_with("json_content") | ||
2550 | self.m.report.to_json.assert_called_once_with() | ||
2551 | |||
2552 | with self.subTest(file="error"),\ | ||
2553 | mock.patch("helper.open") as file_open,\ | ||
2554 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
2555 | file_open.side_effect = FileNotFoundError | ||
2556 | |||
2557 | helper.main_store_report("error", 1, self.m) | ||
2558 | |||
2559 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
2560 | |||
2561 | @mock.patch("helper.process_sell_all__1_all_sell") | ||
2562 | @mock.patch("helper.process_sell_all__2_all_buy") | ||
2563 | @mock.patch("portfolio.Portfolio.wait_for_recent") | ||
2564 | def test_main_process_market(self, wait, buy, sell): | ||
2565 | with self.subTest(before=False, after=False): | ||
2566 | helper.main_process_market("user") | ||
2567 | |||
2568 | wait.assert_not_called() | ||
2569 | buy.assert_not_called() | ||
2570 | sell.assert_not_called() | ||
2571 | |||
2572 | buy.reset_mock() | ||
2573 | wait.reset_mock() | ||
2574 | sell.reset_mock() | ||
2575 | with self.subTest(before=True, after=False): | ||
2576 | helper.main_process_market("user", before=True) | ||
2577 | |||
2578 | wait.assert_not_called() | ||
2579 | buy.assert_not_called() | ||
2580 | sell.assert_called_once_with("user") | ||
2581 | |||
2582 | buy.reset_mock() | ||
2583 | wait.reset_mock() | ||
2584 | sell.reset_mock() | ||
2585 | with self.subTest(before=False, after=True): | ||
2586 | helper.main_process_market("user", after=True) | ||
2587 | |||
2588 | wait.assert_called_once_with("user") | ||
2589 | buy.assert_called_once_with("user") | ||
2590 | sell.assert_not_called() | ||
2591 | |||
2592 | buy.reset_mock() | ||
2593 | wait.reset_mock() | ||
2594 | sell.reset_mock() | ||
2595 | with self.subTest(before=True, after=True): | ||
2596 | helper.main_process_market("user", before=True, after=True) | ||
2597 | |||
2598 | wait.assert_called_once_with("user") | ||
2599 | buy.assert_called_once_with("user") | ||
2600 | sell.assert_called_once_with("user") | ||
2601 | |||
2602 | @mock.patch.object(helper, "psycopg2") | ||
2603 | def test_fetch_markets(self, psycopg2): | ||
2604 | connect_mock = mock.Mock() | ||
2605 | cursor_mock = mock.MagicMock() | ||
2606 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
2607 | |||
2608 | connect_mock.cursor.return_value = cursor_mock | ||
2609 | psycopg2.connect.return_value = connect_mock | ||
2610 | |||
2611 | rows = list(helper.main_fetch_markets({"foo": "bar"})) | ||
2612 | |||
2613 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2614 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
2615 | |||
2616 | self.assertEqual(["row_1", "row_2"], rows) | ||
2617 | |||
2618 | @mock.patch.object(helper.sys, "exit") | ||
2619 | def test_main_parse_args(self, exit): | ||
2620 | with self.subTest(config="config.ini"): | ||
2621 | args = helper.main_parse_args([]) | ||
2622 | self.assertEqual("config.ini", args.config) | ||
2623 | self.assertFalse(args.before) | ||
2624 | self.assertFalse(args.after) | ||
2625 | self.assertFalse(args.debug) | ||
2626 | |||
2627 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | ||
2628 | self.assertTrue(args.before) | ||
2629 | self.assertTrue(args.after) | ||
2630 | self.assertTrue(args.debug) | ||
2631 | |||
2632 | exit.assert_not_called() | ||
2633 | |||
2634 | with self.subTest(config="inexistant"),\ | ||
2635 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
2636 | args = helper.main_parse_args(["--config", "foo.bar"]) | ||
2637 | exit.assert_called_once_with(1) | ||
2638 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
2639 | |||
2640 | @mock.patch.object(helper.sys, "exit") | ||
2641 | @mock.patch("helper.configparser") | ||
2642 | @mock.patch("helper.os") | ||
2643 | def test_main_parse_config(self, os, configparser, exit): | ||
2644 | with self.subTest(pg_config=True, report_path=None): | ||
2645 | config_mock = mock.MagicMock() | ||
2646 | configparser.ConfigParser.return_value = config_mock | ||
2647 | def config(element): | ||
2648 | return element == "postgresql" | ||
2649 | |||
2650 | config_mock.__contains__.side_effect = config | ||
2651 | config_mock.__getitem__.return_value = "pg_config" | ||
2652 | |||
2653 | result = helper.main_parse_config("configfile") | ||
2654 | |||
2655 | config_mock.read.assert_called_with("configfile") | ||
2656 | |||
2657 | self.assertEqual(["pg_config", None], result) | ||
2658 | |||
2659 | with self.subTest(pg_config=True, report_path="present"): | ||
2660 | config_mock = mock.MagicMock() | ||
2661 | configparser.ConfigParser.return_value = config_mock | ||
2662 | |||
2663 | config_mock.__contains__.return_value = True | ||
2664 | config_mock.__getitem__.side_effect = [ | ||
2665 | {"report_path": "report_path"}, | ||
2666 | {"report_path": "report_path"}, | ||
2667 | "pg_config", | ||
2668 | ] | ||
2669 | |||
2670 | os.path.exists.return_value = False | ||
2671 | result = helper.main_parse_config("configfile") | ||
2672 | |||
2673 | config_mock.read.assert_called_with("configfile") | ||
2674 | self.assertEqual(["pg_config", "report_path"], result) | ||
2675 | os.path.exists.assert_called_once_with("report_path") | ||
2676 | os.makedirs.assert_called_once_with("report_path") | ||
2677 | |||
2678 | with self.subTest(pg_config=False),\ | ||
2679 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
2680 | config_mock = mock.MagicMock() | ||
2681 | configparser.ConfigParser.return_value = config_mock | ||
2682 | result = helper.main_parse_config("configfile") | ||
2683 | |||
2684 | config_mock.read.assert_called_with("configfile") | ||
2685 | exit.assert_called_once_with(1) | ||
2686 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | ||
2687 | |||
2688 | |||
2689 | def test_print_orders(self): | ||
2690 | helper.print_orders(self.m) | ||
2691 | |||
2692 | self.m.report.log_stage.assert_called_with("print_orders") | ||
2693 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | ||
2694 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | ||
2695 | compute_value="average") | ||
2696 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | ||
2697 | |||
2698 | def test_print_balances(self): | ||
2699 | self.m.balances.in_currency.return_value = { | ||
2700 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
2701 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
2702 | } | ||
2703 | |||
2704 | helper.print_balances(self.m) | ||
2705 | |||
2706 | self.m.balances.fetch_balances.assert_called_with() | ||
2707 | self.m.report.print_log.assert_has_calls([ | ||
2708 | mock.call("total:"), | ||
2709 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
2710 | ]) | ||
2711 | |||
2712 | def test_process_sell_needed__1_sell(self): | ||
2713 | helper.process_sell_needed__1_sell(self.m) | ||
2714 | |||
2715 | self.m.balances.fetch_balances.assert_has_calls([ | ||
2716 | mock.call(tag="process_sell_needed__1_sell_begin"), | ||
2717 | mock.call(tag="process_sell_needed__1_sell_end"), | ||
2718 | ]) | ||
2719 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | ||
2720 | liquidity="medium") | ||
2721 | self.m.trades.prepare_orders.assert_called_with(compute_value="average", | ||
2722 | only="dispose") | ||
2723 | self.m.trades.run_orders.assert_called() | ||
2724 | self.m.follow_orders.assert_called() | ||
2725 | self.m.report.log_stage.assert_has_calls([ | ||
2726 | mock.call("process_sell_needed__1_sell_begin"), | ||
2727 | mock.call("process_sell_needed__1_sell_end") | ||
2728 | ]) | ||
2729 | |||
2730 | def test_process_sell_needed__2_buy(self): | ||
2731 | helper.process_sell_needed__2_buy(self.m) | ||
2732 | |||
2733 | self.m.balances.fetch_balances.assert_has_calls([ | ||
2734 | mock.call(tag="process_sell_needed__2_buy_begin"), | ||
2735 | mock.call(tag="process_sell_needed__2_buy_end"), | ||
2736 | ]) | ||
2737 | self.m.update_trades.assert_called_with(base_currency="BTC", | ||
2738 | liquidity="medium", only="acquire") | ||
2739 | self.m.trades.prepare_orders.assert_called_with(compute_value="average", | ||
2740 | only="acquire") | ||
2741 | self.m.move_balances.assert_called_with() | ||
2742 | self.m.trades.run_orders.assert_called() | ||
2743 | self.m.follow_orders.assert_called() | ||
2744 | self.m.report.log_stage.assert_has_calls([ | ||
2745 | mock.call("process_sell_needed__2_buy_begin"), | ||
2746 | mock.call("process_sell_needed__2_buy_end") | ||
2747 | ]) | ||
2748 | |||
2749 | def test_process_sell_all__1_sell(self): | ||
2750 | helper.process_sell_all__1_all_sell(self.m) | ||
2751 | |||
2752 | self.m.balances.fetch_balances.assert_has_calls([ | ||
2753 | mock.call(tag="process_sell_all__1_all_sell_begin"), | ||
2754 | mock.call(tag="process_sell_all__1_all_sell_end"), | ||
2755 | ]) | ||
2756 | self.m.prepare_trades_to_sell_all.assert_called_with(base_currency="BTC") | ||
2757 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | ||
2758 | self.m.trades.run_orders.assert_called() | ||
2759 | self.m.follow_orders.assert_called() | ||
2760 | self.m.report.log_stage.assert_has_calls([ | ||
2761 | mock.call("process_sell_all__1_all_sell_begin"), | ||
2762 | mock.call("process_sell_all__1_all_sell_end") | ||
2763 | ]) | ||
2764 | |||
2765 | def test_process_sell_all__2_all_buy(self): | ||
2766 | helper.process_sell_all__2_all_buy(self.m) | ||
2767 | |||
2768 | self.m.balances.fetch_balances.assert_has_calls([ | ||
2769 | mock.call(tag="process_sell_all__2_all_buy_begin"), | ||
2770 | mock.call(tag="process_sell_all__2_all_buy_end"), | ||
2771 | ]) | ||
2772 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | ||
2773 | liquidity="medium") | ||
2774 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | ||
2775 | self.m.move_balances.assert_called_with() | ||
2776 | self.m.trades.run_orders.assert_called() | ||
2777 | self.m.follow_orders.assert_called() | ||
2778 | self.m.report.log_stage.assert_has_calls([ | ||
2779 | mock.call("process_sell_all__2_all_buy_begin"), | ||
2780 | mock.call("process_sell_all__2_all_buy_end") | ||
2781 | ]) | ||
2782 | |||
2681 | @unittest.skipUnless("acceptance" in limits, "Acceptance skipped") | 2783 | @unittest.skipUnless("acceptance" in limits, "Acceptance skipped") |
2682 | class AcceptanceTest(WebMockTestCase): | 2784 | class AcceptanceTest(WebMockTestCase): |
2683 | @unittest.expectedFailure | 2785 | @unittest.expectedFailure |
2684 | def test_success_sell_only_necessary(self): | 2786 | def test_success_sell_only_necessary(self): |
2685 | # FIXME: catch stdout | 2787 | # FIXME: catch stdout |
2686 | portfolio.ReportStore.verbose_print = False | 2788 | self.m.report.verbose_print = False |
2687 | fetch_balance = { | 2789 | fetch_balance = { |
2688 | "ETH": { | 2790 | "ETH": { |
2689 | "exchange_free": D("1.0"), | 2791 | "exchange_free": D("1.0"), |