Ver código fonte

order size fix

Lukas Goldschmidt 2 semanas atrás
pai
commit
ff3f6a6ef3
2 arquivos alterados com 103 adições e 4 exclusões
  1. 6 4
      src/trader_mcp/strategy_sizing.py
  2. 97 0
      tests/test_strategies.py

+ 6 - 4
src/trader_mcp/strategy_sizing.py

@@ -3,9 +3,9 @@ from __future__ import annotations
 from typing import Any
 
 
-def _call_context_suggest_order_amount(context: Any, kwargs: dict[str, Any]) -> float:
+def _call_context_suggest_order_amount(context: Any, kwargs: dict[str, Any]) -> float | None:
     if not hasattr(context, "suggest_order_amount"):
-        return 0.0
+        return None
 
     variants: list[dict[str, Any]] = []
     drop_sets = (
@@ -32,7 +32,7 @@ def _call_context_suggest_order_amount(context: Any, kwargs: dict[str, Any]) ->
 
     if last_error is not None:
         raise last_error
-    return 0.0
+    return None
 
 
 def suggest_quote_sized_amount(
@@ -72,7 +72,9 @@ def suggest_quote_sized_amount(
         "order_size": order_size,
     }
     amount = _call_context_suggest_order_amount(context, kwargs)
-    if amount > 0:
+    if amount is not None:
+        if amount <= 0:
+            return 0.0
         if side == "buy":
             if min_notional > 0 and (amount * price * (1 + fee_rate)) < min_notional:
                 return 0.0

+ 97 - 0
tests/test_strategies.py

@@ -1085,6 +1085,103 @@ def test_dumb_trader_sell_holds_sub_minimum_order():
     assert ctx.orders == []
 
 
+def test_dumb_trader_holds_when_live_sizing_reports_no_affordable_sell():
+    class FakeContext:
+        id = "s-sell-no-funds"
+        account_id = "acct-1"
+        client_id = "cid-1"
+        mode = "active"
+        market_symbol = "solusd"
+        base_currency = "SOL"
+        counter_currency = "USD"
+        minimum_order_value = 0.5
+
+        def __init__(self):
+            self.orders = []
+
+        def get_price(self, symbol):
+            return {"price": 86.69}
+
+        def get_fee_rates(self, market_symbol=None):
+            return {"maker": 0.0, "taker": 0.0}
+
+        def suggest_order_amount(self, **kwargs):
+            return 0.0
+
+        def place_order(self, **kwargs):
+            self.orders.append(kwargs)
+            return {"ok": True, "order": kwargs}
+
+        def get_account_info(self):
+            return {"balances": [{"asset_code": "USD", "available": 1000.0}, {"asset_code": "SOL", "available": 0.00447}]}
+
+        def get_strategy_snapshot(self):
+            return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
+
+    ctx = FakeContext()
+    strat = DumbStrategy(ctx, {"trade_side": "sell", "order_notional_quote": 11.0, "dust_collect": True})
+
+    result = strat.on_tick({})
+
+    assert result["action"] == "hold"
+    assert result["reason"] == "no usable size"
+    assert ctx.orders == []
+
+
+def test_dumb_trader_holds_when_balance_refresh_fails_after_restart():
+    class FakeContext:
+        id = "s-restart-hold"
+        account_id = "acct-1"
+        client_id = "cid-1"
+        mode = "active"
+        market_symbol = "solusd"
+        base_currency = "SOL"
+        counter_currency = "USD"
+        minimum_order_value = 1.0
+
+        def __init__(self):
+            self.orders = []
+
+        def get_price(self, symbol):
+            return {"price": 86.51}
+
+        def get_fee_rates(self, market_symbol=None):
+            return {"maker": 0.0, "taker": 0.0}
+
+        def get_account_info(self):
+            raise RuntimeError("Bitstamp auth breaker active, retry later")
+
+        def suggest_order_amount(self, **kwargs):
+            raise AssertionError("should not size an order after balance refresh failure")
+
+        def place_order(self, **kwargs):
+            self.orders.append(kwargs)
+            return {"ok": True, "order": kwargs}
+
+        def get_strategy_snapshot(self):
+            return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
+
+    ctx = FakeContext()
+    strat = DumbStrategy(
+        ctx,
+        {
+            "trade_side": "sell",
+            "order_notional_quote": 5.0,
+            "dust_collect": True,
+        },
+    )
+    strat.state["base_available"] = 0.127153
+    strat.state["counter_available"] = 0.0
+
+    result = strat.on_tick({})
+
+    assert result["action"] == "hold"
+    assert result["reason"] == "balance refresh unavailable"
+    assert strat.state["balance_snapshot_ok"] is False
+    assert strat.state["base_available"] == 0.0
+    assert ctx.orders == []
+
+
 def test_dumb_trader_holds_when_trade_side_is_symmetrical():
     class FakeContext:
         id = "s-target-hold"