Some checks failed
Build and Test / build (push) Has been cancelled
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
282 lines
10 KiB
C#
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("\"", "\\\"");
|
|
}
|
|
}
|
|
}
|