feat(plan): plan activation + enforcement + direction lock

Flat->Positioned copies plan_params to ps[23-29]. Auto-exit on
profit_target (ISV*asymmetry), stop_loss (ISV-modulated), target_bars,
regime_stability<0.3. Scale schedule via scale_aggression. Conviction
sizing. Direction locked during active plan. Wired trainer->collector.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-17 01:24:20 +02:00
parent 6cf09e0806
commit b6314ceec1
3 changed files with 79 additions and 1 deletions

View File

@@ -987,6 +987,17 @@ extern "C" __global__ void experience_action_select(
}
}
/* Plan direction lock: during active plan, force current direction */
if (portfolio_states != NULL) {
int ps_base_plan = i * PORTFOLIO_STRIDE;
int has_plan_active = (portfolio_states[ps_base_plan + 23] > 0.5f);
if (has_plan_active) {
float cur_pos_plan = portfolio_states[ps_base_plan + 0];
if (cur_pos_plan > 0.001f) dir_idx = 2; /* Long locked */
else if (cur_pos_plan < -0.001f) dir_idx = 0; /* Short locked */
}
}
/* Compose factored action: 4-branch encoding
* action = dir * (b1*b2*b3) + mag * (b2*b3) + order * b3 + urgency */
int action_idx = dir_idx * b1_size * b2_size * b3_size
@@ -1099,7 +1110,8 @@ extern "C" __global__ void experience_env_step(
int total_actions_for_var, /* b0+b1+b2+b3 to index q_variance */
const float* __restrict__ cost_anneal_ptr, /* [1] pinned device-mapped: 0.0=no costs, 1.0=full costs */
const float* __restrict__ commit_lambda_buf, /* [N] per-sample commitment lambda. NULL = use 0.01. */
const float* __restrict__ isv_signals_ptr /* [8] pinned device-mapped ISV signals. NULL = static hold. */
const float* __restrict__ isv_signals_ptr, /* [8] pinned device-mapped ISV signals. NULL = static hold. */
const float* __restrict__ plan_params_ptr /* [N, 6] trade plan params. NULL = no plan. */
) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= N) return;
@@ -1427,6 +1439,51 @@ extern "C" __global__ void experience_env_step(
tx_cost_multiplier, spread_cost, max_position,
order_type_idx, spread_scale);
/* ── Trade Plan: activation on Flat → Positioned ── */
int was_flat_plan = (fabsf(ps0_f) < 0.001f);
int now_positioned_plan = (fabsf(position) > 0.001f);
if (was_flat_plan && now_positioned_plan && plan_params_ptr != NULL) {
const float* pp = plan_params_ptr + i * 6;
ps[23] = pp[0]; /* target_bars */
ps[24] = pp[1]; /* profit_target */
ps[25] = pp[2]; /* stop_loss */
ps[26] = pp[3]; /* scale_aggression */
ps[27] = pp[4]; /* conviction */
ps[28] = pp[5]; /* asymmetry */
/* Apply conviction to position size */
position *= fmaxf(ps[27], 0.1f);
}
/* ── Trade Plan: enforcement (auto-exit conditions) ── */
int has_plan = (ps[23] > 0.5f);
if (has_plan && fabsf(position) > 0.001f) {
float pnl_pct = (raw_close - entry_price) / fmaxf(fabsf(entry_price), 1.0f)
* ((position > 0.0f) ? 1.0f : -1.0f);
/* ISV-modulated thresholds */
float stability = (isv_signals_ptr != NULL) ? isv_signals_ptr[11] : 1.0f;
float eff_profit = ps[24] * ps[28] * (0.5f + 0.5f * stability);
float eff_stop = ps[25] * fmaxf(stability, 0.5f);
int plan_exit = 0;
if (pnl_pct >= eff_profit) plan_exit = 1;
if (pnl_pct <= -eff_stop) plan_exit = 1;
if (hold_time >= ps[23]) plan_exit = 1;
if (stability < 0.3f && hold_time > 3.0f) plan_exit = 1;
/* Scale schedule: ramp over first 3 bars */
if (hold_time < 3.0f && !plan_exit) {
float scale = ps[26] + (1.0f - ps[26]) * hold_time / 3.0f;
position *= fminf(scale, 1.0f);
}
if (plan_exit) {
position = 0.0f;
for (int s = 23; s <= 29; s++) ps[s] = 0.0f;
}
}
/* ---- Mark-to-market PnL for this timestep ---- */
/* Per-bar mark-to-market P&L removed: used raw_next (future price),
* creating 1-bar action-reward misalignment. Rewards are now

View File

@@ -678,6 +678,10 @@ pub struct GpuExperienceCollector {
/// Set via set_isv_signals_ptr(). 0 = NULL (static hold).
isv_signals_dev_ptr: u64,
/// Trade plan params dev_ptr from fused training context.
/// Set via set_plan_params_ptr(). 0 = NULL (no plan).
plan_params_dev_ptr: u64,
/// v8: Per-bar curriculum difficulty scoring kernel.
difficulty_scores_kernel: CudaFunction,
/// v8: Hindsight experience relabeling kernel (optimal exit).
@@ -1203,6 +1207,7 @@ impl GpuExperienceCollector {
cost_anneal_pinned,
cost_anneal_dev_ptr,
isv_signals_dev_ptr: 0, // NULL until trainer sets it
plan_params_dev_ptr: 0, // NULL until trainer sets it
difficulty_scores_kernel,
hindsight_relabel_kernel,
td_lambda_kernel,
@@ -1227,6 +1232,13 @@ impl GpuExperienceCollector {
self.isv_signals_dev_ptr = dev_ptr;
}
/// Set trade plan params device pointer for plan activation/enforcement.
/// The env_step kernel reads plan params to copy into portfolio state on
/// Flat→Positioned transitions. Pass 0 to disable (NULL = no plan).
pub fn set_plan_params_ptr(&mut self, dev_ptr: u64) {
self.plan_params_dev_ptr = dev_ptr;
}
/// Set CVaR position scaling from IQN dual-head.
/// The device pointer will be passed to the env_step kernel.
/// Call with 0 to disable (NULL pointer = no scaling).
@@ -2323,6 +2335,7 @@ impl GpuExperienceCollector {
.arg(&self.cost_anneal_dev_ptr) // G2: transaction cost curriculum
.arg(&0u64) // commit_lambda_buf: NULL = use 0.01 fallback (risk branch not in experience path yet)
.arg(&self.isv_signals_dev_ptr) // ISV signals for adaptive hold (0 = NULL = static hold)
.arg(&self.plan_params_dev_ptr) // trade plan params (0 = NULL = no plan)
.launch(launch_cfg)
.map_err(|e| MLError::ModelError(format!(
"experience_env_step t={t}: {e}"

View File

@@ -526,6 +526,14 @@ impl DQNTrainer {
}
}
// Trade plan params for plan activation/enforcement
if let Some(ref fused) = self.fused_ctx {
let plan_ptr = fused.plan_params_buf_ptr();
if let Some(ref mut collector) = self.gpu_experience_collector {
collector.set_plan_params_ptr(plan_ptr);
}
}
// IQR exploration bonus from IQN quantile spread
if let Some(ref fused) = self.fused_ctx {
let iqr_ptr = fused.iqn_iqr_ptr().unwrap_or(0);