diff options
-rw-r--r-- | market.py | 9 | ||||
-rw-r--r-- | portfolio.py | 50 | ||||
-rw-r--r-- | test.py | 112 |
3 files changed, 94 insertions, 77 deletions
@@ -181,10 +181,17 @@ class Market: | |||
181 | self.report.log_stage("follow_orders_tick_{}".format(tick)) | 181 | self.report.log_stage("follow_orders_tick_{}".format(tick)) |
182 | self.report.log_orders(open_orders, tick=tick) | 182 | self.report.log_orders(open_orders, tick=tick) |
183 | for order in open_orders: | 183 | for order in open_orders: |
184 | if order.get_status() != "open": | 184 | status = order.get_status() |
185 | if status != "open": | ||
185 | self.report.log_order(order, tick, finished=True) | 186 | self.report.log_order(order, tick, finished=True) |
186 | else: | 187 | else: |
187 | order.trade.update_order(order, tick) | 188 | order.trade.update_order(order, tick) |
189 | if status == "error_disappeared": | ||
190 | self.report.log_error("follow_orders", | ||
191 | message="{} disappeared, recreating it".format(order)) | ||
192 | order.trade.prepare_order( | ||
193 | compute_value=order.trade.tick_actions_recreate(tick)) | ||
194 | |||
188 | self.report.log_stage("follow_orders_end") | 195 | self.report.log_stage("follow_orders_end") |
189 | 196 | ||
190 | def prepare_trades(self, base_currency="BTC", liquidity="medium", | 197 | def prepare_trades(self, base_currency="BTC", liquidity="medium", |
diff --git a/portfolio.py b/portfolio.py index 9e98c43..535aaa8 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -269,20 +269,24 @@ class Trade: | |||
269 | filled_amount += order.filled_amount(in_base_currency=in_base_currency) | 269 | filled_amount += order.filled_amount(in_base_currency=in_base_currency) |
270 | return filled_amount | 270 | return filled_amount |
271 | 271 | ||
272 | def update_order(self, order, tick): | 272 | tick_actions = { |
273 | actions = { | 273 | 0: ["waiting", None], |
274 | 0: ["waiting", None], | 274 | 1: ["waiting", None], |
275 | 1: ["waiting", None], | 275 | 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2], |
276 | 2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2], | 276 | 3: ["waiting", None], |
277 | 3: ["waiting", None], | 277 | 4: ["waiting", None], |
278 | 4: ["waiting", None], | 278 | 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3], |
279 | 5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3], | 279 | 6: ["waiting", None], |
280 | 6: ["waiting", None], | 280 | 7: ["market_fallback", "default"], |
281 | 7: ["market_fallback", "default"], | 281 | } |
282 | } | ||
283 | 282 | ||
284 | if tick in actions: | 283 | def tick_actions_recreate(self, tick, default="average"): |
285 | update, compute_value = actions[tick] | 284 | return ([default] + \ |
285 | [ y[1] for x, y in self.tick_actions.items() if x <= tick and y[1] is not None ])[-1] | ||
286 | |||
287 | def update_order(self, order, tick): | ||
288 | if tick in self.tick_actions: | ||
289 | update, compute_value = self.tick_actions[tick] | ||
286 | elif tick % 3 == 1: | 290 | elif tick % 3 == 1: |
287 | update = "market_adjust" | 291 | update = "market_adjust" |
288 | compute_value = "default" | 292 | compute_value = "default" |
@@ -381,14 +385,6 @@ class Trade: | |||
381 | self.orders.append(order) | 385 | self.orders.append(order) |
382 | return order | 386 | return order |
383 | 387 | ||
384 | def reopen_same_order(self, order): | ||
385 | new_order = Order(order.action, order.amount, order.rate, | ||
386 | order.base_currency, order.trade_type, self.market, | ||
387 | self, close_if_possible=order.close_if_possible) | ||
388 | self.orders.append(new_order) | ||
389 | new_order.run() | ||
390 | return new_order | ||
391 | |||
392 | def as_json(self): | 388 | def as_json(self): |
393 | return { | 389 | return { |
394 | "action": self.action, | 390 | "action": self.action, |
@@ -550,14 +546,11 @@ class Order: | |||
550 | self.fetch() | 546 | self.fetch() |
551 | return self.status | 547 | return self.status |
552 | 548 | ||
553 | def fix_disappeared_order(self): | 549 | def mark_disappeared_order(self): |
554 | if self.status.startswith("closed") and \ | 550 | if self.status.startswith("closed") and \ |
555 | len(self.mouvements) == 1 and \ | 551 | len(self.mouvements) > 0 and \ |
556 | self.mouvements[0].total_in_base == 0: | 552 | self.mouvements[-1].total_in_base == 0: |
557 | self.status = "error_disappeared" | 553 | self.status = "error_disappeared" |
558 | new_order = self.trade.reopen_same_order(self) | ||
559 | self.market.report.log_error("fetch", | ||
560 | message="Order {} disappeared, recreating it as {}".format(self, new_order)) | ||
561 | 554 | ||
562 | def mark_finished_order(self): | 555 | def mark_finished_order(self): |
563 | if self.status.startswith("closed") and self.market.debug: | 556 | if self.status.startswith("closed") and self.market.debug: |
@@ -582,7 +575,7 @@ class Order: | |||
582 | 575 | ||
583 | self.fetch_mouvements() | 576 | self.fetch_mouvements() |
584 | 577 | ||
585 | self.fix_disappeared_order() | 578 | self.mark_disappeared_order() |
586 | 579 | ||
587 | self.mark_finished_order() | 580 | self.mark_finished_order() |
588 | # FIXME: consider open order with dust remaining as closed | 581 | # FIXME: consider open order with dust remaining as closed |
@@ -614,6 +607,7 @@ class Order: | |||
614 | for mouvement_hash in mouvements: | 607 | for mouvement_hash in mouvements: |
615 | self.mouvements.append(Mouvement(self.amount.currency, | 608 | self.mouvements.append(Mouvement(self.amount.currency, |
616 | self.base_currency, mouvement_hash)) | 609 | self.base_currency, mouvement_hash)) |
610 | self.mouvements.sort(key= lambda x: x.date) | ||
617 | 611 | ||
618 | def cancel(self): | 612 | def cancel(self): |
619 | if self.market.debug: | 613 | if self.market.debug: |
@@ -1403,6 +1403,38 @@ class MarketTest(WebMockTestCase): | |||
1403 | else: | 1403 | else: |
1404 | time_mock.assert_called_with(sleep) | 1404 | time_mock.assert_called_with(sleep) |
1405 | 1405 | ||
1406 | with self.subTest("disappearing order"), \ | ||
1407 | mock.patch("market.ReportStore"): | ||
1408 | all_orders.reset_mock() | ||
1409 | m = market.Market(self.ccxt, self.market_args()) | ||
1410 | |||
1411 | order_mock1 = mock.Mock() | ||
1412 | order_mock2 = mock.Mock() | ||
1413 | all_orders.side_effect = [ | ||
1414 | [order_mock1, order_mock2], | ||
1415 | [order_mock1, order_mock2], | ||
1416 | |||
1417 | [order_mock1, order_mock2], | ||
1418 | [order_mock1, order_mock2], | ||
1419 | |||
1420 | [] | ||
1421 | ] | ||
1422 | |||
1423 | order_mock1.get_status.side_effect = ["open", "closed"] | ||
1424 | order_mock2.get_status.side_effect = ["open", "error_disappeared"] | ||
1425 | |||
1426 | order_mock1.trade = mock.Mock() | ||
1427 | trade_mock = mock.Mock() | ||
1428 | order_mock2.trade = trade_mock | ||
1429 | |||
1430 | trade_mock.tick_actions_recreate.return_value = "tick1" | ||
1431 | |||
1432 | m.follow_orders() | ||
1433 | |||
1434 | trade_mock.tick_actions_recreate.assert_called_once_with(2) | ||
1435 | trade_mock.prepare_order.assert_called_once_with(compute_value="tick1") | ||
1436 | m.report.log_error.assert_called_once_with("follow_orders", message=mock.ANY) | ||
1437 | |||
1406 | @mock.patch.object(market.BalanceStore, "fetch_balances") | 1438 | @mock.patch.object(market.BalanceStore, "fetch_balances") |
1407 | def test_move_balance(self, fetch_balances): | 1439 | def test_move_balance(self, fetch_balances): |
1408 | for debug in [True, False]: | 1440 | for debug in [True, False]: |
@@ -2406,27 +2438,6 @@ class TradeTest(WebMockTestCase): | |||
2406 | order1.filled_amount.assert_called_with(in_base_currency=True) | 2438 | order1.filled_amount.assert_called_with(in_base_currency=True) |
2407 | order2.filled_amount.assert_called_with(in_base_currency=True) | 2439 | order2.filled_amount.assert_called_with(in_base_currency=True) |
2408 | 2440 | ||
2409 | def test_reopen_same_order(self): | ||
2410 | value_from = portfolio.Amount("BTC", "0.5") | ||
2411 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | ||
2412 | value_to = portfolio.Amount("BTC", "1.0") | ||
2413 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) | ||
2414 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | ||
2415 | D("0.1"), "BTC", "long", self.m, "trade") | ||
2416 | with mock.patch("portfolio.Order.run") as run: | ||
2417 | new_order = trade.reopen_same_order(order) | ||
2418 | self.assertEqual("buy", new_order.action) | ||
2419 | self.assertEqual(portfolio.Amount("ETH", 10), new_order.amount) | ||
2420 | self.assertEqual(D("0.1"), new_order.rate) | ||
2421 | self.assertEqual("BTC", new_order.base_currency) | ||
2422 | self.assertEqual("long", new_order.trade_type) | ||
2423 | self.assertEqual(self.m, new_order.market) | ||
2424 | self.assertEqual(False, new_order.close_if_possible) | ||
2425 | self.assertEqual(trade, new_order.trade) | ||
2426 | run.assert_called_once() | ||
2427 | self.assertEqual(1, len(trade.orders)) | ||
2428 | self.assertEqual(new_order, trade.orders[0]) | ||
2429 | |||
2430 | @mock.patch.object(portfolio.Computation, "compute_value") | 2441 | @mock.patch.object(portfolio.Computation, "compute_value") |
2431 | @mock.patch.object(portfolio.Trade, "filled_amount") | 2442 | @mock.patch.object(portfolio.Trade, "filled_amount") |
2432 | @mock.patch.object(portfolio, "Order") | 2443 | @mock.patch.object(portfolio, "Order") |
@@ -2582,6 +2593,21 @@ class TradeTest(WebMockTestCase): | |||
2582 | D("125"), "FOO", "long", self.m, | 2593 | D("125"), "FOO", "long", self.m, |
2583 | trade, close_if_possible=False) | 2594 | trade, close_if_possible=False) |
2584 | 2595 | ||
2596 | def test_tick_actions_recreate(self): | ||
2597 | value_from = portfolio.Amount("BTC", "0.5") | ||
2598 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | ||
2599 | value_to = portfolio.Amount("BTC", "1.0") | ||
2600 | trade = portfolio.Trade(value_from, value_to, "ETH", self.m) | ||
2601 | |||
2602 | self.assertEqual("average", trade.tick_actions_recreate(0)) | ||
2603 | self.assertEqual("foo", trade.tick_actions_recreate(0, default="foo")) | ||
2604 | self.assertEqual("average", trade.tick_actions_recreate(1)) | ||
2605 | self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(2)) | ||
2606 | self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(3)) | ||
2607 | self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(5)) | ||
2608 | self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(6)) | ||
2609 | self.assertEqual("default", trade.tick_actions_recreate(7)) | ||
2610 | self.assertEqual("default", trade.tick_actions_recreate(8)) | ||
2585 | 2611 | ||
2586 | @mock.patch.object(portfolio.Trade, "prepare_order") | 2612 | @mock.patch.object(portfolio.Trade, "prepare_order") |
2587 | def test_update_order(self, prepare_order): | 2613 | def test_update_order(self, prepare_order): |
@@ -2984,12 +3010,12 @@ class OrderTest(WebMockTestCase): | |||
2984 | self.m.ccxt.privatePostReturnOrderTrades.return_value = [ | 3010 | self.m.ccxt.privatePostReturnOrderTrades.return_value = [ |
2985 | { | 3011 | { |
2986 | "tradeID": 42, "type": "buy", "fee": "0.0015", | 3012 | "tradeID": 42, "type": "buy", "fee": "0.0015", |
2987 | "date": "2017-12-30 12:00:12", "rate": "0.1", | 3013 | "date": "2017-12-30 13:00:12", "rate": "0.1", |
2988 | "amount": "3", "total": "0.3" | 3014 | "amount": "3", "total": "0.3" |
2989 | }, | 3015 | }, |
2990 | { | 3016 | { |
2991 | "tradeID": 43, "type": "buy", "fee": "0.0015", | 3017 | "tradeID": 43, "type": "buy", "fee": "0.0015", |
2992 | "date": "2017-12-30 13:00:12", "rate": "0.2", | 3018 | "date": "2017-12-30 12:00:12", "rate": "0.2", |
2993 | "amount": "2", "total": "0.4" | 3019 | "amount": "2", "total": "0.4" |
2994 | } | 3020 | } |
2995 | ] | 3021 | ] |
@@ -3002,8 +3028,8 @@ class OrderTest(WebMockTestCase): | |||
3002 | 3028 | ||
3003 | self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12}) | 3029 | self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12}) |
3004 | self.assertEqual(2, len(order.mouvements)) | 3030 | self.assertEqual(2, len(order.mouvements)) |
3005 | self.assertEqual(42, order.mouvements[0].id) | 3031 | self.assertEqual(43, order.mouvements[0].id) |
3006 | self.assertEqual(43, order.mouvements[1].id) | 3032 | self.assertEqual(42, order.mouvements[1].id) |
3007 | 3033 | ||
3008 | self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError | 3034 | self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError |
3009 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3035 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -3059,9 +3085,9 @@ class OrderTest(WebMockTestCase): | |||
3059 | self.m.report.log_debug_action.assert_called_once() | 3085 | self.m.report.log_debug_action.assert_called_once() |
3060 | 3086 | ||
3061 | @mock.patch.object(portfolio.Order, "fetch_mouvements") | 3087 | @mock.patch.object(portfolio.Order, "fetch_mouvements") |
3062 | @mock.patch.object(portfolio.Order, "fix_disappeared_order") | 3088 | @mock.patch.object(portfolio.Order, "mark_disappeared_order") |
3063 | @mock.patch.object(portfolio.Order, "mark_finished_order") | 3089 | @mock.patch.object(portfolio.Order, "mark_finished_order") |
3064 | def test_fetch(self, mark_finished_order, fix_disappeared_order, fetch_mouvements): | 3090 | def test_fetch(self, mark_finished_order, mark_disappeared_order, fetch_mouvements): |
3065 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3091 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
3066 | D("0.1"), "BTC", "long", self.m, "trade") | 3092 | D("0.1"), "BTC", "long", self.m, "trade") |
3067 | order.id = 45 | 3093 | order.id = 45 |
@@ -3072,7 +3098,7 @@ class OrderTest(WebMockTestCase): | |||
3072 | self.m.report.log_debug_action.reset_mock() | 3098 | self.m.report.log_debug_action.reset_mock() |
3073 | self.m.ccxt.fetch_order.assert_not_called() | 3099 | self.m.ccxt.fetch_order.assert_not_called() |
3074 | mark_finished_order.assert_not_called() | 3100 | mark_finished_order.assert_not_called() |
3075 | fix_disappeared_order.assert_not_called() | 3101 | mark_disappeared_order.assert_not_called() |
3076 | fetch_mouvements.assert_not_called() | 3102 | fetch_mouvements.assert_not_called() |
3077 | 3103 | ||
3078 | with self.subTest(debug=False): | 3104 | with self.subTest(debug=False): |
@@ -3090,7 +3116,7 @@ class OrderTest(WebMockTestCase): | |||
3090 | self.assertEqual(1, len(order.results)) | 3116 | self.assertEqual(1, len(order.results)) |
3091 | self.m.report.log_debug_action.assert_not_called() | 3117 | self.m.report.log_debug_action.assert_not_called() |
3092 | mark_finished_order.assert_called_once() | 3118 | mark_finished_order.assert_called_once() |
3093 | fix_disappeared_order.assert_called_once() | 3119 | mark_disappeared_order.assert_called_once() |
3094 | 3120 | ||
3095 | mark_finished_order.reset_mock() | 3121 | mark_finished_order.reset_mock() |
3096 | with self.subTest(missing_order=True): | 3122 | with self.subTest(missing_order=True): |
@@ -3101,7 +3127,7 @@ class OrderTest(WebMockTestCase): | |||
3101 | self.assertEqual("closed_unknown", order.status) | 3127 | self.assertEqual("closed_unknown", order.status) |
3102 | mark_finished_order.assert_called_once() | 3128 | mark_finished_order.assert_called_once() |
3103 | 3129 | ||
3104 | def test_fix_disappeared_order(self): | 3130 | def test_mark_disappeared_order(self): |
3105 | with self.subTest("Open order"): | 3131 | with self.subTest("Open order"): |
3106 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3132 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
3107 | D("0.1"), "BTC", "long", self.m, "trade") | 3133 | D("0.1"), "BTC", "long", self.m, "trade") |
@@ -3116,9 +3142,8 @@ class OrderTest(WebMockTestCase): | |||
3116 | "fee":"0.00150000", | 3142 | "fee":"0.00150000", |
3117 | "date":"2018-04-02 00:09:13" | 3143 | "date":"2018-04-02 00:09:13" |
3118 | })) | 3144 | })) |
3119 | with mock.patch.object(order, "trade") as trade: | 3145 | order.mark_disappeared_order() |
3120 | order.fix_disappeared_order() | 3146 | self.assertEqual("pending", order.status) |
3121 | trade.reopen_same_order.assert_not_called() | ||
3122 | 3147 | ||
3123 | with self.subTest("Non-zero amount"): | 3148 | with self.subTest("Non-zero amount"): |
3124 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3149 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -3135,10 +3160,8 @@ class OrderTest(WebMockTestCase): | |||
3135 | "fee":"0.00150000", | 3160 | "fee":"0.00150000", |
3136 | "date":"2018-04-02 00:09:13" | 3161 | "date":"2018-04-02 00:09:13" |
3137 | })) | 3162 | })) |
3138 | with mock.patch.object(order, "trade") as trade: | 3163 | order.mark_disappeared_order() |
3139 | order.fix_disappeared_order() | 3164 | self.assertEqual("closed", order.status) |
3140 | self.assertEqual("closed", order.status) | ||
3141 | trade.reopen_same_order.assert_not_called() | ||
3142 | 3165 | ||
3143 | with self.subTest("Other mouvements"): | 3166 | with self.subTest("Other mouvements"): |
3144 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3167 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -3165,10 +3188,8 @@ class OrderTest(WebMockTestCase): | |||
3165 | "fee":"0.00150000", | 3188 | "fee":"0.00150000", |
3166 | "date":"2018-04-02 00:09:13" | 3189 | "date":"2018-04-02 00:09:13" |
3167 | })) | 3190 | })) |
3168 | with mock.patch.object(order, "trade") as trade: | 3191 | order.mark_disappeared_order() |
3169 | order.fix_disappeared_order() | 3192 | self.assertEqual("error_disappeared", order.status) |
3170 | self.assertEqual("closed", order.status) | ||
3171 | trade.reopen_same_order.assert_not_called() | ||
3172 | 3193 | ||
3173 | with self.subTest("Order disappeared"): | 3194 | with self.subTest("Order disappeared"): |
3174 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), | 3195 | order = portfolio.Order("buy", portfolio.Amount("ETH", 10), |
@@ -3185,13 +3206,8 @@ class OrderTest(WebMockTestCase): | |||
3185 | "fee":"0.00150000", | 3206 | "fee":"0.00150000", |
3186 | "date":"2018-04-02 00:09:13" | 3207 | "date":"2018-04-02 00:09:13" |
3187 | })) | 3208 | })) |
3188 | with mock.patch.object(order, "trade") as trade: | 3209 | order.mark_disappeared_order() |
3189 | trade.reopen_same_order.return_value = "New order" | 3210 | self.assertEqual("error_disappeared", order.status) |
3190 | order.fix_disappeared_order() | ||
3191 | self.assertEqual("error_disappeared", order.status) | ||
3192 | trade.reopen_same_order.assert_called_once_with(order) | ||
3193 | self.m.report.log_error.assert_called_once_with('fetch', | ||
3194 | message='Order Order(buy long 10.00000000 ETH at 0.1 BTC [error_disappeared]) disappeared, recreating it as New order') | ||
3195 | 3211 | ||
3196 | @mock.patch.object(portfolio.Order, "fetch") | 3212 | @mock.patch.object(portfolio.Order, "fetch") |
3197 | def test_get_status(self, fetch): | 3213 | def test_get_status(self, fetch): |