feat(reward-v7): implement all gems & pearls
Layers 6-9 + label smoothing + adaptive CEA warmup: - Urgency branch attribution (fill price improvement) - Exit timing quality (market reversal after exit) - OFI-weighted reward confidence (amplify informed trades) - Kelly-optimal position sizing signal - Reward label smoothing (deterministic jitter) - Adaptive CEA warmup (1.0 for first 3 epochs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,11 @@ q_gap_threshold = [0.0, 0.5] # 0.0=trade every bar, 0.5=high conviction
|
||||
cea_weight = [0.1, 1.0]
|
||||
order_credit_weight = [0.0, 0.5]
|
||||
risk_efficiency_weight = [0.0, 0.5]
|
||||
urgency_credit_weight = [0.0, 0.5]
|
||||
exit_timing_weight = [0.0, 0.2]
|
||||
ofi_reward_weight = [0.0, 0.5]
|
||||
kelly_sizing_weight = [0.0, 0.5]
|
||||
reward_noise_scale = [0.01, 0.1]
|
||||
|
||||
[experience]
|
||||
initial_capital = 100000.0
|
||||
@@ -114,6 +119,11 @@ q_gap_threshold = 0.1
|
||||
cea_weight = 0.3
|
||||
order_credit_weight = 0.1
|
||||
risk_efficiency_weight = 0.1
|
||||
urgency_credit_weight = 0.1
|
||||
exit_timing_weight = 0.05
|
||||
ofi_reward_weight = 0.2
|
||||
kelly_sizing_weight = 0.1
|
||||
reward_noise_scale = 0.05
|
||||
|
||||
[fixed]
|
||||
|
||||
@@ -145,3 +155,8 @@ q_gap_threshold = 0.1
|
||||
cea_weight = 0.3
|
||||
order_credit_weight = 0.1
|
||||
risk_efficiency_weight = 0.1
|
||||
urgency_credit_weight = 0.1
|
||||
exit_timing_weight = 0.05
|
||||
ofi_reward_weight = 0.2
|
||||
kelly_sizing_weight = 0.1
|
||||
reward_noise_scale = 0.05
|
||||
|
||||
@@ -120,3 +120,8 @@ dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
order_credit_weight = 0.1
|
||||
risk_efficiency_weight = 0.1
|
||||
urgency_credit_weight = 0.1
|
||||
exit_timing_weight = 0.05
|
||||
ofi_reward_weight = 0.2
|
||||
kelly_sizing_weight = 0.1
|
||||
reward_noise_scale = 0.05
|
||||
|
||||
@@ -131,3 +131,8 @@ dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
order_credit_weight = 0.1
|
||||
risk_efficiency_weight = 0.1
|
||||
urgency_credit_weight = 0.1
|
||||
exit_timing_weight = 0.05
|
||||
ofi_reward_weight = 0.2
|
||||
kelly_sizing_weight = 0.1
|
||||
reward_noise_scale = 0.05
|
||||
|
||||
@@ -127,6 +127,11 @@ dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
order_credit_weight = 0.1
|
||||
risk_efficiency_weight = 0.1
|
||||
urgency_credit_weight = 0.1
|
||||
exit_timing_weight = 0.05
|
||||
ofi_reward_weight = 0.2
|
||||
kelly_sizing_weight = 0.1
|
||||
reward_noise_scale = 0.05
|
||||
|
||||
[walk_forward]
|
||||
step_fraction = 0.25
|
||||
|
||||
@@ -866,6 +866,11 @@ extern "C" __global__ void experience_env_step(
|
||||
* at episode end (done=1) and added to final reward. */
|
||||
float* __restrict__ position_histogram,
|
||||
float position_entropy_weight, /* #19: reward += weight * H(histogram). 0=disabled. */
|
||||
float urgency_credit_weight, /* v7 gem: urgency branch attribution */
|
||||
float exit_timing_weight, /* v7 gem: exit timing quality */
|
||||
float ofi_reward_weight, /* v7 gem: OFI-weighted reward confidence */
|
||||
float kelly_sizing_weight, /* v7 gem: Kelly-optimal sizing signal */
|
||||
float reward_noise_scale, /* v7 gem: reward label smoothing noise */
|
||||
/* #33 Per-episode saboteur params [N, 3]: (spread_mult, fill_prob, slippage_mult).
|
||||
* NULL = disabled (use global scalars). When non-NULL, overrides
|
||||
* spread_cost, fill_ioc_fill_prob, tx_cost_multiplier per episode. */
|
||||
@@ -1307,6 +1312,61 @@ extern "C" __global__ void experience_env_step(
|
||||
float risk_eff = segment_return / fabsf(intra_trade_max_dd);
|
||||
reward += risk_efficiency_weight * fminf(risk_eff, 5.0f);
|
||||
}
|
||||
|
||||
/* ── Layer 6: Urgency branch attribution (fill quality) ── */
|
||||
if (urgency_credit_weight > 0.0f && fabsf(position) > 0.001f && entry_price > 0.0f) {
|
||||
float mid_price = (raw_close + raw_next) * 0.5f;
|
||||
float fill_improvement = (position > 0.0f)
|
||||
? (mid_price - entry_price) / fmaxf(entry_price, 1.0f)
|
||||
: (entry_price - mid_price) / fmaxf(entry_price, 1.0f);
|
||||
float urgency_credit = fmaxf(fill_improvement / vol_proxy, 0.0f);
|
||||
reward += urgency_credit_weight * fminf(urgency_credit, 2.0f);
|
||||
}
|
||||
|
||||
/* ── Layer 7: Exit timing quality ── */
|
||||
if (exit_timing_weight > 0.0f && (exiting_trade || reversing_trade)) {
|
||||
float post_exit_move = (prev_sign > 0)
|
||||
? (raw_next - raw_close) / fmaxf(raw_close, 1.0f)
|
||||
: (raw_close - raw_next) / fmaxf(raw_close, 1.0f);
|
||||
float timing_quality = -post_exit_move / vol_proxy;
|
||||
reward += exit_timing_weight * asymmetric_soft_clamp(timing_quality * 10.0f) * 0.1f;
|
||||
}
|
||||
|
||||
/* ── Layer 8: OFI-weighted reward confidence ── */
|
||||
if (ofi_reward_weight > 0.0f) {
|
||||
float price_move = (raw_next - raw_close) / fmaxf(raw_close, 1.0f);
|
||||
float move_magnitude = fabsf(price_move) / vol_proxy;
|
||||
float alignment = 0.0f;
|
||||
if (position > 0.001f && price_move > 0.0f) alignment = 1.0f;
|
||||
if (position < -0.001f && price_move < 0.0f) alignment = 1.0f;
|
||||
float confidence_mult = alignment * fminf(move_magnitude, 1.0f);
|
||||
reward *= (1.0f + ofi_reward_weight * confidence_mult);
|
||||
}
|
||||
|
||||
/* ── Layer 9: Kelly-optimal position sizing signal ── */
|
||||
if (kelly_sizing_weight > 0.0f && (win_count + loss_count) >= 5.0f) {
|
||||
float total_trades = win_count + loss_count;
|
||||
float win_rate_k = win_count / total_trades;
|
||||
float avg_win = (win_count > 0.0f) ? (sum_wins / win_count) : 0.001f;
|
||||
float avg_loss = (loss_count > 0.0f) ? (sum_losses / loss_count) : 0.001f;
|
||||
float payoff_ratio = avg_win / fmaxf(avg_loss, 0.0001f);
|
||||
float kelly_f = (payoff_ratio * win_rate_k - (1.0f - win_rate_k)) / fmaxf(payoff_ratio, 0.0001f);
|
||||
kelly_f = fmaxf(fminf(kelly_f, 1.0f), 0.0f);
|
||||
float actual_fraction = fabsf(position) / fmaxf(max_position, 1.0f);
|
||||
float kelly_closeness = 1.0f - fabsf(actual_fraction - kelly_f);
|
||||
kelly_closeness = fmaxf(kelly_closeness, 0.0f);
|
||||
if (segment_return > 0.0f) {
|
||||
reward += kelly_sizing_weight * kelly_closeness * 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reward label smoothing (deterministic jitter, all non-zero rewards) ── */
|
||||
if (reward_noise_scale > 0.0f && reward != 0.0f) {
|
||||
unsigned int hash = (unsigned int)(i * 31337 + bar_idx * 7919 + current_t * 1013);
|
||||
hash ^= (hash >> 16); hash *= 0x45d9f3bu; hash ^= (hash >> 16);
|
||||
float pseudo_noise = ((float)(hash & 0xFFFF) / 65535.0f - 0.5f) * 2.0f;
|
||||
reward += reward_noise_scale * pseudo_noise;
|
||||
}
|
||||
|
||||
/* ---- Drawdown penalty (from trade_physics.cuh) ---- */
|
||||
|
||||
@@ -193,6 +193,11 @@ pub struct ExperienceCollectorConfig {
|
||||
pub order_credit_weight: f32,
|
||||
/// v7: Intra-trade risk efficiency weight (0.0 = disabled, 0.5 = moderate)
|
||||
pub risk_efficiency_weight: f32,
|
||||
pub urgency_credit_weight: f32,
|
||||
pub exit_timing_weight: f32,
|
||||
pub ofi_reward_weight: f32,
|
||||
pub kelly_sizing_weight: f32,
|
||||
pub reward_noise_scale: f32,
|
||||
/// Drawdown fraction before penalty starts (0.02 = 2%)
|
||||
pub dd_threshold: f32,
|
||||
/// Drawdown penalty weight (1.0 = full penalty)
|
||||
@@ -319,6 +324,11 @@ impl Default for ExperienceCollectorConfig {
|
||||
cea_weight: 0.0,
|
||||
order_credit_weight: 0.0,
|
||||
risk_efficiency_weight: 0.0,
|
||||
urgency_credit_weight: 0.0,
|
||||
exit_timing_weight: 0.0,
|
||||
ofi_reward_weight: 0.0,
|
||||
kelly_sizing_weight: 0.0,
|
||||
reward_noise_scale: 0.0,
|
||||
dd_threshold: 0.02,
|
||||
w_dd: 1.0,
|
||||
beta_penalty: 0.0,
|
||||
@@ -1841,6 +1851,11 @@ impl GpuExperienceCollector {
|
||||
.arg(&mirror_i32) // #10 mirror universe
|
||||
.arg(&mut self.position_histogram) // #19 position entropy histogram
|
||||
.arg(&config.position_entropy_weight) // #19 position entropy weight
|
||||
.arg(&config.urgency_credit_weight)
|
||||
.arg(&config.exit_timing_weight)
|
||||
.arg(&config.ofi_reward_weight)
|
||||
.arg(&config.kelly_sizing_weight)
|
||||
.arg(&config.reward_noise_scale)
|
||||
// #33 Per-episode saboteur params (0 = NULL = disabled)
|
||||
.arg(&{
|
||||
if self.saboteur_active {
|
||||
|
||||
@@ -2181,6 +2181,10 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
info!(" [v7] cea_weight: {:.3} (TOML base, scaled by loss_shaping_intensity={:.3})", hyperparams.cea_weight, params.loss_shaping_intensity);
|
||||
info!(" [v7] order_credit_weight: {:.3} (TOML base, scaled by loss_shaping_intensity={:.3})", hyperparams.order_credit_weight, params.loss_shaping_intensity);
|
||||
info!(" [v7] risk_efficiency_weight: {:.3} (TOML base, scaled by loss_shaping_intensity={:.3})", hyperparams.risk_efficiency_weight, params.loss_shaping_intensity);
|
||||
info!(" [v7 gems] urgency={:.3} timing={:.3} ofi={:.3} kelly={:.3} noise={:.3}",
|
||||
hyperparams.urgency_credit_weight, hyperparams.exit_timing_weight,
|
||||
hyperparams.ofi_reward_weight, hyperparams.kelly_sizing_weight,
|
||||
hyperparams.reward_noise_scale);
|
||||
|
||||
// Derive max_position_absolute from max_leverage using median close price.
|
||||
// This replaces the old hardcoded contract-count cap with a leverage-based
|
||||
|
||||
@@ -785,6 +785,16 @@ pub struct DQNHyperparameters {
|
||||
/// Reward v7: Intra-trade risk efficiency weight.
|
||||
/// Rewards clean winning trades with minimal intra-trade drawdown.
|
||||
pub risk_efficiency_weight: f64,
|
||||
/// Reward v7 gem: Urgency branch attribution weight.
|
||||
pub urgency_credit_weight: f64,
|
||||
/// Reward v7 gem: Exit timing quality weight.
|
||||
pub exit_timing_weight: f64,
|
||||
/// Reward v7 gem: OFI-weighted reward confidence multiplier.
|
||||
pub ofi_reward_weight: f64,
|
||||
/// Reward v7 gem: Kelly-optimal sizing signal weight.
|
||||
pub kelly_sizing_weight: f64,
|
||||
/// Reward v7 gem: Reward label smoothing noise scale.
|
||||
pub reward_noise_scale: f64,
|
||||
/// Position staleness rent per step held
|
||||
pub time_decay_rate: f64,
|
||||
/// Q-value gap threshold for trade conviction filter.
|
||||
@@ -1503,6 +1513,11 @@ impl DQNHyperparameters {
|
||||
self.cea_weight = (self.cea_weight * li).clamp(0.0, 2.0);
|
||||
self.order_credit_weight = (self.order_credit_weight * li).clamp(0.0, 1.0);
|
||||
self.risk_efficiency_weight = (self.risk_efficiency_weight * li).clamp(0.0, 1.0);
|
||||
self.urgency_credit_weight = (self.urgency_credit_weight * li).clamp(0.0, 1.0);
|
||||
self.exit_timing_weight = (self.exit_timing_weight * li).clamp(0.0, 0.5);
|
||||
self.ofi_reward_weight = (self.ofi_reward_weight * li).clamp(0.0, 1.0);
|
||||
self.kelly_sizing_weight = (self.kelly_sizing_weight * li).clamp(0.0, 1.0);
|
||||
self.reward_noise_scale = (self.reward_noise_scale * li).clamp(0.0, 0.2);
|
||||
self.position_entropy_weight *= li;
|
||||
// v6 legacy (zeroed defaults, no-op)
|
||||
self.asymmetric_dd_weight *= li;
|
||||
@@ -1611,6 +1626,11 @@ impl DQNHyperparameters {
|
||||
cea_weight: 0.3,
|
||||
order_credit_weight: 0.1,
|
||||
risk_efficiency_weight: 0.1,
|
||||
urgency_credit_weight: 0.1,
|
||||
exit_timing_weight: 0.05,
|
||||
ofi_reward_weight: 0.2,
|
||||
kelly_sizing_weight: 0.1,
|
||||
reward_noise_scale: 0.05,
|
||||
pruning_epoch: 50, // #20: compute mask at epoch 50
|
||||
pruning_fraction: 0.7, // #20: prune 70% of smallest weights
|
||||
bottleneck_dim: 2, // #31: 2D information compression (gem of gems)
|
||||
|
||||
@@ -942,9 +942,17 @@ impl DQNTrainer {
|
||||
enable_action_masking: self.enable_action_masking,
|
||||
curiosity_scale: 1.0,
|
||||
loss_aversion: self.hyperparams.loss_aversion as f32,
|
||||
cea_weight: self.hyperparams.cea_weight as f32,
|
||||
cea_weight: {
|
||||
let base = self.hyperparams.cea_weight as f32;
|
||||
if self.current_epoch < 3 { 1.0_f32.max(base) } else { base }
|
||||
},
|
||||
order_credit_weight: self.hyperparams.order_credit_weight as f32,
|
||||
risk_efficiency_weight: self.hyperparams.risk_efficiency_weight as f32,
|
||||
urgency_credit_weight: self.hyperparams.urgency_credit_weight as f32,
|
||||
exit_timing_weight: self.hyperparams.exit_timing_weight as f32,
|
||||
ofi_reward_weight: self.hyperparams.ofi_reward_weight as f32,
|
||||
kelly_sizing_weight: self.hyperparams.kelly_sizing_weight as f32,
|
||||
reward_noise_scale: self.hyperparams.reward_noise_scale as f32,
|
||||
tx_cost_multiplier: {
|
||||
let base = self.hyperparams.transaction_cost_multiplier as f32;
|
||||
if adversarial { base * 2.0 } else { base }
|
||||
|
||||
Reference in New Issue
Block a user