# No-Transaction-Band Policy — Architectural Design Spec

**Date:** 2026-06-03
**Status:** Design — pre-implementation, awaiting Phase 3D cluster verdict
**Branch target:** `ml-alpha-no-transaction-band` (off Phase 3D `bd811a774`)
**Estimated effort:** 1.5-2.5 weeks focused work + 2-3 cluster verification cycles
**Linked specs:**
  * `2026-06-02-multi-head-policy-with-r-multiple.md` (Phase 2A — must remain compatible)
  * `2026-06-02-trainer-rollout-buffer-gae.md` (Phase 1B — orthogonal, both can ship)
**Linked pearls:**
  * `pearl_foxhunt_pi_trained_by_q_distillation_not_ppo` — the architectural pathology this spec addresses
  * `pearl_reward_signal_anti_aligned_with_pnl` — why proxy-reward fixes alone are insufficient
  * `pearl_fleet_fraction_not_aggregate` — diag emission discipline
  * `pearl_bootstrap_must_respect_clamp_range` — ISV bootstrap discipline
  * `pearl_scoped_init_seed_for_reproducibility` — init seed discipline (band-head weights)

---

## §0. Mechanism overview

### §0.1 Core idea

Replace "policy learns *whether* to trade" with "policy learns *the boundaries of a no-transaction region*". When the current position lies inside the learned region, the *architectural default* is "do nothing" — bypassing all action-selection machinery. Trading is only allowed when position is *outside* the region.

Foxhunt's discrete action grid (`crates/ml-alpha/src/rl/common.rs:56-70`, 11 actions) is preserved. A new band-head outputs two scalars per batch element — `b_l`, `b_u` ∈ ℝ (position-space bounds) — and an argmax-mask layer (analogous to `rl_confidence_gate.cu`) forces `actions[b] = Hold` whenever `position_lots[b] ∈ [b_l, b_u]`.

### §0.2 Theoretical foundation

* **Davis & Norman (1990)** — "Portfolio Selection with Transaction Costs" (Math. Op. Res. 15:676-713): proved that for an investor maximising long-run utility under proportional transaction costs, the *optimal policy class* is a no-transaction region in position space. Inside the region, hold; outside, trade to the nearest boundary.
* **Davis, Pan, Rynik** — extended to multi-asset under proportional + fixed cost mixtures: the optimal region is convex and learned from the cost structure.
* **Whalley & Wilmott (1997)** — "An asymptotic analysis of an optimal hedging model" (Math. Finance 7:307-324): for option hedging, the band width scales as `(transaction_cost)^(1/3) × (Γ × σ²)^(2/3)`. Same structure for trading.
* **Imaki, Imajo, Ito et al. 2021** (arXiv:2103.01775) — "No-Transaction Band Network: A Neural Network Architecture for Efficient Deep Hedging": adapted the band class to deep RL. The architecture parameterises `(b_l, b_u)` as a neural-network output, structurally guaranteeing the no-trade default. Achieved ~3× lower turnover at iso-utility vs. unconstrained deep hedging on the same option-hedging benchmark.
* **Cao 2026** (arXiv:2603.29086) — quadratic impact-aware cost in RL HFT: 96% turnover reduction by structural cost rather than proxy-reward fixes. Phase 3D-B already implements this; this spec adds the *complementary structural* piece.
* **Goodhart-Skalse 2024** — "On the limits of proxy-reward shaping in deep RL" (NeurIPS): proxy-reward fixes attenuate through any learned-approximator chain. Foxhunt has FOUR such chains (reward → V → Q-Bellman target → softmax(Q/τ) → π distill, per `pearl_foxhunt_pi_trained_by_q_distillation_not_ppo`). The no-trade-by-default architecture sidesteps this entirely.

### §0.3 Why this solves foxhunt's specific failure modes

| Failure mode | Source pearl | Why band fixes it |
|---|---|---|
| Q-distill cannot transmit fee signal | `pearl_foxhunt_pi_trained_by_q_distillation_not_ppo` | No-trade is now the architectural default. Q-distill can still drive *which* action when band is exited, but cannot drive *how often* π trades. |
| Train wr=0.56 → eval wr=0.219 (2.5× collapse) | `pearl_surfer_baseline_was_train_only_never_eval` | Band-width regularizer (turnover target, §3) is regime-independent: training regime-specific trade frequencies do not bake into weights. |
| Pearson(rewards, Δpnl) = 0.11-0.45 (anti-aligned) | `pearl_reward_signal_anti_aligned_with_pnl` | Band loss does *not* use reward as direct signal — it uses *realized turnover vs target*. Decoupled from shaped reward sign. |
| Phase 3D Hold-bias + quadratic cost = 20% reduction (not 96%) | Phase 3D cluster diag | Hold-bias is a soft prior; quadratic cost is a soft penalty. Band is a *hard architectural constraint*. The three layers compose: bias seeds, cost fines, band forbids. |

---

## §1. Architecture changes

### §1.1 New band-head module

A new struct `BandHead` parallel to `PolicyHead` (`crates/ml-alpha/src/rl/ppo.rs:87`) and `ValueHead` (line 544). Lives in `crates/ml-alpha/src/rl/band_head.rs`.

**Layout:**

```text
W_band ∈ ℝ^[2 × HIDDEN_DIM]        # two output neurons: b_l, b_u
b_band ∈ ℝ^[2]                      # biases
forward:
    z[b, 0] = b_band[0] + Σ_c W_band[0, c] · h_t[b, c]   # raw b_l
    z[b, 1] = b_band[1] + Σ_c W_band[1, c] · h_t[b, c]   # raw b_u
    b_l_norm[b] = -|tanh(z[b, 0])| × N_max_eff           # [-N_max_eff, 0]
    b_u_norm[b] = +|tanh(z[b, 1])| × N_max_eff           # [0, +N_max_eff]
```

Where `N_max_eff` = current effective position-cap, read from `RL_HEAT_CAP_MAX_LOTS_INDEX` (slot, bootstrap 8). The activation enforces the invariant `b_l ≤ 0 ≤ b_u` — a position of 0 (flat) is *always* inside the band. Both heads share the same architectural shape; the `|tanh| × N_max` form keeps `b_l ∈ [-N_max, 0]` and `b_u ∈ [0, +N_max]` by construction.

**Why not unbounded:** the band must lie inside the feasible position range. Unbounded output is meaningless above |N_max|. Sigmoid would force the band to be centered at N_max/2 — wrong since position 0 is the natural pivot. The asymmetric `±|tanh|` is the cleanest activation that respects both constraints.

**Why not "wider"** (e.g., direct `b_l`, `b_u` outputs that may not bracket 0): would let the agent learn a band like `[3, 5]` which forbids being flat. Davis-Norman's optimality theorem assumes the no-trade region brackets the no-position state. Forcing 0 ∈ band by construction is the right architectural prior.

### §1.2 Multi-head variant

For `MultiHeadPolicy` (`crates/ml-alpha/src/rl/multi_head_policy.rs`), the band-head is added as a **single shared band** (NOT per-head). Rationale:

* Davis-Norman optimality is a *position-space* property. Different policy *modes* (trend vs reversion, per pearl_two_alpha_modes_surfer_vs_trend) can share the same no-trade region — they differ in *which* direction to trade when outside the band, not in *whether* to trade.
* Per-head bands would 3× the band-head parameter count and produce a mixture-of-bands semantics that has no Davis-Norman analogue.
* Implementation is simpler: one band-head feeding all K mixture variants.

If Phase 4-B cluster verification shows mode-dependent band optima (e.g., trend mode wants narrow band, reversion wants wide), Phase 4-D can split to per-head bands.

### §1.3 Initialization

Per `pearl_scoped_init_seed_for_reproducibility`, init draws under `scoped_init_seed` guard.

**Weight init:** Xavier-uniform scaled by 0.01 (same as PolicyHead — keeps initial logits near zero). Drawn from `ChaCha8Rng` seeded by `cfg.seed.wrapping_add(0xBA_ND_5EED)`.

**Bias init — start with a moderate-width band:**

```text
b_band_init[0] = -atanh(0.5)  ≈ -0.549   →  b_l_init = -|tanh(-0.549)| × N_max = -0.5 × N_max
b_band_init[1] = +atanh(0.5)  ≈ +0.549   →  b_u_init = +0.5 × N_max
```

At `N_max = 8`, initial band = `[-4, +4]`. Justification:
1. Conservative start — covers most likely position states at init (the agent will mostly be |pos| ≤ 4 during early training).
2. Phase 3D's hold-bias already raises Hold probability to ~29% at init; this complementary band-default raises it further by forcing Hold whenever flat or small-positioned.
3. Allows clear *narrowing* signal during training (if band-loss says "trade more", b_l/b_u both shrink toward 0). Wide-band init would be hard to escape.
4. `pearl_bootstrap_must_respect_clamp_range`: 0.5 ∈ [0, 1] satisfies the natural `|tanh|` clamp range.

### §1.4 Position normalization

Foxhunt's `position_lots` is `i32` ∈ `{-N_max, ..., 0, ..., +N_max}` lots (read from `PosFlat.position_lots`, offset 0 of `pos_state_d`, see `crates/ml-backtesting/src/lob/mod.rs:71-78`). The band masker (§2.2) compares the `int32` lots directly against the `f32` band outputs:

```c
float b_l_f = band_outputs[b * 2 + 0];   // already in lots units
float b_u_f = band_outputs[b * 2 + 1];
bool in_band = ((float)position_lots) >= b_l_f && ((float)position_lots) <= b_u_f;
```

No further normalization needed. `RL_HEAT_CAP_MAX_LOTS_INDEX` already establishes `N_max` as the natural unit; band outputs live in the same scale.

---

## §2. Action selection logic

### §2.1 Insertion point

Action selection in `step_with_lobsim_gpu_body` runs (see `crates/ml-alpha/src/trainer/integrated.rs:7410-7445`):

1. **`rl_pi_action_kernel`** — multinomial sample from softmax(pi_logits) → `actions_d`
2. **`argmax_expected_q`** — Double-DQN argmax over Q at h_{t+1} → `next_actions_d`
3. *(outside graph capture, lines 7487-7530)*
4. **`rl_confidence_gate`** — overrides opening actions to Hold when Q-LCB < threshold (current masking site)
5. **`rl_frd_gate`** — analogous FRD-based override
6. **`action_entropy_per_step`** — observability

The band masker is inserted as **Step 3.5** (between rl_pi_action_kernel and rl_confidence_gate), in the same "outside graph capture" block — bands change every step so they cannot be captured.

### §2.2 Band masker kernel — `rl_band_mask.cu`

**Pseudocode:**

```c
extern "C" __global__ void rl_band_mask(
    int*                 __restrict__ actions,        // [B] IN/OUT
    const float*         __restrict__ band_outputs,   // [B × 2]  (b_l, b_u)
    const unsigned char* __restrict__ pos_state,      // [B × pos_bytes]
    float*               __restrict__ isv,
    int                                b_size,
    int                                pos_bytes
) {
    const int b = blockIdx.x;
    if (b >= b_size) return;

    // Read warmup gate — bands cannot mask during warmup or training will
    // never see opening actions and Q will never learn to evaluate them.
    const int current_step = (int)isv[RL_STEP_COUNTER_ISV_INDEX];
    const int warmup       = (int)isv[RL_BAND_WARMUP_STEPS_INDEX];
    if (current_step < warmup) return;

    // Exploration slots (same pattern as rl_confidence_gate.cu:62-64):
    // first `min_explore` batch elements are NEVER masked, breaking the
    // self-reinforcing "band-forces-Hold → Q sees only Hold → Q learns
    // Hold→V_band stays narrow" trap.
    const float max_mask = isv[RL_BAND_MAX_MASK_FRAC_INDEX];
    const int min_explore = (int)((float)b_size * (1.0f - max_mask));
    if (b < min_explore) return;

    // Read current position (lots, signed). Layout: pos_state[0..4] = i32.
    const int position_lots =
        *reinterpret_cast<const int*>(pos_state + b * pos_bytes);

    const float b_l = band_outputs[b * 2 + 0];
    const float b_u = band_outputs[b * 2 + 1];

    // If position ∈ [b_l, b_u], force Hold (action index 2).
    const float pos_f = (float)position_lots;
    if (pos_f >= b_l && pos_f <= b_u) {
        actions[b] = ACTION_HOLD;
        // Diag: b==0 writes mask-count, fleet-fraction goes elsewhere.
        if (b == 0) {
            isv[RL_BAND_MASK_FIRED_STEP_INDEX] = 1.0f;
        }
    }
}
```

One block per batch, single thread per block (matches `rl_confidence_gate` shape). Grid=(b_size, 1, 1), Block=(1, 1, 1). No shared memory.

### §2.3 Interaction with confidence gate

The confidence gate (`rl_confidence_gate.cu`) runs *after* the band mask. Order matters: band mask is the *primary* default (architectural); confidence gate is the *secondary* safety net (Q-distribution-aware). If the band already forced Hold, the confidence gate is a no-op (line 67: `if (action == ACTION_HOLD) return;`).

**Decision:** keep both. Band handles the structural "should I trade at all" question; confidence gate handles "if I'm trading, is Q confident enough about this specific action". Different abstraction layers, no conflict.

### §2.4 Interaction with Phase 3D hold-bias

The +log(4) Hold-action logit bias (PolicyHead::new line 208; MultiHeadPolicy::new lines 314-330) raises Hold mass at the *softmax* level, BEFORE multinomial sampling. The band mask runs AFTER sampling.

**Decision:** keep hold-bias. Rationale:
* Hold-bias improves Q-distill targets (Q sees more Hold examples → Q learns Hold-value better → softmax(Q/τ) for distillation is better calibrated).
* The two mechanisms compose: hold-bias raises P(Hold sampled) from ~9% to ~29%; band mask additionally forces Hold when position ∈ band. Final P(Hold) = P(sampled Hold) + P(sampled non-Hold ∧ in-band).
* Disabling hold-bias once band is active is a future ablation (Phase 4-E), not part of foundation.

**Gating slot** `RL_BAND_ENABLED_INDEX` defaults to 1.0 (on); operator can set 0.0 to A/B test "band off + hold-bias only" vs "band on".

### §2.5 Eval-time vs train-time

The band mask runs **at both train and eval time** — band is an architectural property of the policy, not a training-time regularizer. At eval, deterministic action (argmax of pi_logits) is used in some configurations; the band mask still applies post-argmax.

**Justification:** the entire point of no-transaction-band is to encode the Davis-Norman optimal structure into the *deployed* policy. Disabling at eval would reintroduce the pathology this spec exists to fix.

The exploration-slot bypass (lines `min_explore`) is a *training-only* concern (preserves on-policy Q learning). At eval, set `RL_BAND_MAX_MASK_FRAC_INDEX = 1.0` so all batches are gated. Trainer flag is read from the existing `is_eval` boolean already plumbed for diag emission; the same boolean writes `1.0` to slot at eval start, restores train value at train resume.

---

## §3. Training mechanism

### §3.1 Three options surveyed

**(a) Counterfactual reward (Davis-Norman flavored).** For each batch element where band masked the action, estimate `Q(s, would-have-traded)` — pick the argmax non-Hold action's Q-value, subtract `Q(s, Hold)`. The signed difference, summed over the batch, is the band-loss gradient. Pushes band narrower when "trading would have been better", wider when "holding was correct".

* Pros: directly grounded in Q-values, leverages existing infrastructure (`q_logits_d`).
* Cons: noisy at training start when Q is uncalibrated. Q-distill chain is what we're trying to *not* depend on per Goodhart-Skalse — using Q to drive band loss reintroduces the chain.

**(b) Direct turnover regularizer.** ISV slot `RL_BAND_TURNOVER_TARGET_INDEX` holds a target trades-per-step (e.g., 0.05 ≈ 1 trade per 20 steps). Measured turnover EMA is computed from per-batch `dones` (already emitted by `rl_fused_reward_pipeline.cu` Phase 1). Band loss:

```text
turnover_ema_t = (1-α) · turnover_ema_{t-1} + α · mean(dones)
loss_band = (turnover_ema_t - turnover_target)²
```

Gradient routed back to `(b_l, b_u)` via the chain `turnover ~ P(out-of-band)` — analytical gradient: widening the band reduces turnover, narrowing increases it. Specifically:

```text
dL/d(b_u) = 2·(turnover - target) · sign(b_u) · (dP_in_band/dz_u) · dz_u/dh_t · ...
```

where `P_in_band` is the fraction of batches with `position ∈ [b_l, b_u]`, estimated empirically per step.

* Pros: Goodhart-Skalse safe (band loss decoupled from reward/Q). CMDP-style — direct constraint satisfaction.
* Cons: requires explicit turnover target (one more ISV slot to tune); does not respect actual reward signal (a regime where 0.1 turnover IS optimal would be punished if target=0.05).

**(c) Hybrid (RECOMMENDED).** Use (b) as the primary band-loss with weight `RL_BAND_LOSS_WEIGHT_INDEX` (ISV-tuned). Use (a) as a *secondary modulator*: the turnover target itself is adapted by a controller (`rl_band_turnover_controller.cu`) that watches:
* Current realized pnl (from cluster's reward stream, post-Phase 0.2 USD-correct path).
* Q-distill softmax entropy (proxy for Q calibration).
* Cumulative dones (so the target only adapts after the agent has accumulated experience — per `pearl_welford_trade_count_is_step_not_trade`).

Adaptation rule (Schulman-bounded, like rl_confidence_gate.cu:152-156):

```text
if pnl_ema > 0 and turnover_ema < turnover_target:    # agent profitable but holding too much
    turnover_target *= 1.1                             # encourage more trading
elif pnl_ema < 0 and turnover_ema > turnover_target:  # agent losing and trading too much
    turnover_target /= 1.1                             # discourage trading
turnover_target = clamp(turnover_target, 0.005, 0.5)
```

* Pros: combines structural safety of (b) with reward-aware modulation of (a). Adaptive controller pattern matches the 12 existing controllers (consistent maintenance burden).
* Cons: one more controller to debug. Risk that pnl_ema and turnover_ema are correlated in pathological ways (TBD via local mid-smoke).

**Recommendation: ship Option (c) with the controller initialized to a fixed turnover target (effectively Option (b)) for Phase 4-A, then enable controller adaptation in Phase 4-B.**

### §3.2 Band loss kernel — `rl_band_loss.cu`

**Pseudocode:**

```c
extern "C" __global__ void rl_band_loss(
    const float* __restrict__ band_outputs,    // [B × 2]
    const unsigned char* __restrict__ pos_state, // [B × pos_bytes]
    const float* __restrict__ dones,           // [B]   (current step dones)
    float* __restrict__ isv,                   // RW (turnover EMA, loss scalar)
    float* __restrict__ band_loss_per_b,       // [B]   per-batch loss
    float* __restrict__ grad_band_outputs,     // [B × 2] per-batch grad
    int b_size,
    int pos_bytes
) {
    const int b = blockIdx.x;
    if (b >= b_size) return;

    const int position_lots =
        *reinterpret_cast<const int*>(pos_state + b * pos_bytes);
    const float pos_f = (float)position_lots;

    const float b_l = band_outputs[b * 2 + 0];
    const float b_u = band_outputs[b * 2 + 1];

    // Indicator: is this batch's position currently in band?
    const float in_band = (pos_f >= b_l && pos_f <= b_u) ? 1.0f : 0.0f;

    // Per-batch contribution to turnover EMA update is computed in
    // a reducer kernel; here we just emit per-batch loss + grad.

    // Read current turnover EMA + target (controller-emitted).
    const float turnover_ema    = isv[RL_BAND_TURNOVER_EMA_INDEX];
    const float turnover_target = isv[RL_BAND_TURNOVER_TARGET_INDEX];
    const float loss_w          = isv[RL_BAND_LOSS_WEIGHT_INDEX];

    // Loss = (turnover_ema - target)² — same value for all batches, but
    // the GRADIENT is per-batch because it routes through this batch's
    // contribution to turnover via P(out-of-band).
    const float diff = turnover_ema - turnover_target;
    band_loss_per_b[b] = loss_w * diff * diff / (float)b_size;

    // Gradient of loss w.r.t. (b_l, b_u) for this batch:
    //   turnover ≈ 1 - mean(in_band)
    //   d(turnover)/d(b_u) = +(1/B) · δ(pos = b_u)  ≈ smoothed indicator
    //   d(turnover)/d(b_l) = -(1/B) · δ(pos = b_l)
    // Use a sigmoid surrogate near the boundary for differentiability.
    const float sharpness = isv[RL_BAND_GRAD_SHARPNESS_INDEX];  // bootstrap 2.0
    const float surrogate_u = 1.0f / (1.0f + expf(-sharpness * (pos_f - b_u)));
    const float surrogate_l = 1.0f / (1.0f + expf(-sharpness * (b_l - pos_f)));

    // 2·diff because L = diff², gradient is 2·diff · d(diff)/d(b_*).
    // diff = turnover - target; turnover = 1 - mean(in_band); so
    // d(diff)/d(b_u) = -(1/B) · (-surrogate_u) = +surrogate_u/B (wider b_u
    // → more positions in band → lower turnover → diff goes down if it was
    // positive). Sign: when diff > 0 (over-trading), widening b_u reduces
    // loss, so grad on b_u should be NEGATIVE. Confirmed by chain rule.
    const float inv_b = 1.0f / (float)b_size;
    grad_band_outputs[b * 2 + 0] = -loss_w * 2.0f * diff * surrogate_l * inv_b;
    grad_band_outputs[b * 2 + 1] = -loss_w * 2.0f * diff * surrogate_u * inv_b;
}
```

The sigmoid surrogate is the standard trick to make a step-function indicator differentiable. `sharpness=2.0` gives a soft boundary (the derivative is non-zero in a ±1-lot window around `b_u`); higher sharpness means more decisive but noisier gradient. ISV-tuned for A/B.

### §3.3 Backward chain into encoder

After `rl_band_loss` produces `grad_band_outputs[B × 2]`, a backward kernel `rl_band_grad_h_t.cu` propagates into:
* `grad_W_band[B × 2 × HIDDEN_DIM]` (per-batch, reduced via existing `reduce_axis0`)
* `grad_b_band[B × 2]` (per-batch, reduced)
* `grad_h_t[B × HIDDEN_DIM]` (OVERWRITE — caller folds into encoder grad via existing `grad_h_accumulate_scaled`)

Chain rule:
```text
b_l = -|tanh(z[0])| · N_max
db_l/dz[0] = -sign(tanh(z[0])) · (1 - tanh²(z[0])) · N_max
           = -(±1) · sech²(z[0]) · N_max
```

Per `pearl_foxhunt_pi_trained_by_q_distillation_not_ppo` discipline: this gradient is INDEPENDENT of the Q-distill / PPO surrogate path. It's a third gradient channel into the encoder, additive at `grad_h_t`. Loss weight controls its dominance.

### §3.4 Loss weight ISV (adaptive)

`RL_BAND_LOSS_WEIGHT_INDEX` bootstrap 0.05. Adaptive controller `rl_band_loss_weight_controller.cu` reads:
* `RL_BAND_TURNOVER_EMA_INDEX` — current measured turnover
* `RL_BAND_TURNOVER_TARGET_INDEX` — target
* Step count for warmup

Adjust weight if turnover diverges from target despite gradient: high divergence → raise weight (forces band to adapt faster); convergence → lower weight (back off, let other losses dominate).

Schulman-bounded, clamp [1e-4, 1.0].

### §3.5 Determinism preservation

* Band-head init under `scoped_init_seed(cfg.seed.wrapping_add(0xBA_ND_5EED))` per `pearl_determinism_achieved`.
* No PRNG inside band kernels (rl_band_mask is deterministic from inputs; rl_band_loss uses no RNG).
* Per-batch grad scratch reduced via `reduce_axis0` (same deterministic reducer used by all heads).
* No `cublasGemmEx` — band-head is small enough (2 × 128 = 256 params) that direct `raw_launch` matmul kernels are sufficient. Sidesteps the `cuBLAS PEDANTIC_MATH` concern from `pearl_determinism_cublas_gemm_root_cause`.

`determinism-check.sh` must pass at seeds 42/43/44 post-merge as a precondition for cluster submit.

---

## §4. ISV slots needed

10 new slots, indices 799-808 (continues from Phase 3D's 798). Names follow `RL_BAND_*` convention.

| Slot | Index | Bootstrap | Clamp | Semantic |
|---|---|---|---|---|
| `RL_BAND_ENABLED_INDEX` | 799 | 1.0 | {0.0, 1.0} | Master gate — > 0.5 enables band mask + band loss. Operator-controlled A/B. |
| `RL_BAND_LOSS_WEIGHT_INDEX` | 800 | 0.05 | [1e-4, 1.0] | Multiplier on band loss before joint-loss combine. |
| `RL_BAND_TURNOVER_TARGET_INDEX` | 801 | 0.05 | [0.005, 0.5] | Target trades-per-step (close events / step). Controller-adapted in Phase 4-B. |
| `RL_BAND_TURNOVER_EMA_INDEX` | 802 | 0.0 | [0.0, 1.0] | Measured turnover EMA — controller READS this, kernel WRITES. |
| `RL_BAND_TURNOVER_ALPHA_INDEX` | 803 | 0.01 | [0.001, 0.1] | EMA smoothing α for turnover. |
| `RL_BAND_GRAD_SHARPNESS_INDEX` | 804 | 2.0 | [0.5, 10.0] | Sigmoid surrogate sharpness for boundary gradient. |
| `RL_BAND_WARMUP_STEPS_INDEX` | 805 | 500 | [0, 5000] | Steps before band mask activates (Q needs initial training). |
| `RL_BAND_MAX_MASK_FRAC_INDEX` | 806 | 0.85 | [0.5, 1.0] | Max fraction of batch that can be band-masked (preserves Q exploration). 1.0 at eval. |
| `RL_BAND_WIDTH_EMA_INDEX` | 807 | 0.0 | [0.0, ∞) | Diag — EMA of `(b_u - b_l)` across batch. Pure observability. |
| `RL_BAND_MASK_FIRED_STEP_INDEX` | 808 | 0.0 | {0.0, 1.0} | Diag — 1.0 if any batch was masked this step. Used by diag JSONL emitter. |

ISV last-allocated slot post-Phase-4-A: 808 (was 798 in Phase 3D).

All slots respect `pearl_bootstrap_must_respect_clamp_range` — bootstraps lie in their clamp ranges.

---

## §5. Diag emission

### §5.1 Per-step scalar fields (single-writer, no atomicAdd)

Emitted into JSONL under the `band` namespace:

```json
"band": {
    "enabled": 1.0,
    "loss_weight": 0.05,
    "turnover_target": 0.05,
    "turnover_ema": 0.043,
    "loss": 0.000234,
    "width_mean": 5.42,        # mean(b_u - b_l) across batch
    "width_std":  1.13,        # std(b_u - b_l) across batch
    "b_l_mean": -2.71,
    "b_u_mean": +2.71,
    "mask_fired_step": 1.0,
    "frac_in_band": 0.624,     # fleet-fraction (§5.3)
    "frac_masked":  0.531,     # fleet-fraction (§5.3)
    "frac_pre_warmup": 0.0,    # 1.0 during first `RL_BAND_WARMUP_STEPS_INDEX`
    "warmup_steps": 500
}
```

### §5.2 Diag aggregation kernel — `rl_band_diag_aggregate.cu`

Tree-reduces band-output statistics across batch (B → 1 scalar each). Writes:
* mean/std of `b_l`, `b_u`, `width = b_u - b_l`
* count of batches with `pos ∈ band` (raw count, divided by `b_size` host-side for fraction)
* count of batches whose action was overridden to Hold by the mask

Same `reduce_axis0` shape as `multi_head_policy_aggregate_diag.cu`. Single-block tree reduce in shared memory, no atomic.

### §5.3 Fleet-fraction discipline

Per `pearl_fleet_fraction_not_aggregate`: every "X happened" diag MUST emit a per-batch fleet-fraction, NOT just an aggregate scalar. The four new fleet-fraction slots:

* `frac_in_band` = `count(pos ∈ band) / b_size` — measures how often the band default is engaged.
* `frac_masked` = `count(action overridden to Hold by band) / b_size` — narrower, ≤ frac_in_band.
* `frac_pre_warmup` = 1.0 if step < warmup, else 0.0 (binary, but emitted for schema constancy).

The "warmup-frac" diagnostic specifically catches the failure mode where the agent is silently running with band disabled because warmup is too long.

### §5.4 Comparison to Phase 3D diag

Phase 3D emits `controllers.quadratic_cost_*` for the cost mechanism. Mirror that pattern: band gets its own top-level namespace (`band.*`), parallel to `confidence_gate.*` and `frd_gate.*`.

---

## §6. Compatibility with existing infrastructure

### §6.1 Multi-head policy (Phase 2A)

Per §1.2: **single shared band** across all K policy heads. The band-head's forward runs ONCE per step (before the mixture forward). The band mask runs ONCE after the mixture's sampled action.

`MultiHeadPolicy::forward` is unchanged. `BandHead::forward` runs in parallel (separate kernel launch on the same stream). No coupling between them in the forward path; both feed independently into the trainer step.

In backward: band loss writes its own `grad_h_t_scratch_band` (OVERWRITE), folded into encoder grad accumulator alongside the MultiHeadPolicy's grad_h_t contribution. Both use `grad_h_accumulate_scaled` — standard pattern.

### §6.2 The 12 adaptive controllers

| Controller | Interaction with band | Resolution |
|---|---|---|
| Kelly fraction (`rl_kelly_*`) | Both control "size" — Kelly caps lots, band controls "should we be at any non-zero lots". | Band runs FIRST (mask before sizing). Kelly sees post-band action distribution and adapts. Compatible. |
| Edge decay (Page-Hinkley) | Edge decay throttles trading on detected EV degradation. | Both push toward less trading on bad signal. Compose multiplicatively (band masks structurally; edge decay scales Kelly down). |
| Gate threshold (confidence gate) | Already coexists per §2.3. | Order: band → confidence gate. |
| Gate LR multiplier | Multi-head policy gate LR boost. | Orthogonal — gate LR is on MultiHeadPolicy.gate, not band head. |
| Reward scale | Normalizes reward magnitude. | Band loss is decoupled from reward, so unaffected. |
| PPO clip, entropy coef, etc. | Phase 3D PPO surrogate. | Band loss is a NEW loss channel, doesn't enter PPO surrogate. Loss weight added to `IntegratedStepStats.l_total`. |
| C51 v_max controller | Adapts Q-atom support range. | Band loss does not touch C51. Q can be uncalibrated while band still works. |
| Hold-frac controller (in confidence gate) | Adaptive Hold target via threshold adjustment. | Operator decision: keep on for redundancy, OR set `RL_HOLD_TARGET_FRAC_INDEX = 0.0` once band proves effective (Phase 4-E ablation). |
| CMDP risk stack | Tail-event circuit breakers. | Different abstraction layer (CMDP for tail kills, band for steady-state turnover). Compose cleanly. |
| Welford trade-duration var | EMA on closed-trade durations. | Band reduces trade frequency → fewer closes → slower Welford convergence. Acceptable; Welford readout slot 660 (cumulative dones) is the canonical "have we seen enough trades" gate. |
| Surfer-scaffold weight | Smooth interpolation pnl-only ↔ shaped reward. | Orthogonal — band loss is added on top of reward, unaffected by w. |
| Popart normalize gate | Normalizes V regression. | Band loss is separate channel. V quality matters less for band gradient. |

**Net:** no controller needs to be disabled. Band integrates as a new layer below all existing risk-stack controllers.

### §6.3 Q-distill mechanism

Per `pearl_foxhunt_pi_trained_by_q_distillation_not_ppo`, Q-distill remains the primary π training signal. Band changes what π *can* output (post-mask), but does not change π's training:

* Q is trained on (state, action, reward) tuples where action is the post-band masked action.
* π is trained via Q-distill on softmax(Q/τ) → KL with π.
* When position ∈ band, action is always Hold → Q learns Q(s, Hold | in-band) accurately. Q(s, non-Hold | in-band) is rarely sampled (only when batch is in `min_explore` exception) → Q estimate is noisier there → softmax(Q/τ) puts more mass on Hold → π distill output gradient pushes π toward Hold for in-band states. Self-reinforcing in the *correct* direction.
* When position ∉ band, action is freely chosen → Q sees full action diversity → π distill learns to discriminate.

This is the architectural elegance: Q-distill still drives action CHOICE; band constrains action FREQUENCY. The two mechanisms operate on orthogonal axes.

### §6.4 CMDP risk stack

CMDP's `worst_consec_loss_count` and cooldown latch are tail-event protections. Band's turnover regulation is steady-state. Different timescales (CMDP: per-trade tail; band: per-batch steady state). Both fire independently; band cannot prevent CMDP cooldown from forcing a position close (CMDP override has priority over band default).

If CMDP forces flat-close while band wants flat (position 0 in band), the actions agree. If CMDP forces flat-close while band says "you're outside band, trade!", CMDP wins (safety stack precedence). The trainer step's existing action-override stack (`apply_cmdp_override` after gates) preserves this ordering.

### §6.5 Phase 3D additions (hold-bias, quadratic cost, PPO surrogate)

* **Hold-bias (Phase 3D-A):** keep. Improves Q-distill calibration of Hold action. Disabling would lose Q learning quality on the most-frequent action. Phase 4-E ablation: try disabling hold-bias once band is active to test minimum-effective-mechanism.
* **Quadratic cost (Phase 3D-B):** keep. Provides reward-signal pressure on trade-size (not just trade-frequency, which band controls). Complementary. Cao 2026 reports both as needed.
* **PPO surrogate (Phase 3D-C):** keep. Per pearl_foxhunt_pi_trained_by_q_distillation_not_ppo, PPO surrogate currently has weight 0.005 — small but present. Provides a direct policy-gradient channel that Q-distill cannot match. Band loss flows through encoder, not directly into π; PPO surrogate flows directly into π. Different chains.

All three Phase 3D mechanisms remain enabled by default. Phase 4-E ablation matrix can toggle each.

---

## §7. Implementation phases

### Phase 4-A — Foundation (week 1, ~600-800 LOC)

* `crates/ml-alpha/cuda/rl_band_head_forward.cu` — band-head forward (~60 LOC CUDA).
* `crates/ml-alpha/cuda/rl_band_head_backward.cu` — band-head backward + per-batch grad scratch (~80 LOC CUDA).
* `crates/ml-alpha/cuda/rl_band_mask.cu` — argmax mask kernel (~50 LOC, modeled on `rl_confidence_gate.cu`).
* `crates/ml-alpha/cuda/rl_band_loss.cu` — band loss + grad on (b_l, b_u) (~80 LOC).
* `crates/ml-alpha/cuda/rl_band_diag_aggregate.cu` — diag tree-reduce (~70 LOC, modeled on `multi_head_policy_aggregate_diag.cu`).
* `crates/ml-alpha/cuda/rl_band_turnover_ema_update.cu` — single-block EMA writer (~40 LOC).
* `crates/ml-alpha/src/rl/band_head.rs` — `BandHead` struct, `new()`, `forward()`, `backward()`, `emit_diag()` (~280 LOC, modeled on `multi_head_policy.rs:209-810` patterns).
* `crates/ml-alpha/src/rl/isv_slots.rs` — 10 new slot constants + doc comments (~80 LOC).
* `crates/ml-alpha/build.rs` — add 6 new cubin entries (~20 LOC).
* `crates/ml-alpha/src/trainer/integrated.rs` — `BandHead` field, allocate in `new()`, call forward/backward in `step_with_lobsim_gpu_body` (~150 LOC scattered edits).

**Falsification gate G4-A1:** local unit test `band_head_forward_emits_valid_band` — random `h_t` → forward → assert `b_l ≤ 0 ≤ b_u` for all batches, magnitudes ≤ N_max.

**Falsification gate G4-A2:** local unit test `band_mask_overrides_to_hold_when_in_band` — set band = `[-2, +2]`, position = 1 lot, action = LongSmall (5) → assert post-mask action = Hold (2). Position = 3 lots → action unchanged.

**Falsification gate G4-A3:** local determinism-check.sh exits 0 at seeds 42/43/44 with `FOXHUNT_BAND_ENABLED=1` (env var wiring band gate).

**Time estimate:** 5-7 days.

### Phase 4-B — Integration + turnover controller (week 2, ~250-350 LOC)

* `crates/ml-alpha/cuda/rl_band_turnover_controller.cu` — Schulman-bounded turnover-target adapter (~80 LOC, modeled on rl_confidence_gate's adaptive section).
* `crates/ml-alpha/cuda/rl_band_loss_weight_controller.cu` — band loss weight adapter (~80 LOC).
* Trainer integration: wire band loss into `IntegratedStepStats`, add `l_band` field, route into joint-loss combine alongside `l_pi`, `l_q`, `l_v`.
* Diag JSONL emission in `step_synthetic_diag_emit`: add `"band": {...}` namespace.

**Falsification gate G4-B1:** local mid-smoke (Tier 1.5, b=128, 2000+500 steps): `frac_masked` mean across last 200 steps ∈ [0.3, 0.7] (mechanism is active but not stuck). Width-EMA must change by ≥10% during training (band is learning, not stuck at init).

**Falsification gate G4-B2:** local mid-smoke: train pnl must not collapse vs Phase 3D baseline by more than 30% (band doesn't break training). Eval-step trade count must drop by ≥40% vs Phase 3D's 11,767-trade baseline.

**Falsification gate G4-B3:** `determinism-check.sh --quick` exits 0 with band enabled.

**Time estimate:** 3-5 days.

### Phase 4-C — Cluster verification (1-2 days work + 70 min cluster)

* Submit Tier 2 cluster run (b=1024, 20k+5k, 9 files) via `argo-alpha-rl.sh`.
* Verify eval-time band engagement (frac_masked > 0 in eval phase diag).
* Compare eval pnl, eval wr, trades count to Phase 3D bd811a774 baseline.

**Falsification gate G4-C1 (architectural success):**
* Eval trades / Phase 3D eval trades ≤ 0.2 (5× reduction; Cao 2026 reported 96% reduction).
* Eval pnl > -$5M (Phase 3D baseline TBD; absolute floor based on most-recent eval signals).
* Eval wr > 0.30 (vs Phase 3D's collapse pattern; not necessarily a win, but no worse).

**Falsification gate G4-C2 (kill criteria):**
* Eval trades > Phase 3D eval trades — mechanism is anti-effective. KILL.
* Train pnl < 50% of Phase 3D train pnl — mechanism breaks training. KILL.
* `frac_in_band` saturates at 1.0 or 0.0 — band collapsed (see §9 risks). KILL.

**Time estimate:** 1-2 days analysis + cluster wait.

### Phase 4-D (conditional) — Per-head bands

Only if Phase 4-C shows mode-dependent band optima (e.g., trend mode wants wider band, reversion narrower). ~400 LOC. NOT in foundation.

### Phase 4-E (conditional) — Ablation matrix

A/B test:
* Band enabled, hold-bias disabled
* Band enabled, quadratic cost disabled
* Band enabled, PPO surrogate disabled

To identify minimum-effective-mechanism. ~80 LOC of ISV gates + 3 cluster runs.

---

## §8. Falsification criteria

### §8.1 Local mid-smoke gates (b=128, ~10 min)

| Gate | Criterion | Threshold |
|---|---|---|
| G4-LM1 | Band activates by step 500 | `frac_masked` > 0.1 at step 1000 |
| G4-LM2 | Band is learning, not stuck | `width_std / width_mean` increases ≥10% by step 2000 |
| G4-LM3 | No mechanism deadlock | Trade count over 2000 steps > 50 (agent IS still trading sometimes) |
| G4-LM4 | Determinism | `determinism-check.sh --quick` exits 0 |
| G4-LM5 | Q-distill still works | `q_pi_agree_ema` trends to > 0.5 by step 2000 (band doesn't break Q-distill alignment) |
| G4-LM6 | No NaN | No NaN in any diag field over 2500 steps |

ALL must pass before cluster submit. If any fail, STOP and surface findings per spec discipline.

### §8.2 Cluster gates (b=1024, 20k+5k, ~70 min)

| Gate | Criterion | Threshold |
|---|---|---|
| G4-C1 | Trade reduction | Eval close events < 0.2 × Phase 3D eval close events |
| G4-C2 | No catastrophic loss | Eval pnl > -$10M (loose floor; tighter once Phase 3D eval baseline known) |
| G4-C3 | Eval wr stability | Eval wr > 0.30 |
| G4-C4 | Band convergence | `width_mean` over last 1000 eval steps stable (CV < 0.2) |
| G4-C5 | No band collapse | 0.1 < `frac_masked` < 0.9 over last 1000 eval steps |

**Comparison baseline:**
* Phase 3D commit `bd811a774` produced 11,767 trades over 20k train steps (post Phase 3D-A+B+C). Eval verdict TBD.
* Phase 3D cluster `grwwh` (without quadratic cost) showed 47K closes / 500 steps in eval — the pathology this spec addresses.

**Target:** Phase 4 eval close events < 2,400 over 20k steps (5× reduction from Phase 3D). Stretch: < 470 (25× reduction, approaching Cao 2026's 96% reduction).

**Predetermined kill criteria:**
* If G4-C1 fails (band did NOT reduce trades): mechanism is grafted incorrectly, NOT a tuning issue. KILL and revisit §1.2 (per-head vs single band) and §3.2 (loss formulation).
* If G4-C5 fails (band collapsed): bootstrap or controller adaptation is broken. KILL and re-do §1.3 init and §3.4 controller.
* If G4-C2 fails (catastrophic loss): band is forcing Hold when trading was correct, OR Q-distill broke. KILL and check Q-distill agreement diag (G4-C concomitant check).

---

## §9. Risks and mitigations

### §9.1 Risk: band collapses to extremes

**Failure mode A — band → `[-N_max, +N_max]` (never trades):** the loss weight on band is too low OR turnover target is too low (≤ 0.001), so the band keeps widening without resistance.

* **Mitigation 1:** turnover target clamp `[0.005, 0.5]` enforces a minimum target turnover. Band cannot be rewarded for zero turnover.
* **Mitigation 2:** `RL_BAND_MAX_MASK_FRAC_INDEX` (bootstrap 0.85) caps the maximum fraction of batches that can be masked at any single step. Force at least 15% exploration. Cannot be set below 0.5 per clamp.
* **Mitigation 3:** width-EMA clamp — if `width_mean > 1.5 × N_max` for >1000 steps, controller forcibly narrows by setting `RL_BAND_LOSS_WEIGHT_INDEX *= 2`. Discrete escape mechanism per `pearl_dead_signal_resurrection_discipline`.

**Failure mode B — band → `[0, 0]` (always trades):** turnover target is set too high (≥ 0.5) or loss weight is too aggressive, so band keeps shrinking.

* **Mitigation 1:** turnover target clamp `[0.005, 0.5]` enforces a maximum.
* **Mitigation 2:** `|tanh|` activation has natural soft-zero behavior; the gradient saturates as z → 0. Hard collapse requires explicit pathological loss.
* **Mitigation 3:** init bias `±atanh(0.5)` keeps b_l, b_u at half-N_max at step 0; the agent has to do significant gradient steps to push to zero — visible in width-EMA diag, observable kill criterion.

### §9.2 Risk: non-differentiable boundaries

The hard indicator `pos ∈ [b_l, b_u]` is non-differentiable in pos at b_l, b_u. Mitigation: sigmoid surrogate (§3.2) with `RL_BAND_GRAD_SHARPNESS_INDEX` (bootstrap 2.0, clamp [0.5, 10.0]). Higher sharpness = closer to true gradient but more noisy; lower = smoother but more biased.

**A/B test in Phase 4-A:** sweep sharpness ∈ {0.5, 1.0, 2.0, 5.0} on local mid-smoke. Pick the value that gives most-stable width-EMA convergence. Lock for Phase 4-B+.

### §9.3 Risk: eval-time determinism breaks

The band mask is conditional (depends on position state). CUDA Graph capture (used by `step_with_lobsim_gpu_body` prefill path) does not support runtime control flow. Mitigation: band mask runs OUTSIDE graph capture (same pattern as `rl_confidence_gate` at integrated.rs:7487-7530). Already established pattern, low risk.

### §9.4 Risk: Q-distill / band gradient conflict

Both gradients flow into `grad_h_t`. If they cancel, neither learns.

* **Mitigation 1:** start `RL_BAND_LOSS_WEIGHT_INDEX = 0.05` (small). Q-distill grad dominates initially.
* **Mitigation 2:** band loss is on (b_l, b_u) outputs, which depend on h_t via 2 linear projections — very small parameter count (256 weights). Q-distill grad is on policy logits, depends on a much larger MLP. The two gradients act on different scales of representation.
* **Mitigation 3:** diag emits `band.grad_h_t_norm` (per-step L2 norm of band's contribution to grad_h_t) and `multi_head.grad_h_t_norm`. If they're comparable in magnitude, conflict risk is real; if band's is 10× smaller, no conflict.

### §9.5 Risk: cluster compute graph capture issues

The graph-capture path (lines 7475-7485) terminates before the gates. Band-head FORWARD lives BEFORE capture-end (inside the captured block) — needs to be capturable. Band-head forward is purely `h_t → linear → activation`, no control flow, fully capturable. Band MASK lives OUTSIDE capture (same as confidence gate) — already-supported pattern.

**Verification:** Phase 4-A unit test must explicitly run `step_with_lobsim_gpu` (the graph-capture path) AND assert that band-head forward output is produced. Trivial check, but easy to miss.

### §9.6 Risk: discrete action mismatch with band intent

**Critical assessment (per spec discipline — "if band doesn't graft cleanly, STOP and surface"):**

Does the discrete 11-action space cleanly represent "small adjustment toward band boundary"?

Davis-Norman's idealized policy: when `pos > b_u`, trade *exactly* `pos - b_u` lots to land on the boundary. Foxhunt's discrete actions can produce:
* ShortSmall (1 lot reduction in long direction)
* ShortLarge (4 lot reduction)
* FlatFromLong (close all)
* HalfFlatLong (close ⌈|pos|/2⌉)

If `pos = 3`, `b_u = 1.5` → ideal trade is 1.5 lots toward short. Discrete options: ShortSmall (1 lot, undershoots) or ShortLarge (4 lots, overshoots and reverses). **Mismatch is real but bounded.**

**However:** this mismatch is NOT specific to no-transaction-band — it's a property of the discrete action space at any non-aligned target. The band mask only forces Hold when position is INSIDE the band; when position is OUTSIDE, the agent gets to pick from the full discrete grid via Q-distill softmax. Q learns which discrete action best moves position toward the band over many trials. The discrete-vs-continuous error is absorbed by Q's value estimate.

**Conclusion:** clean grafting. The band sets the *target region*; Q-distill picks the *best discrete action* to reach it. No further mechanism needed.

If empirically this turns out to bottleneck performance (Phase 4-C diag shows lots of oscillation around band boundaries — e.g., agent alternates +1/-1 to track a target band of width 0.5), Phase 4-D could add finer-grained 1-lot incremental actions. Not in scope for foundation.

### §9.7 Risk: train→eval generalization

The band is learned from training data. If training regime turnover ≠ eval regime turnover, the band may be miscalibrated at eval.

* **Mitigation 1:** the band is decoupled from regime-specific reward statistics — it learns from realized turnover vs target, which is policy-relative, not regime-relative.
* **Mitigation 2:** the turnover *target* (set by controller) is regime-naïve by default (constant 0.05). The Phase 4-B controller adapts target slowly (Schulman-bounded 1.1× per step at most). Eval boundary changes lock the target.
* **Mitigation 3:** diag emits `band.turnover_target` over eval — operator can observe whether target locks at train-end value or drifts at eval start. Eval boundary reset (per `2026-05-31-eval-boundary-normalization-preservation-design.md` pattern) freezes turnover EMA + target at eval start.

---

## §10. Literature references

* **Imaki, T., Imajo, K., Ito, K., et al.** (2021). "No-Transaction Band Network: A Neural Network Architecture for Efficient Deep Hedging." arXiv:2103.01775. https://arxiv.org/abs/2103.01775
* **Davis, M. H. A., & Norman, A. R.** (1990). "Portfolio Selection with Transaction Costs." *Mathematics of Operations Research* 15(4): 676-713. The optimality theorem.
* **Davis, M. H. A., Pan, P. Y., Rynik, M.** (2010+). "Portfolio Selection under Fixed and Proportional Transaction Costs." Extended Davis-Norman to mixed cost structures.
* **Whalley, A. E., & Wilmott, P.** (1997). "An asymptotic analysis of an optimal hedging model for option pricing with transaction costs." *Mathematical Finance* 7(3): 307-324. The `(cost)^(1/3)` band-width scaling law.
* **Cao, X., et al.** (2026). "Impact-aware Reinforcement Learning for High-Frequency Trading." arXiv:2603.29086. Quadratic cost methodology; 96% turnover reduction (Phase 3D-B foundation).
* **Goodhart-Skalse** (2024). "On the Limits of Proxy-Reward Shaping in Deep Reinforcement Learning." NeurIPS. The proxy-reward-attenuation theorem motivating the band approach.
* **Schulman, J., Wolski, F., Dhariwal, P., Radford, A., Klimov, O.** (2017). "Proximal Policy Optimization Algorithms." arXiv:1707.06347. PPO clip mechanism — referenced for Schulman-bounded controller updates throughout foxhunt.
* **Preferred Networks `pfhedge`** — production reference implementation of no-transaction-band in PyTorch. https://github.com/pfnet-research/pfhedge — specifically `pfhedge/nn/modules/bs/no_transaction_band.py` and `examples/no_transaction_band_network.py`.

---

## §11. Implementation discipline checklist

Per `feedback_investigation_first_falsification_methodology`:

- [x] Spec-first — this document precedes any code.
- [x] Predetermined falsification gates with numbers (§8).
- [x] Memory pearl chain — links 5 pearls in header.
- [x] Theorem-level rigor — Davis-Norman optimality cited.
- [x] Falsifiable verdict — Phase 4-C kill criteria explicit.
- [ ] Multi-agent parallel research — deferred to plan document.
- [ ] Investigation-first dispatch with STOP-on-unexpected-finding — applies to implementation execution, not this spec.
- [ ] Cluster + Phase 0 cross-source contract — band diag must respect `pearl_diag_pnl_fields_are_shaped_reward_not_usd` (do NOT use `_usd` fields for kill criteria; use `eval_summary.total_pnl_usd`).
- [x] Foxhunt discipline — no atomicAdd, no nvrtc, ISV-driven controllers, scoped_init_seed.
