Files
goldenChart/ta4j-master/ta4j-examples/src/test/java/ta4jexamples/analysis/elliottwave/ElliottWaveAnalysisTest.java
T
2026-05-23 15:11:48 +09:00

607 lines
28 KiB
Java

/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.analysis.elliottwave;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.awt.GraphicsEnvironment;
import java.io.InputStream;
import java.time.Duration;
import java.util.Optional;
import java.time.Instant;
import java.util.List;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.JFreeChart;
import org.junit.Assume;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.ta4j.core.indicators.elliott.ElliottFibonacciValidator;
import org.ta4j.core.indicators.elliott.ElliottSwingIndicator;
import org.ta4j.core.indicators.elliott.ElliottPhaseIndicator;
import org.ta4j.core.indicators.elliott.ElliottScenario;
import org.ta4j.core.indicators.elliott.ElliottDegree;
import org.ta4j.core.indicators.elliott.ElliottSwing;
import org.ta4j.core.indicators.elliott.ElliottPhase;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.BarSeries;
import ta4jexamples.charting.annotation.BarSeriesLabelIndicator.LabelPlacement;
import ta4jexamples.charting.annotation.BarSeriesLabelIndicator.BarLabel;
import ta4jexamples.charting.annotation.BarSeriesLabelIndicator;
import ta4jexamples.charting.display.SwingChartDisplayer;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.JsonFileBarSeriesDataSource;
import ta4jexamples.datasources.BarSeriesDataSource;
class ElliottWaveAnalysisTest {
private static final String OSSIFIED_OHLCV_RESOURCE = "Coinbase-BTC-USD-PT1D-20230616_20231011.json";
private static final double FIB_TOLERANCE = 0.25;
@BeforeEach
void setUp() {
// Disable chart display to prevent windows from appearing in tests
// This allows tests to create charts without failing in headless environments
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
}
@AfterEach
void tearDown() {
// Clear the property after each test
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
}
@Test
@Tag("analysis-demo")
void ossifiedDatasetProducesImpulseAndCorrection() {
BarSeries series = loadOssifiedSeries();
ElliottSwingIndicator swingIndicator = ElliottSwingIndicator.zigZag(series, ElliottDegree.PRIMARY);
ElliottFibonacciValidator validator = new ElliottFibonacciValidator(series.numFactory(),
series.numFactory().numOf(FIB_TOLERANCE));
ElliottPhaseIndicator phaseIndicator = new ElliottPhaseIndicator(swingIndicator, validator);
int endIndex = series.getEndIndex();
assertEquals(ElliottPhase.CORRECTIVE_C, phaseIndicator.getValue(endIndex));
assertTrue(phaseIndicator.isImpulseConfirmed(endIndex));
assertTrue(phaseIndicator.isCorrectiveConfirmed(endIndex));
assertEquals(5, phaseIndicator.impulseSwings(endIndex).size());
assertEquals(3, phaseIndicator.correctiveSwings(endIndex).size());
}
@Test
@Tag("analysis-demo")
void rendersWaveLabelsOnChart() {
Assume.assumeFalse("Headless environment", GraphicsEnvironment.isHeadless());
BarSeries series = loadOssifiedSeries();
ElliottSwingIndicator swingIndicator = ElliottSwingIndicator.zigZag(series, ElliottDegree.PRIMARY);
ElliottFibonacciValidator validator = new ElliottFibonacciValidator(series.numFactory(),
series.numFactory().numOf(FIB_TOLERANCE));
ElliottPhaseIndicator phaseIndicator = new ElliottPhaseIndicator(swingIndicator, validator);
BarSeriesLabelIndicator waveLabels = buildWaveLabels(series, phaseIndicator);
ChartWorkflow workflow = new ChartWorkflow();
JFreeChart chart = workflow.builder()
.withSeries(series)
.withIndicatorOverlay(waveLabels)
.withLabel("Wave labels")
.toChart();
CombinedDomainXYPlot combinedPlot = (CombinedDomainXYPlot) chart.getPlot();
XYPlot pricePlot = combinedPlot.getSubplots().get(0);
List<String> annotationTexts = pricePlot.getAnnotations()
.stream()
.filter(XYTextAnnotation.class::isInstance)
.map(annotation -> ((XYTextAnnotation) annotation).getText())
.toList();
assertTrue(annotationTexts.containsAll(List.of("1", "2", "3", "4", "5", "A", "B", "C")));
}
private static BarSeries loadOssifiedSeries() {
try (InputStream stream = ElliottWaveAnalysisTest.class.getClassLoader()
.getResourceAsStream(OSSIFIED_OHLCV_RESOURCE)) {
assertNotNull(stream, "Missing resource: " + OSSIFIED_OHLCV_RESOURCE);
BarSeries loaded = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(stream);
assertNotNull(loaded, "Failed to deserialize BarSeries from resource");
BarSeries series = new BaseBarSeriesBuilder().withName("BTC-USD_PT1D@Coinbase (ossified)").build();
for (int i = 0; i < loaded.getBarCount(); i++) {
series.addBar(loaded.getBar(i));
}
return series;
} catch (Exception ex) {
throw new AssertionError("Failed to load dataset", ex);
}
}
private static BarSeries syntheticSeries(String name, int barCount) {
BarSeries series = new BaseBarSeriesBuilder().withName(name).build();
Duration period = Duration.ofDays(1);
Instant start = Instant.parse("2023-01-01T00:00:00Z");
for (int index = 0; index < barCount; index++) {
series.barBuilder()
.timePeriod(period)
.endTime(start.plus(period.multipliedBy(index + 1L)))
.openPrice(100 + index)
.highPrice(103 + index)
.lowPrice(97 + index)
.closePrice(101 + index)
.volume(1_000 + index)
.add();
}
return series;
}
private static BarSeriesLabelIndicator buildWaveLabels(BarSeries series, ElliottPhaseIndicator phaseIndicator) {
int endIndex = series.getEndIndex();
List<ElliottSwing> impulse = phaseIndicator.impulseSwings(endIndex);
List<ElliottSwing> correction = phaseIndicator.correctiveSwings(endIndex);
List<BarLabel> labels = new ArrayList<>();
if (!impulse.isEmpty()) {
ElliottSwing first = impulse.get(0);
labels.add(new BarLabel(first.fromIndex(), first.fromPrice(), "", placementForPivot(!first.isRising())));
}
for (int i = 0; i < impulse.size(); i++) {
ElliottSwing swing = impulse.get(i);
labels.add(new BarLabel(swing.toIndex(), swing.toPrice(), String.valueOf(i + 1),
placementForPivot(swing.isRising())));
}
for (int i = 0; i < correction.size(); i++) {
ElliottSwing swing = correction.get(i);
String label = switch (i) {
case 0 -> "A";
case 1 -> "B";
default -> "C";
};
labels.add(new BarLabel(swing.toIndex(), swing.toPrice(), label, placementForPivot(swing.isRising())));
}
return new BarSeriesLabelIndicator(series, labels);
}
private static LabelPlacement placementForPivot(boolean isHighPivot) {
return isHighPivot ? LabelPlacement.ABOVE : LabelPlacement.BELOW;
}
private static final class StubBarSeriesDataSource implements BarSeriesDataSource {
private final boolean throwOnLoad;
private final String sourceName;
private final BarSeries series;
private StubBarSeriesDataSource(String sourceName, BarSeries series, boolean throwOnLoad) {
this.throwOnLoad = throwOnLoad;
this.sourceName = sourceName;
this.series = series;
}
@Override
public BarSeries loadSeries(String ticker, Duration interval, Instant start, Instant end) {
if (throwOnLoad) {
throw new IllegalStateException("Stubbed load failure");
}
return series;
}
@Override
public BarSeries loadSeries(String source) {
return series;
}
@Override
public String getSourceName() {
return sourceName;
}
}
@Test
void loadSeriesFromDataSource_withUnsupportedDataSource_returnsNull() {
String unsupportedDataSource = "UnsupportedSource";
String ticker = "BTC-USD";
Duration barDuration = Duration.ofDays(1);
Instant startTime = Instant.parse("2023-01-01T00:00:00Z");
Instant endTime = Instant.parse("2023-01-31T23:59:59Z");
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(unsupportedDataSource, ticker,
barDuration, startTime, endTime);
assertNull(result, "Should return null for unsupported data source");
}
@Test
void loadSeriesFromDataSource_withNullDataSource_returnsNull() {
String ticker = "BTC-USD";
Duration barDuration = Duration.ofDays(1);
Instant startTime = Instant.parse("2023-01-01T00:00:00Z");
Instant endTime = Instant.parse("2023-01-31T23:59:59Z");
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource((String) null, ticker, barDuration,
startTime, endTime);
assertNull(result, "Should return null for null data source");
}
@Test
void loadSeriesFromDataSource_withStubSeries_returnsSeries() {
BarSeries series = syntheticSeries("Stub", 8);
BarSeriesDataSource source = new StubBarSeriesDataSource("Stub", series, false);
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(source, "BTC-USD", Duration.ofDays(1),
Instant.parse("2023-01-01T00:00:00Z"), Instant.parse("2023-01-31T23:59:59Z"));
assertEquals(series, result, "Should return the stubbed series");
}
@Test
void loadSeriesFromDataSource_withStubReturningNull_returnsNull() {
BarSeriesDataSource source = new StubBarSeriesDataSource("Stub", null, false);
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(source, "BTC-USD", Duration.ofDays(1),
Instant.parse("2023-01-01T00:00:00Z"), Instant.parse("2023-01-31T23:59:59Z"));
assertNull(result, "Should return null when the data source returns null");
}
@Test
void loadSeriesFromDataSource_withStubReturningEmpty_returnsNull() {
BarSeries emptySeries = new BaseBarSeriesBuilder().withName("Empty").build();
BarSeriesDataSource source = new StubBarSeriesDataSource("Stub", emptySeries, false);
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(source, "BTC-USD", Duration.ofDays(1),
Instant.parse("2023-01-01T00:00:00Z"), Instant.parse("2023-01-31T23:59:59Z"));
assertNull(result, "Should return null when the data source returns an empty series");
}
@Test
void loadSeriesFromDataSource_withStubThrowingException_returnsNull() {
BarSeriesDataSource source = new StubBarSeriesDataSource("Stub", null, true);
BarSeries result = ElliottWaveIndicatorSuiteDemo.loadSeriesFromDataSource(source, "BTC-USD", Duration.ofDays(1),
Instant.parse("2023-01-01T00:00:00Z"), Instant.parse("2023-01-31T23:59:59Z"));
assertNull(result, "Should return null when the data source throws");
}
@Test
void parseBarSeriesRequest_withDayBasedDuration_convertsToHours() {
String[] args = { "Coinbase", "BTC-USD", "PT1D", "1686960000", "1697040000" };
Optional<ElliottWaveIndicatorSuiteDemo.BarSeriesRequest> request = ElliottWaveIndicatorSuiteDemo
.parseBarSeriesRequest(args);
assertTrue(request.isPresent(), "Request should parse successfully");
assertEquals(Duration.ofDays(1), request.get().barDuration(), "Duration should normalize to 24 hours");
}
@Test
void parseBarSeriesRequest_withInvalidEpoch_returnsEmpty() {
String[] args = { "Coinbase", "BTC-USD", "PT1D", "invalid-epoch" };
Optional<ElliottWaveIndicatorSuiteDemo.BarSeriesRequest> request = ElliottWaveIndicatorSuiteDemo
.parseBarSeriesRequest(args);
assertTrue(request.isEmpty(), "Invalid epoch should return empty request");
}
@Test
void parseBarSeriesRequest_withInvalidDataSource_returnsEmpty() {
String[] args = { "InvalidSource", "BTC-USD", "PT1D", "1686960000", "1697040000" };
Optional<ElliottWaveIndicatorSuiteDemo.BarSeriesRequest> request = ElliottWaveIndicatorSuiteDemo
.parseBarSeriesRequest(args);
assertTrue(request.isEmpty(), "Invalid data source should return empty request");
}
@Test
void parseBarSeriesRequest_withInvalidTimeRange_returnsEmpty() {
String[] args = { "Coinbase", "BTC-USD", "PT1D", "1697040000", "1686960000" };
Optional<ElliottWaveIndicatorSuiteDemo.BarSeriesRequest> request = ElliottWaveIndicatorSuiteDemo
.parseBarSeriesRequest(args);
assertTrue(request.isEmpty(), "Invalid time range should return empty request");
}
@Test
void resolveDegree_withExplicitDegree_usesProvidedDegree() {
BarSeries series = syntheticSeries("Degree", 90);
String[] args = { "Coinbase", "BTC-USD", "PT1D", "PRIMARY", "1686960000", "1697040000" };
ElliottDegree resolved = ElliottWaveIndicatorSuiteDemo.resolveDegree(args, series);
assertEquals(ElliottDegree.PRIMARY, resolved, "Explicit degree should be used");
}
@Test
void resolveDegree_withCaseInsensitiveDegree_usesProvidedDegree() {
BarSeries series = syntheticSeries("Degree", 90);
String[] args = { "Coinbase", "BTC-USD", "PT1D", "primary", "1686960000", "1697040000" };
ElliottDegree resolved = ElliottWaveIndicatorSuiteDemo.resolveDegree(args, series);
assertEquals(ElliottDegree.PRIMARY, resolved, "Degree should be parsed case-insensitively");
}
@Test
void resolveDegree_withoutExplicitDegree_usesRecommendation() {
BarSeries series = syntheticSeries("Degree", 90);
String[] args = { "Coinbase", "BTC-USD", "PT1D", "1686960000", "1697040000" };
List<ElliottDegree> recommendations = ElliottDegree.getRecommendedDegrees(series.getFirstBar().getTimePeriod(),
series.getBarCount());
ElliottDegree resolved = ElliottWaveIndicatorSuiteDemo.resolveDegree(args, series);
if (recommendations.isEmpty()) {
assertEquals(ElliottDegree.PRIMARY, resolved, "Default degree should be used when none recommended");
} else {
assertEquals(recommendations.get(0), resolved, "Recommended degree should be used");
}
}
@Test
@Tag("analysis-demo")
void analyze_withValidSeries_completesSuccessfully() {
// Test that analyze() completes successfully and generates wave labels
// This tests buildWaveLabelsFromScenario and placementForPivot indirectly
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Should not throw exception
analysis.analyze(series, ElliottDegree.PRIMARY, FIB_TOLERANCE);
// The analyze method internally calls buildWaveLabelsFromScenario
// We can verify it worked by checking that charts were generated
// (though we can't directly access the labels, we know they were created)
}
@Test
@Tag("analysis-demo")
void analyze_withDifferentScenarioTypes_generatesAppropriateLabels() {
// Test that analyze() handles different scenario types
// This indirectly tests buildWaveLabelsFromScenario with different types
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Test with different degrees which may produce different scenario types
analysis.analyze(series, ElliottDegree.PRIMARY, FIB_TOLERANCE);
analysis.analyze(series, ElliottDegree.INTERMEDIATE, FIB_TOLERANCE);
analysis.analyze(series, ElliottDegree.MINOR, FIB_TOLERANCE);
// Should complete without exception, indicating label generation worked
}
@Test
@Tag("analysis-demo")
void analyze_withDifferentDegrees_completesSuccessfully() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Test with different degrees
analysis.analyze(series, ElliottDegree.INTERMEDIATE, FIB_TOLERANCE);
analysis.analyze(series, ElliottDegree.MINOR, FIB_TOLERANCE);
}
@Test
@Tag("analysis-demo")
void analyze_withDifferentFibTolerances_completesSuccessfully() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Test with different tolerances
analysis.analyze(series, ElliottDegree.PRIMARY, 0.1);
analysis.analyze(series, ElliottDegree.PRIMARY, 0.5);
}
@Test
@Tag("analysis-demo")
void analyze_returnsAnalysisResult() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
assertNotNull(result, "Analysis result should not be null");
assertEquals(series, result.series(), "Series should match");
assertEquals(ElliottDegree.PRIMARY, result.degree(), "Degree should match");
assertNotNull(result.phaseIndicator(), "Phase indicator should not be null");
assertNotNull(result.invalidationIndicator(), "Invalidation indicator should not be null");
assertNotNull(result.channelIndicator(), "Channel indicator should not be null");
assertNotNull(result.ratioIndicator(), "Ratio indicator should not be null");
assertNotNull(result.confluenceIndicator(), "Confluence indicator should not be null");
assertNotNull(result.swingCount(), "Swing count indicator should not be null");
assertNotNull(result.filteredSwingCount(), "Filtered swing count indicator should not be null");
assertNotNull(result.scenarioIndicator(), "Scenario indicator should not be null");
assertNotNull(result.swingMetadata(), "Swing metadata should not be null");
assertNotNull(result.scenarioSet(), "Scenario set should not be null");
assertNotNull(result.ratioValue(), "Ratio value indicator should not be null");
assertNotNull(result.swingCountAsNum(), "Swing count as num indicator should not be null");
assertNotNull(result.filteredSwingCountAsNum(), "Filtered swing count as num indicator should not be null");
assertNotNull(result.baseCaseChartPlan(), "Base case chart plan optional should not be null");
assertNotNull(result.alternativeChartPlans(), "Alternative chart plans list should not be null");
assertNotNull(result.structuredResult(), "Structured result should not be null");
assertNotNull(result.structuredResult().swingSnapshot(), "Structured result swing snapshot should not be null");
assertNotNull(result.structuredResult().latestAnalysis(),
"Structured result latest analysis should not be null");
assertNotNull(result.structuredResult().scenarioSummary(),
"Structured result scenario summary should not be null");
}
@Test
@Tag("analysis-demo")
void analyze_withBaseCaseScenario_createsBaseCaseChartPlan() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
assertTrue(result.baseCaseChartPlan().isPresent(),
"Base case chart plan should be present when base case scenario exists");
assertNotNull(result.baseCaseChartPlan().get(), "Base case chart plan should not be null");
assertNotNull(result.scenarioSet().base(), "Base case scenario should exist");
}
@Test
@Tag("analysis-demo")
void analyze_createsChartPlansForAllScenarios() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
// Verify base case chart plan exists if base case scenario exists
if (result.scenarioSet().base().isPresent()) {
assertTrue(result.baseCaseChartPlan().isPresent(),
"Base case chart plan should exist when base case scenario exists");
}
// Verify alternative chart plans match alternative scenarios
int alternativeCount = result.scenarioSet().alternatives().size();
assertEquals(alternativeCount, result.alternativeChartPlans().size(),
"Number of alternative chart plans should match number of alternative scenarios");
}
@Test
@Tag("analysis-demo")
void visualizeAnalysisResult_withValidResult_completesSuccessfully() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
// Should not throw exception
analysis.visualizeAnalysisResult(result);
}
@Test
void visualizeAnalysisResult_withNullResult_throwsNullPointerException() {
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
NullPointerException exception = assertThrows(NullPointerException.class,
() -> analysis.visualizeAnalysisResult(null));
assertNotNull(exception.getMessage(), "Exception message should not be null");
assertTrue(exception.getMessage().contains("Analysis result cannot be null"),
"Exception message should indicate null result");
}
@Test
@Tag("analysis-demo")
void visualizeAnalysisResult_withDifferentDegrees_formatsWindowTitlesCorrectly() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Test with PRIMARY degree
ElliottWaveIndicatorSuiteDemo.AnalysisResult primaryResult = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
if (primaryResult.baseCaseChartPlan().isPresent() && primaryResult.scenarioSet().base().isPresent()) {
ElliottScenario baseCase = primaryResult.scenarioSet().base().get();
String expectedBaseCaseTitle = String.format("%s - BASE CASE: %s (%s) - %.1f%% - %s", ElliottDegree.PRIMARY,
baseCase.currentPhase(), baseCase.type(), baseCase.confidence().asPercentage(), series.getName());
assertTrue(expectedBaseCaseTitle.startsWith("PRIMARY -"),
"Base case window title should start with degree");
assertTrue(expectedBaseCaseTitle.contains("BASE CASE:"),
"Base case window title should contain BASE CASE:");
}
// Test with INTERMEDIATE degree
ElliottWaveIndicatorSuiteDemo.AnalysisResult intermediateResult = analysis.analyze(series,
ElliottDegree.INTERMEDIATE, FIB_TOLERANCE);
if (intermediateResult.baseCaseChartPlan().isPresent() && intermediateResult.scenarioSet().base().isPresent()) {
ElliottScenario baseCase = intermediateResult.scenarioSet().base().get();
String expectedIntermediateTitle = String.format("%s - BASE CASE: %s (%s) - %.1f%% - %s",
ElliottDegree.INTERMEDIATE, baseCase.currentPhase(), baseCase.type(),
baseCase.confidence().asPercentage(), series.getName());
assertTrue(expectedIntermediateTitle.startsWith("INTERMEDIATE -"),
"Intermediate window title should start with degree");
}
// Test with MINOR degree
ElliottWaveIndicatorSuiteDemo.AnalysisResult minorResult = analysis.analyze(series, ElliottDegree.MINOR,
FIB_TOLERANCE);
if (minorResult.baseCaseChartPlan().isPresent() && minorResult.scenarioSet().base().isPresent()) {
ElliottScenario baseCase = minorResult.scenarioSet().base().get();
String expectedMinorTitle = String.format("%s - BASE CASE: %s (%s) - %.1f%% - %s", ElliottDegree.MINOR,
baseCase.currentPhase(), baseCase.type(), baseCase.confidence().asPercentage(), series.getName());
assertTrue(expectedMinorTitle.startsWith("MINOR -"), "Minor window title should start with degree");
}
}
@Test
@Tag("analysis-demo")
void visualizeAnalysisResult_withAlternativeScenarios_formatsWindowTitlesCorrectly() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
List<ElliottScenario> alternatives = result.scenarioSet().alternatives();
for (int i = 0; i < alternatives.size() && i < result.alternativeChartPlans().size(); i++) {
ElliottScenario alt = alternatives.get(i);
String expectedAltTitle = String.format("%s - ALTERNATIVE %d: %s (%s) - %.1f%% - %s", result.degree(),
i + 1, alt.currentPhase(), alt.type(), alt.confidence().asPercentage(), series.getName());
assertTrue(expectedAltTitle.startsWith(result.degree().toString() + " -"),
"Alternative window title should start with degree");
assertTrue(expectedAltTitle.contains("ALTERNATIVE " + (i + 1) + ":"),
"Alternative window title should contain alternative number");
}
}
@Test
void analyze_withEmptySeries_throwsIllegalArgumentException() {
BarSeries emptySeries = new BaseBarSeriesBuilder().withName("Empty").build();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> analysis.analyze(emptySeries, ElliottDegree.PRIMARY, FIB_TOLERANCE));
assertNotNull(exception.getMessage(), "Exception message should not be null");
assertTrue(exception.getMessage().contains("Series cannot be empty"),
"Exception message should indicate empty series");
}
@Test
void analyze_withNullSeries_throwsNullPointerException() {
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
NullPointerException exception = assertThrows(NullPointerException.class,
() -> analysis.analyze(null, ElliottDegree.PRIMARY, FIB_TOLERANCE));
assertNotNull(exception.getMessage(), "Exception message should not be null");
assertTrue(exception.getMessage().contains("Series cannot be null"),
"Exception message should indicate null series");
}
@Test
@Tag("analysis-demo")
void analyze_separatesAnalysisFromVisualization() {
BarSeries series = loadOssifiedSeries();
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
// Analyze without visualization
ElliottWaveIndicatorSuiteDemo.AnalysisResult result = analysis.analyze(series, ElliottDegree.PRIMARY,
FIB_TOLERANCE);
// Verify analysis completed
assertNotNull(result);
assertNotNull(result.scenarioSet());
// Now visualize separately
analysis.visualizeAnalysisResult(result);
// Both should complete without exception
}
}