From 514c235f1c99b7d80f7e28bfb99b1110dde80550 Mon Sep 17 00:00:00 2001 From: porcelaincode Date: Wed, 3 Jun 2026 21:26:51 +0530 Subject: [PATCH] feat(core): add TickData struct, TimeExit reason, compute_backtest_metrics pub fn - TickData: parallel tick arrays (timestamps, ltp, bid, ask, buy_qty_delta, sell_qty_delta, oi) with len/is_empty helpers; callers must pre-convert Zerodha cumulative totals to per-tick deltas before passing - ExitReason::TimeExit: max hold time exceeded variant for tick backtest - compute_backtest_metrics: pub free fn wrapping PortfolioEngine::calculate_metrics so non-OHLCV strategies can produce identical metrics without duplication Co-Authored-By: Claude Sonnet 4.6 --- src/core/types.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/portfolio/engine.rs | 19 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/core/types.rs b/src/core/types.rs index dab37b6..4fa1b49 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -104,6 +104,44 @@ impl OhlcvData { } } +/// Raw tick data series for tick-level backtesting. +/// +/// All fields are parallel arrays of length N (one entry per tick). +/// `buy_qty_delta` and `sell_qty_delta` must be per-tick deltas, not +/// cumulative session totals — callers are responsible for converting +/// Zerodha-style running sums before passing them here. +#[derive(Debug, Clone)] +pub struct TickData { + /// Nanoseconds-since-epoch timestamp for each tick. + pub timestamps: Vec, + /// Last traded price at each tick. + pub ltp: Vec, + /// Best bid price at each tick (0.0 if unavailable). + pub bid: Vec, + /// Best ask price at each tick (0.0 if unavailable). + pub ask: Vec, + /// Per-tick buy quantity delta (not cumulative). + pub buy_qty_delta: Vec, + /// Per-tick sell quantity delta (not cumulative). + pub sell_qty_delta: Vec, + /// Open interest at each tick (0 if unavailable). + pub oi: Vec, +} + +impl TickData { + /// Number of ticks. + #[inline] + pub fn len(&self) -> usize { + self.ltp.len() + } + + /// Whether the series is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.ltp.is_empty() + } +} + /// Compiled trading signals from strategy. #[derive(Debug, Clone)] pub struct CompiledSignals { @@ -214,6 +252,8 @@ pub enum ExitReason { EndOfData, /// Option expiry settlement. Settlement, + /// Max hold time exceeded (tick backtest). + TimeExit, } /// Backtest configuration. diff --git a/src/portfolio/engine.rs b/src/portfolio/engine.rs index 198ea3b..e3aff20 100644 --- a/src/portfolio/engine.rs +++ b/src/portfolio/engine.rs @@ -761,6 +761,25 @@ impl PortfolioEngine { } } +/// Compute `BacktestMetrics` from pre-built curves and trade list. +/// +/// Exposed as a standalone function so non-OHLCV strategies (e.g. tick backtest) +/// can produce identical metrics without duplicating the calculation logic. +pub fn compute_backtest_metrics( + equity_curve: &[f64], + drawdown_curve: &[f64], + returns: &[f64], + trades: &[Trade], + initial_capital: f64, +) -> BacktestMetrics { + // Delegate to a throwaway engine instance — avoids duplicating the logic. + let engine = PortfolioEngine::new(BacktestConfig { + initial_capital, + ..Default::default() + }); + engine.calculate_metrics(equity_curve, drawdown_curve, returns, trades, &StreamingMetrics::new()) +} + #[cfg(test)] mod tests { use super::*;