|
|
@@ -911,7 +911,236 @@ def test_trend_follower_passes_live_fee_rate_into_sizing_helper():
|
|
|
return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
|
|
|
|
|
|
ctx = FakeContext()
|
|
|
- strat = TrendStrategy(ctx, {"trade_side": "buy", "order_notional_quote": 10.5})
|
|
|
+ strat = TrendStrategy(ctx, {"trade_side": "buy", "order_notional_quote": 10.5, "dust_collect": True})
|
|
|
strat.on_tick({})
|
|
|
assert ctx.fee_calls == ["xrpusd"]
|
|
|
assert ctx.suggest_calls[-1]["fee_rate"] == 0.0025
|
|
|
+ assert ctx.suggest_calls[-1]["dust_collect"] is True
|
|
|
+
|
|
|
+
|
|
|
+def test_grid_sizing_helper_receives_quote_controls_and_dust_collect():
|
|
|
+ class FakeContext:
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ minimum_order_value = 10.0
|
|
|
+ mode = "active"
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.suggest_calls = []
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.001, "taker": 0.004}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ self.suggest_calls.append(kwargs)
|
|
|
+ return 7.0
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strategy = GridStrategy(
|
|
|
+ ctx,
|
|
|
+ {
|
|
|
+ "grid_levels": 3,
|
|
|
+ "order_notional_quote": 11.0,
|
|
|
+ "max_order_notional_quote": 12.0,
|
|
|
+ "dust_collect": True,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ amount = strategy._suggest_amount("buy", 1.5, 3, 10.0)
|
|
|
+
|
|
|
+ assert amount == 7.0
|
|
|
+ assert ctx.suggest_calls[-1]["fee_rate"] == 0.004
|
|
|
+ assert ctx.suggest_calls[-1]["quote_notional"] == 11.0
|
|
|
+ assert ctx.suggest_calls[-1]["max_notional_per_order"] == 12.0
|
|
|
+ assert ctx.suggest_calls[-1]["dust_collect"] is True
|
|
|
+ assert ctx.suggest_calls[-1]["levels"] == 3
|
|
|
+
|
|
|
+
|
|
|
+def test_trend_follower_buy_clamps_last_order_to_balance_target():
|
|
|
+ class FakeContext:
|
|
|
+ id = "s-buy-clamp"
|
|
|
+ account_id = "acct-1"
|
|
|
+ client_id = "cid-1"
|
|
|
+ mode = "active"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ minimum_order_value = 0.5
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.orders = []
|
|
|
+
|
|
|
+ def get_price(self, symbol):
|
|
|
+ return {"price": 1.0}
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ return float(kwargs.get("quote_notional") or 0.0) / float(kwargs.get("price") or 1.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": 6.0}, {"asset_code": "XRP", "available": 4.0}]}
|
|
|
+
|
|
|
+ def get_strategy_snapshot(self):
|
|
|
+ return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strat = TrendStrategy(ctx, {"trade_side": "buy", "order_notional_quote": 3.0, "balance_target": 0.5})
|
|
|
+
|
|
|
+ result = strat.on_tick({})
|
|
|
+
|
|
|
+ assert result["action"] == "buy"
|
|
|
+ assert ctx.orders[-1]["amount"] == 1.0
|
|
|
+
|
|
|
+
|
|
|
+def test_trend_follower_sell_clamps_last_order_to_balance_target():
|
|
|
+ class FakeContext:
|
|
|
+ id = "s-sell-clamp"
|
|
|
+ account_id = "acct-1"
|
|
|
+ client_id = "cid-1"
|
|
|
+ mode = "active"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ minimum_order_value = 0.5
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.orders = []
|
|
|
+
|
|
|
+ def get_price(self, symbol):
|
|
|
+ return {"price": 1.0}
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ return float(kwargs.get("quote_notional") or 0.0) / float(kwargs.get("price") or 1.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": 3.0}, {"asset_code": "XRP", "available": 7.0}]}
|
|
|
+
|
|
|
+ def get_strategy_snapshot(self):
|
|
|
+ return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strat = TrendStrategy(ctx, {"trade_side": "sell", "order_notional_quote": 5.0, "balance_target": 0.5})
|
|
|
+
|
|
|
+ result = strat.on_tick({})
|
|
|
+
|
|
|
+ assert result["action"] == "sell"
|
|
|
+ assert ctx.orders[-1]["amount"] == 2.0
|
|
|
+
|
|
|
+
|
|
|
+def test_trend_follower_holds_when_balance_target_already_reached():
|
|
|
+ class FakeContext:
|
|
|
+ id = "s-target-hold"
|
|
|
+ account_id = "acct-1"
|
|
|
+ client_id = "cid-1"
|
|
|
+ mode = "active"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ minimum_order_value = 0.5
|
|
|
+
|
|
|
+ def get_price(self, symbol):
|
|
|
+ return {"price": 1.0}
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ return 10.0
|
|
|
+
|
|
|
+ def get_account_info(self):
|
|
|
+ return {"balances": [{"asset_code": "USD", "available": 5.0}, {"asset_code": "XRP", "available": 5.0}]}
|
|
|
+
|
|
|
+ def get_strategy_snapshot(self):
|
|
|
+ return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
|
|
|
+
|
|
|
+ strat = TrendStrategy(FakeContext(), {"trade_side": "buy", "order_notional_quote": 3.0, "balance_target": 0.5})
|
|
|
+
|
|
|
+ result = strat.on_tick({})
|
|
|
+
|
|
|
+ assert result["action"] == "hold"
|
|
|
+ assert result["reason"] == "balance target reached"
|
|
|
+
|
|
|
+
|
|
|
+def test_trend_follower_balance_target_one_does_not_clamp_size():
|
|
|
+ class FakeContext:
|
|
|
+ id = "s-target-open"
|
|
|
+ account_id = "acct-1"
|
|
|
+ client_id = "cid-1"
|
|
|
+ mode = "active"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+ minimum_order_value = 0.5
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.orders = []
|
|
|
+
|
|
|
+ def get_price(self, symbol):
|
|
|
+ return {"price": 1.0}
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.0, "taker": 0.0}
|
|
|
+
|
|
|
+ def suggest_order_amount(self, **kwargs):
|
|
|
+ return float(kwargs.get("quote_notional") or 0.0) / float(kwargs.get("price") or 1.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": 6.0}, {"asset_code": "XRP", "available": 4.0}]}
|
|
|
+
|
|
|
+ def get_strategy_snapshot(self):
|
|
|
+ return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
|
|
|
+
|
|
|
+ ctx = FakeContext()
|
|
|
+ strat = TrendStrategy(ctx, {"trade_side": "buy", "order_notional_quote": 3.0, "balance_target": 1.0})
|
|
|
+
|
|
|
+ result = strat.on_tick({})
|
|
|
+
|
|
|
+ assert result["action"] == "buy"
|
|
|
+ assert ctx.orders[-1]["amount"] == 3.0
|
|
|
+
|
|
|
+
|
|
|
+def test_exposure_protector_buy_sizing_respects_fee_when_min_order_quote_is_unaffordable():
|
|
|
+ class FakeContext:
|
|
|
+ account_id = "acct-1"
|
|
|
+ client_id = "cid-1"
|
|
|
+ mode = "active"
|
|
|
+ market_symbol = "xrpusd"
|
|
|
+ base_currency = "XRP"
|
|
|
+ counter_currency = "USD"
|
|
|
+
|
|
|
+ def get_fee_rates(self, market_symbol=None):
|
|
|
+ return {"maker": 0.1, "taker": 0.2}
|
|
|
+
|
|
|
+ strategy = ExposureStrategy(
|
|
|
+ FakeContext(),
|
|
|
+ {
|
|
|
+ "rebalance_target_ratio": 0.9,
|
|
|
+ "rebalance_step_ratio": 1.0,
|
|
|
+ "balance_tolerance": 0.0,
|
|
|
+ "min_order_notional_quote": 10.0,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ strategy.state["base_available"] = 0.0
|
|
|
+ strategy.state["counter_available"] = 10.0
|
|
|
+
|
|
|
+ amount = strategy._suggest_amount("buy", 1.0)
|
|
|
+
|
|
|
+ assert amount == 0.0
|