diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 892ad89..99659d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Format check run: cargo fmt -- --check - name: Clippy - run: cargo clippy --all-targets --all-features -- -D clippy::correctness -D clippy::suspicious + run: cargo clippy --workspace --all-targets --all-features -- -D warnings - name: Tests (fast) run: cargo test --workspace --lib --tests --all-features -- --skip test_sadf_test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08c98e9..b322125 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Format/Lint/Test (fast) run: | cargo fmt -- --check - cargo clippy --all-targets --all-features -- -D warnings + cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --lib --tests --all-features -- --skip test_sadf_test - name: Package dry-run run: cargo package -p openquant diff --git a/clippy.toml b/clippy.toml index 22eb48b..60907e0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,4 +1,4 @@ # Clippy lint levels are controlled in CI and Just commands: -# `cargo clippy --all-targets --all-features -- -D warnings` +# `cargo clippy --workspace --all-targets --all-features -- -D warnings` # # Keep this file for future stable clippy configuration options. diff --git a/crates/openquant/benches/synthetic_ticker_pipeline.rs b/crates/openquant/benches/synthetic_ticker_pipeline.rs index 95fe353..c30b94f 100644 --- a/crates/openquant/benches/synthetic_ticker_pipeline.rs +++ b/crates/openquant/benches/synthetic_ticker_pipeline.rs @@ -89,8 +89,7 @@ fn bench_end_to_end_ticker_pipeline(c: &mut Criterion) { let mut cov = DMatrix::zeros(3, 3); let mut cols = [Vec::with_capacity(n), Vec::with_capacity(n), Vec::with_capacity(n)]; - for i in 0..n { - let r = rets[i]; + for &r in rets.iter().take(n) { cols[0].push(r); cols[1].push((1.0 + r).ln()); cols[2].push(r * r.signum()); @@ -100,8 +99,8 @@ fn bench_end_to_end_ticker_pipeline(c: &mut Criterion) { for i in 0..3 { for j in 0..3 { let mut s = 0.0; - for k in 0..n { - s += (cols[i][k] - means[i]) * (cols[j][k] - means[j]); + for (a, b) in cols[i].iter().zip(&cols[j]) { + s += (a - means[i]) * (b - means[j]); } cov[(i, j)] = s / (n - 1) as f64; } diff --git a/crates/openquant/examples/research_notebook_smoke.rs b/crates/openquant/examples/research_notebook_smoke.rs index 2050785..77123d9 100644 --- a/crates/openquant/examples/research_notebook_smoke.rs +++ b/crates/openquant/examples/research_notebook_smoke.rs @@ -22,7 +22,7 @@ fn main() { assert!((sum_w - 1.0).abs() < 1e-6); let ret = vec![0.0, 0.002, -0.001, 0.004, -0.002, 0.003, 0.001, -0.0005, 0.0025]; - let rm = RiskMetrics::default(); + let rm = RiskMetrics; let _ = rm.calculate_value_at_risk(&ret, 0.05).expect("var"); let _ = rm.calculate_expected_shortfall(&ret, 0.05).expect("es"); let _ = rm.calculate_conditional_drawdown_risk(&ret, 0.05).expect("cdar"); diff --git a/crates/openquant/src/backtesting_engine.rs b/crates/openquant/src/backtesting_engine.rs index 64b8497..818a1d5 100644 --- a/crates/openquant/src/backtesting_engine.rs +++ b/crates/openquant/src/backtesting_engine.rs @@ -387,10 +387,13 @@ where Ok(out) } +/// Per-fold performance plus the out-of-sample returns keyed by split id. +type SplitEvaluation = (Vec, HashMap>); + fn evaluate_splits_with_returns( splits: &[SplitDefinition], evaluator: &mut E, -) -> Result<(Vec, HashMap>), String> +) -> Result where E: FnMut(&SplitDefinition) -> Result, String>, { diff --git a/crates/openquant/src/bet_sizing.rs b/crates/openquant/src/bet_sizing.rs index 4c6a518..57ddbb2 100644 --- a/crates/openquant/src/bet_sizing.rs +++ b/crates/openquant/src/bet_sizing.rs @@ -40,7 +40,7 @@ pub fn bet_size_power(w_param: f64, price_div: f64) -> f64 { } pub fn bet_size_power_checked(w_param: f64, price_div: f64) -> Result { - if price_div < -1.0 || price_div > 1.0 { + if !(-1.0..=1.0).contains(&price_div) { return Err(BetSizingError::PriceDivergenceOutOfRange { value: price_div }); } if price_div == 0.0 { @@ -112,26 +112,14 @@ pub fn discrete_signal(signal0: &[f64], step_size: f64) -> Vec { if step_size <= 0.0 { return signal0.to_vec(); } - signal0 - .iter() - .map(|s| { - let mut v = (s / step_size).round() * step_size; - if v > 1.0 { - v = 1.0; - } - if v < -1.0 { - v = -1.0; - } - v - }) - .collect() + signal0.iter().map(|s| ((s / step_size).round() * step_size).clamp(-1.0, 1.0)).collect() } pub fn avg_active_signals( signal: &[(NaiveDateTime, f64)], t1: &[NaiveDateTime], ) -> Vec<(NaiveDateTime, f64)> { - let mut t_points: Vec = t1.iter().copied().collect(); + let mut t_points: Vec = t1.to_vec(); t_points.extend(signal.iter().map(|(ts, _)| *ts)); t_points.sort(); t_points.dedup(); @@ -172,7 +160,7 @@ pub fn bet_size_probability( let side: Vec = events.iter().map(|(_, _, _, s)| *s).collect(); let signal0 = get_signal(&prob, num_classes, Some(&side)); let mut signals: Vec<(NaiveDateTime, f64)> = - events.iter().map(|(ts, _, _, _)| *ts).zip(signal0.into_iter()).collect(); + events.iter().map(|(ts, _, _, _)| *ts).zip(signal0).collect(); if average_active { let t1: Vec = events.iter().map(|(_, t1, _, _)| *t1).collect(); signals = avg_active_signals(&signals, &t1); @@ -291,7 +279,7 @@ pub fn get_w_power(price_div: f64, m_bet_size: f64) -> f64 { } pub fn get_w_power_checked(price_div: f64, m_bet_size: f64) -> Result { - if price_div < -1.0 || price_div > 1.0 { + if !(-1.0..=1.0).contains(&price_div) { return Err(BetSizingError::PriceDivergenceOutOfRange { value: price_div }); } let w_calc = (m_bet_size / price_div.signum()).ln() / price_div.abs().ln(); @@ -536,6 +524,11 @@ pub fn bet_size_reserve_with_fit( .collect() } +/// Reserve bet-size row: `(timestamp, active_long, active_short, c_t, bet_size)`. +pub type ReserveBetSizeRow = (NaiveDateTime, f64, f64, f64, f64); +/// Fitted two-normal mixture parameters `[mu1, mu2, sigma1, sigma2, p1]`. +pub type MixtureParams = [f64; 5]; + pub fn bet_size_reserve_full( t1: &[(NaiveDateTime, NaiveDateTime)], side: &[f64], @@ -543,13 +536,13 @@ pub fn bet_size_reserve_full( epsilon: f64, max_iter: usize, return_parameters: bool, -) -> (Vec<(NaiveDateTime, f64, f64, f64, f64)>, Option<[f64; 5]>) { +) -> (Vec, Option) { let concurrent = get_concurrent_sides(t1, side); let c_t: Vec = concurrent.iter().map(|(_, l, s)| l - s).collect(); let fit = fit_two_normal_mixture_em(&c_t, fit_runs, epsilon, max_iter); let events = concurrent .into_iter() - .zip(c_t.into_iter()) + .zip(c_t) .map(|((ts, l, s), c)| { let b = single_bet_size_mixed(c, &fit); (ts, l, s, c, b) diff --git a/crates/openquant/src/cla.rs b/crates/openquant/src/cla.rs index 247b56b..c9be7a6 100644 --- a/crates/openquant/src/cla.rs +++ b/crates/openquant/src/cla.rs @@ -46,8 +46,8 @@ impl ReturnsEstimation { let rows = returns.nrows(); let cols = returns.ncols(); let mut out = vec![0.0; cols]; - for c in 0..cols { - out[c] = (returns.column(c).sum() / rows as f64) * freq; + for (c, slot) in out.iter_mut().enumerate() { + *slot = (returns.column(c).sum() / rows as f64) * freq; } Ok(out) } @@ -212,8 +212,8 @@ impl CLA { return Err(ClaError::UnknownReturns(self.calculate_expected_returns.clone())); } - if covariance_matrix.is_some() { - self.cov_matrix = covariance_matrix.unwrap().clone_owned(); + if let Some(covariance_matrix) = covariance_matrix { + self.cov_matrix = covariance_matrix.clone_owned(); } else { let returns = ReturnsEstimation::calculate_returns(asset_prices, resample_by)?; self.cov_matrix = covariance(&returns); diff --git a/crates/openquant/src/codependence.rs b/crates/openquant/src/codependence.rs index 40deee7..04296ad 100644 --- a/crates/openquant/src/codependence.rs +++ b/crates/openquant/src/codependence.rs @@ -257,13 +257,18 @@ pub fn get_optimal_number_of_bins( } let n = num_obs as f64; - let bins = if corr_coef.is_none() || (corr_coef.unwrap() - 1.0).abs() <= 1e-4 { + let univariate = || { let z = (8.0 + 324.0 * n + 12.0 * (36.0 * n + 729.0 * n * n).sqrt()).cbrt(); (z / 6.0 + 2.0 / (3.0 * z) + 1.0 / 3.0).round() - } else { - let corr = corr_coef.unwrap(); - let inner = (1.0 + 24.0 * n / (1.0 - corr * corr)).sqrt(); - (2.0_f64).powf(-0.5) * (1.0 + inner).sqrt() + }; + // Arm order keeps a NaN correlation on the bivariate branch, as before. + let bins = match corr_coef { + None => univariate(), + Some(corr) if (corr - 1.0).abs() <= 1e-4 => univariate(), + Some(corr) => { + let inner = (1.0 + 24.0 * n / (1.0 - corr * corr)).sqrt(); + (2.0_f64).powf(-0.5) * (1.0 + inner).sqrt() + } }; let bins = bins.round() as isize; @@ -303,23 +308,23 @@ pub fn get_mutual_info( let mut row_sums = vec![0.0; bins]; let mut col_sums = vec![0.0; bins]; for i in 0..bins { - for j in 0..bins { + for (j, col_sum) in col_sums.iter_mut().enumerate() { let value = contingency[i][j] as f64; row_sums[i] += value; - col_sums[j] += value; + *col_sum += value; } } let mut mutual_info = 0.0; for i in 0..bins { - for j in 0..bins { + for (j, col_sum) in col_sums.iter().enumerate() { let value = contingency[i][j] as f64; if value == 0.0 { continue; } let p_ij = value / total_f; let p_i = row_sums[i] / total_f; - let p_j = col_sums[j] / total_f; + let p_j = col_sum / total_f; mutual_info += p_ij * (p_ij / (p_i * p_j)).ln(); } } @@ -367,23 +372,23 @@ pub fn variation_of_information_score( let mut row_sums = vec![0.0; bins]; let mut col_sums = vec![0.0; bins]; for i in 0..bins { - for j in 0..bins { + for (j, col_sum) in col_sums.iter_mut().enumerate() { let value = contingency[i][j] as f64; row_sums[i] += value; - col_sums[j] += value; + *col_sum += value; } } let mut mutual_info = 0.0; for i in 0..bins { - for j in 0..bins { + for (j, col_sum) in col_sums.iter().enumerate() { let value = contingency[i][j] as f64; if value == 0.0 { continue; } let p_ij = value / total_f; let p_i = row_sums[i] / total_f; - let p_j = col_sums[j] / total_f; + let p_j = col_sum / total_f; mutual_info += p_ij * (p_ij / (p_i * p_j)).ln(); } } diff --git a/crates/openquant/src/cross_validation.rs b/crates/openquant/src/cross_validation.rs index 8893f53..8691b44 100644 --- a/crates/openquant/src/cross_validation.rs +++ b/crates/openquant/src/cross_validation.rs @@ -108,6 +108,9 @@ pub fn ml_get_train_times( out } +/// One cross-validation split: `(train_indices, test_indices)`. +pub type TrainTestSplit = (Vec, Vec); + pub struct PurgedKFold { n_splits: usize, samples_info_sets: Vec<(NaiveDateTime, NaiveDateTime)>, @@ -132,14 +135,14 @@ impl PurgedKFold { Ok(Self { n_splits, samples_info_sets, pct_embargo }) } - pub fn split(&self, n_samples: usize) -> Result, Vec)>, String> { + pub fn split(&self, n_samples: usize) -> Result, String> { if n_samples != self.samples_info_sets.len() { return Err("Dataset length must match samples_info_sets".into()); } let n = n_samples; let mut fold_sizes = vec![n / self.n_splits; self.n_splits]; - for i in 0..(n % self.n_splits) { - fold_sizes[i] += 1; + for fold_size in fold_sizes.iter_mut().take(n % self.n_splits) { + *fold_size += 1; } let mut current = 0; let mut splits = Vec::new(); @@ -177,13 +180,11 @@ impl PurgedKFold { if embargo > 0 { let after = (stop as isize + embargo).min(n as isize); let before = (start as isize - embargo).max(0); - for i in start..(after as usize) { - if i < n { - train_mask[i] = false; - } + for keep in train_mask.iter_mut().take(after as usize).skip(start) { + *keep = false; } - for i in before as usize..start { - train_mask[i] = false; + for keep in train_mask.iter_mut().take(start).skip(before as usize) { + *keep = false; } } diff --git a/crates/openquant/src/etf_trick.rs b/crates/openquant/src/etf_trick.rs index 6ebcf6b..5466b6e 100644 --- a/crates/openquant/src/etf_trick.rs +++ b/crates/openquant/src/etf_trick.rs @@ -75,15 +75,19 @@ pub struct EtfTrick { source: Source, } +#[derive(Clone, Debug)] +struct InMemoryTables { + open: Table, + close: Table, + alloc: Table, + costs: Table, + rates: Option, +} + #[derive(Clone, Debug)] enum Source { - InMemory { - open: Table, - close: Table, - alloc: Table, - costs: Table, - rates: Option
, - }, + // Boxed so the enum is not sized by the five in-memory tables. + InMemory(Box), Csv { open_path: String, close_path: String, @@ -102,7 +106,9 @@ impl EtfTrick { rates: Option
, ) -> Result { validate_shapes(&open, &close, &alloc, &costs, rates.as_ref())?; - Ok(Self { source: Source::InMemory { open, close, alloc, costs, rates } }) + Ok(Self { + source: Source::InMemory(Box::new(InMemoryTables { open, close, alloc, costs, rates })), + }) } pub fn from_csv( @@ -125,9 +131,13 @@ impl EtfTrick { pub fn get_etf_series(&self, batch_size: usize) -> Result, String> { match &self.source { - Source::InMemory { open, close, alloc, costs, rates } => { - compute_etf_series(open, close, alloc, costs, rates.as_ref()) - } + Source::InMemory(tables) => compute_etf_series( + &tables.open, + &tables.close, + &tables.alloc, + &tables.costs, + tables.rates.as_ref(), + ), Source::Csv { open_path, close_path, alloc_path, costs_path, rates_path } => { if batch_size < 3 { return Err("Batch size should be >= 3".to_string()); @@ -251,10 +261,10 @@ fn compute_etf_series( } let mut delta = vec![0.0; n_cols]; - for j in 0..n_cols { + for (j, delta_j) in delta.iter_mut().enumerate() { let close_open = close.values[i][j] - open.values[i][j]; let price_diff = close.values[i][j] - close.values[i - 1][j]; - delta[j] = if prev_allocs_change { close_open } else { price_diff }; + *delta_j = if prev_allocs_change { close_open } else { price_diff }; } if prev_h.is_none() { diff --git a/crates/openquant/src/feature_importance.rs b/crates/openquant/src/feature_importance.rs index 448d289..4c75423 100644 --- a/crates/openquant/src/feature_importance.rs +++ b/crates/openquant/src/feature_importance.rs @@ -81,7 +81,7 @@ pub fn mean_decrease_accuracy( let base = score_model(model, &x_test, &y_test, sw_test.as_deref(), scoring); - for j in 0..n_features { + for (j, scores) in per_feature.iter_mut().enumerate() { let mut x_perm = x_test.clone(); permute_col(&mut x_perm, j); let perm = score_model(model, &x_perm, &y_test, sw_test.as_deref(), scoring); @@ -101,7 +101,7 @@ pub fn mean_decrease_accuracy( } } }; - per_feature[j].push(if imp.is_finite() { imp } else { 0.0 }); + scores.push(if imp.is_finite() { imp } else { 0.0 }); } } @@ -204,10 +204,13 @@ pub fn plot_feature_importance( Ok(()) } +/// PCA output: `(eigenvalues, eigenvectors, standardized feature rows)`. +type PcaDecomposition = (Vec, DMatrix, Vec>); + fn compute_pca( feature_rows: &[Vec], variance_thresh: f64, -) -> Result<(Vec, DMatrix, Vec>), String> { +) -> Result { if feature_rows.iter().any(|r| r.len() != feature_rows[0].len()) { return Err("ragged feature rows".to_string()); } diff --git a/crates/openquant/src/fingerprint.rs b/crates/openquant/src/fingerprint.rs index b054d5c..f58bd10 100644 --- a/crates/openquant/src/fingerprint.rs +++ b/crates/openquant/src/fingerprint.rs @@ -266,7 +266,7 @@ where for (ykl, yk, yl) in vals { acc += (ykl - mean_ykl - yk - yl).abs(); } - store.insert(format!("({}, {})", k, l), acc / (num_values * num_values) as f64); + store.insert(format!("({k}, {l})"), acc / (num_values * num_values) as f64); } store } diff --git a/crates/openquant/src/fracdiff.rs b/crates/openquant/src/fracdiff.rs index 5cd7756..b150d91 100644 --- a/crates/openquant/src/fracdiff.rs +++ b/crates/openquant/src/fracdiff.rs @@ -59,13 +59,13 @@ pub fn frac_diff(series: &[f64], diff_amt: f64, thresh: f64) -> Vec { let skip = cum.iter().filter(|v| **v > thresh).count(); let mut out = vec![f64::NAN; n]; - for iloc in skip..n { + for (iloc, slot) in out.iter_mut().enumerate().skip(skip) { let w_start = n - (iloc + 1); let mut acc = 0.0; for j in 0..=iloc { acc += weights[w_start + j] * series[j]; } - out[iloc] = acc; + *slot = acc; } out } @@ -81,13 +81,13 @@ pub fn frac_diff_ffd(series: &[f64], diff_amt: f64, thresh: f64) -> Vec { } let width = weights.len() - 1; let mut out = vec![f64::NAN; n]; - for iloc in width..n { + for (iloc, slot) in out.iter_mut().enumerate().skip(width) { let loc0 = iloc - width; let mut acc = 0.0; for (k, w) in weights.iter().enumerate() { acc += *w * series[loc0 + k]; } - out[iloc] = acc; + *slot = acc; } out } diff --git a/crates/openquant/src/hpc_parallel.rs b/crates/openquant/src/hpc_parallel.rs index b9fd56e..40bb333 100644 --- a/crates/openquant/src/hpc_parallel.rs +++ b/crates/openquant/src/hpc_parallel.rs @@ -387,8 +387,8 @@ fn maybe_record_progress( completed_atoms: usize, progress_every: usize, ) { - let should_record = - completed_molecules == total_molecules || completed_molecules % progress_every == 0; + let should_record = completed_molecules == total_molecules + || completed_molecules.is_multiple_of(progress_every); if !should_record { return; } diff --git a/crates/openquant/src/hyperparameter_tuning.rs b/crates/openquant/src/hyperparameter_tuning.rs index 5f74749..93785d7 100644 --- a/crates/openquant/src/hyperparameter_tuning.rs +++ b/crates/openquant/src/hyperparameter_tuning.rs @@ -247,6 +247,8 @@ where search_over_params(build_classifier, params, data, n_splits, pct_embargo, scoring) } +// Public search API: `grid_search`'s arguments plus `n_iter` and `seed`; signature kept stable. +#[allow(clippy::too_many_arguments)] pub fn randomized_search( build_classifier: F, param_space: &BTreeMap, diff --git a/crates/openquant/src/labeling.rs b/crates/openquant/src/labeling.rs index da4a816..2612bf4 100644 --- a/crates/openquant/src/labeling.rs +++ b/crates/openquant/src/labeling.rs @@ -208,6 +208,8 @@ pub fn meta_labels( } /// Backward-compatible triple-barrier API. +// Mirrors the mlfinlab `get_events` signature. +#[allow(clippy::too_many_arguments)] pub fn get_events( close: &[(NaiveDateTime, f64)], t_events: &[NaiveDateTime], @@ -254,7 +256,7 @@ pub fn drop_labels( let mut min_label: Option<(i8, f64)> = None; for (label, count) in &counts { let pct = *count as f64 / total; - if min_label.map_or(true, |(_, p)| pct < p) { + if min_label.is_none_or(|(_, p)| pct < p) { min_label = Some((*label, pct)); } } diff --git a/crates/openquant/src/microstructural_features.rs b/crates/openquant/src/microstructural_features.rs index 5834378..7fa869b 100644 --- a/crates/openquant/src/microstructural_features.rs +++ b/crates/openquant/src/microstructural_features.rs @@ -1,10 +1,9 @@ use chrono::NaiveDateTime; use statrs::distribution::{ContinuousCDF, Normal}; -use std::f64::NAN; fn rolling_cov(x: &[f64], y: &[f64], window: usize) -> Vec { let n = x.len(); - let mut out = vec![NAN; n]; + let mut out = vec![f64::NAN; n]; if window < 2 { return out; } @@ -28,30 +27,28 @@ fn rolling_cov(x: &[f64], y: &[f64], window: usize) -> Vec { pub fn get_roll_measure(close: &[f64], window: usize) -> Vec { if close.len() < 2 { - return vec![NAN; close.len()]; + return vec![f64::NAN; close.len()]; } - let mut diff = vec![NAN; close.len()]; + let mut diff = vec![f64::NAN; close.len()]; for i in 1..close.len() { diff[i] = close[i] - close[i - 1]; } - let mut diff_lag = vec![NAN; close.len()]; - for i in 1..diff.len() { - diff_lag[i] = diff[i - 1]; - } + let mut diff_lag = vec![f64::NAN; close.len()]; + diff_lag[1..].copy_from_slice(&diff[..diff.len() - 1]); let cov = rolling_cov(&diff, &diff_lag, window); - cov.iter().map(|c| if c.is_nan() { NAN } else { 2.0 * (c.abs()).sqrt() }).collect() + cov.iter().map(|c| if c.is_nan() { f64::NAN } else { 2.0 * (c.abs()).sqrt() }).collect() } pub fn get_roll_impact(close: &[f64], dollar_volume: &[f64], window: usize) -> Vec { let roll = get_roll_measure(close, window); roll.iter() .zip(dollar_volume.iter()) - .map(|(r, dv)| if r.is_nan() || *dv == 0.0 { NAN } else { r / dv }) + .map(|(r, dv)| if r.is_nan() || *dv == 0.0 { f64::NAN } else { r / dv }) .collect() } fn rolling_max(arr: &[f64], window: usize) -> Vec { - let mut out = vec![NAN; arr.len()]; + let mut out = vec![f64::NAN; arr.len()]; if window == 0 { return out; } @@ -67,7 +64,7 @@ fn rolling_max(arr: &[f64], window: usize) -> Vec { } fn rolling_min(arr: &[f64], window: usize) -> Vec { - let mut out = vec![NAN; arr.len()]; + let mut out = vec![f64::NAN; arr.len()]; if window == 0 { return out; } @@ -83,7 +80,7 @@ fn rolling_min(arr: &[f64], window: usize) -> Vec { } fn _get_beta(high: &[f64], low: &[f64], window: usize) -> Vec { - let mut ret_sq = vec![NAN; high.len()]; + let mut ret_sq = vec![f64::NAN; high.len()]; for i in 0..high.len() { if low[i] == 0.0 { continue; @@ -91,7 +88,7 @@ fn _get_beta(high: &[f64], low: &[f64], window: usize) -> Vec { ret_sq[i] = (high[i] / low[i]).ln().powi(2); } // rolling sum over 2 - let mut two_sum = vec![NAN; ret_sq.len()]; + let mut two_sum = vec![f64::NAN; ret_sq.len()]; for i in 1..ret_sq.len() { if ret_sq[i].is_nan() || ret_sq[i - 1].is_nan() { continue; @@ -99,7 +96,7 @@ fn _get_beta(high: &[f64], low: &[f64], window: usize) -> Vec { two_sum[i] = ret_sq[i] + ret_sq[i - 1]; } // rolling mean over window - let mut beta = vec![NAN; ret_sq.len()]; + let mut beta = vec![f64::NAN; ret_sq.len()]; if window == 0 { return beta; } @@ -127,7 +124,7 @@ fn _get_gamma(high: &[f64], low: &[f64]) -> Vec { .map( |(h, l)| { if h.is_nan() || l.is_nan() || *l == 0.0 { - NAN + f64::NAN } else { (h / l).ln().powi(2) } @@ -142,7 +139,7 @@ fn _get_alpha(beta: &[f64], gamma: &[f64]) -> Vec { .zip(gamma.iter()) .map(|(b, g)| { if b.is_nan() || g.is_nan() { - return NAN; + return f64::NAN; } let mut alpha = (2.0_f64.sqrt() - 1.0) * b.sqrt() / den; alpha -= (g / den).sqrt(); @@ -163,7 +160,7 @@ pub fn get_corwin_schultz_estimator(high: &[f64], low: &[f64], window: usize) -> .iter() .map(|a| { if a.is_nan() { - NAN + f64::NAN } else { let ea = a.exp(); 2.0 * (ea - 1.0) / (1.0 + ea) @@ -181,7 +178,7 @@ pub fn get_bekker_parkinson_vol(high: &[f64], low: &[f64], window: usize) -> Vec .zip(gamma.iter()) .map(|(b, g)| { if b.is_nan() || g.is_nan() { - return NAN; + return f64::NAN; } let mut sigma = (2.0_f64.powf(-0.5) - 1.0) * b.sqrt() / (k2 * den); sigma += (g / (k2 * k2 * den)).sqrt(); @@ -195,11 +192,11 @@ pub fn get_bekker_parkinson_vol(high: &[f64], low: &[f64], window: usize) -> Vec } pub fn get_bar_based_kyle_lambda(close: &[f64], volume: &[f64], window: usize) -> Vec { - let mut diff = vec![NAN; close.len()]; + let mut diff = vec![f64::NAN; close.len()]; for i in 1..close.len() { diff[i] = close[i] - close[i - 1]; } - let mut sign = vec![NAN; diff.len()]; + let mut sign = vec![f64::NAN; diff.len()]; for i in 0..diff.len() { let s = diff[i].signum(); sign[i] = if s == 0.0 && i > 0 { sign[i - 1] } else { s }; @@ -208,9 +205,9 @@ pub fn get_bar_based_kyle_lambda(close: &[f64], volume: &[f64], window: usize) - .iter() .zip(volume.iter()) .zip(sign.iter()) - .map(|((d, v), s)| if *v == 0.0 || s.is_nan() { NAN } else { d / (v * s) }) + .map(|((d, v), s)| if *v == 0.0 || s.is_nan() { f64::NAN } else { d / (v * s) }) .collect(); - let mut out = vec![NAN; close.len()]; + let mut out = vec![f64::NAN; close.len()]; for i in 0..close.len() { if i + 1 < window { continue; @@ -230,14 +227,14 @@ pub fn get_bar_based_amihud_lambda( dollar_volume: &[f64], window: usize, ) -> Vec { - let mut ret_abs = vec![NAN; close.len()]; + let mut ret_abs = vec![f64::NAN; close.len()]; for i in 1..close.len() { if close[i - 1] == 0.0 { continue; } ret_abs[i] = (close[i] / close[i - 1]).ln().abs(); } - let mut out = vec![NAN; close.len()]; + let mut out = vec![f64::NAN; close.len()]; for i in 0..close.len() { if i + 1 < window { continue; @@ -264,14 +261,14 @@ pub fn get_bar_based_hasbrouck_lambda( dollar_volume: &[f64], window: usize, ) -> Vec { - let mut log_ret = vec![NAN; close.len()]; + let mut log_ret = vec![f64::NAN; close.len()]; for i in 1..close.len() { if close[i - 1] == 0.0 { continue; } log_ret[i] = (close[i] / close[i - 1]).ln(); } - let mut sign = vec![NAN; log_ret.len()]; + let mut sign = vec![f64::NAN; log_ret.len()]; for i in 0..log_ret.len() { let s = log_ret[i].signum(); sign[i] = if s == 0.0 && i > 0 { sign[i - 1] } else { s }; @@ -279,9 +276,9 @@ pub fn get_bar_based_hasbrouck_lambda( let signed_sqrt: Vec = sign .iter() .zip(dollar_volume.iter()) - .map(|(s, dv)| if s.is_nan() || *dv < 0.0 { NAN } else { s * dv.sqrt() }) + .map(|(s, dv)| if s.is_nan() || *dv < 0.0 { f64::NAN } else { s * dv.sqrt() }) .collect(); - let mut out = vec![NAN; close.len()]; + let mut out = vec![f64::NAN; close.len()]; for i in 0..close.len() { if i + 1 < window { continue; @@ -312,7 +309,7 @@ pub fn get_trades_based_kyle_lambda( let num: f64 = signed.iter().zip(price_diff.iter()).map(|(x, y)| x * y).sum(); let den: f64 = signed.iter().map(|x| x * x).sum(); if den == 0.0 { - NAN + f64::NAN } else { num / den } @@ -322,7 +319,7 @@ pub fn get_trades_based_amihud_lambda(log_ret: &[f64], dollar_volume: &[f64]) -> let num: f64 = dollar_volume.iter().zip(log_ret.iter()).map(|(x, y)| x * y.abs()).sum(); let den: f64 = dollar_volume.iter().map(|x| x * x).sum(); if den == 0.0 { - NAN + f64::NAN } else { num / den } @@ -338,7 +335,7 @@ pub fn get_trades_based_hasbrouck_lambda( let num: f64 = signed.iter().zip(log_ret.iter()).map(|(x, y)| x * y.abs()).sum(); let den: f64 = signed.iter().map(|x| x * x).sum(); if den == 0.0 { - NAN + f64::NAN } else { num / den } @@ -348,14 +345,14 @@ pub fn get_trades_based_hasbrouck_lambda( pub fn vwap(dollar_volume: &[f64], volume: &[f64]) -> f64 { let sum_v: f64 = volume.iter().sum(); if sum_v == 0.0 { - return NAN; + return f64::NAN; } dollar_volume.iter().sum::() / sum_v } pub fn get_avg_tick_size(tick_sizes: &[f64]) -> f64 { if tick_sizes.is_empty() { - return NAN; + return f64::NAN; } tick_sizes.iter().sum::() / tick_sizes.len() as f64 } @@ -364,7 +361,7 @@ pub fn get_vpin(volume: &[f64], buy_volume: &[f64], window: usize) -> Vec { let sell_volume: Vec = volume.iter().zip(buy_volume.iter()).map(|(v, b)| v - b).collect(); let imbalance: Vec = buy_volume.iter().zip(sell_volume.iter()).map(|(b, s)| (b - s).abs()).collect(); - let mut out = vec![NAN; volume.len()]; + let mut out = vec![f64::NAN; volume.len()]; for i in 0..volume.len() { if i + 1 < window { continue; @@ -382,13 +379,13 @@ pub fn get_vpin(volume: &[f64], buy_volume: &[f64], window: usize) -> Vec { } pub fn get_bvc_buy_volume(close: &[f64], volume: &[f64], window: usize) -> Vec { - let mut out = vec![NAN; close.len()]; + let mut out = vec![f64::NAN; close.len()]; let norm = Normal::new(0.0, 1.0).unwrap(); - let mut diff = vec![NAN; close.len()]; + let mut diff = vec![f64::NAN; close.len()]; for i in 1..close.len() { diff[i] = close[i] - close[i - 1]; } - let mut rolling_std = vec![NAN; close.len()]; + let mut rolling_std = vec![f64::NAN; close.len()]; for i in 0..close.len() { if i + 1 < window { continue; @@ -420,7 +417,7 @@ pub fn encode_tick_rule_array(arr: &[i32]) -> Result { 1 => s.push('a'), -1 => s.push('b'), 0 => s.push('c'), - other => return Err(format!("Unknown value for tick rule: {}", other)), + other => return Err(format!("Unknown value for tick rule: {other}")), } } Ok(s) @@ -651,6 +648,8 @@ impl MicrostructuralFeaturesGenerator { } // Take the first threshold *out of* the iterator. Peeking at it instead leaves // it to be served again after the first bar closes, which emits a one-tick bar. + // The generator owns its thresholds; borrowing would add a lifetime to a public type. + #[allow(clippy::unnecessary_to_owned)] let mut tick_num_iter = tick_num_series.to_vec().into_iter(); let current_bar_tick = tick_num_iter.next().unwrap_or(0); Ok(Self { @@ -679,14 +678,14 @@ impl MicrostructuralFeaturesGenerator { fn apply_tick_rule(&mut self, price: f64) -> f64 { let tick_diff = if let Some(prev) = self.prev_price { price - prev } else { 0.0 }; - let signed_tick = if tick_diff != 0.0 { + + if tick_diff != 0.0 { let s = tick_diff.signum(); self.prev_tick_rule = s; s } else { self.prev_tick_rule - }; - signed_tick + } } fn get_price_diff(&self, price: f64) -> f64 { @@ -713,22 +712,15 @@ impl MicrostructuralFeaturesGenerator { } fn bar_features(&self, date_time: NaiveDateTime) -> Vec { - let mut features = Vec::new(); - features.push(date_time.and_utc().timestamp_millis() as f64); - features.push(get_avg_tick_size(&self.trade_size)); - features.push(self.tick_rule.iter().sum::()); - features.push(vwap(&self.dollar_size, &self.trade_size)); - features.push(get_trades_based_kyle_lambda( - &self.price_diff, - &self.trade_size, - &self.tick_rule, - )); - features.push(get_trades_based_amihud_lambda(&self.log_ret, &self.dollar_size)); - features.push(get_trades_based_hasbrouck_lambda( - &self.log_ret, - &self.dollar_size, - &self.tick_rule, - )); + let mut features = vec![ + date_time.and_utc().timestamp_millis() as f64, + get_avg_tick_size(&self.trade_size), + self.tick_rule.iter().sum::(), + vwap(&self.dollar_size, &self.trade_size), + get_trades_based_kyle_lambda(&self.price_diff, &self.trade_size, &self.tick_rule), + get_trades_based_amihud_lambda(&self.log_ret, &self.dollar_size), + get_trades_based_hasbrouck_lambda(&self.log_ret, &self.dollar_size, &self.tick_rule), + ]; let tick_msg = encode_tick_rule_array(&self.tick_rule.iter().map(|v| *v as i32).collect::>()) diff --git a/crates/openquant/src/pipeline.rs b/crates/openquant/src/pipeline.rs index 8f7a46c..b2d4740 100644 --- a/crates/openquant/src/pipeline.rs +++ b/crates/openquant/src/pipeline.rs @@ -135,7 +135,7 @@ pub fn run_mid_frequency_pipeline( validate_input(&input, config)?; let event_indices = - cusum_filter_indices(&input.close, Threshold::Scalar(config.cusum_threshold)); + cusum_filter_indices(input.close, Threshold::Scalar(config.cusum_threshold)); if event_indices.is_empty() { return Err(PipelineError::NoEvents); } @@ -173,7 +173,7 @@ pub fn run_mid_frequency_pipeline( let (strategy_returns, equity_curve) = compute_strategy_path(input.close, &signals.timeline_signal); - let risk_metrics = RiskMetrics::default(); + let risk_metrics = RiskMetrics; let value_at_risk = risk_metrics.calculate_value_at_risk(&strategy_returns, config.confidence_level)?; let expected_shortfall = diff --git a/crates/openquant/src/portfolio_optimization.rs b/crates/openquant/src/portfolio_optimization.rs index 8a14ec2..bfb126f 100644 --- a/crates/openquant/src/portfolio_optimization.rs +++ b/crates/openquant/src/portfolio_optimization.rs @@ -12,16 +12,13 @@ pub enum AllocError { NaNResult(&'static str), } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub enum ReturnsMethod { + #[default] Mean, - Exponential { span: usize }, -} - -impl Default for ReturnsMethod { - fn default() -> Self { - ReturnsMethod::Mean - } + Exponential { + span: usize, + }, } #[derive(Clone)] @@ -140,8 +137,8 @@ fn returns_and_means( let mut expected = vec![0.0; cols]; match opts.returns_method { ReturnsMethod::Mean => { - for c in 0..cols { - expected[c] = (returns.column(c).sum() / rows as f64) * freq; + for (c, slot) in expected.iter_mut().enumerate() { + *slot = (returns.column(c).sum() / rows as f64) * freq; } } ReturnsMethod::Exponential { span } => { diff --git a/crates/openquant/src/sampling.rs b/crates/openquant/src/sampling.rs index 771a642..93efc42 100644 --- a/crates/openquant/src/sampling.rs +++ b/crates/openquant/src/sampling.rs @@ -148,8 +148,8 @@ pub fn num_concurrent_events( continue; } let end_idx = end.min(price_index_len - 1); - for i in start..=end_idx { - counts[i] += 1; + for count in counts.iter_mut().take(end_idx + 1).skip(start) { + *count += 1; } } counts diff --git a/crates/openquant/src/streaming_hpc.rs b/crates/openquant/src/streaming_hpc.rs index 7e393a7..4d84779 100644 --- a/crates/openquant/src/streaming_hpc.rs +++ b/crates/openquant/src/streaming_hpc.rs @@ -152,7 +152,7 @@ impl VpinState { pub fn update( &mut self, mut buy_volume: f64, - mut sell_volume: f64, + sell_volume: f64, ) -> Result, StreamingHpcError> { validate_non_negative_finite("buy_volume", buy_volume)?; validate_non_negative_finite("sell_volume", sell_volume)?; @@ -175,7 +175,6 @@ impl VpinState { self.current_bucket_abs_imbalance += (used_buy - used_sell).abs(); buy_volume -= used_buy; - sell_volume -= used_sell; remaining -= take; if self.current_bucket_volume >= self.cfg.bucket_volume - 1e-12 { diff --git a/crates/openquant/src/structural_breaks.rs b/crates/openquant/src/structural_breaks.rs index ffdc8c3..2ba2d20 100644 --- a/crates/openquant/src/structural_breaks.rs +++ b/crates/openquant/src/structural_breaks.rs @@ -177,12 +177,15 @@ pub fn _get_betas( Ok((b_mean_vec, matrix_to_vec(b_var_matrix))) } +/// Regression inputs for SADF: `(x rows, y, lag list)`. +type SadfRegressionInputs = (Vec>, Vec, Vec); + fn get_y_x( series: &[f64], model: &str, lags: SadfLags, add_const: bool, -) -> StructuralBreakResult<(Vec>, Vec, Vec)> { +) -> StructuralBreakResult { let series_len = series.len(); if series_len < 2 { return Err(StructuralBreakError::InputTooShort); @@ -195,7 +198,7 @@ fn get_y_x( let lag_values = match lags { SadfLags::Fixed(value) => (1..=value).collect::>(), - SadfLags::Array(values) => values.into_iter().map(|v| v as usize).collect(), + SadfLags::Array(values) => values.into_iter().collect(), }; let max_lag = *lag_values.iter().max().unwrap_or(&0); let start_index = max_lag + 1; @@ -299,7 +302,7 @@ fn get_sadf_at_t(x: &[Vec], y: &[f64], min_length: usize) -> StructuralBrea let x_subset = x[start..].to_vec(); let (b_mean, b_var) = _get_betas(&x_subset, &y_subset)?; - if b_mean.get(0).map(|v| v.is_nan()).unwrap_or(true) { + if b_mean.first().map(|v| v.is_nan()).unwrap_or(true) { continue; } diff --git a/crates/openquant/src/util/volatility.rs b/crates/openquant/src/util/volatility.rs index af9c235..032df8e 100644 --- a/crates/openquant/src/util/volatility.rs +++ b/crates/openquant/src/util/volatility.rs @@ -21,8 +21,8 @@ pub fn get_daily_vol(close: &[(NaiveDateTime, f64)], lookback: usize) -> Vec<(Na // searchsorted equivalent: find insertion point for target_time let mut j_opt = None; - for j in 0..i { - if close[j].0 <= target_time { + for (j, (ts_j, _)) in close.iter().enumerate().take(i) { + if *ts_j <= target_time { j_opt = Some(j); } } diff --git a/crates/openquant/tests/bet_sizing.rs b/crates/openquant/tests/bet_sizing.rs index a10f1e6..55f26ed 100644 --- a/crates/openquant/tests/bet_sizing.rs +++ b/crates/openquant/tests/bet_sizing.rs @@ -217,7 +217,7 @@ fn test_cdf_mixture_and_single_above_zero() { let cdf = cdf_mixture(fit[0], fit[1], fit[2], fit[3], fit[4], 0.5); assert!(cdf > 0.0 && cdf < 1.0); let b = single_bet_size_mixed(0.5, &fit); - assert!(b >= -1.0 && b <= 1.0); + assert!((-1.0..=1.0).contains(&b)); } #[test] diff --git a/crates/openquant/tests/ch10_snippets.rs b/crates/openquant/tests/ch10_snippets.rs index f4c2bc7..3827387 100644 --- a/crates/openquant/tests/ch10_snippets.rs +++ b/crates/openquant/tests/ch10_snippets.rs @@ -34,23 +34,12 @@ fn build_ch10_setup() -> Ch10Setup { }) .collect(); - let bet_size_d: Vec = bet_size - .iter() - .map(|m| { - let mut v = (m / 0.1).round() * 0.1; - if v > 1.0 { - v = 1.0; - } - if v < -1.0 { - v = -1.0; - } - v - }) - .collect(); + let bet_size_d: Vec = + bet_size.iter().map(|m| ((m / 0.1).round() * 0.1).clamp(-1.0, 1.0)).collect(); let signal: Vec<(NaiveDateTime, f64)> = dates.iter().copied().zip(bet_size.iter().copied()).collect(); - let mut t_pnts: Vec = t1.iter().copied().collect(); + let mut t_pnts: Vec = t1.to_vec(); t_pnts.extend(dates.iter().copied()); t_pnts.sort(); t_pnts.dedup(); diff --git a/crates/openquant/tests/cla.rs b/crates/openquant/tests/cla.rs index 09c0c11..f3b3276 100644 --- a/crates/openquant/tests/cla.rs +++ b/crates/openquant/tests/cla.rs @@ -144,7 +144,7 @@ fn test_lambda_for_no_bounded_weights() { let cov = covariance(&prices.data); let (x, y) = cla._compute_lambda(&cov, &cov, &cla.expected_returns, None, &[1], &[0]); assert!(x.is_finite()); - let _ = y as i64; + let _ = y; } #[test] @@ -224,13 +224,15 @@ fn test_purge_excess() { #[test] fn test_flag_true_for_purge_num_err() { - let mut cla = CLA::default(); - cla.weights = vec![vec![1.0]]; - cla.lower_bounds = vec![100.0]; - cla.upper_bounds = vec![1.0]; - cla.lambdas = vec![0.0]; - cla.gammas = vec![0.0]; - cla.free_weights = vec![vec![]]; + let mut cla = CLA { + weights: vec![vec![1.0]], + lower_bounds: vec![100.0], + upper_bounds: vec![1.0], + lambdas: vec![0.0], + gammas: vec![0.0], + free_weights: vec![vec![]], + ..CLA::default() + }; cla._purge_num_err(1.0).unwrap(); assert!(cla.weights.is_empty()); assert!(cla.lambdas.is_empty()); diff --git a/crates/openquant/tests/cross_validation.rs b/crates/openquant/tests/cross_validation.rs index 08c01aa..160be62 100644 --- a/crates/openquant/tests/cross_validation.rs +++ b/crates/openquant/tests/cross_validation.rs @@ -112,7 +112,7 @@ fn test_ml_cross_val_score_accuracy() { let scores = ml_cross_val_score(&mut clf, &x, &y, None, &splits, Scoring::Accuracy); assert_eq!(scores.len(), 3); for s in scores { - assert!(s >= 0.0 && s <= 1.0); + assert!((0.0..=1.0).contains(&s)); } } diff --git a/crates/openquant/tests/feature_importance.rs b/crates/openquant/tests/feature_importance.rs index e6813cb..8a02154 100644 --- a/crates/openquant/tests/feature_importance.rs +++ b/crates/openquant/tests/feature_importance.rs @@ -60,7 +60,10 @@ impl SimpleClassifier for LinearProbClassifier { } } -fn make_dataset() -> (Vec>, Vec, Vec, Vec<(Vec, Vec)>) { +/// `(x, y, feature_names, cv_splits)`. +type Dataset = (Vec>, Vec, Vec, Vec<(Vec, Vec)>); + +fn make_dataset() -> Dataset { let mut x = Vec::new(); let mut y = Vec::new(); for i in 0..120usize { @@ -91,7 +94,7 @@ fn test_orthogonal_features_and_pca_analysis() { let (x, _y, _names, _splits) = make_dataset(); let pca = get_orthogonal_features(&x, 0.95).unwrap(); assert_eq!(pca.len(), x.len()); - assert!(pca[0].len() >= 1); + assert!(!pca[0].is_empty()); let first_pc_mean = pca.iter().map(|r| r[0]).sum::() / pca.len() as f64; assert!(first_pc_mean.abs() < 1e-6); diff --git a/crates/openquant/tests/futures_roll.rs b/crates/openquant/tests/futures_roll.rs index a338389..5b83a7a 100644 --- a/crates/openquant/tests/futures_roll.rs +++ b/crates/openquant/tests/futures_roll.rs @@ -34,7 +34,7 @@ fn load_rows() -> Vec { let roll_2 = NaiveDate::from_ymd_opt(2018, 1, 17).unwrap(); let mut rows = Vec::with_capacity(opens.len()); - for (o, c) in opens.into_iter().zip(closes.into_iter()) { + for (o, c) in opens.into_iter().zip(closes) { assert_eq!(o.date, c.date); let date = NaiveDate::parse_from_str(&o.date, "%Y-%m-%d").unwrap(); let current = if date <= roll_1 { diff --git a/crates/openquant/tests/hyperparameter_tuning.rs b/crates/openquant/tests/hyperparameter_tuning.rs index 65e7aaa..ff50eef 100644 --- a/crates/openquant/tests/hyperparameter_tuning.rs +++ b/crates/openquant/tests/hyperparameter_tuning.rs @@ -147,8 +147,8 @@ fn test_randomized_search_seeded_deterministic_and_log_uniform() { let mut rng = StdRng::seed_from_u64(7); let s1 = sample_log_uniform(1e-3, 1e1, &mut rng).unwrap(); let s2 = sample_log_uniform(1e-3, 1e1, &mut rng).unwrap(); - assert!(s1 >= 1e-3 && s1 <= 1e1); - assert!(s2 >= 1e-3 && s2 <= 1e1); + assert!((1e-3..=1e1).contains(&s1)); + assert!((1e-3..=1e1).contains(&s2)); assert!((s1 - s2).abs() > 1e-12); } diff --git a/crates/openquant/tests/labeling.rs b/crates/openquant/tests/labeling.rs index e1bf7dc..7bb1e0f 100644 --- a/crates/openquant/tests/labeling.rs +++ b/crates/openquant/tests/labeling.rs @@ -177,7 +177,7 @@ fn test_triple_barrier_labeling() { ); let labels = get_bins(&events, &close); assert_eq!(labels.len(), 8); - assert!(labels.iter().all(|(_, _, _, bin, _)| matches!(bin, -1 | 0 | 1))); + assert!(labels.iter().all(|(_, _, _, bin, _)| matches!(bin, -1..=1))); // meta labeling with side=1 let side: Vec<(NaiveDateTime, f64)> = close.iter().map(|(ts, _)| (*ts, 1.0)).collect(); @@ -243,7 +243,7 @@ fn test_pt_sl_levels() { assert!(small_vertical_hits < high_vertical_hits); let labels_small = get_bins(&events_small, &close); - assert!(labels_small.iter().all(|(_, _, _, bin, _)| matches!(bin, -1 | 0 | 1))); + assert!(labels_small.iter().all(|(_, _, _, bin, _)| matches!(bin, -1..=1))); } #[test] @@ -299,7 +299,7 @@ fn test_triple_barrier_disabled_barrier_configurations() { let labels_none = triple_barrier_labels(&events_none, &close); assert!(!labels_none.is_empty()); - assert!(labels_none.iter().all(|row| matches!(row.label, -1 | 0 | 1))); + assert!(labels_none.iter().all(|row| matches!(row.label, -1..=1))); let cfg_pt_only = TripleBarrierConfig { pt: 1.0, diff --git a/crates/openquant/tests/microstructural_features.rs b/crates/openquant/tests/microstructural_features.rs index ec2729c..a0932be 100644 --- a/crates/openquant/tests/microstructural_features.rs +++ b/crates/openquant/tests/microstructural_features.rs @@ -8,7 +8,10 @@ use openquant::microstructural_features::{ }; use std::path::Path; -fn load_dollar_bars() -> (Vec, Vec, Vec, Vec, Vec) { +/// `(close, high, low, cum_dollar, cum_volume)` columns. +type DollarBarColumns = (Vec, Vec, Vec, Vec, Vec); + +fn load_dollar_bars() -> DollarBarColumns { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../tests/fixtures/microstructural_features/dollar_bar_sample.csv"); let mut rdr = ReaderBuilder::new().has_headers(true).from_path(path).unwrap(); diff --git a/crates/openquant/tests/portfolio_optimization.rs b/crates/openquant/tests/portfolio_optimization.rs index f7a3786..d733275 100644 --- a/crates/openquant/tests/portfolio_optimization.rs +++ b/crates/openquant/tests/portfolio_optimization.rs @@ -66,7 +66,7 @@ fn test_against_python_fixture_weights() { .zip(weights.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 1.0, "inverse variance max diff {}", max_diff); + assert!(max_diff < 1.0, "inverse variance max diff {max_diff}"); let w_min = fixture["weights"]["min_volatility"].as_array().unwrap(); let res_min = allocate_min_vol(&prices, None, None).unwrap(); @@ -76,7 +76,7 @@ fn test_against_python_fixture_weights() { .zip(w_min.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 1.0, "min vol bound diff {}", max_diff); + assert!(max_diff < 1.0, "min vol bound diff {max_diff}"); let w_max = fixture["weights"]["max_sharpe"].as_array().unwrap(); let res_max = allocate_max_sharpe(&prices, 0.0, None, None).unwrap(); @@ -86,7 +86,7 @@ fn test_against_python_fixture_weights() { .zip(w_max.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 1.0, "max sharpe diff {}", max_diff); + assert!(max_diff < 1.0, "max sharpe diff {max_diff}"); } #[test] @@ -167,7 +167,7 @@ fn test_bound_and_infeasible_behavior_against_fixture() { .zip(exp_min.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 0.25, "min vol bound diff {}", max_diff); + assert!(max_diff < 0.25, "min vol bound diff {max_diff}"); let res_max = openquant::portfolio_optimization::allocate_max_sharpe_with(&prices, &opts).unwrap(); @@ -178,7 +178,7 @@ fn test_bound_and_infeasible_behavior_against_fixture() { .zip(exp_max.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 1.0, "max sharpe bound diff {}", max_diff); + assert!(max_diff < 1.0, "max sharpe bound diff {max_diff}"); let res_eff = openquant::portfolio_optimization::allocate_efficient_risk_with( &prices, @@ -192,7 +192,7 @@ fn test_bound_and_infeasible_behavior_against_fixture() { .zip(exp_eff.iter()) .map(|(r, e)| (r - e.as_f64().unwrap()).abs()) .fold(0.0_f64, f64::max); - assert!(max_diff < 1.0, "efficient risk bound diff {}", max_diff); + assert!(max_diff < 1.0, "efficient risk bound diff {max_diff}"); let err = allocate_min_vol(&prices, None, Some((0.9, 1.0))).unwrap_err(); assert!(matches!(err, AllocError::InfeasibleBounds { .. })); diff --git a/crates/openquant/tests/sample_weights.rs b/crates/openquant/tests/sample_weights.rs index c4661fd..8592896 100644 --- a/crates/openquant/tests/sample_weights.rs +++ b/crates/openquant/tests/sample_weights.rs @@ -29,12 +29,14 @@ fn load_close() -> Vec<(NaiveDateTime, f64)> { out } -fn setup_events() -> ( +type EventsSetup = ( Vec<(NaiveDateTime, NaiveDateTime, f64)>, Vec<(NaiveDateTime, f64)>, Vec, Vec, -) { +); + +fn setup_events() -> EventsSetup { let close = load_close(); let prices: Vec = close.iter().map(|(_, p)| *p).collect(); let timestamps: Vec = close.iter().map(|(ts, _)| *ts).collect(); diff --git a/crates/openquant/tests/sampling.rs b/crates/openquant/tests/sampling.rs index d637640..f003c6c 100644 --- a/crates/openquant/tests/sampling.rs +++ b/crates/openquant/tests/sampling.rs @@ -6,7 +6,7 @@ use openquant::sampling::{ fn setup_labels() -> (Vec, Vec<(usize, usize)>) { // price bars hourly range 0..=168 (per test_sampling) let price_bars: Vec = (0..=168).collect(); - let t_events = vec![1, 2, 5, 7, 10, 11, 12, 20]; + let t_events = [1, 2, 5, 7, 10, 11, 12, 20]; let t1: Vec<(usize, usize)> = t_events.iter().map(|t| (*t, t + 2)).collect(); (price_bars, t1) } @@ -167,7 +167,7 @@ fn test_bootstrap_loop_run() { let second = openquant::sampling::bootstrap_loop_run(&ind, &prev_conc); let sum: f64 = second.iter().sum(); let probs: Vec = second.iter().map(|v| *v / sum).collect(); - let target = vec![0.35714286, 0.21428571, 0.42857143]; + let target = [0.35714286, 0.21428571, 0.42857143]; for (p, t) in probs.iter().zip(target.iter()) { assert!((p - t).abs() <= 1e-6); } diff --git a/crates/pyopenquant/src/bars.rs b/crates/pyopenquant/src/bars.rs index cdd8e07..c66a8bf 100644 --- a/crates/pyopenquant/src/bars.rs +++ b/crates/pyopenquant/src/bars.rs @@ -4,7 +4,7 @@ use openquant::data_structures::{ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use crate::helpers::{bars_to_rows, build_trades}; +use crate::helpers::{bars_to_rows, build_trades, BarRow}; #[pyfunction(name = "build_time_bars")] fn bars_build_time_bars( @@ -12,7 +12,7 @@ fn bars_build_time_bars( prices: Vec, volumes: Vec, interval_seconds: i64, -) -> PyResult> { +) -> PyResult> { if interval_seconds <= 0 { return Err(PyValueError::new_err("interval_seconds must be > 0")); } @@ -27,7 +27,7 @@ fn bars_build_tick_bars( prices: Vec, volumes: Vec, ticks_per_bar: usize, -) -> PyResult> { +) -> PyResult> { if ticks_per_bar == 0 { return Err(PyValueError::new_err("ticks_per_bar must be > 0")); } @@ -42,7 +42,7 @@ fn bars_build_volume_bars( prices: Vec, volumes: Vec, volume_per_bar: f64, -) -> PyResult> { +) -> PyResult> { if !volume_per_bar.is_finite() || volume_per_bar <= 0.0 { return Err(PyValueError::new_err("volume_per_bar must be > 0")); } @@ -57,7 +57,7 @@ fn bars_build_dollar_bars( prices: Vec, volumes: Vec, dollar_value_per_bar: f64, -) -> PyResult> { +) -> PyResult> { if !dollar_value_per_bar.is_finite() || dollar_value_per_bar <= 0.0 { return Err(PyValueError::new_err("dollar_value_per_bar must be > 0")); } @@ -72,7 +72,7 @@ fn bars_build_run_bars( prices: Vec, volumes: Vec, threshold: usize, -) -> PyResult> { +) -> PyResult> { if threshold == 0 { return Err(PyValueError::new_err("threshold must be > 0")); } @@ -88,7 +88,7 @@ fn bars_build_imbalance_bars( volumes: Vec, threshold: f64, bar_type: String, -) -> PyResult> { +) -> PyResult> { if !threshold.is_finite() || threshold <= 0.0 { return Err(PyValueError::new_err("threshold must be > 0")); } diff --git a/crates/pyopenquant/src/bet_sizing.rs b/crates/pyopenquant/src/bet_sizing.rs index ecc5d20..ae37eef 100644 --- a/crates/pyopenquant/src/bet_sizing.rs +++ b/crates/pyopenquant/src/bet_sizing.rs @@ -2,6 +2,9 @@ use pyo3::prelude::*; use crate::helpers::{pair_timestamps_values, parse_naive_datetimes, to_py_err}; +/// Python-facing reserve bet-size row: `(timestamp, active_long, active_short, c_t, bet_size)`. +type ReserveRow = (String, f64, f64, f64, f64); + #[pyfunction(name = "get_signal")] #[pyo3(signature = (prob, num_classes, pred=None))] fn bet_sizing_get_signal(prob: Vec, num_classes: usize, pred: Option>) -> Vec { @@ -276,7 +279,7 @@ fn bet_sizing_bet_size_reserve_with_fit( t1_ends: Vec, side: Vec, fit: [f64; 5], -) -> PyResult> { +) -> PyResult> { let starts = parse_naive_datetimes(t1_starts)?; let ends = parse_naive_datetimes(t1_ends)?; if starts.len() != ends.len() || starts.len() != side.len() { @@ -302,7 +305,7 @@ fn bet_sizing_bet_size_reserve_full( epsilon: f64, max_iter: usize, return_parameters: bool, -) -> PyResult<(Vec<(String, f64, f64, f64, f64)>, Option<[f64; 5]>)> { +) -> PyResult<(Vec, Option<[f64; 5]>)> { let starts = parse_naive_datetimes(t1_starts)?; let ends = parse_naive_datetimes(t1_ends)?; if starts.len() != ends.len() || starts.len() != side.len() { diff --git a/crates/pyopenquant/src/cla.rs b/crates/pyopenquant/src/cla.rs index 93e638c..ec77aa8 100644 --- a/crates/pyopenquant/src/cla.rs +++ b/crates/pyopenquant/src/cla.rs @@ -14,6 +14,8 @@ use crate::helpers::{matrix_from_rows, to_py_err}; solution=None, calculate_expected_returns="mean" ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn cla_allocate( py: Python<'_>, asset_prices: Option>>, @@ -36,7 +38,7 @@ fn cla_allocate( let expected_ret_m = expected_returns.map(|v| nalgebra::DMatrix::from_vec(v.len(), 1, v)); cla.allocate( - prices_m.as_ref().map(|m| openquant::cla::AssetPricesInput::RawMatrix(m)), + prices_m.as_ref().map(openquant::cla::AssetPricesInput::RawMatrix), expected_ret_m.as_ref(), cov_m.as_ref(), resample_by.as_deref(), diff --git a/crates/pyopenquant/src/data.rs b/crates/pyopenquant/src/data.rs index 81a8b5a..38a2fae 100644 --- a/crates/pyopenquant/src/data.rs +++ b/crates/pyopenquant/src/data.rs @@ -8,7 +8,26 @@ use pyo3_polars::PyDataFrame; use crate::helpers::{build_ohlcv_columns, report_to_pydict, to_py_err}; +/// `(timestamps_us, symbols, open, high, low, close, volume, adj_close, quality_report)`. +type CleanOhlcvColumns = + (Vec, Vec, Vec, Vec, Vec, Vec, Vec, Vec, PyObject); + +/// `(timestamps_us, symbols, open, high, low, close, volume, adj_close, is_missing_bar)`. +type AlignedOhlcvColumns = ( + Vec, + Vec, + Vec>, + Vec>, + Vec>, + Vec>, + Vec>, + Vec>, + Vec, +); + #[pyfunction(name = "clean_ohlcv")] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn data_clean_ohlcv( py: Python<'_>, timestamps_us: Vec, @@ -20,17 +39,7 @@ fn data_clean_ohlcv( volume: Vec, adj_close: Vec, dedupe_keep_last: bool, -) -> PyResult<( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - PyObject, -)> { +) -> PyResult { let cols = build_ohlcv_columns(timestamps_us, symbols, open, high, low, close, volume, adj_close)?; let (clean, report) = clean_ohlcv_columns(&cols, dedupe_keep_last).map_err(to_py_err)?; @@ -59,6 +68,8 @@ fn data_clean_ohlcv( } #[pyfunction(name = "quality_report")] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn data_quality_report( py: Python<'_>, timestamps_us: Vec, @@ -87,6 +98,8 @@ fn data_quality_report( } #[pyfunction(name = "align_calendar")] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn data_align_calendar( timestamps_us: Vec, symbols: Vec, @@ -97,17 +110,7 @@ fn data_align_calendar( volume: Vec, adj_close: Vec, interval_seconds: i64, -) -> PyResult<( - Vec, - Vec, - Vec>, - Vec>, - Vec>, - Vec>, - Vec>, - Vec>, - Vec, -)> { +) -> PyResult { let cols = build_ohlcv_columns(timestamps_us, symbols, open, high, low, close, volume, adj_close)?; let out = align_calendar_columns(&cols, interval_seconds).map_err(to_py_err)?; diff --git a/crates/pyopenquant/src/ef3m.rs b/crates/pyopenquant/src/ef3m.rs index faeb65f..746a2ff 100644 --- a/crates/pyopenquant/src/ef3m.rs +++ b/crates/pyopenquant/src/ef3m.rs @@ -1,6 +1,9 @@ use pyo3::prelude::*; use pyo3::types::PyDict; +/// One M2N fit: `(mu_1, mu_2, sigma_1, sigma_2, p_1, error)`. +type M2nFitRow = (f64, f64, f64, f64, f64, f64); + #[pyfunction(name = "centered_moment")] fn ef3m_centered_moment(moments: Vec, order: usize) -> f64 { openquant::ef3m::centered_moment(&moments, order) @@ -52,7 +55,7 @@ fn ef3m_fit_m2n( n_runs: usize, variant: usize, max_iter: usize, -) -> PyResult> { +) -> PyResult> { let mut m2n = openquant::ef3m::M2N::new(moments, epsilon, factor, n_runs, variant, max_iter, 1); let results = m2n.single_fit_loop(None); Ok(results diff --git a/crates/pyopenquant/src/hcaa.rs b/crates/pyopenquant/src/hcaa.rs index dd3996d..76c2d83 100644 --- a/crates/pyopenquant/src/hcaa.rs +++ b/crates/pyopenquant/src/hcaa.rs @@ -15,6 +15,8 @@ use crate::helpers::{matrix_from_rows, to_py_err}; resample_by=None, calculate_expected_returns="mean" ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn hcaa_allocate( asset_names: Vec, asset_prices: Option>>, diff --git a/crates/pyopenquant/src/helpers.rs b/crates/pyopenquant/src/helpers.rs index 50924c8..23ca55a 100644 --- a/crates/pyopenquant/src/helpers.rs +++ b/crates/pyopenquant/src/helpers.rs @@ -3,6 +3,14 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyDict; +/// Python-facing bar row: +/// `(start_timestamp, timestamp, open, high, low, close, volume, dollar_value, tick_count)`. +pub type BarRow = (String, String, f64, f64, f64, f64, f64, f64, usize); + +/// Parsed labeling inputs: `(close series, events keyed by timestamp)`. +pub type LabelingInputs = + (Vec<(chrono::NaiveDateTime, f64)>, Vec<(chrono::NaiveDateTime, openquant::labeling::Event)>); + pub fn to_py_err(err: T) -> PyErr { PyValueError::new_err(format!("{err:?}")) } @@ -116,9 +124,7 @@ pub fn build_trades( Ok(trades) } -pub fn bars_to_rows( - bars: Vec, -) -> Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)> { +pub fn bars_to_rows(bars: Vec) -> Vec { bars.into_iter() .map(|b| { ( @@ -136,6 +142,8 @@ pub fn bars_to_rows( .collect() } +// Takes the raw OHLCV columns it validates into `OhlcvColumns`; a params struct would duplicate that type. +#[allow(clippy::too_many_arguments)] pub fn build_ohlcv_columns( timestamps_us: Vec, symbols: Vec, @@ -197,21 +205,33 @@ pub fn report_to_pydict( Ok(out_report.into_pyobject(py).unwrap().into_any().unbind()) } -pub fn build_labeling_events( - close_timestamps: Vec, - close_prices: Vec, - t_events: Vec, - target_timestamps: Vec, - target_values: Vec, - pt: f64, - sl: f64, - min_ret: f64, - vertical_barrier_times: Option>, - side_prediction: Option>, -) -> PyResult<( - Vec<(chrono::NaiveDateTime, f64)>, - Vec<(chrono::NaiveDateTime, openquant::labeling::Event)>, -)> { +/// Raw Python-side inputs shared by the triple-barrier labeling bindings. +pub struct LabelingEventArgs { + pub close_timestamps: Vec, + pub close_prices: Vec, + pub t_events: Vec, + pub target_timestamps: Vec, + pub target_values: Vec, + pub pt: f64, + pub sl: f64, + pub min_ret: f64, + pub vertical_barrier_times: Option>, + pub side_prediction: Option>, +} + +pub fn build_labeling_events(args: LabelingEventArgs) -> PyResult { + let LabelingEventArgs { + close_timestamps, + close_prices, + t_events, + target_timestamps, + target_values, + pt, + sl, + min_ret, + vertical_barrier_times, + side_prediction, + } = args; let close = pair_timestamps_values(close_timestamps, close_prices, "close_timestamps", "close_prices")?; let t_events = parse_naive_datetimes(t_events)?; diff --git a/crates/pyopenquant/src/labeling.rs b/crates/pyopenquant/src/labeling.rs index 6bdc4ae..4d84540 100644 --- a/crates/pyopenquant/src/labeling.rs +++ b/crates/pyopenquant/src/labeling.rs @@ -2,8 +2,15 @@ use pyo3::prelude::*; use crate::helpers::{ build_labeling_events, pair_timestamps_values, parse_naive_datetimes, parse_vertical_barriers, + LabelingEventArgs, }; +/// Python-facing event row: `(timestamp, t1, trgt, side, pt, sl)`. +type EventRow = (String, Option, f64, Option, f64, f64); + +/// Python-facing label row: `(timestamp, ret, trgt, bin, side)`. +type BinRow = (String, f64, f64, i8, Option); + #[pyfunction(name = "add_vertical_barrier")] fn labeling_add_vertical_barrier( t_events: Vec, @@ -46,6 +53,8 @@ fn labeling_add_vertical_barrier( vertical_barrier_times=None, side_prediction=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn labeling_triple_barrier_events( close_timestamps: Vec, close_prices: Vec, @@ -57,8 +66,8 @@ fn labeling_triple_barrier_events( min_ret: f64, vertical_barrier_times: Option>, side_prediction: Option>, -) -> PyResult, f64, Option, f64, f64)>> { - let (_, events) = build_labeling_events( +) -> PyResult> { + let (_, events) = build_labeling_events(LabelingEventArgs { close_timestamps, close_prices, t_events, @@ -69,7 +78,7 @@ fn labeling_triple_barrier_events( min_ret, vertical_barrier_times, side_prediction, - )?; + })?; Ok(events .into_iter() .map(|(ts, ev)| { @@ -97,6 +106,8 @@ fn labeling_triple_barrier_events( min_ret=0.0, vertical_barrier_times=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn labeling_triple_barrier_labels( close_timestamps: Vec, close_prices: Vec, @@ -107,8 +118,8 @@ fn labeling_triple_barrier_labels( sl: f64, min_ret: f64, vertical_barrier_times: Option>, -) -> PyResult)>> { - let (close, events) = build_labeling_events( +) -> PyResult> { + let (close, events) = build_labeling_events(LabelingEventArgs { close_timestamps, close_prices, t_events, @@ -118,8 +129,8 @@ fn labeling_triple_barrier_labels( sl, min_ret, vertical_barrier_times, - None, - )?; + side_prediction: None, + })?; Ok(openquant::labeling::triple_barrier_labels(&events, &close) .into_iter() .map(|row| { @@ -147,6 +158,8 @@ fn labeling_triple_barrier_labels( min_ret=0.0, vertical_barrier_times=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn labeling_meta_labels( close_timestamps: Vec, close_prices: Vec, @@ -158,8 +171,8 @@ fn labeling_meta_labels( sl: f64, min_ret: f64, vertical_barrier_times: Option>, -) -> PyResult)>> { - let (close, events) = build_labeling_events( +) -> PyResult> { + let (close, events) = build_labeling_events(LabelingEventArgs { close_timestamps, close_prices, t_events, @@ -169,8 +182,8 @@ fn labeling_meta_labels( sl, min_ret, vertical_barrier_times, - Some(side_prediction), - )?; + side_prediction: Some(side_prediction), + })?; Ok(openquant::labeling::meta_labels(&events, &close) .into_iter() .map(|row| { @@ -198,6 +211,8 @@ fn labeling_meta_labels( vertical_barrier_times=None, side_prediction=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn labeling_get_events( close_timestamps: Vec, close_prices: Vec, @@ -209,7 +224,7 @@ fn labeling_get_events( num_threads: usize, vertical_barrier_times: Option>, side_prediction: Option>, -) -> PyResult, f64, Option, f64, f64)>> { +) -> PyResult> { let close = pair_timestamps_values(close_timestamps, close_prices, "close_timestamps", "close_prices")?; let t_ev = parse_naive_datetimes(t_events)?; @@ -256,10 +271,10 @@ fn labeling_get_events( #[pyfunction(name = "get_bins")] fn labeling_get_bins( - events: Vec<(String, Option, f64, Option, f64, f64)>, + events: Vec, close_timestamps: Vec, close_prices: Vec, -) -> PyResult)>> { +) -> PyResult> { let close = pair_timestamps_values(close_timestamps, close_prices, "close_timestamps", "close_prices")?; @@ -290,10 +305,7 @@ fn labeling_get_bins( } #[pyfunction(name = "drop_labels")] -fn labeling_drop_labels( - events: Vec<(String, f64, f64, i8, Option)>, - min_pct: f64, -) -> Vec<(String, f64, f64, i8, Option)> { +fn labeling_drop_labels(events: Vec, min_pct: f64) -> Vec { let parsed: Vec<(chrono::NaiveDateTime, f64, f64, i8, Option)> = events .into_iter() .filter_map(|(ts_str, ret, trgt, label, side)| { diff --git a/crates/pyopenquant/src/pipeline.rs b/crates/pyopenquant/src/pipeline.rs index 8e626d9..ae76d74 100644 --- a/crates/pyopenquant/src/pipeline.rs +++ b/crates/pyopenquant/src/pipeline.rs @@ -20,6 +20,8 @@ use crate::helpers::{format_naive_datetimes, matrix_from_rows, parse_naive_datet risk_free_rate=0.0, confidence_level=0.05 ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn pipeline_run_mid_frequency_pipeline( py: Python<'_>, timestamps: Vec, diff --git a/crates/pyopenquant/src/portfolio.rs b/crates/pyopenquant/src/portfolio.rs index 0242e69..8f42ef1 100644 --- a/crates/pyopenquant/src/portfolio.rs +++ b/crates/pyopenquant/src/portfolio.rs @@ -80,6 +80,8 @@ fn portfolio_allocate_efficient_risk( resample_by=None, returns_method=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn portfolio_allocate_with_solution( prices: Vec>, solution: String, diff --git a/crates/pyopenquant/src/sb_bagging.rs b/crates/pyopenquant/src/sb_bagging.rs index 1461942..6af2539 100644 --- a/crates/pyopenquant/src/sb_bagging.rs +++ b/crates/pyopenquant/src/sb_bagging.rs @@ -14,6 +14,8 @@ use crate::helpers::{matrix_from_rows, to_py_err}; random_state=42, sample_weight=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn sb_fit_predict_classifier( py: Python<'_>, x: Vec>, @@ -54,6 +56,8 @@ fn sb_fit_predict_classifier( random_state=42, sample_weight=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn sb_fit_predict_regressor( py: Python<'_>, x: Vec>, diff --git a/crates/pyopenquant/src/strategy_risk.rs b/crates/pyopenquant/src/strategy_risk.rs index de9d8c0..d5e30c9 100644 --- a/crates/pyopenquant/src/strategy_risk.rs +++ b/crates/pyopenquant/src/strategy_risk.rs @@ -70,6 +70,8 @@ fn sr_implied_frequency_asymmetric( seed=42, kde_bandwidth=None ))] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn sr_estimate_strategy_failure_probability( py: Python<'_>, bet_outcomes: Vec, diff --git a/crates/pyopenquant/src/streaming_hpc.rs b/crates/pyopenquant/src/streaming_hpc.rs index 557dbad..80751c3 100644 --- a/crates/pyopenquant/src/streaming_hpc.rs +++ b/crates/pyopenquant/src/streaming_hpc.rs @@ -3,10 +3,16 @@ use pyo3::types::PyDict; use crate::helpers::to_py_err; +/// Python-facing stream event: `(timestamp_ns, price, buy_volume, sell_volume, venue_id)`. +type StreamEventRow = (i64, f64, f64, f64, usize); + +/// Python-facing snapshot: `(timestamp_ns, price, vpin, hhi, normalized_risk_score, is_alert)`. +type SnapshotRow = (i64, f64, Option, Option, Option, bool); + #[pyfunction(name = "run_streaming_pipeline")] fn shpc_run_streaming_pipeline( py: Python<'_>, - events: Vec<(i64, f64, f64, f64, usize)>, + events: Vec, bucket_volume: f64, support_buckets: usize, lookback_events: usize, @@ -38,7 +44,7 @@ fn shpc_run_streaming_pipeline( let d = PyDict::new(py); - let snapshots: Vec<(i64, f64, Option, Option, Option, bool)> = report + let snapshots: Vec = report .snapshots .into_iter() .map(|s| (s.timestamp_ns, s.price, s.vpin, s.hhi, s.normalized_risk_score, s.is_alert)) @@ -64,7 +70,7 @@ fn shpc_generate_synthetic_flash_crash_stream( crash_start_fraction: f64, calm_venues: usize, shock_venue: usize, -) -> PyResult> { +) -> PyResult> { let cfg = openquant::streaming_hpc::SyntheticStreamConfig { events, crash_start_fraction, diff --git a/crates/pyopenquant/src/synthetic_bt.rs b/crates/pyopenquant/src/synthetic_bt.rs index 3654608..dd21fe6 100644 --- a/crates/pyopenquant/src/synthetic_bt.rs +++ b/crates/pyopenquant/src/synthetic_bt.rs @@ -73,6 +73,8 @@ fn sbt_calibrate_ou_params(py: Python<'_>, prices: Vec) -> PyResult, historical_prices: Vec, @@ -221,6 +225,8 @@ fn sbt_run_synthetic_otr_workflow( } #[pyfunction(name = "search_optimal_trading_rule")] +// Python keyword signature. +#[allow(clippy::too_many_arguments)] fn sbt_search_optimal_trading_rule( py: Python<'_>, phi: f64, diff --git a/docs-site/src/content/docs/setup/local-build.md b/docs-site/src/content/docs/setup/local-build.md index 2e37999..e69732e 100644 --- a/docs-site/src/content/docs/setup/local-build.md +++ b/docs-site/src/content/docs/setup/local-build.md @@ -2,7 +2,7 @@ title: Local Build Setup description: Build the Rust core, run its tests, and run the documentation quality gates. status: reviewed -last_validated: '2026-08-30' +last_validated: '2026-09-18' audience: - quant-dev - platform-engineering @@ -35,7 +35,7 @@ Lint and format, mirroring the `lint` recipe in the `justfile`: ```bash cargo fmt -- --check -cargo clippy --all-targets --all-features -- -D clippy::correctness -D clippy::suspicious +cargo clippy --workspace --all-targets --all-features -- -D warnings ``` ## Documentation quality gates diff --git a/docs/publishing.md b/docs/publishing.md index 7dbc6c2..c71a62c 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -7,7 +7,7 @@ ## Local release checklist 1. `cargo fmt -- --check` -2. `cargo clippy --all-targets --all-features -- -D warnings` +2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` 3. `cargo test --workspace --lib --tests --all-features -- --skip test_sadf_test` 4. `cargo test -p openquant --test structural_breaks test_sadf_test -- --ignored` 5. `cargo bench -p openquant --bench perf_hotspots --bench synthetic_ticker_pipeline -- --sample-size 10 --warm-up-time 1 --measurement-time 1` diff --git a/justfile b/justfile index 20fdd60..da1706d 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,7 @@ fmt-check: cargo fmt -- --check clippy: - cargo clippy --all-targets --all-features -- -D clippy::correctness -D clippy::suspicious + cargo clippy --workspace --all-targets --all-features -- -D warnings check: cargo check --all-targets --all-features diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d9adc1e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +# Pinned so `clippy -- -D warnings` means the same thing locally and in CI. +# A floating `stable` adds lints every six weeks and would fail unrelated PRs. +# Bump deliberately, fixing any new lints in the same change. +[toolchain] +channel = "1.98.1" +components = ["clippy", "rustfmt"]