|
|
@@ -8,6 +8,7 @@ from fastapi.testclient import TestClient
|
|
|
from src.trader_mcp import strategy_registry, strategy_store
|
|
|
from src.trader_mcp.server import app
|
|
|
from src.trader_mcp.strategy_context import StrategyContext
|
|
|
+from strategies.grid_trader import Strategy as GridStrategy
|
|
|
|
|
|
|
|
|
STRATEGY_CODE = '''
|
|
|
@@ -109,3 +110,84 @@ def test_stop_loss_strategy_loads_with_aligned_regime_config(tmp_path):
|
|
|
finally:
|
|
|
strategy_store.DB_PATH = original_db
|
|
|
strategy_registry.STRATEGIES_DIR = original_dir
|
|
|
+
|
|
|
+
|
|
|
+def test_grid_top_up_uses_missing_levels_budget():
|
|
|
+ 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 get_fee_rates(self, market):
|
|
|
+ return {"maker": 0.0, "taker": 0.004}
|
|
|
+
|
|
|
+ def get_account_info(self):
|
|
|
+ return {
|
|
|
+ "balances": [
|
|
|
+ {"asset_code": "USD", "available": 13.55},
|
|
|
+ {"asset_code": "XRP", "available": 22.0103},
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ def suggest_order_amount(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ side,
|
|
|
+ price,
|
|
|
+ levels,
|
|
|
+ min_notional,
|
|
|
+ fee_rate,
|
|
|
+ max_notional_per_order=0.0,
|
|
|
+ dust_collect=False,
|
|
|
+ inventory_cap_pct=0.0,
|
|
|
+ order_size=0.0,
|
|
|
+ safety=0.995,
|
|
|
+ ):
|
|
|
+ if side == "buy":
|
|
|
+ quote_available = 13.55
|
|
|
+ spendable_quote = quote_available * safety
|
|
|
+ quote_cap = min(spendable_quote, max_notional_per_order) if max_notional_per_order > 0 else spendable_quote
|
|
|
+ if quote_cap < min_notional * (1 + fee_rate):
|
|
|
+ return 0.0
|
|
|
+ return quote_cap / (price * (1 + fee_rate))
|
|
|
+ return 0.0
|
|
|
+
|
|
|
+ def place_order(self, **kwargs):
|
|
|
+ self.placed_orders.append(kwargs)
|
|
|
+ return {"status": "ok", "id": f"oid-{len(self.placed_orders)}"}
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strategy = GridStrategy(
|
|
|
+ ctx,
|
|
|
+ {
|
|
|
+ "grid_levels": 2,
|
|
|
+ "grid_step_pct": 0.0062,
|
|
|
+ "grid_step_min_pct": 0.0033,
|
|
|
+ "grid_step_max_pct": 0.012,
|
|
|
+ "max_notional_per_order": 12,
|
|
|
+ "order_call_delay_ms": 0,
|
|
|
+ "trade_sides": "both",
|
|
|
+ "debug_orders": True,
|
|
|
+ "dust_collect": True,
|
|
|
+ "enable_trend_guard": False,
|
|
|
+ "fee_rate": 0.004,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ strategy.state["center_price"] = 1.3285
|
|
|
+ strategy.state["orders"] = [
|
|
|
+ {"side": "buy", "price": 1.3243993, "amount": 7.63, "id": "existing-buy"},
|
|
|
+ {"side": "sell", "price": 1.3326007, "amount": 9.0, "id": "sell-1"},
|
|
|
+ {"side": "sell", "price": 1.3367011, "amount": 9.0, "id": "sell-2"},
|
|
|
+ ]
|
|
|
+ strategy.state["order_ids"] = ["existing-buy", "sell-1", "sell-2"]
|
|
|
+
|
|
|
+ strategy._top_up_missing_levels(strategy.state["center_price"], strategy.state["orders"])
|
|
|
+
|
|
|
+ assert len(ctx.placed_orders) == 1
|
|
|
+ assert ctx.placed_orders[0]["side"] == "buy"
|
|
|
+ assert float(ctx.placed_orders[0]["amount"]) > 7.57
|