using System; using System.Collections.Generic; using System.Linq; namespace QuantEngine.Core.Domain; public record BacktestTrade( string Ticker, DateTime EntryDate, DateTime ExitDate, decimal EntryPrice, decimal ExitPrice, int Quantity, decimal ReturnRate, decimal FeeCost ); public record BacktestResult( string RunId, decimal SharpeRatio, decimal MaxDrawdown, decimal AnnualizedReturn, decimal TurnoverRate, decimal CostDrag, string GateStatus ); /// /// Point-in-time Backtesting & Transaction Cost Model Engine /// SOLID: Single Responsibility for deterministic quantitative backtesting /// public class Backtester { private const decimal DefaultFeeRateBps = 15m; // 15 bps per trade private const decimal SlippageBps = 5m; // 5 bps slippage public BacktestResult RunBacktest( string runId, List dailyPortfolioValues, List trades, decimal initialCapital) { if (dailyPortfolioValues == null || dailyPortfolioValues.Count < 2) { return new BacktestResult(runId, 0m, 0m, 0m, 0m, 0m, "FAIL_INSUFFICIENT_DATA"); } // 1. Daily Returns & Sharpe Ratio Calculation var dailyReturns = new List(); for (int i = 1; i < dailyPortfolioValues.Count; i++) { var prev = dailyPortfolioValues[i - 1]; var curr = dailyPortfolioValues[i]; var ret = prev > 0 ? (curr - prev) / prev : 0m; dailyReturns.Add(ret); } var avgReturn = dailyReturns.Average(); var stdDev = CalculateStdDev(dailyReturns); var annualFactor = (decimal)Math.Sqrt(252); var sharpeRatio = stdDev > 0 ? (avgReturn / stdDev) * annualFactor : 0m; // 2. Max Drawdown (MDD) Calculation decimal peak = dailyPortfolioValues[0]; decimal maxDrawdown = 0m; foreach (var val in dailyPortfolioValues) { if (val > peak) peak = val; var dd = peak > 0 ? (peak - val) / peak : 0m; if (dd > maxDrawdown) maxDrawdown = dd; } // 3. Turnover Rate & Cost Drag decimal totalTradedVolume = trades.Sum(t => (t.EntryPrice * t.Quantity) + (t.ExitPrice * t.Quantity)); decimal totalFees = trades.Sum(t => t.FeeCost) + (totalTradedVolume * (FeeRateBpsToRatio(DefaultFeeRateBps + SlippageBps))); decimal turnoverRate = initialCapital > 0 ? totalTradedVolume / initialCapital : 0m; decimal costDrag = initialCapital > 0 ? totalFees / initialCapital : 0m; decimal totalReturn = (dailyPortfolioValues.Last() - dailyPortfolioValues[0]) / dailyPortfolioValues[0]; decimal annualizedReturn = totalReturnsToAnnualized(totalReturn, dailyPortfolioValues.Count); return new BacktestResult( runId, Math.Round(sharpeRatio, 4), Math.Round(maxDrawdown, 4), Math.Round(annualizedReturn, 4), Math.Round(turnoverRate, 4), Math.Round(costDrag, 4), "PASS" ); } private static decimal CalculateStdDev(List values) { if (values.Count < 2) return 0m; var avg = values.Average(); var sumSquares = values.Sum(v => (v - avg) * (v - avg)); var variance = sumSquares / (values.Count - 1); return (decimal)Math.Sqrt((double)variance); } private static decimal FeeRateBpsToRatio(decimal bps) => bps / 10000m; private static decimal totalReturnsToAnnualized(decimal totalReturn, int days) { if (days <= 0) return 0m; double years = days / 252.0; if (years <= 0) return totalReturn; double compound = Math.Pow((double)(1m + totalReturn), 1.0 / years) - 1.0; return (decimal)compound; } }