Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 3 additions & 4 deletions crates/openquant/benches/synthetic_ticker_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/openquant/examples/research_notebook_smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 4 additions & 1 deletion crates/openquant/src/backtesting_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,13 @@ where
Ok(out)
}

/// Per-fold performance plus the out-of-sample returns keyed by split id.
type SplitEvaluation = (Vec<FoldPerformance>, HashMap<usize, Vec<f64>>);

fn evaluate_splits_with_returns<E>(
splits: &[SplitDefinition],
evaluator: &mut E,
) -> Result<(Vec<FoldPerformance>, HashMap<usize, Vec<f64>>), String>
) -> Result<SplitEvaluation, String>
where
E: FnMut(&SplitDefinition) -> Result<Vec<f64>, String>,
{
Expand Down
31 changes: 12 additions & 19 deletions crates/openquant/src/bet_sizing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64, BetSizingError> {
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 {
Expand Down Expand Up @@ -112,26 +112,14 @@ pub fn discrete_signal(signal0: &[f64], step_size: f64) -> Vec<f64> {
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<NaiveDateTime> = t1.iter().copied().collect();
let mut t_points: Vec<NaiveDateTime> = t1.to_vec();
t_points.extend(signal.iter().map(|(ts, _)| *ts));
t_points.sort();
t_points.dedup();
Expand Down Expand Up @@ -172,7 +160,7 @@ pub fn bet_size_probability(
let side: Vec<f64> = 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<NaiveDateTime> = events.iter().map(|(_, t1, _, _)| *t1).collect();
signals = avg_active_signals(&signals, &t1);
Expand Down Expand Up @@ -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<f64, BetSizingError> {
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();
Expand Down Expand Up @@ -536,20 +524,25 @@ 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],
fit_runs: usize,
epsilon: f64,
max_iter: usize,
return_parameters: bool,
) -> (Vec<(NaiveDateTime, f64, f64, f64, f64)>, Option<[f64; 5]>) {
) -> (Vec<ReserveBetSizeRow>, Option<MixtureParams>) {
let concurrent = get_concurrent_sides(t1, side);
let c_t: Vec<f64> = 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)
Expand Down
8 changes: 4 additions & 4 deletions crates/openquant/src/cla.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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);
Expand Down
31 changes: 18 additions & 13 deletions crates/openquant/src/codependence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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();
}
}
Expand Down
19 changes: 10 additions & 9 deletions crates/openquant/src/cross_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ pub fn ml_get_train_times(
out
}

/// One cross-validation split: `(train_indices, test_indices)`.
pub type TrainTestSplit = (Vec<usize>, Vec<usize>);

pub struct PurgedKFold {
n_splits: usize,
samples_info_sets: Vec<(NaiveDateTime, NaiveDateTime)>,
Expand All @@ -132,14 +135,14 @@ impl PurgedKFold {
Ok(Self { n_splits, samples_info_sets, pct_embargo })
}

pub fn split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>, String> {
pub fn split(&self, n_samples: usize) -> Result<Vec<TrainTestSplit>, 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();
Expand Down Expand Up @@ -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;
}
}

Expand Down
36 changes: 23 additions & 13 deletions crates/openquant/src/etf_trick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,19 @@ pub struct EtfTrick {
source: Source,
}

#[derive(Clone, Debug)]
struct InMemoryTables {
open: Table,
close: Table,
alloc: Table,
costs: Table,
rates: Option<Table>,
}

#[derive(Clone, Debug)]
enum Source {
InMemory {
open: Table,
close: Table,
alloc: Table,
costs: Table,
rates: Option<Table>,
},
// Boxed so the enum is not sized by the five in-memory tables.
InMemory(Box<InMemoryTables>),
Csv {
open_path: String,
close_path: String,
Expand All @@ -102,7 +106,9 @@ impl EtfTrick {
rates: Option<Table>,
) -> Result<Self, String> {
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(
Expand All @@ -125,9 +131,13 @@ impl EtfTrick {

pub fn get_etf_series(&self, batch_size: usize) -> Result<Vec<(String, f64)>, 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());
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading