feat(alpha): decision-stride + cluster 9-fold CV workflow
Two complementary additions to validate the minute-horizon alpha hypothesis at IBKR-realistic costs: 1. `alpha_baseline --decision-stride N`: emits a new action every N steps; between decisions force action=0 (wait) so an open position is held rather than re-decided per bar. Cuts per-bar trade counts ~stride× and removes the coin-flip overtrading. Local 2Q sweep showed stride=200 + scaled training (8K episodes × 25 envs × H=1200) flipped Sharpe at ¼-tick from -4.29 (per-bar, 3-fold mean) to +1.78, with std collapsing from ±8.8 to ±1.15. Break-even cost moved from <¼-tick to ~1-tick — for the first time positive at IBKR-realistic passive-execution frictions. 2. `alpha_train_stacker --max-rows N`: optional cap on bars consumed from the fxcache. Used during local 2Q smoke (--max-rows 4M against the 17.8M-row 9Q fxcache) to fit Mamba2 training on a 4 GB consumer GPU; on the cluster (--no-cap) it sees all 9Q. 3. New Argo workflow `alpha-cv`: standalone template that compiles alpha_train_stacker + alpha_baseline + alpha_fill_coeffs.json, trains the stacker on the 9Q fxcache, then runs 9 sequential walk-forward folds of alpha_baseline on disjoint 1.9M-bar windows (one per quarter). Launcher script `scripts/argo-alpha-cv.sh` mirrors argo-train.sh conventions. The local 2Q test that motivated this commit is summarised inline in the alpha-cv template comments; the verdict was "framing was the bug — once decision cadence matches the multi-minute alpha horizon, the strategy is positive at IBKR commission". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -225,6 +225,15 @@ struct Cli {
|
||||
eps_end: f32,
|
||||
#[arg(long, default_value_t = 0.99)]
|
||||
gamma: f32,
|
||||
|
||||
/// Decision stride — emit a new action only every N steps; between
|
||||
/// decisions force `action=0` (wait), preserving the previously-opened
|
||||
/// position. Aligns DQN decision cadence with the multi-minute alpha
|
||||
/// horizon and stops the policy from flip-flopping on per-bar noise
|
||||
/// ("coin-flip problem"). 1 = current per-bar behaviour. Pair with
|
||||
/// γ→0.999+ so the Bellman chain reaches across the wait segments.
|
||||
#[arg(long, default_value_t = 1)]
|
||||
decision_stride: u32,
|
||||
#[arg(long, default_value_t = 0.9)]
|
||||
alpha_m: f32,
|
||||
#[arg(long, default_value_t = 0.03)]
|
||||
@@ -651,7 +660,16 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
stream.synchronize()?;
|
||||
let actions = batched_action_pinned_train.read_all();
|
||||
// `--decision-stride N`: only emit a new action every N steps;
|
||||
// between strides force action=0 (wait) so an open position is
|
||||
// held rather than re-decided per bar. Avoids the per-bar
|
||||
// "coin-flip" overtrading. step==0 is always a decision.
|
||||
let stride_train = cli.decision_stride.max(1) as usize;
|
||||
let actions = if step % stride_train == 0 {
|
||||
batched_action_pinned_train.read_all()
|
||||
} else {
|
||||
vec![0_i32; train_n_par]
|
||||
};
|
||||
|
||||
for i in 0..train_n_par {
|
||||
if done_flags[i] { continue; }
|
||||
@@ -901,7 +919,7 @@ fn main() -> Result<()> {
|
||||
.context("reset vol_obs min slot")?;
|
||||
}
|
||||
// Lockstep step loop.
|
||||
for _step in 0..cli.horizon {
|
||||
for step in 0..cli.horizon {
|
||||
// Gather current states (CPU; <100μs for N=500).
|
||||
let mut batched_states_host = vec![0.0_f32; n_par * STATE_DIM];
|
||||
for i in 0..n_par {
|
||||
@@ -993,7 +1011,17 @@ fn main() -> Result<()> {
|
||||
}
|
||||
// ONE sync per step (instead of N).
|
||||
stream.synchronize()?;
|
||||
let actions = batched_action_pinned.read_all();
|
||||
// `--decision-stride N`: same rate-limit semantics as training.
|
||||
// Between strides force action=0 so an open passive limit
|
||||
// post / live position is held rather than re-decided. Cuts
|
||||
// per-bar trade counts ~stride× and removes the coin-flip
|
||||
// overtrading on noise.
|
||||
let stride_eval = cli.decision_stride.max(1) as usize;
|
||||
let actions = if step % stride_eval == 0 {
|
||||
batched_action_pinned.read_all()
|
||||
} else {
|
||||
vec![0_i32; n_par]
|
||||
};
|
||||
// Step all envs on CPU.
|
||||
for i in 0..n_par {
|
||||
if done_flags[i] { continue; }
|
||||
|
||||
Reference in New Issue
Block a user