examples: fix and harmonize the strategy backtests across all languages (#324)

The strategy_* examples were only syntax-smoked in CI, never run, which hid two
classes of problem:

1. Python strategy_macd_adx / strategy_bollinger_squeeze passed three separate
   arguments to the candle indicators ADX/ATR, whose .update() takes a single
   candle — a TypeError at runtime — and read the ADX tuple at index 0 (plus_di)
   instead of 2 (adx). Both fixed.

2. The Go / C# / R / Java strategies defaulted to synthetic data and used a
   different (annualised) one-line summary, so they printed wildly different
   numbers from the Rust/Python/Node/C/WASM suite. Rewrite them to the shared
   per-trade backtest (load the bundled BTCUSDT CSV by default, same entry/exit
   logic, same print_summary output).

All nine runnable bindings now print byte-identical backtest summaries on the
same data (MACD+ADX 246 trades / -47.19%, RSI 37 / -17.84%, Bollinger 1 / -7.82%),
verified by diffing each language's output against the Python reference. WASM
shares the same logic and bundled dataset (browser-rendered).
This commit is contained in:
kingchenc
2026-06-17 17:56:22 +02:00
committed by GitHub
parent 2e07c07a40
commit 75eefbbd08
20 changed files with 886 additions and 196 deletions
+71
View File
@@ -42,4 +42,75 @@ public static class Backtest
Console.WriteLine(
$"{name,-26} return={r.TotalReturnPct,8:F2}% sharpe={r.Sharpe,6:F2} maxDD={r.MaxDrawdownPct,6:F2}% trades={r.Trades}");
}
/// <summary>
/// Prints the per-trade backtest summary shared verbatim with the Rust,
/// Python, Node, Go and C example suites (same labels, same numbers).
/// </summary>
public static void PrintSummary(string name, double firstPrice, double lastPrice, int bars,
IReadOnlyList<double> closedTrades, double finalEquity, IReadOnlyList<double> 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.");
}
}
+11
View File
@@ -58,4 +58,15 @@ public static class MarketData
return bars;
}
/// <summary>
/// Loads one of the checked-in datasets under examples/data, resolved
/// relative to this source file so it works from any working directory.
/// </summary>
public static Bar[] BundledCandles(string filename,
[System.Runtime.CompilerServices.CallerFilePath] string self = "")
{
var dir = Path.GetDirectoryName(self)!;
return LoadOhlcvCsv(Path.Combine(dir, "..", "..", "data", filename));
}
}