using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Generates performance reports and export formats. /// 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); } /// /// Generates daily report. /// public DailyReport GenerateDailyReport(DateTime date, List 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; } } /// /// Generates weekly report. /// public WeeklyReport GenerateWeeklyReport(DateTime weekStart, List 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; } } /// /// Generates monthly report. /// public MonthlyReport GenerateMonthlyReport(int year, int month, List 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; } } /// /// Exports report to text format. /// 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; } } /// /// Exports trade records to CSV. /// public string ExportToCsv(List 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; } } /// /// Exports report summary to JSON. /// 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; } } /// /// Builds equity curve points from realized pnl. /// public EquityCurve BuildEquityCurve(List 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("\"", "\\\""); } } }