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:
+18
-48
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
×tamps,
|
||||
|
||||
Reference in New Issue
Block a user