Fix CI workflow: use correct rust-toolchain action (#1)

* Fix CI workflow: use correct rust-toolchain action

* Fix CI: resolve Rust compilation and formatting issues
- Fix rust-toolchain action name in CI workflow
- Add missing Direction import in test modules
- Add missing entry_fees argument to open_position test calls
- Comment out nightly-only rustfmt options
- Auto-format code with cargo fmt
This commit is contained in:
vatsal
2026-01-28 15:38:31 +05:30
committed by GitHub
parent e6476b771f
commit 0f032dd39e
32 changed files with 193 additions and 660 deletions
+18 -48
View File
@@ -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();
+14 -48
View File
@@ -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<f64>> = 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::<f64>()
.max(n_strategies as f64);
let total_weight: f64 =
self.config.strategy_weights.iter().sum::<f64>().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<CompiledSignals>) {
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();
+8 -28
View File
@@ -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
+10 -38
View File
@@ -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::<f64>() > 0.0)
.count();
let winning_trades =
trades.chunks(2).filter(|chunk| chunk.iter().map(|t| t.pnl).sum::<f64>() > 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];
+3 -9
View File
@@ -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<f64> = 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(
&timestamps,