from hypothesis import given, settings from hypothesis import strategies as st from ktrade.risk.gateway import MarketState, OrderIntent, PortfolioSnapshot, RiskGateway, RiskLimits def base(): return ( OrderIntent(symbol="BTC/USD", side="buy", qty=0.01), PortfolioSnapshot(equity=10_000, cash=10_000, positions={}, marks={}, peak_equity=10_000, day_start_equity=10_000), MarketState(last_price=30_000, data_age_seconds=1, ordermin=0.0001, costmin=0.5), ) def test_allows_a_sane_order(): i, p, m = base() assert RiskGateway(RiskLimits(max_order_notional=1000, max_position_notional=5000)).evaluate(i, p, m).allowed def test_default_deny_on_missing_inputs(): i, _, _ = base() d = RiskGateway().evaluate(i, None, None) assert not d.allowed and "default-deny" in " ".join(d.reasons) def test_kill_switch_blocks_everything(): i, p, m = base() assert not RiskGateway(kill_switch=True).evaluate(i, p, m).allowed def test_stale_data_blocks(): i, p, m = base() m.data_age_seconds = 999 d = RiskGateway().evaluate(i, p, m) assert not d.allowed and d.checks["data_fresh"] is False def test_min_size_and_notional_limits(): i, p, m = base() i.qty = 0.00001 assert RiskGateway().evaluate(i, p, m).checks["min_size"] is False i.qty = 1.0 # 30k notional assert RiskGateway(RiskLimits(max_order_notional=1000)).evaluate(i, p, m).checks["order_notional"] is False def test_no_short_on_spot(): i, p, m = base() i.side = "sell" assert RiskGateway().evaluate(i, p, m).checks["no_short"] is False def test_reducing_sell_allowed_under_drawdown_but_buy_not(): i, p, m = base() p.positions = {"BTC/USD": 0.05} p.equity = 8_000 p.cash = 6_500 gw = RiskGateway(RiskLimits(max_drawdown_limit=0.10, max_order_notional=5000, max_position_notional=5000)) assert not gw.evaluate(i, p, m).allowed sell = OrderIntent(symbol="BTC/USD", side="sell", qty=0.05) assert gw.evaluate(sell, p, m).allowed def test_unreconciled_ledger_blocks(): i, p, m = base() p.reconciled = False assert not RiskGateway().evaluate(i, p, m).allowed @settings(max_examples=200, deadline=None) @given(qty=st.floats(min_value=0, max_value=10) | st.just(float('nan')) | st.just(float('inf')), cash=st.floats(0, 1e6), price=st.floats(1, 1e6), age=st.floats(0, 1000)) def test_allowed_implies_all_checks_pass(qty, cash, price, age): i = OrderIntent(symbol="X", side="buy", qty=qty) p = PortfolioSnapshot(equity=cash, cash=cash) m = MarketState(last_price=price, data_age_seconds=age) d = RiskGateway().evaluate(i, p, m) if d.allowed: assert all(d.checks.values()) and not d.reasons else: assert d.reasons