style: apply cargo fmt formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Pratik Bhadane
2026-04-01 20:12:19 +05:30
co-authored by Claude Sonnet 4.6
parent 3ab6daa853
commit 70b99ad870
17 changed files with 128 additions and 128 deletions
+11 -17
View File
@@ -21,14 +21,13 @@ type Ohlcv5AndLabels = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<i6
///
/// # Panics
/// Panics if `ticks_per_bar == 0`, arrays are empty, or lengths differ.
pub fn aggregate_tick_bars(
price: &[f64],
size: &[f64],
ticks_per_bar: usize,
) -> Ohlcv5 {
pub fn aggregate_tick_bars(price: &[f64], size: &[f64], ticks_per_bar: usize) -> Ohlcv5 {
assert!(ticks_per_bar >= 1, "ticks_per_bar must be >= 1");
let n = price.len();
assert!(n > 0 && size.len() == n, "price and size must be non-empty and equal length");
assert!(
n > 0 && size.len() == n,
"price and size must be non-empty and equal length"
);
let n_bars = n.div_ceil(ticks_per_bar);
let mut out_open = Vec::with_capacity(n_bars);
@@ -71,14 +70,13 @@ pub fn aggregate_tick_bars(
///
/// # Panics
/// Panics if `volume_threshold <= 0`, arrays are empty, or lengths differ.
pub fn aggregate_volume_bars_ticks(
price: &[f64],
size: &[f64],
volume_threshold: f64,
) -> Ohlcv5 {
pub fn aggregate_volume_bars_ticks(price: &[f64], size: &[f64], volume_threshold: f64) -> Ohlcv5 {
assert!(volume_threshold > 0.0, "volume_threshold must be > 0");
let n = price.len();
assert!(n > 0 && size.len() == n, "price and size must be non-empty and equal length");
assert!(
n > 0 && size.len() == n,
"price and size must be non-empty and equal length"
);
let mut out_open: Vec<f64> = Vec::new();
let mut out_high: Vec<f64> = Vec::new();
@@ -140,11 +138,7 @@ pub fn aggregate_volume_bars_ticks(
///
/// # Panics
/// Panics if arrays are empty or have unequal lengths.
pub fn aggregate_time_bars(
price: &[f64],
size: &[f64],
labels: &[i64],
) -> Ohlcv5AndLabels {
pub fn aggregate_time_bars(price: &[f64], size: &[f64], labels: &[i64]) -> Ohlcv5AndLabels {
let n = price.len();
assert!(
n > 0 && size.len() == n && labels.len() == n,
+5 -1
View File
@@ -28,7 +28,11 @@ use std::collections::HashMap;
pub fn trade_stats(pnl: &[f64], hold_bars: &[f64]) -> (f64, f64, f64, f64, f64) {
let n = pnl.len();
assert!(n > 0, "pnl must be non-empty");
assert_eq!(n, hold_bars.len(), "pnl and hold_bars must have equal length");
assert_eq!(
n,
hold_bars.len(),
"pnl and hold_bars must have equal length"
);
let mut wins: Vec<f64> = Vec::new();
let mut losses: Vec<f64> = Vec::new();
+28 -13
View File
@@ -287,7 +287,12 @@ pub struct StreamingSummary {
// ---------------------------------------------------------------------------
/// RSI threshold strategy: +1 when RSI <= oversold, -1 when RSI >= overbought, 0 otherwise.
pub fn rsi_threshold_signals(close: &[f64], timeperiod: usize, oversold: f64, overbought: f64) -> Vec<f64> {
pub fn rsi_threshold_signals(
close: &[f64],
timeperiod: usize,
oversold: f64,
overbought: f64,
) -> Vec<f64> {
let rsi = crate::momentum::rsi(close, timeperiod);
rsi.iter()
.map(|&v| {
@@ -1128,8 +1133,7 @@ pub fn compute_performance_metrics(
}
let mean_r: f64 = valid_r.iter().sum::<f64>() / n_valid as f64;
let variance: f64 =
valid_r.iter().map(|&v| (v - mean_r).powi(2)).sum::<f64>() / n_valid as f64;
let variance: f64 = valid_r.iter().map(|&v| (v - mean_r).powi(2)).sum::<f64>() / n_valid as f64;
let std_r = variance.sqrt();
let downside_sq_sum: f64 = valid_r
@@ -1253,13 +1257,19 @@ pub fn compute_performance_metrics(
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
});
let p5 = pct_r[idx_5];
let worst_bar = pct_r[..=idx_5].iter().copied().fold(f64::INFINITY, f64::min);
let worst_bar = pct_r[..=idx_5]
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
// Find 95th percentile in the remaining upper partition
pct_r[idx_5..].select_nth_unstable_by(idx_95 - idx_5, |a, b| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
});
let p95 = pct_r[idx_95];
let best_bar = pct_r[idx_95..].iter().copied().fold(f64::NEG_INFINITY, f64::max);
let best_bar = pct_r[idx_95..]
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let tail_ratio = if p5.abs() > 0.0 {
p95.abs() / p5.abs()
} else {
@@ -1451,10 +1461,8 @@ pub fn extract_trades_ohlcv(
}
} else {
if trade_entry_price > 0.0 {
let unreal_high =
trade_dir * (high[i] - trade_entry_price) / trade_entry_price;
let unreal_low =
trade_dir * (low[i] - trade_entry_price) / trade_entry_price;
let unreal_high = trade_dir * (high[i] - trade_entry_price) / trade_entry_price;
let unreal_low = trade_dir * (low[i] - trade_entry_price) / trade_entry_price;
let bar_best = unreal_high.max(unreal_low);
let bar_worst = unreal_high.min(unreal_low);
if bar_best > trade_mfe {
@@ -1615,8 +1623,12 @@ pub fn backtest_multi_asset_core(
// Per-asset backtests
let asset_strategy_returns: Vec<Vec<f64>> = (0..n_assets)
.map(|j| {
let (_, strat_rets, _) =
single_asset_backtest(&close_2d[j], &constrained[j], commission_per_trade, slippage_bps);
let (_, strat_rets, _) = single_asset_backtest(
&close_2d[j],
&constrained[j],
commission_per_trade,
slippage_bps,
);
strat_rets
})
.collect();
@@ -1757,7 +1769,9 @@ pub fn walk_forward_indices(
}
if folds.is_empty() {
return Err("No complete folds fit within n_bars with the given train/test sizes".to_string());
return Err(
"No complete folds fit within n_bars with the given train/test sizes".to_string(),
);
}
Ok(folds)
@@ -2018,7 +2032,8 @@ mod tests {
let signals: Vec<f64> = vec![0.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0];
let config = BacktestConfig::default();
let result = backtest_ohlcv_core(&open, &high, &low, &close, &signals, &config, None).unwrap();
let result =
backtest_ohlcv_core(&open, &high, &low, &close, &signals, &config, None).unwrap();
assert_eq!(result.equity.len(), n);
// Equity should be positive
assert!(*result.equity.last().unwrap() > 0.0);
+14 -16
View File
@@ -46,11 +46,7 @@ fn validate_hlc_columns(
return Ok((0, 0));
}
let n = high[0].len();
for (idx, (h, (l, c))) in high
.iter()
.zip(low.iter().zip(close.iter()))
.enumerate()
{
for (idx, (h, (l, c))) in high.iter().zip(low.iter().zip(close.iter())).enumerate() {
if h.len() != n || l.len() != n || c.len() != n {
return Err(format!(
"column {idx}: high len={}, low len={}, close len={} — must all be {n}",
@@ -358,24 +354,26 @@ fn compute_close_indicator(
.collect()),
"LINEARREG" => {
let last_x = (timeperiod - 1) as f64;
Ok(rolling_linreg_apply(close, timeperiod, |slope, intercept| {
intercept + slope * last_x
}))
Ok(rolling_linreg_apply(
close,
timeperiod,
|slope, intercept| intercept + slope * last_x,
))
}
"LINEARREG_SLOPE" => Ok(rolling_linreg_apply(close, timeperiod, |slope, _| slope)),
"LINEARREG_INTERCEPT" => {
Ok(rolling_linreg_apply(close, timeperiod, |_, intercept| {
intercept
}))
}
"LINEARREG_INTERCEPT" => Ok(rolling_linreg_apply(close, timeperiod, |_, intercept| {
intercept
})),
"LINEARREG_ANGLE" => Ok(rolling_linreg_apply(close, timeperiod, |slope, _| {
slope.atan() * 180.0 / std::f64::consts::PI
})),
"TSF" => {
let forecast_x = timeperiod as f64;
Ok(rolling_linreg_apply(close, timeperiod, |slope, intercept| {
intercept + slope * forecast_x
}))
Ok(rolling_linreg_apply(
close,
timeperiod,
|slope, intercept| intercept + slope * forecast_x,
))
}
_ => Err(format!(
"unsupported close indicator for grouped execution: {name}"
+4 -4
View File
@@ -72,11 +72,11 @@ mod tests {
fn test_mark_session_boundaries() {
let ns_per_day: i64 = 86_400_000_000_000;
let ts = vec![
0, // day 0
ns_per_day / 2, // day 0
ns_per_day, // day 1
0, // day 0
ns_per_day / 2, // day 0
ns_per_day, // day 1
ns_per_day + ns_per_day / 2, // day 1
ns_per_day * 2, // day 2
ns_per_day * 2, // day 2
];
let result = mark_session_boundaries(&ts);
assert_eq!(result, vec![0, 2, 4]);
+4 -15
View File
@@ -215,16 +215,14 @@ pub fn supertrend(
let lower_basic = hl2 - multiplier * atr[i];
// Adjust lower band
lower_band[i] = if lower_basic > lower_band[i - 1] || close[i - 1] < lower_band[i - 1]
{
lower_band[i] = if lower_basic > lower_band[i - 1] || close[i - 1] < lower_band[i - 1] {
lower_basic
} else {
lower_band[i - 1]
};
// Adjust upper band
upper_band[i] = if upper_basic < upper_band[i - 1] || close[i - 1] > upper_band[i - 1]
{
upper_band[i] = if upper_basic < upper_band[i - 1] || close[i - 1] > upper_band[i - 1] {
upper_basic
} else {
upper_band[i - 1]
@@ -269,11 +267,7 @@ pub fn supertrend(
///
/// # Returns
/// `(upper, middle, lower)` arrays.
pub fn donchian(
high: &[f64],
low: &[f64],
timeperiod: usize,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
pub fn donchian(high: &[f64], low: &[f64], timeperiod: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let n = high.len();
let mut upper = vec![f64::NAN; n];
let mut lower = vec![f64::NAN; n];
@@ -305,12 +299,7 @@ pub fn donchian(
///
/// Values near 100 indicate a choppy market; near 0 indicates trending.
/// The first `timeperiod` values are `NaN`.
pub fn choppiness_index(
high: &[f64],
low: &[f64],
close: &[f64],
timeperiod: usize,
) -> Vec<f64> {
pub fn choppiness_index(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
let n = high.len();
let mut result = vec![f64::NAN; n];
if timeperiod < 1 || n <= timeperiod {
+6 -3
View File
@@ -1,6 +1,5 @@
//! Momentum indicators.
/// Compute the Relative Strength Index (RSI).
///
/// Returns values in the range `[0, 100]`. Uses Wilder's smoothing method
@@ -119,8 +118,12 @@ pub fn stoch(
for j in (win_start + 1)..=i {
let h = high[j];
let l = low[j];
if h > hh { hh = h; }
if l < ll { ll = l; }
if h > hh {
hh = h;
}
if l < ll {
ll = l;
}
}
let range = hh - ll;
fastk_valid[i - fastk_start] = if range != 0.0 {
+16 -15
View File
@@ -73,7 +73,10 @@ pub fn beta_full(asset_returns: &[f64], benchmark_returns: &[f64]) -> f64 {
cov += da * db;
var_b += db * db;
}
assert!(var_b != 0.0, "benchmark_returns has zero variance; cannot compute beta");
assert!(
var_b != 0.0,
"benchmark_returns has zero variance; cannot compute beta"
);
cov / var_b
}
@@ -225,7 +228,11 @@ pub fn relative_strength(asset_returns: &[f64], benchmark_returns: &[f64]) -> Ve
for i in 0..n {
cum_a *= 1.0 + asset_returns[i];
cum_b *= 1.0 + benchmark_returns[i];
result[i] = if cum_b == 0.0 { f64::NAN } else { cum_a / cum_b };
result[i] = if cum_b == 0.0 {
f64::NAN
} else {
cum_a / cum_b
};
}
result
}
@@ -243,7 +250,10 @@ pub fn spread(a: &[f64], b: &[f64], hedge: f64) -> Vec<f64> {
n > 0 && b.len() == n,
"a and b must be non-empty and equal length"
);
a.iter().zip(b.iter()).map(|(&x, &y)| x - hedge * y).collect()
a.iter()
.zip(b.iter())
.map(|(&x, &y)| x - hedge * y)
.collect()
}
// ---------------------------------------------------------------------------
@@ -358,10 +368,7 @@ mod tests {
#[test]
fn test_portfolio_volatility_identity_cov() {
// Identity covariance, equal weights => sqrt(sum(w_i^2))
let cov = vec![
vec![1.0, 0.0],
vec![0.0, 1.0],
];
let cov = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let w = vec![0.5, 0.5];
let vol = portfolio_volatility(&cov, &w);
// w' I w = 0.25 + 0.25 = 0.5, sqrt = 0.7071...
@@ -378,10 +385,7 @@ mod tests {
#[test]
fn test_portfolio_volatility_correlated() {
// Fully correlated: cov = [[0.04, 0.04], [0.04, 0.04]]
let cov = vec![
vec![0.04, 0.04],
vec![0.04, 0.04],
];
let cov = vec![vec![0.04, 0.04], vec![0.04, 0.04]];
let w = vec![0.5, 0.5];
// w' Σ w = 0.04, sqrt = 0.2
let vol = portfolio_volatility(&cov, &w);
@@ -594,10 +598,7 @@ mod tests {
#[test]
fn test_compose_weighted_basic() {
let data = vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
];
let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
let weights = vec![0.3, 0.7];
let cw = compose_weighted(&data, &weights);
// bar 0: 1*0.3 + 4*0.7 = 3.1
+1 -6
View File
@@ -54,12 +54,7 @@ pub fn regime_combined(
/// Detect structural breaks using a CUSUM (cumulative sum) approach.
///
/// `window` must be >= 2. Returns `Vec<i8>`: `1` at break bars, `0` elsewhere.
pub fn detect_breaks_cusum(
series: &[f64],
window: usize,
threshold: f64,
slack: f64,
) -> Vec<i8> {
pub fn detect_breaks_cusum(series: &[f64], window: usize, threshold: f64, slack: f64) -> Vec<i8> {
let n = series.len();
let mut out = vec![0i8; n];
if n < window || window < 2 {
+1 -2
View File
@@ -197,8 +197,7 @@ mod tests {
#[test]
fn test_volume_bars_single_element() {
let (ro, rh, rl, rc, rv) =
volume_bars(&[10.0], &[12.0], &[8.0], &[11.0], &[50.0], 100.0);
let (ro, rh, rl, rc, rv) = volume_bars(&[10.0], &[12.0], &[8.0], &[11.0], &[50.0], 100.0);
assert_eq!(rv.len(), 1);
assert!((rv[0] - 50.0).abs() < 1e-10);
assert!((ro[0] - 10.0).abs() < 1e-10);
+1 -2
View File
@@ -932,8 +932,7 @@ mod tests {
if i + 1 < period {
assert!(streaming[i].is_nan(), "bar {} should be NaN", i);
} else {
let batch: f64 =
data[i + 1 - period..=i].iter().sum::<f64>() / period as f64;
let batch: f64 = data[i + 1 - period..=i].iter().sum::<f64>() / period as f64;
assert!(
approx_eq(streaming[i], batch, 1e-10),
"bar {}: streaming={} batch={}",
+2 -1
View File
@@ -71,7 +71,8 @@ pub fn aggregate_volume_bars_ticks<'py>(
"price and size must be non-empty and equal length",
));
}
let (ro, rh, rl, rc, rv) = ferro_ta_core::aggregation::aggregate_volume_bars_ticks(p, s, volume_threshold);
let (ro, rh, rl, rc, rv) =
ferro_ta_core::aggregation::aggregate_volume_bars_ticks(p, s, volume_threshold);
Ok((
ro.into_pyarray(py),
rh.into_pyarray(py),
+21 -8
View File
@@ -147,8 +147,8 @@ pub fn sma_crossover_signals<'py>(
validation::validate_timeperiod(fast, "fast", 1)?;
validation::validate_timeperiod(slow, "slow", 1)?;
let prices = close.as_slice()?;
let out = core_bt::sma_crossover_signals(prices, fast, slow)
.map_err(|e| PyValueError::new_err(e))?;
let out =
core_bt::sma_crossover_signals(prices, fast, slow).map_err(|e| PyValueError::new_err(e))?;
Ok(out.into_pyarray(py))
}
@@ -202,8 +202,15 @@ pub fn backtest_core<'py>(
validation::validate_equal_length(&[(c.len(), "close"), (s.len(), "signals")])?;
let cm = commission.as_ref().map(|c| &c.inner);
let result = core_bt::backtest_core(c, s, cm, slippage_bps, initial_capital, commission_per_trade)
.map_err(|e| PyValueError::new_err(e))?;
let result = core_bt::backtest_core(
c,
s,
cm,
slippage_bps,
initial_capital,
commission_per_trade,
)
.map_err(|e| PyValueError::new_err(e))?;
Ok((
result.positions.into_pyarray(py),
@@ -348,8 +355,14 @@ pub fn compute_performance_metrics<'py>(
dict.set_item("calmar", metrics.calmar)?;
dict.set_item("max_drawdown", metrics.max_drawdown)?;
dict.set_item("avg_drawdown", metrics.avg_drawdown)?;
dict.set_item("max_drawdown_duration_bars", metrics.max_drawdown_duration_bars as i64)?;
dict.set_item("avg_drawdown_duration_bars", metrics.avg_drawdown_duration_bars)?;
dict.set_item(
"max_drawdown_duration_bars",
metrics.max_drawdown_duration_bars as i64,
)?;
dict.set_item(
"avg_drawdown_duration_bars",
metrics.avg_drawdown_duration_bars,
)?;
dict.set_item("ulcer_index", metrics.ulcer_index)?;
dict.set_item("omega_ratio", metrics.omega_ratio)?;
dict.set_item("win_rate", metrics.win_rate)?;
@@ -428,8 +441,8 @@ pub fn extract_trades_ohlcv<'py>(
(l.len(), "low"),
])?;
let trades = core_bt::extract_trades_ohlcv(pos, fp, h, l)
.map_err(|e| PyValueError::new_err(e))?;
let trades =
core_bt::extract_trades_ohlcv(pos, fp, h, l).map_err(|e| PyValueError::new_err(e))?;
let mut entry_bars: Vec<i64> = Vec::with_capacity(trades.len());
let mut exit_bars: Vec<i64> = Vec::with_capacity(trades.len());
+6 -8
View File
@@ -27,9 +27,7 @@ use rayon::prelude::*;
/// `result[j]` is column j (one time-series of length n_samples).
fn numpy2d_to_columns(arr: &ndarray::ArrayView2<'_, f64>) -> Vec<Vec<f64>> {
let (_n_samples, n_series) = arr.dim();
(0..n_series)
.map(|j| arr.column(j).to_vec())
.collect()
(0..n_series).map(|j| arr.column(j).to_vec()).collect()
}
/// Convert `Vec<Vec<f64>>` (columns) back into a numpy (n_samples, n_series) array.
@@ -320,9 +318,8 @@ pub fn batch_adx<'py>(
let c_cols = numpy2d_to_columns(&arr_c);
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
let process = |i: usize| {
ferro_ta_core::momentum::adx(&h_cols[i], &l_cols[i], &c_cols[i], timeperiod)
};
let process =
|i: usize| ferro_ta_core::momentum::adx(&h_cols[i], &l_cols[i], &c_cols[i], timeperiod);
if parallel {
(0..n_series).into_par_iter().map(process).collect()
} else {
@@ -369,8 +366,9 @@ pub fn run_close_indicators<'py>(
.map(|r| r.map(|v| v.into_pyarray(py).unbind()).map_err(map_core_err))
.collect()
} else {
let results = ferro_ta_core::batch::run_close_indicators(close_values, &names, &timeperiods)
.map_err(map_core_err)?;
let results =
ferro_ta_core::batch::run_close_indicators(close_values, &names, &timeperiods)
.map_err(map_core_err)?;
Ok(results
.into_iter()
.map(|v| v.into_pyarray(py).unbind())
+3 -4
View File
@@ -63,12 +63,11 @@ pub fn rolling_variance_break<'py>(
) -> PyResult<Bound<'py, PyArray1<i8>>> {
validation::validate_timeperiod(short_window, "short_window", 2)?;
if long_window <= short_window {
return Err(PyValueError::new_err(
"long_window must be > short_window",
));
return Err(PyValueError::new_err("long_window must be > short_window"));
}
let s = series.as_slice()?;
let result = ferro_ta_core::regime::rolling_variance_break(s, short_window, long_window, threshold);
let result =
ferro_ta_core::regime::rolling_variance_break(s, short_window, long_window, threshold);
Ok(result.into_pyarray(py))
}
+2 -1
View File
@@ -38,7 +38,8 @@ pub fn volume_bars<'py>(
"All input arrays must be non-empty and have equal length",
));
}
let (ro, rh, rl, rc, rv) = ferro_ta_core::resampling::volume_bars(o, h, l, c, v, volume_threshold);
let (ro, rh, rl, rc, rv) =
ferro_ta_core::resampling::volume_bars(o, h, l, c, v, volume_threshold);
Ok((
ro.into_pyarray(py),
rh.into_pyarray(py),
+3 -12
View File
@@ -209,10 +209,7 @@ impl StreamingBBands {
}
fn __repr__(&self) -> String {
format!(
"StreamingBBands(period={})",
self.inner.period()
)
format!("StreamingBBands(period={})", self.inner.period())
}
}
@@ -287,10 +284,7 @@ impl StreamingStoch {
}
fn __repr__(&self) -> String {
format!(
"StreamingStoch(fastk_period={})",
self.inner.period()
)
format!("StreamingStoch(fastk_period={})", self.inner.period())
}
}
@@ -363,10 +357,7 @@ impl StreamingSupertrend {
}
fn __repr__(&self) -> String {
format!(
"StreamingSupertrend(period={})",
self.inner.period()
)
format!("StreamingSupertrend(period={})", self.inner.period())
}
}