first commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "wickra"
|
||||
description = "Streaming-first technical analysis library: incremental indicators, drop-in TA-Lib replacement, multi-language."
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
documentation = "https://docs.wickra.org"
|
||||
|
||||
# Render the docs on docs.rs with every feature enabled.
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
wickra-core = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["parallel"]
|
||||
parallel = ["wickra-core/parallel"]
|
||||
|
||||
[dev-dependencies]
|
||||
approx = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
wickra-data = { path = "../wickra-data" }
|
||||
|
||||
[[bench]]
|
||||
name = "indicators"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "data_layer"
|
||||
harness = false
|
||||
@@ -0,0 +1,119 @@
|
||||
//! Throughput microbenchmarks for the native data layer: CSV parsing, tick
|
||||
//! aggregation, and resampling.
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```text
|
||||
//! cargo bench -p wickra --bench data_layer
|
||||
//! ```
|
||||
//!
|
||||
//! These exercise `wickra-data` — the same native code every binding rides
|
||||
//! through the FFI boundary characterised in `BENCHMARKS.md` §3. It is what
|
||||
//! replaces `pandas` / `csv-parse` / manual tick bucketing / `pandas.resample`
|
||||
//! in the nine non-Rust languages, so the numbers here are the upper bound a
|
||||
//! binding can reach for "load a CSV, roll ticks into candles, resample a
|
||||
//! series" without pulling in a single third-party package.
|
||||
//!
|
||||
//! The dataset is the checked-in `examples/data/btcusdt-1m.csv` (50 000 real
|
||||
//! BTCUSDT one-minute candles). Regenerate it with
|
||||
//! `cargo run -p wickra-examples --bin fetch_btcusdt`.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
|
||||
use std::hint::black_box;
|
||||
use wickra::{Candle, Tick};
|
||||
use wickra_data::{
|
||||
aggregator::{TickAggregator, Timeframe},
|
||||
csv::CandleReader,
|
||||
resample::Resampler,
|
||||
};
|
||||
|
||||
const DATASET: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../examples/data/btcusdt-1m.csv"
|
||||
);
|
||||
const ONE_MINUTE_MS: i64 = 60_000;
|
||||
|
||||
fn dataset_bytes() -> Vec<u8> {
|
||||
std::fs::read(DATASET).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"could not read the benchmark dataset {DATASET}: {e}\n\
|
||||
generate it with `cargo run -p wickra-examples --bin fetch_btcusdt`"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_candles() -> Vec<Candle> {
|
||||
CandleReader::from_reader(dataset_bytes().as_slice())
|
||||
.unwrap()
|
||||
.read_all()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// CSV bytes -> `Vec<Candle>`. Throughput is candles (rows) parsed per second.
|
||||
fn bench_csv_read(c: &mut Criterion) {
|
||||
let bytes = dataset_bytes();
|
||||
let rows = load_candles().len() as u64;
|
||||
let mut group = c.benchmark_group("data_layer/csv_read");
|
||||
group.throughput(Throughput::Elements(rows));
|
||||
group.bench_function("btcusdt_1m", |b| {
|
||||
b.iter(|| {
|
||||
let candles = CandleReader::from_reader(black_box(bytes.as_slice()))
|
||||
.unwrap()
|
||||
.read_all()
|
||||
.unwrap();
|
||||
black_box(candles.len())
|
||||
});
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Ticks -> one-minute candles. Throughput is ticks aggregated per second.
|
||||
fn bench_tick_aggregate(c: &mut Criterion) {
|
||||
let ticks: Vec<Tick> = load_candles()
|
||||
.iter()
|
||||
.map(|candle| Tick::new(candle.close, candle.volume, candle.timestamp).unwrap())
|
||||
.collect();
|
||||
let mut group = c.benchmark_group("data_layer/tick_aggregate");
|
||||
group.throughput(Throughput::Elements(ticks.len() as u64));
|
||||
group.bench_function("1m_buckets", |b| {
|
||||
b.iter(|| {
|
||||
let mut agg = TickAggregator::new(Timeframe::millis(ONE_MINUTE_MS).unwrap());
|
||||
let mut emitted = 0usize;
|
||||
for tick in &ticks {
|
||||
emitted += agg.push(black_box(*tick)).unwrap().len();
|
||||
}
|
||||
black_box(emitted)
|
||||
});
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// One-minute candles -> five-minute candles. Throughput is input candles per second.
|
||||
fn bench_resample(c: &mut Criterion) {
|
||||
let candles = load_candles();
|
||||
let mut group = c.benchmark_group("data_layer/resample");
|
||||
group.throughput(Throughput::Elements(candles.len() as u64));
|
||||
group.bench_function("1m_to_5m", |b| {
|
||||
b.iter(|| {
|
||||
let mut resampler = Resampler::new(Timeframe::millis(5 * ONE_MINUTE_MS).unwrap());
|
||||
let mut emitted = 0usize;
|
||||
for candle in &candles {
|
||||
if resampler.push(black_box(*candle)).unwrap().is_some() {
|
||||
emitted += 1;
|
||||
}
|
||||
}
|
||||
if resampler.flush().unwrap().is_some() {
|
||||
emitted += 1;
|
||||
}
|
||||
black_box(emitted)
|
||||
});
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_csv_read,
|
||||
bench_tick_aggregate,
|
||||
bench_resample
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,455 @@
|
||||
//! Microbenchmarks for a curated subset of the indicator catalogue.
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```text
|
||||
//! cargo bench -p wickra
|
||||
//! ```
|
||||
//!
|
||||
//! Each benchmark feeds real BTCUSDT 1-minute candles — read from the
|
||||
//! checked-in dataset at the workspace `examples/data/btcusdt-1m.csv` —
|
||||
//! through both the streaming (`update` loop) and batch APIs of an
|
||||
//! indicator. Sizes cover small (1 000), medium (10 000), and large
|
||||
//! (50 000) workloads, taken as prefixes of that dataset.
|
||||
//!
|
||||
//! ## Why curated rather than exhaustive
|
||||
//!
|
||||
//! The indicator catalogue has 214 entries; benching every single one
|
||||
//! at three sizes inflates `cargo bench` to >10 minutes for diminishing
|
||||
//! signal. The selection below picks the cheapest baseline and the
|
||||
//! most-expensive representative in each family — a regression in any
|
||||
//! of those is the meaningful signal; per-family redundancy benches
|
||||
//! mostly produce noise.
|
||||
//!
|
||||
//! If you need a benchmark for a specific indicator that is not in this
|
||||
//! list, add it locally and run `cargo bench -- <name>` to target just
|
||||
//! that bench.
|
||||
//!
|
||||
//! Regenerate the dataset with:
|
||||
//! ```text
|
||||
//! cargo run -p wickra-examples --bin fetch_btcusdt
|
||||
//! ```
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use std::hint::black_box;
|
||||
use wickra::{
|
||||
Adx, AnchoredRsi, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio,
|
||||
Candle, Cci, ClassicPivots, ConnorsRsi, DepthSlope, DerivativesTick, EffectiveSpread, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, Frama, FundingRate, FundingRateZScore,
|
||||
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, KylesLambda,
|
||||
Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice,
|
||||
Obv, OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1, ParkinsonVolatility, Ppo, Psar,
|
||||
RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc, SuperTrend, SuperTrendOutput,
|
||||
TdSequential, TdSequentialOutput, TpoProfile, TpoProfileOutput, Trade, TradeImbalance,
|
||||
TradeQuote, TtmSqueeze, TtmSqueezeOutput, ValueArea, ValueAreaOutput, ValueAtRisk,
|
||||
VolumeProfile, VolumeProfileOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, WaveTrend,
|
||||
YangZhangVolatility, T3,
|
||||
};
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
/// Workload sizes, in candles. Each is taken as a prefix of the dataset.
|
||||
const SIZES: &[usize] = &[1_000, 10_000, 50_000];
|
||||
|
||||
/// Load the checked-in BTCUSDT 1-minute candle dataset from the workspace
|
||||
/// `examples/data/` directory.
|
||||
fn load_candles() -> Vec<Candle> {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../examples/data/btcusdt-1m.csv"
|
||||
);
|
||||
let mut reader = CandleReader::open(path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"could not open the benchmark dataset {path}: {e}\n\
|
||||
generate it with `cargo run -p wickra-examples --bin fetch_btcusdt`"
|
||||
)
|
||||
});
|
||||
reader
|
||||
.read_all()
|
||||
.expect("the benchmark dataset is valid OHLCV")
|
||||
}
|
||||
|
||||
fn bench_scalar<I, F>(c: &mut Criterion, name: &str, prices: &[f64], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = f64, Output = f64> + BatchExt,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(prices.len());
|
||||
let series = &prices[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, prices| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for p in prices {
|
||||
black_box(ind.update(*p));
|
||||
}
|
||||
});
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("batch", n), series, |b, prices| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
black_box(ind.batch(prices));
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_candle_input<I, F, O>(c: &mut Criterion, name: &str, candles: &[Candle], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = Candle, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(candles.len());
|
||||
let series = &candles[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, candles| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for c in candles {
|
||||
black_box(ind.update(*c));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_orderbook_input<I, F, O>(c: &mut Criterion, name: &str, books: &[OrderBook], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = OrderBook, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(books.len());
|
||||
let series = &books[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, books| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for book in books {
|
||||
black_box(ind.update(book.clone()));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_trade_input<I, F, O>(c: &mut Criterion, name: &str, trades: &[Trade], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = Trade, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(trades.len());
|
||||
let series = &trades[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, trades| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for t in trades {
|
||||
black_box(ind.update(*t));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_tradequote_input<I, F, O>(c: &mut Criterion, name: &str, quotes: &[TradeQuote], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = TradeQuote, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(quotes.len());
|
||||
let series = "es[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, quotes| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for q in quotes {
|
||||
black_box(ind.update(*q));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_derivatives_input<I, F, O>(
|
||||
c: &mut Criterion,
|
||||
name: &str,
|
||||
ticks: &[DerivativesTick],
|
||||
make: F,
|
||||
) where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = DerivativesTick, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(ticks.len());
|
||||
let series = &ticks[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, ticks| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for tick in ticks {
|
||||
black_box(ind.update(*tick));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_scalar_multi<I, F, O>(c: &mut Criterion, name: &str, prices: &[f64], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = f64, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(prices.len());
|
||||
let series = &prices[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, prices| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for p in prices {
|
||||
black_box(ind.update(*p));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn benches(c: &mut Criterion) {
|
||||
let candles = load_candles();
|
||||
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
|
||||
|
||||
// === Family 01 — Moving Averages ===
|
||||
// Sma: cheapest baseline; Ema: recursive baseline; Frama / Jma / T3: adaptive / expensive.
|
||||
bench_scalar(c, "sma", &closes, || Sma::new(14).unwrap());
|
||||
bench_scalar(c, "ema", &closes, || Ema::new(14).unwrap());
|
||||
bench_scalar(c, "frama", &closes, || Frama::new(16).unwrap());
|
||||
bench_scalar(c, "jma", &closes, || Jma::new(14, 0.0, 2).unwrap());
|
||||
bench_scalar(c, "t3", &closes, || T3::new(14, 0.7).unwrap());
|
||||
|
||||
// === Family 02 — Momentum Oscillators ===
|
||||
// Rsi: textbook baseline; ConnorsRsi: three-component composite.
|
||||
bench_scalar(c, "rsi", &closes, || Rsi::new(14).unwrap());
|
||||
bench_scalar(c, "anchored_rsi", &closes, AnchoredRsi::new);
|
||||
bench_candle_input(c, "cci", &candles, || Cci::new(20).unwrap());
|
||||
bench_scalar(c, "connors_rsi", &closes, ConnorsRsi::classic);
|
||||
|
||||
// === Family 03 — Trend & Directional ===
|
||||
// Adx is multi-component (DI+/DI-/ADX); WaveTrend is the heaviest in this group.
|
||||
bench_candle_input(c, "adx", &candles, || Adx::new(14).unwrap());
|
||||
bench_candle_input(c, "wave_trend", &candles, || WaveTrend::classic().unwrap());
|
||||
|
||||
// === Family 04 — Price Oscillators ===
|
||||
// Macd: multi-output baseline; Stc: deeply recursive (most expensive in family).
|
||||
bench_scalar_multi::<_, _, MacdOutput>(c, "macd", &closes, MacdIndicator::classic);
|
||||
bench_scalar(c, "ppo", &closes, || Ppo::new(12, 26).unwrap());
|
||||
bench_scalar(c, "stc", &closes, Stc::classic);
|
||||
|
||||
// === Family 05 — Volatility & Bands ===
|
||||
// Atr: cheap baseline; Bollinger: stddev-heavy; YangZhang: most-expensive volatility metric.
|
||||
bench_candle_input(c, "atr", &candles, || Atr::new(14).unwrap());
|
||||
bench_scalar_multi::<_, _, BollingerOutput>(c, "bollinger", &closes, || {
|
||||
BollingerBands::new(20, 2.0).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "parkinson", &candles, || {
|
||||
ParkinsonVolatility::new(20, 252).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "yang_zhang", &candles, || {
|
||||
YangZhangVolatility::new(20, 252).unwrap()
|
||||
});
|
||||
|
||||
// === Family 06 — Bands & Channels ===
|
||||
// TtmSqueeze: multi-indicator composite; VwapStdDevBands: volume-weighted.
|
||||
bench_candle_input::<_, _, TtmSqueezeOutput>(c, "ttm_squeeze", &candles, || {
|
||||
TtmSqueeze::new(20, 2.0, 1.5).unwrap()
|
||||
});
|
||||
bench_candle_input::<_, _, VwapStdDevBandsOutput>(c, "vwap_stddev_bands", &candles, || {
|
||||
VwapStdDevBands::new(2.0).unwrap()
|
||||
});
|
||||
|
||||
// === Family 07 — Trailing Stops ===
|
||||
// Psar: textbook trailing stop; SuperTrend: ATR-anchored band.
|
||||
bench_candle_input(c, "psar", &candles, || Psar::new(0.02, 0.02, 0.2).unwrap());
|
||||
bench_candle_input::<_, _, SuperTrendOutput>(c, "super_trend", &candles, || {
|
||||
SuperTrend::new(10, 3.0).unwrap()
|
||||
});
|
||||
|
||||
// === Family 08 — Volume ===
|
||||
// Obv: simplest volume cumul; Vwap: session cumul; RollingVwap: rolling window.
|
||||
bench_candle_input(c, "obv", &candles, Obv::new);
|
||||
bench_candle_input(c, "vwap", &candles, Vwap::new);
|
||||
bench_candle_input(c, "rolling_vwap", &candles, || {
|
||||
RollingVwap::new(20).unwrap()
|
||||
});
|
||||
|
||||
// === Family 09 — Price Statistics ===
|
||||
// LinearRegression: OLS baseline; HurstExponent: R/S analysis (most expensive in family);
|
||||
// Autocorrelation: lag-correlation.
|
||||
bench_scalar(c, "linear_regression", &closes, || {
|
||||
LinearRegression::new(14).unwrap()
|
||||
});
|
||||
bench_scalar(c, "hurst_exponent", &closes, || {
|
||||
HurstExponent::new(100, 4).unwrap()
|
||||
});
|
||||
bench_scalar(c, "autocorrelation", &closes, || {
|
||||
Autocorrelation::new(20, 1).unwrap()
|
||||
});
|
||||
|
||||
// === Family 10 — Ehlers / Cycle (DSP) ===
|
||||
// Mama: paired adaptive MA (multi-output); HilbertDominantCycle: cycle estimation;
|
||||
// EmpiricalModeDecomposition: heaviest DSP indicator in the catalogue.
|
||||
bench_scalar_multi::<_, _, MamaOutput>(c, "mama", &closes, Mama::classic);
|
||||
bench_scalar(
|
||||
c,
|
||||
"hilbert_dominant_cycle",
|
||||
&closes,
|
||||
HilbertDominantCycle::new,
|
||||
);
|
||||
bench_scalar(c, "empirical_mode_decomposition", &closes, || {
|
||||
EmpiricalModeDecomposition::new(20, 0.5).unwrap()
|
||||
});
|
||||
|
||||
// === Family 11 — Pivots & Support/Resistance ===
|
||||
bench_candle_input(c, "classic_pivots", &candles, ClassicPivots::new);
|
||||
|
||||
// === Family 12 — DeMark ===
|
||||
// TdSequential is the most complex in the family (state machine + countdown).
|
||||
bench_candle_input::<_, _, TdSequentialOutput>(
|
||||
c,
|
||||
"td_sequential",
|
||||
&candles,
|
||||
TdSequential::classic,
|
||||
);
|
||||
|
||||
// === Family 13 — Ichimoku & Charts ===
|
||||
bench_candle_input::<_, _, IchimokuOutput>(c, "ichimoku", &candles, Ichimoku::classic);
|
||||
|
||||
// === Family 14 — Candlestick Patterns ===
|
||||
// Engulfing is two-bar so representative across the candlestick family.
|
||||
bench_candle_input(c, "engulfing", &candles, Engulfing::new);
|
||||
|
||||
// === Family 15 — Market Profile ===
|
||||
bench_candle_input::<_, _, ValueAreaOutput>(c, "value_area", &candles, || {
|
||||
ValueArea::new(20, 50, 0.70).unwrap()
|
||||
});
|
||||
bench_candle_input::<_, _, VolumeProfileOutput>(c, "volume_profile", &candles, || {
|
||||
VolumeProfile::new(20, 50).unwrap()
|
||||
});
|
||||
bench_candle_input::<_, _, TpoProfileOutput>(c, "tpo_profile", &candles, || {
|
||||
TpoProfile::new(20, 50).unwrap()
|
||||
});
|
||||
|
||||
// === Family 16 — Risk / Performance Metrics ===
|
||||
// Close-prices stand in for the equity curve / return stream; absolute
|
||||
// numbers aren't meaningful here — what matters is the per-update cost.
|
||||
bench_scalar(c, "sharpe_ratio", &closes, || {
|
||||
SharpeRatio::new(20, 0.0).unwrap()
|
||||
});
|
||||
bench_scalar(c, "max_drawdown", &closes, || MaxDrawdown::new(20).unwrap());
|
||||
bench_scalar(c, "calmar_ratio", &closes, || CalmarRatio::new(20).unwrap());
|
||||
bench_scalar(c, "value_at_risk", &closes, || {
|
||||
ValueAtRisk::new(50, 0.95).unwrap()
|
||||
});
|
||||
|
||||
// === Family — Microstructure ===
|
||||
// No order-book dataset ships with the repo, so synthesise a five-level
|
||||
// book around each candle close. Benches the cheapest (top-of-book) and the
|
||||
// most-expensive (full-depth sum) representatives of the family.
|
||||
let books: Vec<OrderBook> = candles
|
||||
.iter()
|
||||
.map(|candle| {
|
||||
let mid = candle.close;
|
||||
let tick = (mid * 0.0001).max(0.01);
|
||||
let bids = (0..5u32)
|
||||
.map(|i| Level::new_unchecked(mid - tick * f64::from(i + 1), 1.0 + f64::from(i)))
|
||||
.collect();
|
||||
let asks = (0..5u32)
|
||||
.map(|i| Level::new_unchecked(mid + tick * f64::from(i + 1), 1.0 + f64::from(i)))
|
||||
.collect();
|
||||
OrderBook::new_unchecked(bids, asks)
|
||||
})
|
||||
.collect();
|
||||
bench_orderbook_input(c, "ob_imbalance_top1", &books, OrderBookImbalanceTop1::new);
|
||||
bench_orderbook_input(c, "ob_imbalance_full", &books, OrderBookImbalanceFull::new);
|
||||
bench_orderbook_input(c, "microprice", &books, Microprice::new);
|
||||
bench_orderbook_input(c, "depth_slope", &books, DepthSlope::new);
|
||||
|
||||
// Synthesise a trade tape from candles: one trade per bar, sided by the
|
||||
// candle's direction. SignedVolume is the cheapest; TradeImbalance carries
|
||||
// a rolling window and is the most expensive.
|
||||
let trades: Vec<Trade> = candles
|
||||
.iter()
|
||||
.map(|candle| {
|
||||
let side = if candle.close >= candle.open {
|
||||
Side::Buy
|
||||
} else {
|
||||
Side::Sell
|
||||
};
|
||||
Trade::new_unchecked(candle.close, candle.volume, side, candle.timestamp)
|
||||
})
|
||||
.collect();
|
||||
bench_trade_input(c, "signed_volume", &trades, SignedVolume::new);
|
||||
bench_trade_input(c, "trade_imbalance", &trades, || {
|
||||
TradeImbalance::new(50).unwrap()
|
||||
});
|
||||
|
||||
// Pair each synthetic trade with the candle close as the prevailing mid to
|
||||
// exercise the price-impact family. EffectiveSpread is the stateless
|
||||
// representative.
|
||||
let quotes: Vec<TradeQuote> = trades
|
||||
.iter()
|
||||
.map(|trade| TradeQuote::new_unchecked(*trade, trade.price))
|
||||
.collect();
|
||||
bench_tradequote_input(c, "effective_spread", "es, EffectiveSpread::new);
|
||||
bench_tradequote_input(c, "kyles_lambda", "es, || KylesLambda::new(50).unwrap());
|
||||
|
||||
// === Family — Derivatives ===
|
||||
// No derivatives feed ships with the repo, so synthesise a tick per candle:
|
||||
// the close drives the mark price, funding tracks the candle's body, and
|
||||
// open interest follows volume. FundingRate is the cheapest (passthrough);
|
||||
// FundingRateZScore carries a rolling window and is the most expensive.
|
||||
let ticks: Vec<DerivativesTick> = candles
|
||||
.iter()
|
||||
.map(|candle| {
|
||||
let funding = (candle.close - candle.open) / candle.open * 0.01;
|
||||
DerivativesTick::new_unchecked(
|
||||
funding,
|
||||
candle.close,
|
||||
candle.close * 0.999,
|
||||
candle.close * 1.001,
|
||||
candle.volume * 100.0,
|
||||
candle.volume * 0.6,
|
||||
candle.volume * 0.4,
|
||||
candle.volume * 0.5,
|
||||
candle.volume * 0.5,
|
||||
0.0,
|
||||
0.0,
|
||||
candle.timestamp,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
bench_derivatives_input(c, "funding_rate", &ticks, FundingRate::new);
|
||||
bench_derivatives_input(c, "funding_rate_zscore", &ticks, || {
|
||||
FundingRateZScore::new(50).unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(name = wickra_benches; config = Criterion::default(); targets = benches);
|
||||
criterion_main!(wickra_benches);
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Wickra: streaming-first technical analysis.
|
||||
//!
|
||||
//! This crate is a thin re-export of [`wickra_core`] so downstream users can depend on
|
||||
//! a single `wickra` package without thinking about the internal split. Every public
|
||||
//! item lives in `wickra_core`; only the names re-exported here are part of the stable
|
||||
//! public API.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use wickra::{Indicator, Sma};
|
||||
//!
|
||||
//! let mut sma = Sma::new(3).unwrap();
|
||||
//! let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
//! let out: Vec<Option<f64>> = prices.iter().map(|p| sma.update(*p)).collect();
|
||||
//! assert_eq!(out, vec![None, None, Some(2.0), Some(3.0), Some(4.0)]);
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use wickra_core::*;
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Integration test: the checked-in BTCUSDT example datasets parse cleanly,
|
||||
//! hold enough rows, and carry strictly increasing — and, for the fixed
|
||||
//! timeframes, evenly spaced — timestamps.
|
||||
//!
|
||||
//! The datasets live in the workspace `examples/data/` directory and are
|
||||
//! produced by the `fetch_btcusdt` example. Regenerate them with:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p wickra-examples --bin fetch_btcusdt
|
||||
//! ```
|
||||
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
/// `(file name, minimum row count, expected step in ms)`. The step is `None`
|
||||
/// for the monthly file, whose buckets are 28–31 days and thus uneven.
|
||||
const DATASETS: &[(&str, usize, Option<i64>)] = &[
|
||||
("btcusdt-1m.csv", 50_000, Some(60_000)),
|
||||
("btcusdt-5m.csv", 10_000, Some(300_000)),
|
||||
("btcusdt-15m.csv", 10_000, Some(900_000)),
|
||||
("btcusdt-1h.csv", 10_000, Some(3_600_000)),
|
||||
("btcusdt-12h.csv", 5_000, Some(43_200_000)),
|
||||
// 1d and 1month collect all the history Binance offers, which grows over
|
||||
// time — assert a lower bound rather than an exact count.
|
||||
("btcusdt-1d.csv", 3_000, Some(86_400_000)),
|
||||
("btcusdt-1month.csv", 100, None),
|
||||
];
|
||||
|
||||
fn dataset_path(file: &str) -> String {
|
||||
format!("{}/../../examples/data/{file}", env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_dataset_parses_and_is_well_formed() {
|
||||
for &(file, min_rows, step) in DATASETS {
|
||||
let path = dataset_path(file);
|
||||
let mut reader =
|
||||
CandleReader::open(&path).unwrap_or_else(|e| panic!("{file}: cannot open {path}: {e}"));
|
||||
// `read_all` validates every row through `Candle::new`, so a successful
|
||||
// read already proves each OHLC tuple is finite and internally
|
||||
// consistent (high >= low, etc.).
|
||||
let candles = reader
|
||||
.read_all()
|
||||
.unwrap_or_else(|e| panic!("{file}: invalid OHLCV row: {e}"));
|
||||
|
||||
assert!(
|
||||
candles.len() >= min_rows,
|
||||
"{file}: expected at least {min_rows} rows, got {}",
|
||||
candles.len()
|
||||
);
|
||||
|
||||
for pair in candles.windows(2) {
|
||||
let (prev, next) = (pair[0], pair[1]);
|
||||
assert!(
|
||||
next.timestamp > prev.timestamp,
|
||||
"{file}: timestamps must strictly increase, saw {} then {}",
|
||||
prev.timestamp,
|
||||
next.timestamp
|
||||
);
|
||||
if let Some(step) = step {
|
||||
assert_eq!(
|
||||
next.timestamp - prev.timestamp,
|
||||
step,
|
||||
"{file}: a fixed timeframe must be evenly spaced by {step} ms"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user