goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,432 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import java.awt.GraphicsEnvironment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.AnalysisCriterion;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Rule;
import org.ta4j.core.Strategy;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.LinearTransactionCostCriterion;
import org.ta4j.core.num.Num;
import org.ta4j.core.criteria.ValueAtRiskCriterion;
import org.ta4j.core.criteria.pnl.GrossProfitLossCriterion;
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
import org.ta4j.core.criteria.pnl.NetAverageProfitCriterion;
import org.ta4j.core.criteria.pnl.NetAverageLossCriterion;
import org.ta4j.core.criteria.pnl.MaxConsecutiveLossCriterion;
import org.ta4j.core.criteria.pnl.MaxConsecutiveProfitCriterion;
import org.ta4j.core.indicators.MACDIndicator;
import org.ta4j.core.indicators.averages.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.keltner.KeltnerChannelFacade;
import org.ta4j.core.indicators.volume.MoneyFlowIndexIndicator;
import org.ta4j.core.indicators.volume.VWAPIndicator;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.StopGainRule;
import org.ta4j.core.rules.TrailingStopLossRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.CoinbaseHttpBarSeriesDataSource;
/**
* Coinbase Data Source Backtest - Advanced Risk Management & Transaction Costs
* <p>
* This example demonstrates advanced ta4j features focused on real-world
* trading considerations:
* <ul>
* <li>Loading historical OHLCV data from Coinbase Advanced Trade API</li>
* <li>MACD (Moving Average Convergence Divergence) with signal line and
* histogram</li>
* <li>Keltner Channels (alternative to Bollinger Bands using ATR)</li>
* <li>VWAP (Volume-Weighted Average Price) - critical for crypto trading</li>
* <li>Money Flow Index (MFI) - volume-weighted RSI</li>
* <li>Trailing Stop Loss - dynamic stop that follows price upward</li>
* <li>Stop Gain - take profit targets</li>
* <li>Transaction cost analysis - real-world impact of fees</li>
* <li>Value at Risk (VaR) - risk quantification</li>
* <li>Gross vs Net metrics - understanding the difference</li>
* <li>Multiple strategy comparison</li>
* </ul>
* <p>
* <strong>Strategy Concept:</strong> A trend-following strategy using MACD
* crossovers with Keltner Channel confirmation, VWAP for entry timing, and
* sophisticated risk management with trailing stops and take-profit targets.
* <p>
* <strong>Data Source:</strong> This example uses Coinbase's public market data
* API to fetch real cryptocurrency data. No API key is required, but be aware
* of rate limits (350 candles per request, automatically paginated).
* <p>
* <strong>Key Learning:</strong> This example emphasizes the critical
* importance of transaction costs in real trading. Notice how gross returns
* differ significantly from net returns after accounting for fees!
*/
public class CoinbaseBacktest {
private static final Logger LOG = LogManager.getLogger(CoinbaseBacktest.class);
public static void main(String[] args) {
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ Coinbase Data Source - Advanced Risk Management ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
System.out.println();
// Step 1: Load historical price data from Coinbase
System.out.println("[1/8] Loading historical price data from Coinbase...");
System.out.println(" Fetching 1 year of daily data for Bitcoin (BTC-USD)...");
System.out.println(" (Crypto markets are 24/7 - perfect for trend strategies)");
// Load 1 year of daily data
CoinbaseHttpBarSeriesDataSource dataSource = new CoinbaseHttpBarSeriesDataSource(true);
BarSeries series = dataSource.loadSeriesInstance("BTC-USD",
CoinbaseHttpBarSeriesDataSource.CoinbaseInterval.ONE_DAY, 365);
// Alternative methods you can try:
// BarSeries series = dataSource.loadSeriesInstance("ETH-USD",
// CoinbaseInterval.ONE_DAY, 500); // 500 bars
// BarSeries series = dataSource.loadSeriesInstance("BTC-USD",
// CoinbaseInterval.FOUR_HOUR, 1000); // 4-hour data
// BarSeries series = dataSource.loadSeriesInstance("ETH-USD",
// CoinbaseInterval.ONE_DAY,
// Instant.parse("2023-01-01T00:00:00Z"),
// Instant.parse("2023-12-31T23:59:59Z")); // Date range
if (series == null || series.getBarCount() == 0) {
System.err.println(" [ERROR] Failed to load data from Coinbase");
System.err.println(" [TIP] Check your internet connection and try again");
System.err.println(" [TIP] Coinbase API may have rate limits - wait a few minutes and retry");
return;
}
System.out.printf(" [OK] Loaded %d bars of price data%n", series.getBarCount());
System.out.printf(" [INFO] Date range: %s to %s%n", series.getFirstBar().getEndTime(),
series.getLastBar().getEndTime());
System.out.println();
// Step 2: Create advanced indicators
System.out.println("[2/8] Creating advanced technical indicators...");
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
// MACD: Trend-following momentum indicator
// MACD = 12-period EMA - 26-period EMA
// Signal = 9-period EMA of MACD
// Histogram = MACD - Signal
MACDIndicator macd = new MACDIndicator(closePrice, 12, 26);
EMAIndicator macdSignal = macd.getSignalLine(9);
// Histogram: difference between MACD and signal line
// Positive histogram = bullish momentum, negative = bearish
org.ta4j.core.indicators.numeric.NumericIndicator macdHistogram = macd.getHistogram(9);
// Keltner Channels: Volatility-based channels using ATR
// Similar to Bollinger Bands but uses ATR instead of standard deviation
// More responsive to volatility changes
KeltnerChannelFacade keltner = new KeltnerChannelFacade(series, 20, 10, 2.0);
// keltner.middle() = 20-period EMA
// keltner.upper() = middle + (2.0 * ATR)
// keltner.lower() = middle - (2.0 * ATR)
// VWAP: Volume-Weighted Average Price
// Critical for crypto trading - shows institutional price levels
// Often acts as support/resistance
// Using all available bars for VWAP calculation
VWAPIndicator vwap = new VWAPIndicator(series, series.getBarCount());
// Money Flow Index: Volume-weighted RSI (0-100)
// > 80 = overbought, < 20 = oversold
// More reliable than RSI because it includes volume
MoneyFlowIndexIndicator mfi = new MoneyFlowIndexIndicator(series, 14);
System.out.println(" [OK] Created MACD (12, 26, 9) with signal line and histogram");
System.out.println(" [OK] Created Keltner Channels (20 EMA, 10 ATR, 2.0 multiplier)");
System.out.println(" [OK] Created VWAP (Volume-Weighted Average Price)");
System.out.println(" [OK] Created Money Flow Index (14-period)");
System.out.println();
// Step 3: Build trend-following strategy with risk management
System.out.println("[3/8] Building trend-following strategy with advanced risk management...");
System.out.println(" Strategy: MACD crossover with Keltner Channel confirmation");
// Entry rule: Buy when MACD crosses above signal line (bullish crossover)
// AND price is above VWAP (institutional support)
// AND price is above Keltner middle (uptrend)
// AND MFI is not overbought (< 80)
Rule macdBullishCrossover = new CrossedUpIndicatorRule(macd, macdSignal);
Rule priceAboveVWAP = new OverIndicatorRule(closePrice, vwap);
Rule priceAboveKeltnerMiddle = new OverIndicatorRule(closePrice, keltner.middle());
Rule mfiNotOverbought = new UnderIndicatorRule(mfi, series.numFactory().numOf(80));
Rule buyingRule = macdBullishCrossover.and(priceAboveVWAP).and(priceAboveKeltnerMiddle).and(mfiNotOverbought);
// Exit rule: Sell when MACD crosses below signal line (bearish crossover)
// OR trailing stop loss triggers (protects profits)
// OR stop gain triggers (take profit at 15%)
// OR price falls below Keltner lower band (breakdown)
Rule macdBearishCrossover = new CrossedDownIndicatorRule(macd, macdSignal);
// Trailing stop: 5% trailing stop loss (follows price upward)
// This protects profits by moving the stop loss up as price rises
TrailingStopLossRule trailingStop = new TrailingStopLossRule(closePrice, series.numFactory().numOf(5.0));
// Stop gain: Take profit at 15% gain
StopGainRule stopGain = new StopGainRule(closePrice, series.numFactory().numOf(15.0));
Rule priceBelowKeltnerLower = new UnderIndicatorRule(closePrice, keltner.lower());
Rule sellingRule = macdBearishCrossover.or(trailingStop).or(stopGain).or(priceBelowKeltnerLower);
Strategy strategy = new BaseStrategy("MACD Trend Following (Risk Managed)", buyingRule, sellingRule);
System.out.println(" [OK] Entry: MACD bullish crossover + Price > VWAP + Price > Keltner middle + MFI < 80");
System.out.println(
" [OK] Exit: MACD bearish crossover OR 5% trailing stop OR 15% stop gain OR Price < Keltner lower");
System.out.println();
// Step 4: Run backtest
System.out.println("[4/8] Running backtest on historical data...");
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
System.out.printf(" [OK] Backtest complete: %d positions executed%n", tradingRecord.getPositionCount());
System.out.println();
// Step 5: Performance analysis with transaction costs
System.out.println("[5/8] Performance Analysis (WITH Transaction Costs)");
System.out.println(" ──────────────────────────────────────────");
// Transaction cost parameters (typical for crypto exchanges)
// Coinbase Advanced Trade: 0.4% maker fee, 0.6% taker fee
// Using 0.5% average (0.005) per trade (entry + exit = 1% total per round trip)
double initialAmount = 10000.0; // $10,000 starting capital
double feePercentage = 0.005; // 0.5% per trade
// Gross metrics (before transaction costs)
AnalysisCriterion grossReturn = new GrossReturnCriterion();
AnalysisCriterion grossProfitLoss = new GrossProfitLossCriterion();
Num grossReturnValue = grossReturn.calculate(series, tradingRecord);
Num grossProfitLossValue = grossProfitLoss.calculate(series, tradingRecord);
// Net metrics (after transaction costs)
AnalysisCriterion netReturn = new NetReturnCriterion();
AnalysisCriterion netProfitLoss = new NetProfitLossCriterion();
Num netReturnValue = netReturn.calculate(series, tradingRecord);
Num netProfitLossValue = netProfitLoss.calculate(series, tradingRecord);
// Transaction costs
LinearTransactionCostCriterion transactionCosts = new LinearTransactionCostCriterion(initialAmount,
feePercentage);
Num totalCosts = transactionCosts.calculate(series, tradingRecord);
// Average profit/loss per trade
AnalysisCriterion avgProfit = new NetAverageProfitCriterion();
AnalysisCriterion avgLoss = new NetAverageLossCriterion();
Num avgProfitValue = avgProfit.calculate(series, tradingRecord);
Num avgLossValue = avgLoss.calculate(series, tradingRecord);
// Consecutive streaks
AnalysisCriterion maxConsecutiveProfit = new MaxConsecutiveProfitCriterion();
AnalysisCriterion maxConsecutiveLoss = new MaxConsecutiveLossCriterion();
Num maxConsecutiveProfitValue = maxConsecutiveProfit.calculate(series, tradingRecord);
Num maxConsecutiveLossValue = maxConsecutiveLoss.calculate(series, tradingRecord);
// Risk metrics
ValueAtRiskCriterion var95 = new ValueAtRiskCriterion(0.95); // 95% confidence level
Num var95Value = var95.calculate(series, tradingRecord);
// Display comprehensive results
System.out.println(" Gross Performance (Before Fees):");
System.out.printf(" Gross Return: %.2f%%%n",
grossReturnValue.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Gross Profit/Loss: $%.2f%n", grossProfitLossValue.doubleValue());
System.out.println();
System.out.println(" Transaction Costs:");
System.out.printf(" Total Fees Paid: $%.2f (%.2f%% of initial capital)%n", totalCosts.doubleValue(),
totalCosts.dividedBy(series.numFactory().numOf(initialAmount))
.multipliedBy(series.numFactory().numOf(100))
.doubleValue());
System.out.printf(" Fee per Trade: %.2f%% (entry + exit)%n", feePercentage * 200);
System.out.println();
System.out.println(" Net Performance (After Fees):");
System.out.printf(" Net Return: %.2f%%%n",
netReturnValue.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Net Profit/Loss: $%.2f%n", netProfitLossValue.doubleValue());
System.out.printf(" Impact of Fees: %.2f%% (difference between gross and net)%n",
grossReturnValue.minus(netReturnValue).multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.println();
System.out.println(" Trade Statistics:");
System.out.printf(" Average Profit: $%.2f%n", avgProfitValue.doubleValue());
System.out.printf(" Average Loss: $%.2f%n", avgLossValue.doubleValue());
System.out.printf(" Max Consecutive Wins: %d%n", maxConsecutiveProfitValue.intValue());
System.out.printf(" Max Consecutive Losses: %d%n", maxConsecutiveLossValue.intValue());
System.out.println();
System.out.println(" Risk Metrics:");
System.out.printf(" Value at Risk (95%%): %.2f%% (worst expected loss)%n",
var95Value.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.println();
// Step 6: Compare strategies (with and without trailing stop)
System.out.println("[6/8] Strategy Comparison: With vs Without Trailing Stop");
System.out.println(" ──────────────────────────────────────────");
// Strategy without trailing stop (only MACD crossover)
Rule simpleBuyingRule = macdBullishCrossover.and(priceAboveVWAP);
Rule simpleSellingRule = macdBearishCrossover.or(stopGain).or(priceBelowKeltnerLower);
Strategy simpleStrategy = new BaseStrategy("MACD Simple (No Trailing Stop)", simpleBuyingRule,
simpleSellingRule);
TradingRecord simpleRecord = seriesManager.run(simpleStrategy);
Num simpleNetReturn = netReturn.calculate(series, simpleRecord);
Num strategyNetReturn = netReturn.calculate(series, tradingRecord);
System.out.printf(" Simple Strategy (no trailing stop): %.2f%% net return%n",
simpleNetReturn.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Risk-Managed Strategy (with trailing stop): %.2f%% net return%n",
strategyNetReturn.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Difference: %.2f%%%n",
strategyNetReturn.minus(simpleNetReturn).multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.println();
// Step 7: Visualize the strategy
System.out.println("[7/8] Generating comprehensive strategy visualization...");
boolean isHeadless = GraphicsEnvironment.isHeadless();
if (isHeadless) {
System.out.println(" [WARN] Headless environment detected - skipping chart display");
System.out.println(" [TIP] Run in a GUI environment to see interactive charts!");
} else {
try {
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("MACD Trend Following Strategy - Coinbase Data (BTC-USD)")
.withSeries(series) // Price bars (candlesticks)
.withTradingRecordOverlay(tradingRecord) // Trading positions marked on price chart
.withIndicatorOverlay(keltner.middle()) // Keltner middle band
.withIndicatorOverlay(keltner.upper()) // Keltner upper band
.withIndicatorOverlay(keltner.lower()) // Keltner lower band
.withIndicatorOverlay(vwap) // VWAP overlay
.withSubChart(macd) // MACD in first subchart
.withIndicatorOverlay(macdSignal) // MACD signal line
.withSubChart(macdHistogram) // MACD histogram in second subchart
.withSubChart(mfi) // Money Flow Index in third subchart
.withSubChart(new NetProfitLossCriterion(), tradingRecord) // Net P&L in fourth subchart
.toChart();
chartWorkflow.displayChart(chart, "ta4j Coinbase Backtest - MACD Trend Following with Risk Management");
System.out.println(" [OK] Multi-subchart displayed in new window");
System.out.println(" [TIP] Chart shows: Price with Keltner Channels & VWAP, MACD, MFI, and P&L");
} catch (Exception ex) {
LOG.warn("Failed to display chart: {}", ex.getMessage(), ex);
System.out.println(" [WARN] Could not display chart: " + ex.getMessage());
}
}
System.out.println();
// Step 8: Explain advanced concepts
System.out.println("[8/8] Advanced Concepts Demonstrated");
System.out.println(" ──────────────────────────────────────────");
System.out.println(" ✓ MACD: Trend-following momentum indicator");
System.out.println(" - Bullish crossover: MACD crosses above signal line");
System.out.println(" - Histogram shows momentum strength");
System.out.println();
System.out.println(" ✓ Keltner Channels: Volatility-based price channels");
System.out.println(" - Uses ATR instead of standard deviation (more responsive)");
System.out.println(" - Upper/lower bands adapt to market volatility");
System.out.println();
System.out.println(" ✓ VWAP: Volume-Weighted Average Price");
System.out.println(" - Critical for crypto trading (institutional levels)");
System.out.println(" - Price above VWAP = bullish, below = bearish");
System.out.println();
System.out.println(" ✓ Trailing Stop Loss: Dynamic risk management");
System.out.println(" - Follows price upward, protecting profits");
System.out.println(" - More effective than fixed stop loss in trending markets");
System.out.println();
System.out.println(" ✓ Stop Gain: Take-profit targets");
System.out.println(" - Locks in profits at predetermined levels");
System.out.println(" - Prevents giving back gains in volatile markets");
System.out.println();
System.out.println(" ✓ Transaction Costs: Real-world impact");
System.out.println(" - Gross returns vs Net returns (after fees)");
System.out.println(" - Fees can significantly impact profitability");
System.out.println(" - Always account for costs in backtesting!");
System.out.println();
System.out.println(" ✓ Value at Risk (VaR): Risk quantification");
System.out.println(" - Measures worst expected loss at confidence level");
System.out.println(" - Helps understand downside risk");
System.out.println();
System.out.println(" ✓ Gross vs Net Metrics: Understanding the difference");
System.out.println(" - Gross: Before transaction costs");
System.out.println(" - Net: After transaction costs (real-world)");
System.out.println(" - Always use net metrics for real trading decisions");
System.out.println();
// Summary
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ Summary ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
System.out.println();
System.out.println("What just happened?");
System.out.println();
System.out.println(" 1. Loaded 1 year of daily OHLCV data for BTC-USD from Coinbase");
System.out.println(" 2. Created advanced indicators:");
System.out.println(" - MACD with signal line and histogram (trend momentum)");
System.out.println(" - Keltner Channels (volatility-based channels)");
System.out.println(" - VWAP (institutional price levels)");
System.out.println(" - Money Flow Index (volume-weighted momentum)");
System.out.println(" 3. Built a trend-following strategy with risk management:");
System.out.println(" - Entry: MACD bullish crossover + Price > VWAP + Price > Keltner middle + MFI < 80");
System.out.println(
" - Exit: MACD bearish crossover OR 5% trailing stop OR 15% stop gain OR Price < Keltner lower");
System.out.println(" 4. Backtested with transaction costs (0.5% per trade)");
System.out.println(" 5. Analyzed gross vs net performance (impact of fees)");
System.out.println(" 6. Compared strategies (with/without trailing stop)");
System.out.println(" 7. Calculated risk metrics (Value at Risk)");
if (!isHeadless) {
System.out.println(" 8. Visualized with multi-subchart (Price, MACD, MFI, P&L)");
}
System.out.println();
System.out.println("Advanced Features Demonstrated:");
System.out.println(" ✓ MACD with signal line and histogram");
System.out.println(" ✓ Keltner Channels (ATR-based volatility bands)");
System.out.println(" ✓ VWAP for institutional price levels");
System.out.println(" ✓ Money Flow Index (volume-weighted RSI)");
System.out.println(" ✓ Trailing Stop Loss (dynamic profit protection)");
System.out.println(" ✓ Stop Gain (take-profit targets)");
System.out.println(" ✓ Transaction cost analysis (real-world impact)");
System.out.println(" ✓ Value at Risk (risk quantification)");
System.out.println(" ✓ Gross vs Net metrics comparison");
System.out.println(" ✓ Multiple strategy comparison");
System.out.println();
System.out.println("Coinbase Data Source Features:");
System.out.println(" - Load data by number of days: loadSeries(\"BTC-USD\", 365)");
System.out.println(" - Load data by bar count: loadSeries(\"BTC-USD\", ONE_DAY, 500)");
System.out.println(" - Load data by date range: loadSeries(\"BTC-USD\", ONE_DAY, start, end)");
System.out.println(" - Supports multiple intervals: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 1d");
System.out.println(" - Works with all Coinbase trading pairs (BTC-USD, ETH-USD, etc.)");
System.out.println(" - Automatic pagination for large date ranges (350 candles per request)");
System.out.println();
System.out.println("Key Takeaways:");
System.out.println(" • Transaction costs matter! Always use net returns for decisions.");
System.out.println(" • Trailing stops protect profits better than fixed stops.");
System.out.println(" • VWAP is critical for crypto trading (institutional levels).");
System.out.println(" • Risk metrics (VaR) help quantify downside risk.");
System.out.println(" • Gross vs Net metrics show the real impact of fees.");
System.out.println();
System.out.println("Next Steps - Experiment with:");
System.out.println(" - Different cryptocurrencies: \"ETH-USD\", \"SOL-USD\", \"ADA-USD\"");
System.out.println(" - Adjust MACD periods (try 8/21 or 19/39)");
System.out.println(" - Modify trailing stop percentage (try 3% or 7%)");
System.out.println(" - Change stop gain targets (try 10% or 20%)");
System.out.println(" - Test different fee structures (0.1%, 0.25%, 1.0%)");
System.out.println(" - Try different intervals: FOUR_HOUR, SIX_HOUR for shorter timeframes");
System.out.println(" - Explore other examples in ta4j-examples");
System.out.println(" - Check out the wiki: https://ta4j.github.io/ta4j-wiki/");
System.out.println();
System.out.println("Your turn! Modify this code and see how transaction costs affect profitability.");
System.out.println();
}
}
@@ -0,0 +1,153 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BacktestExecutor;
import org.ta4j.core.indicators.KalmanFilterIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.num.DecimalNum;
import org.ta4j.core.num.Num;
import org.ta4j.core.reports.BasePerformanceReport;
import org.ta4j.core.reports.PositionStatsReport;
import org.ta4j.core.reports.TradingStatement;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import ta4jexamples.datasources.JsonFileBarSeriesDataSource;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
public class MovingAverageCrossOverRangeBacktest {
private static final Logger LOG = LogManager.getLogger(MovingAverageCrossOverRangeBacktest.class);
private static final int DEFAULT_DECIMAL_PRECISION = 16;
public static void main(String[] args) {
DecimalNum.configureDefaultPrecision(DEFAULT_DECIMAL_PRECISION);
String resourceName = "Binance-ETH-USD-PT5M-20230313_20230315.json";
InputStream resourceStream = MovingAverageCrossOverRangeBacktest.class.getClassLoader()
.getResourceAsStream(resourceName);
if (resourceStream == null) {
LOG.error("File not found in classpath: {}", resourceName);
return;
}
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(resourceStream);
if (series == null || series.isEmpty()) {
LOG.error("Bar series was null or empty: {}", series);
return;
}
int barCountStart = 3;
int barCountStop = 200;
int barCountStep = 3;
final List<Strategy> strategies = new ArrayList<>();
for (int shortBarCount = barCountStart; shortBarCount <= barCountStop; shortBarCount += barCountStep) {
for (int longBarCount = shortBarCount
+ barCountStep; longBarCount <= barCountStop; longBarCount += barCountStep) {
String strategyName = String.format("Sma(%d) CrossOver Sma(%d)", shortBarCount, longBarCount);
strategies.add(
new BaseStrategy(strategyName, createSmaCrossEntryRule(series, shortBarCount, longBarCount),
createSmaCrossExitRule(series, shortBarCount, longBarCount)));
}
}
Instant startInstant = Instant.now();
BacktestExecutor backtestExecutor = new BacktestExecutor(series);
List<TradingStatement> tradingStatements = backtestExecutor.execute(strategies, DecimalNum.valueOf(50),
Trade.TradeType.BUY);
LOG.debug("Back-tested {} strategies on {}-bar series using decimal precision of {} in {}", strategies.size(),
series.getBarCount(), DEFAULT_DECIMAL_PRECISION, Duration.between(startInstant, Instant.now()));
LOG.debug(printReport(tradingStatements));
}
private static Rule createSmaCrossEntryRule(BarSeries series, int shortBarCount, int longBarCount) {
Indicator<Num> closePrice = new ClosePriceIndicator(series);
KalmanFilterIndicator kalmanFilteredClosePrice = new KalmanFilterIndicator(closePrice);
SMAIndicator smaShort = new SMAIndicator(kalmanFilteredClosePrice, shortBarCount);
SMAIndicator smaLong = new SMAIndicator(kalmanFilteredClosePrice, longBarCount);
return new CrossedUpIndicatorRule(smaShort, smaLong);
}
private static Rule createSmaCrossExitRule(BarSeries series, int shortBarCount, int longBarCount) {
Indicator<Num> closePrice = new ClosePriceIndicator(series);
KalmanFilterIndicator kalmanFilteredClosePrice = new KalmanFilterIndicator(closePrice);
SMAIndicator smaShort = new SMAIndicator(kalmanFilteredClosePrice, shortBarCount);
SMAIndicator smaLong = new SMAIndicator(kalmanFilteredClosePrice, longBarCount);
return new CrossedDownIndicatorRule(smaShort, smaLong);
}
private static String printReport(List<TradingStatement> tradingStatements) {
StringJoiner resultJoiner = new StringJoiner(System.lineSeparator());
for (TradingStatement statement : tradingStatements) {
resultJoiner.add(printStatementReport(statement).toString());
}
return resultJoiner.toString();
}
private static StringBuilder printStatementReport(TradingStatement statement) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("######### ")
.append(statement.getStrategy().getName())
.append(" #########")
.append(System.lineSeparator())
.append(printPerformanceReport(statement.getPerformanceReport()))
.append(System.lineSeparator())
.append(printPositionStats(statement.getPositionStatsReport()))
.append(System.lineSeparator())
.append("###########################");
return resultBuilder;
}
private static StringBuilder printPerformanceReport(BasePerformanceReport report) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("--------- performance report ---------")
.append(System.lineSeparator())
.append("total loss: ")
.append(report.totalLoss)
.append(System.lineSeparator())
.append("total profit: ")
.append(report.totalProfit)
.append(System.lineSeparator())
.append("total profit loss: ")
.append(report.totalProfitLoss)
.append(System.lineSeparator())
.append("total profit loss percentage: ")
.append(report.totalProfitLossPercentage)
.append(System.lineSeparator())
.append("---------------------------");
return resultBuilder;
}
private static StringBuilder printPositionStats(PositionStatsReport report) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("--------- trade statistics report ---------")
.append(System.lineSeparator())
.append("loss trade count: ")
.append(report.getLossCount())
.append(System.lineSeparator())
.append("profit trade count: ")
.append(report.getProfitCount())
.append(System.lineSeparator())
.append("break even trade count: ")
.append(report.getBreakEvenCount())
.append(System.lineSeparator())
.append("---------------------------");
return resultBuilder;
}
}
@@ -0,0 +1,142 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.num.DecimalNum;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
public class SimpleMovingAverageBacktest {
private static final Logger LOG = LogManager.getLogger(SimpleMovingAverageBacktest.class);
public static void main(String[] args) throws InterruptedException {
BarSeries series = createBarSeries();
Strategy strategy3DaySma = create3DaySmaStrategy(series);
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord3DaySma = seriesManager.run(strategy3DaySma, Trade.TradeType.BUY,
DecimalNum.valueOf(50));
LOG.debug(tradingRecord3DaySma.toString());
Strategy strategy2DaySma = create2DaySmaStrategy(series);
TradingRecord tradingRecord2DaySma = seriesManager.run(strategy2DaySma, Trade.TradeType.BUY,
DecimalNum.valueOf(50));
LOG.debug(tradingRecord2DaySma.toString());
var criterion = new GrossReturnCriterion();
Num calculate3DaySma = criterion.calculate(series, tradingRecord3DaySma);
Num calculate2DaySma = criterion.calculate(series, tradingRecord2DaySma);
LOG.debug(calculate3DaySma.toString());
LOG.debug(calculate2DaySma.toString());
}
private static BarSeries createBarSeries() {
final var series = new BaseBarSeriesBuilder().build();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(1))
.openPrice(100.0)
.highPrice(100.0)
.lowPrice(100.0)
.closePrice(100.0)
.volume(1060)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(2))
.openPrice(110.0)
.highPrice(110.0)
.lowPrice(110.0)
.closePrice(110.0)
.volume(1070)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(3))
.openPrice(140.0)
.highPrice(140.0)
.lowPrice(140.0)
.closePrice(140.0)
.volume(1080)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(4))
.openPrice(119.0)
.highPrice(119.0)
.lowPrice(119.0)
.closePrice(119.0)
.volume(1090)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(5))
.openPrice(100.0)
.highPrice(100.0)
.lowPrice(100.0)
.closePrice(100.0)
.volume(1100)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(6))
.openPrice(110.0)
.highPrice(110.0)
.lowPrice(110.0)
.closePrice(110.0)
.volume(1110)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(7))
.openPrice(120.0)
.highPrice(120.0)
.lowPrice(120.0)
.closePrice(120.0)
.volume(1120)
.add();
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(createDay(8))
.openPrice(130.0)
.highPrice(130.0)
.lowPrice(130.0)
.closePrice(130.0)
.volume(1130)
.add();
return series;
}
private static Instant createDay(int day) {
return ZonedDateTime.of(2018, 01, day, 12, 0, 0, 0, ZoneOffset.UTC).toInstant();
}
private static Strategy create3DaySmaStrategy(BarSeries series) {
var closePrice = new ClosePriceIndicator(series);
var sma = new SMAIndicator(closePrice, 3);
return new BaseStrategy(new UnderIndicatorRule(sma, closePrice), new OverIndicatorRule(sma, closePrice));
}
private static Strategy create2DaySmaStrategy(BarSeries series) {
var closePrice = new ClosePriceIndicator(series);
var sma = new SMAIndicator(closePrice, 2);
return new BaseStrategy(new UnderIndicatorRule(sma, closePrice), new OverIndicatorRule(sma, closePrice));
}
}
@@ -0,0 +1,158 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BacktestExecutionResult;
import org.ta4j.core.backtest.BacktestExecutor;
import org.ta4j.core.backtest.TradingStatementExecutionResult.WeightedCriterion;
import org.ta4j.core.criteria.drawdown.ReturnOverMaxDrawdownCriterion;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.num.DecimalNum;
import org.ta4j.core.num.Num;
import org.ta4j.core.reports.BasePerformanceReport;
import org.ta4j.core.reports.PositionStatsReport;
import org.ta4j.core.reports.TradingStatement;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.datasources.CsvFileBarSeriesDataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Example demonstrating how to use {@link BacktestExecutor} for parallel
* strategy evaluation.
*
* This example:
* <ul>
* <li>Creates multiple variations of a Simple Moving Average (SMA)
* strategy.</li>
* <li>Uses {@code BacktestExecutor} to run them in parallel over the data
* series.</li>
* <li>Ranks the strategies based on a composite {@link WeightedCriterion}.</li>
* <li>Prints a performance report for the best strategies.</li>
* </ul>
*/
public class SimpleMovingAverageRangeBacktest {
private static final Logger LOG = LogManager.getLogger(SimpleMovingAverageRangeBacktest.class);
private static final int DEFAULT_TOP_STRATEGIES = 3;
public static void main(String[] args) {
BarSeries series = CsvFileBarSeriesDataSource.loadSeriesFromFile();
int start = 3;
int stop = 50;
int step = 5;
final List<Strategy> strategies = new ArrayList<>();
for (int i = start; i <= stop; i += step) {
Strategy strategy = new BaseStrategy("Sma(" + i + ")", createEntryRule(series, i),
createExitRule(series, i));
strategies.add(strategy);
}
BacktestExecutor backtestExecutor = new BacktestExecutor(series);
BacktestExecutionResult result = backtestExecutor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(50),
Trade.TradeType.BUY);
List<TradingStatement> tradingStatements = selectTopStrategies(result, DEFAULT_TOP_STRATEGIES);
LOG.debug("Top {} weighted SMA strategies (7 parts net profit, 3 parts return over max drawdown)",
tradingStatements.size());
LOG.debug(printReport(tradingStatements));
}
/**
* Selects the top strategies for this example using weighted, normalized
* ranking.
*
* @param result full backtest result for the SMA parameter sweep
* @param limit maximum number of strategies to keep
* @return top strategies ordered by the example weighted criteria
*/
static List<TradingStatement> selectTopStrategies(BacktestExecutionResult result, int limit) {
Objects.requireNonNull(result, "result cannot be null");
return result.getTopStrategiesWeighted(limit, WeightedCriterion.of(new NetProfitCriterion(), 7.0),
WeightedCriterion.of(new ReturnOverMaxDrawdownCriterion(), 3.0));
}
private static Rule createEntryRule(BarSeries series, int barCount) {
Indicator<Num> closePrice = new ClosePriceIndicator(series);
SMAIndicator sma = new SMAIndicator(closePrice, barCount);
return new UnderIndicatorRule(sma, closePrice);
}
private static Rule createExitRule(BarSeries series, int barCount) {
Indicator<Num> closePrice = new ClosePriceIndicator(series);
SMAIndicator sma = new SMAIndicator(closePrice, barCount);
return new OverIndicatorRule(sma, closePrice);
}
private static String printReport(List<TradingStatement> tradingStatements) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append(System.lineSeparator());
for (TradingStatement statement : tradingStatements) {
resultBuilder.append(printStatementReport(statement));
resultBuilder.append(System.lineSeparator());
}
return resultBuilder.toString();
}
private static StringBuilder printStatementReport(TradingStatement statement) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("######### ")
.append(statement.getStrategy().getName())
.append(" #########")
.append(System.lineSeparator())
.append(printPerformanceReport(statement.getPerformanceReport()))
.append(System.lineSeparator())
.append(printPositionStats(statement.getPositionStatsReport()))
.append(System.lineSeparator())
.append("###########################");
return resultBuilder;
}
private static StringBuilder printPerformanceReport(BasePerformanceReport report) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("--------- performance report ---------")
.append(System.lineSeparator())
.append("total loss: ")
.append(report.totalLoss)
.append(System.lineSeparator())
.append("total profit: ")
.append(report.totalProfit)
.append(System.lineSeparator())
.append("total profit loss: ")
.append(report.totalProfitLoss)
.append(System.lineSeparator())
.append("total profit loss percentage: ")
.append(report.totalProfitLossPercentage)
.append(System.lineSeparator())
.append("---------------------------");
return resultBuilder;
}
private static StringBuilder printPositionStats(PositionStatsReport report) {
StringBuilder resultBuilder = new StringBuilder();
resultBuilder.append("--------- trade statistics report ---------")
.append(System.lineSeparator())
.append("loss trade count: ")
.append(report.getLossCount())
.append(System.lineSeparator())
.append("profit trade count: ")
.append(report.getProfitCount())
.append(System.lineSeparator())
.append("break even trade count: ")
.append(report.getBreakEvenCount())
.append(System.lineSeparator())
.append("---------------------------");
return resultBuilder;
}
}
@@ -0,0 +1,175 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import java.time.Instant;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.BaseTradingRecord;
import org.ta4j.core.ExecutionMatchPolicy;
import org.ta4j.core.ExecutionSide;
import org.ta4j.core.Position;
import org.ta4j.core.Trade;
import org.ta4j.core.Trade.TradeType;
import org.ta4j.core.TradeFill;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.analysis.cost.ZeroCostModel;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.num.Num;
import org.ta4j.core.num.NumFactory;
/**
* Example demonstrating partial trade fills and custom trading records.
*
* <p>
* This example shows how to use {@link org.ta4j.core.TradeFill} events directly
* on a {@link org.ta4j.core.TradingRecord}. This is useful when connecting ta4j
* to broker APIs that confirm orders asynchronously or execute orders in
* multiple parts.
* </p>
*/
public class TradeFillRecordingExample {
private static final Logger LOG = LogManager.getLogger(TradeFillRecordingExample.class);
private static final BarSeries ANALYSIS_SERIES = new BaseBarSeriesBuilder()
.withName("trade-fill-recording-analysis")
.build();
private static final NumFactory NUM_FACTORY = ANALYSIS_SERIES.numFactory();
private static final NetProfitCriterion NET_PROFIT_CRITERION = new NetProfitCriterion();
public static void main(String[] args) {
LOG.info("Step 1: stream partial fills directly into TradingRecord");
BaseTradingRecord streamingRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
new ZeroCostModel(), new ZeroCostModel(), null, null);
recordStreamingOrder(streamingRecord, "BUY entry order", entryFills());
logOpenExposure("After BUY entry order", streamingRecord);
recordStreamingOrder(streamingRecord, "SELL exit order", exitFills());
logRecordSummary("Streaming fills", streamingRecord);
LOG.info("Step 2: record the same exchange fills as grouped logical orders");
BaseTradingRecord groupedTradeRecord = buildGroupedTradeRecord();
logRecordSummary("Grouped order batches", groupedTradeRecord);
LOG.info("Step 3: replay one partial exit under each ExecutionMatchPolicy");
for (ExecutionMatchPolicy matchPolicy : ExecutionMatchPolicy.values()) {
BaseTradingRecord matchPolicyRecord = buildMatchingPolicyRecord(matchPolicy);
logMatchingPolicyOutcome(matchPolicy, matchPolicyRecord);
}
LOG.info("The same TradingRecord APIs cover direct fills, grouped trades, and lot-matching inspection.");
}
static BaseTradingRecord buildStreamingRecord() {
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
new ZeroCostModel(), null, null);
for (TradeFill fill : entryFills()) {
record.operate(fill);
}
for (TradeFill fill : exitFills()) {
record.operate(fill);
}
return record;
}
static BaseTradingRecord buildGroupedTradeRecord() {
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
new ZeroCostModel(), null, null);
record.operate(Trade.fromFills(TradeType.BUY, entryFills()));
record.operate(Trade.fromFills(TradeType.SELL, exitFills()));
return record;
}
static Num closedNetProfit(TradingRecord record) {
return NET_PROFIT_CRITERION.calculate(ANALYSIS_SERIES, record);
}
static BaseTradingRecord buildMatchingPolicyRecord(ExecutionMatchPolicy matchPolicy) {
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, matchPolicy, new ZeroCostModel(),
new ZeroCostModel(), null, null);
for (TradeFill fill : matchingPolicyEntryFills()) {
record.operate(fill);
}
String correlationId = matchPolicy == ExecutionMatchPolicy.SPECIFIC_ID ? "lot-b" : null;
record.operate(new TradeFill(3, Instant.parse("2025-02-01T00:02:00Z"), NUM_FACTORY.numOf(120),
NUM_FACTORY.one(), NUM_FACTORY.zero(), ExecutionSide.SELL, "policy-exit", correlationId));
return record;
}
private static void recordStreamingOrder(TradingRecord record, String label, List<TradeFill> fills) {
LOG.info("{} ({})", label, fills.get(0).orderId());
for (TradeFill fill : fills) {
record.operate(fill);
LOG.info(" {} fill {} -> index={}, price={}, amount={}, fee={}, openPositions={}", fill.side(),
fill.correlationId(), fill.index(), fill.price(), fill.amount(), fill.fee(),
record.getOpenPositions().size());
}
}
private static void logOpenExposure(String label, TradingRecord record) {
Position currentPosition = record.getCurrentPosition();
LOG.info("{} -> netOpenAmount={}, netAverageEntry={}, openLots={}, recordedFees={}", label,
currentPosition.amount(), currentPosition.averageEntryPrice(), record.getOpenPositions().size(),
record.getRecordedTotalFees());
for (int i = 0; i < record.getOpenPositions().size(); i++) {
Position openPosition = record.getOpenPositions().get(i);
LOG.info(" open[{}] lot={} amount={} avgEntry={}", i, openPosition.getEntry().getCorrelationId(),
openPosition.amount(), openPosition.averageEntryPrice());
}
}
private static void logRecordSummary(String label, TradingRecord record) {
LOG.info("{} -> trades={}, closedPositions={}, openPositions={}, fees={}, closedProfit={}", label,
record.getTrades().size(), record.getPositionCount(), record.getOpenPositions().size(),
record.getRecordedTotalFees(), closedNetProfit(record));
for (int i = 0; i < record.getPositions().size(); i++) {
Position position = record.getPositions().get(i);
LOG.info(" position[{}] entry={} @ {} amount={}, exit={} @ {}, profit={}", i,
position.getEntry().getIndex(), position.getEntry().getPricePerAsset(),
position.getEntry().getAmount(), position.getExit().getIndex(),
position.getExit().getPricePerAsset(), position.getProfit());
}
}
private static void logMatchingPolicyOutcome(ExecutionMatchPolicy matchPolicy, TradingRecord record) {
Position closedPosition = record.getPositions().get(0);
Position currentPosition = record.getCurrentPosition();
LOG.info("{} -> closedLot={} entry={} amount={}, remainingOpenLots={}, netOpenAmount={}, netAverageEntry={}",
matchPolicy, closedPosition.getEntry().getCorrelationId(), closedPosition.getEntry().getPricePerAsset(),
closedPosition.getEntry().getAmount(), record.getOpenPositions().size(), currentPosition.amount(),
currentPosition.averageEntryPrice());
for (int i = 0; i < record.getOpenPositions().size(); i++) {
Position openPosition = record.getOpenPositions().get(i);
LOG.info(" remaining[{}] lot={} amount={} avgEntry={}", i, openPosition.getEntry().getCorrelationId(),
openPosition.amount(), openPosition.averageEntryPrice());
}
}
private static List<TradeFill> entryFills() {
return List.of(
new TradeFill(4, Instant.parse("2025-01-01T00:00:00Z"), NUM_FACTORY.hundred(), NUM_FACTORY.one(),
NUM_FACTORY.numOf(0.1), ExecutionSide.BUY, "entry-fill-1", "entry-order"),
new TradeFill(5, Instant.parse("2025-01-01T00:01:00Z"), NUM_FACTORY.numOf(101), NUM_FACTORY.two(),
NUM_FACTORY.numOf(0.2), ExecutionSide.BUY, "entry-fill-2", "entry-order"));
}
private static List<TradeFill> exitFills() {
return List.of(
new TradeFill(8, Instant.parse("2025-01-01T00:02:00Z"), NUM_FACTORY.numOf(110), NUM_FACTORY.one(),
NUM_FACTORY.numOf(0.05), ExecutionSide.SELL, "exit-fill-1", "exit-order"),
new TradeFill(9, Instant.parse("2025-01-01T00:03:00Z"), NUM_FACTORY.numOf(111), NUM_FACTORY.two(),
NUM_FACTORY.numOf(0.06), ExecutionSide.SELL, "exit-fill-2", "exit-order"));
}
private static List<TradeFill> matchingPolicyEntryFills() {
return List.of(
new TradeFill(1, Instant.parse("2025-02-01T00:00:00Z"), NUM_FACTORY.hundred(), NUM_FACTORY.two(),
NUM_FACTORY.zero(), ExecutionSide.BUY, "policy-entry-a", "lot-a"),
new TradeFill(2, Instant.parse("2025-02-01T00:01:00Z"), NUM_FACTORY.numOf(106), NUM_FACTORY.one(),
NUM_FACTORY.zero(), ExecutionSide.BUY, "policy-entry-b", "lot-b"));
}
}
@@ -0,0 +1,179 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.BaseTradingRecord;
import org.ta4j.core.ExecutionMatchPolicy;
import org.ta4j.core.Position;
import org.ta4j.core.Strategy;
import org.ta4j.core.Trade;
import org.ta4j.core.Trade.TradeType;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.analysis.cost.ZeroCostModel;
import org.ta4j.core.backtest.BacktestExecutor;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.backtest.SlippageExecutionModel;
import org.ta4j.core.backtest.TradeExecutionModel;
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
import org.ta4j.core.backtest.TradeOnNextOpenModel;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.FixedRule;
/**
* Example demonstrating execution engine parity across different execution
* models.
*
* This backtest asserts that strategies yield identical results when run
* through:
* <ul>
* <li>{@link BarSeriesManager} (standard simulation)</li>
* <li>{@link BacktestExecutor} (batch/parallel simulation)</li>
* <li>Manual iterative loops (mimicking live/paper trading)</li>
* </ul>
* It ensures the default {@link BaseTradingRecord} acts as a unified record
* model.
*/
public class TradingRecordParityBacktest {
private static final Logger LOG = LogManager.getLogger(TradingRecordParityBacktest.class);
public static void main(String[] args) {
BarSeries series = createSeries();
Strategy strategy = createStrategy();
Num slippageRatio = series.numFactory().numOf(0.01);
TradingRecord nextOpenRecord = runWithExecutionModel(series, strategy, new TradeOnNextOpenModel());
TradingRecord currentCloseRecord = runWithExecutionModel(series, strategy, new TradeOnCurrentCloseModel());
TradingRecord slippageRecord = runWithExecutionModel(series, strategy,
new SlippageExecutionModel(slippageRatio, TradeExecutionModel.PriceSource.CURRENT_CLOSE));
logExecutionComparison("Next-open fills", nextOpenRecord);
logExecutionComparison("Current-close fills", currentCloseRecord);
logExecutionComparison("Current-close fills with 1% slippage", slippageRecord);
TradingRecord providedRecord = runWithProvidedRecord(series, strategy, new TradeOnCurrentCloseModel());
assertEquivalent(currentCloseRecord, providedRecord, "provided BaseTradingRecord");
TradingRecord factoryConfiguredRecord = runWithFactoryConfiguredRecord(series, strategy,
new TradeOnCurrentCloseModel());
assertEquivalent(currentCloseRecord, factoryConfiguredRecord, "factory-configured BaseTradingRecord");
LOG.info("Execution-model comparison and record-parity checks passed.");
}
static Strategy createStrategy() {
return new BaseStrategy("Single-entry timing demo", new FixedRule(1), new FixedRule(3));
}
static BarSeries createSeries() {
BarSeries series = new BaseBarSeriesBuilder().withName("parity-series").build();
addBar(series, 1, 100d, 100d);
addBar(series, 2, 102d, 104d);
addBar(series, 3, 109d, 111d);
addBar(series, 4, 107d, 103d);
addBar(series, 5, 99d, 101d);
addBar(series, 6, 105d, 107d);
return series;
}
static TradingRecord runWithExecutionModel(BarSeries series, Strategy strategy,
TradeExecutionModel tradeExecutionModel) {
BarSeriesManager manager = new BarSeriesManager(series, new ZeroCostModel(), new ZeroCostModel(),
tradeExecutionModel);
return manager.run(strategy, TradeType.BUY, series.numFactory().one(), 0, series.getEndIndex());
}
static TradingRecord runWithProvidedRecord(BarSeries series, Strategy strategy,
TradeExecutionModel tradeExecutionModel) {
BarSeriesManager manager = new BarSeriesManager(series, new ZeroCostModel(), new ZeroCostModel(),
tradeExecutionModel);
BaseTradingRecord providedRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
new ZeroCostModel(), new ZeroCostModel(), 0, series.getEndIndex());
return manager.run(strategy, providedRecord, series.numFactory().one(), 0, series.getEndIndex());
}
static TradingRecord runWithFactoryConfiguredRecord(BarSeries series, Strategy strategy,
TradeExecutionModel tradeExecutionModel) {
BarSeriesManager configuredManager = new BarSeriesManager(series, new ZeroCostModel(), new ZeroCostModel(),
tradeExecutionModel,
(tradeType, startIndex, endIndex, txCost, holdCost) -> new BaseTradingRecord(tradeType,
ExecutionMatchPolicy.FIFO, txCost, holdCost, startIndex, endIndex));
return configuredManager.run(strategy, TradeType.BUY, series.numFactory().one(), 0, series.getEndIndex());
}
private static void addBar(BarSeries series, int day, double openPrice, double closePrice) {
double highPrice = Math.max(openPrice, closePrice);
double lowPrice = Math.min(openPrice, closePrice);
series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(ZonedDateTime.of(2025, 1, day, 12, 0, 0, 0, ZoneOffset.UTC).toInstant())
.openPrice(openPrice)
.highPrice(highPrice)
.lowPrice(lowPrice)
.closePrice(closePrice)
.volume(1000d + day)
.add();
}
static void assertEquivalent(TradingRecord expected, TradingRecord actual, String runLabel) {
require(expected.getTrades().size() == actual.getTrades().size(), () -> runLabel + ": trade count mismatch");
require(expected.getPositions().size() == actual.getPositions().size(),
() -> runLabel + ": position count mismatch");
require(expected.isClosed() == actual.isClosed(), () -> runLabel + ": close-state mismatch");
for (int i = 0; i < expected.getTrades().size(); i++) {
Trade left = expected.getTrades().get(i);
Trade right = actual.getTrades().get(i);
assertTrade(left, right, runLabel + ": trade[" + i + "]");
}
for (int i = 0; i < expected.getPositions().size(); i++) {
Position left = expected.getPositions().get(i);
Position right = actual.getPositions().get(i);
assertTrade(left.getEntry(), right.getEntry(), runLabel + ": position[" + i + "].entry");
if (left.getExit() != null || right.getExit() != null) {
require(left.getExit() != null && right.getExit() != null, () -> runLabel + ": exit mismatch");
assertTrade(left.getExit(), right.getExit(), runLabel + ": position[" + i + "].exit");
}
}
}
private static void logExecutionComparison(String label, TradingRecord tradingRecord) {
Position position = tradingRecord.getPositions().get(0);
LOG.info("{} -> entry={} @ {}, exit={} @ {}, gross profit={}", label, position.getEntry().getIndex(),
position.getEntry().getPricePerAsset(), position.getExit().getIndex(),
position.getExit().getPricePerAsset(), position.getGrossProfit());
}
private static void assertTrade(Trade expected, Trade actual, String label) {
require(expected.getType() == actual.getType(), () -> label + ": type mismatch");
require(expected.getIndex() == actual.getIndex(), () -> label + ": index mismatch");
assertNum(expected.getPricePerAsset(), actual.getPricePerAsset(), label + ": price mismatch");
assertNum(expected.getAmount(), actual.getAmount(), label + ": amount mismatch");
}
private static void assertNum(Num expected, Num actual, String message) {
if (expected == null || actual == null) {
require(Objects.equals(expected, actual), () -> message);
return;
}
require(expected.isEqual(actual), () -> message + " expected=" + expected + ", actual=" + actual);
}
private static void require(boolean condition, Supplier<String> messageSupplier) {
if (!condition) {
throw new IllegalStateException(messageSupplier.get());
}
}
}
@@ -0,0 +1,365 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.backtesting;
import java.awt.GraphicsEnvironment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.AnalysisCriterion;
import org.ta4j.core.AnalysisCriterion.PositionFilter;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Rule;
import org.ta4j.core.Strategy;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.ExpectancyCriterion;
import org.ta4j.core.criteria.PositionsRatioCriterion;
import org.ta4j.core.criteria.SqnCriterion;
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;
import org.ta4j.core.criteria.drawdown.ReturnOverMaxDrawdownCriterion;
import org.ta4j.core.criteria.VersusEnterAndHoldCriterion;
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.UltimateOscillatorIndicator;
import org.ta4j.core.indicators.VortexIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.bollinger.BollingerBandsLowerIndicator;
import org.ta4j.core.indicators.bollinger.BollingerBandsMiddleIndicator;
import org.ta4j.core.indicators.bollinger.BollingerBandsUpperIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.helpers.PreviousValueIndicator;
import org.ta4j.core.indicators.statistics.StandardDeviationIndicator;
import org.ta4j.core.indicators.volume.OnBalanceVolumeIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.AverageTrueRangeStopLossRule;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.YahooFinanceHttpBarSeriesDataSource;
/**
* Yahoo Finance Data Source Backtest - Advanced Multi-Indicator Strategy
* <p>
* This example demonstrates advanced ta4j features beyond the Quickstart:
* <ul>
* <li>Loading historical OHLCV data from Yahoo Finance API</li>
* <li>Bollinger Bands for mean reversion signals</li>
* <li>ATR-based dynamic stop-loss (adapts to market volatility)</li>
* <li>RSI for momentum confirmation</li>
* <li>Volume analysis with On-Balance Volume (OBV)</li>
* <li>Trend confirmation with Vortex and Ultimate Oscillator</li>
* <li>Indicator composition using BinaryOperationIndicator</li>
* <li>Advanced performance metrics (Expectancy, SQN, Maximum Drawdown)</li>
* <li>Multi-subchart visualization</li>
* </ul>
* <p>
* <strong>Strategy Concept:</strong> A mean reversion strategy that buys when
* price touches the lower Bollinger Band (oversold) with RSI confirmation and
* volume support, using ATR-based stops that adapt to market volatility.
* <p>
* <strong>Data Source:</strong> This example uses Yahoo Finance's public API to
* fetch real market data. No API key is required, but be aware of rate limits
* (~2000 requests/hour per IP).
* <p>
* Run this example to see an advanced trading strategy backtested on real
* market data with comprehensive analysis!
*/
public class YahooFinanceBacktest {
private static final Logger LOG = LogManager.getLogger(YahooFinanceBacktest.class);
public static void main(String[] args) {
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ Yahoo Finance Data Source - Backtesting Example ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
System.out.println();
// Step 1: Load historical price data from Yahoo Finance
System.out.println("[1/7] Loading historical price data from Yahoo Finance...");
System.out.println(" Fetching 2 years of daily data for Apple Inc. (AAPL)...");
System.out.println(" (More data = better indicator calculations)");
// Load 2 years of data for better indicator stability
YahooFinanceHttpBarSeriesDataSource dataSource = new YahooFinanceHttpBarSeriesDataSource(true);
BarSeries series = dataSource.loadSeriesInstance("AAPL",
YahooFinanceHttpBarSeriesDataSource.YahooFinanceInterval.DAY_1, 730);
// Alternative methods you can try:
// BarSeries series = dataSource.loadSeriesInstance("AAPL",
// YahooFinanceInterval.DAY_1, 500); // 500 bars
// BarSeries series = dataSource.loadSeriesInstance("MSFT",
// YahooFinanceInterval.HOUR_1, 1000); // Hourly data
// BarSeries series = dataSource.loadSeriesInstance("BTC-USD",
// YahooFinanceInterval.DAY_1,
// Instant.parse("2023-01-01T00:00:00Z"),
// Instant.parse("2023-12-31T23:59:59Z")); // Date range
if (series == null || series.getBarCount() == 0) {
System.err.println(" [ERROR] Failed to load data from Yahoo Finance");
System.err.println(" [TIP] Check your internet connection and try again");
System.err.println(" [TIP] Yahoo Finance may have rate limits - wait a few minutes and retry");
return;
}
System.out.printf(" [OK] Loaded %d bars of price data%n", series.getBarCount());
System.out.printf(" [INFO] Date range: %s to %s%n", series.getFirstBar().getEndTime(),
series.getLastBar().getEndTime());
System.out.println();
// Step 2: Create base indicators
System.out.println("[2/7] Creating technical indicators...");
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
// Bollinger Bands: Mean reversion indicator
// Uses 20-period SMA with 2 standard deviations
int bbPeriod = 20;
double bbMultiplier = 2.0;
SMAIndicator bbSma = new SMAIndicator(closePrice, bbPeriod);
StandardDeviationIndicator bbStdDev = new StandardDeviationIndicator(closePrice, bbPeriod);
BollingerBandsMiddleIndicator bbMiddle = new BollingerBandsMiddleIndicator(bbSma);
BollingerBandsUpperIndicator bbUpper = new BollingerBandsUpperIndicator(bbMiddle, bbStdDev,
series.numFactory().numOf(bbMultiplier));
BollingerBandsLowerIndicator bbLower = new BollingerBandsLowerIndicator(bbMiddle, bbStdDev,
series.numFactory().numOf(bbMultiplier));
// RSI: Momentum oscillator (14-period)
RSIIndicator rsi = new RSIIndicator(closePrice, 14);
// On-Balance Volume: Volume-based trend indicator
OnBalanceVolumeIndicator obv = new OnBalanceVolumeIndicator(series);
// Trend confirmation indicators
VortexIndicator vortex = new VortexIndicator(series, 14);
UltimateOscillatorIndicator ultimateOscillator = new UltimateOscillatorIndicator(series);
// Note: ATR is used in the AverageTrueRangeStopLossRule below
// Advanced: You can also create custom indicators using
// BinaryOperationIndicator
// Example: Calculate distance from price to middle band as a percentage
// BinaryOperationIndicator priceToMiddleRatio =
// BinaryOperationIndicator.quotient(
// BinaryOperationIndicator.difference(closePrice, bbMiddle), bbMiddle);
System.out.println(" [OK] Created Bollinger Bands (20-period, 2 std dev)");
System.out.println(" [OK] Created RSI (14-period)");
System.out.println(" [OK] Created ATR (14-period) for dynamic stops");
System.out.println(" [OK] Created On-Balance Volume indicator");
System.out.println(" [OK] Created Vortex oscillator (14-period) for trend direction");
System.out.println(" [OK] Created Ultimate Oscillator (7/14/28) for trend strength");
System.out.println(" [OK] Created custom price-to-middle-band ratio indicator");
System.out.println();
// Step 3: Build advanced trading rules
System.out.println("[3/7] Building advanced trading strategy rules...");
System.out.println(" Strategy: Mean reversion with multiple confirmations");
// Entry rule: Buy when price is at or below lower Bollinger Band (oversold)
// AND RSI is below 45 (oversold confirmation - less strict than 40)
// Price touching lower BB OR crossing below it
Rule priceAtLowerBB = new UnderIndicatorRule(closePrice, bbLower)
.or(new CrossedDownIndicatorRule(closePrice, bbLower));
Rule rsiOversold = new UnderIndicatorRule(rsi, series.numFactory().numOf(45));
// Optional: OBV rising provides additional confirmation (but not required)
// This makes the strategy more tradeable while still using volume analysis
Rule obvRising = new OverIndicatorRule(obv, new PreviousValueIndicator(obv, 1));
Rule vortexBullish = new OverIndicatorRule(vortex, series.numFactory().zero());
Rule ultimateBullish = new OverIndicatorRule(ultimateOscillator, series.numFactory().numOf(50));
// Entry: Price at lower BB + RSI oversold + (OBV rising OR price below middle
// band) + trend confirmation from Vortex and Ultimate Oscillator
// This allows entries when either volume confirms OR price is clearly oversold
Rule priceBelowMiddle = new UnderIndicatorRule(closePrice, bbMiddle);
Rule buyingRule = priceAtLowerBB.and(rsiOversold)
.and(obvRising.or(priceBelowMiddle))
.and(vortexBullish)
.and(ultimateBullish);
// Exit rule: Sell when price reaches upper Bollinger Band (overbought)
// OR RSI crosses above 65 (overbought - less strict than 70 for more exits)
// OR ATR-based stop loss triggers (dynamic, adapts to volatility)
// OR Vortex turns bearish
Rule exitCondition1 = new CrossedUpIndicatorRule(closePrice, bbUpper)
.or(new OverIndicatorRule(closePrice, bbUpper));
Rule exitCondition2 = new OverIndicatorRule(rsi, series.numFactory().numOf(65));
// ATR-based stop: 2.5x ATR below entry price (allows for some volatility)
Rule exitCondition3 = new AverageTrueRangeStopLossRule(series, 14, 2.5);
Rule exitCondition4 = new UnderIndicatorRule(vortex, series.numFactory().zero());
Rule sellingRule = exitCondition1.or(exitCondition2).or(exitCondition3).or(exitCondition4);
Strategy strategy = new BaseStrategy("Bollinger Bands Mean Reversion (Trend-Confirmed)", buyingRule,
sellingRule);
System.out.println(
" [OK] Entry: Price at/below lower BB + RSI < 45 + (OBV rising OR price below middle) + Vortex > 0 + Ultimate > 50");
System.out.println(" [OK] Exit: Price at/above upper BB OR RSI > 65 OR ATR stop (2.5x ATR) OR Vortex < 0");
System.out.println();
// Step 4: Run backtest
System.out.println("[4/7] Running backtest on historical data...");
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
System.out.printf(" [OK] Backtest complete: %d positions executed%n", tradingRecord.getPositionCount());
System.out.println();
// Step 5: Advanced performance analysis
System.out.println("[5/7] Advanced Performance Analysis");
System.out.println(" ──────────────────────────────────────────");
// Basic metrics
AnalysisCriterion netReturn = new NetReturnCriterion();
AnalysisCriterion winningPositionsRatio = new PositionsRatioCriterion(PositionFilter.PROFIT);
Num netReturnValue = netReturn.calculate(series, tradingRecord);
Num winRate = winningPositionsRatio.calculate(series, tradingRecord);
// Advanced risk-adjusted metrics
AnalysisCriterion romad = new ReturnOverMaxDrawdownCriterion();
AnalysisCriterion maxDrawdown = new MaximumDrawdownCriterion();
AnalysisCriterion expectancy = new ExpectancyCriterion();
AnalysisCriterion sqn = new SqnCriterion(); // System Quality Number (higher = better)
AnalysisCriterion versusEnterAndHoldCriterion = new VersusEnterAndHoldCriterion(new NetReturnCriterion());
Num romadValue = romad.calculate(series, tradingRecord);
Num maxDrawdownValue = maxDrawdown.calculate(series, tradingRecord);
Num expectancyValue = expectancy.calculate(series, tradingRecord);
Num sqnValue = sqn.calculate(series, tradingRecord);
Num vsBuyHold = versusEnterAndHoldCriterion.calculate(series, tradingRecord);
// Display comprehensive results
System.out.println(" Basic Metrics:");
System.out.printf(" Total Positions: %d%n", tradingRecord.getPositionCount());
System.out.printf(" Net Return: %.2f%%%n",
netReturnValue.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Win Rate: %.1f%%%n",
winRate.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.println();
System.out.println(" Risk Metrics:");
System.out.printf(" Maximum Drawdown: %.2f%%%n",
maxDrawdownValue.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf(" Return/Max Drawdown: %.2f%n", romadValue.doubleValue());
System.out.println();
System.out.println(" Advanced Metrics:");
System.out.printf(" Expectancy: %.4f (avg profit per trade)%n", expectancyValue.doubleValue());
System.out.printf(" SQN (System Quality): %.2f (higher = better)%n", sqnValue.doubleValue());
System.out.printf(" vs Buy & Hold: %.2f%%%n",
vsBuyHold.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.println();
// Step 6: Visualize the strategy with multiple subcharts
System.out.println("[6/7] Generating comprehensive strategy visualization...");
boolean isHeadless = GraphicsEnvironment.isHeadless();
if (isHeadless) {
System.out.println(" [WARN] Headless environment detected - skipping chart display");
System.out.println(" [TIP] Run in a GUI environment to see interactive charts!");
} else {
try {
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("Bollinger Bands Mean Reversion Strategy - Yahoo Finance Data (AAPL)")
.withSeries(series) // Price bars (candlesticks)
.withTradingRecordOverlay(tradingRecord) // Trading positions marked on price chart
.withIndicatorOverlay(bbMiddle) // Middle band overlay
.withIndicatorOverlay(bbUpper) // Upper band overlay
.withIndicatorOverlay(bbLower) // Lower band overlay
.withSubChart(rsi) // RSI in first subchart
.withSubChart(obv) // OBV in second subchart
.withSubChart(vortex) // Vortex oscillator in third subchart
.withSubChart(ultimateOscillator) // Ultimate Oscillator in fourth subchart
.withSubChart(new NetProfitLossCriterion(), tradingRecord) // Net profit/loss in fifth subchart
.toChart();
chartWorkflow.displayChart(chart, "ta4j Yahoo Finance Backtest - Advanced Mean Reversion Strategy");
System.out.println(" [OK] Multi-subchart displayed in new window");
System.out.println(" [TIP] Chart shows: Price with BB bands, RSI, OBV, Vortex, Ultimate, and P&L");
} catch (Exception ex) {
LOG.warn("Failed to display chart: {}", ex.getMessage(), ex);
System.out.println(" [WARN] Could not display chart: " + ex.getMessage());
}
}
System.out.println();
// Step 7: Explain advanced concepts
System.out.println("[7/7] Advanced Concepts Demonstrated");
System.out.println(" ──────────────────────────────────────────");
System.out.println(" ✓ Bollinger Bands: Mean reversion indicator");
System.out.println(" - Price tends to revert to the middle band");
System.out.println(" - Lower band = oversold, Upper band = overbought");
System.out.println();
System.out.println(" ✓ ATR-based Stop Loss: Dynamic risk management");
System.out.println(" - Adapts to market volatility automatically");
System.out.println(" - Tighter stops in calm markets, wider in volatile markets");
System.out.println();
System.out.println(" ✓ Multi-Indicator Confirmation: Reduces false signals");
System.out.println(" - RSI confirms oversold/overbought conditions");
System.out.println(" - OBV confirms volume support for price moves");
System.out.println(" - Vortex confirms directional trend bias (+VI vs -VI)");
System.out.println(" - Ultimate Oscillator confirms multi-timeframe buying pressure");
System.out.println();
System.out.println(" ✓ Advanced Metrics: Deeper performance insights");
System.out.println(" - Expectancy: Average profit per trade");
System.out.println(" - SQN: System Quality Number (risk-adjusted performance)");
System.out.println(" - Maximum Drawdown: Largest peak-to-trough decline");
System.out.println();
// Summary
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ Summary ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
System.out.println();
System.out.println("What just happened?");
System.out.println();
System.out.println(" 1. Loaded 2 years of daily OHLCV data for AAPL from Yahoo Finance");
System.out.println(" 2. Created advanced indicators:");
System.out.println(" - Bollinger Bands (mean reversion)");
System.out.println(" - RSI (momentum confirmation)");
System.out.println(" - ATR (volatility for dynamic stops)");
System.out.println(" - OBV (volume trend confirmation)");
System.out.println(" - Vortex oscillator (trend direction confirmation)");
System.out.println(" - Ultimate Oscillator (trend strength confirmation)");
System.out.println(" - Custom price-to-middle-band ratio (indicator composition)");
System.out.println(" 3. Built a sophisticated mean reversion strategy:");
System.out.println(
" - Entry: Price at/below lower BB + RSI < 45 + (OBV rising OR price below middle) + Vortex > 0 + Ultimate > 50");
System.out.println(" - Exit: Price at/above upper BB OR RSI > 65 OR ATR stop (2.5x ATR) OR Vortex < 0");
System.out.println(" 4. Backtested with ATR-based dynamic stop-loss (adapts to volatility)");
System.out.println(" 5. Analyzed with advanced metrics (Expectancy, SQN, Max Drawdown)");
if (!isHeadless) {
System.out.println(" 6. Visualized with multi-subchart (Price, RSI, OBV, P&L)");
}
System.out.println();
System.out.println("Advanced Features Demonstrated:");
System.out.println(" ✓ Bollinger Bands for mean reversion trading");
System.out.println(" ✓ ATR-based dynamic stop-loss (better than fixed %)");
System.out.println(" ✓ Multi-indicator trend confirmation (RSI, OBV, Vortex, Ultimate)");
System.out.println(" ✓ Indicator composition (BinaryOperationIndicator)");
System.out.println(" ✓ Advanced performance metrics (Expectancy, SQN)");
System.out.println(" ✓ Multi-subchart visualization");
System.out.println();
System.out.println("Yahoo Finance Data Source Features:");
System.out.println(" - Load data by number of days: loadSeries(\"AAPL\", 730)");
System.out.println(" - Load data by bar count: loadSeries(\"AAPL\", DAY_1, 500)");
System.out.println(" - Load data by date range: loadSeries(\"AAPL\", DAY_1, start, end)");
System.out.println(" - Supports multiple intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo");
System.out.println(" - Works with stocks, ETFs, and cryptocurrencies");
System.out.println(" - Automatic pagination for large date ranges");
System.out.println();
System.out.println("Next Steps - Experiment with:");
System.out.println(" - Different tickers: \"MSFT\", \"GOOGL\", \"BTC-USD\", \"ETH-USD\"");
System.out.println(" - Adjust BB period (try 10, 30) and multiplier (try 1.5, 2.5)");
System.out.println(" - Modify RSI thresholds (try 30/70 or 35/65)");
System.out.println(" - Change ATR multiplier for stops (try 1.5x or 3.0x)");
System.out.println(" - Add MACD or Stochastic for additional confirmation");
System.out.println(" - Try different intervals: HOUR_1, WEEK_1 for different timeframes");
System.out.println(" - Explore other examples in ta4j-examples");
System.out.println(" - Check out the wiki: https://ta4j.github.io/ta4j-wiki/");
System.out.println();
System.out.println("Your turn! Modify this code and see how it affects performance.");
System.out.println();
}
}