diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-04-22 13:50:15 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-04-22 13:50:15 +0200 |
commit | 96959ceaa9dd53421b752ae3a4dfe12d237866a6 (patch) | |
tree | 574a75d6b72c22e0d9a06487d7587370dce2defd | |
parent | 868233ee7c536a244fa873a2f4cf6be328011808 (diff) | |
download | Trader-96959ceaa9dd53421b752ae3a4dfe12d237866a6.tar.gz Trader-96959ceaa9dd53421b752ae3a4dfe12d237866a6.tar.zst Trader-96959ceaa9dd53421b752ae3a4dfe12d237866a6.zip |
Fixes https://git.immae.eu/mantisbt/view.php?id=44
-rw-r--r-- | market.py | 24 | ||||
-rw-r--r-- | tests/test_market.py | 131 |
2 files changed, 135 insertions, 20 deletions
@@ -210,18 +210,32 @@ class Market: | |||
210 | self.report.log_stage("follow_orders_end") | 210 | self.report.log_stage("follow_orders_end") |
211 | 211 | ||
212 | def prepare_trades(self, base_currency="BTC", liquidity="medium", | 212 | def prepare_trades(self, base_currency="BTC", liquidity="medium", |
213 | compute_value="average", repartition=None, only=None): | 213 | compute_value="average", repartition=None, only=None, |
214 | available_balance_only=False): | ||
214 | 215 | ||
215 | self.report.log_stage("prepare_trades", | 216 | self.report.log_stage("prepare_trades", |
216 | base_currency=base_currency, liquidity=liquidity, | 217 | base_currency=base_currency, liquidity=liquidity, |
217 | compute_value=compute_value, only=only, | 218 | compute_value=compute_value, only=only, |
218 | repartition=repartition) | 219 | repartition=repartition, available_balance_only=available_balance_only) |
219 | 220 | ||
220 | values_in_base = self.balances.in_currency(base_currency, | 221 | values_in_base = self.balances.in_currency(base_currency, |
221 | compute_value=compute_value) | 222 | compute_value=compute_value) |
222 | total_base_value = sum(values_in_base.values()) | 223 | if available_balance_only: |
224 | balance = self.balances.all.get(base_currency) | ||
225 | if balance is None: | ||
226 | total_base_value = portfolio.Amount(base_currency, 0) | ||
227 | else: | ||
228 | total_base_value = balance.exchange_free + balance.margin_available | ||
229 | else: | ||
230 | total_base_value = sum(values_in_base.values()) | ||
223 | new_repartition = self.balances.dispatch_assets(total_base_value, | 231 | new_repartition = self.balances.dispatch_assets(total_base_value, |
224 | liquidity=liquidity, repartition=repartition) | 232 | liquidity=liquidity, repartition=repartition) |
233 | if available_balance_only: | ||
234 | for currency, amount in values_in_base.items(): | ||
235 | if currency != base_currency: | ||
236 | new_repartition.setdefault(currency, portfolio.Amount(base_currency, 0)) | ||
237 | new_repartition[currency] += amount | ||
238 | |||
225 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 239 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
226 | 240 | ||
227 | def print_tickers(self, base_currency="BTC"): | 241 | def print_tickers(self, base_currency="BTC"): |
@@ -292,7 +306,7 @@ class Processor: | |||
292 | "before": False, | 306 | "before": False, |
293 | "after": True, | 307 | "after": True, |
294 | "fetch_balances": ["begin", "end"], | 308 | "fetch_balances": ["begin", "end"], |
295 | "prepare_trades": { "only": "acquire" }, | 309 | "prepare_trades": { "only": "acquire", "available_balance_only": True }, |
296 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | 310 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, |
297 | "move_balances": {}, | 311 | "move_balances": {}, |
298 | "run_orders": {}, | 312 | "run_orders": {}, |
@@ -326,7 +340,7 @@ class Processor: | |||
326 | "before": False, | 340 | "before": False, |
327 | "after": True, | 341 | "after": True, |
328 | "fetch_balances": ["begin", "end"], | 342 | "fetch_balances": ["begin", "end"], |
329 | "prepare_trades": {}, | 343 | "prepare_trades": { "available_balance_only": True }, |
330 | "prepare_orders": { "compute_value": "average" }, | 344 | "prepare_orders": { "compute_value": "average" }, |
331 | "move_balances": {}, | 345 | "move_balances": {}, |
332 | "run_orders": {}, | 346 | "run_orders": {}, |
diff --git a/tests/test_market.py b/tests/test_market.py index e0cf70a..53630b7 100644 --- a/tests/test_market.py +++ b/tests/test_market.py | |||
@@ -130,19 +130,20 @@ class MarketTest(WebMockTestCase): | |||
130 | @mock.patch.object(market.Market, "get_ticker") | 130 | @mock.patch.object(market.Market, "get_ticker") |
131 | @mock.patch.object(market.TradeStore, "compute_trades") | 131 | @mock.patch.object(market.TradeStore, "compute_trades") |
132 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 132 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
133 | repartition.return_value = { | 133 | with self.subTest(available_balance_only=False),\ |
134 | "XEM": (D("0.75"), "long"), | 134 | mock.patch("market.ReportStore"): |
135 | "BTC": (D("0.25"), "long"), | 135 | def _get_ticker(c1, c2): |
136 | } | 136 | if c1 == "USDT" and c2 == "BTC": |
137 | def _get_ticker(c1, c2): | 137 | return { "average": D("0.0001") } |
138 | if c1 == "USDT" and c2 == "BTC": | 138 | if c1 == "XVG" and c2 == "BTC": |
139 | return { "average": D("0.0001") } | 139 | return { "average": D("0.000001") } |
140 | if c1 == "XVG" and c2 == "BTC": | 140 | self.fail("Should not be called with {}, {}".format(c1, c2)) |
141 | return { "average": D("0.000001") } | 141 | get_ticker.side_effect = _get_ticker |
142 | self.fail("Should be called with {}, {}".format(c1, c2)) | 142 | |
143 | get_ticker.side_effect = _get_ticker | 143 | repartition.return_value = { |
144 | 144 | "XEM": (D("0.75"), "long"), | |
145 | with mock.patch("market.ReportStore"): | 145 | "BTC": (D("0.25"), "long"), |
146 | } | ||
146 | m = market.Market(self.ccxt, self.market_args()) | 147 | m = market.Market(self.ccxt, self.market_args()) |
147 | self.ccxt.fetch_all_balances.return_value = { | 148 | self.ccxt.fetch_all_balances.return_value = { |
148 | "USDT": { | 149 | "USDT": { |
@@ -171,9 +172,109 @@ class MarketTest(WebMockTestCase): | |||
171 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 172 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
172 | m.report.log_stage.assert_called_once_with("prepare_trades", | 173 | m.report.log_stage.assert_called_once_with("prepare_trades", |
173 | base_currency='BTC', compute_value='average', | 174 | base_currency='BTC', compute_value='average', |
174 | liquidity='medium', only=None, repartition=None) | 175 | available_balance_only=False, liquidity='medium', |
176 | only=None, repartition=None) | ||
175 | m.report.log_balances.assert_called_once_with(tag="tag") | 177 | m.report.log_balances.assert_called_once_with(tag="tag") |
176 | 178 | ||
179 | compute_trades.reset_mock() | ||
180 | with self.subTest(available_balance_only=True),\ | ||
181 | mock.patch("market.ReportStore"): | ||
182 | def _get_ticker(c1, c2): | ||
183 | if c1 == "ZRC" and c2 == "BTC": | ||
184 | return { "average": D("0.0001") } | ||
185 | if c1 == "DOGE" and c2 == "BTC": | ||
186 | return { "average": D("0.000001") } | ||
187 | if c1 == "ETH" and c2 == "BTC": | ||
188 | return { "average": D("0.1") } | ||
189 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
190 | get_ticker.side_effect = _get_ticker | ||
191 | |||
192 | repartition.return_value = { | ||
193 | "DOGE": (D("0.25"), "short"), | ||
194 | "BTC": (D("0.25"), "long"), | ||
195 | "ETH": (D("0.25"), "long"), | ||
196 | "XMR": (D("0.25"), "long"), | ||
197 | } | ||
198 | m = market.Market(self.ccxt, self.market_args()) | ||
199 | self.ccxt.fetch_all_balances.return_value = { | ||
200 | "ZRC": { | ||
201 | "exchange_free": D("2.0"), | ||
202 | "exchange_used": D("0.0"), | ||
203 | "exchange_total": D("2.0"), | ||
204 | "total": D("2.0") | ||
205 | }, | ||
206 | "DOGE": { | ||
207 | "exchange_free": D("5.0"), | ||
208 | "exchange_used": D("0.0"), | ||
209 | "exchange_total": D("5.0"), | ||
210 | "total": D("5.0") | ||
211 | }, | ||
212 | "BTC": { | ||
213 | "exchange_free": D("0.075"), | ||
214 | "exchange_used": D("0.02"), | ||
215 | "exchange_total": D("0.095"), | ||
216 | "margin_available": D("0.025"), | ||
217 | "margin_in_position": D("0.01"), | ||
218 | "margin_total": D("0.035"), | ||
219 | "total": D("0.13") | ||
220 | }, | ||
221 | "ETH": { | ||
222 | "exchange_free": D("1.0"), | ||
223 | "exchange_used": D("0.0"), | ||
224 | "exchange_total": D("1.0"), | ||
225 | "total": D("1.0") | ||
226 | }, | ||
227 | } | ||
228 | |||
229 | m.balances.fetch_balances(tag="tag") | ||
230 | m.prepare_trades(available_balance_only=True) | ||
231 | compute_trades.assert_called_once() | ||
232 | |||
233 | call = compute_trades.call_args[0] | ||
234 | values_in_base = call[0] | ||
235 | new_repartition = call[1] | ||
236 | |||
237 | self.assertEqual(portfolio.Amount("BTC", "-0.025"), | ||
238 | new_repartition["DOGE"] - values_in_base["DOGE"]) | ||
239 | self.assertEqual(portfolio.Amount("BTC", "0.025"), | ||
240 | new_repartition["ETH"] - values_in_base["ETH"]) | ||
241 | self.assertEqual(0, | ||
242 | new_repartition["ZRC"] - values_in_base["ZRC"]) | ||
243 | self.assertEqual(portfolio.Amount("BTC", "0.025"), | ||
244 | new_repartition["XMR"]) | ||
245 | |||
246 | compute_trades.reset_mock() | ||
247 | with self.subTest(available_balance_only=True, balance=0),\ | ||
248 | mock.patch("market.ReportStore"): | ||
249 | def _get_ticker(c1, c2): | ||
250 | if c1 == "ETH" and c2 == "BTC": | ||
251 | return { "average": D("0.1") } | ||
252 | self.fail("Should not be called with {}, {}".format(c1, c2)) | ||
253 | get_ticker.side_effect = _get_ticker | ||
254 | |||
255 | repartition.return_value = { | ||
256 | "BTC": (D("0.5"), "long"), | ||
257 | "ETH": (D("0.5"), "long"), | ||
258 | } | ||
259 | m = market.Market(self.ccxt, self.market_args()) | ||
260 | self.ccxt.fetch_all_balances.return_value = { | ||
261 | "ETH": { | ||
262 | "exchange_free": D("1.0"), | ||
263 | "exchange_used": D("0.0"), | ||
264 | "exchange_total": D("1.0"), | ||
265 | "total": D("1.0") | ||
266 | }, | ||
267 | } | ||
268 | |||
269 | m.balances.fetch_balances(tag="tag") | ||
270 | m.prepare_trades(available_balance_only=True) | ||
271 | compute_trades.assert_called_once() | ||
272 | |||
273 | call = compute_trades.call_args[0] | ||
274 | values_in_base = call[0] | ||
275 | new_repartition = call[1] | ||
276 | |||
277 | self.assertEqual(new_repartition["ETH"], values_in_base["ETH"]) | ||
177 | 278 | ||
178 | @mock.patch.object(market.time, "sleep") | 279 | @mock.patch.object(market.time, "sleep") |
179 | @mock.patch.object(market.TradeStore, "all_orders") | 280 | @mock.patch.object(market.TradeStore, "all_orders") |
@@ -859,7 +960,7 @@ class ProcessorTest(WebMockTestCase): | |||
859 | 960 | ||
860 | method, arguments = processor.method_arguments("prepare_trades") | 961 | method, arguments = processor.method_arguments("prepare_trades") |
861 | self.assertEqual(m.prepare_trades, method) | 962 | self.assertEqual(m.prepare_trades, method) |
862 | self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only'], arguments) | 963 | self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only', 'available_balance_only'], arguments) |
863 | 964 | ||
864 | method, arguments = processor.method_arguments("prepare_orders") | 965 | method, arguments = processor.method_arguments("prepare_orders") |
865 | self.assertEqual(m.trades.prepare_orders, method) | 966 | self.assertEqual(m.trades.prepare_orders, method) |