|
1 | 1 | use nalgebra::DMatrix; |
| 2 | +use openquant::data_structures::{standard_bars, time_bars, StandardBarType, Trade}; |
2 | 3 | use openquant::filters::Threshold; |
3 | 4 | use openquant::pipeline::{ |
4 | 5 | run_mid_frequency_pipeline, ResearchPipelineConfig, ResearchPipelineInput, |
@@ -50,6 +51,61 @@ fn format_naive_datetimes(values: Vec<chrono::NaiveDateTime>) -> Vec<String> { |
50 | 51 | values.into_iter().map(|v| v.format("%Y-%m-%d %H:%M:%S").to_string()).collect() |
51 | 52 | } |
52 | 53 |
|
| 54 | +fn parse_one_naive_datetime(value: &str) -> PyResult<chrono::NaiveDateTime> { |
| 55 | + chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") |
| 56 | + .or_else(|_| { |
| 57 | + chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d") |
| 58 | + .map(|d| d.and_hms_opt(0, 0, 0).expect("valid fixed midnight")) |
| 59 | + }) |
| 60 | + .map_err(|e| { |
| 61 | + PyValueError::new_err(format!( |
| 62 | + "invalid datetime '{value}' (expected '%Y-%m-%d %H:%M:%S' or '%Y-%m-%d'): {e}" |
| 63 | + )) |
| 64 | + }) |
| 65 | +} |
| 66 | + |
| 67 | +fn build_trades( |
| 68 | + timestamps: Vec<String>, |
| 69 | + prices: Vec<f64>, |
| 70 | + volumes: Vec<f64>, |
| 71 | +) -> PyResult<Vec<Trade>> { |
| 72 | + if timestamps.len() != prices.len() || prices.len() != volumes.len() { |
| 73 | + return Err(PyValueError::new_err(format!( |
| 74 | + "timestamps/prices/volumes length mismatch: {} / {} / {}", |
| 75 | + timestamps.len(), |
| 76 | + prices.len(), |
| 77 | + volumes.len() |
| 78 | + ))); |
| 79 | + } |
| 80 | + let mut trades = Vec::with_capacity(prices.len()); |
| 81 | + for i in 0..prices.len() { |
| 82 | + trades.push(Trade { |
| 83 | + timestamp: parse_one_naive_datetime(×tamps[i])?, |
| 84 | + price: prices[i], |
| 85 | + volume: volumes[i], |
| 86 | + }); |
| 87 | + } |
| 88 | + Ok(trades) |
| 89 | +} |
| 90 | + |
| 91 | +fn bars_to_rows(bars: Vec<openquant::data_structures::StandardBar>) -> Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)> { |
| 92 | + bars.into_iter() |
| 93 | + .map(|b| { |
| 94 | + ( |
| 95 | + b.start_timestamp.format("%Y-%m-%d %H:%M:%S").to_string(), |
| 96 | + b.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(), |
| 97 | + b.open, |
| 98 | + b.high, |
| 99 | + b.low, |
| 100 | + b.close, |
| 101 | + b.volume, |
| 102 | + b.dollar_value, |
| 103 | + b.tick_count, |
| 104 | + ) |
| 105 | + }) |
| 106 | + .collect() |
| 107 | +} |
| 108 | + |
53 | 109 | #[pyfunction(name = "calculate_value_at_risk")] |
54 | 110 | fn risk_calculate_value_at_risk(returns: Vec<f64>, confidence_level: f64) -> PyResult<f64> { |
55 | 111 | RiskMetrics::default().calculate_value_at_risk(&returns, confidence_level).map_err(to_py_err) |
@@ -154,6 +210,66 @@ fn sampling_seq_bootstrap( |
154 | 210 | openquant::sampling::seq_bootstrap(&ind_mat, sample_length, warmup_samples) |
155 | 211 | } |
156 | 212 |
|
| 213 | +#[pyfunction(name = "build_time_bars")] |
| 214 | +fn bars_build_time_bars( |
| 215 | + timestamps: Vec<String>, |
| 216 | + prices: Vec<f64>, |
| 217 | + volumes: Vec<f64>, |
| 218 | + interval_seconds: i64, |
| 219 | +) -> PyResult<Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)>> { |
| 220 | + if interval_seconds <= 0 { |
| 221 | + return Err(PyValueError::new_err("interval_seconds must be > 0")); |
| 222 | + } |
| 223 | + let trades = build_trades(timestamps, prices, volumes)?; |
| 224 | + let bars = time_bars(&trades, chrono::Duration::seconds(interval_seconds)); |
| 225 | + Ok(bars_to_rows(bars)) |
| 226 | +} |
| 227 | + |
| 228 | +#[pyfunction(name = "build_tick_bars")] |
| 229 | +fn bars_build_tick_bars( |
| 230 | + timestamps: Vec<String>, |
| 231 | + prices: Vec<f64>, |
| 232 | + volumes: Vec<f64>, |
| 233 | + ticks_per_bar: usize, |
| 234 | +) -> PyResult<Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)>> { |
| 235 | + if ticks_per_bar == 0 { |
| 236 | + return Err(PyValueError::new_err("ticks_per_bar must be > 0")); |
| 237 | + } |
| 238 | + let trades = build_trades(timestamps, prices, volumes)?; |
| 239 | + let bars = standard_bars(&trades, ticks_per_bar as f64, StandardBarType::Tick); |
| 240 | + Ok(bars_to_rows(bars)) |
| 241 | +} |
| 242 | + |
| 243 | +#[pyfunction(name = "build_volume_bars")] |
| 244 | +fn bars_build_volume_bars( |
| 245 | + timestamps: Vec<String>, |
| 246 | + prices: Vec<f64>, |
| 247 | + volumes: Vec<f64>, |
| 248 | + volume_per_bar: f64, |
| 249 | +) -> PyResult<Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)>> { |
| 250 | + if !volume_per_bar.is_finite() || volume_per_bar <= 0.0 { |
| 251 | + return Err(PyValueError::new_err("volume_per_bar must be > 0")); |
| 252 | + } |
| 253 | + let trades = build_trades(timestamps, prices, volumes)?; |
| 254 | + let bars = standard_bars(&trades, volume_per_bar, StandardBarType::Volume); |
| 255 | + Ok(bars_to_rows(bars)) |
| 256 | +} |
| 257 | + |
| 258 | +#[pyfunction(name = "build_dollar_bars")] |
| 259 | +fn bars_build_dollar_bars( |
| 260 | + timestamps: Vec<String>, |
| 261 | + prices: Vec<f64>, |
| 262 | + volumes: Vec<f64>, |
| 263 | + dollar_value_per_bar: f64, |
| 264 | +) -> PyResult<Vec<(String, String, f64, f64, f64, f64, f64, f64, usize)>> { |
| 265 | + if !dollar_value_per_bar.is_finite() || dollar_value_per_bar <= 0.0 { |
| 266 | + return Err(PyValueError::new_err("dollar_value_per_bar must be > 0")); |
| 267 | + } |
| 268 | + let trades = build_trades(timestamps, prices, volumes)?; |
| 269 | + let bars = standard_bars(&trades, dollar_value_per_bar, StandardBarType::Dollar); |
| 270 | + Ok(bars_to_rows(bars)) |
| 271 | +} |
| 272 | + |
157 | 273 | #[pyfunction(name = "get_signal")] |
158 | 274 | fn bet_sizing_get_signal(prob: Vec<f64>, num_classes: usize, pred: Option<Vec<f64>>) -> Vec<f64> { |
159 | 275 | openquant::bet_sizing::get_signal(&prob, num_classes, pred.as_deref()) |
@@ -325,6 +441,14 @@ fn _core(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { |
325 | 441 | m.add_submodule(&sampling)?; |
326 | 442 | m.add("sampling", sampling)?; |
327 | 443 |
|
| 444 | + let bars = PyModule::new_bound(py, "bars")?; |
| 445 | + bars.add_function(wrap_pyfunction!(bars_build_time_bars, &bars)?)?; |
| 446 | + bars.add_function(wrap_pyfunction!(bars_build_tick_bars, &bars)?)?; |
| 447 | + bars.add_function(wrap_pyfunction!(bars_build_volume_bars, &bars)?)?; |
| 448 | + bars.add_function(wrap_pyfunction!(bars_build_dollar_bars, &bars)?)?; |
| 449 | + m.add_submodule(&bars)?; |
| 450 | + m.add("bars", bars)?; |
| 451 | + |
328 | 452 | let bet_sizing = PyModule::new_bound(py, "bet_sizing")?; |
329 | 453 | bet_sizing.add_function(wrap_pyfunction!(bet_sizing_get_signal, &bet_sizing)?)?; |
330 | 454 | bet_sizing.add_function(wrap_pyfunction!(bet_sizing_discrete_signal, &bet_sizing)?)?; |
|
0 commit comments