from decimal import Decimal import pytest from llm_trade_bot.models import ( AccountSnapshot, Category, InstrumentRules, OpenTradeRequest, OrderType, Side, ) from llm_trade_bot.risk import OrderRejected, OrderValidator @pytest.fixture def engine() -> OrderValidator: return OrderValidator() @pytest.fixture def account() -> AccountSnapshot: return AccountSnapshot(Decimal(10000), Decimal(10000), Decimal(0), 0) @pytest.fixture def rules() -> InstrumentRules: return InstrumentRules(Decimal("0.1"), Decimal("0.001"), Decimal("0.001"), Decimal(5), Decimal(100)) def request(**overrides) -> OpenTradeRequest: values = { "category": Category.LINEAR, "symbol": "BTCUSDT", "side": Side.BUY, "order_type": OrderType.MARKET, "reference_price": Decimal(100), "limit_price": None, "stop_loss": Decimal(90), "take_profit": Decimal(120), "trailing_distance": None, "leverage": Decimal(3), "rationale": "test", } values.update(overrides) return OpenTradeRequest(**values) def test_uses_all_available_buying_power(engine, account, rules): result = engine.validate_open(request(), account, rules) assert result.quantity == Decimal(300) assert result.notional == Decimal(30000) def test_existing_exposure_does_not_cap_position(engine, rules): account = AccountSnapshot(Decimal(10000), Decimal(10000), Decimal(3900), 2) result = engine.validate_open(request(), account, rules) assert result.quantity == Decimal(300) assert result.notional == Decimal(30000) @pytest.mark.parametrize( "overrides, message", [ ({"leverage": Decimal(101)}, "Leverage"), ({"stop_loss": Decimal(101)}, "stop-loss"), ({"take_profit": None, "trailing_distance": None}, "take-profit or trailing"), ({"category": Category.SPOT, "order_type": OrderType.LIMIT, "limit_price": Decimal(100), "take_profit": None}, "Spot entry requires"), ], ) def test_rejects_unsafe_requests(engine, account, rules, overrides, message): with pytest.raises(OrderRejected, match=message): engine.validate_open(request(**overrides), account, rules)