goldenChat base source add
This commit is contained in:
+2005
File diff suppressed because it is too large
Load Diff
+432
@@ -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();
|
||||
}
|
||||
}
|
||||
+153
@@ -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;
|
||||
}
|
||||
}
|
||||
+142
@@ -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));
|
||||
}
|
||||
}
|
||||
+158
@@ -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;
|
||||
}
|
||||
}
|
||||
+175
@@ -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"));
|
||||
}
|
||||
}
|
||||
+179
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+365
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user