feat: shared discover_and_load() — strict key match, no fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-04 09:51:28 +02:00
parent 8b0122a5ac
commit 757ea080ee

View File

@@ -514,8 +514,6 @@ fn read_body_bf16(
// ── Finder ───────────────────────────────────────────────────────────────────
// ── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod has_ofi_tests {
use super::*;
@@ -575,3 +573,156 @@ pub fn find_fxcache(cache_dir: &Path, cache_key: &[u8; 32]) -> Option<PathBuf> {
None
}
}
/// Resolve fxcache directory: explicit override > env var > walk-up sibling.
pub fn resolve_cache_dir(data_dir: &Path, override_dir: Option<&Path>) -> Option<PathBuf> {
// 1. Explicit override (CLI --feature-cache-dir)
if let Some(dir) = override_dir {
if dir.exists() {
return Some(dir.to_path_buf());
}
}
// 2. Environment variable
if let Ok(dir) = std::env::var("FOXHUNT_FEATURE_CACHE_DIR") {
let p = PathBuf::from(dir);
if p.exists() {
return Some(p);
}
}
// 3. Walk up from data_dir to find sibling feature-cache/
let mut dir = data_dir;
loop {
if let Some(parent) = dir.parent() {
let candidate = parent.join("feature-cache");
if candidate.exists() {
return Some(candidate);
}
if parent == dir {
break;
}
dir = parent;
} else {
break;
}
}
None
}
/// Discover and load an fxcache file. Single source of truth for all callers.
///
/// Cache dir priority: `cache_dir_override` > `FOXHUNT_FEATURE_CACHE_DIR` env > walk-up sibling.
/// Strict key match only — returns `None` on miss. No "most recent" fallback.
///
/// # Arguments
/// * `data_dir` — Base data directory (e.g. `test_data/futures-baseline`)
/// * `symbol` — Trading symbol (e.g. `"ES.FUT"`)
/// * `mbp10_dir` — Optional MBP-10 order book data directory
/// * `trades_dir` — Optional trades data directory
/// * `data_source` — Data source mode (`"ohlcv"` or `"mbp10"`)
/// * `cache_dir_override` — Explicit cache directory (from CLI `--feature-cache-dir`)
pub fn discover_and_load(
data_dir: &Path,
symbol: &str,
mbp10_dir: Option<&Path>,
trades_dir: Option<&Path>,
data_source: &str,
cache_dir_override: Option<&Path>,
) -> Option<FxCacheData> {
// 1. Resolve cache directory
let cache_dir = resolve_cache_dir(data_dir, cache_dir_override)?;
// 2. Compute cache key (includes symbol + data_source)
let key_hex = crate::feature_cache::calculate_dbn_cache_key_full(
data_dir, mbp10_dir, trades_dir, symbol, data_source,
)
.ok()?;
let key: [u8; 32] = hex::decode(&key_hex).ok()?.try_into().ok()?;
// 3. Strict key match — no fallback to "most recent"
let path = find_fxcache(&cache_dir, &key)?;
// 4. Load and return
match load_fxcache(&path) {
Ok(data) => {
info!(
"fxcache hit: {} bars, OFI={} from {:?}",
data.bar_count, data.has_ofi, path
);
Some(data)
}
Err(e) => {
tracing::warn!("fxcache load failed: {e}");
None
}
}
}
// ── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod discover_tests {
use super::*;
#[test]
fn test_discover_returns_none_for_nonexistent_dir() {
let result = discover_and_load(
Path::new("/nonexistent/path"),
"ES.FUT",
None,
None,
"ohlcv",
None,
);
assert!(result.is_none());
}
#[test]
fn test_discover_returns_none_on_key_mismatch() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("feature-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
// Write a cache file with a known dummy key
let path = cache_dir.join(
"0000000000000000000000000000000000000000000000000000000000000000.fxcache",
);
let features = vec![[1.0_f64; 42]; 5];
let targets = vec![[0.0_f64; 4]; 5];
let ofi = vec![[0.0_f64; 8]; 5];
let timestamps = vec![1_i64; 5];
write_fxcache(&path, &features, &targets, &ofi, &timestamps, [0u8; 32], false, false)
.unwrap();
// Try to discover with a real data_dir (different key) — should NOT match
let result = discover_and_load(
Path::new("test_data/futures-baseline"),
"ES.FUT",
None,
None,
"ohlcv",
Some(&cache_dir),
);
// Strict match only — no "most recent" fallback
assert!(result.is_none(), "Wrong key should not match (strict mode)");
}
#[test]
fn test_resolve_cache_dir_explicit_override() {
let dir = tempfile::tempdir().unwrap();
let result = resolve_cache_dir(Path::new("/some/data"), Some(dir.path()));
assert_eq!(result, Some(dir.path().to_path_buf()));
}
#[test]
fn test_resolve_cache_dir_nonexistent_override_falls_through() {
let result = resolve_cache_dir(
Path::new("/some/data"),
Some(Path::new("/nonexistent/override")),
);
// Falls through to env var / walk-up (both will fail here)
assert!(result.is_none());
}
}