Files
foxhunt/testing/test-common/src/builders/bar_builder.rs
jgrusewski 2fbb19d9df refactor(ml): rename real_data_loader to data_loader
The real_ prefix was misleading — there is no fake data loader.
Mechanical rename across 18 source files, no logic changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00

155 lines
3.7 KiB
Rust

//! OHLCV Bar builder for testing
use chrono::{DateTime, Duration, Utc};
use ml::data_loader::OHLCVBar;
/// Builder for creating OHLCV bars
pub struct BarBuilder {
timestamp: DateTime<Utc>,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
}
impl BarBuilder {
pub fn new() -> Self {
let price = 100.0;
Self {
timestamp: Utc::now(),
open: price,
high: price * 1.01,
low: price * 0.99,
close: price,
volume: 1000.0,
}
}
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = timestamp;
self
}
pub fn price(mut self, price: f64) -> Self {
self.open = price;
self.close = price;
self.high = price * 1.01;
self.low = price * 0.99;
self
}
pub fn open(mut self, open: f64) -> Self {
self.open = open;
self
}
pub fn high(mut self, high: f64) -> Self {
self.high = high;
self
}
pub fn low(mut self, low: f64) -> Self {
self.low = low;
self
}
pub fn close(mut self, close: f64) -> Self {
self.close = close;
self
}
pub fn volume(mut self, volume: f64) -> Self {
self.volume = volume;
self
}
pub fn bullish(mut self) -> Self {
self.close = self.open * 1.05;
self.high = self.close * 1.01;
self.low = self.open * 0.99;
self
}
pub fn bearish(mut self) -> Self {
self.close = self.open * 0.95;
self.high = self.open * 1.01;
self.low = self.close * 0.99;
self
}
pub fn build(self) -> OHLCVBar {
OHLCVBar {
timestamp: self.timestamp,
open: self.open,
high: self.high,
low: self.low,
close: self.close,
volume: self.volume,
}
}
/// Build a series of bars with specified spacing
pub fn build_series(self, count: usize, interval_minutes: i64) -> Vec<OHLCVBar> {
let mut bars = Vec::with_capacity(count);
let mut current = self.build();
for _ in 0..count {
bars.push(current.clone());
current.timestamp = current.timestamp + Duration::minutes(interval_minutes);
// Add small random walk
current.open = current.close;
current.close = current.close * (1.0 + (rand::random::<f64>() - 0.5) * 0.02);
current.high = current.close.max(current.open) * 1.005;
current.low = current.close.min(current.open) * 0.995;
}
bars
}
}
impl Default for BarBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bar_builder_defaults() {
let bar = BarBuilder::new().build();
assert_eq!(bar.open, 100.0);
assert!(bar.high >= bar.open);
assert!(bar.low <= bar.open);
}
#[test]
fn test_bullish_bar() {
let bar = BarBuilder::new().bullish().build();
assert!(bar.close > bar.open);
assert_eq!(bar.close / bar.open, 1.05);
}
#[test]
fn test_bearish_bar() {
let bar = BarBuilder::new().bearish().build();
assert!(bar.close < bar.open);
assert_eq!(bar.close / bar.open, 0.95);
}
#[test]
fn test_build_series() {
let bars = BarBuilder::new().build_series(10, 5);
assert_eq!(bars.len(), 10);
// Check timestamps are 5 minutes apart
for i in 0..bars.len() - 1 {
let diff = bars[i + 1].timestamp - bars[i].timestamp;
assert_eq!(diff.num_minutes(), 5);
}
}
}