|
|
@@ -93,6 +93,88 @@ def test_strategy_context_binds_identity(monkeypatch):
|
|
|
assert calls["cancel_all"] == {"account_id": "acct-1", "client_id": "client-1"}
|
|
|
|
|
|
|
|
|
+def test_strategy_context_cancel_all_orders_confirmed_after_empty_followup(monkeypatch):
|
|
|
+ calls = {"open_orders": 0}
|
|
|
+
|
|
|
+ def fake_cancel_all(account_id, client_id=None):
|
|
|
+ calls["cancel_all"] = {"account_id": account_id, "client_id": client_id}
|
|
|
+ return {"ok": True, "cancelled": [{"ok": True, "order_id": "o1"}]}
|
|
|
+
|
|
|
+ def fake_open_orders(account_id, client_id=None):
|
|
|
+ calls["open_orders"] += 1
|
|
|
+ return {"orders": []}
|
|
|
+
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.cancel_all_orders", fake_cancel_all)
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.list_open_orders", fake_open_orders)
|
|
|
+
|
|
|
+ ctx = StrategyContext(id="inst-1", account_id="acct-1", client_id="client-1", mode="active")
|
|
|
+ result = ctx.cancel_all_orders_confirmed()
|
|
|
+
|
|
|
+ assert result["conclusive"] is True
|
|
|
+ assert result["cleanup_status"] == "cleanup_confirmed"
|
|
|
+ assert result["cancelled_order_ids"] == ["o1"]
|
|
|
+ assert result["remaining_orders"] == []
|
|
|
+ assert result["error"] is None
|
|
|
+ assert calls["cancel_all"] == {"account_id": "acct-1", "client_id": "client-1"}
|
|
|
+ assert calls["open_orders"] == 1
|
|
|
+
|
|
|
+
|
|
|
+def test_strategy_context_cancel_all_orders_confirmed_preserves_inconclusive_failure(monkeypatch):
|
|
|
+ calls = {"open_orders": 0}
|
|
|
+
|
|
|
+ def fake_cancel_all(account_id, client_id=None):
|
|
|
+ raise RuntimeError("auth breaker active")
|
|
|
+
|
|
|
+ def fake_open_orders(account_id, client_id=None):
|
|
|
+ calls["open_orders"] += 1
|
|
|
+ return {"orders": [{"id": "o1"}]}
|
|
|
+
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.cancel_all_orders", fake_cancel_all)
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.list_open_orders", fake_open_orders)
|
|
|
+
|
|
|
+ ctx = StrategyContext(id="inst-1", account_id="acct-1", client_id="client-1", mode="active")
|
|
|
+ result = ctx.cancel_all_orders_confirmed()
|
|
|
+
|
|
|
+ assert result["conclusive"] is False
|
|
|
+ assert result["cleanup_status"] == "cleanup_failed"
|
|
|
+ assert result["cancelled_order_ids"] == []
|
|
|
+ assert result["remaining_orders"] == [{"id": "o1"}]
|
|
|
+ assert result["error"] == "auth breaker active"
|
|
|
+ assert calls["open_orders"] == 1
|
|
|
+
|
|
|
+
|
|
|
+def test_strategy_context_cancel_all_orders_confirmed_keeps_partial_reply_inconclusive(monkeypatch):
|
|
|
+ calls = {"open_orders": 0}
|
|
|
+
|
|
|
+ def fake_cancel_all(account_id, client_id=None):
|
|
|
+ calls["cancel_all"] = {"account_id": account_id, "client_id": client_id}
|
|
|
+ return {
|
|
|
+ "ok": True,
|
|
|
+ "cancelled": [
|
|
|
+ {"ok": True, "order_id": "o1"},
|
|
|
+ {"ok": False, "order_id": "o2", "status": "deferred", "error": "auth breaker active"},
|
|
|
+ ],
|
|
|
+ }
|
|
|
+
|
|
|
+ def fake_open_orders(account_id, client_id=None):
|
|
|
+ calls["open_orders"] += 1
|
|
|
+ return {"orders": []}
|
|
|
+
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.cancel_all_orders", fake_cancel_all)
|
|
|
+ monkeypatch.setattr("src.trader_mcp.strategy_context.list_open_orders", fake_open_orders)
|
|
|
+
|
|
|
+ ctx = StrategyContext(id="inst-1", account_id="acct-1", client_id="client-1", mode="active")
|
|
|
+ result = ctx.cancel_all_orders_confirmed()
|
|
|
+
|
|
|
+ assert result["conclusive"] is False
|
|
|
+ assert result["cleanup_status"] == "cleanup_partial"
|
|
|
+ assert result["cancelled_order_ids"] == ["o1"]
|
|
|
+ assert result["remaining_orders"] == []
|
|
|
+ assert result["error"] == "cancel-all reported uncancelled orders"
|
|
|
+ assert calls["cancel_all"] == {"account_id": "acct-1", "client_id": "client-1"}
|
|
|
+ assert calls["open_orders"] == 1
|
|
|
+
|
|
|
+
|
|
|
def test_stop_loss_strategy_loads_with_aligned_regime_config(tmp_path):
|
|
|
original_db = strategy_store.DB_PATH
|
|
|
original_dir = strategy_registry.STRATEGIES_DIR
|
|
|
@@ -584,6 +666,9 @@ def test_grid_recenters_exactly_on_live_price():
|
|
|
def cancel_all_orders(self):
|
|
|
return {"ok": True}
|
|
|
|
|
|
+ def cancel_all_orders_confirmed(self):
|
|
|
+ return {"conclusive": True, "error": None, "cleanup_status": "cleanup_confirmed", "cancelled_order_ids": []}
|
|
|
+
|
|
|
def get_fee_rates(self, market):
|
|
|
return {"maker": 0.0, "taker": 0.0}
|
|
|
|
|
|
@@ -601,6 +686,53 @@ def test_grid_recenters_exactly_on_live_price():
|
|
|
assert strategy.state["center_price"] == 160.0
|
|
|
|
|
|
|
|
|
+def test_grid_recenter_preserves_tracked_orders_when_cancel_is_inconclusive():
|
|
|
+ class FakeContext:
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ minimum_order_value = 10.0
|
|
|
+ mode = "active"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.placed_orders = []
|
|
|
+
|
|
|
+ def cancel_all_orders_confirmed(self):
|
|
|
+ return {
|
|
|
+ "conclusive": False,
|
|
|
+ "error": "auth breaker active",
|
|
|
+ "cleanup_status": "cleanup_partial",
|
|
|
+ "cancelled_order_ids": ["o1"],
|
|
|
+ }
|
|
|
+
|
|
|
+ def get_fee_rates(self, market):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ return 0.1
|
|
|
+
|
|
|
+ def place_order(self, **kwargs):
|
|
|
+ self.placed_orders.append(kwargs)
|
|
|
+ return {"status": "ok", "id": "oid-1"}
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strategy = GridStrategy(ctx, {})
|
|
|
+ strategy.state["center_price"] = 100.0
|
|
|
+ strategy.state["orders"] = [{"id": "o1"}, {"id": "o2"}]
|
|
|
+ strategy.state["order_ids"] = ["o1", "o2"]
|
|
|
+ strategy.state["open_order_count"] = 2
|
|
|
+
|
|
|
+ strategy._recenter_and_rebuild_from_price(160.0, "test recenter")
|
|
|
+
|
|
|
+ assert strategy.state["center_price"] == 100.0
|
|
|
+ assert strategy.state["orders"] == [{"id": "o2"}]
|
|
|
+ assert strategy.state["order_ids"] == ["o2"]
|
|
|
+ assert strategy.state["open_order_count"] == 1
|
|
|
+ assert strategy.state["cleanup_status"] == "cleanup_partial"
|
|
|
+ assert strategy.state["last_action"] == "test recenter cleanup pending"
|
|
|
+ assert ctx.placed_orders == []
|
|
|
+
|
|
|
+
|
|
|
def test_grid_stop_cancels_all_open_orders():
|
|
|
class FakeContext:
|
|
|
base_currency = "XRP"
|
|
|
@@ -616,6 +748,10 @@ def test_grid_stop_cancels_all_open_orders():
|
|
|
self.cancelled = True
|
|
|
return {"ok": True}
|
|
|
|
|
|
+ def cancel_all_orders_confirmed(self):
|
|
|
+ self.cancelled = True
|
|
|
+ return {"conclusive": True, "error": None, "cleanup_status": "cleanup_confirmed", "cancelled_order_ids": ["o1"]}
|
|
|
+
|
|
|
def get_fee_rates(self, market):
|
|
|
return {"maker": 0.0, "taker": 0.0}
|
|
|
|
|
|
@@ -628,9 +764,90 @@ def test_grid_stop_cancels_all_open_orders():
|
|
|
|
|
|
assert strategy.context.cancelled is True
|
|
|
assert strategy.state["open_order_count"] == 0
|
|
|
+ assert strategy.state["cleanup_status"] == "cleanup_confirmed"
|
|
|
assert strategy.state["last_action"] == "stopped"
|
|
|
|
|
|
|
|
|
+def test_grid_stop_preserves_tracked_orders_when_cancel_is_inconclusive():
|
|
|
+ class FakeContext:
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ minimum_order_value = 10.0
|
|
|
+ mode = "active"
|
|
|
+
|
|
|
+ def cancel_all_orders_confirmed(self):
|
|
|
+ return {
|
|
|
+ "conclusive": False,
|
|
|
+ "error": "auth breaker active",
|
|
|
+ "cleanup_status": "cleanup_failed",
|
|
|
+ "cancelled_order_ids": [],
|
|
|
+ }
|
|
|
+
|
|
|
+ def get_fee_rates(self, market):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ strategy = GridStrategy(FakeContext(), {})
|
|
|
+ strategy.state["orders"] = [{"id": "o1"}]
|
|
|
+ strategy.state["order_ids"] = ["o1"]
|
|
|
+ strategy.state["open_order_count"] = 1
|
|
|
+
|
|
|
+ strategy.on_stop()
|
|
|
+
|
|
|
+ assert strategy.state["orders"] == [{"id": "o1"}]
|
|
|
+ assert strategy.state["order_ids"] == ["o1"]
|
|
|
+ assert strategy.state["open_order_count"] == 1
|
|
|
+ assert strategy.state["cleanup_status"] == "cleanup_failed"
|
|
|
+ assert strategy.state["last_action"] == "stop cleanup pending"
|
|
|
+ assert strategy.state["last_error"] == "auth breaker active"
|
|
|
+
|
|
|
+
|
|
|
+def test_grid_observe_mode_preserves_tracked_orders_when_cancel_is_inconclusive(monkeypatch):
|
|
|
+ class FakeContext:
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ minimum_order_value = 10.0
|
|
|
+ mode = "observe"
|
|
|
+
|
|
|
+ def cancel_all_orders_confirmed(self):
|
|
|
+ return {
|
|
|
+ "conclusive": False,
|
|
|
+ "error": "auth breaker active",
|
|
|
+ "cleanup_status": "cleanup_failed",
|
|
|
+ "cancelled_order_ids": [],
|
|
|
+ }
|
|
|
+
|
|
|
+ def get_fee_rates(self, market):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def get_account_info(self):
|
|
|
+ return {"balances": [{"asset_code": "USD", "available": 50.0}, {"asset_code": "XRP", "available": 5.0}]}
|
|
|
+
|
|
|
+ strategy = GridStrategy(FakeContext(), {})
|
|
|
+ strategy.state["center_price"] = 1.42
|
|
|
+ strategy.state["seeded"] = True
|
|
|
+ strategy.state["orders"] = [{"id": "o1", "side": "buy", "price": 1.4, "amount": 10.0}]
|
|
|
+ strategy.state["order_ids"] = ["o1"]
|
|
|
+ strategy.state["open_order_count"] = 1
|
|
|
+
|
|
|
+ monkeypatch.setattr(strategy, "_price", lambda: 1.42)
|
|
|
+ monkeypatch.setattr(strategy, "_refresh_regimes", lambda: None)
|
|
|
+ monkeypatch.setattr(strategy, "_refresh_balance_snapshot", lambda: True)
|
|
|
+ monkeypatch.setattr(strategy, "_sync_open_orders_state", lambda: [{"id": "o1", "side": "buy", "price": 1.4, "amount": 10.0}])
|
|
|
+
|
|
|
+ result = strategy.on_tick({})
|
|
|
+
|
|
|
+ assert result["action"] == "observe"
|
|
|
+ assert result["cleanup_pending"] is True
|
|
|
+ assert strategy.state["orders"] == [{"id": "o1", "side": "buy", "price": 1.4, "amount": 10.0}]
|
|
|
+ assert strategy.state["order_ids"] == ["o1"]
|
|
|
+ assert strategy.state["open_order_count"] == 1
|
|
|
+ assert strategy.state["cleanup_status"] == "cleanup_failed"
|
|
|
+ assert strategy.state["last_action"] == "observe cleanup pending"
|
|
|
+ assert strategy.state["last_error"] == "auth breaker active"
|
|
|
+
|
|
|
+
|
|
|
def test_base_strategy_report_uses_context_snapshot():
|
|
|
class FakeContext:
|
|
|
id = "s-1"
|