Files
nt8-sdk/src/NT8.Core/Analytics/ReportGenerator.cs
mo 0e36fe5d23
Some checks failed
Build and Test / build (push) Has been cancelled
feat: Complete Phase 5 Analytics & Reporting implementation
Analytics Layer (15 components):
- TradeRecorder: Full trade lifecycle tracking with partial fills
- PerformanceCalculator: Sharpe, Sortino, win rate, profit factor, expectancy
- PnLAttributor: Multi-dimensional attribution (grade/regime/time/strategy)
- DrawdownAnalyzer: Period detection and recovery metrics
- GradePerformanceAnalyzer: Grade-level edge analysis
- RegimePerformanceAnalyzer: Regime segmentation and transitions
- ConfluenceValidator: Factor validation and weighting optimization
- ReportGenerator: Daily/weekly/monthly reporting with export
- TradeBlotter: Real-time trade ledger with filtering
- ParameterOptimizer: Grid search and walk-forward scaffolding
- MonteCarloSimulator: Confidence intervals and risk-of-ruin
- PortfolioOptimizer: Multi-strategy allocation and portfolio metrics

Test Coverage (90 new tests):
- 240+ total tests, 100% pass rate
- >85% code coverage
- Zero new warnings

Project Status: Phase 5 complete (85% overall), ready for NT8 integration
2026-02-16 21:30:51 -05:00

282 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using NT8.Core.Logging;
namespace NT8.Core.Analytics
{
/// <summary>
/// Generates performance reports and export formats.
/// </summary>
public class ReportGenerator
{
private readonly ILogger _logger;
private readonly PerformanceCalculator _calculator;
public ReportGenerator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
_calculator = new PerformanceCalculator(logger);
}
/// <summary>
/// Generates daily report.
/// </summary>
public DailyReport GenerateDailyReport(DateTime date, List<TradeRecord> trades)
{
if (trades == null)
throw new ArgumentNullException("trades");
try
{
var dayStart = date.Date;
var dayEnd = dayStart.AddDays(1);
var subset = trades.Where(t => t.EntryTime >= dayStart && t.EntryTime < dayEnd).ToList();
var report = new DailyReport();
report.Date = dayStart;
report.SummaryMetrics = _calculator.Calculate(subset);
foreach (var g in subset.GroupBy(t => t.Grade.ToString()))
{
report.GradePnL[g.Key] = g.Sum(t => t.RealizedPnL);
}
return report;
}
catch (Exception ex)
{
_logger.LogError("GenerateDailyReport failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Generates weekly report.
/// </summary>
public WeeklyReport GenerateWeeklyReport(DateTime weekStart, List<TradeRecord> trades)
{
if (trades == null)
throw new ArgumentNullException("trades");
try
{
var start = weekStart.Date;
var end = start.AddDays(7);
var subset = trades.Where(t => t.EntryTime >= start && t.EntryTime < end).ToList();
var report = new WeeklyReport();
report.WeekStart = start;
report.WeekEnd = end.AddTicks(-1);
report.SummaryMetrics = _calculator.Calculate(subset);
foreach (var g in subset.GroupBy(t => t.StrategyName ?? string.Empty))
{
report.StrategyPnL[g.Key] = g.Sum(t => t.RealizedPnL);
}
return report;
}
catch (Exception ex)
{
_logger.LogError("GenerateWeeklyReport failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Generates monthly report.
/// </summary>
public MonthlyReport GenerateMonthlyReport(int year, int month, List<TradeRecord> trades)
{
if (trades == null)
throw new ArgumentNullException("trades");
try
{
var start = new DateTime(year, month, 1);
var end = start.AddMonths(1);
var subset = trades.Where(t => t.EntryTime >= start && t.EntryTime < end).ToList();
var report = new MonthlyReport();
report.Year = year;
report.Month = month;
report.SummaryMetrics = _calculator.Calculate(subset);
foreach (var g in subset.GroupBy(t => t.Symbol ?? string.Empty))
{
report.SymbolPnL[g.Key] = g.Sum(t => t.RealizedPnL);
}
return report;
}
catch (Exception ex)
{
_logger.LogError("GenerateMonthlyReport failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Exports report to text format.
/// </summary>
public string ExportToText(Report report)
{
if (report == null)
throw new ArgumentNullException("report");
try
{
var sb = new StringBuilder();
sb.AppendLine(string.Format("=== {0} Report ===", report.ReportName));
sb.AppendLine(string.Format("Generated: {0:O}", report.GeneratedAtUtc));
sb.AppendLine();
sb.AppendLine(string.Format("Total Trades: {0}", report.SummaryMetrics.TotalTrades));
sb.AppendLine(string.Format("Win Rate: {0:P2}", report.SummaryMetrics.WinRate));
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "Net Profit: {0:F2}", report.SummaryMetrics.NetProfit));
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "Profit Factor: {0:F2}", report.SummaryMetrics.ProfitFactor));
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "Expectancy: {0:F2}", report.SummaryMetrics.Expectancy));
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "Max Drawdown %: {0:F2}", report.SummaryMetrics.MaxDrawdownPercent));
return sb.ToString();
}
catch (Exception ex)
{
_logger.LogError("ExportToText failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Exports trade records to CSV.
/// </summary>
public string ExportToCsv(List<TradeRecord> trades)
{
if (trades == null)
throw new ArgumentNullException("trades");
try
{
var sb = new StringBuilder();
sb.AppendLine("TradeId,Symbol,Strategy,EntryTime,ExitTime,Side,Qty,Entry,Exit,PnL,RMultiple,Grade,RiskMode");
foreach (var t in trades.OrderBy(x => x.EntryTime))
{
sb.AppendFormat(CultureInfo.InvariantCulture,
"{0},{1},{2},{3:O},{4},{5},{6},{7:F4},{8},{9:F2},{10:F4},{11},{12}",
Escape(t.TradeId),
Escape(t.Symbol),
Escape(t.StrategyName),
t.EntryTime,
t.ExitTime.HasValue ? t.ExitTime.Value.ToString("O") : string.Empty,
t.Side,
t.Quantity,
t.EntryPrice,
t.ExitPrice.HasValue ? t.ExitPrice.Value.ToString("F4", CultureInfo.InvariantCulture) : string.Empty,
t.RealizedPnL,
t.RMultiple,
t.Grade,
t.RiskMode);
sb.AppendLine();
}
return sb.ToString();
}
catch (Exception ex)
{
_logger.LogError("ExportToCsv failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Exports report summary to JSON.
/// </summary>
public string ExportToJson(Report report)
{
if (report == null)
throw new ArgumentNullException("report");
try
{
var json = new StringBuilder();
json.Append("{");
json.AppendFormat(CultureInfo.InvariantCulture, "\"reportName\":\"{0}\"", EscapeJson(report.ReportName));
json.AppendFormat(CultureInfo.InvariantCulture, ",\"generatedAtUtc\":\"{0:O}\"", report.GeneratedAtUtc);
json.Append(",\"summary\":{");
json.AppendFormat(CultureInfo.InvariantCulture, "\"totalTrades\":{0}", report.SummaryMetrics.TotalTrades);
json.AppendFormat(CultureInfo.InvariantCulture, ",\"winRate\":{0}", report.SummaryMetrics.WinRate);
json.AppendFormat(CultureInfo.InvariantCulture, ",\"netProfit\":{0}", report.SummaryMetrics.NetProfit);
json.AppendFormat(CultureInfo.InvariantCulture, ",\"profitFactor\":{0}", report.SummaryMetrics.ProfitFactor);
json.AppendFormat(CultureInfo.InvariantCulture, ",\"expectancy\":{0}", report.SummaryMetrics.Expectancy);
json.Append("}");
json.Append("}");
return json.ToString();
}
catch (Exception ex)
{
_logger.LogError("ExportToJson failed: {0}", ex.Message);
throw;
}
}
/// <summary>
/// Builds equity curve points from realized pnl.
/// </summary>
public EquityCurve BuildEquityCurve(List<TradeRecord> trades)
{
if (trades == null)
throw new ArgumentNullException("trades");
try
{
var curve = new EquityCurve();
var equity = 0.0;
var peak = 0.0;
foreach (var trade in trades.OrderBy(t => t.ExitTime.HasValue ? t.ExitTime.Value : t.EntryTime))
{
equity += trade.RealizedPnL;
if (equity > peak)
peak = equity;
var point = new EquityPoint();
point.Time = trade.ExitTime.HasValue ? trade.ExitTime.Value : trade.EntryTime;
point.Equity = equity;
point.Drawdown = peak - equity;
curve.Points.Add(point);
}
return curve;
}
catch (Exception ex)
{
_logger.LogError("BuildEquityCurve failed: {0}", ex.Message);
throw;
}
}
private static string Escape(string value)
{
if (value == null)
return string.Empty;
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
return string.Format("\"{0}\"", value.Replace("\"", "\"\""));
return value;
}
private static string EscapeJson(string value)
{
if (value == null)
return string.Empty;
return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
}
}