aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-04-03 20:43:05 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-04-03 20:43:05 +0200
commit7ba831c52bc08032a37b576e3fa1098fed0b7635 (patch)
tree9c63fc677c00ec89fd4c2ca0b9434606f4685b56
parent5542e9e31a0074f4ed3b91cadce643ad60083cde (diff)
downloadTrader-7ba831c52bc08032a37b576e3fa1098fed0b7635.tar.gz
Trader-7ba831c52bc08032a37b576e3fa1098fed0b7635.tar.zst
Trader-7ba831c52bc08032a37b576e3fa1098fed0b7635.zip
Improve fix of vanishing orders
-rw-r--r--market.py9
-rw-r--r--portfolio.py50
-rw-r--r--test.py112
3 files changed, 94 insertions, 77 deletions
diff --git a/market.py b/market.py
index d0e6ab4..ac3aa14 100644
--- a/market.py
+++ b/market.py
@@ -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:
diff --git a/test.py b/test.py
index 7b17f9e..a9a80c5 100644
--- a/test.py
+++ b/test.py
@@ -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):