fix kernel-read gap
Adds 12 features to the DQN input pipeline:
- 10 MicrostructureState::snapshot()[0..10] slots that were previously computed
every bar and then discarded before reaching fxcache: ofi_trajectory,
realized_variance, hawkes_intensity, book_pressure (weighted 10-level),
spread_dynamics, aggression_ratio, queue_depletion_asymmetry,
order_count_flux, intra_bar_momentum, regime_score.
- 2 TLOB-novel slots derived directly from Mbp10Snapshot:
order_count_imbalance = (Σbid_ct − Σask_ct) / Σ(bid_ct + ask_ct),
microprice_residual = (weighted_mid − mid) / mid.
Also fixes a production gap: ofi_acceleration (slot 18) and
toxicity_gradient (slot 19) were persisted to fxcache via OFI_DIM=20
but the OFI embed kernel (experience_kernels.cu:6146-6173) only read
[0..18), silently discarding them every bar. Kernel extended to
consume full SL_OFI_DIM=32.
Dimension bumps (all 8-aligned):
OFI_DIM 20 → 32
FXCACHE_VERSION 4 → 5 (invalidates existing caches; regen via
precompute_features)
STATE_DIM 96 → 104
PADDING_DIM 4 → 0 (OFI expansion consumed padding, still 8-aligned)
STATE_DIM_PADDED 128 (unchanged)
OFI_EMBED_IN 18 → 32 (MLP input width; W/grad/Adam/m/v buffers
resized in lockstep via named constants)
fxcache regen results (175874 bars ES.FUT 2024-Q1):
deltas_nonzero: 175781 / 175874 (99.9 percent)
book_aggression: 102137 / 175874 (58.1 percent)
microstructure[20-30): 175874 / 175874 (100 percent)
tlob_novel[30-32): 133615 / 175874 (76.0 percent)
Compile status: SQLX_OFFLINE=true CARGO_INCREMENTAL=0 cargo check
--workspace --tests passes cleanly (0 errors, pre-existing warnings
only).
Test results:
fxcache roundtrip (unit + integration): PASS (4+6 tests)
magnitude_distribution smoke: ran through epoch 1 successfully
(OFI_DIAG fires, state_dim=104 confirmed, feature_dim=74 in
validation kernel); epoch 2 OOM on local RTX 3050 Ti (4 GB) —
expected hardware limit from state_dim growth. Full 20-epoch run
requires L40S/H100 CI verification.
multi_fold_convergence smoke: not verified locally (same VRAM
ceiling applies). L40S/H100 CI verification required.
The new slots follow the existing OFICalculator/MicrostructureState
pattern and consume signals already computed by ml-features — no new
crate, no ONNX, no stubs. All 12 sources were audited against their
implementation before persistence; every slot traces back to real
Mbp10Snapshot or MicrostructureState math.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
7.4 KiB
Rust
222 lines
7.4 KiB
Rust
//! Roundtrip tests for the `.fxcache` binary format.
|
|
//!
|
|
//! Covers f32 serialization, cache-key lookup, header validation,
|
|
//! and the empty-bars error path.
|
|
|
|
use ml::fxcache::{find_fxcache, load_fxcache, write_fxcache, FxCacheData, FXCACHE_VERSION, OFI_DIM};
|
|
use tempfile::TempDir;
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
/// Deterministic feature vector for bar `i`.
|
|
fn make_features(n: usize) -> Vec<[f64; 42]> {
|
|
(0..n)
|
|
.map(|i| {
|
|
let mut row = [0.0_f64; 42];
|
|
for j in 0..42 {
|
|
row[j] = (i * 42 + j) as f64 * 0.01;
|
|
}
|
|
row
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Deterministic target vector for bar `i`.
|
|
fn make_targets(n: usize) -> Vec<[f64; 6]> {
|
|
(0..n)
|
|
.map(|i| {
|
|
let mut row = [0.0_f64; 6];
|
|
for j in 0..6 {
|
|
row[j] = 5000.0 + (i * 6 + j) as f64 * 0.25;
|
|
}
|
|
row
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Deterministic OFI vector for bar `i`.
|
|
fn make_ofi(n: usize) -> Vec<[f64; OFI_DIM]> {
|
|
(0..n)
|
|
.map(|i| {
|
|
let mut row = [0.0_f64; OFI_DIM];
|
|
for j in 0..OFI_DIM {
|
|
row[j] = (i * OFI_DIM + j) as f64 * 0.001;
|
|
}
|
|
row
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Deterministic per-bar timestamps (1-minute intervals starting 2022-01-01).
|
|
fn make_timestamps(n: usize) -> Vec<i64> {
|
|
(0..n)
|
|
.map(|i| 1640995200_000_000_000i64 + i as i64 * 60_000_000_000)
|
|
.collect()
|
|
}
|
|
|
|
/// A fixed 32-byte cache key for tests.
|
|
fn test_cache_key() -> [u8; 32] {
|
|
let mut key = [0u8; 32];
|
|
for (i, slot) in key.iter_mut().enumerate() {
|
|
*slot = (i as u8).wrapping_mul(7).wrapping_add(13);
|
|
}
|
|
key
|
|
}
|
|
|
|
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
|
|
/// Write 100 bars as f32 (version 1), read back, verify within f32 tolerance.
|
|
#[test]
|
|
fn test_fxcache_f32_roundtrip() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test_f32.fxcache");
|
|
|
|
let n = 100;
|
|
let features = make_features(n);
|
|
let targets = make_targets(n);
|
|
let ofi = make_ofi(n);
|
|
let timestamps = make_timestamps(n);
|
|
let key = test_cache_key();
|
|
|
|
let bytes_written =
|
|
write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false).unwrap();
|
|
assert!(bytes_written > 0, "expected nonzero bytes written");
|
|
|
|
let data: FxCacheData = load_fxcache(&path).unwrap();
|
|
assert_eq!(data.bar_count, n);
|
|
assert_eq!(data.cache_key, key);
|
|
assert_eq!(data.features.len(), n);
|
|
assert_eq!(data.targets.len(), n);
|
|
assert_eq!(data.ofi.len(), n);
|
|
assert_eq!(data.timestamps.len(), n);
|
|
|
|
// Timestamps are always i64 — bit-exact.
|
|
for i in 0..n {
|
|
assert_eq!(
|
|
data.timestamps[i], timestamps[i],
|
|
"timestamp mismatch at bar {i}"
|
|
);
|
|
}
|
|
|
|
// f32 roundtrip tolerance checks (~7 significant digits).
|
|
for i in 0..n {
|
|
for j in 0..42 {
|
|
let diff = (data.features[i][j] - features[i][j]).abs();
|
|
assert!(
|
|
diff < 1e-4,
|
|
"feature[{i}][{j}]: expected {}, got {}, diff={diff}",
|
|
features[i][j],
|
|
data.features[i][j]
|
|
);
|
|
}
|
|
for j in 0..6 {
|
|
let diff = (data.targets[i][j] - targets[i][j]).abs();
|
|
// f32 at ~5000 has resolution of ~0.0005, allow generous margin.
|
|
assert!(
|
|
diff < 0.01,
|
|
"target[{i}][{j}]: expected {}, got {}, diff={diff}",
|
|
targets[i][j],
|
|
data.targets[i][j]
|
|
);
|
|
}
|
|
for j in 0..OFI_DIM {
|
|
let diff = (data.ofi[i][j] - ofi[i][j]).abs();
|
|
assert!(
|
|
diff < 1e-4,
|
|
"ofi[{i}][{j}]: expected {}, got {}, diff={diff}",
|
|
ofi[i][j],
|
|
data.ofi[i][j]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Write a file with a known cache key, verify `find_fxcache` locates it.
|
|
/// Also verify that a different key returns `None`.
|
|
#[test]
|
|
fn test_fxcache_find() {
|
|
let dir = TempDir::new().unwrap();
|
|
let key = test_cache_key();
|
|
let hex_key = hex::encode(key);
|
|
let path = dir.path().join(format!("{hex_key}.fxcache"));
|
|
|
|
let features = make_features(5);
|
|
let targets = make_targets(5);
|
|
let ofi = make_ofi(5);
|
|
let timestamps = make_timestamps(5);
|
|
|
|
write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false).unwrap();
|
|
|
|
// Should find the file.
|
|
let found = find_fxcache(dir.path(), &key);
|
|
assert!(found.is_some(), "expected to find fxcache file");
|
|
assert_eq!(found.unwrap(), path);
|
|
|
|
// Wrong key should miss.
|
|
let mut wrong_key = [0xFFu8; 32];
|
|
wrong_key[0] = 0x00;
|
|
let miss = find_fxcache(dir.path(), &wrong_key);
|
|
assert!(miss.is_none(), "expected miss for wrong key");
|
|
}
|
|
|
|
/// Write garbage bytes to a file, verify `load_fxcache` returns an error
|
|
/// whose message contains "magic".
|
|
#[test]
|
|
fn test_fxcache_header_validation() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("garbage.fxcache");
|
|
|
|
// Write 128 bytes of garbage (more than header size).
|
|
std::fs::write(&path, vec![0xABu8; 128]).unwrap();
|
|
|
|
let err = load_fxcache(&path).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.to_lowercase().contains("magic"),
|
|
"expected error about magic bytes, got: {msg}"
|
|
);
|
|
}
|
|
|
|
/// Attempting to write 0 bars should fail (writer rejects empty data).
|
|
/// Attempting to read a hand-crafted header with bar_count=0 should also fail.
|
|
#[test]
|
|
fn test_fxcache_empty() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("empty.fxcache");
|
|
|
|
// Writer rejects 0 bars.
|
|
let features: Vec<[f64; 42]> = vec![];
|
|
let targets: Vec<[f64; 6]> = vec![];
|
|
let ofi: Vec<[f64; OFI_DIM]> = vec![];
|
|
let timestamps: Vec<i64> = vec![];
|
|
let key = test_cache_key();
|
|
|
|
let err = write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("0 bars") || msg.contains("empty"),
|
|
"expected empty/0-bars error from writer, got: {msg}"
|
|
);
|
|
|
|
// Reader also rejects a header with bar_count=0.
|
|
// Hand-craft a valid-magic header with current version + bar_count=0.
|
|
let mut header = [0u8; 64];
|
|
header[0..8].copy_from_slice(b"FXCACHE\0");
|
|
header[8..10].copy_from_slice(&FXCACHE_VERSION.to_le_bytes()); // current version
|
|
header[10..12].copy_from_slice(&42u16.to_le_bytes()); // feat_dim
|
|
header[12..14].copy_from_slice(&6u16.to_le_bytes()); // target_dim
|
|
header[14..16].copy_from_slice(&(OFI_DIM as u16).to_le_bytes()); // ofi_dim
|
|
header[16..24].copy_from_slice(&0u64.to_le_bytes()); // bar_count=0
|
|
// cache_key + reserved stay zeroed.
|
|
|
|
let path_reader = dir.path().join("empty_header.fxcache");
|
|
std::fs::write(&path_reader, &header).unwrap();
|
|
|
|
let err = load_fxcache(&path_reader).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("zero") || msg.contains("bar_count"),
|
|
"expected bar_count/zero error from reader, got: {msg}"
|
|
);
|
|
}
|