/* * SPDX-License-Identifier: MIT */ package ta4jexamples.analysis.elliottwave; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.Locale; import java.util.Objects; import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ta4j.core.BarSeries; import org.ta4j.core.indicators.elliott.ElliottDegree; import ta4jexamples.analysis.elliottwave.backtest.ElliottWaveBtcMacroCycleDemo; /** * Consolidated preset launcher for Elliott Wave demos. * *

* This entry point replaces multiple thin wrappers by supporting: *

* *

* Usage: *

* *

* Examples: *

* * @since 0.22.4 */ public class ElliottWavePresetDemo { private static final Logger LOG = LogManager.getLogger(ElliottWavePresetDemo.class); private static final String DEFAULT_BAR_DURATION = "PT1D"; private static final long DEFAULT_LOOKBACK_DAYS = 1825L; private static final Path DEFAULT_BTC_MACRO_CHART_DIRECTORY = Path.of("temp", "charts"); /** * Runs a preset Elliott Wave demo. * * @param args command-line arguments; see class-level usage documentation */ public static void main(String[] args) { if (args == null || args.length == 0) { logUsage(); return; } final String mode = normalize(args[0]); switch (mode) { case "ossified": runOssified(args); return; case "live": runLive(args); return; default: LOG.error("Unknown mode '{}'. Expected 'ossified' or 'live'.", args[0]); logUsage(); } } private static void runOssified(String[] args) { if (args.length < 2) { LOG.error("Missing ossified preset. Expected one of: btc, eth, sp500"); logUsage(); return; } Optional preset = OssifiedPreset.fromToken(args[1]); if (preset.isEmpty()) { LOG.error("Unknown ossified preset '{}'. Expected one of: btc, eth, sp500", args[1]); logUsage(); return; } OssifiedPreset selected = preset.orElseThrow(); ElliottWaveIndicatorSuiteDemo.runOssifiedResource(ElliottWavePresetDemo.class, selected.resource(), selected.seriesName(), selected.degreeOverride().orElse(null)); } private static void runLive(String[] args) { if (args.length < 3) { LOG.error("Live mode expects at least dataSource and ticker"); logUsage(); return; } final String dataSource = Objects.requireNonNull(args[1], "dataSource"); final String ticker = Objects.requireNonNull(args[2], "ticker"); final String barDuration = args.length > 3 ? args[3] : DEFAULT_BAR_DURATION; long lookbackDays = DEFAULT_LOOKBACK_DAYS; if (args.length > 4) { try { lookbackDays = Long.parseLong(args[4]); } catch (NumberFormatException ex) { LOG.error("Invalid lookbackDays '{}': expected a positive integer", args[4]); return; } } if (lookbackDays <= 0) { LOG.error("Invalid lookbackDays '{}': must be > 0", lookbackDays); return; } ElliottDegree degree = null; if (args.length > 5) { try { degree = ElliottDegree.valueOf(args[5].trim().toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { LOG.error("Invalid degree '{}'", args[5]); return; } } if (shouldUseBtcMacroPreset(ticker, barDuration)) { if (degree != null) { LOG.info( "Ignoring explicit degree '{}' for BTC daily macro preset; the validated macro profile selects its own structural interpretation.", degree); } Instant endTime = Instant.now(); Instant startTime = endTime.minus(Duration.ofDays(lookbackDays)); Duration parsedDuration = parseBarDuration(barDuration); BarSeries series = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(dataSource, ticker, parsedDuration, startTime, endTime); if (series == null || series.isEmpty()) { LOG.error("Unable to load live BTC series for macro preset from {} {} {}", dataSource, ticker, barDuration); return; } ElliottWaveBtcMacroCycleDemo.runLivePreset(series, DEFAULT_BTC_MACRO_CHART_DIRECTORY); return; } String[] suiteArgs = buildLiveSuiteArgs(dataSource, ticker, barDuration, lookbackDays, Instant.now(), degree); ElliottWaveIndicatorSuiteDemo.main(suiteArgs); } static boolean shouldUseBtcMacroPreset(String ticker, String barDuration) { if (ticker == null || barDuration == null) { return false; } String normalizedTicker = ticker.trim().toUpperCase(Locale.ROOT); String normalizedDuration = barDuration.trim().toUpperCase(Locale.ROOT); return "BTC-USD".equals(normalizedTicker) && ("PT1D".equals(normalizedDuration) || "PT24H".equals(normalizedDuration)); } static String[] buildLiveSuiteArgs(String dataSource, String ticker, String barDuration, long lookbackDays, Instant endTime, ElliottDegree degree) { Objects.requireNonNull(dataSource, "dataSource"); Objects.requireNonNull(ticker, "ticker"); Objects.requireNonNull(barDuration, "barDuration"); Objects.requireNonNull(endTime, "endTime"); if (lookbackDays <= 0) { throw new IllegalArgumentException("lookbackDays must be > 0"); } Instant startTime = endTime.minus(Duration.ofDays(lookbackDays)); if (degree == null) { return new String[] { dataSource, ticker, barDuration, Long.toString(startTime.getEpochSecond()), Long.toString(endTime.getEpochSecond()) }; } return new String[] { dataSource, ticker, barDuration, degree.name(), Long.toString(startTime.getEpochSecond()), Long.toString(endTime.getEpochSecond()) }; } private static void logUsage() { LOG.info("Usage:"); LOG.info(" ossified "); LOG.info(" live [barDuration] [lookbackDays] [degree]"); } private static Duration parseBarDuration(String barDuration) { Objects.requireNonNull(barDuration, "barDuration"); String normalized = barDuration.trim().toUpperCase(Locale.ROOT); if (normalized.startsWith("PT") && normalized.endsWith("D")) { final String dayString = normalized.substring(2, normalized.length() - 1); try { final int days = Integer.parseInt(dayString); return Duration.ofHours(days * 24L); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid day count in barDuration: " + barDuration, e); } } return Duration.parse(normalized); } private static String normalize(String value) { if (value == null) { return ""; } return value.trim().toLowerCase(Locale.ROOT); } private enum OssifiedPreset { BTC("Coinbase-BTC-USD-PT1D-20230616_20231011.json", "BTC-USD_PT1D@Coinbase (ossified)", Optional.empty()), ETH("Coinbase-ETH-USD-PT1D-20241105_20251020.json", "ETH-USD_PT1D@Coinbase (ossified)", Optional.empty()), SP500("YahooFinance-SP500-PT1D-20230616_20231011.json", "^GSPC_PT1D@YahooFinance (ossified)", Optional.empty()); private final String resource; private final String seriesName; private final Optional degreeOverride; OssifiedPreset(String resource, String seriesName, Optional degreeOverride) { this.resource = resource; this.seriesName = seriesName; this.degreeOverride = degreeOverride; } static Optional fromToken(String token) { if (token == null) { return Optional.empty(); } String normalized = token.trim().toLowerCase(Locale.ROOT); return switch (normalized) { case "btc" -> Optional.of(BTC); case "eth" -> Optional.of(ETH); case "sp500", "s&p500", "gspc", "^gspc" -> Optional.of(SP500); default -> Optional.empty(); }; } String resource() { return resource; } String seriesName() { return seriesName; } Optional degreeOverride() { return degreeOverride; } } }