namespace Wickra.Examples; /// Summary statistics for a long-only equity curve. public sealed record EquityResult(double TotalReturnPct, double Sharpe, double MaxDrawdownPct, int Trades, double FinalEquity); /// /// Minimal long-only backtest helper: turn a stream of per-bar fractional /// returns into a PnL / Sharpe / max-drawdown summary. The strategy examples /// produce the returns; this aggregates them. /// public static class Backtest { /// Per-bar fractional returns (0.01 == +1%). /// Number of position entries. /// Annualisation factor for the Sharpe ratio. public static EquityResult Summarize(IReadOnlyList periodReturns, int trades, double periodsPerYear = 252.0) { double equity = 1.0, peak = 1.0, maxDrawdown = 0.0; foreach (var r in periodReturns) { equity *= 1.0 + r; peak = Math.Max(peak, equity); if (peak > 0) { maxDrawdown = Math.Max(maxDrawdown, (peak - equity) / peak); } } var mean = periodReturns.Count > 0 ? periodReturns.Average() : 0.0; var variance = periodReturns.Count > 1 ? periodReturns.Sum(x => (x - mean) * (x - mean)) / (periodReturns.Count - 1) : 0.0; var stdDev = Math.Sqrt(variance); var sharpe = stdDev > 1e-12 ? mean / stdDev * Math.Sqrt(periodsPerYear) : 0.0; return new EquityResult((equity - 1.0) * 100.0, sharpe, maxDrawdown * 100.0, trades, equity); } /// Prints a one-line summary. public static void Print(string name, EquityResult r) { Console.WriteLine( $"{name,-26} return={r.TotalReturnPct,8:F2}% sharpe={r.Sharpe,6:F2} maxDD={r.MaxDrawdownPct,6:F2}% trades={r.Trades}"); } /// /// Prints the per-trade backtest summary shared verbatim with the Rust, /// Python, Node, Go and C example suites (same labels, same numbers). /// public static void PrintSummary(string name, double firstPrice, double lastPrice, int bars, IReadOnlyList closedTrades, double finalEquity, IReadOnlyList equityCurve) { var ci = System.Globalization.CultureInfo.InvariantCulture; var buyHold = lastPrice / firstPrice; var stratReturn = finalEquity - 1.0; var bhReturn = buyHold - 1.0; int wins = 0, losses = 0; double best = 0.0, worst = 0.0; for (var i = 0; i < closedTrades.Count; i++) { var r = closedTrades[i]; if (r > 0) { wins++; } else if (r < 0) { losses++; } if (i == 0 || r > best) { best = r; } if (i == 0 || r < worst) { worst = r; } } var n = closedTrades.Count; var mean = n > 0 ? closedTrades.Average() : 0.0; var variance = n > 1 ? closedTrades.Sum(x => (x - mean) * (x - mean)) / (n - 1) : 0.0; var sharpe = variance > 0 ? mean / Math.Sqrt(variance) : 0.0; var peak = equityCurve.Count > 0 ? equityCurve[0] : 1.0; var maxDd = 0.0; foreach (var eq in equityCurve) { if (eq > peak) { peak = eq; } var dd = (peak - eq) / peak; if (dd > maxDd) { maxDd = dd; } } Console.WriteLine($"=== {name} ==="); Console.WriteLine(string.Create(ci, $"{"Bars:",-23}{bars}")); Console.WriteLine(string.Create(ci, $"{"Trades:",-23}{n} (W{wins} / L{losses})")); Console.WriteLine(string.Create(ci, $"{"Strategy return:",-23}{stratReturn * 100:+0.00;-0.00}%")); Console.WriteLine(string.Create(ci, $"{"Buy & Hold return:",-23}{bhReturn * 100:+0.00;-0.00}%")); Console.WriteLine(string.Create(ci, $"{"Excess over BH:",-23}{(stratReturn - bhReturn) * 100:+0.00;-0.00}%")); Console.WriteLine(string.Create(ci, $"{"Max drawdown:",-23}{maxDd * 100:0.00}%")); Console.WriteLine(string.Create(ci, $"{"Per-trade Sharpe:",-23}{sharpe:0.00} (mean {mean:+0.0000;-0.0000}, stddev {Math.Sqrt(variance):0.0000})")); Console.WriteLine(string.Create(ci, $"{"Best / worst trade:",-23}{best * 100:+0.00;-0.00}% / {worst * 100:+0.00;-0.00}%")); Console.WriteLine(); Console.WriteLine("NOTE: Educational example — fees, slippage, funding costs and tax " + "effects are simplified or omitted. Past performance is not " + "indicative of future results."); } }