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