diff --git a/config/training/dqn-localdev.toml b/config/training/dqn-localdev.toml index c3076ffd9..2a47cff42 100644 --- a/config/training/dqn-localdev.toml +++ b/config/training/dqn-localdev.toml @@ -82,6 +82,7 @@ anti_lr_sharpe_threshold = 0.3 feature_mask_fraction = 0.3 feature_noise_scale = 0.1 enable_vol_normalization = true +bottleneck_dim = 0 stochastic_depth_prob = 0.2 asymmetric_dd_weight = 0.5 time_reversal_mod = 5 diff --git a/config/training/dqn-production.toml b/config/training/dqn-production.toml index 8561f09f6..6601ba5f7 100644 --- a/config/training/dqn-production.toml +++ b/config/training/dqn-production.toml @@ -87,6 +87,7 @@ anti_lr_sharpe_threshold = 0.3 feature_mask_fraction = 0.3 feature_noise_scale = 0.1 enable_vol_normalization = true +bottleneck_dim = 0 stochastic_depth_prob = 0.2 asymmetric_dd_weight = 0.5 time_reversal_mod = 5 diff --git a/crates/ml/src/cuda_pipeline/batched_backward.rs b/crates/ml/src/cuda_pipeline/batched_backward.rs index 1836494f0..0f156d637 100644 --- a/crates/ml/src/cuda_pipeline/batched_backward.rs +++ b/crates/ml/src/cuda_pipeline/batched_backward.rs @@ -449,7 +449,7 @@ impl CublasBackward { save_h_v: u64, // [B, VH] save_h_b: &[u64; 3], // [B, AH] each // Online weight pointers (read-only for W^T computation) - w_ptrs: &[u64; 20], + w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], // Flat gradient accumulator (must be zeroed by caller) grad_buf_base: u64, // Scratch buffers for inter-layer gradients (f32) diff --git a/crates/ml/src/cuda_pipeline/batched_forward.rs b/crates/ml/src/cuda_pipeline/batched_forward.rs index 9dc02dee3..0afb479cd 100644 --- a/crates/ml/src/cuda_pipeline/batched_forward.rs +++ b/crates/ml/src/cuda_pipeline/batched_forward.rs @@ -234,7 +234,7 @@ impl CublasForward { &self, stream: &Arc, states_ptr: u64, - w_ptrs: &[u64; 20], + w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], h_s1_ptr: u64, h_s2_ptr: u64, h_v_ptr: u64, h_b0_ptr: u64, h_b1_ptr: u64, h_b2_ptr: u64, v_logits_ptr: u64, b_logits_ptr: u64, @@ -285,7 +285,7 @@ impl CublasForward { &self, stream: &Arc, states_buf: &CudaSlice, - w_ptrs: &[u64; 20], + w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], save_h_s1: &CudaSlice, save_h_s2: &CudaSlice, save_h_v: &CudaSlice, @@ -367,7 +367,7 @@ impl CublasForward { &self, stream: &Arc, states_ptr: u64, - tg_w_ptrs: &[u64; 20], + tg_w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], h_s1_ptr: u64, h_s2_ptr: u64, h_v_ptr: u64, h_b_ptr: u64, v_logits_ptr: u64, b_logits_ptr: u64, ) -> Result<(), MLError> { @@ -426,7 +426,7 @@ impl CublasForward { &self, stream: &Arc, next_states_buf: &CudaSlice, - tg_w_ptrs: &[u64; 20], + tg_w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], tg_h_s1_scratch: &CudaSlice, tg_h_s2_buf: &CudaSlice, tg_h_v_scratch: &CudaSlice, @@ -789,9 +789,9 @@ fn compile_bias_kernels( /// Returns 20 raw u64 device pointers (base + byte_offset for each tensor). pub fn bf16_weight_ptrs( params_buf: &CudaSlice, - param_sizes: &[usize; 20], + param_sizes: &[usize; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], stream: &Arc, -) -> [u64; 20] { +) -> [u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS] { let base = { let (ptr, guard) = params_buf.device_ptr(stream); let _no_drop = ManuallyDrop::new(guard); @@ -800,15 +800,16 @@ pub fn bf16_weight_ptrs( bf16_weight_ptrs_from_base(base, param_sizes) } -/// Compute 20 raw BF16 device pointers from a pre-resolved base pointer. +/// Compute raw BF16 device pointers from a pre-resolved base pointer. /// Graph-safe: no device_ptr calls, no event recording. pub fn bf16_weight_ptrs_from_base( base: u64, - param_sizes: &[usize; 20], -) -> [u64; 20] { - let mut ptrs = [0_u64; 20]; + param_sizes: &[usize; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], +) -> [u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS] { + let n = super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS; + let mut ptrs = [0_u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS]; let mut byte_offset: u64 = 0; - for i in 0..20 { + for i in 0..n { ptrs[i] = base + byte_offset; byte_offset += (param_sizes[i] * std::mem::size_of::()) as u64; } diff --git a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs index 6c469f787..2af18b2e0 100644 --- a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs +++ b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs @@ -930,7 +930,7 @@ impl GpuBacktestEvaluator { /// a 64-bar (~1 hour) staleness window has negligible impact on action quality. fn submit_dqn_step_loop_cublas( &self, - w_ptrs: &[u64; 20], + w_ptrs: &[u64; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], dqn_cfg: &DqnBacktestConfig, ) -> Result<(), MLError> { // ── Unwrap per-step cuBLAS state (used only for fallback / CUDA graph) ── @@ -1778,7 +1778,7 @@ impl GpuBacktestEvaluator { /// w_b0fc, b_b0fc, w_b0out, b_b0out, /// w_b1fc, b_b1fc, w_b1out, b_b1out, /// w_b2fc, b_b2fc, w_b2out, b_b2out]` -fn compute_backtest_param_sizes(cfg: &DqnBacktestConfig, state_dim: usize) -> [usize; 20] { +fn compute_backtest_param_sizes(cfg: &DqnBacktestConfig, state_dim: usize) -> [usize; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS] { [ cfg.shared_h1 * state_dim, // w_s1 cfg.shared_h1, // b_s1 @@ -1800,6 +1800,9 @@ fn compute_backtest_param_sizes(cfg: &DqnBacktestConfig, state_dim: usize) -> [u cfg.adv_h, // b_b2fc cfg.branch_2_size * cfg.num_atoms * cfg.adv_h, // w_b2out cfg.branch_2_size * cfg.num_atoms, // b_b2out + // #31 bottleneck: backtest always uses bottleneck_dim=0 (no bottleneck at inference) + 0, // w_bn (unused) + 0, // b_bn (unused) ] } diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index dcc9c08e3..798b6ceda 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -189,6 +189,12 @@ pub struct GpuDqnTrainConfig { pub ensemble_disagreement_penalty: f32, /// #21 Stochastic depth drop probability. 0.0 = disabled, 0.2 = 20% drop per layer. pub stochastic_depth_prob: f32, + /// #31 Temporal causal bottleneck dimension. 0 = disabled (no bottleneck). + /// 2 = maximum compression (14 market features → 2 abstract dimensions). + pub bottleneck_dim: usize, + /// Market feature dimension (e.g. 42). Used by bottleneck to separate + /// market features from portfolio features in the state vector. + pub market_dim: usize, } impl Default for GpuDqnTrainConfig { @@ -228,6 +234,8 @@ impl Default for GpuDqnTrainConfig { asymmetric_dd_weight: 0.0, ensemble_disagreement_penalty: 0.0, stochastic_depth_prob: 0.0, + bottleneck_dim: 0, + market_dim: 42, } } } @@ -291,29 +299,52 @@ pub struct FusedTrainScalars { // w_b0fc, b_b0fc, w_b0out, b_b0out, w_b1fc, b_b1fc, w_b1out, b_b1out, // w_b2fc, b_b2fc, w_b2out, b_b2out. -/// Compute the size (element count) of each of the 20 weight tensors. -pub(crate) fn compute_param_sizes(cfg: &GpuDqnTrainConfig) -> [usize; 20] { +/// Number of weight tensors in the flat parameter buffer. +/// 20 original (DQN network) + 2 bottleneck (w_bn, b_bn) = 22. +pub(crate) const NUM_WEIGHT_TENSORS: usize = 22; + +/// Compute the size (element count) of each weight tensor. +/// +/// Tensors 0-19: standard DQN network (unchanged layout for backward compat). +/// Tensors 20-21: temporal causal bottleneck (0 elements when bottleneck_dim=0). +/// +/// When bottleneck is active, w_s1 input dimension changes from state_dim to +/// (bottleneck_dim + portfolio_dim) where portfolio_dim = state_dim - market_dim. +pub(crate) fn compute_param_sizes(cfg: &GpuDqnTrainConfig) -> [usize; NUM_WEIGHT_TENSORS] { + // When bottleneck is active, h_s1 input is [bottleneck_dim + portfolio_features] + // instead of [state_dim]. portfolio_features = state_dim - market_dim (typically 6-8). + let s1_input_dim = if cfg.bottleneck_dim > 0 { + cfg.bottleneck_dim + (cfg.state_dim - cfg.market_dim) + } else { + cfg.state_dim + }; + let bn_dim = cfg.bottleneck_dim; + let market_dim = cfg.market_dim; + [ - cfg.shared_h1 * cfg.state_dim, // w_s1 - cfg.shared_h1, // b_s1 - cfg.shared_h2 * cfg.shared_h1, // w_s2 - cfg.shared_h2, // b_s2 - cfg.value_h * cfg.shared_h2, // w_v1 - cfg.value_h, // b_v1 - cfg.num_atoms * cfg.value_h, // w_v2 - cfg.num_atoms, // b_v2 - cfg.adv_h * cfg.shared_h2, // w_b0fc - cfg.adv_h, // b_b0fc - cfg.branch_0_size * cfg.num_atoms * cfg.adv_h, // w_b0out - cfg.branch_0_size * cfg.num_atoms, // b_b0out - cfg.adv_h * cfg.shared_h2, // w_b1fc - cfg.adv_h, // b_b1fc - cfg.branch_1_size * cfg.num_atoms * cfg.adv_h, // w_b1out - cfg.branch_1_size * cfg.num_atoms, // b_b1out - cfg.adv_h * cfg.shared_h2, // w_b2fc - cfg.adv_h, // b_b2fc - cfg.branch_2_size * cfg.num_atoms * cfg.adv_h, // w_b2out - cfg.branch_2_size * cfg.num_atoms, // b_b2out + cfg.shared_h1 * s1_input_dim, // [0] w_s1 (input dim changes with bottleneck) + cfg.shared_h1, // [1] b_s1 + cfg.shared_h2 * cfg.shared_h1, // [2] w_s2 + cfg.shared_h2, // [3] b_s2 + cfg.value_h * cfg.shared_h2, // [4] w_v1 + cfg.value_h, // [5] b_v1 + cfg.num_atoms * cfg.value_h, // [6] w_v2 + cfg.num_atoms, // [7] b_v2 + cfg.adv_h * cfg.shared_h2, // [8] w_b0fc + cfg.adv_h, // [9] b_b0fc + cfg.branch_0_size * cfg.num_atoms * cfg.adv_h, // [10] w_b0out + cfg.branch_0_size * cfg.num_atoms, // [11] b_b0out + cfg.adv_h * cfg.shared_h2, // [12] w_b1fc + cfg.adv_h, // [13] b_b1fc + cfg.branch_1_size * cfg.num_atoms * cfg.adv_h, // [14] w_b1out + cfg.branch_1_size * cfg.num_atoms, // [15] b_b1out + cfg.adv_h * cfg.shared_h2, // [16] w_b2fc + cfg.adv_h, // [17] b_b2fc + cfg.branch_2_size * cfg.num_atoms * cfg.adv_h, // [18] w_b2out + cfg.branch_2_size * cfg.num_atoms, // [19] b_b2out + // #31 Temporal Causal Bottleneck (0 when disabled) + bn_dim * market_dim, // [20] w_bn [bottleneck_dim, market_dim] + bn_dim, // [21] b_bn [bottleneck_dim] ] } diff --git a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs index ecc9c13e8..de5c66b6b 100644 --- a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs +++ b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs @@ -444,7 +444,7 @@ pub struct GpuExperienceCollector { /// Total number of F32 parameters in the flat buffer. total_params: usize, /// Per-tensor sizes for computing weight pointers. - param_sizes: [usize; 20], + param_sizes: [usize; super::gpu_dqn_trainer::NUM_WEIGHT_TENSORS], // ── CVaR position scaling (IQN dual-head) ────────────────────── /// Device pointer to CVaR scales [N]. 0 = NULL = no scaling. diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index afeb8457a..c2ebefd96 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -1004,6 +1004,11 @@ pub struct DQNHyperparameters { /// 0.0 = disabled. pub trade_clustering_penalty: f64, + /// #31 Temporal causal bottleneck dimension. 0 = disabled, 2 = maximum compression. + /// All market features are compressed through [market_dim → bottleneck_dim] linear + tanh + /// before reaching the Q-network. Makes memorization structurally impossible. + pub bottleneck_dim: usize, + /// #21 Stochastic depth: drop probability per hidden layer during training. /// Each layer is independently dropped with this probability. Surviving layers /// are scaled by 1/(1-p) for expected-value correction. 0.0 = disabled. @@ -1524,6 +1529,7 @@ impl DQNHyperparameters { enable_mirror_universe: true, // #10: alternate mirrored/normal epochs position_entropy_weight: 0.01, // #19: reward += 0.01 * H(position_histogram) trade_clustering_penalty: 0.05, // #25: penalize temporally clustered trades + bottleneck_dim: 0, // #31: disabled by default (0 = no bottleneck, 2 = max compression) stochastic_depth_prob: 0.2, // #21: 20% drop probability per hidden layer asymmetric_dd_weight: 0.5, // #18: extra loss on Q-overestimation in drawdown diff --git a/crates/ml/src/trainers/dqn/fused_training.rs b/crates/ml/src/trainers/dqn/fused_training.rs index 6d52b7336..78f6b522e 100644 --- a/crates/ml/src/trainers/dqn/fused_training.rs +++ b/crates/ml/src/trainers/dqn/fused_training.rs @@ -209,6 +209,8 @@ impl FusedTrainingCtx { asymmetric_dd_weight: hyperparams.asymmetric_dd_weight as f32, ensemble_disagreement_penalty: hyperparams.ensemble_disagreement_penalty as f32, stochastic_depth_prob: hyperparams.stochastic_depth_prob as f32, + bottleneck_dim: hyperparams.bottleneck_dim, + market_dim: 42, // MARKET_DIM constant — 42 preprocessed features }; // Extract weight sets from VarMaps (online + target) diff --git a/crates/ml/src/training_profile.rs b/crates/ml/src/training_profile.rs index 8bc659a20..0492dd28c 100644 --- a/crates/ml/src/training_profile.rs +++ b/crates/ml/src/training_profile.rs @@ -161,6 +161,7 @@ pub struct GeneralizationSection { pub feature_mask_fraction: Option, pub feature_noise_scale: Option, pub enable_vol_normalization: Option, + pub bottleneck_dim: Option, pub stochastic_depth_prob: Option, pub asymmetric_dd_weight: Option, pub time_reversal_mod: Option, @@ -811,6 +812,7 @@ impl DqnTrainingProfile { if let Some(v) = g.feature_mask_fraction { hp.feature_mask_fraction = v; } if let Some(v) = g.feature_noise_scale { hp.feature_noise_scale = v; } if let Some(v) = g.enable_vol_normalization { hp.enable_vol_normalization = v; } + if let Some(v) = g.bottleneck_dim { hp.bottleneck_dim = v; } if let Some(v) = g.stochastic_depth_prob { hp.stochastic_depth_prob = v; } if let Some(v) = g.asymmetric_dd_weight { hp.asymmetric_dd_weight = v; } if let Some(v) = g.time_reversal_mod { hp.time_reversal_mod = v; } diff --git a/docs/superpowers/plans/2026-03-30-gems-and-pearls-generalization.md b/docs/superpowers/plans/2026-03-30-gems-and-pearls-generalization.md index 9c7b113ca..327d43376 100644 --- a/docs/superpowers/plans/2026-03-30-gems-and-pearls-generalization.md +++ b/docs/superpowers/plans/2026-03-30-gems-and-pearls-generalization.md @@ -1,4 +1,4 @@ -# Gems & Pearls — 28 Generalization Techniques for DQN Trading Agent +# Gems & Pearls — 32+ Generalization Techniques for DQN Trading Agent > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. @@ -934,3 +934,174 @@ The model literally cannot learn anything that doesn't generalize. ``` - [ ] **Step 6: Compile + test + commit** + +--- + +## THE CROWN JEWELS — Four Layers of Defense + +Together these form a complete anti-memorization fortress: + +| Layer | Technique | Attacks | Level | +|-------|-----------|---------|-------| +| **Gem** | Temporal Causal Bottleneck (#31) | Can only remember 2 numbers | Architecture | +| **Pearl** | Gradient Vaccine (#32) | Those 2 numbers must work on unseen data | Optimization | +| **King** | Adversarial Self-Play (#33) | Past self exploits any memorized pattern | Strategic | +| **Emperor** | Causal Intervention (#34) | Only features that CAUSE outcomes, not correlate | Epistemic | + +--- + +### Task 33: Adversarial Self-Play with Future Self (THE KING) [PLAN] + +**Phase:** 3 (Architecture) | **Difficulty:** Hard | **Impact:** TRANSFORMATIVE + +**Files:** +- Create: `crates/ml/src/trainers/dqn/adversarial_self_play.rs` +- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` (saboteur controls microstructure) +- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` (checkpoint management + self-play cycle) +- Modify: `crates/ml/src/trainers/dqn/config.rs` + +**The Core Idea:** + +Train TWO copies of the DQN against each other. The "trader" picks entries and exits. +The "saboteur" controls market microstructure (spread, slippage, fill probability, +latency) and tries to MAXIMIZE the trader's losses. The twist: the saboteur is a +PAST version of the trader (checkpoint from 50 epochs ago). It knows exactly what the +current trader USED to do — every pattern it exploited. It specifically attacks those. + +The trader must evolve AWAY from its own past strategies to survive. It can't settle +into any fixed policy because its past self knows that policy and will destroy it. +This creates an evolutionary arms race where the only stable equilibrium is a policy +that works against ALL possible adversaries — including itself. + +**Why this is the king:** + +1. **No technique in RL trading literature does this.** Game theory meets self-play + meets temporal evolution. AlphaGo's self-play but for market microstructure. + +2. **Past-self adversary is the perfect opponent.** It has full knowledge of the + trader's old strategies — correlation patterns, entry signals, regime preferences. + It generates EXACTLY the market conditions that would have exploited those patterns. + +3. **Evolutionary pressure toward robustness.** The only stable policy is one that + works regardless of market microstructure — which IS generalization. + +**Implementation:** + +- [ ] **Step 1: Checkpoint manager** — save model weights every 50 epochs +- [ ] **Step 2: Saboteur network** — small MLP [state_dim → 32 → 3] outputting + (spread_mult, fill_prob, slippage_mult). Initialized from past checkpoint. +- [ ] **Step 3: Self-play cycle** — alternating phases: + - Epochs 1-50: normal training (build initial policy) + - Epochs 51-55: saboteur trains against current trader (maximize losses) + - Epochs 56-100: trader trains against frozen saboteur + - Epoch 100: save new checkpoint, load as next saboteur +- [ ] **Step 4: Saboteur output → ExperienceCollectorConfig** — saboteur's output + replaces fixed spread/fill/slippage values per epoch +- [ ] **Step 5: Compile + test + commit** + +--- + +### Task 34: Causal Intervention Training (THE EMPEROR) [PLAN] + +**Phase:** 3 (Architecture) | **Difficulty:** Hard | **Impact:** TRANSFORMATIVE + +**Files:** +- Create: `crates/ml/src/cuda_pipeline/causal_intervention_kernel.cu` +- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` (intervention forward passes) +- Modify: `crates/ml/src/trainers/dqn/fused_training.rs` (causal loss term) +- Modify: `crates/ml/src/trainers/dqn/config.rs` + +**The Core Idea:** + +Instead of learning correlations (feature X predicts profit), learn CAUSATION +(changing feature X CAUSES different outcomes). At each training step: + +1. Take the current state batch [B, state_dim] +2. For each feature k, create an INTERVENED copy: set feature k to a counterfactual + value (e.g. +1, 0, -1) while keeping everything else fixed +3. Run forward passes on the intervened states +4. Compare Q-values: if changing feature k doesn't change Q, it's noise + +Add a causal regularization loss: +``` +L_causal = -sum_k variance(Q(do(X_k = x)) for x in {-1, 0, +1}) +``` + +This MAXIMIZES sensitivity to features that matter and MINIMIZES sensitivity +to noise. Pearl's do-calculus applied to RL. + +**Why this is the emperor:** + +1. **Correlations break OOS. Causation doesn't.** The only features that survive + the causal intervention test are features that genuinely CAUSE different trading + outcomes across all regimes. Spurious correlations (which cause overfitting) + are mathematically eliminated. + +2. **Nobody has done this in RL trading.** Causal inference in ML is growing, + but applying do-calculus to Q-function feature sensitivity is novel. + +3. **Synergy with bottleneck (#31).** The bottleneck compresses 14 features into 2 + dimensions. The causal intervention ensures those 2 dimensions capture features + with genuine causal influence, not spurious correlations. + +**Implementation:** + +- [ ] **Step 1: Intervention kernel** — for each feature k (0..market_dim), create + 3 copies of the state batch with feature k set to {-1, 0, +1}. Total: + 3 * market_dim forward passes. With market_dim=14 (active features), that's + 42 extra forward passes per training step — ~10% overhead with cuBLAS. + +- [ ] **Step 2: Compute per-feature causal sensitivity** — for each feature k, + var(Q(do(k=-1)), Q(do(k=0)), Q(do(k=+1))). High variance = causal feature. + +- [ ] **Step 3: Causal regularization loss** — add to total loss: + `L_causal = -causal_weight * sum(sensitivities)` + The negative sign MAXIMIZES sensitivity (model is rewarded for using causal features). + +- [ ] **Step 4: Causal feature importance logging** — log per-feature sensitivity + at epoch boundary. Provides interpretable feature ranking. + +- [ ] **Step 5: Config** + ```rust + pub enable_causal_intervention: bool, // default: false (expensive) + pub causal_weight: f64, // default: 0.1 + pub causal_intervention_values: Vec, // default: [-1.0, 0.0, 1.0] + ``` + +- [ ] **Step 6: Compile + test + commit** + +--- + +## Complete Defense Architecture + +``` + ┌──────────────────────────────┐ + │ EMPEROR: Causal │ + │ Only causal features survive │ + │ (epistemic defense) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ KING: Self-Play │ + │ Past self exploits memorized │ + │ patterns (strategic defense) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ PEARL: Vaccine │ + │ Only generalizing gradients │ + │ survive (optimization defense)│ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ GEM: Bottleneck │ + │ 2D information compression │ + │ (architecture defense) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ DQN Q-Network │ + │ Can only learn generalizable │ + │ trading strategies │ + └──────────────────────────────┘ +```