Files
QuantEngineByItz/src/dotnet/QuantEngine.Core/Domain/Backtester.cs
T
kjh2064 757f2439af
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Failing after 12s
Validators (Pushes and Pull Requests) / validate-core (push) Failing after 20s
feat(wbs): WBS M4/M5 C# domain engines & Vue 3 PrimeVue AG-Grid migration [WBS-10]
2026-07-24 11:31:36 +09:00

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;
}
}