malkhut(wire): fill quality as PRIMARY optimization target

Fill quality is MALKHUT's core aim. Wired end-to-end:

1. FillQuality state (state.py):
   - slippage_bps, price_improvement_bps, levels_consumed
   - is_maker_fill, rolling_fill_rate, post_fill_adverse_bps
   - fill_value_score: composite metric for optimization
   - Added to MarketWorldState.fill_quality field

2. HftBacktestCWM.transition() (hft_cwm.py):
   - _compute_fill_quality() computes all metrics per transition
   - Fill quality now tracked for every CWM step
   - Empty book guards added for safety

3. MinimalCryptoLOBCWM.transition() (core.py):
   - Same fill quality computation for deterministic fallback
   - Empty book guards added

4. Reward function (hft_cwm.py):
   - fill_quality_reward = w_fill_probability * fill_value_score (PRIMARY)
   - Bonus for maker fills that improve price
   - Penalty for adverse selection after fill
   - Base reward (PnL, adverse selection, fees) preserved

5. PerformanceMatrix (selector.py):
   - RegimeStrategyScore: 4 new fill quality fields
   - record(): accepts fill_rate, slippage, price_improvement, fill_value_score
   - EMA updates for all fill quality metrics

6. EpisodeResult (cma_trainer.py):
   - avg_fill_value_score, avg_price_improvement_bps, avg_post_fill_adverse_bps
   - Accumulated per-step during _run_episode
   - Recorded to PerformanceMatrix in evaluate_candidate

All 1379+ tests green.
This commit is contained in:
Codex
2026-07-15 15:22:25 +02:00
parent fa76070c79
commit 618ad723e3
5 changed files with 284 additions and 29 deletions

View File

@@ -23,6 +23,7 @@ import numpy as np
from malkhut.state import (
AccountState,
FulfilmentPolicyParams,
FillQuality,
MarketWorldState,
OpenOrderState,
OrderBookState,
@@ -429,6 +430,17 @@ class HftBacktestCWM:
positions=new_positions,
)
# ── Fill Quality computation (CORE metric) ──────────────────────────
fq = self._compute_fill_quality(
prev_state=state,
action=our_action,
new_fill_qty=new_fill_qty,
new_fill_price=new_fill_price,
book=book,
prev_book=state.book,
now_ts=now_ts,
)
return MarketWorldState(
ts_ns=now_ts,
mode=state.mode,
@@ -442,6 +454,95 @@ class HftBacktestCWM:
volatility_state=state.volatility_state,
market_regime=state.market_regime,
feed_latency_ms=state.feed_latency_ms,
fill_quality=fq,
)
def _compute_fill_quality(
self,
prev_state: MarketWorldState,
action: FulfilmentAction,
new_fill_qty: float,
new_fill_price: float,
book: OrderBookState,
prev_book: OrderBookState,
now_ts: int,
) -> FillQuality:
"""Compute fill quality metrics for this transition.
Fill quality is the CORE optimization target of MALKHUT.
Metrics:
- slippage_bps: how far from mid did we fill (aggressive)
- price_improvement_bps: how much better than touch (passive)
- levels_consumed: queue depth of fill
- is_maker_fill: passive vs aggressive
- rolling_fill_rate: recent fill success rate
- post_fill_adverse_bps: price movement after fill
- fill_value_score: composite optimization metric
"""
filled = new_fill_qty > 0
mid = prev_book.mid if prev_book.bids and prev_book.asks else 0.0
spread_bps = prev_book.spread_bps if prev_book.bids and prev_book.asks else 0.0
# Slippage: how far from mid did we fill?
slippage_bps = 0.0
if filled and mid > 0 and new_fill_price > 0:
slippage_bps = abs(new_fill_price - mid) / mid * 10_000
# Price improvement: how much better than best bid/ask?
price_improvement_bps = 0.0
if filled and action.post_only and action.side:
if action.side == Side.BUY and prev_book.bids:
price_improvement_bps = (prev_book.best_bid - new_fill_price) / max(prev_book.best_bid, 1e-12) * 10_000
elif action.side == Side.SELL and prev_book.asks:
price_improvement_bps = (new_fill_price - prev_book.best_ask) / max(prev_book.best_ask, 1e-12) * 10_000
# Is maker fill?
is_maker = (action.order_type and action.order_type.value == "LIMIT") or action.post_only
# Levels consumed (estimate: fill_qty / avg level qty)
levels_consumed = 0
if filled and is_maker:
avg_level_qty = sum(l.qty for l in prev_book.asks if prev_book.asks) / max(len(prev_book.asks), 1) if action.side == Side.BUY else \
sum(l.qty for l in prev_book.bids if prev_book.bids) / max(len(prev_book.bids), 1)
levels_consumed = max(1, int(new_fill_qty / max(avg_level_qty, 1e-12)))
# Post-fill adverse: did price move against us?
post_fill_adverse = 0.0
new_mid = book.mid if book.bids and book.asks else 0.0
if filled and mid > 0 and new_mid > 0:
if action.side == Side.BUY:
post_fill_adverse = (new_mid - mid) / mid * 10_000 # negative = adverse
elif action.side == Side.SELL:
post_fill_adverse = (mid - new_mid) / mid * 10_000 # negative = adverse
# Rolling fill rate (from state history)
prev_fq = prev_state.fill_quality
rolling_fill_rate = 0.0
if prev_fq and prev_fq.filled:
rolling_fill_rate = 0.8 * prev_fq.rolling_fill_rate + 0.2 * (1.0 if filled else 0.0)
elif filled:
rolling_fill_rate = 0.2
else:
rolling_fill_rate = 0.0
# Composite fill value score
fill_value = 0.0
if filled:
quality = price_improvement_bps if is_maker else max(0.0, spread_bps - slippage_bps)
fill_value = quality - abs(post_fill_adverse) * 0.5
return FillQuality(
filled=filled,
fill_qty=new_fill_qty,
fill_price=new_fill_price,
requested_qty=action.qty_fraction * prev_state.account.available_balance / max(mid, 1e-12) if action.qty_fraction > 0 and mid > 0 else 0.0,
slippage_bps=slippage_bps,
price_improvement_bps=price_improvement_bps,
levels_consumed=levels_consumed,
is_maker_fill=is_maker,
rolling_fill_rate=rolling_fill_rate,
post_fill_adverse_bps=post_fill_adverse,
fill_value_score=fill_value,
)
def reward(
@@ -451,7 +552,18 @@ class HftBacktestCWM:
next_state: MarketWorldState,
params: FulfilmentPolicyParams,
) -> float:
"""Same reward function as MinimalCryptoLOBCWM."""
"""Reward function — fill quality is the PRIMARY optimization target.
MALKHUT is an execution improvement engine. Fill quality IS the core aim.
Reward = w_fill_probability * fill_value_score (PRIMARY)
+ w_expected_pnl * pnl (secondary)
- w_adverse_selection * toxicity
- w_inventory_risk * inventory_risk
- w_tail_loss * tail_risk
- w_time_decay * time_in_loss
+ w_fee_quality * maker_fee_benefit
- spread_cost - taker_fee
"""
try:
from malkhut.cwm.numba_core import compute_reward_vectorized
@@ -469,7 +581,7 @@ class HftBacktestCWM:
is_cross = action.kind.value == "CROSS_SPREAD"
is_cancel = action.kind.value in ("CANCEL", "CANCEL_REPLACE")
return compute_reward_vectorized(
base_reward = compute_reward_vectorized(
pnl, toxicity, churn, time_in_loss, spread_bps,
inv_risk, tail_risk,
params.w_expected_pnl, params.w_adverse_selection,
@@ -481,36 +593,49 @@ class HftBacktestCWM:
params.w_queue_priority, params.w_adverse_selection,
)
except ImportError:
pass
# Fallback: Python path
fv = self.feature_extractor.extract(next_state).values
pnl = fv.get("pnl_bps", 0.0)
toxicity = fv.get("orderflow_toxicity", 0.0)
churn = fv.get("queue_churn_score", 0.0)
time_in_loss = fv.get("time_in_loss_s", 0.0)
spread_bps = fv.get("spread_bps", 0.0)
# Fallback: Python path
fv = self.feature_extractor.extract(next_state).values
pnl = fv.get("pnl_bps", 0.0)
toxicity = fv.get("orderflow_toxicity", 0.0)
churn = fv.get("queue_churn_score", 0.0)
time_in_loss = fv.get("time_in_loss_s", 0.0)
spread_bps = fv.get("spread_bps", 0.0)
base_reward = 0.0
base_reward += params.w_expected_pnl * pnl
base_reward -= params.w_adverse_selection * toxicity
base_reward -= params.w_inventory_risk * self._inventory_risk(next_state)
base_reward -= params.w_tail_loss * self._tail_risk_proxy(next_state)
base_reward -= params.w_time_decay * math.log1p(max(time_in_loss, 0.0))
reward = 0.0
reward += params.w_expected_pnl * pnl
reward -= params.w_adverse_selection * toxicity
reward -= params.w_inventory_risk * self._inventory_risk(next_state)
reward -= params.w_tail_loss * self._tail_risk_proxy(next_state)
reward -= params.w_time_decay * math.log1p(max(time_in_loss, 0.0))
if (action.order_type and action.order_type.value == "LIMIT") or action.post_only:
base_reward += params.w_fee_quality * max(0.0, -prev_state.venue.maker_fee_bps)
if (action.order_type and action.order_type.value == "LIMIT") or action.post_only:
reward += params.w_fee_quality * max(0.0, -prev_state.venue.maker_fee_bps)
if action.kind.value == "CROSS_SPREAD":
base_reward -= spread_bps + max(prev_state.venue.taker_fee_bps, 0.0)
if action.kind.value == "CROSS_SPREAD":
reward -= spread_bps + max(prev_state.venue.taker_fee_bps, 0.0)
if action.kind.value in ("CANCEL", "CANCEL_REPLACE"):
if toxicity > params.adverse_toxicity_cancel_threshold:
base_reward += params.w_adverse_selection * toxicity
if churn > params.queue_churn_cancel_threshold:
base_reward += params.w_queue_priority * churn
if action.kind.value in ("CANCEL", "CANCEL_REPLACE"):
if toxicity > params.adverse_toxicity_cancel_threshold:
reward += params.w_adverse_selection * toxicity
if churn > params.queue_churn_cancel_threshold:
reward += params.w_queue_priority * churn
# ── FILL QUALITY: the CORE reward signal ──────────────────────────
fq = next_state.fill_quality
fill_quality_reward = 0.0
if fq:
# Primary: fill value score (price quality + fill success)
fill_quality_reward += params.w_fill_probability * fq.fill_value_score
return reward
# Bonus for maker fills that improve price
if fq.is_maker_fill and fq.price_improvement_bps > 0:
fill_quality_reward += params.w_fill_probability * fq.price_improvement_bps * 0.5
# Penalty for adverse selection after fill
if fq.filled and fq.post_fill_adverse_bps < 0:
fill_quality_reward += params.w_adverse_selection * fq.post_fill_adverse_bps
return base_reward + fill_quality_reward
def terminal(self, state: MarketWorldState, depth: int) -> bool:
return depth <= 0