feat(wbs): WBS M4/M5 C# domain engines & Vue 3 PrimeVue AG-Grid migration [WBS-10]
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user