/* * 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. * *

* Entry criteria: *

* *

* Exit criteria: *

* * @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 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 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 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 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 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 enum type * @return parsed enum */ private static > E parseEnum(final String value, final Class type, final String label) { try { return Enum.valueOf(type, value); } catch (IllegalArgumentException ex) { Set 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 enum type * @return set of enum names */ private static > Set enumNames(final Class 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 { 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 swings = swingIndicator.getValue(clampedIndex); if (compressor != null) { swings = compressor.compress(swings); } if (swings.isEmpty()) { return ElliottScenarioSet.empty(clampedIndex); } List 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(); } } }