255 lines
9.2 KiB
Java
255 lines
9.2 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* <p>
|
|
* This entry point replaces multiple thin wrappers by supporting:
|
|
* <ul>
|
|
* <li>Ossified presets mapped to bundled datasets</li>
|
|
* <li>Live runs where users can specify any ticker</li>
|
|
* </ul>
|
|
*
|
|
* <p>
|
|
* Usage:
|
|
* <ul>
|
|
* <li>{@code ossified <btc|eth|sp500>}</li>
|
|
* <li>{@code live <Coinbase|YahooFinance> <ticker> [barDuration] [lookbackDays] [degree]}</li>
|
|
* </ul>
|
|
*
|
|
* <p>
|
|
* Examples:
|
|
* <ul>
|
|
* <li>{@code ossified btc}</li>
|
|
* <li>{@code live Coinbase BTC-USD PT1D 1825}</li>
|
|
* <li>{@code live YahooFinance AAPL PT1D 1460 PRIMARY}</li>
|
|
* </ul>
|
|
*
|
|
* @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<OssifiedPreset> 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 <btc|eth|sp500>");
|
|
LOG.info(" live <Coinbase|YahooFinance> <ticker> [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<ElliottDegree> degreeOverride;
|
|
|
|
OssifiedPreset(String resource, String seriesName, Optional<ElliottDegree> degreeOverride) {
|
|
this.resource = resource;
|
|
this.seriesName = seriesName;
|
|
this.degreeOverride = degreeOverride;
|
|
}
|
|
|
|
static Optional<OssifiedPreset> 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<ElliottDegree> degreeOverride() {
|
|
return degreeOverride;
|
|
}
|
|
}
|
|
}
|