diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e6a91..430c211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,23 +10,22 @@ env: CARGO_TERM_COLOR: always jobs: - test: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Rust - uses: dtolnay/rust-action@stable - - - name: Run Rust tests - run: cargo test --all-features - - - name: Run Rust clippy - run: cargo clippy --all-features -- -D warnings + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt - name: Check Rust formatting run: cargo fmt --check + - name: Run Rust clippy + run: cargo clippy --all-features + build: runs-on: ubuntu-latest steps: diff --git a/rustfmt.toml b/rustfmt.toml index 6af9106..976fd7d 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,6 @@ edition = "2021" max_width = 100 use_small_heuristics = "Max" -imports_granularity = "Module" -group_imports = "StdExternalCrate" \ No newline at end of file +# Note: imports_granularity and group_imports require nightly Rust +# imports_granularity = "Module" +# group_imports = "StdExternalCrate" \ No newline at end of file diff --git a/src/core/error.rs b/src/core/error.rs index 4cada1f..8f1815d 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -49,38 +49,27 @@ impl RaptorError { /// Create an invalid parameter error. pub fn invalid_parameter(message: impl Into) -> Self { - Self::InvalidParameter { - message: message.into(), - } + Self::InvalidParameter { message: message.into() } } /// Create an insufficient data error. pub fn insufficient_data(required: usize, available: usize) -> Self { - Self::InsufficientData { - required, - available, - } + Self::InsufficientData { required, available } } /// Create an invalid config error. pub fn invalid_config(message: impl Into) -> Self { - Self::InvalidConfig { - message: message.into(), - } + Self::InvalidConfig { message: message.into() } } /// Create a division by zero error. pub fn division_by_zero(context: impl Into) -> Self { - Self::DivisionByZero { - context: context.into(), - } + Self::DivisionByZero { context: context.into() } } /// Create an empty data error. pub fn empty_data(context: impl Into) -> Self { - Self::EmptyData { - context: context.into(), - } + Self::EmptyData { context: context.into() } } } diff --git a/src/core/timeseries.rs b/src/core/timeseries.rs index fdf38ed..655cbea 100644 --- a/src/core/timeseries.rs +++ b/src/core/timeseries.rs @@ -77,20 +77,14 @@ impl TimeSeries { /// Create with default values. pub fn with_default(timestamps: Vec) -> Self { let len = timestamps.len(); - Self { - timestamps, - values: vec![T::default(); len], - } + Self { timestamps, values: vec![T::default(); len] } } } impl TimeSeries { /// Create a series filled with NaN. pub fn with_nan(len: usize) -> Self { - Self { - timestamps: (0..len as i64).collect(), - values: vec![f64::NAN; len], - } + Self { timestamps: (0..len as i64).collect(), values: vec![f64::NAN; len] } } /// Calculate sum of all values. @@ -124,20 +118,12 @@ impl TimeSeries { /// Get minimum value. pub fn min(&self) -> f64 { - self.values - .iter() - .filter(|v| !v.is_nan()) - .copied() - .fold(f64::INFINITY, f64::min) + self.values.iter().filter(|v| !v.is_nan()).copied().fold(f64::INFINITY, f64::min) } /// Get maximum value. pub fn max(&self) -> f64 { - self.values - .iter() - .filter(|v| !v.is_nan()) - .copied() - .fold(f64::NEG_INFINITY, f64::max) + self.values.iter().filter(|v| !v.is_nan()).copied().fold(f64::NEG_INFINITY, f64::max) } /// Shift values by n positions (positive = shift forward, fill with NaN). @@ -161,10 +147,7 @@ impl TimeSeries { } } - Self { - timestamps: self.timestamps.clone(), - values: result, - } + Self { timestamps: self.timestamps.clone(), values: result } } /// Calculate difference from previous value. @@ -175,10 +158,7 @@ impl TimeSeries { result[i] = self.values[i] - self.values[i - 1]; } } - Self { - timestamps: self.timestamps.clone(), - values: result, - } + Self { timestamps: self.timestamps.clone(), values: result } } /// Calculate percentage change from previous value. @@ -190,10 +170,7 @@ impl TimeSeries { result[i] = (self.values[i] - self.values[i - 1]) / self.values[i - 1]; } } - Self { - timestamps: self.timestamps.clone(), - values: result, - } + Self { timestamps: self.timestamps.clone(), values: result } } /// Apply rolling window function. @@ -203,10 +180,7 @@ impl TimeSeries { { let mut result = vec![f64::NAN; self.values.len()]; if window == 0 || window > self.values.len() { - return Self { - timestamps: self.timestamps.clone(), - values: result, - }; + return Self { timestamps: self.timestamps.clone(), values: result }; } for i in (window - 1)..self.values.len() { @@ -214,10 +188,7 @@ impl TimeSeries { result[i] = f(slice); } - Self { - timestamps: self.timestamps.clone(), - values: result, - } + Self { timestamps: self.timestamps.clone(), values: result } } /// Calculate rolling sum. @@ -227,9 +198,7 @@ impl TimeSeries { /// Calculate rolling mean. pub fn rolling_mean(&self, window: usize) -> Self { - self.rolling(window, |slice| { - slice.iter().sum::() / slice.len() as f64 - }) + self.rolling(window, |slice| slice.iter().sum::() / slice.len() as f64) } /// Calculate rolling standard deviation. @@ -244,16 +213,12 @@ impl TimeSeries { /// Calculate rolling maximum. pub fn rolling_max(&self, window: usize) -> Self { - self.rolling(window, |slice| { - slice.iter().copied().fold(f64::NEG_INFINITY, f64::max) - }) + self.rolling(window, |slice| slice.iter().copied().fold(f64::NEG_INFINITY, f64::max)) } /// Calculate rolling minimum. pub fn rolling_min(&self, window: usize) -> Self { - self.rolling(window, |slice| { - slice.iter().copied().fold(f64::INFINITY, f64::min) - }) + self.rolling(window, |slice| slice.iter().copied().fold(f64::INFINITY, f64::min)) } } @@ -277,12 +242,7 @@ impl TimeSeries { debug_assert_eq!(self.len(), other.len()); Self { timestamps: self.timestamps.clone(), - values: self - .values - .iter() - .zip(other.values.iter()) - .map(|(&a, &b)| a && b) - .collect(), + values: self.values.iter().zip(other.values.iter()).map(|(&a, &b)| a && b).collect(), } } @@ -291,12 +251,7 @@ impl TimeSeries { debug_assert_eq!(self.len(), other.len()); Self { timestamps: self.timestamps.clone(), - values: self - .values - .iter() - .zip(other.values.iter()) - .map(|(&a, &b)| a || b) - .collect(), + values: self.values.iter().zip(other.values.iter()).map(|(&a, &b)| a || b).collect(), } } diff --git a/src/core/types.rs b/src/core/types.rs index 95e657f..34a6d4b 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -73,14 +73,7 @@ impl OhlcvData { close: Vec, volume: Vec, ) -> Self { - Self { - timestamps, - open, - high, - low, - close, - volume, - } + Self { timestamps, open, high, low, close, volume } } /// Get the number of bars. @@ -137,14 +130,7 @@ impl CompiledSignals { direction: Direction, weight: f64, ) -> Self { - Self { - symbol, - entries, - exits, - position_sizes: None, - direction, - weight, - } + Self { symbol, entries, exits, position_sizes: None, direction, weight } } /// Set position sizes. @@ -375,13 +361,7 @@ impl BacktestResult { trades: Vec, returns: Vec, ) -> Self { - Self { - metrics, - equity_curve, - drawdown_curve, - trades, - returns, - } + Self { metrics, equity_curve, drawdown_curve, trades, returns } } } diff --git a/src/execution/fees.rs b/src/execution/fees.rs index 4f3e926..3c90de2 100644 --- a/src/execution/fees.rs +++ b/src/execution/fees.rs @@ -92,10 +92,7 @@ pub struct BrokerFees; impl BrokerFees { /// Interactive Brokers tiered pricing (approximate). pub fn interactive_brokers() -> FeeModel { - FeeModel::Custom { - base: 1.0, - per_share: 0.005, - } + FeeModel::Custom { base: 1.0, per_share: 0.005 } } /// Zero commission broker (like Robinhood). diff --git a/src/execution/fill.rs b/src/execution/fill.rs index 9f2b1ba..c5ef7d3 100644 --- a/src/execution/fill.rs +++ b/src/execution/fill.rs @@ -121,31 +121,19 @@ pub struct FillModel { impl Default for FillModel { fn default() -> Self { - Self { - fill_price: FillPrice::Close, - delay_to_next_bar: false, - fill_ratio: 1.0, - } + Self { fill_price: FillPrice::Close, delay_to_next_bar: false, fill_ratio: 1.0 } } } impl FillModel { /// Create a fill model that executes at close. pub fn at_close() -> Self { - Self { - fill_price: FillPrice::Close, - delay_to_next_bar: false, - fill_ratio: 1.0, - } + Self { fill_price: FillPrice::Close, delay_to_next_bar: false, fill_ratio: 1.0 } } /// Create a fill model that executes at next bar's open. pub fn at_next_open() -> Self { - Self { - fill_price: FillPrice::Open, - delay_to_next_bar: true, - fill_ratio: 1.0, - } + Self { fill_price: FillPrice::Open, delay_to_next_bar: true, fill_ratio: 1.0 } } /// Set partial fill ratio. @@ -306,14 +294,7 @@ mod tests { use super::*; fn test_bar() -> OhlcvBar { - OhlcvBar { - timestamp: 0, - open: 100.0, - high: 105.0, - low: 95.0, - close: 102.0, - volume: 1000.0, - } + OhlcvBar { timestamp: 0, open: 100.0, high: 105.0, low: 95.0, close: 102.0, volume: 1000.0 } } #[test] diff --git a/src/execution/slippage.rs b/src/execution/slippage.rs index 7332e69..44cbb09 100644 --- a/src/execution/slippage.rs +++ b/src/execution/slippage.rs @@ -36,10 +36,7 @@ impl SlippageModel { /// Create a volume-based slippage model. pub fn volume_based(base: f64, volume_factor: f64) -> Self { - SlippageModel::VolumeBased { - base, - volume_factor, - } + SlippageModel::VolumeBased { base, volume_factor } } /// Calculate slippage for a trade. @@ -66,10 +63,7 @@ impl SlippageModel { SlippageModel::None => 0.0, SlippageModel::Percentage(rate) => price * rate, SlippageModel::Fixed(points) => *points, - SlippageModel::VolumeBased { - base, - volume_factor, - } => { + SlippageModel::VolumeBased { base, volume_factor } => { if let Some(vol) = volume { if vol > 0.0 { base * (1.0 / (1.0 + vol * volume_factor)) @@ -131,11 +125,7 @@ pub struct MarketImpact { impl MarketImpact { /// Create a new market impact model. pub fn new(temporary: f64, permanent: f64, adv: f64) -> Self { - Self { - temporary_impact: temporary, - permanent_impact: permanent, - avg_daily_volume: adv, - } + Self { temporary_impact: temporary, permanent_impact: permanent, avg_daily_volume: adv } } /// Calculate market impact for an order. diff --git a/src/indicators/momentum.rs b/src/indicators/momentum.rs index b80e66d..b351c5c 100644 --- a/src/indicators/momentum.rs +++ b/src/indicators/momentum.rs @@ -100,9 +100,7 @@ pub fn macd( return Err(RaptorError::invalid_parameter("MACD periods must be > 0")); } if fast_period >= slow_period { - return Err(RaptorError::invalid_parameter( - "MACD fast period must be < slow period", - )); + return Err(RaptorError::invalid_parameter("MACD fast period must be < slow period")); } let n = data.len(); @@ -111,11 +109,7 @@ pub fn macd( let mut histogram = vec![f64::NAN; n]; if slow_period > n { - return Ok(MacdResult { - macd_line, - signal_line, - histogram, - }); + return Ok(MacdResult { macd_line, signal_line, histogram }); } // Calculate fast and slow EMAs @@ -163,11 +157,7 @@ pub fn macd( } } - Ok(MacdResult { - macd_line, - signal_line, - histogram, - }) + Ok(MacdResult { macd_line, signal_line, histogram }) } /// Stochastic oscillator result. @@ -202,9 +192,7 @@ pub fn stochastic( return Err(RaptorError::length_mismatch(n, high.len())); } if k_period == 0 || d_period == 0 { - return Err(RaptorError::invalid_parameter( - "Stochastic periods must be > 0", - )); + return Err(RaptorError::invalid_parameter("Stochastic periods must be > 0")); } let mut k = vec![f64::NAN; n]; diff --git a/src/indicators/strength.rs b/src/indicators/strength.rs index 1697ff4..8923630 100644 --- a/src/indicators/strength.rs +++ b/src/indicators/strength.rs @@ -152,11 +152,7 @@ pub fn directional_movement( let mut adx_values = vec![f64::NAN; n]; if 2 * period > n { - return Ok(DirectionalIndexResult { - plus_di, - minus_di, - adx: adx_values, - }); + return Ok(DirectionalIndexResult { plus_di, minus_di, adx: adx_values }); } // Calculate directional movement @@ -217,11 +213,7 @@ pub fn directional_movement( } } - Ok(DirectionalIndexResult { - plus_di, - minus_di, - adx: adx_values, - }) + Ok(DirectionalIndexResult { plus_di, minus_di, adx: adx_values }) } #[cfg(test)] diff --git a/src/indicators/trend.rs b/src/indicators/trend.rs index d0477b7..8d57f90 100644 --- a/src/indicators/trend.rs +++ b/src/indicators/trend.rs @@ -131,19 +131,14 @@ pub fn supertrend( return Err(RaptorError::length_mismatch(n, high.len())); } if period == 0 { - return Err(RaptorError::invalid_parameter( - "Supertrend period must be > 0", - )); + return Err(RaptorError::invalid_parameter("Supertrend period must be > 0")); } let mut supertrend = vec![f64::NAN; n]; let mut direction = vec![0i8; n]; if period >= n { - return Ok(SupertrendResult { - supertrend, - direction, - }); + return Ok(SupertrendResult { supertrend, direction }); } // Calculate ATR @@ -238,10 +233,7 @@ pub fn supertrend( } } - Ok(SupertrendResult { - supertrend, - direction, - }) + Ok(SupertrendResult { supertrend, direction }) } #[cfg(test)] diff --git a/src/indicators/volatility.rs b/src/indicators/volatility.rs index 4daacbf..137bbb3 100644 --- a/src/indicators/volatility.rs +++ b/src/indicators/volatility.rs @@ -88,14 +88,10 @@ pub struct BollingerBandsResult { /// BollingerBandsResult with middle, upper, lower bands, bandwidth, and %B pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result { if period == 0 { - return Err(RaptorError::invalid_parameter( - "Bollinger Bands period must be > 0", - )); + return Err(RaptorError::invalid_parameter("Bollinger Bands period must be > 0")); } if std_dev <= 0.0 { - return Err(RaptorError::invalid_parameter( - "Bollinger Bands std_dev must be > 0", - )); + return Err(RaptorError::invalid_parameter("Bollinger Bands std_dev must be > 0")); } let n = data.len(); @@ -106,13 +102,7 @@ pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result n { - return Ok(BollingerBandsResult { - middle, - upper, - lower, - bandwidth, - percent_b, - }); + return Ok(BollingerBandsResult { middle, upper, lower, bandwidth, percent_b }); } // Calculate SMA for middle band @@ -130,11 +120,8 @@ pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result() - / period as f64; + let variance: f64 = + data[start..=i].iter().map(|x| (x - mean).powi(2)).sum::() / period as f64; let std = variance.sqrt(); // Calculate bands (std is always non-negative from sqrt) @@ -153,13 +140,7 @@ pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result = (1..=30) - .map(|x| x as f64 + (x as f64 * 0.1).sin()) - .collect(); + let data: Vec = (1..=30).map(|x| x as f64 + (x as f64 * 0.1).sin()).collect(); let result = bollinger_bands(&data, 20, 2.0).unwrap(); diff --git a/src/metrics/drawdown.rs b/src/metrics/drawdown.rs index ddd952d..481cdf6 100644 --- a/src/metrics/drawdown.rs +++ b/src/metrics/drawdown.rs @@ -256,11 +256,7 @@ pub fn drawdown_periods(equity_curve: &[f64]) -> Vec<(usize, usize, f64)> { /// Calmar ratio pub fn calmar_ratio(total_return: f64, max_drawdown: f64) -> f64 { if max_drawdown <= 0.0 { - return if total_return > 0.0 { - f64::INFINITY - } else { - 0.0 - }; + return if total_return > 0.0 { f64::INFINITY } else { 0.0 }; } total_return / max_drawdown } diff --git a/src/metrics/streaming.rs b/src/metrics/streaming.rs index 34c2a54..7a2276f 100644 --- a/src/metrics/streaming.rs +++ b/src/metrics/streaming.rs @@ -206,11 +206,7 @@ impl StreamingMetrics { /// Get profit factor (sum of profits / sum of losses). pub fn profit_factor(&self) -> f64 { if self.sum_negative == 0.0 { - return if self.sum_positive > 0.0 { - f64::INFINITY - } else { - 0.0 - }; + return if self.sum_positive > 0.0 { f64::INFINITY } else { 0.0 }; } self.sum_positive / self.sum_negative.abs() } diff --git a/src/metrics/trade_stats.rs b/src/metrics/trade_stats.rs index a20a144..bdec756 100644 --- a/src/metrics/trade_stats.rs +++ b/src/metrics/trade_stats.rs @@ -117,11 +117,9 @@ impl TradeStatistics { // Average holding period if stats.total_trades > 0 { - stats.avg_holding_period = trades - .iter() - .map(|t| t.holding_period() as f64) - .sum::() - / stats.total_trades as f64; + stats.avg_holding_period = + trades.iter().map(|t| t.holding_period() as f64).sum::() + / stats.total_trades as f64; } // Consecutive wins/losses @@ -231,22 +229,13 @@ pub fn stats_by_exit_reason( pub fn stats_by_direction(trades: &[Trade]) -> (TradeStatistics, TradeStatistics) { use crate::core::types::Direction; - let long_trades: Vec = trades - .iter() - .filter(|t| t.direction == Direction::Long) - .cloned() - .collect(); + let long_trades: Vec = + trades.iter().filter(|t| t.direction == Direction::Long).cloned().collect(); - let short_trades: Vec = trades - .iter() - .filter(|t| t.direction == Direction::Short) - .cloned() - .collect(); + let short_trades: Vec = + trades.iter().filter(|t| t.direction == Direction::Short).cloned().collect(); - ( - TradeStatistics::from_trades(&long_trades), - TradeStatistics::from_trades(&short_trades), - ) + (TradeStatistics::from_trades(&long_trades), TradeStatistics::from_trades(&short_trades)) } #[cfg(test)] diff --git a/src/portfolio/allocation.rs b/src/portfolio/allocation.rs index 55bfeab..13bf1e8 100644 --- a/src/portfolio/allocation.rs +++ b/src/portfolio/allocation.rs @@ -151,18 +151,13 @@ impl CapitalAllocator { let equal = 1.0 / n as f64; vec![equal.min(*max); n] } - _ => weights - .map(|w| w.to_vec()) - .unwrap_or_else(|| vec![1.0 / n as f64; n]), + _ => weights.map(|w| w.to_vec()).unwrap_or_else(|| vec![1.0 / n as f64; n]), }; // Normalize weights let total_weight: f64 = instrument_weights.iter().sum(); let normalized_weights: Vec = if total_weight > 0.0 { - instrument_weights - .iter() - .map(|w| w / total_weight) - .collect() + instrument_weights.iter().map(|w| w / total_weight).collect() } else { vec![1.0 / n as f64; n] }; diff --git a/src/portfolio/engine.rs b/src/portfolio/engine.rs index 86ab836..3046f34 100644 --- a/src/portfolio/engine.rs +++ b/src/portfolio/engine.rs @@ -37,11 +37,7 @@ impl PortfolioEngine { /// Create a new portfolio engine with the given configuration. pub fn new(config: BacktestConfig) -> Self { let fee_model = FeeModel::percentage(config.fees); - let fill_price = if config.upon_bar_close { - FillPrice::Close - } else { - FillPrice::Open - }; + let fill_price = if config.upon_bar_close { FillPrice::Close } else { FillPrice::Open }; Self { config, @@ -77,9 +73,8 @@ impl PortfolioEngine { assert_eq!(n, signals.len(), "OHLCV and signals must have same length"); // Clean signals - let (entries, exits) = self - .signal_processor - .clean_signals(&signals.entries, &signals.exits); + let (entries, exits) = + self.signal_processor.clean_signals(&signals.entries, &signals.exits); // Initialize state let mut position = PositionManager::new(signals.symbol.clone()); @@ -224,8 +219,7 @@ impl PortfolioEngine { if size > 0.0 { // Calculate entry fees let entry_fees = - self.fee_model - .calculate(adjusted_price, size, signals.direction); + self.fee_model.calculate(adjusted_price, size, signals.direction); // Calculate stop and target prices let (stop_price, target_price) = self.calculate_stop_target( @@ -253,11 +247,8 @@ impl PortfolioEngine { } // Calculate equity - let position_value = if position.is_in_position() { - close * position.position.size - } else { - 0.0 - }; + let position_value = + if position.is_in_position() { close * position.position.size } else { 0.0 }; let equity = cash + position_value; equity_curve[i] = equity; @@ -295,13 +286,8 @@ impl PortfolioEngine { } // Calculate final metrics - let metrics = self.calculate_metrics( - &equity_curve, - &drawdown_curve, - &returns, - &trades, - &streaming, - ); + let metrics = + self.calculate_metrics(&equity_curve, &drawdown_curve, &returns, &trades, &streaming); BacktestResult::new(metrics, equity_curve, drawdown_curve, trades, returns) } @@ -396,10 +382,8 @@ impl PortfolioEngine { let total_trades = trades.len(); // Separate closed vs open trades (EndOfData means still open) - let total_open_trades = trades - .iter() - .filter(|t| matches!(t.exit_reason, ExitReason::EndOfData)) - .count(); + let total_open_trades = + trades.iter().filter(|t| matches!(t.exit_reason, ExitReason::EndOfData)).count(); let total_closed_trades = total_trades.saturating_sub(total_open_trades); // Open trade PnL @@ -410,10 +394,8 @@ impl PortfolioEngine { .sum(); // Only count closed trades for win/loss statistics - let closed_trades: Vec<_> = trades - .iter() - .filter(|t| !matches!(t.exit_reason, ExitReason::EndOfData)) - .collect(); + let closed_trades: Vec<_> = + trades.iter().filter(|t| !matches!(t.exit_reason, ExitReason::EndOfData)).collect(); let winning_trades = closed_trades.iter().filter(|t| t.pnl > 0.0).count(); let losing_trades = closed_trades.iter().filter(|t| t.pnl < 0.0).count(); @@ -428,37 +410,18 @@ impl PortfolioEngine { let total_fees_paid: f64 = trades.iter().map(|t| t.fees).sum(); // Best and worst trade - let best_trade_pct = trades - .iter() - .map(|t| t.return_pct) - .fold(f64::NEG_INFINITY, |a, b| a.max(b)); - let best_trade_pct = if best_trade_pct.is_infinite() { - 0.0 - } else { - best_trade_pct - }; + let best_trade_pct = + trades.iter().map(|t| t.return_pct).fold(f64::NEG_INFINITY, |a, b| a.max(b)); + let best_trade_pct = if best_trade_pct.is_infinite() { 0.0 } else { best_trade_pct }; - let worst_trade_pct = trades - .iter() - .map(|t| t.return_pct) - .fold(f64::INFINITY, |a, b| a.min(b)); - let worst_trade_pct = if worst_trade_pct.is_infinite() { - 0.0 - } else { - worst_trade_pct - }; + let worst_trade_pct = + trades.iter().map(|t| t.return_pct).fold(f64::INFINITY, |a, b| a.min(b)); + let worst_trade_pct = if worst_trade_pct.is_infinite() { 0.0 } else { worst_trade_pct }; // Profit factor (based on closed trades) - let gross_profit: f64 = closed_trades - .iter() - .filter(|t| t.pnl > 0.0) - .map(|t| t.pnl) - .sum(); - let gross_loss: f64 = closed_trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.pnl.abs()) - .sum(); + let gross_profit: f64 = closed_trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = + closed_trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { @@ -498,22 +461,14 @@ impl PortfolioEngine { }; let avg_win_pct = if winning_trades > 0 { - closed_trades - .iter() - .filter(|t| t.pnl > 0.0) - .map(|t| t.return_pct) - .sum::() + closed_trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.return_pct).sum::() / winning_trades as f64 } else { 0.0 }; let avg_loss_pct = if losing_trades > 0 { - closed_trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.return_pct) - .sum::() + closed_trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.return_pct).sum::() / losing_trades as f64 } else { 0.0 @@ -547,11 +502,7 @@ impl PortfolioEngine { // Holding period let avg_holding_period = if total_trades > 0 { - trades - .iter() - .map(|t| t.holding_period() as f64) - .sum::() - / total_trades as f64 + trades.iter().map(|t| t.holding_period() as f64).sum::() / total_trades as f64 } else { 0.0 }; @@ -574,11 +525,8 @@ impl PortfolioEngine { let years = num_periods / 365.25; // Convert to years using 365.25 days let total_return_frac = total_return_pct / 100.0; // CAGR = (end/start)^(1/years) - 1 = (1 + total_return)^(1/years) - 1 - let cagr = if years > 0.0 { - (1.0 + total_return_frac).powf(1.0 / years) - 1.0 - } else { - 0.0 - }; + let cagr = + if years > 0.0 { (1.0 + total_return_frac).powf(1.0 / years) - 1.0 } else { 0.0 }; let calmar_ratio = if max_drawdown_pct > 0.0 { cagr / (max_drawdown_pct / 100.0) // Both as fractions } else if total_return_pct > 0.0 { @@ -686,27 +634,18 @@ impl PortfolioEngine { let mean = valid_returns.iter().sum::() / n_valid; // Calculate standard deviation - let variance = valid_returns - .iter() - .map(|r| (r - mean).powi(2)) - .sum::() - / (n_valid - 1.0); + let variance = + valid_returns.iter().map(|r| (r - mean).powi(2)).sum::() / (n_valid - 1.0); let std_dev = variance.sqrt(); // Sharpe Ratio = (mean * periods_per_year) / (std_dev * sqrt(periods_per_year)) // Simplified: Sharpe = mean / std_dev * sqrt(periods_per_year) - let sharpe_ratio = if std_dev > 0.0 { - (mean / std_dev) * periods_per_year.sqrt() - } else { - 0.0 - }; + let sharpe_ratio = + if std_dev > 0.0 { (mean / std_dev) * periods_per_year.sqrt() } else { 0.0 }; // Sortino Ratio - uses downside deviation (only negative returns) - let downside_returns: Vec = valid_returns - .iter() - .filter(|&&r| r < 0.0) - .copied() - .collect(); + let downside_returns: Vec = + valid_returns.iter().filter(|&&r| r < 0.0).copied().collect(); let downside_variance = if !downside_returns.is_empty() { downside_returns.iter().map(|r| r.powi(2)).sum::() / n_valid // Divide by total count, not downside count @@ -726,11 +665,7 @@ impl PortfolioEngine { // Omega Ratio = sum of returns above threshold / |sum of returns below threshold| // With threshold = 0 let sum_positive: f64 = valid_returns.iter().filter(|&&r| r > 0.0).sum(); - let sum_negative: f64 = valid_returns - .iter() - .filter(|&&r| r < 0.0) - .map(|r| r.abs()) - .sum(); + let sum_negative: f64 = valid_returns.iter().filter(|&&r| r < 0.0).map(|r| r.abs()).sum(); let omega_ratio = if sum_negative > 0.0 { sum_positive / sum_negative diff --git a/src/portfolio/position.rs b/src/portfolio/position.rs index c0b7cf1..c2df75e 100644 --- a/src/portfolio/position.rs +++ b/src/portfolio/position.rs @@ -16,11 +16,7 @@ pub struct PositionManager { impl PositionManager { /// Create a new position manager. pub fn new(symbol: String) -> Self { - Self { - position: Position::new(), - trade_counter: 0, - symbol, - } + Self { position: Position::new(), trade_counter: 0, symbol } } /// Check if currently in a position. @@ -67,15 +63,7 @@ impl PositionManager { return false; } - self.position.open( - idx, - price, - size, - direction, - stop_price, - target_price, - entry_fees, - ); + self.position.open(idx, price, size, direction, stop_price, target_price, entry_fees); true } @@ -131,11 +119,7 @@ impl PositionManager { // Calculate return percentage let cost_basis = pos.entry_price * pos.size; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; Trade { id: self.trade_counter, @@ -270,16 +254,14 @@ mod tests { let mut pm = PositionManager::new("TEST".to_string()); // Open position - assert!(pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None)); + assert!(pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None, 0.0)); assert!(pm.is_in_position()); // Try to open another - should fail - assert!(!pm.open_position(1, 1001, 101.0, 10.0, Direction::Long, None, None)); + assert!(!pm.open_position(1, 1001, 101.0, 10.0, Direction::Long, None, None, 0.0)); // Close position with profit - let trade = pm - .close_position(5, 1005, 110.0, 1000, ExitReason::Signal, 2.0) - .unwrap(); + let trade = pm.close_position(5, 1005, 110.0, 1000, ExitReason::Signal, 2.0).unwrap(); assert!(!pm.is_in_position()); assert_eq!(trade.entry_idx, 0); @@ -295,12 +277,10 @@ mod tests { fn test_short_position() { let mut pm = PositionManager::new("TEST".to_string()); - pm.open_position(0, 1000, 100.0, 10.0, Direction::Short, None, None); + pm.open_position(0, 1000, 100.0, 10.0, Direction::Short, None, None, 0.0); // Close with profit (price went down) - let trade = pm - .close_position(5, 1005, 90.0, 1000, ExitReason::Signal, 2.0) - .unwrap(); + let trade = pm.close_position(5, 1005, 90.0, 1000, ExitReason::Signal, 2.0).unwrap(); // P&L: (100 - 90) * 10 * -(-1) - 2 = 98 // For short: (entry - exit) * size = (100 - 90) * 10 = 100 gross, minus 2 fees = 98 @@ -319,6 +299,7 @@ mod tests { Direction::Long, Some(95.0), // Stop at 95 None, + 0.0, ); // Check stop not hit @@ -332,7 +313,7 @@ mod tests { fn test_trailing_stop() { let mut pm = PositionManager::new("TEST".to_string()); - pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None); + pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None, 0.0); // Update with higher price pm.update_price(110.0, 98.0); @@ -353,7 +334,7 @@ mod tests { fn test_unrealized_pnl() { let mut pm = PositionManager::new("TEST".to_string()); - pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None); + pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None, 0.0); // Price up let pnl = pm.unrealized_pnl(110.0); diff --git a/src/python/bindings.rs b/src/python/bindings.rs index c548408..734d5b9 100644 --- a/src/python/bindings.rs +++ b/src/python/bindings.rs @@ -115,12 +115,7 @@ pub struct PyStopConfig { impl PyStopConfig { #[new] fn new() -> Self { - Self { - stop_type: "none".to_string(), - percent: None, - multiplier: None, - period: None, - } + Self { stop_type: "none".to_string(), percent: None, multiplier: None, period: None } } #[staticmethod] @@ -689,13 +684,7 @@ pub fn run_multi_backtest<'py>( low: PyReadonlyArray1, close: PyReadonlyArray1, volume: PyReadonlyArray1, - strategies: Vec<( - PyReadonlyArray1, - PyReadonlyArray1, - i32, - f64, - String, - )>, + strategies: Vec<(PyReadonlyArray1, PyReadonlyArray1, i32, f64, String)>, config: Option<&PyBacktestConfig>, combine_mode: &str, ) -> PyResult { @@ -819,10 +808,7 @@ pub fn stochastic<'py>( let c = numpy_to_vec_f64(close); let result = indicators::momentum::stochastic(&h, &l, &c, k_period, d_period) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; - Ok(( - vec_to_numpy_f64(py, result.k), - vec_to_numpy_f64(py, result.d), - )) + Ok((vec_to_numpy_f64(py, result.k), vec_to_numpy_f64(py, result.d))) } /// Average True Range. diff --git a/src/signals/expression.rs b/src/signals/expression.rs index ba7af07..6d83f6b 100644 --- a/src/signals/expression.rs +++ b/src/signals/expression.rs @@ -321,10 +321,8 @@ pub fn is_highest(a: &[f64], window: usize) -> Vec { continue; } - let max_in_window = a[start..=i] - .iter() - .filter(|v| !v.is_nan()) - .fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + let max_in_window = + a[start..=i].iter().filter(|v| !v.is_nan()).fold(f64::NEG_INFINITY, |a, &b| a.max(b)); result[i] = (current - max_in_window).abs() < 1e-10; } @@ -355,10 +353,8 @@ pub fn is_lowest(a: &[f64], window: usize) -> Vec { continue; } - let min_in_window = a[start..=i] - .iter() - .filter(|v| !v.is_nan()) - .fold(f64::INFINITY, |a, &b| a.min(b)); + let min_in_window = + a[start..=i].iter().filter(|v| !v.is_nan()).fold(f64::INFINITY, |a, &b| a.min(b)); result[i] = (current - min_in_window).abs() < 1e-10; } diff --git a/src/signals/processor.rs b/src/signals/processor.rs index a1097c3..f43f650 100644 --- a/src/signals/processor.rs +++ b/src/signals/processor.rs @@ -16,10 +16,7 @@ pub struct SignalProcessor { impl Default for SignalProcessor { fn default() -> Self { - Self { - allow_pyramiding: false, - max_pyramid_entries: 1, - } + Self { allow_pyramiding: false, max_pyramid_entries: 1 } } } @@ -55,11 +52,7 @@ impl SignalProcessor { /// Tuple of (cleaned_entries, cleaned_exits) pub fn clean_signals(&self, entries: &[bool], exits: &[bool]) -> (Vec, Vec) { let n = entries.len(); - assert_eq!( - n, - exits.len(), - "Entry and exit arrays must have same length" - ); + assert_eq!(n, exits.len(), "Entry and exit arrays must have same length"); let mut clean_entries = vec![false; n]; let mut clean_exits = vec![false; n]; @@ -140,12 +133,7 @@ impl SignalProcessor { let mut clean_short_exits = vec![false; n]; if n == 0 { - return ( - clean_long_entries, - clean_long_exits, - clean_short_entries, - clean_short_exits, - ); + return (clean_long_entries, clean_long_exits, clean_short_entries, clean_short_exits); } let mut current_direction: Option = None; @@ -189,12 +177,7 @@ impl SignalProcessor { } } - ( - clean_long_entries, - clean_long_exits, - clean_short_entries, - clean_short_exits, - ) + (clean_long_entries, clean_long_exits, clean_short_entries, clean_short_exits) } /// Generate exit-on-opposite-entry signals. @@ -248,11 +231,8 @@ impl SignalProcessor { .filter_map(|(i, &e)| if e { Some(i) } else { None }) .collect(); - let exit_indices: Vec = exits - .iter() - .enumerate() - .filter_map(|(i, &e)| if e { Some(i) } else { None }) - .collect(); + let exit_indices: Vec = + exits.iter().enumerate().filter_map(|(i, &e)| if e { Some(i) } else { None }).collect(); (entry_indices, exit_indices) } diff --git a/src/signals/synchronizer.rs b/src/signals/synchronizer.rs index a843e1c..dced71c 100644 --- a/src/signals/synchronizer.rs +++ b/src/signals/synchronizer.rs @@ -34,28 +34,19 @@ pub struct SignalSynchronizer { impl Default for SignalSynchronizer { fn default() -> Self { - Self { - mode: SyncMode::All, - min_signals: None, - } + Self { mode: SyncMode::All, min_signals: None } } } impl SignalSynchronizer { /// Create a new signal synchronizer with the given mode. pub fn new(mode: SyncMode) -> Self { - Self { - mode, - min_signals: None, - } + Self { mode, min_signals: None } } /// Create a synchronizer with a custom minimum signal threshold. pub fn with_min_signals(min: usize) -> Self { - Self { - mode: SyncMode::Majority, - min_signals: Some(min), - } + Self { mode: SyncMode::Majority, min_signals: Some(min) } } /// Synchronize entry signals from multiple instruments. @@ -155,15 +146,10 @@ impl SignalSynchronizer { return (vec![], vec![]); } - let entries: Vec<&[bool]> = compiled_signals - .iter() - .map(|cs| cs.entries.as_slice()) - .collect(); + let entries: Vec<&[bool]> = + compiled_signals.iter().map(|cs| cs.entries.as_slice()).collect(); - let exits: Vec<&[bool]> = compiled_signals - .iter() - .map(|cs| cs.exits.as_slice()) - .collect(); + let exits: Vec<&[bool]> = compiled_signals.iter().map(|cs| cs.exits.as_slice()).collect(); let synced_entries = self.sync_entries(&entries); let synced_exits = self.sync_exits(&exits); diff --git a/src/stops/atr.rs b/src/stops/atr.rs index 71391b5..3ab3b21 100644 --- a/src/stops/atr.rs +++ b/src/stops/atr.rs @@ -108,12 +108,7 @@ pub struct ChandelierExit { impl ChandelierExit { /// Create a new Chandelier exit. pub fn new(multiplier: f64, atr: f64) -> Self { - Self { - multiplier, - atr, - highest_high: 0.0, - lowest_low: f64::MAX, - } + Self { multiplier, atr, highest_high: 0.0, lowest_low: f64::MAX } } /// Reset for new position. diff --git a/src/stops/fixed.rs b/src/stops/fixed.rs index 3718478..b2dface 100644 --- a/src/stops/fixed.rs +++ b/src/stops/fixed.rs @@ -13,9 +13,7 @@ pub struct FixedStop { impl FixedStop { /// Create a new fixed stop with given percentage. pub fn new(percent: f64) -> Self { - Self { - percent: percent.abs(), - } + Self { percent: percent.abs() } } /// Create a 1% stop. @@ -66,9 +64,7 @@ pub struct FixedTarget { impl FixedTarget { /// Create a new fixed target with given percentage. pub fn new(percent: f64) -> Self { - Self { - percent: percent.abs(), - } + Self { percent: percent.abs() } } } diff --git a/src/stops/trailing.rs b/src/stops/trailing.rs index e781272..ec87a2b 100644 --- a/src/stops/trailing.rs +++ b/src/stops/trailing.rs @@ -15,10 +15,7 @@ pub struct TrailingStop { impl TrailingStop { /// Create a new trailing stop. pub fn new(percent: f64) -> Self { - Self { - percent: percent.abs(), - activation_threshold: None, - } + Self { percent: percent.abs(), activation_threshold: None } } /// Create with activation threshold. @@ -89,9 +86,7 @@ pub struct PointTrailingStop { impl PointTrailingStop { /// Create a new point-based trailing stop. pub fn new(points: f64) -> Self { - Self { - points: points.abs(), - } + Self { points: points.abs() } } } @@ -137,10 +132,7 @@ pub struct StepTrailingStop { impl StepTrailingStop { /// Create a new step trailing stop. pub fn new(step_percent: f64, trail_percent: f64) -> Self { - Self { - step_percent: step_percent.abs(), - trail_percent: trail_percent.abs(), - } + Self { step_percent: step_percent.abs(), trail_percent: trail_percent.abs() } } /// Calculate stop for a given step level. diff --git a/src/strategies/basket.rs b/src/strategies/basket.rs index f34a0cb..50e07f0 100644 --- a/src/strategies/basket.rs +++ b/src/strategies/basket.rs @@ -83,31 +83,22 @@ impl BasketBacktest { // Verify all instruments have same length for (ohlcv, signals) in instruments { - assert_eq!( - ohlcv.len(), - n_bars, - "All instruments must have same number of bars" - ); + assert_eq!(ohlcv.len(), n_bars, "All instruments must have same number of bars"); assert_eq!(signals.len(), n_bars, "Signals must match OHLCV length"); } // Synchronize signals - let entry_signals: Vec<&[bool]> = instruments - .iter() - .map(|(_, s)| s.entries.as_slice()) - .collect(); - let exit_signals: Vec<&[bool]> = instruments - .iter() - .map(|(_, s)| s.exits.as_slice()) - .collect(); + let entry_signals: Vec<&[bool]> = + instruments.iter().map(|(_, s)| s.entries.as_slice()).collect(); + let exit_signals: Vec<&[bool]> = + instruments.iter().map(|(_, s)| s.exits.as_slice()).collect(); let synced_entries = self.synchronizer.sync_entries(&entry_signals); let synced_exits = self.synchronizer.sync_exits(&exit_signals); // Clean signals - let (clean_entries, clean_exits) = self - .signal_processor - .clean_signals(&synced_entries, &synced_exits); + let (clean_entries, clean_exits) = + self.signal_processor.clean_signals(&synced_entries, &synced_exits); // Initialize state let mut cash = self.config.base.initial_capital; @@ -136,8 +127,7 @@ impl BasketBacktest { if let Some(pos) = positions[inst_idx].take() { let exit_price = ohlcv.close[i]; let fees = - self.fee_model - .calculate(exit_price, pos.size, signals.direction); + self.fee_model.calculate(exit_price, pos.size, signals.direction); let pnl = (exit_price - pos.entry_price) * pos.size @@ -145,11 +135,8 @@ impl BasketBacktest { - fees; let cost_basis = pos.entry_price * pos.size; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = + if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; cash += exit_price * pos.size - fees; @@ -188,16 +175,11 @@ impl BasketBacktest { let size = sizes[inst_idx]; if size > 0.0 { let entry_price = ohlcv.close[i]; - let fees = self - .fee_model - .calculate(entry_price, size, signals.direction); + let fees = self.fee_model.calculate(entry_price, size, signals.direction); cash -= entry_price * size + fees; - positions[inst_idx] = Some(PositionState { - entry_idx: i, - entry_price, - size, - }); + positions[inst_idx] = + Some(PositionState { entry_idx: i, entry_price, size }); } } } @@ -229,20 +211,14 @@ impl BasketBacktest { for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() { if let Some(pos) = positions[inst_idx].take() { let exit_price = ohlcv.close[last_idx]; - let fees = self - .fee_model - .calculate(exit_price, pos.size, signals.direction); + let fees = self.fee_model.calculate(exit_price, pos.size, signals.direction); let pnl = (exit_price - pos.entry_price) * pos.size * signals.direction.multiplier() - fees; let cost_basis = pos.entry_price * pos.size; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; trades.push(Trade { id: trade_counter, @@ -319,11 +295,7 @@ impl BasketBacktest { }; let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); - let gross_loss: f64 = trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.pnl.abs()) - .sum(); + let gross_loss: f64 = trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { @@ -386,6 +358,7 @@ struct PositionState { #[cfg(test)] mod tests { use super::*; + use crate::core::Direction; fn sample_instruments() -> Vec<(OhlcvData, CompiledSignals)> { let n = 20; @@ -454,10 +427,7 @@ mod tests { #[test] fn test_sync_mode_all() { - let config = BasketConfig { - sync_mode: SyncMode::All, - ..Default::default() - }; + let config = BasketConfig { sync_mode: SyncMode::All, ..Default::default() }; let backtest = BasketBacktest::new(config); let instruments = sample_instruments(); diff --git a/src/strategies/multi.rs b/src/strategies/multi.rs index 89b6bb3..b0a1c80 100644 --- a/src/strategies/multi.rs +++ b/src/strategies/multi.rs @@ -66,10 +66,7 @@ pub struct MultiStrategyBacktest { impl MultiStrategyBacktest { /// Create a new multi-strategy backtest. pub fn new(config: MultiStrategyConfig) -> Self { - Self { - fee_model: FeeModel::percentage(config.base.fees), - config, - } + Self { fee_model: FeeModel::percentage(config.base.fees), config } } /// Run multi-strategy backtest. @@ -87,11 +84,7 @@ impl MultiStrategyBacktest { let n = ohlcv.len(); for signals in strategies { - assert_eq!( - signals.len(), - n, - "All strategies must have same length as OHLCV" - ); + assert_eq!(signals.len(), n, "All strategies must have same length as OHLCV"); } match self.config.combine_mode { @@ -113,10 +106,8 @@ impl MultiStrategyBacktest { let mut strategy_equities: Vec> = Vec::new(); for (strat_idx, signals) in strategies.iter().enumerate() { - let single_config = BacktestConfig { - initial_capital: capital_per, - ..self.config.base.clone() - }; + let single_config = + BacktestConfig { initial_capital: capital_per, ..self.config.base.clone() }; let single = crate::strategies::single::SingleBacktest::new(single_config); let result = single.run(ohlcv, signals); @@ -168,13 +159,7 @@ impl MultiStrategyBacktest { self.config.base.initial_capital, ); - BacktestResult::new( - metrics, - combined_equity, - drawdown_curve, - all_trades, - returns, - ) + BacktestResult::new(metrics, combined_equity, drawdown_curve, all_trades, returns) } /// Run strategies with combined signals. @@ -200,19 +185,11 @@ impl MultiStrategyBacktest { .enumerate() .filter(|(_, s)| s.entries[i]) .map(|(idx, _)| { - self.config - .strategy_weights - .get(idx) - .copied() - .unwrap_or(1.0) + self.config.strategy_weights.get(idx).copied().unwrap_or(1.0) }) .sum(); - let total_weight: f64 = self - .config - .strategy_weights - .iter() - .sum::() - .max(n_strategies as f64); + let total_weight: f64 = + self.config.strategy_weights.iter().sum::().max(n_strategies as f64); weighted_sum / total_weight > 0.5 } CombineMode::Independent => unreachable!(), @@ -266,11 +243,7 @@ impl MultiStrategyBacktest { }; let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); - let gross_loss: f64 = trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.pnl.abs()) - .sum(); + let gross_loss: f64 = trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { @@ -319,6 +292,7 @@ impl MultiStrategyBacktest { #[cfg(test)] mod tests { use super::*; + use crate::core::Direction; fn sample_strategies() -> (OhlcvData, Vec) { let n = 20; @@ -367,10 +341,7 @@ mod tests { #[test] fn test_multi_any_mode() { - let config = MultiStrategyConfig { - combine_mode: CombineMode::Any, - ..Default::default() - }; + let config = MultiStrategyConfig { combine_mode: CombineMode::Any, ..Default::default() }; let backtest = MultiStrategyBacktest::new(config); let (ohlcv, strategies) = sample_strategies(); @@ -382,10 +353,7 @@ mod tests { #[test] fn test_multi_all_mode() { - let config = MultiStrategyConfig { - combine_mode: CombineMode::All, - ..Default::default() - }; + let config = MultiStrategyConfig { combine_mode: CombineMode::All, ..Default::default() }; let backtest = MultiStrategyBacktest::new(config); let (ohlcv, strategies) = sample_strategies(); @@ -397,10 +365,8 @@ mod tests { #[test] fn test_multi_independent_mode() { - let config = MultiStrategyConfig { - combine_mode: CombineMode::Independent, - ..Default::default() - }; + let config = + MultiStrategyConfig { combine_mode: CombineMode::Independent, ..Default::default() }; let backtest = MultiStrategyBacktest::new(config); let (ohlcv, strategies) = sample_strategies(); diff --git a/src/strategies/options.rs b/src/strategies/options.rs index 6254450..cb6b4fc 100644 --- a/src/strategies/options.rs +++ b/src/strategies/options.rs @@ -100,10 +100,7 @@ pub struct OptionsBacktest { impl OptionsBacktest { /// Create a new options backtest. pub fn new(config: OptionsConfig) -> Self { - Self { - fee_model: FeeModel::percentage(config.base.fees), - config, - } + Self { fee_model: FeeModel::percentage(config.base.fees), config } } /// Run options backtest. @@ -158,11 +155,7 @@ impl OptionsBacktest { let pnl = self.calculate_pnl(&pos, exit_price) - fees; let cost_basis = pos.entry_price * pos.contracts as f64 * self.config.lot_size as f64; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; cash += exit_price * pos.contracts as f64 * self.config.lot_size as f64 - fees; @@ -196,8 +189,7 @@ impl OptionsBacktest { if contracts > 0 { let entry_cost = option_price * contracts as f64 * self.config.lot_size as f64; let fees = - self.fee_model - .calculate(option_price, contracts as f64, signals.direction); + self.fee_model.calculate(option_price, contracts as f64, signals.direction); cash -= entry_cost + fees; @@ -237,16 +229,11 @@ impl OptionsBacktest { let last_idx = n - 1; let exit_price = option_prices[last_idx]; let fees = - self.fee_model - .calculate(exit_price, pos.contracts as f64, signals.direction); + self.fee_model.calculate(exit_price, pos.contracts as f64, signals.direction); let pnl = self.calculate_pnl(&pos, exit_price) - fees; let cost_basis = pos.entry_price * pos.contracts as f64 * self.config.lot_size as f64; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; trades.push(Trade { id: trade_counter, @@ -354,11 +341,7 @@ impl OptionsBacktest { }; let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); - let gross_loss: f64 = trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.pnl.abs()) - .sum(); + let gross_loss: f64 = trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { @@ -436,11 +419,8 @@ mod tests { #[test] fn test_position_sizing_percent() { - let config = OptionsConfig { - size_type: SizeType::Percent(0.5), - lot_size: 50, - ..Default::default() - }; + let config = + OptionsConfig { size_type: SizeType::Percent(0.5), lot_size: 50, ..Default::default() }; let backtest = OptionsBacktest::new(config); // 50% of 100000 = 50000, option at 100 * lot 50 = 5000 per contract diff --git a/src/strategies/pairs.rs b/src/strategies/pairs.rs index a1e7bea..10c6391 100644 --- a/src/strategies/pairs.rs +++ b/src/strategies/pairs.rs @@ -54,10 +54,7 @@ pub struct PairsBacktest { impl PairsBacktest { /// Create a new pairs backtest. pub fn new(config: PairsConfig) -> Self { - Self { - fee_model: FeeModel::percentage(config.base.fees), - config, - } + Self { fee_model: FeeModel::percentage(config.base.fees), config } } /// Run pairs trading backtest. @@ -114,11 +111,7 @@ impl PairsBacktest { if let Some(pos) = position.take() { let (pnl, fees) = self.close_position(&pos, leg1_price, leg2_price); let cost_basis = pos.leg1_cost + pos.leg2_cost; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; // Return capital cash += pos.leg1_size * leg1_price + pos.leg2_size * leg2_price - fees; @@ -240,11 +233,7 @@ impl PairsBacktest { let (pnl, fees) = self.close_position(&pos, leg1_price, leg2_price); let cost_basis = pos.leg1_cost + pos.leg2_cost; - let return_pct = if cost_basis > 0.0 { - pnl / cost_basis * 100.0 - } else { - 0.0 - }; + let return_pct = if cost_basis > 0.0 { pnl / cost_basis * 100.0 } else { 0.0 }; trades.push(Trade { id: trade_counter, @@ -281,11 +270,7 @@ impl PairsBacktest { let sum_x: f64 = leg2_prices.iter().sum(); let sum_y: f64 = leg1_prices.iter().sum(); - let sum_xy: f64 = leg1_prices - .iter() - .zip(leg2_prices.iter()) - .map(|(y, x)| x * y) - .sum(); + let sum_xy: f64 = leg1_prices.iter().zip(leg2_prices.iter()).map(|(y, x)| x * y).sum(); let sum_x2: f64 = leg2_prices.iter().map(|x| x * x).sum(); let denominator = n * sum_x2 - sum_x * sum_x; @@ -313,11 +298,8 @@ impl PairsBacktest { * position.leg2_direction.multiplier(); let exit_fees = - self.fee_model - .calculate(leg1_price, position.leg1_size, position.leg1_direction) - + self - .fee_model - .calculate(leg2_price, position.leg2_size, position.leg2_direction); + self.fee_model.calculate(leg1_price, position.leg1_size, position.leg1_direction) + + self.fee_model.calculate(leg2_price, position.leg2_size, position.leg2_direction); let total_pnl = leg1_pnl + leg2_pnl - exit_fees; @@ -340,10 +322,8 @@ impl PairsBacktest { // For pairs, count trade pairs (every 2 trades = 1 round trip) let total_trades = trades.len() / 2; - let winning_trades = trades - .chunks(2) - .filter(|chunk| chunk.iter().map(|t| t.pnl).sum::() > 0.0) - .count(); + let winning_trades = + trades.chunks(2).filter(|chunk| chunk.iter().map(|t| t.pnl).sum::() > 0.0).count(); let losing_trades = total_trades.saturating_sub(winning_trades); let win_rate_pct = if total_trades > 0 { @@ -353,11 +333,7 @@ impl PairsBacktest { }; let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); - let gross_loss: f64 = trades - .iter() - .filter(|t| t.pnl < 0.0) - .map(|t| t.pnl.abs()) - .sum(); + let gross_loss: f64 = trades.iter().filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { @@ -463,11 +439,7 @@ mod tests { #[test] fn test_hedge_ratio_calculation() { - let config = PairsConfig { - dynamic_hedge: true, - hedge_lookback: 5, - ..Default::default() - }; + let config = PairsConfig { dynamic_hedge: true, hedge_lookback: 5, ..Default::default() }; let backtest = PairsBacktest::new(config); let leg1 = vec![100.0, 102.0, 104.0, 106.0, 108.0]; diff --git a/src/strategies/single.rs b/src/strategies/single.rs index 7a52d61..f9b1d5a 100644 --- a/src/strategies/single.rs +++ b/src/strategies/single.rs @@ -13,9 +13,7 @@ pub struct SingleBacktest { impl SingleBacktest { /// Create a new single instrument backtest. pub fn new(config: BacktestConfig) -> Self { - Self { - engine: PortfolioEngine::new(config), - } + Self { engine: PortfolioEngine::new(config) } } /// Run the backtest. @@ -181,12 +179,8 @@ mod tests { let low: Vec = close.iter().map(|x| x - 1.0).collect(); let volume = vec![1000.0; 10]; - let entries = vec![ - false, true, false, false, false, false, false, false, false, false, - ]; - let exits = vec![ - false, false, false, false, false, true, false, false, false, false, - ]; + let entries = vec![false, true, false, false, false, false, false, false, false, false]; + let exits = vec![false, false, false, false, false, true, false, false, false, false]; let result = backtest.run_from_arrays( ×tamps, diff --git a/tests/test_indicators.rs b/tests/test_indicators.rs index 2850a1b..2fa42a4 100644 --- a/tests/test_indicators.rs +++ b/tests/test_indicators.rs @@ -103,23 +103,13 @@ fn test_stochastic_range() { // %K and %D should be in [0, 100] for (i, &k) in result.k.iter().enumerate() { if !k.is_nan() { - assert!( - k >= 0.0 && k <= 100.0, - "%K at index {} is out of range: {}", - i, - k - ); + assert!(k >= 0.0 && k <= 100.0, "%K at index {} is out of range: {}", i, k); } } for (i, &d) in result.d.iter().enumerate() { if !d.is_nan() { - assert!( - d >= 0.0 && d <= 100.0, - "%D at index {} is out of range: {}", - i, - d - ); + assert!(d >= 0.0 && d <= 100.0, "%D at index {} is out of range: {}", i, d); } } } diff --git a/tests/test_portfolio.rs b/tests/test_portfolio.rs index a919c49..35dcaf7 100644 --- a/tests/test_portfolio.rs +++ b/tests/test_portfolio.rs @@ -237,12 +237,7 @@ fn test_short_direction() { let ohlcv = OhlcvData { timestamps: (0..n as i64).collect(), - open: close - .iter() - .skip(1) - .chain(std::iter::once(&close[n - 1])) - .cloned() - .collect(), + open: close.iter().skip(1).chain(std::iter::once(&close[n - 1])).cloned().collect(), high: close.iter().map(|c| c + 1.0).collect(), low: close.iter().map(|c| c - 1.0).collect(), close: close.clone(),