114 lines
3.8 KiB
C#
114 lines
3.8 KiB
C#
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
|
|
);
|
|
|
|
/// <summary>
|
|
/// Point-in-time Backtesting & Transaction Cost Model Engine
|
|
/// SOLID: Single Responsibility for deterministic quantitative backtesting
|
|
/// </summary>
|
|
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<decimal> dailyPortfolioValues,
|
|
List<BacktestTrade> 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<decimal>();
|
|
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<decimal> 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;
|
|
}
|
|
}
|