Files
sentiment-engine/MALKHUT/malkhut/tests/test_path_risk.py
Codex 4c239f7774 malkhut(tests): 1140 test functions across 46 test files
CWM (103): core mechanics, exhaustive edge cases, numba, exchange mechanics
Replay (118): exhaustive verification, microstructure, trajectory
Training (190): asset classification, phase0 extensive, pipeline, exhaustive
DSL (102): v2 syntax, expanded, new features
ASEx (33): validate-before-mutate, single-writer
Planner (48): MCTS, alternatives, hooks
Counterparties (19): 9 adversarial agent policies
Clock (30): event-driven reactor
BingX (28): venue adapter
IPC (8): Zinc SHM
Storage (9): ClickHouse
Risk (4): hard invariants
State (17): frozen dataclass invariants
Integration: E2E, concurrency, sync/async seams, hypothesis, fuzz, adversarial
2026-07-11 10:46:12 +02:00

209 lines
8.8 KiB
Python

"""
Path risk / trade path state — SL/TP path-aware logic.
Tests that the CWM reward and risk functions correctly respond to
various trade path states (MAE, MFE, recovery velocity, failed recovery).
"""
import math
import pytest
from malkhut.state import (
AccountState, FulfilmentPolicyParams, IntentKind, MarketWorldState,
Mode, OrderBookState, PriceLevel, Side, TradePathState, VenueRules,
)
from malkhut.cwm.core import MinimalCryptoLOBCWM
def _params(**kw):
d = dict(
version="test", ucb_c=1.414, max_sims=64, max_depth=2,
rollout_depth=2, root_temperature=0.5, min_root_entropy=0.25,
quote_offsets_ticks=(0, 1), quote_size_fractions=(0.25,),
passive_ttl_ms=200, aggressive_ttl_ms=50,
maker_edge_min_bps=0.5, cross_spread_edge_min_bps=5.0,
adverse_toxicity_cancel_threshold=0.5, queue_churn_cancel_threshold=0.5,
mae_tail_cut_bps=50.0, mfe_giveback_cut_fraction=0.5,
max_time_in_loss_s=300.0, failed_recovery_cut_count=3,
recovery_velocity_min_bps_per_s=0.0,
max_symbol_notional_fraction=0.20, max_single_order_notional_fraction=0.05,
reduce_when_global_up_fraction=0.30, session_profit_lock_fraction=0.02,
w_expected_pnl=1.0, w_fill_probability=0.5, w_adverse_selection=2.0,
w_queue_priority=0.5, w_inventory_risk=1.5, w_tail_loss=5.0,
w_fee_quality=0.5, w_time_decay=0.3, w_policy_entropy=0.5,
robust_tail_weight=2.0, toxic_counterparty_weight=3.0,
low_liquidity_weight=2.0, latency_stress_weight=1.0,
)
d.update(kw)
return FulfilmentPolicyParams(**d)
def _path(**kw):
d = dict(
symbol="BTCUSDT", side=Side.BUY, entry_ts_ns=0, now_ts_ns=100_000_000,
bars_held=10, seconds_held=100.0, pnl_bps=0.0, mae_bps=-10.0,
mfe_bps=15.0, distance_from_mfe_bps=15.0, distance_from_entry_bps=0.0,
time_to_mfe_s=30.0, time_in_loss_s=50.0, time_in_profit_s=50.0,
time_since_last_profit_s=10.0, time_since_deep_mae_s=20.0,
loss_to_profit_transitions=1, deep_loss_recoveries=0,
failed_recovery_count=0, recovery_velocity_bps_per_s=1.0,
adverse_velocity_bps_per_s=-0.5,
dolphin_regime_score=0.5, jericho_signal_strength=0.3,
volatility_bps=15.0, orderflow_toxicity=0.3,
queue_churn_score=0.2, book_imbalance=0.1, cross_venue_lead_score=0.1,
)
d.update(kw)
return TradePathState(**d)
def _state(**kw):
book = OrderBookState(
ts_ns=1_000_000_000, symbol="BTCUSDT",
bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),),
)
return MarketWorldState(
ts_ns=1_000_000_000, mode=Mode.REPLAY_NO_IMPACT,
venue=VenueRules(
exchange="bingx", symbol="BTCUSDT", tick_size=0.1, lot_size=0.001,
min_qty=0.001, min_notional=5.0, maker_fee_bps=-0.2, taker_fee_bps=0.5,
post_only_supported=True, reduce_only_supported=True,
max_orders_per_second=100, max_cancels_per_minute=120,
),
book=book,
account=kw.get("account", AccountState(
ts_ns=1_000_000_000, equity=10000.0, wallet_balance=10000.0,
available_balance=10000.0, margin_used=0.0, total_notional=0.0,
)),
trade_path=kw.get("trade_path"),
)
class TestPathRiskExitDecision:
def test_no_exit_when_no_path(self):
from malkhut.planner.action_menu import _path_risk_says_exit
s = _state()
assert not _path_risk_says_exit(s, _params())
def test_exit_when_mae_exceeds_threshold(self):
from malkhut.planner.action_menu import _path_risk_says_exit
path = _path(mae_bps=-60.0, recovery_velocity_bps_per_s=-2.0)
s = _state(trade_path=path)
assert _path_risk_says_exit(s, _params(mae_tail_cut_bps=50.0))
def test_no_exit_when_mae_below_threshold(self):
from malkhut.planner.action_menu import _path_risk_says_exit
# mae=30 < threshold=50, time_in_loss=50 < 300, failed_recovery=0 < 3
# mfe=15, distance=3 => giveback=3/15=0.2 < 0.5
path = _path(mae_bps=-30.0, distance_from_mfe_bps=3.0)
s = _state(trade_path=path)
assert not _path_risk_says_exit(s, _params(mae_tail_cut_bps=50.0))
def test_exit_when_time_in_loss_exceeds(self):
from malkhut.planner.action_menu import _path_risk_says_exit
path = _path(time_in_loss_s=400.0)
s = _state(trade_path=path)
assert _path_risk_says_exit(s, _params(max_time_in_loss_s=300.0))
def test_exit_when_failed_recovery_count_exceeds(self):
from malkhut.planner.action_menu import _path_risk_says_exit
path = _path(failed_recovery_count=5)
s = _state(trade_path=path)
assert _path_risk_says_exit(s, _params(failed_recovery_cut_count=3))
def test_exit_on_mfe_giveback(self):
from malkhut.planner.action_menu import _path_risk_says_exit
# mfe=10, distance_from_mfe=8 => giveback = 8/10 = 0.8 > 0.5
path = _path(mfe_bps=10.0, distance_from_mfe_bps=8.0)
s = _state(trade_path=path)
assert _path_risk_says_exit(s, _params(mfe_giveback_cut_fraction=0.5))
def test_no_exit_when_mfe_giveback_below_threshold(self):
from malkhut.planner.action_menu import _path_risk_says_exit
# mfe=10, distance_from_mfe=3 => giveback = 3/10 = 0.3 < 0.5
path = _path(mfe_bps=10.0, distance_from_mfe_bps=3.0)
s = _state(trade_path=path)
assert not _path_risk_says_exit(s, _params(mfe_giveback_cut_fraction=0.5))
def test_exit_when_both_mae_and_slow_recovery(self):
from malkhut.planner.action_menu import _path_risk_says_exit
path = _path(mae_bps=-60.0, recovery_velocity_bps_per_s=-3.0)
s = _state(trade_path=path)
assert _path_risk_says_exit(s, _params(mae_tail_cut_bps=50.0, recovery_velocity_min_bps_per_s=0.0))
def test_no_exit_when_mae_high_but_recovery_fast(self):
from malkhut.planner.action_menu import _path_risk_says_exit
# mae=60 > threshold=50, but recovery_velocity=5.0 > min=0.0 → no MAE exit
# time_in_loss=50 < 300, failed_recovery=0 < 3
# mfe=15, distance=3 => giveback=0.2 < 0.5
path = _path(mae_bps=-60.0, recovery_velocity_bps_per_s=5.0,
distance_from_mfe_bps=3.0)
s = _state(trade_path=path)
assert not _path_risk_says_exit(s, _params(mae_tail_cut_bps=50.0, recovery_velocity_min_bps_per_s=0.0))
class TestPathRiskProxy:
def test_zero_when_no_path(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
assert cwm._tail_risk_proxy(s) == 0.0
def test_increases_with_mae(self):
cwm = MinimalCryptoLOBCWM()
p1 = _path(mae_bps=-10.0)
p2 = _path(mae_bps=-50.0)
r1 = cwm._tail_risk_proxy(_state(trade_path=p1))
r2 = cwm._tail_risk_proxy(_state(trade_path=p2))
assert r2 > r1
def test_increases_with_time_in_loss(self):
cwm = MinimalCryptoLOBCWM()
p1 = _path(time_in_loss_s=10.0)
p2 = _path(time_in_loss_s=100.0)
r1 = cwm._tail_risk_proxy(_state(trade_path=p1))
r2 = cwm._tail_risk_proxy(_state(trade_path=p2))
assert r2 > r1
def test_increases_with_failed_recovery(self):
cwm = MinimalCryptoLOBCWM()
p1 = _path(failed_recovery_count=0)
p2 = _path(failed_recovery_count=3)
r1 = cwm._tail_risk_proxy(_state(trade_path=p1))
r2 = cwm._tail_risk_proxy(_state(trade_path=p2))
assert r2 > r1
class TestInventoryRisk:
def test_zero_when_no_position(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
assert cwm._inventory_risk(s) == 0.0
def test_positive_when_position_exists(self):
from malkhut.state import PositionState, AccountState
cwm = MinimalCryptoLOBCWM()
pos = PositionState(
symbol="BTCUSDT", qty=0.1, avg_entry=50000.0,
unrealized_pnl=0.0, realized_pnl=0.0,
liquidation_price=None, leverage=0.5, side=Side.BUY,
)
s = _state(account=AccountState(
ts_ns=1_000_000_000, equity=10000.0, wallet_balance=10000.0,
available_balance=10000.0, margin_used=0.0, total_notional=5000.0,
positions={"BTCUSDT": pos},
))
risk = cwm._inventory_risk(s)
assert risk > 0
class TestRewardPathSensitivity:
def test_higher_w_tail_loss_more_penalty_for_deep_mae(self):
cwm = MinimalCryptoLOBCWM()
path = _path(mae_bps=-40.0, failed_recovery_count=2)
s = _state(trade_path=path)
from malkhut.actions import FulfilmentAction, ActionKind
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
r = cwm.transition(s, (a,))
p1 = _params(w_tail_loss=1.0)
p2 = _params(w_tail_loss=10.0)
# reward = 0 - tail_risk * w_tail_loss - ...
# Higher w_tail_loss should produce more negative reward
assert cwm.reward(s, a, r, p2) < cwm.reward(s, a, r, p1)