feat(ml): re-enable OFI features from MBP-10 order book data
Wire 8 OFI features (OFI L1/L5, depth imbalance, VPIN, Kyle's lambda, bid/ask slopes, trade imbalance) through the DQN training pipeline: - Add mbp10_data_dir config field to DQNHyperparameters - Dynamic state_dim: 43 (no OFI) or 51 (with OFI) based on config - Compute OFI per bar during data loading, store on trainer - Pass OFI features through regime_features slot in TradingState - Configurable MBP-10 path with recursive .dbn/.dbn.zst discovery - Add zstd auto-detection to DbnParser::parse_mbp10_file() - Add --mbp10-data-dir CLI flag to train_baseline_rl - Fix hardcoded [f64; 51] → FeatureVector51 ([f64; 40]) across examples, walk_forward, GPU memory profile, and test fixtures - Fix stale state_dim=51 in dqn_config_2025() and DQN tests 2747 tests pass, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
16
config/universe-es-mbp10-test.toml
Normal file
16
config/universe-es-mbp10-test.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# ES.FUT MBP-10 — small test subset (1 quarter)
|
||||
# For local OFI pipeline validation only.
|
||||
|
||||
[universe]
|
||||
name = "es-mbp10-test"
|
||||
description = "ES.FUT MBP-10 test subset (Q2 2024)"
|
||||
date_range_start = "2024-04-01"
|
||||
date_range_end = "2024-07-01"
|
||||
bar_size = "tick"
|
||||
databento_dataset = "GLBX.MDP3"
|
||||
databento_schema = "mbp-10"
|
||||
|
||||
[[symbols]]
|
||||
symbol = "ES.FUT"
|
||||
exchange = "GLBX.MDP3"
|
||||
asset_class = "Future"
|
||||
@@ -656,9 +656,16 @@ impl DbnParser {
|
||||
let path = path.as_ref();
|
||||
info!("📖 Parsing MBP-10 file: {:?}", path);
|
||||
|
||||
// Open file and create DBN decoder
|
||||
let file = File::open(path)?; // DataError::Io is automatically converted from std::io::Error
|
||||
let reader = BufReader::new(file);
|
||||
// Open file and create DBN decoder (auto-detect .dbn.zst vs .dbn)
|
||||
let file = File::open(path)?;
|
||||
let is_zstd = path.to_string_lossy().ends_with(".dbn.zst");
|
||||
let reader: Box<dyn std::io::Read> = if is_zstd {
|
||||
Box::new(zstd::Decoder::new(BufReader::new(file)).map_err(|e| {
|
||||
DataError::InvalidFormat(format!("Failed to create zstd decoder: {}", e))
|
||||
})?)
|
||||
} else {
|
||||
Box::new(BufReader::new(file))
|
||||
};
|
||||
|
||||
let mut decoder = DbnDecoder::new(reader).map_err(|e| {
|
||||
DataError::InvalidFormat(format!("Failed to create DBN decoder: {}", e))
|
||||
|
||||
@@ -43,6 +43,7 @@ use tracing::{error, info, warn};
|
||||
use common::metrics::{server as metrics_server, training_metrics as tm};
|
||||
use ml::common::action::{ExposureLevel, FactoredAction, OrderType as ActionOrderType, Urgency};
|
||||
use ml::dqn::{DQNConfig, OrderRouter, DQN};
|
||||
use ml::trainers::dqn::FeatureVector51;
|
||||
|
||||
#[allow(unreachable_pub)]
|
||||
mod baseline_common;
|
||||
@@ -401,7 +402,7 @@ struct PortfolioState {
|
||||
/// the START of the chunk. Market features are per-bar. Returns a contiguous
|
||||
/// `Vec<f32>` of length `chunk_len * feature_dim`.
|
||||
fn build_chunk_states(
|
||||
test_features: &[[f64; 51]],
|
||||
test_features: &[FeatureVector51],
|
||||
test_bars: &[OHLCVBar],
|
||||
chunk_start: usize,
|
||||
chunk_end: usize,
|
||||
@@ -542,7 +543,7 @@ fn simulate_chunk_trades(
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn evaluate_dqn_fold(
|
||||
fold: usize,
|
||||
test_features: &[[f64; 51]],
|
||||
test_features: &[FeatureVector51],
|
||||
test_bars: &[OHLCVBar],
|
||||
models_dir: &Path,
|
||||
args: &Args,
|
||||
@@ -705,7 +706,7 @@ fn evaluate_dqn_fold(
|
||||
/// for sequential trade simulation.
|
||||
fn evaluate_ppo_fold(
|
||||
fold: usize,
|
||||
test_features: &[[f64; 51]],
|
||||
test_features: &[FeatureVector51],
|
||||
test_bars: &[OHLCVBar],
|
||||
models_dir: &Path,
|
||||
args: &Args,
|
||||
|
||||
@@ -32,6 +32,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
use common::metrics::{server as metrics_server, training_metrics as tm};
|
||||
use ml::features::extraction::extract_ml_features;
|
||||
use ml::trainers::dqn::FeatureVector51;
|
||||
use ml::training::unified_trainer::UnifiedTrainable;
|
||||
use ml::types::OHLCVBar;
|
||||
use ml::walk_forward::{generate_walk_forward_windows, NormStats, WalkForwardConfig};
|
||||
@@ -397,7 +398,7 @@ fn create_model(
|
||||
fn evaluate_fold(
|
||||
fold: usize,
|
||||
model_name: &str,
|
||||
test_features: &[[f64; 51]],
|
||||
test_features: &[FeatureVector51],
|
||||
test_bars: &[OHLCVBar],
|
||||
args: &Args,
|
||||
device: &Device,
|
||||
|
||||
@@ -30,7 +30,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
use ml::cuda_pipeline::DqnGpuData;
|
||||
use ml::prelude::Device;
|
||||
use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer};
|
||||
use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer, FeatureVector51};
|
||||
use ml::trainers::ppo::{PpoHyperparameters, PpoTrainer};
|
||||
|
||||
#[allow(unreachable_pub)]
|
||||
@@ -140,6 +140,12 @@ struct Args {
|
||||
/// model for each param set, saving as `dqn_ensemble_{k}_fold_{fold}.safetensors`.
|
||||
#[arg(long, default_value_t = 1)]
|
||||
ensemble_top_k: usize,
|
||||
|
||||
/// Optional path to MBP-10 order book data directory for OFI features.
|
||||
/// When set, enables 8 OFI features (OFI L1/L5, depth imbalance, VPIN, etc.)
|
||||
/// expanding state dimension from 43 to 51.
|
||||
#[arg(long)]
|
||||
mbp10_data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -234,7 +240,7 @@ fn hp_bool(params: &Option<Value>, key: &str) -> Option<bool> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Prepared fold data: (`train_features`, `val_features`, `train_bars`, `val_bars`)
|
||||
type FoldData = (Vec<[f64; 51]>, Vec<[f64; 51]>, Vec<OHLCVBar>, Vec<OHLCVBar>);
|
||||
type FoldData = (Vec<FeatureVector51>, Vec<FeatureVector51>, Vec<OHLCVBar>, Vec<OHLCVBar>);
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
/// Prepare a fold's data for training: extract features, normalize, align bars.
|
||||
@@ -302,14 +308,14 @@ fn prepare_fold_data(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Convert pre-aligned features and bars into the format expected by
|
||||
/// `DQNTrainer::train_with_preloaded_data`: `Vec<([f64; 51], Vec<f64>)>`.
|
||||
/// `DQNTrainer::train_with_preloaded_data`: `Vec<(FeatureVector51, Vec<f64>)>`.
|
||||
///
|
||||
/// The `Vec<f64>` target contains OHLCV prices for the trainer's internal
|
||||
/// reward computation and portfolio simulation.
|
||||
fn features_to_trainer_format(
|
||||
features: &[[f64; 51]],
|
||||
features: &[FeatureVector51],
|
||||
bars: &[OHLCVBar],
|
||||
) -> Vec<([f64; 51], Vec<f64>)> {
|
||||
) -> Vec<(FeatureVector51, Vec<f64>)> {
|
||||
features
|
||||
.iter()
|
||||
.zip(bars.iter())
|
||||
@@ -329,8 +335,8 @@ fn features_to_trainer_format(
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_arguments)]
|
||||
fn train_dqn_fold(
|
||||
fold: usize,
|
||||
train_features: &[[f64; 51]],
|
||||
val_features: &[[f64; 51]],
|
||||
train_features: &[FeatureVector51],
|
||||
val_features: &[FeatureVector51],
|
||||
train_bars: &[OHLCVBar],
|
||||
val_bars: &[OHLCVBar],
|
||||
args: &Args,
|
||||
@@ -438,6 +444,7 @@ fn train_dqn_fold(
|
||||
weight_decay: hp_f64(hp, "weight_decay").unwrap_or(1e-4),
|
||||
kelly_fractional: hp_f64(hp, "kelly_fractional").unwrap_or(0.5),
|
||||
kelly_max_fraction: hp_f64(hp, "kelly_max_fraction").unwrap_or(0.25),
|
||||
mbp10_data_dir: args.mbp10_data_dir.as_ref().map(|p| p.to_string_lossy().into_owned()),
|
||||
..DQNHyperparameters::default()
|
||||
};
|
||||
|
||||
@@ -509,8 +516,8 @@ fn train_dqn_fold(
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn train_ppo_fold(
|
||||
fold: usize,
|
||||
train_features: &[[f64; 51]],
|
||||
val_features: &[[f64; 51]],
|
||||
train_features: &[FeatureVector51],
|
||||
val_features: &[FeatureVector51],
|
||||
train_bars: &[OHLCVBar],
|
||||
_val_bars: &[OHLCVBar],
|
||||
args: &Args,
|
||||
@@ -585,7 +592,7 @@ fn train_ppo_fold(
|
||||
None, // num_envs: standard single-env collection
|
||||
).context("Failed to create PpoTrainer")?;
|
||||
|
||||
// Convert features from [f64; 51] to Vec<Vec<f32>> for the trainer
|
||||
// Convert features from FeatureVector51 to Vec<Vec<f32>> for the trainer
|
||||
let market_data: Vec<Vec<f32>> = train_features
|
||||
.iter()
|
||||
.map(|feat| feat.iter().map(|&v| v as f32).collect())
|
||||
|
||||
@@ -35,6 +35,7 @@ use serde_json::Value;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use ml::features::extraction::extract_ml_features;
|
||||
use ml::trainers::dqn::FeatureVector51;
|
||||
use ml::training::unified_trainer::UnifiedTrainable;
|
||||
use ml::types::OHLCVBar;
|
||||
use ml::walk_forward::{generate_walk_forward_windows, NormStats, WalkForwardConfig};
|
||||
@@ -400,7 +401,7 @@ fn prepare_fold_data(
|
||||
|
||||
/// Convert normalized features + bars into (input, target) tensor pairs.
|
||||
fn build_tensor_pairs(
|
||||
norm_features: &[[f64; 51]],
|
||||
norm_features: &[FeatureVector51],
|
||||
bars: &[OHLCVBar],
|
||||
bar_offset: usize,
|
||||
args: &Args,
|
||||
|
||||
@@ -93,7 +93,7 @@ pub struct DqnGpuData {
|
||||
impl DqnGpuData {
|
||||
/// Upload DQN training data to GPU as f32 tensors.
|
||||
///
|
||||
/// Converts `[f64; 51]` features and `Vec<f64>` targets to f32,
|
||||
/// Converts `[f64; 40]` features and `Vec<f64>` targets to f32,
|
||||
/// flattens into contiguous arrays, and uploads once.
|
||||
pub fn upload(
|
||||
data: &[([f64; 40], Vec<f64>)],
|
||||
|
||||
@@ -3755,16 +3755,16 @@ mod tests {
|
||||
let mut config = DQNConfig::emergency_safe_defaults();
|
||||
config.min_replay_size = 4;
|
||||
config.batch_size = 4;
|
||||
config.state_dim = 51; // Match production feature vector size (45 market + 6 portfolio)
|
||||
config.state_dim = 43; // Match production feature vector size (40 market + 3 portfolio)
|
||||
let mut dqn = DQN::new(config)?;
|
||||
|
||||
// Add enough experiences
|
||||
for i in 0..10 {
|
||||
let experience = Experience::new(
|
||||
vec![i as f32 * 0.1; 51],
|
||||
vec![i as f32 * 0.1; 43],
|
||||
(i % 3) as u8,
|
||||
i as f32,
|
||||
vec![(i + 1) as f32 * 0.1; 51],
|
||||
vec![(i + 1) as f32 * 0.1; 43],
|
||||
i == 9,
|
||||
);
|
||||
dqn.store_experience(experience)?;
|
||||
|
||||
@@ -95,7 +95,7 @@ pub mod estimates {
|
||||
activation_multiplier: 2.5,
|
||||
supports_checkpointing: false,
|
||||
default_seq_len: 128,
|
||||
default_feature_dim: 51,
|
||||
default_feature_dim: 43,
|
||||
};
|
||||
|
||||
pub const LIQUID: ModelMemoryEstimate = ModelMemoryEstimate {
|
||||
|
||||
@@ -2497,6 +2497,9 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
use_dsr: true,
|
||||
dsr_eta: params.dsr_eta,
|
||||
|
||||
// MBP-10 data directory for OFI features (None = no OFI in hyperopt)
|
||||
mbp10_data_dir: None,
|
||||
|
||||
// Mixed precision: auto-detect from GPU hardware
|
||||
mixed_precision: {
|
||||
match crate::memory_optimization::auto_batch_size::detect_gpu_memory() {
|
||||
|
||||
@@ -796,6 +796,12 @@ pub struct DQNHyperparameters {
|
||||
pub use_dsr: bool,
|
||||
/// DSR EMA decay rate (0.001=slow, 0.05=fast). Tunable in hyperopt.
|
||||
pub dsr_eta: f64,
|
||||
|
||||
// OFI (Order Flow Imbalance) features from MBP-10 data
|
||||
/// Directory containing MBP-10 .dbn.zst files for OFI feature extraction.
|
||||
/// When Some, 8 OFI features are computed and appended to the state (state_dim += 8).
|
||||
/// When None, no OFI features are used (backward compatible).
|
||||
pub mbp10_data_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for DQNHyperparameters {
|
||||
@@ -994,6 +1000,9 @@ impl DQNHyperparameters {
|
||||
// DSR: disabled by default (opt-in via hyperopt)
|
||||
use_dsr: false,
|
||||
dsr_eta: 0.01, // ~100-step effective EMA window
|
||||
|
||||
// OFI: disabled by default (requires MBP-10 data)
|
||||
mbp10_data_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1046,7 +1055,7 @@ use crate::dqn::dqn::DQNConfig;
|
||||
pub(crate) fn dqn_config_2025() -> DQNConfig {
|
||||
DQNConfig {
|
||||
// Network Architecture
|
||||
state_dim: 51, // 45 market features + 6 portfolio features (Wave 23)
|
||||
state_dim: 43, // 40 market features + 3 portfolio features
|
||||
num_actions: 5, // 5 exposure levels (Short100, Short50, Flat, Long50, Long100)
|
||||
hidden_dims: vec![512, 256, 128], // Sufficient capacity without overfitting (Trial 19)
|
||||
leaky_relu_alpha: 0.01,
|
||||
|
||||
@@ -375,20 +375,36 @@ impl DQNTrainer {
|
||||
|
||||
// WAVE 2-A2: Load MBP-10 snapshots for OFI calculation
|
||||
use data::providers::databento::dbn_parser::DbnParser;
|
||||
let mbp10_dir = Path::new("test_data/mbp10");
|
||||
let mbp10_snapshots = if mbp10_dir.exists() {
|
||||
info!("📊 Loading MBP-10 order book snapshots for OFI calculation...");
|
||||
// Load all .dbn files in the directory
|
||||
let mut all_snapshots = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(mbp10_dir) {
|
||||
let parser = DbnParser::new()
|
||||
.context("Failed to create DBN parser for MBP-10 data")?;
|
||||
let mbp10_dir_path = self.hyperparams.mbp10_data_dir.as_ref().map(Path::new);
|
||||
let mbp10_snapshots = if let Some(mbp10_dir) = mbp10_dir_path {
|
||||
if mbp10_dir.exists() {
|
||||
info!("📊 Loading MBP-10 order book snapshots from {:?}...", mbp10_dir);
|
||||
let mut all_snapshots = Vec::new();
|
||||
// Recursive file discovery (data may be in symbol subdirectories)
|
||||
fn collect_dbn_files(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_dbn_files(&path, out);
|
||||
} else {
|
||||
let name = path.to_string_lossy();
|
||||
if name.ends_with(".dbn") || name.ends_with(".dbn.zst") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut dbn_files = Vec::new();
|
||||
collect_dbn_files(mbp10_dir, &mut dbn_files);
|
||||
dbn_files.sort();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
// Only load .dbn files (not .zst compressed files)
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("dbn") {
|
||||
match parser.parse_mbp10_file(&path).await {
|
||||
if !dbn_files.is_empty() {
|
||||
let parser = DbnParser::new()
|
||||
.context("Failed to create DBN parser for MBP-10 data")?;
|
||||
for path in &dbn_files {
|
||||
match parser.parse_mbp10_file(path).await {
|
||||
Ok(mut snaps) => {
|
||||
info!(" ✅ Loaded {} snapshots from {:?}", snaps.len(), path.file_name());
|
||||
all_snapshots.append(&mut snaps);
|
||||
@@ -399,31 +415,73 @@ impl DQNTrainer {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if all_snapshots.is_empty() {
|
||||
warn!("⚠️ No MBP-10 snapshots loaded. OFI features will be zeros.");
|
||||
None
|
||||
if all_snapshots.is_empty() {
|
||||
warn!("⚠️ No MBP-10 snapshots loaded. OFI features will be zeros.");
|
||||
None
|
||||
} else {
|
||||
all_snapshots.sort_by_key(|s| s.timestamp);
|
||||
info!("✅ Total MBP-10 snapshots loaded: {} (sorted by timestamp)", all_snapshots.len());
|
||||
Some(all_snapshots)
|
||||
}
|
||||
} else {
|
||||
// Sort snapshots by timestamp for efficient lookup
|
||||
all_snapshots.sort_by_key(|s| s.timestamp);
|
||||
info!("✅ Total MBP-10 snapshots loaded: {} (sorted by timestamp)", all_snapshots.len());
|
||||
Some(all_snapshots)
|
||||
warn!("⚠️ MBP-10 directory not found at {:?}. OFI features disabled.", mbp10_dir);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
warn!("⚠️ MBP-10 directory not found at {:?}. OFI features will be zeros.", mbp10_dir);
|
||||
info!("ℹ️ No MBP-10 data directory configured. OFI features disabled.");
|
||||
None
|
||||
};
|
||||
|
||||
// Extract 54-feature vectors (technical indicators, OFI, time, statistical features)
|
||||
info!("Extracting 51-feature vectors from OHLCV bars (51-feature architecture)...");
|
||||
// Extract 40-feature vectors (technical indicators, time, statistical features)
|
||||
info!("Extracting 40-feature vectors from OHLCV bars...");
|
||||
let feature_vectors = self.extract_full_features(&all_ohlcv_bars, mbp10_snapshots.as_deref())?;
|
||||
|
||||
info!(
|
||||
"Extracted {} feature vectors (51 dimensions: technical indicators, time, statistical features (Proxy OFI removed))",
|
||||
"Extracted {} feature vectors (40 dimensions: technical indicators, time, statistical features)",
|
||||
feature_vectors.len()
|
||||
);
|
||||
|
||||
// Compute OFI features from MBP-10 snapshots (8 features per bar)
|
||||
if let Some(ref snapshots) = mbp10_snapshots {
|
||||
use crate::features::ofi_calculator::OFICalculator;
|
||||
use crate::features::mbp10_loader::get_snapshots_for_timestamp;
|
||||
|
||||
info!("📊 Computing OFI features from {} MBP-10 snapshots...", snapshots.len());
|
||||
let mut ofi_calculator = OFICalculator::new();
|
||||
let mut ofi_per_bar = Vec::with_capacity(feature_vectors.len());
|
||||
|
||||
const WARMUP: usize = 50;
|
||||
for i in 0..feature_vectors.len() {
|
||||
let bar = &all_ohlcv_bars[i + WARMUP];
|
||||
let bar_ts = bar.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64;
|
||||
|
||||
// Find the nearest MBP-10 snapshot for this bar's timestamp
|
||||
let window = get_snapshots_for_timestamp(snapshots, bar_ts, 1);
|
||||
if let Some(snap) = window.first() {
|
||||
match ofi_calculator.calculate(snap) {
|
||||
Ok(features) => {
|
||||
let arr = features.to_array();
|
||||
if features.is_valid() {
|
||||
ofi_per_bar.push(arr);
|
||||
} else {
|
||||
ofi_per_bar.push([0.0; 8]);
|
||||
}
|
||||
}
|
||||
Err(_) => ofi_per_bar.push([0.0; 8]),
|
||||
}
|
||||
} else {
|
||||
ofi_per_bar.push([0.0; 8]);
|
||||
}
|
||||
}
|
||||
|
||||
let non_zero = ofi_per_bar.iter().filter(|f| f.iter().any(|&v| v != 0.0)).count();
|
||||
info!("✅ OFI features computed: {} total, {} non-zero ({:.1}%)",
|
||||
ofi_per_bar.len(), non_zero,
|
||||
if ofi_per_bar.is_empty() { 0.0 } else { non_zero as f64 / ofi_per_bar.len() as f64 * 100.0 });
|
||||
self.ofi_features = Some(ofi_per_bar);
|
||||
}
|
||||
|
||||
// Create training data pairs (features, target)
|
||||
// Target: [preprocessed_current, preprocessed_next, raw_current, raw_next]
|
||||
// WAVE 3 BUG FIX: Include raw prices for triple barrier tracker (needs actual market prices in cents)
|
||||
@@ -547,13 +605,12 @@ impl DQNTrainer {
|
||||
all_ohlcv_bars.sort_by_key(|bar| bar.timestamp);
|
||||
debug!("Bars sorted successfully");
|
||||
|
||||
// Extract 54-feature vectors (technical indicators, Proxy OFI, time, statistical features)
|
||||
// Note: DBN loader does not load MBP-10 data, so OFI features will be zeros
|
||||
info!("Extracting 51-feature vectors from OHLCV bars (51-feature architecture)...");
|
||||
// Extract 40-feature vectors (technical indicators, time, statistical features)
|
||||
info!("Extracting 40-feature vectors from OHLCV bars...");
|
||||
let feature_vectors = self.extract_full_features(&all_ohlcv_bars, None)?;
|
||||
|
||||
info!(
|
||||
"Extracted {} feature vectors (51 dimensions: technical indicators, time, statistical features (Proxy OFI removed))",
|
||||
"Extracted {} feature vectors (40 dimensions: technical indicators, time, statistical features)",
|
||||
feature_vectors.len()
|
||||
);
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ pub struct DQNTrainer {
|
||||
|
||||
/// Multi-GPU configuration for data-parallel training (None = single GPU)
|
||||
multi_gpu: Option<crate::cuda_pipeline::multi_gpu::MultiGpuConfig>,
|
||||
|
||||
/// Pre-computed OFI features per bar (indexed by training data position)
|
||||
/// Populated during data loading when MBP-10 order book data is available.
|
||||
/// Passed as `regime_features` in `TradingState::from_normalized()`.
|
||||
pub(crate) ofi_features: Option<Vec<[f64; 8]>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DQNTrainer {
|
||||
@@ -383,8 +388,11 @@ impl DQNTrainer {
|
||||
// Create DQN configuration
|
||||
// 40-feature architecture: OHLCV, technical, patterns, volume, time, statistical
|
||||
// Portfolio features (3) are populated via PortfolioTracker → 43 total state_dim
|
||||
// With MBP-10 OFI features: +8 regime features → 51 total
|
||||
let ofi_enabled = hyperparams.mbp10_data_dir.is_some();
|
||||
let state_dim = if ofi_enabled { 51 } else { 43 };
|
||||
let config = DQNConfig {
|
||||
state_dim: 43, // 43-feature vectors: 40 market features + 3 portfolio
|
||||
state_dim,
|
||||
num_actions: 5, // 5 exposure levels (Short100, Short50, Flat, Long50, Long100)
|
||||
hidden_dims,
|
||||
learning_rate: hyperparams.learning_rate,
|
||||
@@ -863,6 +871,9 @@ impl DQNTrainer {
|
||||
|
||||
// Multi-GPU: auto-detected data parallelism
|
||||
multi_gpu,
|
||||
|
||||
// OFI features: populated during data loading when MBP-10 data is available
|
||||
ofi_features: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1890,7 +1901,7 @@ impl DQNTrainer {
|
||||
};
|
||||
let close_price = rust_decimal::Decimal::try_from(current_close)
|
||||
.unwrap_or(rust_decimal::Decimal::ZERO);
|
||||
self.feature_vector_to_state(&training_data[i].0, Some(close_price))
|
||||
self.feature_vector_to_state_with_ofi(&training_data[i].0, Some(close_price), Some(i))
|
||||
})
|
||||
.collect();
|
||||
(Some(bt), cpu_states?)
|
||||
@@ -1907,7 +1918,7 @@ impl DQNTrainer {
|
||||
};
|
||||
let close_price = rust_decimal::Decimal::try_from(current_close)
|
||||
.unwrap_or(rust_decimal::Decimal::ZERO);
|
||||
self.feature_vector_to_state(&training_data[i].0, Some(close_price))
|
||||
self.feature_vector_to_state_with_ofi(&training_data[i].0, Some(close_price), Some(i))
|
||||
})
|
||||
.collect();
|
||||
(None, cpu_states?)
|
||||
@@ -1989,9 +2000,10 @@ impl DQNTrainer {
|
||||
let mut next_state = if i + 1 < training_data.len() {
|
||||
let next_close_price = rust_decimal::Decimal::try_from(next_close)
|
||||
.unwrap_or(rust_decimal::Decimal::ZERO);
|
||||
self.feature_vector_to_state(
|
||||
self.feature_vector_to_state_with_ofi(
|
||||
&training_data[i + 1].0,
|
||||
Some(next_close_price),
|
||||
Some(i + 1),
|
||||
)?
|
||||
} else {
|
||||
state.clone()
|
||||
@@ -3263,6 +3275,15 @@ impl DQNTrainer {
|
||||
&self,
|
||||
feature_vec: &FeatureVector51,
|
||||
close_price: Option<rust_decimal::Decimal>,
|
||||
) -> Result<TradingState> {
|
||||
self.feature_vector_to_state_with_ofi(feature_vec, close_price, None)
|
||||
}
|
||||
|
||||
fn feature_vector_to_state_with_ofi(
|
||||
&self,
|
||||
feature_vec: &FeatureVector51,
|
||||
close_price: Option<rust_decimal::Decimal>,
|
||||
ofi_index: Option<usize>,
|
||||
) -> Result<TradingState> {
|
||||
// States are pre-normalized during data loading
|
||||
let normalized_features: Vec<f32> = feature_vec.iter().map(|&v| v as f32).collect();
|
||||
@@ -3300,8 +3321,14 @@ impl DQNTrainer {
|
||||
vec![0.0, 0.0, 0.0] // Fallback if no price provided
|
||||
};
|
||||
|
||||
// 43-FEATURE ARCHITECTURE: No regime features (removed for feature reduction)
|
||||
let regime_features: Vec<f32> = vec![];
|
||||
// OFI regime features: 8 features from MBP-10 order book data (or empty if unavailable)
|
||||
let regime_features: Vec<f32> = if let (Some(ofi), Some(idx)) = (&self.ofi_features, ofi_index) {
|
||||
ofi.get(idx)
|
||||
.map(|f| f.iter().map(|&v| v as f32).collect())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Use from_normalized() to preserve sign information
|
||||
Ok(TradingState::from_normalized(
|
||||
|
||||
@@ -213,8 +213,8 @@ pub struct NormStats {
|
||||
pub std: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Number of features in a standard feature vector.
|
||||
const FEATURE_DIM: usize = 51;
|
||||
/// Number of features in a standard feature vector (40 market features).
|
||||
const FEATURE_DIM: usize = 40;
|
||||
|
||||
/// Minimum standard deviation to prevent division by zero.
|
||||
const MIN_STD: f64 = 1e-8;
|
||||
|
||||
@@ -129,7 +129,7 @@ async fn load_features_from_cache(
|
||||
_parquet_path: &Path,
|
||||
_mbp10_dir: &Path,
|
||||
_warmup_period: usize,
|
||||
) -> Result<Option<(Vec<([f64; 51], Vec<f64>)>, Vec<([f64; 51], Vec<f64>)>)>> {
|
||||
) -> Result<Option<(Vec<([f64; 40], Vec<f64>)>, Vec<([f64; 40], Vec<f64>)>)>> {
|
||||
// NOTE: This is a stub that will fail until implemented
|
||||
// Implementation should follow design doc section 4 (Phase 2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user