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:
@@ -42,4 +42,75 @@ public static class Backtest
|
|||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"{name,-26} return={r.TotalReturnPct,8:F2}% sharpe={r.Sharpe,6:F2} maxDD={r.MaxDrawdownPct,6:F2}% trades={r.Trades}");
|
$"{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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,15 @@ public static class MarketData
|
|||||||
|
|
||||||
return bars;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,91 @@
|
|||||||
using Wickra;
|
using Wickra;
|
||||||
using Wickra.Examples;
|
using Wickra.Examples;
|
||||||
|
|
||||||
// Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above the
|
// Strategy example: Bollinger-squeeze breakout with an ATR(14) trailing stop.
|
||||||
// upper band, go long with an ATR(14) trailing stop.
|
//
|
||||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
// Enters long when Bollinger bandwidth makes a new SqueezeLookback low (a
|
||||||
|
// volatility squeeze) and price closes above the upper band; exits on an ATR(14)
|
||||||
|
// trailing stop or when the upper band falls back below the entry. 0.1% fees per
|
||||||
|
// trade. The C# counterpart of examples/python/strategy_bollinger_squeeze.py,
|
||||||
|
// printing the same summary. Uses the checked-in examples/data/btcusdt-1d.csv
|
||||||
|
// dataset (pass a CSV path to override).
|
||||||
|
const double Fee = 0.001;
|
||||||
|
const double AtrStopMult = 2.0;
|
||||||
|
const int SqueezeLookback = 180;
|
||||||
|
|
||||||
|
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.BundledCandles("btcusdt-1d.csv");
|
||||||
|
|
||||||
using var bollinger = new BollingerBands(20, 2.0);
|
using var bollinger = new BollingerBands(20, 2.0);
|
||||||
using var atr = new Atr(14);
|
using var atr = new Atr(14);
|
||||||
|
|
||||||
var returns = new List<double>();
|
|
||||||
var trades = 0;
|
|
||||||
var inPosition = false;
|
var inPosition = false;
|
||||||
var entry = 0.0;
|
var entryPrice = 0.0;
|
||||||
var stop = 0.0;
|
var stopLevel = 0.0;
|
||||||
|
var closedTrades = new List<double>();
|
||||||
|
var equity = 1.0;
|
||||||
|
var equityCurve = new List<double>();
|
||||||
|
var bwWindow = new Queue<double>();
|
||||||
|
|
||||||
foreach (var b in bars)
|
foreach (var b in bars)
|
||||||
{
|
{
|
||||||
var bands = bollinger.Update(b.Close);
|
var bands = bollinger.Update(b.Close);
|
||||||
var atrValue = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
var atrValue = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
||||||
|
var price = b.Close;
|
||||||
|
equityCurve.Add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
|
|
||||||
if (bands is not { } band || !double.IsFinite(atrValue))
|
if (bands is not { } band || !double.IsFinite(atrValue))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bandwidth = band.Middle != 0.0 ? (band.Upper - band.Lower) / band.Middle : double.MaxValue;
|
if (Math.Abs(band.Middle) <= 1e-12)
|
||||||
|
|
||||||
if (!inPosition && bandwidth < 0.06 && b.Close > band.Upper)
|
|
||||||
{
|
{
|
||||||
inPosition = true;
|
continue;
|
||||||
entry = b.Close;
|
|
||||||
stop = b.Close - 2.0 * atrValue;
|
|
||||||
trades++;
|
|
||||||
}
|
}
|
||||||
else if (inPosition)
|
|
||||||
|
var bandwidth = (band.Upper - band.Lower) / band.Middle;
|
||||||
|
bwWindow.Enqueue(bandwidth);
|
||||||
|
if (bwWindow.Count > SqueezeLookback)
|
||||||
{
|
{
|
||||||
stop = Math.Max(stop, b.Close - 2.0 * atrValue); // trail the stop up
|
bwWindow.Dequeue();
|
||||||
if (b.Close < stop)
|
}
|
||||||
|
|
||||||
|
if (bwWindow.Count < SqueezeLookback)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var minBw = bwWindow.Min();
|
||||||
|
|
||||||
|
if (inPosition)
|
||||||
|
{
|
||||||
|
if (price < stopLevel || band.Upper < entryPrice)
|
||||||
{
|
{
|
||||||
returns.Add((b.Close - entry) / entry);
|
var tradeRet = price / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var isNewLow = Math.Abs(bandwidth - minBw) < 1e-12;
|
||||||
|
if (isNewLow && price > band.Upper)
|
||||||
|
{
|
||||||
|
entryPrice = price;
|
||||||
|
stopLevel = price - AtrStopMult * atrValue;
|
||||||
|
equity *= 1.0 - Fee;
|
||||||
|
inPosition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Backtest.Print("Bollinger squeeze", Backtest.Summarize(returns, trades));
|
if (inPosition)
|
||||||
|
{
|
||||||
|
var tradeRet = bars[^1].Close / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
Backtest.PrintSummary("Bollinger Squeeze Breakout (1d, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[^1].Close, bars.Length, closedTrades, equity, equityCurve);
|
||||||
|
|||||||
@@ -1,42 +1,66 @@
|
|||||||
using Wickra;
|
using Wickra;
|
||||||
using Wickra.Examples;
|
using Wickra.Examples;
|
||||||
|
|
||||||
// Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
// Strategy example: MACD crossover with ADX trend-strength filter.
|
||||||
// confirms a trend; exit when the histogram crosses back below zero.
|
//
|
||||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
// Enters long on a MACD histogram cross up (the histogram turns positive) while
|
||||||
|
// ADX(14) > 20 (a directional market); exits on the opposite MACD crossover
|
||||||
|
// regardless of ADX. 0.1% fees per trade. The C# counterpart of
|
||||||
|
// examples/python/strategy_macd_adx.py, printing the same summary. Uses the
|
||||||
|
// checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to override).
|
||||||
|
const double Fee = 0.001;
|
||||||
|
const double AdxFloor = 20.0;
|
||||||
|
|
||||||
|
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.BundledCandles("btcusdt-1h.csv");
|
||||||
|
|
||||||
using var macd = new MacdIndicator(12, 26, 9);
|
using var macd = new MacdIndicator(12, 26, 9);
|
||||||
using var adx = new Adx(14);
|
using var adx = new Adx(14);
|
||||||
|
|
||||||
var returns = new List<double>();
|
|
||||||
var trades = 0;
|
|
||||||
var inPosition = false;
|
var inPosition = false;
|
||||||
var entry = 0.0;
|
var entryPrice = 0.0;
|
||||||
var prevHistogram = double.NaN;
|
var closedTrades = new List<double>();
|
||||||
|
var equity = 1.0;
|
||||||
|
var equityCurve = new List<double>();
|
||||||
|
bool? prevSign = null;
|
||||||
|
|
||||||
foreach (var b in bars)
|
foreach (var b in bars)
|
||||||
{
|
{
|
||||||
var m = macd.Update(b.Close);
|
var m = macd.Update(b.Close);
|
||||||
var a = adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
var a = adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
||||||
|
var price = b.Close;
|
||||||
|
equityCurve.Add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
|
|
||||||
if (m is not { } macdValue || a is not { } adxValue)
|
if (m is not { } macdValue || a is not { } adxValue)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var trending = adxValue.Adx > 20.0;
|
var histSign = macdValue.Histogram > 0.0;
|
||||||
if (!inPosition && trending && double.IsFinite(prevHistogram) && prevHistogram <= 0.0 && macdValue.Histogram > 0.0)
|
var crossUp = prevSign == false && histSign;
|
||||||
|
var crossDown = prevSign == true && !histSign;
|
||||||
|
prevSign = histSign;
|
||||||
|
|
||||||
|
if (!inPosition && crossUp && adxValue.Adx > AdxFloor)
|
||||||
{
|
{
|
||||||
|
entryPrice = price;
|
||||||
|
equity *= 1.0 - Fee;
|
||||||
inPosition = true;
|
inPosition = true;
|
||||||
entry = b.Close;
|
|
||||||
trades++;
|
|
||||||
}
|
}
|
||||||
else if (inPosition && macdValue.Histogram < 0.0)
|
else if (inPosition && crossDown)
|
||||||
{
|
{
|
||||||
returns.Add((b.Close - entry) / entry);
|
var tradeRet = price / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
prevHistogram = macdValue.Histogram;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Backtest.Print("MACD + ADX trend", Backtest.Summarize(returns, trades));
|
if (inPosition)
|
||||||
|
{
|
||||||
|
var tradeRet = bars[^1].Close / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
Backtest.PrintSummary("MACD + ADX Trend Filter (1h, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[^1].Close, bars.Length, closedTrades, equity, equityCurve);
|
||||||
|
|||||||
@@ -1,34 +1,57 @@
|
|||||||
using Wickra;
|
using Wickra;
|
||||||
using Wickra.Examples;
|
using Wickra.Examples;
|
||||||
|
|
||||||
// Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
|
// Strategy example: RSI(14) mean-reversion.
|
||||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
//
|
||||||
|
// Go long when RSI(14) drops below 30 (oversold), exit when it recovers above
|
||||||
|
// 70 (overbought). 0.1% fees per trade. The C# counterpart of
|
||||||
|
// examples/python/strategy_rsi_mean_reversion.py, printing the same summary.
|
||||||
|
// Uses the checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to override).
|
||||||
|
const double Fee = 0.001;
|
||||||
|
const double Oversold = 30.0;
|
||||||
|
const double Overbought = 70.0;
|
||||||
|
|
||||||
|
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.BundledCandles("btcusdt-1h.csv");
|
||||||
|
|
||||||
using var rsi = new Rsi(14);
|
using var rsi = new Rsi(14);
|
||||||
var returns = new List<double>();
|
|
||||||
var trades = 0;
|
|
||||||
var inPosition = false;
|
var inPosition = false;
|
||||||
var entry = 0.0;
|
var entryPrice = 0.0;
|
||||||
|
var closedTrades = new List<double>();
|
||||||
|
var equity = 1.0;
|
||||||
|
var equityCurve = new List<double>();
|
||||||
|
|
||||||
foreach (var b in bars)
|
foreach (var b in bars)
|
||||||
{
|
{
|
||||||
var value = rsi.Update(b.Close);
|
var value = rsi.Update(b.Close);
|
||||||
|
var price = b.Close;
|
||||||
|
equityCurve.Add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
if (!double.IsFinite(value))
|
if (!double.IsFinite(value))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inPosition && value < 30.0)
|
if (!inPosition && value < Oversold)
|
||||||
{
|
{
|
||||||
|
entryPrice = price;
|
||||||
|
equity *= 1.0 - Fee;
|
||||||
inPosition = true;
|
inPosition = true;
|
||||||
entry = b.Close;
|
|
||||||
trades++;
|
|
||||||
}
|
}
|
||||||
else if (inPosition && value > 50.0)
|
else if (inPosition && value > Overbought)
|
||||||
{
|
{
|
||||||
returns.Add((b.Close - entry) / entry);
|
var tradeRet = price / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Backtest.Print("RSI mean-reversion", Backtest.Summarize(returns, trades));
|
if (inPosition)
|
||||||
|
{
|
||||||
|
var tradeRet = bars[^1].Close / entryPrice - 1.0;
|
||||||
|
closedTrades.Add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - Fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
Backtest.PrintSummary("RSI Mean-Reversion (1h, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[^1].Close, bars.Length, closedTrades, equity, equityCurve);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||||
)
|
)
|
||||||
@@ -139,3 +141,85 @@ func Print(name string, r EquityResult) {
|
|||||||
fmt.Printf("%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d\n",
|
fmt.Printf("%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d\n",
|
||||||
name, r.TotalReturnPct, r.Sharpe, r.MaxDrawdownPct, r.Trades)
|
name, r.TotalReturnPct, r.Sharpe, r.MaxDrawdownPct, r.Trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BundledCandles loads one of the checked-in datasets under examples/data,
|
||||||
|
// resolved relative to this source file so it works from any working directory.
|
||||||
|
func BundledCandles(filename string) []Bar {
|
||||||
|
_, self, _, _ := runtime.Caller(0)
|
||||||
|
path := filepath.Join(filepath.Dir(self), "..", "..", "..", "data", filename)
|
||||||
|
bars, err := LoadOhlcvCsv(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bars
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintSummary prints the per-trade backtest summary shared verbatim with the
|
||||||
|
// Rust, Python, Node and C example suites (same labels, same numbers).
|
||||||
|
func PrintSummary(name string, firstPrice, lastPrice float64, bars int, closedTrades []float64, finalEquity float64, equityCurve []float64) {
|
||||||
|
buyHold := lastPrice / firstPrice
|
||||||
|
stratReturn := finalEquity - 1.0
|
||||||
|
bhReturn := buyHold - 1.0
|
||||||
|
wins, losses := 0, 0
|
||||||
|
best, worst := 0.0, 0.0
|
||||||
|
for i, r := range closedTrades {
|
||||||
|
if r > 0 {
|
||||||
|
wins++
|
||||||
|
} else if r < 0 {
|
||||||
|
losses++
|
||||||
|
}
|
||||||
|
if i == 0 || r > best {
|
||||||
|
best = r
|
||||||
|
}
|
||||||
|
if i == 0 || r < worst {
|
||||||
|
worst = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := len(closedTrades)
|
||||||
|
mean := 0.0
|
||||||
|
if n > 0 {
|
||||||
|
var sum float64
|
||||||
|
for _, r := range closedTrades {
|
||||||
|
sum += r
|
||||||
|
}
|
||||||
|
mean = sum / float64(n)
|
||||||
|
}
|
||||||
|
variance := 0.0
|
||||||
|
if n > 1 {
|
||||||
|
var ss float64
|
||||||
|
for _, r := range closedTrades {
|
||||||
|
ss += (r - mean) * (r - mean)
|
||||||
|
}
|
||||||
|
variance = ss / float64(n-1)
|
||||||
|
}
|
||||||
|
sharpe := 0.0
|
||||||
|
if variance > 0 {
|
||||||
|
sharpe = mean / math.Sqrt(variance)
|
||||||
|
}
|
||||||
|
peak, maxDD := 1.0, 0.0
|
||||||
|
if len(equityCurve) > 0 {
|
||||||
|
peak = equityCurve[0]
|
||||||
|
}
|
||||||
|
for _, eq := range equityCurve {
|
||||||
|
if eq > peak {
|
||||||
|
peak = eq
|
||||||
|
}
|
||||||
|
if dd := (peak - eq) / peak; dd > maxDD {
|
||||||
|
maxDD = dd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("=== %s ===\n", name)
|
||||||
|
fmt.Printf("%-23s%d\n", "Bars:", bars)
|
||||||
|
fmt.Printf("%-23s%d (W%d / L%d)\n", "Trades:", n, wins, losses)
|
||||||
|
fmt.Printf("%-23s%+.2f%%\n", "Strategy return:", stratReturn*100)
|
||||||
|
fmt.Printf("%-23s%+.2f%%\n", "Buy & Hold return:", bhReturn*100)
|
||||||
|
fmt.Printf("%-23s%+.2f%%\n", "Excess over BH:", (stratReturn-bhReturn)*100)
|
||||||
|
fmt.Printf("%-23s%.2f%%\n", "Max drawdown:", maxDD*100)
|
||||||
|
fmt.Printf("%-23s%.2f (mean %+.4f, stddev %.4f)\n", "Per-trade Sharpe:", sharpe, mean, math.Sqrt(variance))
|
||||||
|
fmt.Printf("%-23s%+.2f%% / %+.2f%%\n", "Best / worst trade:", best*100, worst*100)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("NOTE: Educational example — fees, slippage, funding costs and tax " +
|
||||||
|
"effects are simplified or omitted. Past performance is not " +
|
||||||
|
"indicative of future results.")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
// Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above
|
// Strategy example: Bollinger-squeeze breakout with an ATR(14) trailing stop.
|
||||||
// the upper band, go long with an ATR(14) trailing stop.
|
//
|
||||||
|
// Enters long when Bollinger bandwidth makes a new SQUEEZE_LOOKBACK low (a
|
||||||
|
// volatility squeeze) and price closes above the upper band; exits on an ATR(14)
|
||||||
|
// trailing stop or when the upper band falls back below the entry. 0.1% fees per
|
||||||
|
// trade. The Go counterpart of examples/python/strategy_bollinger_squeeze.py,
|
||||||
|
// printing the same summary.
|
||||||
|
//
|
||||||
|
// Uses the checked-in examples/data/btcusdt-1d.csv dataset (daily bars give an
|
||||||
|
// interpretable ~6-month-low lookback); pass a CSV path to override.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,47 +19,90 @@ import (
|
|||||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fee = 0.001
|
||||||
|
bbPeriod = 20
|
||||||
|
bbK = 2.0
|
||||||
|
atrPeriod = 14
|
||||||
|
atrStopMult = 2.0
|
||||||
|
squeezeLookback = 180
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
bars := loadBars()
|
bars := loadBars()
|
||||||
|
|
||||||
bollinger, _ := wickra.NewBollingerBands(20, 2.0)
|
bb, _ := wickra.NewBollingerBands(bbPeriod, bbK)
|
||||||
defer bollinger.Close()
|
defer bb.Close()
|
||||||
atr, _ := wickra.NewAtr(14)
|
atr, _ := wickra.NewAtr(atrPeriod)
|
||||||
defer atr.Close()
|
defer atr.Close()
|
||||||
|
|
||||||
var returns []float64
|
|
||||||
trades := 0
|
|
||||||
inPosition := false
|
inPosition := false
|
||||||
entry := 0.0
|
entryPrice := 0.0
|
||||||
stop := 0.0
|
stopLevel := 0.0
|
||||||
|
var closedTrades []float64
|
||||||
|
equity := 1.0
|
||||||
|
var equityCurve []float64
|
||||||
|
var bwWindow []float64
|
||||||
|
|
||||||
for _, b := range bars {
|
for _, b := range bars {
|
||||||
band, okBand := bollinger.Update(b.Close)
|
band, okBand := bb.Update(b.Close)
|
||||||
atrValue := atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
atrVal := atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
||||||
if !okBand || math.IsNaN(atrValue) {
|
price := b.Close
|
||||||
|
mtm := equity
|
||||||
|
if inPosition {
|
||||||
|
mtm = equity * (price / entryPrice)
|
||||||
|
}
|
||||||
|
equityCurve = append(equityCurve, mtm)
|
||||||
|
|
||||||
|
if !okBand || math.IsNaN(atrVal) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
upper, middle, lower := band.Upper, band.Middle, band.Lower
|
||||||
bandwidth := math.MaxFloat64
|
if math.Abs(middle) <= 1e-12 {
|
||||||
if band.Middle != 0.0 {
|
continue
|
||||||
bandwidth = (band.Upper - band.Lower) / band.Middle
|
}
|
||||||
|
bandwidth := (upper - lower) / middle
|
||||||
|
bwWindow = append(bwWindow, bandwidth)
|
||||||
|
if len(bwWindow) > squeezeLookback {
|
||||||
|
bwWindow = bwWindow[len(bwWindow)-squeezeLookback:]
|
||||||
|
}
|
||||||
|
if len(bwWindow) < squeezeLookback {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minBw := bwWindow[0]
|
||||||
|
for _, v := range bwWindow {
|
||||||
|
if v < minBw {
|
||||||
|
minBw = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !inPosition && bandwidth < 0.06 && b.Close > band.Upper {
|
if inPosition {
|
||||||
inPosition = true
|
if price < stopLevel || upper < entryPrice {
|
||||||
entry = b.Close
|
tradeRet := price/entryPrice - 1.0
|
||||||
stop = b.Close - 2.0*atrValue
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
trades++
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
} else if inPosition {
|
|
||||||
stop = math.Max(stop, b.Close-2.0*atrValue) // trail the stop up
|
|
||||||
if b.Close < stop {
|
|
||||||
returns = append(returns, (b.Close-entry)/entry)
|
|
||||||
inPosition = false
|
inPosition = false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
isNewLow := math.Abs(bandwidth-minBw) < 1e-12
|
||||||
|
if isNewLow && price > upper {
|
||||||
|
entryPrice = price
|
||||||
|
stopLevel = price - atrStopMult*atrVal
|
||||||
|
equity *= 1.0 - fee
|
||||||
|
inPosition = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
market.Print("Bollinger squeeze", market.Summarize(returns, trades, 252.0))
|
if inPosition {
|
||||||
|
lastPrice := bars[len(bars)-1].Close
|
||||||
|
tradeRet := lastPrice/entryPrice - 1.0
|
||||||
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
|
}
|
||||||
|
|
||||||
|
market.PrintSummary("Bollinger Squeeze Breakout (1d, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[len(bars)-1].Close, len(bars), closedTrades, equity, equityCurve)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBars() []market.Bar {
|
func loadBars() []market.Bar {
|
||||||
@@ -62,5 +113,5 @@ func loadBars() []market.Bar {
|
|||||||
}
|
}
|
||||||
return bars
|
return bars
|
||||||
}
|
}
|
||||||
return market.SyntheticCandles(2000)
|
return market.BundledCandles("btcusdt-1d.csv")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
// Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
// Strategy example: MACD crossover with ADX trend-strength filter.
|
||||||
// confirms a trend; exit when the histogram crosses back below zero.
|
//
|
||||||
|
// Enters long on a MACD histogram cross up (the histogram turns positive) while
|
||||||
|
// ADX(14) > 20 (a directional market); exits on the opposite MACD crossover
|
||||||
|
// regardless of ADX. 0.1% fees per trade. The Go counterpart of
|
||||||
|
// examples/python/strategy_macd_adx.py and the Rust strategy_macd_adx.rs,
|
||||||
|
// printing the same summary.
|
||||||
|
//
|
||||||
|
// Uses the checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to
|
||||||
|
// override).
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fee = 0.001
|
||||||
|
adxFloor = 20.0
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
bars := loadBars()
|
bars := loadBars()
|
||||||
|
|
||||||
@@ -19,32 +31,55 @@ func main() {
|
|||||||
adx, _ := wickra.NewAdx(14)
|
adx, _ := wickra.NewAdx(14)
|
||||||
defer adx.Close()
|
defer adx.Close()
|
||||||
|
|
||||||
var returns []float64
|
|
||||||
trades := 0
|
|
||||||
inPosition := false
|
inPosition := false
|
||||||
entry := 0.0
|
entryPrice := 0.0
|
||||||
prevHistogram := math.NaN()
|
var closedTrades []float64
|
||||||
|
equity := 1.0
|
||||||
|
var equityCurve []float64
|
||||||
|
havePrev := false
|
||||||
|
prevSign := false
|
||||||
|
|
||||||
for _, b := range bars {
|
for _, b := range bars {
|
||||||
m, okMacd := macd.Update(b.Close)
|
m, okMacd := macd.Update(b.Close)
|
||||||
a, okAdx := adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
a, okAdx := adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
||||||
|
price := b.Close
|
||||||
|
mtm := equity
|
||||||
|
if inPosition {
|
||||||
|
mtm = equity * (price / entryPrice)
|
||||||
|
}
|
||||||
|
equityCurve = append(equityCurve, mtm)
|
||||||
|
|
||||||
if !okMacd || !okAdx {
|
if !okMacd || !okAdx {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
trending := a.Adx > 20.0
|
histSign := m.Histogram > 0.0
|
||||||
if !inPosition && trending && !math.IsNaN(prevHistogram) && prevHistogram <= 0.0 && m.Histogram > 0.0 {
|
crossUp := havePrev && !prevSign && histSign
|
||||||
|
crossDown := havePrev && prevSign && !histSign
|
||||||
|
havePrev = true
|
||||||
|
prevSign = histSign
|
||||||
|
|
||||||
|
if !inPosition && crossUp && a.Adx > adxFloor {
|
||||||
|
entryPrice = price
|
||||||
|
equity *= 1.0 - fee
|
||||||
inPosition = true
|
inPosition = true
|
||||||
entry = b.Close
|
} else if inPosition && crossDown {
|
||||||
trades++
|
tradeRet := price/entryPrice - 1.0
|
||||||
} else if inPosition && m.Histogram < 0.0 {
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
returns = append(returns, (b.Close-entry)/entry)
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
inPosition = false
|
inPosition = false
|
||||||
}
|
}
|
||||||
prevHistogram = m.Histogram
|
|
||||||
}
|
}
|
||||||
|
|
||||||
market.Print("MACD + ADX trend", market.Summarize(returns, trades, 252.0))
|
if inPosition {
|
||||||
|
lastPrice := bars[len(bars)-1].Close
|
||||||
|
tradeRet := lastPrice/entryPrice - 1.0
|
||||||
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
|
}
|
||||||
|
|
||||||
|
market.PrintSummary("MACD + ADX Trend Filter (1h, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[len(bars)-1].Close, len(bars), closedTrades, equity, equityCurve)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBars() []market.Bar {
|
func loadBars() []market.Bar {
|
||||||
@@ -55,5 +90,5 @@ func loadBars() []market.Bar {
|
|||||||
}
|
}
|
||||||
return bars
|
return bars
|
||||||
}
|
}
|
||||||
return market.SyntheticCandles(2000)
|
return market.BundledCandles("btcusdt-1h.csv")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
// Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
|
// Strategy example: RSI(14) mean-reversion.
|
||||||
|
//
|
||||||
|
// Go long when RSI(14) drops below 30 (oversold), exit when it recovers above
|
||||||
|
// 70 (overbought). 0.1% fees per trade. The Go counterpart of
|
||||||
|
// examples/python/strategy_rsi_mean_reversion.py, printing the same summary.
|
||||||
|
//
|
||||||
|
// Uses the checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to
|
||||||
|
// override).
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,33 +17,57 @@ import (
|
|||||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fee = 0.001
|
||||||
|
oversold = 30.0
|
||||||
|
overbought = 70.0
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
bars := loadBars()
|
bars := loadBars()
|
||||||
|
|
||||||
rsi, _ := wickra.NewRsi(14)
|
rsi, _ := wickra.NewRsi(14)
|
||||||
defer rsi.Close()
|
defer rsi.Close()
|
||||||
|
|
||||||
var returns []float64
|
|
||||||
trades := 0
|
|
||||||
inPosition := false
|
inPosition := false
|
||||||
entry := 0.0
|
entryPrice := 0.0
|
||||||
|
var closedTrades []float64
|
||||||
|
equity := 1.0
|
||||||
|
var equityCurve []float64
|
||||||
|
|
||||||
for _, b := range bars {
|
for _, b := range bars {
|
||||||
value := rsi.Update(b.Close)
|
value := rsi.Update(b.Close)
|
||||||
|
price := b.Close
|
||||||
|
mtm := equity
|
||||||
|
if inPosition {
|
||||||
|
mtm = equity * (price / entryPrice)
|
||||||
|
}
|
||||||
|
equityCurve = append(equityCurve, mtm)
|
||||||
if math.IsNaN(value) {
|
if math.IsNaN(value) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !inPosition && value < 30.0 {
|
|
||||||
|
if !inPosition && value < oversold {
|
||||||
|
entryPrice = price
|
||||||
|
equity *= 1.0 - fee
|
||||||
inPosition = true
|
inPosition = true
|
||||||
entry = b.Close
|
} else if inPosition && value > overbought {
|
||||||
trades++
|
tradeRet := price/entryPrice - 1.0
|
||||||
} else if inPosition && value > 50.0 {
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
returns = append(returns, (b.Close-entry)/entry)
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
inPosition = false
|
inPosition = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
market.Print("RSI mean-reversion", market.Summarize(returns, trades, 252.0))
|
if inPosition {
|
||||||
|
lastPrice := bars[len(bars)-1].Close
|
||||||
|
tradeRet := lastPrice/entryPrice - 1.0
|
||||||
|
closedTrades = append(closedTrades, tradeRet)
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - fee)
|
||||||
|
}
|
||||||
|
|
||||||
|
market.PrintSummary("RSI Mean-Reversion (1h, BTCUSDT)",
|
||||||
|
bars[0].Close, bars[len(bars)-1].Close, len(bars), closedTrades, equity, equityCurve)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBars() []market.Bar {
|
func loadBars() []market.Bar {
|
||||||
@@ -47,5 +78,5 @@ func loadBars() []market.Bar {
|
|||||||
}
|
}
|
||||||
return bars
|
return bars
|
||||||
}
|
}
|
||||||
return market.SyntheticCandles(2000)
|
return market.BundledCandles("btcusdt-1h.csv")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.wickra.examples;
|
package org.wickra.examples;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal long-only backtest helper: turn a stream of per-bar fractional returns
|
* Minimal long-only backtest helper: turn a stream of per-bar fractional returns
|
||||||
@@ -60,4 +61,74 @@ public final class Equity {
|
|||||||
"%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d%n",
|
"%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d%n",
|
||||||
name, r.totalReturnPct(), r.sharpe(), r.maxDrawdownPct(), r.trades());
|
name, r.totalReturnPct(), r.sharpe(), r.maxDrawdownPct(), r.trades());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints the per-trade backtest summary shared verbatim with the Rust,
|
||||||
|
* Python, Node, Go, C, C# and R example suites (same labels, same numbers).
|
||||||
|
*/
|
||||||
|
public static void printSummary(String name, double firstPrice, double lastPrice, int bars,
|
||||||
|
List<Double> closedTrades, double finalEquity, List<Double> equityCurve) {
|
||||||
|
double buyHold = lastPrice / firstPrice;
|
||||||
|
double stratReturn = finalEquity - 1.0;
|
||||||
|
double bhReturn = buyHold - 1.0;
|
||||||
|
int wins = 0;
|
||||||
|
int losses = 0;
|
||||||
|
double best = 0.0;
|
||||||
|
double worst = 0.0;
|
||||||
|
for (int i = 0; i < closedTrades.size(); i++) {
|
||||||
|
double r = closedTrades.get(i);
|
||||||
|
if (r > 0) {
|
||||||
|
wins++;
|
||||||
|
} else if (r < 0) {
|
||||||
|
losses++;
|
||||||
|
}
|
||||||
|
if (i == 0 || r > best) {
|
||||||
|
best = r;
|
||||||
|
}
|
||||||
|
if (i == 0 || r < worst) {
|
||||||
|
worst = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = closedTrades.size();
|
||||||
|
double mean = 0.0;
|
||||||
|
for (double r : closedTrades) {
|
||||||
|
mean += r;
|
||||||
|
}
|
||||||
|
mean = n > 0 ? mean / n : 0.0;
|
||||||
|
double variance = 0.0;
|
||||||
|
if (n > 1) {
|
||||||
|
for (double r : closedTrades) {
|
||||||
|
variance += (r - mean) * (r - mean);
|
||||||
|
}
|
||||||
|
variance /= n - 1;
|
||||||
|
}
|
||||||
|
double sharpe = variance > 0 ? mean / Math.sqrt(variance) : 0.0;
|
||||||
|
double peak = equityCurve.isEmpty() ? 1.0 : equityCurve.get(0);
|
||||||
|
double maxDd = 0.0;
|
||||||
|
for (double eq : equityCurve) {
|
||||||
|
if (eq > peak) {
|
||||||
|
peak = eq;
|
||||||
|
}
|
||||||
|
double dd = (peak - eq) / peak;
|
||||||
|
if (dd > maxDd) {
|
||||||
|
maxDd = dd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.printf(Locale.ROOT, "=== %s ===%n", name);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%d%n", "Bars:", bars);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%d (W%d / L%d)%n", "Trades:", n, wins, losses);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%+.2f%%%n", "Strategy return:", stratReturn * 100);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%+.2f%%%n", "Buy & Hold return:", bhReturn * 100);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%+.2f%%%n", "Excess over BH:", (stratReturn - bhReturn) * 100);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%.2f%%%n", "Max drawdown:", maxDd * 100);
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%.2f (mean %+.4f, stddev %.4f)%n",
|
||||||
|
"Per-trade Sharpe:", sharpe, mean, Math.sqrt(variance));
|
||||||
|
System.out.printf(Locale.ROOT, "%-23s%+.2f%% / %+.2f%%%n", "Best / worst trade:", best * 100, worst * 100);
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("NOTE: Educational example — fees, slippage, funding costs and tax "
|
||||||
|
+ "effects are simplified or omitted. Past performance is not "
|
||||||
|
+ "indicative of future results.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,4 +68,12 @@ public final class MarketData {
|
|||||||
return bars;
|
return bars;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads one of the checked-in datasets under examples/data. The Java
|
||||||
|
* examples run from the examples/java directory, so ../data is examples/data.
|
||||||
|
*/
|
||||||
|
public static Bar[] bundledCandles(String filename) throws IOException {
|
||||||
|
return loadOhlcvCsv(Path.of("..", "data", filename).toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,96 @@ import org.wickra.BollingerBands;
|
|||||||
import org.wickra.BollingerOutput;
|
import org.wickra.BollingerOutput;
|
||||||
import org.wickra.examples.MarketData.Bar;
|
import org.wickra.examples.MarketData.Bar;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above the
|
* Strategy example: Bollinger-squeeze breakout with an ATR(14) trailing stop.
|
||||||
* upper band, go long with an ATR(14) trailing stop.
|
*
|
||||||
|
* <p>Enters long when Bollinger bandwidth makes a new SQUEEZE_LOOKBACK low (a
|
||||||
|
* volatility squeeze) and price closes above the upper band; exits on an ATR(14)
|
||||||
|
* trailing stop or when the upper band falls back below the entry. 0.1% fees per
|
||||||
|
* trade. The Java counterpart of
|
||||||
|
* {@code examples/python/strategy_bollinger_squeeze.py}, printing the same
|
||||||
|
* summary. Uses the checked-in {@code examples/data/btcusdt-1d.csv} dataset
|
||||||
|
* (pass a CSV path to override).
|
||||||
*/
|
*/
|
||||||
public final class StrategyBollingerSqueeze {
|
public final class StrategyBollingerSqueeze {
|
||||||
|
private static final double FEE = 0.001;
|
||||||
|
private static final double ATR_STOP_MULT = 2.0;
|
||||||
|
private static final int SQUEEZE_LOOKBACK = 180;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.syntheticCandles(2000);
|
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.bundledCandles("btcusdt-1d.csv");
|
||||||
|
|
||||||
try (BollingerBands bollinger = new BollingerBands(20, 2.0);
|
try (BollingerBands bollinger = new BollingerBands(20, 2.0);
|
||||||
Atr atr = new Atr(14)) {
|
Atr atr = new Atr(14)) {
|
||||||
|
|
||||||
List<Double> returns = new ArrayList<>();
|
|
||||||
int trades = 0;
|
|
||||||
boolean inPosition = false;
|
boolean inPosition = false;
|
||||||
double entry = 0.0;
|
double entryPrice = 0.0;
|
||||||
double stop = 0.0;
|
double stopLevel = 0.0;
|
||||||
|
List<Double> closedTrades = new ArrayList<>();
|
||||||
|
double equity = 1.0;
|
||||||
|
List<Double> equityCurve = new ArrayList<>();
|
||||||
|
Deque<Double> bwWindow = new ArrayDeque<>();
|
||||||
|
|
||||||
for (Bar b : bars) {
|
for (Bar b : bars) {
|
||||||
BollingerOutput band = bollinger.update(b.close());
|
BollingerOutput band = bollinger.update(b.close());
|
||||||
double atrValue = atr.update(b.open(), b.high(), b.low(), b.close(), b.volume(), b.timestamp());
|
double atrValue = atr.update(b.open(), b.high(), b.low(), b.close(), b.volume(), b.timestamp());
|
||||||
|
double price = b.close();
|
||||||
|
equityCurve.add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
|
|
||||||
if (band == null || !Double.isFinite(atrValue)) {
|
if (band == null || !Double.isFinite(atrValue)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
double middle = band.middle();
|
||||||
|
if (Math.abs(middle) <= 1e-12) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double upper = band.upper();
|
||||||
|
double bandwidth = (upper - band.lower()) / middle;
|
||||||
|
bwWindow.addLast(bandwidth);
|
||||||
|
if (bwWindow.size() > SQUEEZE_LOOKBACK) {
|
||||||
|
bwWindow.removeFirst();
|
||||||
|
}
|
||||||
|
if (bwWindow.size() < SQUEEZE_LOOKBACK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double minBw = Double.POSITIVE_INFINITY;
|
||||||
|
for (double v : bwWindow) {
|
||||||
|
if (v < minBw) {
|
||||||
|
minBw = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double bandwidth = band.middle() != 0.0
|
if (inPosition) {
|
||||||
? (band.upper() - band.lower()) / band.middle()
|
if (price < stopLevel || upper < entryPrice) {
|
||||||
: Double.MAX_VALUE;
|
double tradeRet = price / entryPrice - 1.0;
|
||||||
|
closedTrades.add(tradeRet);
|
||||||
if (!inPosition && bandwidth < 0.06 && b.close() > band.upper()) {
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
inPosition = true;
|
|
||||||
entry = b.close();
|
|
||||||
stop = b.close() - 2.0 * atrValue;
|
|
||||||
trades++;
|
|
||||||
} else if (inPosition) {
|
|
||||||
stop = Math.max(stop, b.close() - 2.0 * atrValue); // trail the stop up
|
|
||||||
if (b.close() < stop) {
|
|
||||||
returns.add((b.close() - entry) / entry);
|
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
boolean isNewLow = Math.abs(bandwidth - minBw) < 1e-12;
|
||||||
|
if (isNewLow && price > upper) {
|
||||||
|
entryPrice = price;
|
||||||
|
stopLevel = price - ATR_STOP_MULT * atrValue;
|
||||||
|
equity *= 1.0 - FEE;
|
||||||
|
inPosition = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Equity.print("Bollinger squeeze", Equity.summarize(returns, trades));
|
if (inPosition) {
|
||||||
|
double tradeRet = bars[bars.length - 1].close() / entryPrice - 1.0;
|
||||||
|
closedTrades.add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Equity.printSummary("Bollinger Squeeze Breakout (1d, BTCUSDT)",
|
||||||
|
bars[0].close(), bars[bars.length - 1].close(), bars.length, closedTrades, equity, equityCurve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,44 +10,69 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
* Strategy example: MACD crossover with ADX trend-strength filter.
|
||||||
* confirms a trend; exit when the histogram crosses back below zero.
|
*
|
||||||
|
* <p>Enters long on a MACD histogram cross up (the histogram turns positive)
|
||||||
|
* while ADX(14) > 20 (a directional market); exits on the opposite MACD
|
||||||
|
* crossover regardless of ADX. 0.1% fees per trade. The Java counterpart of
|
||||||
|
* {@code examples/python/strategy_macd_adx.py}, printing the same summary. Uses
|
||||||
|
* the checked-in {@code examples/data/btcusdt-1h.csv} dataset (pass a CSV path to
|
||||||
|
* override).
|
||||||
*/
|
*/
|
||||||
public final class StrategyMacdAdx {
|
public final class StrategyMacdAdx {
|
||||||
|
private static final double FEE = 0.001;
|
||||||
|
private static final double ADX_FLOOR = 20.0;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.syntheticCandles(2000);
|
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.bundledCandles("btcusdt-1h.csv");
|
||||||
|
|
||||||
try (MacdIndicator macd = new MacdIndicator(12, 26, 9);
|
try (MacdIndicator macd = new MacdIndicator(12, 26, 9);
|
||||||
Adx adx = new Adx(14)) {
|
Adx adx = new Adx(14)) {
|
||||||
|
|
||||||
List<Double> returns = new ArrayList<>();
|
|
||||||
int trades = 0;
|
|
||||||
boolean inPosition = false;
|
boolean inPosition = false;
|
||||||
double entry = 0.0;
|
double entryPrice = 0.0;
|
||||||
double prevHistogram = Double.NaN;
|
List<Double> closedTrades = new ArrayList<>();
|
||||||
|
double equity = 1.0;
|
||||||
|
List<Double> equityCurve = new ArrayList<>();
|
||||||
|
boolean havePrev = false;
|
||||||
|
boolean prevSign = false;
|
||||||
|
|
||||||
for (Bar b : bars) {
|
for (Bar b : bars) {
|
||||||
MacdOutput m = macd.update(b.close());
|
MacdOutput m = macd.update(b.close());
|
||||||
AdxOutput a = adx.update(b.open(), b.high(), b.low(), b.close(), b.volume(), b.timestamp());
|
AdxOutput a = adx.update(b.open(), b.high(), b.low(), b.close(), b.volume(), b.timestamp());
|
||||||
|
double price = b.close();
|
||||||
|
equityCurve.add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
|
|
||||||
if (m == null || a == null) {
|
if (m == null || a == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean trending = a.adx() > 20.0;
|
boolean histSign = m.histogram() > 0.0;
|
||||||
if (!inPosition && trending && Double.isFinite(prevHistogram)
|
boolean crossUp = havePrev && !prevSign && histSign;
|
||||||
&& prevHistogram <= 0.0 && m.histogram() > 0.0) {
|
boolean crossDown = havePrev && prevSign && !histSign;
|
||||||
|
havePrev = true;
|
||||||
|
prevSign = histSign;
|
||||||
|
|
||||||
|
if (!inPosition && crossUp && a.adx() > ADX_FLOOR) {
|
||||||
|
entryPrice = price;
|
||||||
|
equity *= 1.0 - FEE;
|
||||||
inPosition = true;
|
inPosition = true;
|
||||||
entry = b.close();
|
} else if (inPosition && crossDown) {
|
||||||
trades++;
|
double tradeRet = price / entryPrice - 1.0;
|
||||||
} else if (inPosition && m.histogram() < 0.0) {
|
closedTrades.add(tradeRet);
|
||||||
returns.add((b.close() - entry) / entry);
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
prevHistogram = m.histogram();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Equity.print("MACD + ADX trend", Equity.summarize(returns, trades));
|
if (inPosition) {
|
||||||
|
double tradeRet = bars[bars.length - 1].close() / entryPrice - 1.0;
|
||||||
|
closedTrades.add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Equity.printSummary("MACD + ADX Trend Filter (1h, BTCUSDT)",
|
||||||
|
bars[0].close(), bars[bars.length - 1].close(), bars.length, closedTrades, equity, equityCurve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,33 +6,58 @@ import org.wickra.examples.MarketData.Bar;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50. */
|
/**
|
||||||
|
* Strategy example: RSI(14) mean-reversion.
|
||||||
|
*
|
||||||
|
* <p>Go long when RSI(14) drops below 30 (oversold), exit when it recovers above
|
||||||
|
* 70 (overbought). 0.1% fees per trade. The Java counterpart of
|
||||||
|
* {@code examples/python/strategy_rsi_mean_reversion.py}, printing the same
|
||||||
|
* summary. Uses the checked-in {@code examples/data/btcusdt-1h.csv} dataset
|
||||||
|
* (pass a CSV path to override).
|
||||||
|
*/
|
||||||
public final class StrategyRsiMeanReversion {
|
public final class StrategyRsiMeanReversion {
|
||||||
|
private static final double FEE = 0.001;
|
||||||
|
private static final double OVERSOLD = 30.0;
|
||||||
|
private static final double OVERBOUGHT = 70.0;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.syntheticCandles(2000);
|
Bar[] bars = args.length > 0 ? MarketData.loadOhlcvCsv(args[0]) : MarketData.bundledCandles("btcusdt-1h.csv");
|
||||||
|
|
||||||
try (Rsi rsi = new Rsi(14)) {
|
try (Rsi rsi = new Rsi(14)) {
|
||||||
List<Double> returns = new ArrayList<>();
|
|
||||||
int trades = 0;
|
|
||||||
boolean inPosition = false;
|
boolean inPosition = false;
|
||||||
double entry = 0.0;
|
double entryPrice = 0.0;
|
||||||
|
List<Double> closedTrades = new ArrayList<>();
|
||||||
|
double equity = 1.0;
|
||||||
|
List<Double> equityCurve = new ArrayList<>();
|
||||||
|
|
||||||
for (Bar b : bars) {
|
for (Bar b : bars) {
|
||||||
double value = rsi.update(b.close());
|
double value = rsi.update(b.close());
|
||||||
|
double price = b.close();
|
||||||
|
equityCurve.add(inPosition ? equity * (price / entryPrice) : equity);
|
||||||
if (!Double.isFinite(value)) {
|
if (!Double.isFinite(value)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!inPosition && value < 30.0) {
|
|
||||||
|
if (!inPosition && value < OVERSOLD) {
|
||||||
|
entryPrice = price;
|
||||||
|
equity *= 1.0 - FEE;
|
||||||
inPosition = true;
|
inPosition = true;
|
||||||
entry = b.close();
|
} else if (inPosition && value > OVERBOUGHT) {
|
||||||
trades++;
|
double tradeRet = price / entryPrice - 1.0;
|
||||||
} else if (inPosition && value > 50.0) {
|
closedTrades.add(tradeRet);
|
||||||
returns.add((b.close() - entry) / entry);
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
inPosition = false;
|
inPosition = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Equity.print("RSI mean-reversion", Equity.summarize(returns, trades));
|
if (inPosition) {
|
||||||
|
double tradeRet = bars[bars.length - 1].close() / entryPrice - 1.0;
|
||||||
|
closedTrades.add(tradeRet);
|
||||||
|
equity *= (1.0 + tradeRet) * (1.0 - FEE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Equity.printSummary("RSI Mean-Reversion (1h, BTCUSDT)",
|
||||||
|
bars[0].close(), bars[bars.length - 1].close(), bars.length, closedTrades, equity, equityCurve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def main() -> None:
|
|||||||
|
|
||||||
for c in candles:
|
for c in candles:
|
||||||
bb_out = bb.update(c["close"])
|
bb_out = bb.update(c["close"])
|
||||||
atr_val = atr.update(c["high"], c["low"], c["close"])
|
atr_val = atr.update(c)
|
||||||
price = c["close"]
|
price = c["close"]
|
||||||
mtm = equity * (price / entry_price) if in_position else equity
|
mtm = equity * (price / entry_price) if in_position else equity
|
||||||
equity_curve.append(mtm)
|
equity_curve.append(mtm)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def main() -> None:
|
|||||||
|
|
||||||
for c in candles:
|
for c in candles:
|
||||||
macd_out = macd.update(c["close"])
|
macd_out = macd.update(c["close"])
|
||||||
adx_out = adx.update(c["high"], c["low"], c["close"])
|
adx_out = adx.update(c)
|
||||||
price = c["close"]
|
price = c["close"]
|
||||||
mtm = equity * (price / entry_price) if in_position else equity
|
mtm = equity * (price / entry_price) if in_position else equity
|
||||||
equity_curve.append(mtm)
|
equity_curve.append(mtm)
|
||||||
@@ -112,7 +112,7 @@ def main() -> None:
|
|||||||
# MACD output is a (macd, signal, histogram) tuple/object across
|
# MACD output is a (macd, signal, histogram) tuple/object across
|
||||||
# bindings. The Python binding returns a namedtuple.
|
# bindings. The Python binding returns a namedtuple.
|
||||||
histogram = macd_out[2] if isinstance(macd_out, tuple) else macd_out.histogram
|
histogram = macd_out[2] if isinstance(macd_out, tuple) else macd_out.histogram
|
||||||
adx_value = adx_out[0] if isinstance(adx_out, tuple) else adx_out.adx
|
adx_value = adx_out[2] if isinstance(adx_out, tuple) else adx_out.adx
|
||||||
|
|
||||||
hist_sign = histogram > 0.0
|
hist_sign = histogram > 0.0
|
||||||
cross_up = prev_hist_sign is False and hist_sign
|
cross_up = prev_hist_sign is False and hist_sign
|
||||||
|
|||||||
@@ -49,3 +49,43 @@ print_equity <- function(name, r) {
|
|||||||
cat(sprintf("%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d\n",
|
cat(sprintf("%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d\n",
|
||||||
name, r$total_return_pct, r$sharpe, r$max_dd_pct, r$trades))
|
name, r$total_return_pct, r$sharpe, r$max_dd_pct, r$trades))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Loads one of the checked-in datasets under examples/data (the R examples run
|
||||||
|
# from this directory, so ../data is examples/data).
|
||||||
|
bundled_candles <- function(filename) {
|
||||||
|
load_ohlcv_csv(file.path("..", "data", filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints the per-trade backtest summary shared verbatim with the Rust, Python,
|
||||||
|
# Node, Go, C and C# example suites (same labels, same numbers).
|
||||||
|
print_summary <- function(name, first_price, last_price, bars, closed_trades, final_equity, equity_curve) {
|
||||||
|
buy_hold <- last_price / first_price
|
||||||
|
strat_return <- final_equity - 1
|
||||||
|
bh_return <- buy_hold - 1
|
||||||
|
n <- length(closed_trades)
|
||||||
|
wins <- sum(closed_trades > 0)
|
||||||
|
losses <- sum(closed_trades < 0)
|
||||||
|
best <- if (n > 0) max(closed_trades) else 0
|
||||||
|
worst <- if (n > 0) min(closed_trades) else 0
|
||||||
|
mean_r <- if (n > 0) mean(closed_trades) else 0
|
||||||
|
var_r <- if (n > 1) stats::var(closed_trades) else 0
|
||||||
|
sharpe <- if (var_r > 0) mean_r / sqrt(var_r) else 0
|
||||||
|
peak <- if (length(equity_curve) > 0) equity_curve[1] else 1
|
||||||
|
maxdd <- 0
|
||||||
|
for (eq in equity_curve) {
|
||||||
|
if (eq > peak) peak <- eq
|
||||||
|
dd <- (peak - eq) / peak
|
||||||
|
if (dd > maxdd) maxdd <- dd
|
||||||
|
}
|
||||||
|
cat(sprintf("=== %s ===\n", name))
|
||||||
|
cat(sprintf("%-23s%d\n", "Bars:", bars))
|
||||||
|
cat(sprintf("%-23s%d (W%d / L%d)\n", "Trades:", n, wins, losses))
|
||||||
|
cat(sprintf("%-23s%+.2f%%\n", "Strategy return:", strat_return * 100))
|
||||||
|
cat(sprintf("%-23s%+.2f%%\n", "Buy & Hold return:", bh_return * 100))
|
||||||
|
cat(sprintf("%-23s%+.2f%%\n", "Excess over BH:", (strat_return - bh_return) * 100))
|
||||||
|
cat(sprintf("%-23s%.2f%%\n", "Max drawdown:", maxdd * 100))
|
||||||
|
cat(sprintf("%-23s%.2f (mean %+.4f, stddev %.4f)\n", "Per-trade Sharpe:", sharpe, mean_r, sqrt(var_r)))
|
||||||
|
cat(sprintf("%-23s%+.2f%% / %+.2f%%\n", "Best / worst trade:", best * 100, worst * 100))
|
||||||
|
cat("\n")
|
||||||
|
cat("NOTE: Educational example — fees, slippage, funding costs and tax effects are simplified or omitted. Past performance is not indicative of future results.\n")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,69 @@
|
|||||||
# Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above
|
# Strategy example: Bollinger-squeeze breakout with an ATR(14) trailing stop.
|
||||||
# the upper band, go long with an ATR(14) trailing stop.
|
#
|
||||||
library(wickra)
|
# Enters long when Bollinger bandwidth makes a new SQUEEZE_LOOKBACK low (a
|
||||||
|
# volatility squeeze) and price closes above the upper band; exits on an ATR(14)
|
||||||
|
# trailing stop or when the upper band falls back below the entry. 0.1% fees per
|
||||||
|
# trade. The R counterpart of examples/python/strategy_bollinger_squeeze.py,
|
||||||
|
# printing the same summary. Uses the checked-in examples/data/btcusdt-1d.csv
|
||||||
|
# dataset (pass a CSV path to override).
|
||||||
|
suppressPackageStartupMessages(library(wickra))
|
||||||
source("_common.R")
|
source("_common.R")
|
||||||
|
|
||||||
|
FEE <- 0.001
|
||||||
|
ATR_STOP_MULT <- 2.0
|
||||||
|
SQUEEZE_LOOKBACK <- 180
|
||||||
|
|
||||||
args <- commandArgs(trailingOnly = TRUE)
|
args <- commandArgs(trailingOnly = TRUE)
|
||||||
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else synthetic_candles(2000)
|
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else bundled_candles("btcusdt-1d.csv")
|
||||||
|
|
||||||
|
opens <- bars$open; highs <- bars$high; lows <- bars$low
|
||||||
|
closes <- bars$close; vols <- bars$volume; ts <- bars$timestamp
|
||||||
|
n_bars <- length(closes)
|
||||||
|
|
||||||
bb <- BollingerBands(20, 2.0); atr <- Atr(14)
|
bb <- BollingerBands(20, 2.0); atr <- Atr(14)
|
||||||
returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0; stop <- 0
|
in_pos <- FALSE; entry_price <- 0; stop_level <- 0
|
||||||
for (i in seq_len(nrow(bars))) {
|
closed <- numeric(0); equity <- 1; equity_curve <- numeric(n_bars)
|
||||||
b <- bars[i, ]
|
bw_window <- numeric(0)
|
||||||
band <- update(bb, b$close)
|
|
||||||
atr_value <- update(atr, b$open, b$high, b$low, b$close, b$volume, b$timestamp)
|
for (i in seq_len(n_bars)) {
|
||||||
|
band <- update(bb, closes[i])
|
||||||
|
atr_value <- update(atr, opens[i], highs[i], lows[i], closes[i], vols[i], ts[i])
|
||||||
|
price <- closes[i]
|
||||||
|
equity_curve[i] <- if (in_pos) equity * (price / entry_price) else equity
|
||||||
if (is.na(band[["middle"]]) || !is.finite(atr_value)) next
|
if (is.na(band[["middle"]]) || !is.finite(atr_value)) next
|
||||||
bandwidth <- if (band[["middle"]] != 0) (band[["upper"]] - band[["lower"]]) / band[["middle"]] else .Machine$double.xmax
|
|
||||||
if (!in_pos && bandwidth < 0.06 && b$close > band[["upper"]]) {
|
middle <- band[["middle"]]
|
||||||
in_pos <- TRUE; entry <- b$close; stop <- b$close - 2 * atr_value; trades <- trades + 1L
|
if (abs(middle) <= 1e-12) next
|
||||||
} else if (in_pos) {
|
upper <- band[["upper"]]; lower <- band[["lower"]]
|
||||||
stop <- max(stop, b$close - 2 * atr_value)
|
bandwidth <- (upper - lower) / middle
|
||||||
if (b$close < stop) { returns <- c(returns, (b$close - entry) / entry); in_pos <- FALSE }
|
bw_window <- c(bw_window, bandwidth)
|
||||||
|
if (length(bw_window) > SQUEEZE_LOOKBACK) {
|
||||||
|
bw_window <- bw_window[(length(bw_window) - SQUEEZE_LOOKBACK + 1):length(bw_window)]
|
||||||
|
}
|
||||||
|
if (length(bw_window) < SQUEEZE_LOOKBACK) next
|
||||||
|
min_bw <- min(bw_window)
|
||||||
|
|
||||||
|
if (in_pos) {
|
||||||
|
if (price < stop_level || upper < entry_price) {
|
||||||
|
trade_ret <- price / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
in_pos <- FALSE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
is_new_low <- abs(bandwidth - min_bw) < 1e-12
|
||||||
|
if (is_new_low && price > upper) {
|
||||||
|
entry_price <- price; stop_level <- price - ATR_STOP_MULT * atr_value
|
||||||
|
equity <- equity * (1 - FEE); in_pos <- TRUE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print_equity("Bollinger squeeze", summarize_equity(returns, trades))
|
|
||||||
|
if (in_pos) {
|
||||||
|
trade_ret <- closes[n_bars] / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary("Bollinger Squeeze Breakout (1d, BTCUSDT)",
|
||||||
|
closes[1], closes[n_bars], n_bars, closed, equity, equity_curve)
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
# Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
# Strategy example: MACD crossover with ADX trend-strength filter.
|
||||||
# confirms a trend; exit when the histogram crosses back below zero.
|
#
|
||||||
library(wickra)
|
# Enters long on a MACD histogram cross up (the histogram turns positive) while
|
||||||
|
# ADX(14) > 20 (a directional market); exits on the opposite MACD crossover
|
||||||
|
# regardless of ADX. 0.1% fees per trade. The R counterpart of
|
||||||
|
# examples/python/strategy_macd_adx.py, printing the same summary. Uses the
|
||||||
|
# checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to override).
|
||||||
|
suppressPackageStartupMessages(library(wickra))
|
||||||
source("_common.R")
|
source("_common.R")
|
||||||
|
|
||||||
|
FEE <- 0.001
|
||||||
|
ADX_FLOOR <- 20
|
||||||
|
|
||||||
args <- commandArgs(trailingOnly = TRUE)
|
args <- commandArgs(trailingOnly = TRUE)
|
||||||
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else synthetic_candles(2000)
|
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else bundled_candles("btcusdt-1h.csv")
|
||||||
|
|
||||||
|
opens <- bars$open; highs <- bars$high; lows <- bars$low
|
||||||
|
closes <- bars$close; vols <- bars$volume; ts <- bars$timestamp
|
||||||
|
n_bars <- length(closes)
|
||||||
|
|
||||||
macd <- MacdIndicator(12, 26, 9); adx <- Adx(14)
|
macd <- MacdIndicator(12, 26, 9); adx <- Adx(14)
|
||||||
returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0; prev_hist <- NA_real_
|
in_pos <- FALSE; entry_price <- 0; closed <- numeric(0); equity <- 1
|
||||||
for (i in seq_len(nrow(bars))) {
|
equity_curve <- numeric(n_bars); have_prev <- FALSE; prev_sign <- FALSE
|
||||||
b <- bars[i, ]
|
|
||||||
m <- update(macd, b$close)
|
for (i in seq_len(n_bars)) {
|
||||||
a <- update(adx, b$open, b$high, b$low, b$close, b$volume, b$timestamp)
|
m <- update(macd, closes[i])
|
||||||
|
a <- update(adx, opens[i], highs[i], lows[i], closes[i], vols[i], ts[i])
|
||||||
|
price <- closes[i]
|
||||||
|
equity_curve[i] <- if (in_pos) equity * (price / entry_price) else equity
|
||||||
if (is.na(m[["macd"]]) || is.na(a[["adx"]])) next
|
if (is.na(m[["macd"]]) || is.na(a[["adx"]])) next
|
||||||
trending <- a[["adx"]] > 20
|
|
||||||
if (!in_pos && trending && is.finite(prev_hist) && prev_hist <= 0 && m[["histogram"]] > 0) {
|
hist_sign <- m[["histogram"]] > 0
|
||||||
in_pos <- TRUE; entry <- b$close; trades <- trades + 1L
|
cross_up <- have_prev && !prev_sign && hist_sign
|
||||||
} else if (in_pos && m[["histogram"]] < 0) {
|
cross_down <- have_prev && prev_sign && !hist_sign
|
||||||
returns <- c(returns, (b$close - entry) / entry); in_pos <- FALSE
|
have_prev <- TRUE; prev_sign <- hist_sign
|
||||||
|
|
||||||
|
if (!in_pos && cross_up && a[["adx"]] > ADX_FLOOR) {
|
||||||
|
entry_price <- price; equity <- equity * (1 - FEE); in_pos <- TRUE
|
||||||
|
} else if (in_pos && cross_down) {
|
||||||
|
trade_ret <- price / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
in_pos <- FALSE
|
||||||
}
|
}
|
||||||
prev_hist <- m[["histogram"]]
|
|
||||||
}
|
}
|
||||||
print_equity("MACD + ADX trend", summarize_equity(returns, trades))
|
|
||||||
|
if (in_pos) {
|
||||||
|
trade_ret <- closes[n_bars] / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary("MACD + ADX Trend Filter (1h, BTCUSDT)",
|
||||||
|
closes[1], closes[n_bars], n_bars, closed, equity, equity_curve)
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
# Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
|
# Strategy example: RSI(14) mean-reversion.
|
||||||
library(wickra)
|
#
|
||||||
|
# Go long when RSI(14) drops below 30 (oversold), exit when it recovers above 70
|
||||||
|
# (overbought). 0.1% fees per trade. The R counterpart of
|
||||||
|
# examples/python/strategy_rsi_mean_reversion.py, printing the same summary. Uses
|
||||||
|
# the checked-in examples/data/btcusdt-1h.csv dataset (pass a CSV path to override).
|
||||||
|
suppressPackageStartupMessages(library(wickra))
|
||||||
source("_common.R")
|
source("_common.R")
|
||||||
|
|
||||||
|
FEE <- 0.001
|
||||||
|
OVERSOLD <- 30
|
||||||
|
OVERBOUGHT <- 70
|
||||||
|
|
||||||
args <- commandArgs(trailingOnly = TRUE)
|
args <- commandArgs(trailingOnly = TRUE)
|
||||||
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else synthetic_candles(2000)
|
bars <- if (length(args) >= 1) load_ohlcv_csv(args[1]) else bundled_candles("btcusdt-1h.csv")
|
||||||
|
|
||||||
|
closes <- bars$close
|
||||||
|
n_bars <- length(closes)
|
||||||
|
|
||||||
rsi <- Rsi(14)
|
rsi <- Rsi(14)
|
||||||
returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0
|
in_pos <- FALSE; entry_price <- 0; closed <- numeric(0); equity <- 1
|
||||||
for (i in seq_len(nrow(bars))) {
|
equity_curve <- numeric(n_bars)
|
||||||
cl <- bars$close[i]
|
|
||||||
value <- update(rsi, cl)
|
for (i in seq_len(n_bars)) {
|
||||||
|
value <- update(rsi, closes[i])
|
||||||
|
price <- closes[i]
|
||||||
|
equity_curve[i] <- if (in_pos) equity * (price / entry_price) else equity
|
||||||
if (!is.finite(value)) next
|
if (!is.finite(value)) next
|
||||||
if (!in_pos && value < 30) {
|
|
||||||
in_pos <- TRUE; entry <- cl; trades <- trades + 1L
|
if (!in_pos && value < OVERSOLD) {
|
||||||
} else if (in_pos && value > 50) {
|
entry_price <- price; equity <- equity * (1 - FEE); in_pos <- TRUE
|
||||||
returns <- c(returns, (cl - entry) / entry); in_pos <- FALSE
|
} else if (in_pos && value > OVERBOUGHT) {
|
||||||
|
trade_ret <- price / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
in_pos <- FALSE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print_equity("RSI mean-reversion", summarize_equity(returns, trades))
|
|
||||||
|
if (in_pos) {
|
||||||
|
trade_ret <- closes[n_bars] / entry_price - 1
|
||||||
|
closed <- c(closed, trade_ret)
|
||||||
|
equity <- equity * (1 + trade_ret) * (1 - FEE)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary("RSI Mean-Reversion (1h, BTCUSDT)",
|
||||||
|
closes[1], closes[n_bars], n_bars, closed, equity, equity_curve)
|
||||||
|
|||||||
Reference in New Issue
Block a user