using System; using System.Collections.Generic; using System.Linq; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Analyzes drawdown behavior from trade history. /// public class DrawdownAnalyzer { private readonly ILogger _logger; /// /// Initializes analyzer. /// /// Logger dependency. public DrawdownAnalyzer(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Runs full drawdown analysis. /// /// Trade records. /// Drawdown report. public DrawdownReport Analyze(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { var periods = IdentifyDrawdowns(trades); var report = new DrawdownReport(); report.DrawdownPeriods = periods; report.NumberOfDrawdowns = periods.Count; report.MaxDrawdownAmount = periods.Count > 0 ? periods.Max(p => p.DrawdownAmount) : 0.0; report.MaxDrawdownPercent = periods.Count > 0 ? periods.Max(p => p.DrawdownPercent) : 0.0; report.CurrentDrawdownAmount = periods.Count > 0 && !periods[periods.Count - 1].RecoveryTime.HasValue ? periods[periods.Count - 1].DrawdownAmount : 0.0; report.AverageDrawdownPercent = periods.Count > 0 ? periods.Average(p => p.DrawdownPercent) : 0.0; report.LongestDuration = periods.Count > 0 ? periods.Max(p => p.DurationToTrough) : TimeSpan.Zero; var recovered = periods.Where(p => p.DurationToRecovery.HasValue).Select(p => p.DurationToRecovery.Value).ToList(); if (recovered.Count > 0) { report.AverageRecoveryTime = TimeSpan.FromTicks((long)recovered.Average(t => t.Ticks)); } return report; } catch (Exception ex) { _logger.LogError("Drawdown Analyze failed: {0}", ex.Message); throw; } } /// /// Identifies drawdown periods from ordered trades. /// /// Trade records. /// Drawdown periods. public List IdentifyDrawdowns(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { var ordered = trades .OrderBy(t => t.ExitTime.HasValue ? t.ExitTime.Value : t.EntryTime) .ToList(); var periods = new List(); var equity = 0.0; var peak = 0.0; DateTime peakTime = DateTime.MinValue; DrawdownPeriod active = null; foreach (var trade in ordered) { var eventTime = trade.ExitTime.HasValue ? trade.ExitTime.Value : trade.EntryTime; equity += trade.RealizedPnL; if (equity >= peak) { peak = equity; peakTime = eventTime; if (active != null) { active.RecoveryTime = eventTime; active.DurationToRecovery = active.RecoveryTime.Value - active.StartTime; periods.Add(active); active = null; } continue; } var drawdownAmount = peak - equity; var drawdownPercent = peak > 0.0 ? (drawdownAmount / peak) * 100.0 : drawdownAmount; if (active == null) { active = new DrawdownPeriod(); active.StartTime = peakTime == DateTime.MinValue ? eventTime : peakTime; active.PeakEquity = peak; active.TroughTime = eventTime; active.TroughEquity = equity; active.DrawdownAmount = drawdownAmount; active.DrawdownPercent = drawdownPercent; active.DurationToTrough = eventTime - active.StartTime; } else if (equity <= active.TroughEquity) { active.TroughTime = eventTime; active.TroughEquity = equity; active.DrawdownAmount = drawdownAmount; active.DrawdownPercent = drawdownPercent; active.DurationToTrough = eventTime - active.StartTime; } } if (active != null) { periods.Add(active); } return periods; } catch (Exception ex) { _logger.LogError("IdentifyDrawdowns failed: {0}", ex.Message); throw; } } /// /// Attributes one drawdown period to likely causes. /// /// Drawdown period. /// Attribution details. public DrawdownAttribution AttributeDrawdown(DrawdownPeriod period) { if (period == null) throw new ArgumentNullException("period"); try { var attribution = new DrawdownAttribution(); if (period.DrawdownPercent >= 20.0) attribution.PrimaryCause = "SevereLossCluster"; else if (period.DrawdownPercent >= 10.0) attribution.PrimaryCause = "ModerateLossCluster"; else attribution.PrimaryCause = "NormalVariance"; attribution.TradeCount = 0; attribution.WorstSymbol = string.Empty; attribution.WorstStrategy = string.Empty; attribution.GradeContributions.Add("Unknown", period.DrawdownAmount); return attribution; } catch (Exception ex) { _logger.LogError("AttributeDrawdown failed: {0}", ex.Message); throw; } } /// /// Calculates recovery time in days for a drawdown period. /// /// Drawdown period. /// Recovery time in days, -1 if unrecovered. public double CalculateRecoveryTime(DrawdownPeriod period) { if (period == null) throw new ArgumentNullException("period"); try { if (!period.RecoveryTime.HasValue) return -1.0; return (period.RecoveryTime.Value - period.StartTime).TotalDays; } catch (Exception ex) { _logger.LogError("CalculateRecoveryTime failed: {0}", ex.Message); throw; } } } }