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.");
+ }
}
diff --git a/examples/java/src/main/java/org/wickra/examples/MarketData.java b/examples/java/src/main/java/org/wickra/examples/MarketData.java
index 1b84e8bf..58953919 100644
--- a/examples/java/src/main/java/org/wickra/examples/MarketData.java
+++ b/examples/java/src/main/java/org/wickra/examples/MarketData.java
@@ -68,4 +68,12 @@ public final class MarketData {
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());
+ }
}
diff --git a/examples/java/src/main/java/org/wickra/examples/StrategyBollingerSqueeze.java b/examples/java/src/main/java/org/wickra/examples/StrategyBollingerSqueeze.java
index bed0caa7..3a7218bc 100644
--- a/examples/java/src/main/java/org/wickra/examples/StrategyBollingerSqueeze.java
+++ b/examples/java/src/main/java/org/wickra/examples/StrategyBollingerSqueeze.java
@@ -5,52 +5,96 @@ import org.wickra.BollingerBands;
import org.wickra.BollingerOutput;
import org.wickra.examples.MarketData.Bar;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
/**
- * Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above the
- * upper band, go long with an ATR(14) trailing stop.
+ * Strategy example: Bollinger-squeeze breakout 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 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 {
+ 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 {
- 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);
Atr atr = new Atr(14)) {
- List returns = new ArrayList<>();
- int trades = 0;
boolean inPosition = false;
- double entry = 0.0;
- double stop = 0.0;
+ double entryPrice = 0.0;
+ double stopLevel = 0.0;
+ List closedTrades = new ArrayList<>();
+ double equity = 1.0;
+ List equityCurve = new ArrayList<>();
+ Deque bwWindow = new ArrayDeque<>();
for (Bar b : bars) {
BollingerOutput band = bollinger.update(b.close());
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)) {
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
- ? (band.upper() - band.lower()) / band.middle()
- : Double.MAX_VALUE;
-
- if (!inPosition && bandwidth < 0.06 && b.close() > band.upper()) {
- 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);
+ if (inPosition) {
+ if (price < stopLevel || upper < entryPrice) {
+ double tradeRet = price / entryPrice - 1.0;
+ closedTrades.add(tradeRet);
+ equity *= (1.0 + tradeRet) * (1.0 - FEE);
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);
}
}
}
diff --git a/examples/java/src/main/java/org/wickra/examples/StrategyMacdAdx.java b/examples/java/src/main/java/org/wickra/examples/StrategyMacdAdx.java
index 43f6c50c..77d500cd 100644
--- a/examples/java/src/main/java/org/wickra/examples/StrategyMacdAdx.java
+++ b/examples/java/src/main/java/org/wickra/examples/StrategyMacdAdx.java
@@ -10,44 +10,69 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
- * confirms a trend; exit when the histogram crosses back below zero.
+ * Strategy example: MACD crossover with ADX trend-strength filter.
+ *
+ * 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 {
+ private static final double FEE = 0.001;
+ private static final double ADX_FLOOR = 20.0;
+
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);
Adx adx = new Adx(14)) {
- List returns = new ArrayList<>();
- int trades = 0;
boolean inPosition = false;
- double entry = 0.0;
- double prevHistogram = Double.NaN;
+ double entryPrice = 0.0;
+ List closedTrades = new ArrayList<>();
+ double equity = 1.0;
+ List equityCurve = new ArrayList<>();
+ boolean havePrev = false;
+ boolean prevSign = false;
for (Bar b : bars) {
MacdOutput m = macd.update(b.close());
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) {
continue;
}
- boolean trending = a.adx() > 20.0;
- if (!inPosition && trending && Double.isFinite(prevHistogram)
- && prevHistogram <= 0.0 && m.histogram() > 0.0) {
+ boolean histSign = m.histogram() > 0.0;
+ boolean crossUp = havePrev && !prevSign && histSign;
+ boolean crossDown = havePrev && prevSign && !histSign;
+ havePrev = true;
+ prevSign = histSign;
+
+ if (!inPosition && crossUp && a.adx() > ADX_FLOOR) {
+ entryPrice = price;
+ equity *= 1.0 - FEE;
inPosition = true;
- entry = b.close();
- trades++;
- } else if (inPosition && m.histogram() < 0.0) {
- returns.add((b.close() - entry) / entry);
+ } else if (inPosition && crossDown) {
+ double tradeRet = price / entryPrice - 1.0;
+ closedTrades.add(tradeRet);
+ equity *= (1.0 + tradeRet) * (1.0 - FEE);
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);
}
}
}
diff --git a/examples/java/src/main/java/org/wickra/examples/StrategyRsiMeanReversion.java b/examples/java/src/main/java/org/wickra/examples/StrategyRsiMeanReversion.java
index c9c85d75..01deba0b 100644
--- a/examples/java/src/main/java/org/wickra/examples/StrategyRsiMeanReversion.java
+++ b/examples/java/src/main/java/org/wickra/examples/StrategyRsiMeanReversion.java
@@ -6,33 +6,58 @@ import org.wickra.examples.MarketData.Bar;
import java.util.ArrayList;
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.
+ *
+ * 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 {
+ 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 {
- 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)) {
- List returns = new ArrayList<>();
- int trades = 0;
boolean inPosition = false;
- double entry = 0.0;
+ double entryPrice = 0.0;
+ List closedTrades = new ArrayList<>();
+ double equity = 1.0;
+ List equityCurve = new ArrayList<>();
for (Bar b : bars) {
double value = rsi.update(b.close());
+ double price = b.close();
+ equityCurve.add(inPosition ? equity * (price / entryPrice) : equity);
if (!Double.isFinite(value)) {
continue;
}
- if (!inPosition && value < 30.0) {
+
+ if (!inPosition && value < OVERSOLD) {
+ entryPrice = price;
+ equity *= 1.0 - FEE;
inPosition = true;
- entry = b.close();
- trades++;
- } else if (inPosition && value > 50.0) {
- returns.add((b.close() - entry) / entry);
+ } else if (inPosition && value > OVERBOUGHT) {
+ double tradeRet = price / entryPrice - 1.0;
+ closedTrades.add(tradeRet);
+ equity *= (1.0 + tradeRet) * (1.0 - FEE);
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);
}
}
}
diff --git a/examples/python/strategy_bollinger_squeeze.py b/examples/python/strategy_bollinger_squeeze.py
index 8374a831..b976b7df 100644
--- a/examples/python/strategy_bollinger_squeeze.py
+++ b/examples/python/strategy_bollinger_squeeze.py
@@ -115,7 +115,7 @@ def main() -> None:
for c in candles:
bb_out = bb.update(c["close"])
- atr_val = atr.update(c["high"], c["low"], c["close"])
+ atr_val = atr.update(c)
price = c["close"]
mtm = equity * (price / entry_price) if in_position else equity
equity_curve.append(mtm)
diff --git a/examples/python/strategy_macd_adx.py b/examples/python/strategy_macd_adx.py
index 18185b12..4880a1a0 100644
--- a/examples/python/strategy_macd_adx.py
+++ b/examples/python/strategy_macd_adx.py
@@ -101,7 +101,7 @@ def main() -> None:
for c in candles:
macd_out = macd.update(c["close"])
- adx_out = adx.update(c["high"], c["low"], c["close"])
+ adx_out = adx.update(c)
price = c["close"]
mtm = equity * (price / entry_price) if in_position else equity
equity_curve.append(mtm)
@@ -112,7 +112,7 @@ def main() -> None:
# MACD output is a (macd, signal, histogram) tuple/object across
# bindings. The Python binding returns a namedtuple.
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
cross_up = prev_hist_sign is False and hist_sign
diff --git a/examples/r/_common.R b/examples/r/_common.R
index fd4805ee..af5f4190 100644
--- a/examples/r/_common.R
+++ b/examples/r/_common.R
@@ -49,3 +49,43 @@ print_equity <- function(name, r) {
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))
}
+
+# 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")
+}
diff --git a/examples/r/strategy_bollinger_squeeze.R b/examples/r/strategy_bollinger_squeeze.R
index f3742809..8e977118 100644
--- a/examples/r/strategy_bollinger_squeeze.R
+++ b/examples/r/strategy_bollinger_squeeze.R
@@ -1,24 +1,69 @@
-# Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above
-# the upper band, go long with an ATR(14) trailing stop.
-library(wickra)
+# Strategy example: Bollinger-squeeze breakout 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 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")
+FEE <- 0.001
+ATR_STOP_MULT <- 2.0
+SQUEEZE_LOOKBACK <- 180
+
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)
-returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0; stop <- 0
-for (i in seq_len(nrow(bars))) {
- b <- bars[i, ]
- band <- update(bb, b$close)
- atr_value <- update(atr, b$open, b$high, b$low, b$close, b$volume, b$timestamp)
+in_pos <- FALSE; entry_price <- 0; stop_level <- 0
+closed <- numeric(0); equity <- 1; equity_curve <- numeric(n_bars)
+bw_window <- numeric(0)
+
+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
- 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"]]) {
- in_pos <- TRUE; entry <- b$close; stop <- b$close - 2 * atr_value; trades <- trades + 1L
- } else if (in_pos) {
- stop <- max(stop, b$close - 2 * atr_value)
- if (b$close < stop) { returns <- c(returns, (b$close - entry) / entry); in_pos <- FALSE }
+
+ middle <- band[["middle"]]
+ if (abs(middle) <= 1e-12) next
+ upper <- band[["upper"]]; lower <- band[["lower"]]
+ bandwidth <- (upper - lower) / middle
+ 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)
diff --git a/examples/r/strategy_macd_adx.R b/examples/r/strategy_macd_adx.R
index 5ab56949..fd8e6a55 100644
--- a/examples/r/strategy_macd_adx.R
+++ b/examples/r/strategy_macd_adx.R
@@ -1,24 +1,54 @@
-# Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
-# confirms a trend; exit when the histogram crosses back below zero.
-library(wickra)
+# Strategy example: MACD crossover with ADX trend-strength filter.
+#
+# 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")
+FEE <- 0.001
+ADX_FLOOR <- 20
+
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)
-returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0; prev_hist <- NA_real_
-for (i in seq_len(nrow(bars))) {
- b <- bars[i, ]
- m <- update(macd, b$close)
- a <- update(adx, b$open, b$high, b$low, b$close, b$volume, b$timestamp)
+in_pos <- FALSE; entry_price <- 0; closed <- numeric(0); equity <- 1
+equity_curve <- numeric(n_bars); have_prev <- FALSE; prev_sign <- FALSE
+
+for (i in seq_len(n_bars)) {
+ 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
- trending <- a[["adx"]] > 20
- if (!in_pos && trending && is.finite(prev_hist) && prev_hist <= 0 && m[["histogram"]] > 0) {
- in_pos <- TRUE; entry <- b$close; trades <- trades + 1L
- } else if (in_pos && m[["histogram"]] < 0) {
- returns <- c(returns, (b$close - entry) / entry); in_pos <- FALSE
+
+ hist_sign <- m[["histogram"]] > 0
+ cross_up <- have_prev && !prev_sign && hist_sign
+ cross_down <- have_prev && prev_sign && !hist_sign
+ 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)
diff --git a/examples/r/strategy_rsi_mean_reversion.R b/examples/r/strategy_rsi_mean_reversion.R
index d0a944db..68032232 100644
--- a/examples/r/strategy_rsi_mean_reversion.R
+++ b/examples/r/strategy_rsi_mean_reversion.R
@@ -1,20 +1,47 @@
-# Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
-library(wickra)
+# 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 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")
+FEE <- 0.001
+OVERSOLD <- 30
+OVERBOUGHT <- 70
+
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)
-returns <- numeric(0); trades <- 0L; in_pos <- FALSE; entry <- 0
-for (i in seq_len(nrow(bars))) {
- cl <- bars$close[i]
- value <- update(rsi, cl)
+in_pos <- FALSE; entry_price <- 0; closed <- numeric(0); equity <- 1
+equity_curve <- numeric(n_bars)
+
+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 (!in_pos && value < 30) {
- in_pos <- TRUE; entry <- cl; trades <- trades + 1L
- } else if (in_pos && value > 50) {
- returns <- c(returns, (cl - entry) / entry); in_pos <- FALSE
+
+ if (!in_pos && value < OVERSOLD) {
+ entry_price <- price; equity <- equity * (1 - FEE); in_pos <- TRUE
+ } 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)