fix(ml): wire spread_cost_bps into RL training — commission + spread = total tx cost

Previously train_baseline_rl.rs only passed commission (tx_cost_bps) to
DQN/PPO trainers, ignoring bid-ask spread slippage. Now computes per-fold
average spread via spread_cost_bps() (same as evaluate_baseline) and passes
total cost (commission + spread) to both trainers.

Removes #[allow(dead_code)] — function is now used by all 4 example binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-01 22:10:43 +01:00
parent f975c9cf71
commit 533249eb91
2 changed files with 29 additions and 4 deletions

View File

@@ -193,7 +193,6 @@ pub fn list_subdirs(dir: &Path) -> Vec<String> {
/// Uses the bid-ask spread model from `backtesting/src/slippage.rs`:
/// half-spread = `tick_size` * `spread_ticks` / 2. Round-trip pays the full spread.
/// Converted to bps: `spread_price` / price * `10_000`.
#[allow(dead_code)] // Used by evaluate_baseline, train_baseline_supervised — not all examples
pub fn spread_cost_bps(price: f64, tick_size: f64, spread_ticks: f64) -> f64 {
if price.abs() < 1e-10 {
return 0.0;

View File

@@ -34,7 +34,7 @@ use ml::trainers::ppo::{PpoHyperparameters, PpoTrainer};
#[allow(unreachable_pub)]
mod baseline_common;
use baseline_common::completion::{write_failure_marker, write_success_marker, CompletionMetrics};
use baseline_common::load_all_bars;
use baseline_common::{load_all_bars, spread_cost_bps};
use common::metrics::{server as metrics_server, training_metrics as metrics};
use ml::features::extraction::extract_ml_features;
use ml::types::OHLCVBar;
@@ -292,9 +292,22 @@ fn train_dqn_fold(
);
info!(" [DQN] Fold {} -- {} train, {} val features", fold, train_features.len(), val_features.len());
// Compute average spread slippage in bps from training bar prices.
// Same pattern as evaluate_baseline: total_cost = commission + spread.
let avg_price = if train_bars.is_empty() {
0.0
} else {
train_bars.iter().map(|b| b.close).sum::<f64>() / train_bars.len() as f64
};
let avg_spread_bps = spread_cost_bps(avg_price, args.tick_size, args.spread_ticks);
let total_cost_bps = args.tx_cost_bps + avg_spread_bps;
info!(" [DQN] Fold {} total tx cost: {:.2} bps (commission {:.1} + spread {:.2})",
fold, total_cost_bps, args.tx_cost_bps, avg_spread_bps);
// Build DQNHyperparameters -- hyperopt JSON overrides defaults, CLI args override both.
// `..DQNHyperparameters::default()` enables all Rainbow components (PER, dueling,
// distributional C51, noisy nets, gradient accumulation, mixed precision auto-detect).
// transaction_cost_multiplier scales internal reward cost by total trading friction.
let hyperparams = DQNHyperparameters {
learning_rate: hp_f64(hp, "learning_rate").unwrap_or(args.learning_rate),
batch_size: hp_usize(hp, "batch_size").unwrap_or(args.batch_size),
@@ -310,6 +323,7 @@ fn train_dqn_fold(
warmup_steps: 0,
// Early stopping is managed by DQNTrainer internally
early_stopping_enabled: true,
transaction_cost_multiplier: total_cost_bps,
..DQNHyperparameters::default()
};
@@ -368,7 +382,7 @@ fn train_ppo_fold(
fold: usize,
train_features: &[[f64; 51]],
_val_features: &[[f64; 51]],
_train_bars: &[OHLCVBar],
train_bars: &[OHLCVBar],
_val_bars: &[OHLCVBar],
args: &Args,
output_dir: &Path,
@@ -382,6 +396,18 @@ fn train_ppo_fold(
return Ok(f64::MAX);
}
// Compute average spread slippage in bps from training bar prices.
// Same pattern as evaluate_baseline: total_cost = commission + spread.
let avg_price = if train_bars.is_empty() {
0.0
} else {
train_bars.iter().map(|b| b.close).sum::<f64>() / train_bars.len() as f64
};
let avg_spread_bps = spread_cost_bps(avg_price, args.tick_size, args.spread_ticks);
let total_cost_bps = args.tx_cost_bps + avg_spread_bps;
info!(" [PPO] Fold {} total tx cost: {:.2} bps (commission {:.1} + spread {:.2})",
fold, total_cost_bps, args.tx_cost_bps, avg_spread_bps);
// Build PpoHyperparameters -- hyperopt JSON overrides defaults, CLI args override both.
// Start from conservative() baseline so all fields have sane values.
let hyperparams = PpoHyperparameters {
@@ -400,7 +426,7 @@ fn train_ppo_fold(
minibatch_size: hp_usize(hp, "minibatch_size").unwrap_or(64),
epochs: args.epochs,
early_stopping_enabled: true,
transaction_cost_bps: args.tx_cost_bps / 100.0, // CLI is in bps (1.0 bps), trainer wants pct (0.01)
transaction_cost_bps: total_cost_bps / 100.0, // total (commission + spread) in bps → pct for trainer
..PpoHyperparameters::conservative()
};