multi_horizon_loader.rs constructs MultiHorizonLoaderConfig with default_three_scale() (matching alpha_train's production default and yielding total_positions()=32). The cfg.seq_len=32 override in loader_stride_4_yields_correct_spacing is now a no-op since the constructor already provides 32, so it's removed with a comment. perception_overfit.rs was untouched — it doesn't construct MultiHorizonLoaderConfig, only PerceptionTrainerConfig (whose own seq_len field is unrelated to the loader migration).
115 lines
4.3 KiB
Rust
115 lines
4.3 KiB
Rust
//! Phase A loader smoke tests.
|
|
//!
|
|
//! Real-data path is `--ignored`; runs only with FOXHUNT_TEST_DATA set.
|
|
|
|
use data::providers::databento::dbn_parser::InstrumentFilter;
|
|
use ml_alpha::data::loader::{
|
|
discover_mbp10_files_sorted, MultiHorizonLoader, MultiHorizonLoaderConfig,
|
|
DEFAULT_OUTCOME_LABEL_COST_ES,
|
|
};
|
|
use ml_alpha::heads::{HORIZONS, N_HORIZONS};
|
|
use std::path::PathBuf;
|
|
|
|
fn cfg_from_env() -> Option<MultiHorizonLoaderConfig> {
|
|
let root = std::env::var("FOXHUNT_TEST_DATA").ok()?;
|
|
let mbp10 = PathBuf::from(format!("{root}/mbp10"));
|
|
let predec = PathBuf::from(format!("{root}/predecoded"));
|
|
if !mbp10.exists() {
|
|
return None;
|
|
}
|
|
let files = discover_mbp10_files_sorted(&mbp10).ok()?;
|
|
Some(MultiHorizonLoaderConfig {
|
|
files,
|
|
predecoded_dir: predec,
|
|
multi_resolution: ml_alpha::data::aggregation::MultiResolutionConfig::default_three_scale(),
|
|
horizons: HORIZONS,
|
|
n_max_sequences: 100,
|
|
seed: 0xA1A2_A3A4,
|
|
inference_only: false,
|
|
outcome_label_cost: DEFAULT_OUTCOME_LABEL_COST_ES,
|
|
instrument_filter: InstrumentFilter::All,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn loader_yields_seq_with_valid_labels() {
|
|
let Some(cfg) = cfg_from_env() else {
|
|
eprintln!("skipping: FOXHUNT_TEST_DATA not set or mbp10 dir missing");
|
|
return;
|
|
};
|
|
let mut loader = MultiHorizonLoader::new(&cfg).expect("loader init");
|
|
assert!(loader.n_files() > 0, "no DBN files found in mbp10 root");
|
|
|
|
let mut count = 0;
|
|
while let Some(seq) = loader.next_sequence().expect("next") {
|
|
assert_eq!(seq.snapshots.len(), cfg.multi_resolution.total_positions());
|
|
for h in 0..N_HORIZONS {
|
|
assert_eq!(seq.labels[h].len(), cfg.multi_resolution.total_positions());
|
|
for &v in &seq.labels[h] {
|
|
assert!(v.is_nan() || v == 0.0 || v == 1.0, "label not binary: {v}");
|
|
}
|
|
}
|
|
let any_valid = seq.labels.iter().any(|l| l.iter().any(|v| !v.is_nan()));
|
|
assert!(any_valid, "expected at least one non-NaN label in a sequence");
|
|
count += 1;
|
|
if count >= 5 {
|
|
break;
|
|
}
|
|
}
|
|
assert!(count >= 5, "expected at least 5 sequences, got {count}");
|
|
}
|
|
|
|
#[test]
|
|
fn loader_errors_on_empty_files() {
|
|
let cfg = MultiHorizonLoaderConfig {
|
|
files: Vec::new(),
|
|
predecoded_dir: PathBuf::from("/tmp/does_not_exist_foxhunt_test"),
|
|
multi_resolution: ml_alpha::data::aggregation::MultiResolutionConfig::default_three_scale(),
|
|
horizons: HORIZONS,
|
|
n_max_sequences: 10,
|
|
seed: 0,
|
|
inference_only: false,
|
|
outcome_label_cost: DEFAULT_OUTCOME_LABEL_COST_ES,
|
|
instrument_filter: InstrumentFilter::All,
|
|
};
|
|
let res = MultiHorizonLoader::new(&cfg);
|
|
assert!(res.is_err(), "expected error for empty file list");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn loader_stride_4_yields_correct_spacing() {
|
|
// A1: decision_stride removed; sequences are always consecutive snapshots.
|
|
// Test repurposed: verifies consecutive-snapshot ts_ns monotonicity.
|
|
let Some(mut cfg) = cfg_from_env() else {
|
|
eprintln!("skipping: FOXHUNT_TEST_DATA not set or mbp10 dir missing");
|
|
return;
|
|
};
|
|
// cfg.multi_resolution stays at default_three_scale() (total_positions=32).
|
|
cfg.n_max_sequences = 5;
|
|
let mut loader = MultiHorizonLoader::new(&cfg).expect("loader init");
|
|
|
|
while let Some(seq) = loader.next_sequence().expect("next") {
|
|
assert_eq!(seq.snapshots.len(), cfg.multi_resolution.total_positions());
|
|
let dts_ns: Vec<i64> = (1..seq.snapshots.len())
|
|
.map(|k| (seq.snapshots[k].ts_ns as i64) - (seq.snapshots[k - 1].ts_ns as i64))
|
|
.filter(|d| *d >= 0) // ts_ns is monotonic in MBP-10
|
|
.collect();
|
|
assert!(!dts_ns.is_empty(), "no positive Δt in sequence");
|
|
let mut sorted = dts_ns.clone();
|
|
sorted.sort();
|
|
let median = sorted[sorted.len() / 2];
|
|
eprintln!("consecutive median Δt_ns between K-positions = {median}");
|
|
assert!(median >= 0, "Δt_ns negative — ts_ns not monotonic?");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn discover_errors_on_missing_root() {
|
|
let res = discover_mbp10_files_sorted(&PathBuf::from(
|
|
"/tmp/does_not_exist_foxhunt_test",
|
|
));
|
|
assert!(res.is_err(), "expected error for missing root dir");
|
|
}
|