Phase 2A scaffolding complete (per spec §7.2): - oracle.rs: OracleAction enum + 3 oracle policies (flat, drift, OU) - harness.rs: BehavioralResult + evaluate_policy_on_market stub. Trainer-side methods (eval_actions_on_features, read_isv_for_test) are documented gap #7; Phase 2B tests add them per-test as needed. - pre-commit hook: cargo test -p ml --test behavioral_suite gates argo-train.sh per spec §4.5 discipline rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.8 KiB
Rust
81 lines
2.8 KiB
Rust
//! SP15 Phase 2A — oracle policies for behavioral test harness.
|
||
//! Each oracle is a known-correct action sequence for a given synthetic
|
||
//! market type; tests assert the trained policy approaches the oracle.
|
||
|
||
use ml::cuda_pipeline::lob_bar::LobBar;
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum MagBucket { Quarter, Half, Full }
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum OracleAction {
|
||
Hold,
|
||
Long(MagBucket),
|
||
Short(MagBucket),
|
||
Flat,
|
||
}
|
||
|
||
/// Oracle for flat markets: always Hold.
|
||
pub fn flat_market_oracle(bars: &[LobBar]) -> Vec<OracleAction> {
|
||
vec![OracleAction::Hold; bars.len()]
|
||
}
|
||
|
||
/// Oracle for drift markets: take direction-of-trend, mag = Half.
|
||
pub fn drift_market_oracle(bars: &[LobBar]) -> Vec<OracleAction> {
|
||
let mut actions = Vec::with_capacity(bars.len());
|
||
actions.push(OracleAction::Hold); // first bar: no prior
|
||
for w in bars.windows(2) {
|
||
let action = if w[1].price > w[0].price + 0.05 {
|
||
OracleAction::Long(MagBucket::Half)
|
||
} else if w[1].price < w[0].price - 0.05 {
|
||
OracleAction::Short(MagBucket::Half)
|
||
} else {
|
||
OracleAction::Hold
|
||
};
|
||
actions.push(action);
|
||
}
|
||
actions
|
||
}
|
||
|
||
/// Oracle for OU markets: reverse at ±2σ extremes.
|
||
pub fn ou_market_oracle(bars: &[LobBar], mu: f32, sigma: f32) -> Vec<OracleAction> {
|
||
bars.iter().map(|b| {
|
||
let z = (b.price - mu) / sigma.max(1e-6);
|
||
if z > 2.0 { OracleAction::Short(MagBucket::Half) }
|
||
else if z < -2.0 { OracleAction::Long(MagBucket::Half) }
|
||
else { OracleAction::Hold }
|
||
}).collect()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::synthetic_markets::*;
|
||
|
||
#[test]
|
||
fn flat_oracle_all_hold() {
|
||
let bars = flat_market(100, 0.1, 0.25, 42);
|
||
let actions = flat_market_oracle(&bars);
|
||
assert_eq!(actions.len(), 100);
|
||
assert!(actions.iter().all(|a| *a == OracleAction::Hold));
|
||
}
|
||
|
||
#[test]
|
||
fn drift_oracle_takes_uptrend() {
|
||
let bars = drift_market(1000, 0.5, 0.1, 0.25, 42);
|
||
let actions = drift_market_oracle(&bars);
|
||
let long_count = actions.iter().filter(|a| matches!(a, OracleAction::Long(_))).count();
|
||
// Strong uptrend → most actions should be Long
|
||
assert!(long_count > 700, "drift_market_oracle Long count = {}, expected > 700", long_count);
|
||
}
|
||
|
||
#[test]
|
||
fn ou_oracle_reverses_at_extremes() {
|
||
let bars = ou_market(1000, 0.05, 1.0, 4500.0, 0.25, 42);
|
||
let actions = ou_market_oracle(&bars, 4500.0, 1.0);
|
||
// OU oracle should produce SOME reversion actions (not all Hold)
|
||
let action_count = actions.iter().filter(|a| !matches!(a, OracleAction::Hold)).count();
|
||
assert!(action_count > 0, "OU oracle produced zero non-Hold actions");
|
||
}
|
||
}
|