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