Files
foxhunt/crates/ml/src/labeling/sample_weights.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

152 lines
4.7 KiB
Rust

//! Sample weighting algorithms for training data enhancement
//!
//! Implements volatility/return/time-based weighting to improve ML model training.
use serde::{Deserialize, Serialize};
use super::gpu_acceleration::LabelingError;
use super::types::{EventLabel, WeightedSample};
/// Configuration for sample weighting calculation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightingConfig {
/// Time decay factor for recency weighting
pub time_decay: f64,
/// Return scaling factor for return-based weighting
pub return_scale: f64,
/// Volatility scaling factor for volatility-based weighting
pub volatility_scale: f64,
}
impl WeightingConfig {
/// Standard configuration values
pub fn standard() -> Self {
Self {
time_decay: 0.95,
return_scale: 1.0,
volatility_scale: 1.0,
}
}
}
/// Calculator for sample weights
#[derive(Debug)]
pub struct SampleWeightCalculator {
config: WeightingConfig,
}
impl SampleWeightCalculator {
/// Create new calculator with given configuration
pub fn new(config: WeightingConfig) -> Self {
Self { config }
}
/// Calculate weights for given labels
pub fn calculate_weights(
&self,
labels: &[EventLabel],
) -> Result<Vec<WeightedSample>, LabelingError> {
if labels.is_empty() {
return Ok(Vec::new());
}
let mut samples = Vec::with_capacity(labels.len());
for label in labels {
let time_weight = self.calculate_time_weight(label.event_timestamp_ns as i64, labels);
let return_weight = self.calculate_return_weight(label.return_bps);
let volatility_weight = self.calculate_volatility_weight(0.2); // Default volatility for now
let combined_weight = time_weight * return_weight * volatility_weight;
// Create features vector from the label data
let features = vec![
label.entry_price_cents as f64 / 100.0, // Price in dollars
label.return_as_ratio(), // Return as ratio
label.quality_score, // Quality score
];
samples.push(WeightedSample {
timestamp_ns: label.event_timestamp_ns,
features,
label: label.label_value,
weight: combined_weight,
sample_id: None,
});
}
Ok(samples)
}
fn calculate_time_weight(&self, timestamp_ns: i64, all_labels: &[EventLabel]) -> f64 {
if all_labels.is_empty() {
return 1.0;
}
let latest_time = all_labels
.iter()
.map(|l| l.event_timestamp_ns as i64)
.max()
.unwrap_or(timestamp_ns); // Default to current timestamp if no labels
let time_diff_hours = (latest_time - timestamp_ns) as f64 / 3_600_000_000_000.0;
self.config.time_decay.powf(time_diff_hours.max(0.0))
}
fn calculate_return_weight(&self, return_bps: i32) -> f64 {
(return_bps.abs() as f64 / 100.0 * self.config.return_scale).max(0.1)
}
fn calculate_volatility_weight(&self, volatility: f64) -> f64 {
(volatility * self.config.volatility_scale).max(0.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::labeling::types::BarrierResult;
#[test]
fn test_sample_weight_calculator() -> Result<(), LabelingError> {
let config = WeightingConfig::standard();
let calculator = SampleWeightCalculator::new(config);
// Create test labels with varying returns
let mut labels = Vec::new();
let returns = [100, 200, 50, 300, 150]; // Basis points
for (i, return_bps) in returns.into_iter().enumerate() {
let barrier_result = BarrierResult::ProfitTarget;
let label = EventLabel::new(
(1692000000_000_000_000 + i as u64 * 3600_000_000_000)
.saturating_sub(3600_000_000_000),
10000,
barrier_result,
1,
return_bps,
0.8,
50,
);
labels.push(label);
}
let weighted_samples = calculator.calculate_weights(&labels)?;
assert_eq!(weighted_samples.len(), labels.len());
// All weights should be positive
for sample in &weighted_samples {
assert!(sample.weight > 0.0);
assert!(!sample.features.is_empty());
assert_eq!(sample.features.len(), 3);
}
// Samples should have the expected structure
assert_eq!(weighted_samples.len(), labels.len());
Ok(())
}
}