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:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user