|
|
@@ -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"
|