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,104 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.adx.ADXIndicator;
import org.ta4j.core.indicators.adx.MinusDIIndicator;
import org.ta4j.core.indicators.adx.PlusDIIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
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.BitStampCsvTradesFileBarSeriesDataSource;
/**
* ADX indicator based strategy
*
* @see <a href=
* "http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:average_directional_index_adx">
* http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:average_directional_index_adx</a>
*/
public class ADXStrategy {
private static final Logger LOG = LogManager.getLogger(ADXStrategy.class);
/**
* @param series a bar series
* @return an adx indicator based strategy
*/
public static Strategy buildStrategy(BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
final ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series);
final SMAIndicator smaIndicator = new SMAIndicator(closePriceIndicator, 50);
final int adxBarCount = 14;
final ADXIndicator adxIndicator = new ADXIndicator(series, adxBarCount);
final OverIndicatorRule adxOver20Rule = new OverIndicatorRule(adxIndicator, 20);
final PlusDIIndicator plusDIIndicator = new PlusDIIndicator(series, adxBarCount);
final MinusDIIndicator minusDIIndicator = new MinusDIIndicator(series, adxBarCount);
final Rule plusDICrossedUpMinusDI = new CrossedUpIndicatorRule(plusDIIndicator, minusDIIndicator);
final Rule plusDICrossedDownMinusDI = new CrossedDownIndicatorRule(plusDIIndicator, minusDIIndicator);
final OverIndicatorRule closePriceOverSma = new OverIndicatorRule(closePriceIndicator, smaIndicator);
final Rule entryRule = adxOver20Rule.and(plusDICrossedUpMinusDI).and(closePriceOverSma);
final UnderIndicatorRule closePriceUnderSma = new UnderIndicatorRule(closePriceIndicator, smaIndicator);
final Rule exitRule = adxOver20Rule.and(plusDICrossedDownMinusDI).and(closePriceUnderSma);
return new BaseStrategy("ADXStrategy", entryRule, exitRule, adxBarCount);
}
public static void main(String[] args) {
// Getting the bar series
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
// Building the trading strategy
Strategy strategy = buildStrategy(series);
// Running the strategy
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
// Analysis
var grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series);
SMAIndicator smaIndicator = new SMAIndicator(closePriceIndicator, 50);
int adxBarCount = 14;
ADXIndicator adxIndicator = new ADXIndicator(series, adxBarCount);
PlusDIIndicator plusDIIndicator = new PlusDIIndicator(series, adxBarCount);
MinusDIIndicator minusDIIndicator = new MinusDIIndicator(series, adxBarCount);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(smaIndicator)
.withSubChart(adxIndicator)
.withIndicatorOverlay(plusDIIndicator)
.withIndicatorOverlay(minusDIIndicator)
.withSubChart(new GrossReturnCriterion(), tradingRecord)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "adx-strategy", "temp/charts");
}
}
@@ -0,0 +1,97 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import java.awt.Color;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
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.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.CCIIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.BitStampCsvTradesFileBarSeriesDataSource;
/**
* CCI Correction Strategy
*
* @see <a href=
* "http://stockcharts.com/school/doku.php?id=chart_school:trading_strategies:cci_correction">
* http://stockcharts.com/school/doku.php?id=chart_school:trading_strategies:cci_correction</a>
*/
public class CCICorrectionStrategy {
private static final Logger LOG = LogManager.getLogger(CCICorrectionStrategy.class);
/**
* @param series a bar series
* @return a CCI correction strategy
*/
public static Strategy buildStrategy(BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
CCIIndicator longCci = new CCIIndicator(series, 200);
CCIIndicator shortCci = new CCIIndicator(series, 5);
Num plus100 = series.numFactory().hundred();
Num minus100 = series.numFactory().numOf(-100);
Rule entryRule = new OverIndicatorRule(longCci, plus100) // Bull trend
.and(new UnderIndicatorRule(shortCci, minus100)); // Signal
Rule exitRule = new UnderIndicatorRule(longCci, minus100) // Bear trend
.and(new OverIndicatorRule(shortCci, plus100)); // Signal
String strategyName = "CCICorrectionStrategy";
Strategy strategy = new BaseStrategy(strategyName, entryRule, exitRule);
strategy.setUnstableBars(5);
return strategy;
}
public static void main(String[] args) {
// Getting the bar series
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
// Building the trading strategy
Strategy strategy = buildStrategy(series);
// Running the strategy
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
// Analysis
var grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
CCIIndicator longCci = new CCIIndicator(series, 200);
CCIIndicator shortCci = new CCIIndicator(series, 5);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withSubChart(longCci)
.withIndicatorOverlay(shortCci)
.withLineColor(Color.ORANGE)
.withSubChart(new GrossReturnCriterion(), tradingRecord)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "cci-correction-strategy", "temp/charts");
}
}
@@ -0,0 +1,141 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Strategy;
import org.ta4j.core.indicators.helpers.DateTimeIndicator;
import org.ta4j.core.rules.DayOfWeekRule;
import org.ta4j.core.strategy.named.NamedStrategy;
import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.List;
/**
* A trading strategy that enters and exits positions based on specific days of
* the week.
*
* <p>
* This strategy uses {@link DayOfWeekRule} to determine entry and exit signals
* based on the day of the week of each bar in the series. The strategy will
* enter a position when the bar's day of the week matches the specified entry
* day, and exit when it matches the specified exit day.
*
* <p>
* The strategy name is automatically generated as
* {@code "DayOfWeekStrategy_<entryDay>_<exitDay>"} (e.g.,
* "DayOfWeekStrategy_MONDAY_FRIDAY").
*
* <p>
* This strategy is useful for testing day-of-week effects in trading, such as
* the "Monday effect" or "Friday effect" observed in some markets.
*
* @since 0.19
*/
public class DayOfWeekStrategy extends NamedStrategy {
static {
registerImplementation(DayOfWeekStrategy.class);
}
/**
* Constructs a new DayOfWeekStrategy with the specified entry and exit days.
*
* @param series the bar series to analyze
* @param entryDayOfWeek the day of the week to enter positions
* @param exitDayOfWeek the day of the week to exit positions
* @throws IllegalArgumentException if series is null or if entryDayOfWeek
* equals exitDayOfWeek
*/
public DayOfWeekStrategy(BarSeries series, DayOfWeek entryDayOfWeek, DayOfWeek exitDayOfWeek) {
super(NamedStrategy.buildLabel(DayOfWeekStrategy.class, entryDayOfWeek.name(), exitDayOfWeek.name()),
new DayOfWeekRule(new DateTimeIndicator(series), entryDayOfWeek),
new DayOfWeekRule(new DateTimeIndicator(series), exitDayOfWeek));
}
/**
* Constructs a new DayOfWeekStrategy from string parameters.
*
* <p>
* The parameters should be two strings representing the entry and exit days of
* the week. Valid values are: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
* SATURDAY, SUNDAY (case-sensitive).
*
* @param series the bar series to analyze
* @param params array containing [entryDayOfWeek, exitDayOfWeek] as strings
* @throws IllegalArgumentException if params is null, has fewer than 2
* elements, or contains invalid day names
*/
public DayOfWeekStrategy(BarSeries series, String... params) {
this(series, parseEntryDayOfWeek(params), parseExitDayOfWeek(params));
}
/**
* Builds all possible strategy permutations for all combinations of entry and
* exit days.
*
* <p>
* This method generates strategies for all pairs of different days of the week
* (7 * 6 = 42 total strategies). Strategies where the entry day equals the exit
* day are excluded. If any strategy construction fails, a warning is logged and
* that strategy is skipped.
*
* @param series the bar series to analyze
* @return a list of all valid DayOfWeekStrategy permutations
*/
public static List<Strategy> buildAllStrategyPermutations(BarSeries series) {
List<String[]> permutations = new ArrayList<>();
for (DayOfWeek entryDay : DayOfWeek.values()) {
for (DayOfWeek exitDay : DayOfWeek.values()) {
if (entryDay != exitDay) {
permutations.add(new String[] { entryDay.name(), exitDay.name() });
}
}
}
return NamedStrategy.buildAllStrategyPermutations(series, permutations, DayOfWeekStrategy::new,
(params, error) -> {
String entry = params.length > 0 ? params[0] : "<missing>";
String exit = params.length > 1 ? params[1] : "<missing>";
LogManager.getLogger()
.warn("Failed to build strategy for entry day {} and exit day {} - {}", entry, exit,
error.getMessage());
});
}
private static DayOfWeek parseEntryDayOfWeek(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 1) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryDayOfWeek, exitDayOfWeek), but got " + params.length);
}
try {
return DayOfWeek.valueOf(params[0]);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid entry DayOfWeek value: '" + params[0]
+ "'. Valid values are: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY", e);
}
}
private static DayOfWeek parseExitDayOfWeek(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 2) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryDayOfWeek, exitDayOfWeek), but got " + params.length);
}
try {
return DayOfWeek.valueOf(params[1]);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid exit DayOfWeek value: '" + params[1]
+ "'. Valid values are: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY", e);
}
}
}
@@ -0,0 +1,107 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Strategy;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.helpers.HighPriceIndicator;
import org.ta4j.core.indicators.helpers.HighestValueIndicator;
import org.ta4j.core.indicators.helpers.LowPriceIndicator;
import org.ta4j.core.indicators.helpers.LowestValueIndicator;
import org.ta4j.core.indicators.numeric.BinaryOperationIndicator;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.BitStampCsvTradesFileBarSeriesDataSource;
/**
* Strategies which compares current price to global extrema over a week.
*/
public class GlobalExtremaStrategy {
private static final Logger LOG = LogManager.getLogger(GlobalExtremaStrategy.class);
// We assume that there were at least one position every 5 minutes during the
// whole
// week
private static final int NB_BARS_PER_WEEK = 12 * 24 * 7;
/**
* @param series the bar series
* @return the global extrema strategy
*/
public static Strategy buildStrategy(final BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
final var closePrices = new ClosePriceIndicator(series);
// Getting the high price over the past week
final var highPrices = new HighPriceIndicator(series);
final var weekHighPrice = new HighestValueIndicator(highPrices, NB_BARS_PER_WEEK);
// Getting the low price over the past week
final var lowPrices = new LowPriceIndicator(series);
final var weekLowPrice = new LowestValueIndicator(lowPrices, NB_BARS_PER_WEEK);
// Going long if the close price goes below the low price
final var downWeek = BinaryOperationIndicator.product(weekLowPrice, 1.004);
final var buyingRule = new UnderIndicatorRule(closePrices, downWeek);
// Going short if the close price goes above the high price
final var upWeek = BinaryOperationIndicator.product(weekHighPrice, 0.996);
final var sellingRule = new OverIndicatorRule(closePrices, upWeek);
String strategyName = "GlobalExtremaStrategy";
return new BaseStrategy(strategyName, buyingRule, sellingRule);
}
public static void main(final String[] args) {
// Getting the bar series
final var series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
// Building the trading strategy
final var strategy = buildStrategy(series);
// Running the strategy
final var seriesManager = new BarSeriesManager(series);
final var tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
// Analysis
final var grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
final var highPrices = new HighPriceIndicator(series);
final var weekHighPrice = new HighestValueIndicator(highPrices, NB_BARS_PER_WEEK);
final var lowPrices = new LowPriceIndicator(series);
final var weekLowPrice = new LowestValueIndicator(lowPrices, NB_BARS_PER_WEEK);
final var downWeek = BinaryOperationIndicator.product(weekLowPrice, 1.004);
final var upWeek = BinaryOperationIndicator.product(weekHighPrice, 0.996);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(weekHighPrice)
.withIndicatorOverlay(weekLowPrice)
.withIndicatorOverlay(downWeek)
.withIndicatorOverlay(upWeek)
.withAnalysisCriterionOverlay(new NetProfitCriterion(), tradingRecord)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "global-extrema-strategy", "temp/charts");
}
}
@@ -0,0 +1,689 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import java.math.BigDecimal;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Indicator;
import org.ta4j.core.Rule;
import org.ta4j.core.indicators.CachedIndicator;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.elliott.ElliottChannelIndicator;
import org.ta4j.core.indicators.elliott.ElliottDegree;
import org.ta4j.core.indicators.elliott.ElliottPhase;
import org.ta4j.core.indicators.elliott.ElliottScenario;
import org.ta4j.core.indicators.elliott.ElliottScenarioGenerator;
import org.ta4j.core.indicators.elliott.ElliottScenarioSet;
import org.ta4j.core.indicators.elliott.ElliottSwing;
import org.ta4j.core.indicators.elliott.ElliottSwingCompressor;
import org.ta4j.core.indicators.elliott.ElliottSwingIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.macd.VolatilityNormalizedMACDIndicator;
import org.ta4j.core.rules.NotRule;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import org.ta4j.core.rules.elliott.ElliottImpulsePhaseRule;
import org.ta4j.core.rules.elliott.ElliottScenarioAlternationRule;
import org.ta4j.core.rules.elliott.ElliottScenarioCompletionRule;
import org.ta4j.core.rules.elliott.ElliottScenarioConfidenceRule;
import org.ta4j.core.rules.elliott.ElliottScenarioDirectionRule;
import org.ta4j.core.rules.elliott.ElliottScenarioInvalidationRule;
import org.ta4j.core.rules.elliott.ElliottScenarioRiskRewardRule;
import org.ta4j.core.rules.elliott.ElliottScenarioStopViolationRule;
import org.ta4j.core.rules.elliott.ElliottScenarioTargetReachedRule;
import org.ta4j.core.rules.elliott.ElliottScenarioTimeStopRule;
import org.ta4j.core.rules.elliott.ElliottScenarioValidRule;
import org.ta4j.core.rules.elliott.ElliottTrendBiasRule;
import org.ta4j.core.strategy.named.NamedStrategy;
/**
* High-reward Elliott Wave strategy that trades only high-confidence impulse
* scenarios with favorable risk/reward and trend/momentum alignment.
*
* <p>
* Entry criteria:
* <ul>
* <li>Impulse scenario in wave 3 or wave 5</li>
* <li>Confidence above the minimum threshold</li>
* <li>Directional trend bias alignment</li>
* <li>Risk/reward meets the minimum threshold using the wave 2/4 stop and the
* furthest Fibonacci target</li>
* <li>Wave 2/4 time alternation exceeds the minimum ratio</li>
* <li>Trend (SMA) and momentum (RSI or volatility-normalized MACD-V)
* confirmation</li>
* </ul>
*
* <p>
* Exit criteria:
* <ul>
* <li>Scenario invalidation or completion</li>
* <li>Corrective swing stop breached (wave 2/4)</li>
* <li>Target reached</li>
* <li>Trend/momentum breakdown</li>
* <li>Time-based stop after an extended wave 3 duration</li>
* </ul>
*
* @since 0.22.2
*/
public class HighRewardElliottWaveStrategy extends NamedStrategy {
static {
registerImplementation(HighRewardElliottWaveStrategy.class);
}
private static final SignalDirection DEFAULT_DIRECTION = SignalDirection.BULLISH;
private static final ElliottDegree DEFAULT_DEGREE = ElliottDegree.PRIMARY;
private static final double DEFAULT_MIN_CONFIDENCE = 0.35;
private static final double DEFAULT_MIN_RISK_REWARD = 2.0;
private static final double DEFAULT_MIN_ALTERNATION_RATIO = 1.50;
private static final double DEFAULT_MIN_TREND_BIAS_STRENGTH = 0.10;
private static final int DEFAULT_TREND_SMA_PERIOD = 100;
private static final int DEFAULT_RSI_PERIOD = 14;
private static final double DEFAULT_RSI_THRESHOLD = 50.0;
private static final int DEFAULT_MACD_FAST = 12;
private static final int DEFAULT_MACD_SLOW = 26;
private static final int DEFAULT_MACD_SIGNAL = 9;
private static final int DEFAULT_ATR_PERIOD = 14;
private static final double DEFAULT_MIN_RELATIVE_SWING = 0.10;
private static final double ANALYZER_MIN_CONFIDENCE = 0.20;
private static final int DEFAULT_SCENARIO_SWING_WINDOW = 5;
private static final double MAX_WAVE_DURATION_MULTIPLIER = 1.5;
private static final int PARAMETER_COUNT = 12;
/**
* Builds the strategy with default parameters.
*
* @param series bar series to analyze
*/
public HighRewardElliottWaveStrategy(final BarSeries series) {
this(series, Config.defaults());
}
/**
* Builds the strategy using serialized label parameters.
*
* @param series bar series to analyze
* @param params serialized parameters (see
* {@link Config#fromParameters(String...)})
*/
public HighRewardElliottWaveStrategy(final BarSeries series, final String... params) {
this(series, Config.fromParameters(params));
}
/**
* Builds the strategy using a precomputed scenario indicator.
*
* @param series bar series to analyze
* @param config strategy configuration
* @param scenarioIndicator indicator supplying scenario sets
*/
HighRewardElliottWaveStrategy(final BarSeries series, final Config config,
final Indicator<ElliottScenarioSet> scenarioIndicator) {
this(config, buildEntryRule(series, config, scenarioIndicator),
buildExitRule(series, config, scenarioIndicator), calculateUnstableBars(config));
}
/**
* Builds the strategy with the default scenario indicator pipeline.
*
* @param series bar series to analyze
* @param config strategy configuration
*/
private HighRewardElliottWaveStrategy(final BarSeries series, final Config config) {
this(series, config, buildScenarioIndicator(series, config));
}
/**
* Internal constructor that wires the prepared rules into the named strategy.
*
* @param config strategy configuration
* @param entryRule precomputed entry rule
* @param exitRule precomputed exit rule
* @param unstableBars unstable bar count for warm-up
*/
private HighRewardElliottWaveStrategy(final Config config, final Rule entryRule, final Rule exitRule,
final int unstableBars) {
super(buildLabel(config), entryRule, exitRule, unstableBars);
}
/**
* Builds the entry rule for the strategy.
*
* @param series bar series backing indicators
* @param config strategy configuration
* @param scenarioIndicator indicator supplying scenario sets
* @return entry rule
*/
private static Rule buildEntryRule(final BarSeries series, final Config config,
final Indicator<ElliottScenarioSet> scenarioIndicator) {
Objects.requireNonNull(series, "series");
Objects.requireNonNull(config, "config");
Objects.requireNonNull(scenarioIndicator, "scenarioIndicator");
validateScenarioIndicator(series, scenarioIndicator);
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator trendSma = new SMAIndicator(close, config.trendSmaPeriod());
RSIIndicator rsi = new RSIIndicator(close, config.rsiPeriod());
VolatilityNormalizedMACDIndicator macd = new VolatilityNormalizedMACDIndicator(close, config.macdFastPeriod(),
config.macdSlowPeriod(), DEFAULT_MACD_SIGNAL);
Rule trendRule = config.direction().isBullish() ? new OverIndicatorRule(close, trendSma)
: new UnderIndicatorRule(close, trendSma);
Rule momentumRule = config.direction().isBullish()
? new OverIndicatorRule(rsi, config.rsiThreshold()).or(new OverIndicatorRule(macd, 0))
: new UnderIndicatorRule(rsi, config.rsiThreshold()).or(new UnderIndicatorRule(macd, 0));
Rule scenarioValidRule = new ElliottScenarioValidRule(scenarioIndicator, close);
Rule impulsePhaseRule = new ElliottImpulsePhaseRule(scenarioIndicator, ElliottPhase.WAVE3, ElliottPhase.WAVE5);
Rule confidenceRule = new ElliottScenarioConfidenceRule(scenarioIndicator, config.minConfidence());
Rule directionRule = new ElliottScenarioDirectionRule(scenarioIndicator, config.direction().isBullish());
Rule trendBiasRule = new ElliottTrendBiasRule(scenarioIndicator, config.direction().isBullish(),
config.minTrendBiasStrength());
Rule alternationRule = new ElliottScenarioAlternationRule(scenarioIndicator, config.minAlternationRatio());
Rule riskRewardRule = new ElliottScenarioRiskRewardRule(scenarioIndicator, close,
config.direction().isBullish(), config.minRiskReward());
Rule entryRule = scenarioValidRule.and(impulsePhaseRule)
.and(confidenceRule)
.and(directionRule)
.and(trendBiasRule)
.and(alternationRule)
.and(riskRewardRule)
.and(trendRule)
.and(momentumRule);
return entryRule;
}
/**
* Builds the exit rule for the strategy.
*
* @param series bar series backing indicators
* @param config strategy configuration
* @param scenarioIndicator indicator supplying scenario sets
* @return exit rule
*/
private static Rule buildExitRule(final BarSeries series, final Config config,
final Indicator<ElliottScenarioSet> scenarioIndicator) {
Objects.requireNonNull(series, "series");
Objects.requireNonNull(config, "config");
Objects.requireNonNull(scenarioIndicator, "scenarioIndicator");
validateScenarioIndicator(series, scenarioIndicator);
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator trendSma = new SMAIndicator(close, config.trendSmaPeriod());
RSIIndicator rsi = new RSIIndicator(close, config.rsiPeriod());
VolatilityNormalizedMACDIndicator macd = new VolatilityNormalizedMACDIndicator(close, config.macdFastPeriod(),
config.macdSlowPeriod(), DEFAULT_MACD_SIGNAL);
Rule trendRule = config.direction().isBullish() ? new OverIndicatorRule(close, trendSma)
: new UnderIndicatorRule(close, trendSma);
Rule momentumRule = config.direction().isBullish()
? new OverIndicatorRule(rsi, config.rsiThreshold()).or(new OverIndicatorRule(macd, 0))
: new UnderIndicatorRule(rsi, config.rsiThreshold()).or(new UnderIndicatorRule(macd, 0));
Rule scenarioValidRule = new ElliottScenarioValidRule(scenarioIndicator, close);
Rule directionRule = new ElliottScenarioDirectionRule(scenarioIndicator, config.direction().isBullish());
Rule trendBiasRule = new ElliottTrendBiasRule(scenarioIndicator, config.direction().isBullish(),
config.minTrendBiasStrength());
Rule exitTriggers = new NotRule(scenarioValidRule).or(new NotRule(directionRule))
.or(new ElliottScenarioCompletionRule(scenarioIndicator))
.or(new ElliottScenarioInvalidationRule(scenarioIndicator, close))
.or(new ElliottScenarioTargetReachedRule(scenarioIndicator, close, config.direction().isBullish()))
.or(new ElliottScenarioStopViolationRule(scenarioIndicator, close, config.direction().isBullish()))
.or(new NotRule(trendBiasRule))
.or(new NotRule(trendRule.and(momentumRule)))
.or(new ElliottScenarioTimeStopRule(scenarioIndicator, MAX_WAVE_DURATION_MULTIPLIER));
return exitTriggers;
}
/**
* Validates that the scenario indicator is bound to the same series instance as
* the strategy.
*
* @param series strategy series
* @param scenarioIndicator scenario source indicator
*/
private static void validateScenarioIndicator(final BarSeries series,
final Indicator<ElliottScenarioSet> scenarioIndicator) {
if (scenarioIndicator.getBarSeries() != series) {
throw new IllegalArgumentException("scenarioIndicator must use the same BarSeries instance");
}
}
/**
* Builds the scenario indicator pipeline used by the strategy.
*
* @param series bar series to analyze
* @param config strategy configuration
* @return scenario indicator
*/
private static Indicator<ElliottScenarioSet> buildScenarioIndicator(final BarSeries series, final Config config) {
ElliottSwingIndicator swingIndicator = ElliottSwingIndicator.zigZag(series, config.degree());
ElliottChannelIndicator channelIndicator = new ElliottChannelIndicator(swingIndicator);
double minConfidence = Math.min(config.minConfidence(), ANALYZER_MIN_CONFIDENCE);
ElliottScenarioGenerator generator = new ElliottScenarioGenerator(series.numFactory(), minConfidence,
ElliottScenarioGenerator.DEFAULT_MAX_SCENARIOS);
ElliottSwingCompressor compressor = new ElliottSwingCompressor(new ClosePriceIndicator(series),
config.minRelativeSwing(), 0);
return new ScenarioSetIndicator(series, swingIndicator, channelIndicator, generator, compressor,
DEFAULT_SCENARIO_SWING_WINDOW);
}
/**
* Builds the named-strategy label for the configured parameters.
*
* @param config strategy configuration
* @return label string
*/
private static String buildLabel(final Config config) {
return NamedStrategy.buildLabel(HighRewardElliottWaveStrategy.class, config.labelParts());
}
/**
* Calculates the number of unstable bars for indicator warm-up.
*
* @param config strategy configuration
* @return unstable bar count
*/
private static int calculateUnstableBars(final Config config) {
int unstable = Math.max(config.trendSmaPeriod(), Math.max(config.rsiPeriod(), config.macdSlowPeriod()));
unstable = Math.max(unstable, DEFAULT_ATR_PERIOD);
return unstable;
}
/**
* Formats a double for strategy labels without trailing zeros.
*
* @param value numeric value
* @return formatted string
*/
private static String formatDouble(final double value) {
return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
}
/**
* Trade direction for the strategy.
*/
enum SignalDirection {
BULLISH, BEARISH;
/**
* @return {@code true} when the direction is bullish
*/
boolean isBullish() {
return this == BULLISH;
}
}
/**
* Immutable configuration for the strategy.
*/
static final class Config {
private final SignalDirection direction;
private final ElliottDegree degree;
private final double minConfidence;
private final double minRiskReward;
private final double minAlternationRatio;
private final double minTrendBiasStrength;
private final int trendSmaPeriod;
private final int rsiPeriod;
private final double rsiThreshold;
private final int macdFastPeriod;
private final int macdSlowPeriod;
private final double minRelativeSwing;
/**
* Creates a configuration with the supplied parameters.
*
* @param direction trade direction
* @param degree Elliott wave degree to analyze
* @param minConfidence minimum confidence threshold
* @param minRiskReward minimum risk/reward ratio
* @param minAlternationRatio minimum alternation duration ratio
* @param minTrendBiasStrength minimum trend bias strength
* @param trendSmaPeriod SMA period for trend confirmation
* @param rsiPeriod RSI period for momentum confirmation
* @param rsiThreshold RSI threshold for momentum
* @param macdFastPeriod MACD fast period
* @param macdSlowPeriod MACD slow period
* @param minRelativeSwing minimum relative swing magnitude
*/
Config(final SignalDirection direction, final ElliottDegree degree, final double minConfidence,
final double minRiskReward, final double minAlternationRatio, final double minTrendBiasStrength,
final int trendSmaPeriod, final int rsiPeriod, final double rsiThreshold, final int macdFastPeriod,
final int macdSlowPeriod, final double minRelativeSwing) {
this.direction = Objects.requireNonNull(direction, "direction");
this.degree = Objects.requireNonNull(degree, "degree");
if (minConfidence <= 0.0 || minConfidence > 1.0) {
throw new IllegalArgumentException("minConfidence must be in (0.0, 1.0]");
}
if (minRiskReward <= 0.0) {
throw new IllegalArgumentException("minRiskReward must be positive");
}
if (minAlternationRatio < 1.0) {
throw new IllegalArgumentException("minAlternationRatio must be >= 1.0");
}
if (minTrendBiasStrength < 0.0 || minTrendBiasStrength > 1.0) {
throw new IllegalArgumentException("minTrendBiasStrength must be in [0.0, 1.0]");
}
if (trendSmaPeriod <= 0) {
throw new IllegalArgumentException("trendSmaPeriod must be positive");
}
if (rsiPeriod <= 0) {
throw new IllegalArgumentException("rsiPeriod must be positive");
}
if (rsiThreshold < 0.0 || rsiThreshold > 100.0) {
throw new IllegalArgumentException("rsiThreshold must be in [0.0, 100.0]");
}
if (macdFastPeriod <= 0 || macdSlowPeriod <= 0) {
throw new IllegalArgumentException("MACD periods must be positive");
}
if (macdFastPeriod >= macdSlowPeriod) {
throw new IllegalArgumentException("macdFastPeriod must be less than macdSlowPeriod");
}
if (minRelativeSwing <= 0.0 || minRelativeSwing > 1.0) {
throw new IllegalArgumentException("minRelativeSwing must be in (0.0, 1.0]");
}
this.minConfidence = minConfidence;
this.minRiskReward = minRiskReward;
this.minAlternationRatio = minAlternationRatio;
this.minTrendBiasStrength = minTrendBiasStrength;
this.trendSmaPeriod = trendSmaPeriod;
this.rsiPeriod = rsiPeriod;
this.rsiThreshold = rsiThreshold;
this.macdFastPeriod = macdFastPeriod;
this.macdSlowPeriod = macdSlowPeriod;
this.minRelativeSwing = minRelativeSwing;
}
/**
* @return default configuration values
*/
static Config defaults() {
return new Config(DEFAULT_DIRECTION, DEFAULT_DEGREE, DEFAULT_MIN_CONFIDENCE, DEFAULT_MIN_RISK_REWARD,
DEFAULT_MIN_ALTERNATION_RATIO, DEFAULT_MIN_TREND_BIAS_STRENGTH, DEFAULT_TREND_SMA_PERIOD,
DEFAULT_RSI_PERIOD, DEFAULT_RSI_THRESHOLD, DEFAULT_MACD_FAST, DEFAULT_MACD_SLOW,
DEFAULT_MIN_RELATIVE_SWING);
}
/**
* Parses a serialized parameter list into a configuration.
*
* @param params serialized parameters
* @return parsed configuration
*/
static Config fromParameters(final String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length == 0) {
return defaults();
}
if (params.length != PARAMETER_COUNT) {
throw new IllegalArgumentException("Expected " + PARAMETER_COUNT
+ " parameters (direction, degree, minConfidence, minRiskReward, minAlternationRatio, "
+ "minTrendBiasStrength, trendSmaPeriod, rsiPeriod, rsiThreshold, macdFastPeriod, "
+ "macdSlowPeriod, minRelativeSwing), but got " + params.length);
}
SignalDirection direction = parseEnum(params[0], SignalDirection.class, "direction");
ElliottDegree degree = parseEnum(params[1], ElliottDegree.class, "degree");
double minConfidence = parseDouble(params[2], "minConfidence");
double minRiskReward = parseDouble(params[3], "minRiskReward");
double minAlternation = parseDouble(params[4], "minAlternationRatio");
double minTrendBias = parseDouble(params[5], "minTrendBiasStrength");
int trendSma = parseInt(params[6], "trendSmaPeriod");
int rsiPeriod = parseInt(params[7], "rsiPeriod");
double rsiThreshold = parseDouble(params[8], "rsiThreshold");
int macdFast = parseInt(params[9], "macdFastPeriod");
int macdSlow = parseInt(params[10], "macdSlowPeriod");
double minRelativeSwing = parseDouble(params[11], "minRelativeSwing");
return new Config(direction, degree, minConfidence, minRiskReward, minAlternation, minTrendBias, trendSma,
rsiPeriod, rsiThreshold, macdFast, macdSlow, minRelativeSwing);
}
/**
* @return label parts used for NamedStrategy labels
*/
String[] labelParts() {
return new String[] { direction.name(), degree.name(), formatDouble(minConfidence),
formatDouble(minRiskReward), formatDouble(minAlternationRatio), formatDouble(minTrendBiasStrength),
String.valueOf(trendSmaPeriod), String.valueOf(rsiPeriod), formatDouble(rsiThreshold),
String.valueOf(macdFastPeriod), String.valueOf(macdSlowPeriod), formatDouble(minRelativeSwing) };
}
/**
* @return configured trade direction
*/
SignalDirection direction() {
return direction;
}
/**
* @return configured Elliott wave degree
*/
ElliottDegree degree() {
return degree;
}
/**
* @return minimum confidence threshold
*/
double minConfidence() {
return minConfidence;
}
/**
* @return minimum risk/reward ratio
*/
double minRiskReward() {
return minRiskReward;
}
/**
* @return minimum alternation ratio
*/
double minAlternationRatio() {
return minAlternationRatio;
}
/**
* @return minimum trend bias strength
*/
double minTrendBiasStrength() {
return minTrendBiasStrength;
}
/**
* @return trend SMA period
*/
int trendSmaPeriod() {
return trendSmaPeriod;
}
/**
* @return RSI period
*/
int rsiPeriod() {
return rsiPeriod;
}
/**
* @return RSI threshold
*/
double rsiThreshold() {
return rsiThreshold;
}
/**
* @return MACD fast period
*/
int macdFastPeriod() {
return macdFastPeriod;
}
/**
* @return MACD slow period
*/
int macdSlowPeriod() {
return macdSlowPeriod;
}
/**
* @return minimum relative swing magnitude
*/
double minRelativeSwing() {
return minRelativeSwing;
}
/**
* Parses an integer parameter.
*
* @param value parameter value
* @param label parameter label
* @return parsed integer
*/
private static int parseInt(final String value, final String label) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid " + label + " value: '" + value + "'", ex);
}
}
/**
* Parses a double parameter.
*
* @param value parameter value
* @param label parameter label
* @return parsed double
*/
private static double parseDouble(final String value, final String label) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid " + label + " value: '" + value + "'", ex);
}
}
/**
* Parses an enum parameter.
*
* @param value parameter value
* @param type enum type
* @param label parameter label
* @param <E> enum type
* @return parsed enum
*/
private static <E extends Enum<E>> E parseEnum(final String value, final Class<E> type, final String label) {
try {
return Enum.valueOf(type, value);
} catch (IllegalArgumentException ex) {
Set<String> allowed = enumNames(type);
throw new IllegalArgumentException(
"Invalid " + label + " value: '" + value + "'. Valid values are: " + String.join(", ", allowed),
ex);
}
}
/**
* Returns all allowed enum names for an error message.
*
* @param type enum type
* @param <E> enum type
* @return set of enum names
*/
private static <E extends Enum<E>> Set<String> enumNames(final Class<E> type) {
if (type == null) {
return Set.of();
}
return EnumSet.allOf(type).stream().map(Enum::name).collect(Collectors.toSet());
}
}
/**
* Cached indicator that assembles scenario sets for each bar index.
*/
private static final class ScenarioSetIndicator extends CachedIndicator<ElliottScenarioSet> {
private final ElliottSwingIndicator swingIndicator;
private final ElliottChannelIndicator channelIndicator;
private final ElliottScenarioGenerator generator;
private final ElliottSwingCompressor compressor;
private final int scenarioSwingWindow;
/**
* Creates a scenario indicator with the supplied dependencies.
*
* @param series bar series
* @param swingIndicator swing detector indicator
* @param channelIndicator channel indicator for scoring
* @param generator scenario generator
* @param compressor optional swing compressor
* @param scenarioSwingWindow max number of swings to score
*/
private ScenarioSetIndicator(final BarSeries series, final ElliottSwingIndicator swingIndicator,
final ElliottChannelIndicator channelIndicator, final ElliottScenarioGenerator generator,
final ElliottSwingCompressor compressor, final int scenarioSwingWindow) {
super(series);
this.swingIndicator = Objects.requireNonNull(swingIndicator, "swingIndicator");
this.channelIndicator = Objects.requireNonNull(channelIndicator, "channelIndicator");
this.generator = Objects.requireNonNull(generator, "generator");
this.compressor = compressor;
this.scenarioSwingWindow = scenarioSwingWindow;
}
/**
* Computes the scenario set for the provided index.
*
* @param index bar index
* @return scenario set for the index
*/
@Override
protected ElliottScenarioSet calculate(final int index) {
final BarSeries series = getBarSeries();
if (series.isEmpty()) {
throw new IllegalArgumentException("series cannot be empty");
}
int clampedIndex = Math.max(series.getBeginIndex(), Math.min(index, series.getEndIndex()));
List<ElliottSwing> swings = swingIndicator.getValue(clampedIndex);
if (compressor != null) {
swings = compressor.compress(swings);
}
if (swings.isEmpty()) {
return ElliottScenarioSet.empty(clampedIndex);
}
List<ElliottSwing> recent = swings;
if (scenarioSwingWindow > 0 && swings.size() > scenarioSwingWindow) {
recent = List.copyOf(swings.subList(swings.size() - scenarioSwingWindow, swings.size()));
}
return generator.generate(recent, swingIndicator.getDegree(), channelIndicator.getValue(clampedIndex),
clampedIndex);
}
/**
* @return the number of unstable bars for the underlying swing indicator
*/
@Override
public int getCountOfUnstableBars() {
return swingIndicator.getCountOfUnstableBars();
}
}
}
@@ -0,0 +1,155 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Strategy;
import org.ta4j.core.indicators.helpers.DateTimeIndicator;
import org.ta4j.core.rules.HourOfDayRule;
import org.ta4j.core.strategy.named.NamedStrategy;
import java.util.ArrayList;
import java.util.List;
/**
* A trading strategy that enters and exits positions based on specific hours of
* the day.
*
* <p>
* This strategy uses {@link HourOfDayRule} to determine entry and exit signals
* based on the hour of the day (0-23) of each bar in the series. The strategy
* will enter a position when the bar's hour of the day matches the specified
* entry hour, and exit when it matches the specified exit hour.
*
* <p>
* The strategy name is automatically generated as
* {@code "HourOfDayStrategy_<entryHour>_<exitHour>"} (e.g.,
* "HourOfDayStrategy_9_17").
*
* <p>
* This strategy is useful for testing intraday trading patterns, such as market
* open/close effects or specific trading hours in different time zones.
*
* @since 0.19
*/
public class HourOfDayStrategy extends NamedStrategy {
static {
registerImplementation(HourOfDayStrategy.class);
}
/**
* Constructs a new HourOfDayStrategy with the specified entry and exit hours.
*
* @param series the bar series to analyze
* @param entryHour the hour of the day (0-23) to enter positions
* @param exitHour the hour of the day (0-23) to exit positions
* @throws IllegalArgumentException if series is null, if entryHour or exitHour
* is not in range 0-23, or if entryHour equals
* exitHour
*/
public HourOfDayStrategy(BarSeries series, int entryHour, int exitHour) {
super(NamedStrategy.buildLabel(HourOfDayStrategy.class, String.valueOf(entryHour), String.valueOf(exitHour)),
new HourOfDayRule(new DateTimeIndicator(series), entryHour),
new HourOfDayRule(new DateTimeIndicator(series), exitHour));
if (entryHour == exitHour) {
throw new IllegalArgumentException(
"Entry hour and exit hour must be different, but both were: " + entryHour);
}
}
/**
* Constructs a new HourOfDayStrategy from string parameters.
*
* <p>
* The parameters should be two strings representing the entry and exit hours of
* the day. Valid values are integers in the range 0-23 (inclusive).
*
* @param series the bar series to analyze
* @param params array containing [entryHour, exitHour] as strings
* @throws IllegalArgumentException if params is null, has fewer than 2
* elements, contains invalid hour values, or
* if entryHour equals exitHour
*/
public HourOfDayStrategy(BarSeries series, String... params) {
this(series, parseEntryHour(params), parseExitHour(params));
}
/**
* Builds all possible strategy permutations for all combinations of entry and
* exit hours.
*
* <p>
* This method generates strategies for all pairs of different hours of the day
* (24 * 23 = 552 total strategies). Strategies where the entry hour equals the
* exit hour are excluded. If any strategy construction fails, a warning is
* logged and that strategy is skipped.
*
* @param series the bar series to analyze
* @return a list of all valid HourOfDayStrategy permutations
*/
public static List<Strategy> buildAllStrategyPermutations(BarSeries series) {
List<String[]> permutations = new ArrayList<>();
for (int entryHour = 0; entryHour < 24; entryHour++) {
for (int exitHour = 0; exitHour < 24; exitHour++) {
if (entryHour != exitHour) {
permutations.add(new String[] { String.valueOf(entryHour), String.valueOf(exitHour) });
}
}
}
return NamedStrategy.buildAllStrategyPermutations(series, permutations, HourOfDayStrategy::new,
(params, error) -> {
String entry = params.length > 0 ? params[0] : "<missing>";
String exit = params.length > 1 ? params[1] : "<missing>";
LogManager.getLogger()
.warn("Failed to build strategy for entry hour {} and exit hour {} - {}", entry, exit,
error.getMessage());
});
}
private static int parseEntryHour(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 1) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryHour, exitHour), but got " + params.length);
}
try {
int hour = Integer.parseInt(params[0]);
if (hour < 0 || hour > 23) {
throw new IllegalArgumentException("Invalid entry hour value: '" + params[0]
+ "'. Valid values are integers in the range 0-23 (inclusive)");
}
return hour;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid entry hour value: '" + params[0]
+ "'. Valid values are integers in the range 0-23 (inclusive)", e);
}
}
private static int parseExitHour(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 2) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryHour, exitHour), but got " + params.length);
}
try {
int hour = Integer.parseInt(params[1]);
if (hour < 0 || hour > 23) {
throw new IllegalArgumentException("Invalid exit hour value: '" + params[1]
+ "'. Valid values are integers in the range 0-23 (inclusive)");
}
return hour;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid exit hour value: '" + params[1]
+ "'. Valid values are integers in the range 0-23 (inclusive)", e);
}
}
}
@@ -0,0 +1,112 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Indicator;
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.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.macd.MACDHistogramMode;
import org.ta4j.core.indicators.macd.MACDVMomentumProfile;
import org.ta4j.core.indicators.macd.MACDVMomentumState;
import org.ta4j.core.indicators.macd.VolatilityNormalizedMACDIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.numeric.NumericIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.OverIndicatorRule;
import org.ta4j.core.rules.UnderIndicatorRule;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.BitStampCsvTradesFileBarSeriesDataSource;
/**
* Strategy showing the volatility-normalized MACD-V workflow:
*
* <ul>
* <li>custom signal-line injection ({@link SMAIndicator})</li>
* <li>histogram polarity configuration</li>
* <li>momentum-state filtering via {@link MACDVMomentumProfile}</li>
* </ul>
*/
public class MACDVMomentumStateStrategy {
private static final Logger LOG = LogManager.getLogger(MACDVMomentumStateStrategy.class);
private static final int FAST_EMA = 12;
private static final int SLOW_EMA = 26;
private static final int ATR_PERIOD = 26;
private static final int SIGNAL_PERIOD = 9;
private static final MACDVMomentumProfile MOMENTUM_PROFILE = new MACDVMomentumProfile(25, 80, -25, -80);
/**
* @param series bar series
* @return strategy based on volatility-normalized MACD-V momentum-state helpers
*/
public static Strategy buildStrategy(BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
VolatilityNormalizedMACDIndicator macdV = new VolatilityNormalizedMACDIndicator(closePrice, FAST_EMA, SLOW_EMA,
ATR_PERIOD, SIGNAL_PERIOD, 100);
NumericIndicator histogram = macdV.getHistogram(SIGNAL_PERIOD, SMAIndicator::new,
MACDHistogramMode.MACD_MINUS_SIGNAL);
Rule bullishMomentum = macdV.inMomentumState(MOMENTUM_PROFILE, MACDVMomentumState.RALLYING_OR_RETRACING)
.or(macdV.inMomentumState(MOMENTUM_PROFILE, MACDVMomentumState.HIGH_RISK));
Rule bearishMomentum = macdV.inMomentumState(MOMENTUM_PROFILE, MACDVMomentumState.REBOUNDING_OR_REVERSING)
.or(macdV.inMomentumState(MOMENTUM_PROFILE, MACDVMomentumState.LOW_RISK));
Rule entryRule = macdV.crossedUpSignal(SIGNAL_PERIOD, SMAIndicator::new)
.and(new OverIndicatorRule(histogram, 0))
.and(bullishMomentum);
Rule exitRule = macdV.crossedDownSignal(SIGNAL_PERIOD, SMAIndicator::new)
.or(new UnderIndicatorRule(histogram, 0))
.or(bearishMomentum);
return new BaseStrategy(MACDVMomentumStateStrategy.class.getSimpleName(), entryRule, exitRule,
macdV.getCountOfUnstableBars());
}
public static void main(String[] args) {
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
Strategy strategy = buildStrategy(series);
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
Num grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
VolatilityNormalizedMACDIndicator macdV = new VolatilityNormalizedMACDIndicator(series, FAST_EMA, SLOW_EMA,
ATR_PERIOD, SIGNAL_PERIOD, 100);
Indicator<Num> signal = macdV.getSignalLine(SIGNAL_PERIOD, SMAIndicator::new);
NumericIndicator histogram = macdV.getHistogram(SIGNAL_PERIOD, SMAIndicator::new,
MACDHistogramMode.MACD_MINUS_SIGNAL);
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withSubChart(macdV)
.withIndicatorOverlay(signal)
.withSubChart(histogram)
.withAnalysisCriterionOverlay(new GrossReturnCriterion(), tradingRecord)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "macdv-momentum-state-strategy", "temp/charts");
}
}
@@ -0,0 +1,156 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Strategy;
import org.ta4j.core.indicators.helpers.DateTimeIndicator;
import org.ta4j.core.rules.MinuteOfHourRule;
import org.ta4j.core.strategy.named.NamedStrategy;
import java.util.ArrayList;
import java.util.List;
/**
* A trading strategy that enters and exits positions based on specific minutes
* of the hour.
*
* <p>
* This strategy uses {@link MinuteOfHourRule} to determine entry and exit
* signals based on the minute of the hour (0-59) of each bar in the series. The
* strategy will enter a position when the bar's minute of the hour matches the
* specified entry minute, and exit when it matches the specified exit minute.
*
* <p>
* The strategy name is automatically generated as
* {@code "MinuteOfHourStrategy_<entryMinute>_<exitMinute>"} (e.g.,
* "MinuteOfHourStrategy_15_45").
*
* <p>
* This strategy is useful for testing intraday trading patterns at a finer
* granularity, such as specific minute-based entry/exit points within an hour.
*
* @since 0.19
*/
public class MinuteOfHourStrategy extends NamedStrategy {
static {
registerImplementation(MinuteOfHourStrategy.class);
}
/**
* Constructs a new MinuteOfHourStrategy with the specified entry and exit
* minutes.
*
* @param series the bar series to analyze
* @param entryMinute the minute of the hour (0-59) to enter positions
* @param exitMinute the minute of the hour (0-59) to exit positions
* @throws IllegalArgumentException if series is null, if entryMinute or
* exitMinute is not in range 0-59, or if
* entryMinute equals exitMinute
*/
public MinuteOfHourStrategy(BarSeries series, int entryMinute, int exitMinute) {
super(NamedStrategy.buildLabel(MinuteOfHourStrategy.class, String.valueOf(entryMinute),
String.valueOf(exitMinute)), new MinuteOfHourRule(new DateTimeIndicator(series), entryMinute),
new MinuteOfHourRule(new DateTimeIndicator(series), exitMinute));
if (entryMinute == exitMinute) {
throw new IllegalArgumentException(
"Entry minute and exit minute must be different, but both were: " + entryMinute);
}
}
/**
* Constructs a new MinuteOfHourStrategy from string parameters.
*
* <p>
* The parameters should be two strings representing the entry and exit minutes
* of the hour. Valid values are integers in the range 0-59 (inclusive).
*
* @param series the bar series to analyze
* @param params array containing [entryMinute, exitMinute] as strings
* @throws IllegalArgumentException if params is null, has fewer than 2
* elements, contains invalid minute values, or
* if entryMinute equals exitMinute
*/
public MinuteOfHourStrategy(BarSeries series, String... params) {
this(series, parseEntryMinute(params), parseExitMinute(params));
}
/**
* Builds all possible strategy permutations for all combinations of entry and
* exit minutes.
*
* <p>
* This method generates strategies for all pairs of different minutes of the
* hour (60 * 59 = 3540 total strategies). Strategies where the entry minute
* equals the exit minute are excluded. If any strategy construction fails, a
* warning is logged and that strategy is skipped.
*
* @param series the bar series to analyze
* @return a list of all valid MinuteOfHourStrategy permutations
*/
public static List<Strategy> buildAllStrategyPermutations(BarSeries series) {
List<String[]> permutations = new ArrayList<>();
for (int entryMinute = 0; entryMinute < 60; entryMinute++) {
for (int exitMinute = 0; exitMinute < 60; exitMinute++) {
if (entryMinute != exitMinute) {
permutations.add(new String[] { String.valueOf(entryMinute), String.valueOf(exitMinute) });
}
}
}
return NamedStrategy.buildAllStrategyPermutations(series, permutations, MinuteOfHourStrategy::new,
(params, error) -> {
String entry = params.length > 0 ? params[0] : "<missing>";
String exit = params.length > 1 ? params[1] : "<missing>";
LogManager.getLogger()
.warn("Failed to build strategy for entry minute {} and exit minute {} - {}", entry, exit,
error.getMessage());
});
}
private static int parseEntryMinute(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 1) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryMinute, exitMinute), but got " + params.length);
}
try {
int minute = Integer.parseInt(params[0]);
if (minute < 0 || minute > 59) {
throw new IllegalArgumentException("Invalid entry minute value: '" + params[0]
+ "'. Valid values are integers in the range 0-59 (inclusive)");
}
return minute;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid entry minute value: '" + params[0]
+ "'. Valid values are integers in the range 0-59 (inclusive)", e);
}
}
private static int parseExitMinute(String... params) {
if (params == null) {
throw new IllegalArgumentException("Params cannot be null");
}
if (params.length < 2) {
throw new IllegalArgumentException(
"At least 2 parameters required (entryMinute, exitMinute), but got " + params.length);
}
try {
int minute = Integer.parseInt(params[1]);
if (minute < 0 || minute > 59) {
throw new IllegalArgumentException("Invalid exit minute value: '" + params[1]
+ "'. Valid values are integers in the range 0-59 (inclusive)");
}
return minute;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid exit minute value: '" + params[1]
+ "'. Valid values are integers in the range 0-59 (inclusive)", e);
}
}
}
@@ -0,0 +1,114 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
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.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.MACDIndicator;
import org.ta4j.core.indicators.StochasticOscillatorKIndicator;
import org.ta4j.core.indicators.averages.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
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.BitStampCsvTradesFileBarSeriesDataSource;
/**
* Moving momentum strategy.
*
* @see <a href=
* "http://stockcharts.com/help/doku.php?id=chart_school:trading_strategies:moving_momentum">
* http://stockcharts.com/help/doku.php?id=chart_school:trading_strategies:moving_momentum</a>
*/
public class MovingMomentumStrategy {
private static final Logger LOG = LogManager.getLogger(MovingMomentumStrategy.class);
/**
* @param series the bar series
* @return the moving momentum strategy
*/
public static Strategy buildStrategy(BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
// The bias is bullish when the shorter-moving average moves above the longer
// moving average.
// The bias is bearish when the shorter-moving average moves below the longer
// moving average.
EMAIndicator shortEma = new EMAIndicator(closePrice, 9);
EMAIndicator longEma = new EMAIndicator(closePrice, 26);
StochasticOscillatorKIndicator stochasticOscillK = new StochasticOscillatorKIndicator(series, 14);
MACDIndicator macd = new MACDIndicator(closePrice, 9, 26);
EMAIndicator emaMacd = new EMAIndicator(macd, 18);
// Entry rule
Rule entryRule = new OverIndicatorRule(shortEma, longEma) // Trend
.and(new CrossedDownIndicatorRule(stochasticOscillK, 20)) // Signal 1
.and(new OverIndicatorRule(macd, emaMacd)); // Signal 2
// Exit rule
Rule exitRule = new UnderIndicatorRule(shortEma, longEma) // Trend
.and(new CrossedUpIndicatorRule(stochasticOscillK, 80)) // Signal 1
.and(new UnderIndicatorRule(macd, emaMacd)); // Signal 2
String strategyName = "MovingMomentumStrategy";
return new BaseStrategy(strategyName, entryRule, exitRule);
}
public static void main(String[] args) {
// Getting the bar series
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
// Building the trading strategy
Strategy strategy = buildStrategy(series);
// Running the strategy
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
// Analysis
var grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
EMAIndicator shortEma = new EMAIndicator(closePrice, 9);
EMAIndicator longEma = new EMAIndicator(closePrice, 26);
MACDIndicator macd = new MACDIndicator(closePrice, 9, 26);
EMAIndicator emaMacd = new EMAIndicator(macd, 18);
StochasticOscillatorKIndicator stochasticOscillK = new StochasticOscillatorKIndicator(series, 14);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(shortEma)
.withIndicatorOverlay(longEma)
.withSubChart(macd)
.withIndicatorOverlay(emaMacd)
.withSubChart(stochasticOscillK)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "moving-momentum-strategy", "temp/charts");
}
}
@@ -0,0 +1,101 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
import org.ta4j.core.indicators.NetMomentumIndicator;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import java.awt.Color;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.JsonFileBarSeriesDataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
public class NetMomentumStrategy {
private static final Logger LOG = LogManager.getLogger(NetMomentumStrategy.class);
private static final int DEFAULT_OVERBOUGHT_THRESHOLD = 900;
private static final int DEFAULT_MOMENTUM_TIMEFRAME = 200;
private static final int DEFAULT_OVERSOLD_THRESHOLD = -200;
private static final int DEFAULT_RSI_BARCOUNT = 14;
private static final double DEFAULT_DECAY_FACTOR = 1;
public static void main(String[] args) {
String jsonOhlcResourceFile = "Coinbase-ETH-USD-PT1D-20160517_20251028.json";
BarSeries series = null;
try (InputStream resourceStream = NetMomentumStrategy.class.getClassLoader()
.getResourceAsStream(jsonOhlcResourceFile)) {
series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(resourceStream);
} catch (IOException ex) {
LOG.error("IOException while loading resource: {} - {}", jsonOhlcResourceFile, ex.getMessage());
}
Objects.requireNonNull(series, "Bar series was null");
// Running the strategy
runSingleStrategy(series);
}
private static void runSingleStrategy(BarSeries series) {
BarSeriesManager seriesManager = new BarSeriesManager(series);
ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series);
RSIIndicator rsiIndicator = new RSIIndicator(closePriceIndicator, DEFAULT_RSI_BARCOUNT);
NetMomentumIndicator rsiM = NetMomentumIndicator.forRsiWithDecay(rsiIndicator, DEFAULT_MOMENTUM_TIMEFRAME,
DEFAULT_DECAY_FACTOR);
Strategy strategy = createStrategy(rsiM);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
var netProfitLoss = new NetProfitLossCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s net profit/loss: {}", strategy.getName(), netProfitLoss);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withAnalysisCriterionOverlay(new NetProfitCriterion(), tradingRecord)
.withSubChart(rsiIndicator)
.withHorizontalMarker(50)
.withLineColor(Color.GRAY)
.withOpacity(0.3f)
.withHorizontalMarker(70)
.withLineColor(Color.RED)
.withOpacity(0.3f)
.withHorizontalMarker(30)
.withLineColor(Color.GREEN)
.withOpacity(0.3f)
.withSubChart(rsiM)
.withHorizontalMarker(0)
.withLineColor(Color.GRAY)
.withOpacity(0.3f)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "net-momentum-strategy", "temp/charts");
}
private static Strategy createStrategy(NetMomentumIndicator rsiM) {
Rule entryRule = new CrossedUpIndicatorRule(rsiM, DEFAULT_OVERSOLD_THRESHOLD);
Rule exitRule = new CrossedDownIndicatorRule(rsiM, DEFAULT_OVERBOUGHT_THRESHOLD);
return new BaseStrategy(NetMomentumStrategy.class.getSimpleName(), entryRule, exitRule);
}
}
@@ -0,0 +1,107 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
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.pnl.GrossReturnCriterion;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
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.BitStampCsvTradesFileBarSeriesDataSource;
/**
* 2-Period RSI Strategy
*
* @see <a href=
* "http://stockcharts.com/school/doku.php?id=chart_school:trading_strategies:rsi2">
* http://stockcharts.com/school/doku.php?id=chart_school:trading_strategies:rsi2</a>
*/
public class RSI2Strategy {
private static final Logger LOG = LogManager.getLogger(RSI2Strategy.class);
/**
* @param series a bar series
* @return a 2-period RSI strategy
*/
public static Strategy buildStrategy(BarSeries series) {
if (series == null) {
throw new IllegalArgumentException("Series cannot be null");
}
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
SMAIndicator shortSma = new SMAIndicator(closePrice, 5);
SMAIndicator longSma = new SMAIndicator(closePrice, 200);
// We use a 2-period RSI indicator to identify buying
// or selling opportunities within the bigger trend.
RSIIndicator rsi = new RSIIndicator(closePrice, 2);
// Entry rule
// The long-term trend is up when a security is above its 200-period SMA.
Rule entryRule = new OverIndicatorRule(shortSma, longSma) // Trend
.and(new CrossedDownIndicatorRule(rsi, 5)) // Signal 1
.and(new OverIndicatorRule(shortSma, closePrice)); // Signal 2
// Exit rule
// The long-term trend is down when a security is below its 200-period SMA.
Rule exitRule = new UnderIndicatorRule(shortSma, longSma) // Trend
.and(new CrossedUpIndicatorRule(rsi, 95)) // Signal 1
.and(new UnderIndicatorRule(shortSma, closePrice)); // Signal 2
String strategyName = "RSI2Strategy";
return new BaseStrategy(strategyName, entryRule, exitRule);
}
public static void main(String[] args) {
// Getting the bar series
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
// Building the trading strategy
Strategy strategy = buildStrategy(series);
// Running the strategy
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug(() -> strategy.toJson());
LOG.debug("{}'s number of positions: {}", strategy.getName(), tradingRecord.getPositionCount());
// Analysis
var grossReturn = new GrossReturnCriterion().calculate(series, tradingRecord);
LOG.debug("{}'s gross return: {}", strategy.getName(), grossReturn);
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
SMAIndicator shortSma = new SMAIndicator(closePrice, 5);
SMAIndicator longSma = new SMAIndicator(closePrice, 200);
RSIIndicator rsiOverlay = new RSIIndicator(closePrice, 2);
// Charting
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(shortSma)
.withIndicatorOverlay(longSma)
.withAnalysisCriterionOverlay(new GrossReturnCriterion(), tradingRecord)
.withSubChart(rsiOverlay)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "rsi2-strategy", "temp/charts");
}
}
@@ -0,0 +1,90 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.strategies;
import java.time.Duration;
import java.time.Instant;
import java.util.stream.Stream;
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.Indicator;
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.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.helpers.UnstableIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
public class UnstableIndicatorStrategy {
private static final Logger LOG = LogManager.getLogger(UnstableIndicatorStrategy.class);
public static final Duration MINUTE = Duration.ofMinutes(1);
public static final Instant TIME = Instant.parse("2020-01-01T00:00:00Z");
public static Strategy buildStrategy(BarSeries series) {
ClosePriceIndicator close = new ClosePriceIndicator(series);
int smaPeriod = 3;
Indicator<Num> sma = new UnstableIndicator(new SMAIndicator(close, smaPeriod), smaPeriod - 1);
Rule entryRule = new CrossedUpIndicatorRule(close, sma);
Rule exitRule = new CrossedDownIndicatorRule(close, sma);
BaseStrategy strategy = new BaseStrategy(entryRule, exitRule);
strategy.setUnstableBars(3);
return strategy;
}
public static void main(String[] args) {
inappropriateTrade();
appropriateTrade();
}
public static void inappropriateTrade() {
// Should not trade
test("Inappropriate trade", Stream.of(10d, 2d, 6d, 16d, 8d));
}
public static void appropriateTrade() {
// Should trade
test("Appropriate trade", Stream.of(10d, 8d, 6d, 16d, 8d));
}
public static void test(String name, Stream<Double> closePrices) {
// Getting the bar series
BarSeries series = new BaseBarSeriesBuilder().build();
Instant[] currentTime = { TIME };
closePrices.forEach(close -> {
series.barBuilder()
.timePeriod(MINUTE)
.endTime(currentTime[0])
.openPrice(0)
.closePrice(close)
.highPrice(0)
.lowPrice(0)
.add();
currentTime[0] = currentTime[0].plus(MINUTE);
});
// Building the trading strategy
Strategy strategy = buildStrategy(series);
// Running the strategy
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
LOG.debug("{} {}", name, tradingRecord.getPositions());
}
}