Files
sentiment-engine/MALKHUT/malkhut/tests/test_extended.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

276 lines
12 KiB
Python

"""
Tests for extended counterparties and structured observability.
"""
import pytest
from malkhut.state import (
AccountState, MarketWorldState, Mode, OrderBookState, PriceLevel,
PositionState, Side, TradePathState, VenueRules,
)
from malkhut.actions import ActionKind, AgentRole
from malkhut.counterparties_extended import (
MomentumTakerPolicy, MeanReversionTakerPolicy,
InventoryMarketMakerPolicy, LiquidationFlowPolicy,
StaleQuoteAttackerPolicy, extended_counterparty_ecology,
)
from malkhut.training.structured_obs import StructuredObservability, DecisionMetrics
def _venue():
return 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)
def _state(**kw):
tp = kw.get("trade_path")
pos = kw.get("position")
positions = {"BTCUSDT": pos} if pos else {}
return MarketWorldState(
ts_ns=kw.get("ts", 1_000_000_000), mode=Mode.REPLAY_NO_IMPACT, venue=_venue(),
book=OrderBookState(ts_ns=1, symbol="BTCUSDT",
bids=(PriceLevel(kw.get("bid", 50000.0), 1.0),),
asks=(PriceLevel(kw.get("ask", 50001.0), 1.0),)),
account=AccountState(ts_ns=1, equity=10000.0, wallet_balance=10000.0,
available_balance=10000.0, margin_used=0.0,
total_notional=0.0, positions=positions),
trade_path=tp, open_orders=kw.get("open_orders", ()),
)
def _tp(**kw):
d = dict(symbol="BTCUSDT", side=Side.BUY, entry_ts_ns=0, now_ts_ns=100_000_000,
bars_held=5, seconds_held=50.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=20.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=10.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)
# ══════════════════════════════════════════════════════════════════════════════
# EXTENDED COUNTERPARTIES (20 tests)
# ══════════════════════════════════════════════════════════════════════════════
class TestExtendedCounterparties:
def test_momentum_taker_buy_on_upward(self):
p = MomentumTakerPolicy()
tp = _tp(pnl_bps=50.0)
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
assert a.side == Side.BUY
def test_momentum_taker_sell_on_downward(self):
p = MomentumTakerPolicy()
tp = _tp(pnl_bps=-50.0)
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
assert a.side == Side.SELL
def test_momentum_taker_noop_when_flat(self):
p = MomentumTakerPolicy()
tp = _tp(pnl_bps=0.0)
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.NOOP
def test_mean_reversion_buy_on_drop(self):
p = MeanReversionTakerPolicy()
tp = _tp(pnl_bps=-60.0) # abs(60) > 0.5*100 = 50
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
assert a.side == Side.BUY
def test_mean_reversion_sell_on_rise(self):
p = MeanReversionTakerPolicy()
tp = _tp(pnl_bps=60.0) # abs(60) > 0.5*100 = 50
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
assert a.side == Side.SELL
def test_inventory_mm_reduces_high_inventory(self):
p = InventoryMarketMakerPolicy(max_inventory=0.05)
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(position=pos)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.PLACE
assert a.side == Side.SELL
def test_inventory_mm_noop_when_balanced(self):
p = InventoryMarketMakerPolicy(max_inventory=0.2)
s = _state()
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.NOOP
def test_liquidation_flow_triggers_on_deep_loss(self):
p = LiquidationFlowPolicy(trigger_bps=50.0)
tp = _tp(mae_bps=-60.0)
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
assert a.side == Side.SELL
assert a.toxicity == 0.9
def test_liquidation_flow_noop_when_no_loss(self):
p = LiquidationFlowPolicy(trigger_bps=50.0)
tp = _tp(mae_bps=-10.0)
s = _state(trade_path=tp)
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.NOOP
def test_stale_quote_attacker_attacks(self):
p = StaleQuoteAttackerPolicy()
from malkhut.state import OpenOrderState
oo = OpenOrderState(client_order_id="c1", venue_order_id="v1", symbol="BTCUSDT",
side=Side.BUY, order_type=OrderType.LIMIT, price=50000.0,
qty=0.001, remaining_qty=0.001, queue_ahead_estimate=0.001,
created_ts_ns=1, last_update_ts_ns=1)
s = _state(open_orders=(oo,))
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.CROSS_SPREAD
def test_stale_quote_attacker_noop_when_no_orders(self):
p = StaleQuoteAttackerPolicy()
s = _state()
rng = random.Random(42)
a = p.rollout_action(s, rng)
assert a.kind == ActionKind.NOOP
def test_extended_ecology_has_9_agents(self):
eco = extended_counterparty_ecology()
assert len(eco) == 9
def test_extended_ecology_unique_roles(self):
eco = extended_counterparty_ecology()
roles = [p.role for p in eco]
assert len(set(roles)) == 9
def test_all_agents_produce_valid_actions(self):
eco = extended_counterparty_ecology()
s = _state()
rng = random.Random(42)
for agent in eco:
a = agent.rollout_action(s, rng)
assert a.kind is not None
# ══════════════════════════════════════════════════════════════════════════════
# STRUCTURED OBSERVABILITY (15 tests)
# ══════════════════════════════════════════════════════════════════════════════
class TestStructuredObservability:
def test_record_decision(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
so.record_decision(s, planned, decision, plan_ns=1000, regime="normal")
assert so.total_decisions == 1
def test_feature_importance(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
for _ in range(10):
so.record_decision(s, planned, decision, plan_ns=1000, regime="normal")
importance = so.get_feature_importance(top_n=5)
assert len(importance) > 0
def test_regime_approval_rate(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
# Approved
decision = RiskDecision(approved=True, action=a, reason="ok")
so.record_decision(s, planned, decision, plan_ns=1000, regime="normal")
# Rejected
decision2 = RiskDecision(approved=False, action=a, reason="kill")
so.record_decision(s, planned, decision2, plan_ns=1000, regime="normal")
rate = so.get_regime_approval_rate("normal")
assert rate == 0.5
def test_avg_latency(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
so.record_decision(s, planned, decision, plan_ns=1000)
so.record_decision(s, planned, decision, plan_ns=2000)
assert so.avg_latency_ns == 1500.0
def test_avg_entropy(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
so.record_decision(s, planned, decision, plan_ns=1000)
so.record_decision(s, planned, decision, plan_ns=1000)
assert so.avg_entropy == 0.5
def test_total_decisions(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
for _ in range(20):
so.record_decision(s, planned, decision, plan_ns=1000)
assert so.total_decisions == 20
def test_feature_importance_sorted(self):
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
so = StructuredObservability()
s = _state()
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
planned = PlannedPolicy(actions=(a,), probabilities=(1.0,),
selected_action=a, diagnostics={"entropy": 0.5, "sims": 10})
decision = RiskDecision(approved=True, action=a, reason="ok")
for _ in range(10):
so.record_decision(s, planned, decision, plan_ns=1000)
importance = so.get_feature_importance(top_n=5)
for i in range(len(importance) - 1):
assert importance[i][1] >= importance[i+1][1]
import random
from malkhut.actions import OrderType