goldenChat base source add
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# ta4jexamples.analysis test instructions
|
||||
|
||||
- Regression tests under this package should assert on underlying analysis data and persisted artifacts, not rendered chart labels or screenshots, unless the visual rendering itself is the feature under test.
|
||||
- For long-running analysis/calibration flows, tests should lock down incremental persistence:
|
||||
- primary results land before portability/secondary checks
|
||||
- phase outputs survive later failures
|
||||
- final aggregate output appends rather than replaces
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.jfree.chart.plot.CombinedDomainXYPlot;
|
||||
import org.jfree.chart.plot.XYPlot;
|
||||
import org.jfree.data.time.TimeSeriesCollection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.SwingPointMarkerIndicator;
|
||||
import org.ta4j.core.indicators.supportresistance.AbstractTrendLineIndicator;
|
||||
import org.ta4j.core.indicators.supportresistance.AbstractTrendLineIndicator.TrendLineSegment;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import ta4jexamples.charting.builder.ChartPlan;
|
||||
import ta4jexamples.charting.workflow.ChartWorkflow;
|
||||
import ta4jexamples.datasources.CsvFileBarSeriesDataSource;
|
||||
|
||||
/**
|
||||
* Analysis coverage for trendline + swing point indicators on realistic data.
|
||||
*/
|
||||
class TrendLineAndSwingPointAnalysisTest {
|
||||
|
||||
@Test
|
||||
void analysisHarnessVerifiesHeadroomAndRendersOverlays() {
|
||||
TrendLineAndSwingPointAnalysis analysis = new TrendLineAndSwingPointAnalysis();
|
||||
analysis.verifyDefaultCapsHeadroomForBundledDatasets();
|
||||
|
||||
BarSeries series = CsvFileBarSeriesDataSource.loadSeriesFromFile();
|
||||
assertNotNull(series, "Example bar series should not be null");
|
||||
assertFalse(series.isEmpty(), "Example bar series should not be empty");
|
||||
|
||||
int lookback = Math.min(series.getBarCount(), TrendLineAndSwingPointAnalysis.DEFAULT_TRENDLINE_LOOKBACK);
|
||||
TrendLineAndSwingPointAnalysis.AnalysisChartArtifacts artifacts = analysis.buildAnalysisChartArtifacts(series,
|
||||
lookback, TrendLineAndSwingPointAnalysis.DEFAULT_SURROUNDING_BARS);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
TrendLineAndSwingPointAnalysis.TrendLineVariants trendLines = artifacts.trendLines();
|
||||
TrendLineAndSwingPointAnalysis.SwingMarkerVariants swings = artifacts.swings();
|
||||
|
||||
assertAll(() -> assertTrendLinePopulates(trendLines.support(), endIndex),
|
||||
() -> assertTrendLinePopulates(trendLines.supportTouchBiased(), endIndex),
|
||||
() -> assertTrendLinePopulates(trendLines.supportExtremeBiased(), endIndex),
|
||||
() -> assertTrendLinePopulates(trendLines.resistance(), endIndex),
|
||||
() -> assertTrendLinePopulates(trendLines.resistanceTouchBiased(), endIndex),
|
||||
() -> assertTrendLinePopulates(trendLines.resistanceExtremeBiased(), endIndex));
|
||||
|
||||
assertAll(() -> assertSwingMarkersPopulated(swings.fractalLows(), series),
|
||||
() -> assertSwingMarkersPopulated(swings.fractalHighs(), series),
|
||||
() -> assertSwingMarkersPopulated(swings.zigzagLows(), series),
|
||||
() -> assertSwingMarkersPopulated(swings.zigzagHighs(), series));
|
||||
|
||||
ChartWorkflow chartWorkflow = new ChartWorkflow();
|
||||
ChartPlan plan = artifacts.plan();
|
||||
JFreeChart chart = chartWorkflow.render(plan);
|
||||
assertNotNull(chart, "Rendered chart should not be null");
|
||||
|
||||
byte[] png = chartWorkflow.getChartAsByteArray(chart);
|
||||
assertTrue(png.length > 0, "Rendered chart should produce non-empty PNG bytes");
|
||||
|
||||
assertRenderedOverlaysHaveData(chart, plan);
|
||||
}
|
||||
|
||||
private void assertTrendLinePopulates(AbstractTrendLineIndicator trendLine, int endIndex) {
|
||||
trendLine.getValue(endIndex);
|
||||
TrendLineSegment segment = trendLine.getCurrentSegment();
|
||||
assertNotNull(segment, "Trendline should have an active segment at the end of the series");
|
||||
|
||||
assertTrue(segment.firstIndex < segment.secondIndex, "Trendline anchors should be ordered in time");
|
||||
assertTrue(segment.windowStart <= segment.firstIndex && segment.firstIndex <= segment.windowEnd,
|
||||
"First anchor should be inside the evaluation window");
|
||||
assertTrue(segment.secondIndex <= segment.windowEnd,
|
||||
"Second anchor should be before or at the end of the evaluation window");
|
||||
assertEquals(endIndex, segment.windowEnd, "Trendline window should be anchored at endIndex");
|
||||
|
||||
List<Integer> swingIndexes = trendLine.getSwingPointIndexes();
|
||||
assertTrue(swingIndexes.contains(segment.firstIndex), "First anchor should be a confirmed swing point");
|
||||
assertTrue(swingIndexes.contains(segment.secondIndex), "Second anchor should be a confirmed swing point");
|
||||
|
||||
Num valueAtEnd = trendLine.getValue(endIndex);
|
||||
assertNotNull(valueAtEnd, "Trendline value should not be null");
|
||||
assertFalse(valueAtEnd.isNaN(), "Trendline value should be defined at endIndex");
|
||||
|
||||
Num valueAtStart = trendLine.getValue(segment.windowStart);
|
||||
assertNotNull(valueAtStart, "Trendline value should not be null within the window");
|
||||
assertFalse(valueAtStart.isNaN(), "Trendline should backfill values within the window");
|
||||
|
||||
if (segment.windowStart > trendLine.getBarSeries().getBeginIndex()) {
|
||||
Num beforeWindow = trendLine.getValue(segment.windowStart - 1);
|
||||
assertTrue(beforeWindow.isNaN(), "Trendline should return NaN before the evaluation window");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSwingMarkersPopulated(SwingPointMarkerIndicator marker, BarSeries series) {
|
||||
int endIndex = series.getEndIndex();
|
||||
marker.getValue(endIndex);
|
||||
|
||||
List<Integer> swingIndexes = marker.getSwingIndicator().getSwingPointIndexesUpTo(endIndex);
|
||||
assertFalse(swingIndexes.isEmpty(), "Swing indicator should produce at least one swing point");
|
||||
assertStrictlyIncreasingWithinBounds(swingIndexes, series.getBeginIndex(), endIndex);
|
||||
|
||||
int firstSwing = swingIndexes.get(0);
|
||||
Num markerPrice = marker.getPriceIndicator().getValue(firstSwing);
|
||||
if (markerPrice == null || markerPrice.isNaN()) {
|
||||
markerPrice = series.getBar(firstSwing).getClosePrice();
|
||||
}
|
||||
assertNotNull(markerPrice, "Swing marker should have a price value available at a swing point index");
|
||||
assertFalse(markerPrice.isNaN(), "Swing marker should have a defined price value at a swing point index");
|
||||
|
||||
Integer nonSwingIndex = findNonSwingIndex(series.getBeginIndex(), endIndex, swingIndexes);
|
||||
assertNotNull(nonSwingIndex, "Series should contain at least one non-swing index");
|
||||
assertFalse(marker.getSwingPointIndexes().contains(nonSwingIndex),
|
||||
"Non-swing index should not be a swing point");
|
||||
}
|
||||
|
||||
private Integer findNonSwingIndex(int beginIndex, int endIndex, List<Integer> swingIndexes) {
|
||||
for (int i = beginIndex; i <= endIndex; i++) {
|
||||
if (!swingIndexes.contains(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void assertStrictlyIncreasingWithinBounds(List<Integer> indexes, int beginIndex, int endIndex) {
|
||||
Integer previous = null;
|
||||
for (Integer index : indexes) {
|
||||
assertNotNull(index, "Swing index must not be null");
|
||||
assertTrue(index >= beginIndex && index <= endIndex,
|
||||
"Swing index " + index + " must be within [" + beginIndex + ", " + endIndex + "]");
|
||||
if (previous != null) {
|
||||
assertTrue(index > previous, "Swing indexes must be strictly increasing");
|
||||
}
|
||||
previous = index;
|
||||
}
|
||||
}
|
||||
|
||||
private void assertRenderedOverlaysHaveData(JFreeChart chart, ChartPlan plan) {
|
||||
CombinedDomainXYPlot combinedPlot = assertInstanceOf(CombinedDomainXYPlot.class, chart.getPlot(),
|
||||
"Regression chart should use a combined domain plot");
|
||||
assertNotNull(combinedPlot.getSubplots(), "Combined plot should expose its subplots");
|
||||
assertFalse(combinedPlot.getSubplots().isEmpty(), "Combined plot should contain at least one subplot");
|
||||
|
||||
XYPlot basePlot = assertInstanceOf(XYPlot.class, combinedPlot.getSubplots().get(0),
|
||||
"First subplot should be an XYPlot");
|
||||
|
||||
int expectedOverlays = plan.definition().basePlot().overlays().size();
|
||||
assertEquals(1 + expectedOverlays, basePlot.getDatasetCount(), "Each overlay should map to a dataset");
|
||||
|
||||
for (int datasetIndex = 1; datasetIndex < basePlot.getDatasetCount(); datasetIndex++) {
|
||||
TimeSeriesCollection dataset = assertInstanceOf(TimeSeriesCollection.class,
|
||||
basePlot.getDataset(datasetIndex), "Overlay datasets should be time series collections");
|
||||
assertTrue(totalItemCount(dataset) > 0, "Overlay dataset " + datasetIndex + " should contain data points");
|
||||
}
|
||||
}
|
||||
|
||||
private int totalItemCount(TimeSeriesCollection dataset) {
|
||||
int total = 0;
|
||||
for (int seriesIndex = 0; seriesIndex < dataset.getSeriesCount(); seriesIndex++) {
|
||||
total += dataset.getSeries(seriesIndex).getItemCount();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.supportresistance.AbstractTrendLineIndicator.TrendLineSegment;
|
||||
import org.ta4j.core.indicators.supportresistance.TrendLineSupportIndicator;
|
||||
|
||||
import ta4jexamples.datasources.CsvFileBarSeriesDataSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TrendLinePresetDivergenceTest {
|
||||
|
||||
@Test
|
||||
void supportPresetsProduceDistinctSegmentsForAppleSeries() {
|
||||
final BarSeries series = CsvFileBarSeriesDataSource.loadSeriesFromFile();
|
||||
assertFalse(series.isEmpty(), "Example bar series should not be empty");
|
||||
|
||||
final int lookback = Math.min(series.getBarCount(), 200);
|
||||
final int surroundingBars = 5;
|
||||
final int endIndex = series.getEndIndex();
|
||||
|
||||
final TrendLineSupportIndicator defaultLine = new TrendLineSupportIndicator(series, surroundingBars, lookback);
|
||||
final TrendLineSupportIndicator touchBiasedLine = new TrendLineSupportIndicator(series, surroundingBars,
|
||||
lookback, TrendLineSupportIndicator.ScoringWeights.touchCountBiasPreset());
|
||||
final TrendLineSupportIndicator extremeBiasedLine = new TrendLineSupportIndicator(series, surroundingBars,
|
||||
lookback, TrendLineSupportIndicator.ScoringWeights.extremeSwingBiasPreset());
|
||||
|
||||
defaultLine.getValue(endIndex);
|
||||
touchBiasedLine.getValue(endIndex);
|
||||
extremeBiasedLine.getValue(endIndex);
|
||||
|
||||
final TrendLineSegment defaultSegment = defaultLine.getCurrentSegment();
|
||||
final TrendLineSegment touchSegment = touchBiasedLine.getCurrentSegment();
|
||||
final TrendLineSegment extremeSegment = extremeBiasedLine.getCurrentSegment();
|
||||
|
||||
assertAll(() -> assertNotNull(defaultSegment), () -> assertNotNull(touchSegment),
|
||||
() -> assertNotNull(extremeSegment));
|
||||
|
||||
assertAll(() -> assertEquals(123, defaultSegment.firstIndex),
|
||||
() -> assertEquals(177, defaultSegment.secondIndex), () -> assertEquals(177, touchSegment.firstIndex),
|
||||
() -> assertEquals(243, touchSegment.secondIndex), () -> assertEquals(74, extremeSegment.firstIndex),
|
||||
() -> assertEquals(123, extremeSegment.secondIndex));
|
||||
|
||||
assertNotEquals(anchorLabel(defaultSegment), anchorLabel(touchSegment));
|
||||
assertNotEquals(anchorLabel(defaultSegment), anchorLabel(extremeSegment));
|
||||
assertNotEquals(anchorLabel(touchSegment), anchorLabel(extremeSegment));
|
||||
|
||||
assertTrue(touchSegment.touchCount > defaultSegment.touchCount,
|
||||
"Touch-biased preset should prefer the line with the most swing touches");
|
||||
assertTrue(extremeSegment.touchesExtreme, "Extreme-biased preset should include the extreme swing");
|
||||
assertFalse(defaultSegment.touchesExtreme,
|
||||
"Default preset should select a line that balances proximity over extreme anchoring");
|
||||
}
|
||||
|
||||
private String anchorLabel(TrendLineSegment segment) {
|
||||
return segment.firstIndex + "-" + segment.secondIndex;
|
||||
}
|
||||
}
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* 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.math.RoundingMode;
|
||||
import java.math.BigDecimal;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Assume;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.indicators.elliott.ElliottRatio.RatioType;
|
||||
import org.ta4j.core.indicators.elliott.ElliottScenarioSet;
|
||||
import org.ta4j.core.indicators.elliott.ElliottConfidence;
|
||||
import org.ta4j.core.indicators.elliott.ElliottScenario;
|
||||
import org.ta4j.core.indicators.elliott.ElliottDegree;
|
||||
import org.ta4j.core.indicators.elliott.ElliottPhase;
|
||||
import org.ta4j.core.indicators.elliott.ElliottTrendBias;
|
||||
import org.ta4j.core.indicators.elliott.ScenarioType;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import ta4jexamples.datasources.JsonFileBarSeriesDataSource;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
@Tag("analysis-demo")
|
||||
class ElliottWaveAnalysisReportTest {
|
||||
|
||||
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
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withValidInputs_createsResult() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
// The structured result is now automatically created in analyze()
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
assertNotNull(result, "Result should not be null");
|
||||
assertEquals(ElliottDegree.PRIMARY, result.degree(), "Degree should match");
|
||||
assertEquals(series.getEndIndex(), result.endIndex(), "End index should match");
|
||||
assertNotNull(result.swingSnapshot(), "Swing snapshot should not be null");
|
||||
assertNotNull(result.latestAnalysis(), "Latest analysis should not be null");
|
||||
assertNotNull(result.scenarioSummary(), "Scenario summary should not be null");
|
||||
assertNotNull(result.probabilityCalibration(), "Probability calibration should not be null");
|
||||
assertNotNull(result.outlookGate(), "Outlook gate should not be null");
|
||||
assertNotNull(result.alternatives(), "Alternatives list should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withBaseCaseScenario_includesBaseCase() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
if (analysisResult.scenarioSet().base().isPresent()) {
|
||||
assertNotNull(result.baseCase(), "Base case should not be null when scenario exists");
|
||||
assertNotNull(result.baseCase().currentPhase(), "Base case phase should not be null");
|
||||
assertNotNull(result.baseCase().type(), "Base case type should not be null");
|
||||
assertTrue(result.baseCase().overallConfidence() >= 0 && result.baseCase().overallConfidence() <= 100,
|
||||
"Confidence should be between 0 and 100");
|
||||
assertTrue(result.baseCase().scenarioProbability() >= 0 && result.baseCase().scenarioProbability() <= 1.0,
|
||||
"Scenario probability should be between 0 and 1");
|
||||
assertTrue(
|
||||
result.baseCase().calibratedProbability() >= 0 && result.baseCase().calibratedProbability() <= 1.0,
|
||||
"Calibrated probability should be between 0 and 1");
|
||||
assertNotNull(result.baseCase().confidenceLevel(), "Confidence level should not be null");
|
||||
assertNotNull(result.baseCase().swings(), "Swings list should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withBaseCaseChartPlan_encodesChartImage() {
|
||||
Assume.assumeFalse("Headless environment", GraphicsEnvironment.isHeadless());
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
if (analysisResult.baseCaseChartPlan().isPresent()) {
|
||||
assertNotNull(result.baseCaseChartImage(), "Base case chart image should not be null");
|
||||
assertTrue(result.baseCaseChartImage().length() > 0, "Base case chart image should not be empty");
|
||||
// Verify it's valid base64
|
||||
assertTrue(isValidBase64(result.baseCaseChartImage()), "Base case chart image should be valid base64");
|
||||
} else {
|
||||
assertNull(result.baseCaseChartImage(), "Base case chart image should be null when no chart plan");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withAlternativeChartPlans_encodesChartImages() {
|
||||
Assume.assumeFalse("Headless environment", GraphicsEnvironment.isHeadless());
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
assertEquals(analysisResult.alternativeChartPlans().size(), result.alternativeChartImages().size(),
|
||||
"Number of alternative chart images should match number of chart plans");
|
||||
|
||||
for (String chartImage : result.alternativeChartImages()) {
|
||||
assertNotNull(chartImage, "Alternative chart image should not be null");
|
||||
assertTrue(chartImage.length() > 0, "Alternative chart image should not be empty");
|
||||
assertTrue(isValidBase64(chartImage), "Alternative chart image should be valid base64");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withNullDegree_throwsNullPointerException() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(null, analysisResult.swingMetadata(),
|
||||
analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex,
|
||||
analysisResult.baseCaseChartPlan(), analysisResult.alternativeChartPlans()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withNullSwingMetadata_throwsNullPointerException() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(analysisResult.degree(), null, analysisResult.phaseIndicator(),
|
||||
analysisResult.ratioIndicator(), analysisResult.channelIndicator(),
|
||||
analysisResult.confluenceIndicator(), analysisResult.invalidationIndicator(),
|
||||
analysisResult.scenarioSet(), endIndex, analysisResult.baseCaseChartPlan(),
|
||||
analysisResult.alternativeChartPlans()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withNullPhaseIndicator_throwsNullPointerException() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(analysisResult.degree(), analysisResult.swingMetadata(), null,
|
||||
analysisResult.ratioIndicator(), analysisResult.channelIndicator(),
|
||||
analysisResult.confluenceIndicator(), analysisResult.invalidationIndicator(),
|
||||
analysisResult.scenarioSet(), endIndex, analysisResult.baseCaseChartPlan(),
|
||||
analysisResult.alternativeChartPlans()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withNullScenarioSet_throwsNullPointerException() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(analysisResult.degree(), analysisResult.swingMetadata(),
|
||||
analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), null, endIndex, analysisResult.baseCaseChartPlan(),
|
||||
analysisResult.alternativeChartPlans()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withNullChartPlans_throwsNullPointerException() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(analysisResult.degree(), analysisResult.swingMetadata(),
|
||||
analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex, null,
|
||||
analysisResult.alternativeChartPlans()));
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> ElliottWaveAnalysisReport.from(analysisResult.degree(), analysisResult.swingMetadata(),
|
||||
analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex,
|
||||
analysisResult.baseCaseChartPlan(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toJson_serializesResultToValidJson() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
String json = result.toJson();
|
||||
assertNotNull(json, "JSON should not be null");
|
||||
assertTrue(json.length() > 0, "JSON should not be empty");
|
||||
assertTrue(json.contains("\"degree\""), "JSON should contain degree field");
|
||||
assertTrue(json.contains("\"endIndex\""), "JSON should contain endIndex field");
|
||||
assertTrue(json.contains("\"swingSnapshot\""), "JSON should contain swingSnapshot field");
|
||||
assertTrue(json.contains("\"latestAnalysis\""), "JSON should contain latestAnalysis field");
|
||||
assertTrue(json.contains("\"scenarioSummary\""), "JSON should contain scenarioSummary field");
|
||||
assertTrue(json.contains("\"trendBias\""), "JSON should contain trendBias field");
|
||||
assertTrue(json.contains("\"probabilityCalibration\""), "JSON should contain probabilityCalibration field");
|
||||
assertTrue(json.contains("\"outlookGate\""), "JSON should contain outlookGate field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toJson_withNaNValues_serializesNulls() {
|
||||
ElliottWaveAnalysisReport.SwingSnapshot snapshot = new ElliottWaveAnalysisReport.SwingSnapshot(false, 0,
|
||||
Double.NaN, Double.NaN);
|
||||
ElliottWaveAnalysisReport.LatestAnalysis latest = new ElliottWaveAnalysisReport.LatestAnalysis(
|
||||
ElliottPhase.NONE, false, false, RatioType.NONE, Double.NaN, null, Double.NaN, false, false);
|
||||
ElliottWaveAnalysisReport.ScenarioSummary summary = new ElliottWaveAnalysisReport.ScenarioSummary("summary",
|
||||
false, ElliottPhase.NONE);
|
||||
ElliottTrendBias trendBias = ElliottTrendBias.unknown();
|
||||
ElliottWaveAnalysisReport.ProbabilityCalibration calibration = new ElliottWaveAnalysisReport.ProbabilityCalibration(
|
||||
"test-profile", "test-method", 0.5);
|
||||
ElliottWaveAnalysisReport.OutlookGate outlookGate = new ElliottWaveAnalysisReport.OutlookGate(false, "NEUTRAL",
|
||||
"test", Double.NaN, Double.NaN, Double.NaN, Double.NaN, false, false, Double.NaN);
|
||||
ElliottWaveAnalysisReport result = new ElliottWaveAnalysisReport(ElliottDegree.PRIMARY, 0, snapshot, latest,
|
||||
summary, trendBias, calibration, outlookGate, null, List.of(), null, List.of());
|
||||
|
||||
String json = result.toJson();
|
||||
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
JsonObject swingSnapshot = root.getAsJsonObject("swingSnapshot");
|
||||
JsonObject latestAnalysis = root.getAsJsonObject("latestAnalysis");
|
||||
|
||||
assertTrue(swingSnapshot.get("high").isJsonNull(), "NaN values should serialize as null");
|
||||
assertTrue(latestAnalysis.get("ratioValue").isJsonNull(), "NaN values should serialize as null");
|
||||
assertTrue(latestAnalysis.get("confluenceScore").isJsonNull(), "NaN values should serialize as null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void swingSnapshot_fromMetadata_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
ElliottWaveAnalysisReport.SwingSnapshot snapshot = result.swingSnapshot();
|
||||
assertNotNull(snapshot, "Swing snapshot should not be null");
|
||||
assertEquals(analysisResult.swingMetadata().isValid(), snapshot.valid(), "Valid flag should match");
|
||||
assertEquals(analysisResult.swingMetadata().size(), snapshot.swings(), "Swing count should match");
|
||||
assertTrue(snapshot.high() >= snapshot.low(), "High should be >= low");
|
||||
}
|
||||
|
||||
@Test
|
||||
void latestAnalysis_fromIndicators_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
ElliottWaveAnalysisReport.LatestAnalysis latest = result.latestAnalysis();
|
||||
assertNotNull(latest, "Latest analysis should not be null");
|
||||
assertNotNull(latest.phase(), "Phase should not be null");
|
||||
assertEquals(analysisResult.phaseIndicator().getValue(endIndex), latest.phase(), "Phase should match");
|
||||
assertEquals(analysisResult.phaseIndicator().isImpulseConfirmed(endIndex), latest.impulseConfirmed(),
|
||||
"Impulse confirmed should match");
|
||||
assertEquals(analysisResult.phaseIndicator().isCorrectiveConfirmed(endIndex), latest.correctiveConfirmed(),
|
||||
"Corrective confirmed should match");
|
||||
assertNotNull(latest.ratioType(), "Ratio type should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void scenarioSummary_fromScenarioSet_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
ElliottWaveAnalysisReport result = ElliottWaveAnalysisReport.from(analysisResult.degree(),
|
||||
analysisResult.swingMetadata(), analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex,
|
||||
analysisResult.baseCaseChartPlan(), analysisResult.alternativeChartPlans());
|
||||
|
||||
ElliottWaveAnalysisReport.ScenarioSummary summary = result.scenarioSummary();
|
||||
assertNotNull(summary, "Scenario summary should not be null");
|
||||
assertNotNull(summary.summary(), "Summary string should not be null");
|
||||
assertEquals(analysisResult.scenarioSet().hasStrongConsensus(), summary.strongConsensus(),
|
||||
"Strong consensus should match");
|
||||
assertEquals(analysisResult.scenarioSet().consensus(), summary.consensusPhase(),
|
||||
"Consensus phase should match");
|
||||
}
|
||||
|
||||
@Test
|
||||
void baseCaseScenario_fromScenario_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
if (result.baseCase() != null) {
|
||||
ElliottWaveAnalysisReport.BaseCaseScenario baseCase = result.baseCase();
|
||||
assertNotNull(baseCase.currentPhase(), "Current phase should not be null");
|
||||
assertNotNull(baseCase.type(), "Type should not be null");
|
||||
assertTrue(baseCase.overallConfidence() >= 0 && baseCase.overallConfidence() <= 100,
|
||||
"Overall confidence should be between 0 and 100");
|
||||
assertTrue(baseCase.scenarioProbability() >= 0 && baseCase.scenarioProbability() <= 1.0,
|
||||
"Scenario probability should be between 0 and 1");
|
||||
assertTrue(baseCase.calibratedProbability() >= 0 && baseCase.calibratedProbability() <= 1.0,
|
||||
"Calibrated probability should be between 0 and 1");
|
||||
assertTrue(
|
||||
baseCase.confidenceLevel().equals("HIGH") || baseCase.confidenceLevel().equals("MEDIUM")
|
||||
|| baseCase.confidenceLevel().equals("LOW"),
|
||||
"Confidence level should be HIGH, MEDIUM, or LOW");
|
||||
assertTrue(baseCase.fibonacciScore() >= 0 && baseCase.fibonacciScore() <= 100,
|
||||
"Fibonacci score should be between 0 and 100");
|
||||
assertTrue(baseCase.timeScore() >= 0 && baseCase.timeScore() <= 100,
|
||||
"Time score should be between 0 and 100");
|
||||
assertTrue(baseCase.alternationScore() >= 0 && baseCase.alternationScore() <= 100,
|
||||
"Alternation score should be between 0 and 100");
|
||||
assertTrue(baseCase.channelScore() >= 0 && baseCase.channelScore() <= 100,
|
||||
"Channel score should be between 0 and 100");
|
||||
assertTrue(baseCase.completenessScore() >= 0 && baseCase.completenessScore() <= 100,
|
||||
"Completeness score should be between 0 and 100");
|
||||
assertNotNull(baseCase.primaryReason(), "Primary reason should not be null");
|
||||
assertNotNull(baseCase.weakestFactor(), "Weakest factor should not be null");
|
||||
assertTrue(baseCase.direction().equals("BULLISH") || baseCase.direction().equals("BEARISH"),
|
||||
"Direction should be BULLISH or BEARISH");
|
||||
assertNotNull(baseCase.swings(), "Swings list should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void alternativeScenario_fromScenario_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
for (ElliottWaveAnalysisReport.AlternativeScenario alt : result.alternatives()) {
|
||||
assertNotNull(alt.currentPhase(), "Current phase should not be null");
|
||||
assertNotNull(alt.type(), "Type should not be null");
|
||||
assertTrue(alt.confidencePercent() >= 0 && alt.confidencePercent() <= 100,
|
||||
"Confidence percent should be between 0 and 100");
|
||||
assertTrue(alt.scenarioProbability() >= 0 && alt.scenarioProbability() <= 1.0,
|
||||
"Scenario probability should be between 0 and 1");
|
||||
assertTrue(alt.calibratedProbability() >= 0 && alt.calibratedProbability() <= 1.0,
|
||||
"Calibrated scenario probability should be between 0 and 1");
|
||||
assertNotNull(alt.swings(), "Swings list should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scenarioProbabilities_sumToOne() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
if (result.baseCase() == null) {
|
||||
assertTrue(result.alternatives().isEmpty(), "No base case implies no alternatives");
|
||||
return;
|
||||
}
|
||||
|
||||
double totalProbability = result.baseCase().scenarioProbability();
|
||||
double totalCalibratedProbability = result.baseCase().calibratedProbability();
|
||||
for (ElliottWaveAnalysisReport.AlternativeScenario alt : result.alternatives()) {
|
||||
totalProbability += alt.scenarioProbability();
|
||||
totalCalibratedProbability += alt.calibratedProbability();
|
||||
}
|
||||
assertEquals(1.0, totalProbability, 0.0001, "Scenario probabilities should sum to 1");
|
||||
assertEquals(1.0, totalCalibratedProbability, 0.0001, "Calibrated probabilities should sum to 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void probabilityCalibration_containsExpectedMetadata() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
ElliottWaveAnalysisReport.ProbabilityCalibration calibration = result.probabilityCalibration();
|
||||
assertNotNull(calibration, "Calibration metadata should be present");
|
||||
assertNotNull(calibration.profile(), "Calibration profile should be present");
|
||||
assertNotNull(calibration.method(), "Calibration method should be present");
|
||||
assertTrue(calibration.shrinkFactor() >= 0.0 && calibration.shrinkFactor() <= 1.0,
|
||||
"Shrink factor should be in [0,1]");
|
||||
|
||||
ElliottWaveAnalysisReport.OutlookGate outlookGate = result.outlookGate();
|
||||
assertNotNull(outlookGate, "Outlook gate should be present");
|
||||
assertNotNull(outlookGate.outlookLabel(), "Outlook label should be present");
|
||||
assertNotNull(outlookGate.reason(), "Outlook reason should be present");
|
||||
}
|
||||
|
||||
@Test
|
||||
void scenarioProbabilities_preferConsensusOverlap() {
|
||||
ElliottScenario scenarioA = buildScenario("A", 0.8, ElliottPhase.WAVE5, ScenarioType.IMPULSE, true);
|
||||
ElliottScenario scenarioB = buildScenario("B", 0.6, ElliottPhase.WAVE5, ScenarioType.IMPULSE, true);
|
||||
ElliottScenario scenarioC = buildScenario("C", 0.4, ElliottPhase.CORRECTIVE_C, ScenarioType.CORRECTIVE_ZIGZAG,
|
||||
false);
|
||||
|
||||
ElliottScenarioSet scenarioSet = ElliottScenarioSet.of(List.of(scenarioA, scenarioB, scenarioC), 0);
|
||||
Map<String, Double> probabilities = ElliottWaveAnalysisReport.computeScenarioProbabilities(scenarioSet);
|
||||
|
||||
double normalizedConsensus = (0.8 + 0.6) / 1.8;
|
||||
double normalizedOutlier = 0.4 / 1.8;
|
||||
|
||||
double probA = probabilities.get("A");
|
||||
double probB = probabilities.get("B");
|
||||
double probC = probabilities.get("C");
|
||||
|
||||
assertEquals(1.0, probA + probB + probC, 0.0001, "Scenario probabilities should sum to 1");
|
||||
assertTrue(probA > probB, "Higher confidence should remain higher within a consensus group");
|
||||
assertTrue(probA + probB > normalizedConsensus, "Consensus overlap should boost aligned scenarios");
|
||||
assertTrue(probC < normalizedOutlier, "Outlier scenarios should receive less than raw confidence");
|
||||
}
|
||||
|
||||
@Test
|
||||
void scenarioProbabilities_applyConfidenceContrastForCloseScores() {
|
||||
ElliottScenario scenarioA = buildScenario("A", 0.55, ElliottPhase.WAVE3, ScenarioType.IMPULSE, true);
|
||||
ElliottScenario scenarioB = buildScenario("B", 0.54, ElliottPhase.WAVE3, ScenarioType.IMPULSE, true);
|
||||
ElliottScenario scenarioC = buildScenario("C", 0.53, ElliottPhase.WAVE3, ScenarioType.IMPULSE, true);
|
||||
|
||||
ElliottScenarioSet scenarioSet = ElliottScenarioSet.of(List.of(scenarioA, scenarioB, scenarioC), 0);
|
||||
Map<String, Double> probabilities = ElliottWaveAnalysisReport.computeScenarioProbabilities(scenarioSet);
|
||||
|
||||
double rawRatio = 0.55 / 0.53;
|
||||
double adjustedRatio = probabilities.get("A") / probabilities.get("C");
|
||||
|
||||
assertTrue(adjustedRatio > rawRatio, "Contrast should widen the gap between close confidence scores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toJson_roundsScenarioProbabilityToThreeDecimals() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
String json = result.toJson();
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
|
||||
if (result.baseCase() != null) {
|
||||
double expected = roundScenarioProbability(result.baseCase().scenarioProbability());
|
||||
double actual = root.getAsJsonObject("baseCase").get("scenarioProbability").getAsDouble();
|
||||
assertEquals(expected, actual, 0.0001, "Base case scenario probability should be rounded to 3 decimals");
|
||||
double expectedCalibrated = roundScenarioProbability(result.baseCase().calibratedProbability());
|
||||
double actualCalibrated = root.getAsJsonObject("baseCase").get("calibratedProbability").getAsDouble();
|
||||
assertEquals(expectedCalibrated, actualCalibrated, 0.0001,
|
||||
"Base case calibrated probability should be rounded to 3 decimals");
|
||||
}
|
||||
if (!result.alternatives().isEmpty()) {
|
||||
ElliottWaveAnalysisReport.AlternativeScenario alt = result.alternatives().get(0);
|
||||
double expected = roundScenarioProbability(alt.scenarioProbability());
|
||||
double actual = root.getAsJsonArray("alternatives")
|
||||
.get(0)
|
||||
.getAsJsonObject()
|
||||
.get("scenarioProbability")
|
||||
.getAsDouble();
|
||||
assertEquals(expected, actual, 0.0001, "Alternative scenario probability should be rounded to 3 decimals");
|
||||
double expectedCalibrated = roundScenarioProbability(alt.calibratedProbability());
|
||||
double actualCalibrated = root.getAsJsonArray("alternatives")
|
||||
.get(0)
|
||||
.getAsJsonObject()
|
||||
.get("calibratedProbability")
|
||||
.getAsDouble();
|
||||
assertEquals(expectedCalibrated, actualCalibrated, 0.0001,
|
||||
"Alternative calibrated probability should be rounded to 3 decimals");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void swingData_fromSwing_capturesCorrectData() {
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
ElliottWaveAnalysisReport result = analysisResult.structuredResult();
|
||||
|
||||
if (result.baseCase() != null && !result.baseCase().swings().isEmpty()) {
|
||||
ElliottWaveAnalysisReport.SwingData swingData = result.baseCase().swings().get(0);
|
||||
assertTrue(swingData.fromIndex() >= 0, "From index should be non-negative");
|
||||
assertTrue(swingData.toIndex() >= 0, "To index should be non-negative");
|
||||
assertTrue(swingData.toIndex() != swingData.fromIndex(), "To index should differ from from index");
|
||||
assertTrue(swingData.fromPrice() >= 0 || Double.isNaN(swingData.fromPrice()),
|
||||
"From price should be non-negative or NaN");
|
||||
assertTrue(swingData.toPrice() >= 0 || Double.isNaN(swingData.toPrice()),
|
||||
"To price should be non-negative or NaN");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withEmptyAlternativeChartPlans_createsEmptyList() {
|
||||
// This test verifies that when there are no alternative chart plans, the list
|
||||
// is empty
|
||||
// The actual integration creates the result automatically, so we test the
|
||||
// factory method directly
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
// Test factory method with empty list
|
||||
ElliottWaveAnalysisReport result = ElliottWaveAnalysisReport.from(analysisResult.degree(),
|
||||
analysisResult.swingMetadata(), analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex,
|
||||
analysisResult.baseCaseChartPlan(), List.of());
|
||||
|
||||
assertNotNull(result.alternativeChartImages(), "Alternative chart images list should not be null");
|
||||
assertEquals(0, result.alternativeChartImages().size(), "Alternative chart images list should be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void from_withEmptyBaseCaseChartPlan_createsNullImage() {
|
||||
// This test verifies that when there's no base case chart plan, the image is
|
||||
// null
|
||||
// The actual integration creates the result automatically, so we test the
|
||||
// factory method directly
|
||||
BarSeries series = loadOssifiedSeries();
|
||||
ElliottWaveIndicatorSuiteDemo analysis = new ElliottWaveIndicatorSuiteDemo();
|
||||
ElliottWaveIndicatorSuiteDemo.AnalysisResult analysisResult = analysis.analyze(series, ElliottDegree.PRIMARY,
|
||||
FIB_TOLERANCE);
|
||||
|
||||
int endIndex = series.getEndIndex();
|
||||
// Test factory method with empty optional
|
||||
ElliottWaveAnalysisReport result = ElliottWaveAnalysisReport.from(analysisResult.degree(),
|
||||
analysisResult.swingMetadata(), analysisResult.phaseIndicator(), analysisResult.ratioIndicator(),
|
||||
analysisResult.channelIndicator(), analysisResult.confluenceIndicator(),
|
||||
analysisResult.invalidationIndicator(), analysisResult.scenarioSet(), endIndex, Optional.empty(),
|
||||
analysisResult.alternativeChartPlans());
|
||||
|
||||
assertNull(result.baseCaseChartImage(), "Base case chart image should be null when no chart plan");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build synthetic scenarios for probability tests.
|
||||
*/
|
||||
private static ElliottScenario buildScenario(String id, double confidence, ElliottPhase phase, ScenarioType type,
|
||||
boolean bullish) {
|
||||
DoubleNum score = DoubleNum.valueOf(confidence);
|
||||
ElliottConfidence confidenceScore = new ElliottConfidence(score, score, score, score, score, score,
|
||||
"Test confidence");
|
||||
return ElliottScenario.builder()
|
||||
.id(id)
|
||||
.currentPhase(phase)
|
||||
.confidence(confidenceScore)
|
||||
.degree(ElliottDegree.PRIMARY)
|
||||
.type(type)
|
||||
.bullishDirection(bullish)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static double roundScenarioProbability(double value) {
|
||||
return BigDecimal.valueOf(value).setScale(3, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to load the ossified dataset used in tests.
|
||||
*/
|
||||
private static BarSeries loadOssifiedSeries() {
|
||||
try (InputStream stream = ElliottWaveAnalysisReportTest.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to validate base64 encoding.
|
||||
*/
|
||||
private static boolean isValidBase64(String str) {
|
||||
try {
|
||||
Base64.getDecoder().decode(str);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.indicators.elliott.ElliottDegree;
|
||||
|
||||
class ElliottWavePresetDemoTest {
|
||||
|
||||
private static final Instant FIXED_END_TIME = Instant.parse("2026-02-25T00:00:00Z");
|
||||
private static final Instant EXPECTED_START_TIME = FIXED_END_TIME.minus(Duration.ofDays(1825L));
|
||||
|
||||
@Test
|
||||
void buildLiveSuiteArgsWithoutDegreeUsesExpectedLayout() {
|
||||
String[] args = ElliottWavePresetDemo.buildLiveSuiteArgs("Coinbase", "BTC-USD", "PT1D", 1825L, FIXED_END_TIME,
|
||||
null);
|
||||
|
||||
assertEquals(5, args.length, "Expected auto-degree argument count");
|
||||
assertEquals("Coinbase", args[0], "Unexpected data source");
|
||||
assertEquals("BTC-USD", args[1], "Unexpected ticker");
|
||||
assertEquals("PT1D", args[2], "Unexpected bar duration");
|
||||
assertEquals(Long.toString(EXPECTED_START_TIME.getEpochSecond()), args[3], "Unexpected start epoch");
|
||||
assertEquals(Long.toString(FIXED_END_TIME.getEpochSecond()), args[4], "Unexpected end epoch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildLiveSuiteArgsWithDegreeUsesExplicitDegreeLayout() {
|
||||
String[] args = ElliottWavePresetDemo.buildLiveSuiteArgs("YahooFinance", "AAPL", "PT1D", 1825L, FIXED_END_TIME,
|
||||
ElliottDegree.PRIMARY);
|
||||
|
||||
assertEquals(6, args.length, "Expected explicit-degree argument count");
|
||||
assertEquals("YahooFinance", args[0], "Unexpected data source");
|
||||
assertEquals("AAPL", args[1], "Unexpected ticker");
|
||||
assertEquals("PT1D", args[2], "Unexpected bar duration");
|
||||
assertEquals("PRIMARY", args[3], "Unexpected degree");
|
||||
assertEquals(Long.toString(EXPECTED_START_TIME.getEpochSecond()), args[4], "Unexpected start epoch");
|
||||
assertEquals(Long.toString(FIXED_END_TIME.getEpochSecond()), args[5], "Unexpected end epoch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseBtcMacroPresetForDailyBitcoin() {
|
||||
assertTrue(ElliottWavePresetDemo.shouldUseBtcMacroPreset("BTC-USD", "PT1D"));
|
||||
assertTrue(ElliottWavePresetDemo.shouldUseBtcMacroPreset("btc-usd", "PT24H"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotUseBtcMacroPresetForOtherAssetsOrIntervals() {
|
||||
assertFalse(ElliottWavePresetDemo.shouldUseBtcMacroPreset("ETH-USD", "PT1D"));
|
||||
assertFalse(ElliottWavePresetDemo.shouldUseBtcMacroPreset("BTC-USD", "PT4H"));
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.backtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
import ta4jexamples.datasources.CoinbaseHttpBarSeriesDataSource;
|
||||
import ta4jexamples.datasources.CoinbaseHttpBarSeriesDataSource.CoinbaseInterval;
|
||||
|
||||
/**
|
||||
* Black-box analysis-demo funnel for the live Elliott Wave macro report.
|
||||
*
|
||||
* <p>
|
||||
* Set {@code ta4j.analysisDemoInstrument} to a provider-qualified input such as
|
||||
* {@code coinbase:BTC-USD}. Set {@code ta4j.analysisDemoOutputDir} to control
|
||||
* where JSON, chart, and cached provider-response artifacts are written.
|
||||
*/
|
||||
@Tag("analysis-demo")
|
||||
class ElliottWaveAnalysisDemoReportTest {
|
||||
|
||||
private static final String INSTRUMENT_PROPERTY = "ta4j.analysisDemoInstrument";
|
||||
private static final String OUTPUT_DIRECTORY_PROPERTY = "ta4j.analysisDemoOutputDir";
|
||||
private static final String DEFAULT_INSTRUMENT = "coinbase:BTC-USD";
|
||||
private static final String COINBASE_PROVIDER = "coinbase";
|
||||
private static final Pattern COINBASE_PRODUCT_ID_PATTERN = Pattern.compile("[A-Z0-9]+(?:-[A-Z0-9]+)+");
|
||||
private static final int DAILY_BAR_COUNT = 1825;
|
||||
|
||||
@TempDir
|
||||
Path tempDirectory;
|
||||
|
||||
@Test
|
||||
void liveMacroReportProducesCurrentElliottAnalysisArtifacts() throws IOException {
|
||||
String previousDisableDisplay = System.getProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
try {
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
AnalysisInstrument instrument = parseInstrument(
|
||||
System.getProperty(INSTRUMENT_PROPERTY, DEFAULT_INSTRUMENT));
|
||||
Path outputDirectory = resolveOutputDirectory();
|
||||
Path responseCacheDirectory = outputDirectory.resolve("responses");
|
||||
Files.createDirectories(responseCacheDirectory);
|
||||
|
||||
BarSeries series = loadSeries(instrument, responseCacheDirectory);
|
||||
|
||||
assertNotNull(series);
|
||||
assertFalse(series.isEmpty());
|
||||
assertTrue(series.getBarCount() > 1000,
|
||||
() -> "Expected enough daily bars for live macro analysis but got " + series.getBarCount());
|
||||
|
||||
ElliottWaveMacroCycleDemo.LivePresetReport report = ElliottWaveMacroCycleDemo
|
||||
.generateLivePresetReport(series, outputDirectory);
|
||||
|
||||
Path summaryPath = Path.of(report.summaryPath());
|
||||
Path chartPath = Path.of(report.chartPath());
|
||||
assertEquals(instrument.productId(), report.seriesName());
|
||||
assertTrue(Files.exists(summaryPath), () -> "Missing summary artifact: " + summaryPath);
|
||||
assertTrue(Files.size(summaryPath) > 0, () -> "Empty summary artifact: " + summaryPath);
|
||||
assertTrue(Files.exists(chartPath), () -> "Missing chart artifact: " + chartPath);
|
||||
assertTrue(Files.size(chartPath) > 0, () -> "Empty chart artifact: " + chartPath);
|
||||
assertTrue(hasCachedResponses(responseCacheDirectory),
|
||||
() -> "Expected cached provider responses under " + responseCacheDirectory);
|
||||
|
||||
JsonObject summary = JsonParser.parseString(Files.readString(summaryPath)).getAsJsonObject();
|
||||
JsonObject currentCycle = summary.getAsJsonObject("currentCycle");
|
||||
assertEquals(instrument.productId(), summary.get("seriesName").getAsString());
|
||||
assertNotNull(currentCycle);
|
||||
assertFalse(currentCycle.get("primaryCount").getAsString().isBlank());
|
||||
assertFalse(currentCycle.get("currentWave").getAsString().isBlank());
|
||||
assertEquals(chartPath.toAbsolutePath().normalize().toString(),
|
||||
currentCycle.get("chartPath").getAsString());
|
||||
} finally {
|
||||
restoreDisableDisplayProperty(previousDisableDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void providerQualifiedInstrumentAcceptsCoinbaseProducts() {
|
||||
AnalysisInstrument btcUsd = parseInstrument(" coinbase:btc/usd ");
|
||||
AnalysisInstrument ethUsd = parseInstrument("COINBASE:eth-usd");
|
||||
|
||||
assertEquals(COINBASE_PROVIDER, btcUsd.provider());
|
||||
assertEquals("BTC-USD", btcUsd.productId());
|
||||
assertEquals(COINBASE_PROVIDER, ethUsd.provider());
|
||||
assertEquals("ETH-USD", ethUsd.productId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void providerQualifiedInstrumentRejectsUnsupportedProviders() {
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> parseInstrument("yahoo:SPY"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Unsupported analysis demo provider"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void providerQualifiedInstrumentRejectsInvalidCoinbaseProductIds() {
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> parseInstrument("coinbase:BTC USD"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Coinbase analysis demo product id"));
|
||||
}
|
||||
|
||||
private static BarSeries loadSeries(final AnalysisInstrument instrument, final Path responseCacheDirectory) {
|
||||
CoinbaseHttpBarSeriesDataSource dataSource = new CoinbaseHttpBarSeriesDataSource(
|
||||
responseCacheDirectory.toString());
|
||||
BarSeries series = dataSource.loadSeriesInstance(instrument.productId(), CoinbaseInterval.ONE_DAY,
|
||||
DAILY_BAR_COUNT, "analysis-demo");
|
||||
if (series == null || series.isEmpty()) {
|
||||
throw new IllegalStateException("No daily Coinbase candles returned for " + instrument.productId());
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
private Path resolveOutputDirectory() {
|
||||
String configured = System.getProperty(OUTPUT_DIRECTORY_PROPERTY);
|
||||
if (configured == null || configured.isBlank()) {
|
||||
return tempDirectory.resolve("analysis-demos").resolve("elliott-wave").toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
Path configuredPath = Path.of(configured.trim());
|
||||
if (configuredPath.isAbsolute()) {
|
||||
return configuredPath.normalize();
|
||||
}
|
||||
|
||||
return repositoryRoot().resolve(configuredPath).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private static AnalysisInstrument parseInstrument(final String rawInstrument) {
|
||||
String instrument = rawInstrument == null ? "" : rawInstrument.trim();
|
||||
int separator = instrument.indexOf(':');
|
||||
if (separator <= 0 || separator == instrument.length() - 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Analysis demo instrument must use provider-qualified format, for example coinbase:BTC-USD");
|
||||
}
|
||||
|
||||
String provider = instrument.substring(0, separator).trim().toLowerCase(Locale.ROOT);
|
||||
String productId = instrument.substring(separator + 1).trim().toUpperCase(Locale.ROOT).replace('/', '-');
|
||||
if (!COINBASE_PROVIDER.equals(provider)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported analysis demo provider '" + provider + "'. Supported providers: coinbase");
|
||||
}
|
||||
if (productId.isBlank()) {
|
||||
throw new IllegalArgumentException("Coinbase analysis demo product id cannot be blank");
|
||||
}
|
||||
if (!COINBASE_PRODUCT_ID_PATTERN.matcher(productId).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Coinbase analysis demo product id must use Coinbase product format, for example BTC-USD");
|
||||
}
|
||||
return new AnalysisInstrument(provider, productId);
|
||||
}
|
||||
|
||||
private static void restoreDisableDisplayProperty(final String previousDisableDisplay) {
|
||||
if (previousDisableDisplay == null) {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
} else {
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, previousDisableDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasCachedResponses(final Path responseCacheDirectory) throws IOException {
|
||||
if (!Files.isDirectory(responseCacheDirectory)) {
|
||||
return false;
|
||||
}
|
||||
try (Stream<Path> responses = Files.list(responseCacheDirectory)) {
|
||||
return responses.anyMatch(path -> path.getFileName().toString().endsWith(".json"));
|
||||
}
|
||||
}
|
||||
|
||||
private static Path repositoryRoot() {
|
||||
String rootDirectory = System.getProperty("maven.multiModuleProjectDirectory");
|
||||
if (rootDirectory != null && !rootDirectory.isBlank()) {
|
||||
Path candidate = Path.of(rootDirectory).toAbsolutePath().normalize();
|
||||
if (isRepositoryRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
Path current = Path.of("").toAbsolutePath().normalize();
|
||||
for (Path candidate = current; candidate != null; candidate = candidate.getParent()) {
|
||||
if (isRepositoryRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static boolean isRepositoryRoot(final Path candidate) {
|
||||
return Files.isRegularFile(candidate.resolve("pom.xml")) && Files.isDirectory(candidate.resolve("ta4j-core"))
|
||||
&& Files.isDirectory(candidate.resolve("ta4j-examples"));
|
||||
}
|
||||
|
||||
private record AnalysisInstrument(String provider, String productId) {
|
||||
|
||||
private AnalysisInstrument {
|
||||
Objects.requireNonNull(provider, "provider");
|
||||
Objects.requireNonNull(productId, "productId");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1212
File diff suppressed because it is too large
Load Diff
+265
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.backtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.elliott.ElliottPhase;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
|
||||
import ta4jexamples.analysis.elliottwave.support.OssifiedElliottWaveSeriesLoader;
|
||||
|
||||
class ElliottWaveAnchorRegistryTest {
|
||||
|
||||
@Test
|
||||
void loadParsesDefaultRegistryResource() {
|
||||
ElliottWaveAnchorRegistry registry = ElliottWaveAnchorRegistry.load(ElliottWaveAnchorRegistry.DEFAULT_RESOURCE);
|
||||
|
||||
assertEquals("btc-macro-cycle-anchors-v2", registry.registryId());
|
||||
assertEquals(ElliottWaveAnchorCalibrationHarness.BTC_RESOURCE, registry.datasetResource());
|
||||
assertFalse(registry.provenance().isBlank());
|
||||
assertEquals(8, registry.anchors().size());
|
||||
assertEquals("btc-2011-cycle-top", registry.anchors().getFirst().id());
|
||||
assertEquals("btc-2013-cycle-top", registry.anchors().get(2).id());
|
||||
assertEquals("btc-2022-cycle-bottom", registry.anchors().getLast().id());
|
||||
assertTrue(registry.anchors()
|
||||
.stream()
|
||||
.filter(anchor -> anchor.kind() == ElliottWaveAnchorRegistry.AnchorKind.BOTTOM)
|
||||
.allMatch(anchor -> anchor.expectedPhases().equals(Set.of(ElliottPhase.CORRECTIVE_C))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveBindsAnchorsToSeriesAndAssignsTrailingHoldoutPartition() {
|
||||
ElliottWaveAnchorRegistry registry = ElliottWaveAnchorRegistry.load(ElliottWaveAnchorRegistry.DEFAULT_RESOURCE);
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(ElliottWaveAnchorRegistryTest.class,
|
||||
ElliottWaveAnchorCalibrationHarness.BTC_RESOURCE, ElliottWaveAnchorCalibrationHarness.BTC_SERIES_NAME,
|
||||
org.apache.logging.log4j.LogManager.getLogger(ElliottWaveAnchorRegistryTest.class));
|
||||
|
||||
List<ElliottWaveAnchorRegistry.ResolvedAnchor> resolved = registry.resolve(series, 3);
|
||||
|
||||
assertEquals(registry.anchors().size(), resolved.size());
|
||||
int firstHoldoutIndex = resolved.size() - 3;
|
||||
for (int index = 0; index < resolved.size(); index++) {
|
||||
ElliottWaveAnchorRegistry.AnchorPartition expectedPartition = index < firstHoldoutIndex
|
||||
? ElliottWaveAnchorRegistry.AnchorPartition.VALIDATION
|
||||
: ElliottWaveAnchorRegistry.AnchorPartition.HOLDOUT;
|
||||
assertEquals(expectedPartition, resolved.get(index).partition(),
|
||||
"partition at resolved anchor index " + index);
|
||||
}
|
||||
for (ElliottWaveAnchorRegistry.ResolvedAnchor anchor : resolved) {
|
||||
assertTrue(anchor.decisionIndex() >= series.getBeginIndex());
|
||||
assertTrue(anchor.decisionIndex() <= series.getEndIndex());
|
||||
assertFalse(anchor.resolvedTime().isBefore(anchor.spec().windowStart()));
|
||||
assertFalse(anchor.resolvedTime().isAfter(anchor.spec().windowEnd()));
|
||||
assertTrue(Double.isFinite(anchor.resolvedPrice()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHonorsZeroRequestedHoldoutAnchors() {
|
||||
ElliottWaveAnchorRegistry registry = ElliottWaveAnchorRegistry.load(ElliottWaveAnchorRegistry.DEFAULT_RESOURCE);
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(ElliottWaveAnchorRegistryTest.class,
|
||||
ElliottWaveAnchorCalibrationHarness.BTC_RESOURCE, ElliottWaveAnchorCalibrationHarness.BTC_SERIES_NAME,
|
||||
org.apache.logging.log4j.LogManager.getLogger(ElliottWaveAnchorRegistryTest.class));
|
||||
|
||||
List<ElliottWaveAnchorRegistry.ResolvedAnchor> resolved = registry.resolve(series, 0);
|
||||
|
||||
assertEquals(0,
|
||||
resolved.stream()
|
||||
.filter(anchor -> anchor.partition() == ElliottWaveAnchorRegistry.AnchorPartition.HOLDOUT)
|
||||
.count());
|
||||
assertTrue(resolved.stream()
|
||||
.allMatch(anchor -> anchor.partition() == ElliottWaveAnchorRegistry.AnchorPartition.VALIDATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRejectsInvalidHoldoutCounts() {
|
||||
ElliottWaveAnchorRegistry registry = ElliottWaveAnchorRegistry.load(ElliottWaveAnchorRegistry.DEFAULT_RESOURCE);
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(ElliottWaveAnchorRegistryTest.class,
|
||||
ElliottWaveAnchorCalibrationHarness.BTC_RESOURCE, ElliottWaveAnchorCalibrationHarness.BTC_SERIES_NAME,
|
||||
org.apache.logging.log4j.LogManager.getLogger(ElliottWaveAnchorRegistryTest.class));
|
||||
|
||||
IllegalArgumentException negative = assertThrows(IllegalArgumentException.class,
|
||||
() -> registry.resolve(series, -1));
|
||||
IllegalArgumentException oversized = assertThrows(IllegalArgumentException.class,
|
||||
() -> registry.resolve(series, registry.anchors().size() + 1));
|
||||
|
||||
assertEquals("holdoutCount must be between 0 and " + registry.anchors().size(), negative.getMessage());
|
||||
assertEquals("holdoutCount must be between 0 and " + registry.anchors().size(), oversized.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void anchorSpecRejectsMissingExpectedPhasesAndBackwardsWindows() {
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
Instant end = Instant.parse("2024-01-02T00:00:00Z");
|
||||
|
||||
IllegalArgumentException emptyPhases = assertThrows(IllegalArgumentException.class,
|
||||
() -> new ElliottWaveAnchorRegistry.AnchorSpec("btc-top", "Top",
|
||||
ElliottWaveAnchorRegistry.AnchorKind.TOP, start, end, Set.of(), "test source", ""));
|
||||
IllegalArgumentException backwardsWindow = assertThrows(IllegalArgumentException.class,
|
||||
() -> new ElliottWaveAnchorRegistry.AnchorSpec("btc-top", "Top",
|
||||
ElliottWaveAnchorRegistry.AnchorKind.TOP, end, start, Set.of(ElliottPhase.WAVE5), "test source",
|
||||
""));
|
||||
|
||||
assertEquals("expectedPhases must not be empty", emptyPhases.getMessage());
|
||||
assertEquals("windowEnd must not be before windowStart", backwardsWindow.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void anchorSpecExposesExpectedPhasesAsUnmodifiableSet() {
|
||||
ElliottWaveAnchorRegistry.AnchorSpec anchor = new ElliottWaveAnchorRegistry.AnchorSpec("btc-top", "Top",
|
||||
ElliottWaveAnchorRegistry.AnchorKind.TOP, Instant.parse("2024-01-01T00:00:00Z"),
|
||||
Instant.parse("2024-01-03T00:00:00Z"), Set.of(ElliottPhase.WAVE5), "test source", "");
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> anchor.expectedPhases().add(ElliottPhase.WAVE3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registryAnchorRejectsUnknownKindsWithAnchorSpecificMessage() {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
|
||||
() -> toSpec("btc-top", "sideways", "2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", List.of("WAVE5")));
|
||||
|
||||
assertEquals("Unknown anchor kind 'sideways' for anchor btc-top", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registryAnchorRejectsUnknownExpectedPhasesWithAnchorSpecificMessage() {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
|
||||
() -> toSpec("btc-top", "TOP", "2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", List.of("WAVE6")));
|
||||
|
||||
assertEquals("Unknown expected phase 'WAVE6' for anchor btc-top", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registryAnchorRejectsInvalidWindowTimestampsWithAnchorSpecificMessage() {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
|
||||
() -> toSpec("btc-top", "TOP", "not-a-timestamp", "2024-01-02T00:00:00Z", List.of("WAVE5")));
|
||||
|
||||
assertEquals("Invalid windowStart 'not-a-timestamp' for anchor btc-top", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadRejectsMissingAnchorsListWithClearMessage() {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> ElliottWaveAnchorRegistry
|
||||
.load("ta4jexamples/analysis/elliottwave/backtest/test-anchor-registry-missing-anchors.json"));
|
||||
|
||||
assertEquals(
|
||||
"Anchor registry /ta4jexamples/analysis/elliottwave/backtest/test-anchor-registry-missing-anchors.json is missing \"anchors\"",
|
||||
thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadRejectsNullAnchorEntriesWithClearMessage() {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> ElliottWaveAnchorRegistry
|
||||
.load("ta4jexamples/analysis/elliottwave/backtest/test-anchor-registry-null-anchor.json"));
|
||||
|
||||
assertEquals(
|
||||
"Anchor registry /ta4jexamples/analysis/elliottwave/backtest/test-anchor-registry-null-anchor.json contains null anchor at index 0",
|
||||
thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveSkipsInvalidWindowPricesWhenLaterBarsRemainUsable() {
|
||||
BarSeries series = syntheticSeriesWithInvalidLeadingWindowPrice();
|
||||
ElliottWaveAnchorRegistry.AnchorSpec anchor = new ElliottWaveAnchorRegistry.AnchorSpec("btc-top", "Top",
|
||||
ElliottWaveAnchorRegistry.AnchorKind.TOP, Instant.parse("2024-01-02T00:00:00Z"),
|
||||
Instant.parse("2024-01-03T00:00:00Z"), Set.of(ElliottPhase.WAVE5), "test source", "");
|
||||
ElliottWaveAnchorRegistry registry = registry(anchor);
|
||||
|
||||
List<ElliottWaveAnchorRegistry.ResolvedAnchor> resolved = registry.resolve(series, 0);
|
||||
|
||||
assertEquals(1, resolved.size());
|
||||
assertEquals(1, resolved.getFirst().decisionIndex());
|
||||
assertEquals(110.0, resolved.getFirst().resolvedPrice());
|
||||
assertEquals(Instant.parse("2024-01-03T00:00:00Z"), resolved.getFirst().resolvedTime());
|
||||
}
|
||||
|
||||
private static BarSeries syntheticSeriesWithInvalidLeadingWindowPrice() {
|
||||
BarSeries series = new BaseBarSeriesBuilder().withName("anchor-registry-test")
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build();
|
||||
Duration period = Duration.ofDays(1);
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(period)
|
||||
.endTime(start.plus(period))
|
||||
.openPrice(100)
|
||||
.highPrice(Double.NaN)
|
||||
.lowPrice(95)
|
||||
.closePrice(Double.NaN)
|
||||
.volume(1000)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(period)
|
||||
.endTime(start.plus(period.multipliedBy(2)))
|
||||
.openPrice(100)
|
||||
.highPrice(110)
|
||||
.lowPrice(96)
|
||||
.closePrice(108)
|
||||
.volume(1000)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(period)
|
||||
.endTime(start.plus(period.multipliedBy(3)))
|
||||
.openPrice(108)
|
||||
.highPrice(105)
|
||||
.lowPrice(94)
|
||||
.closePrice(100)
|
||||
.volume(1000)
|
||||
.add();
|
||||
return series;
|
||||
}
|
||||
|
||||
private static ElliottWaveAnchorRegistry.AnchorSpec toSpec(String id, String kind, String windowStart,
|
||||
String windowEnd, List<String> expectedPhases) {
|
||||
try {
|
||||
Class<?> registryAnchorType = Class.forName(ElliottWaveAnchorRegistry.class.getName() + "$RegistryAnchor");
|
||||
Constructor<?> constructor = registryAnchorType.getDeclaredConstructor(String.class, String.class,
|
||||
String.class, String.class, String.class, List.class, String.class, String.class);
|
||||
constructor.setAccessible(true);
|
||||
Object registryAnchor = constructor.newInstance(id, "Test anchor", kind, windowStart, windowEnd,
|
||||
expectedPhases, "test source", "");
|
||||
Method toSpec = registryAnchorType.getDeclaredMethod("toSpec");
|
||||
toSpec.setAccessible(true);
|
||||
return (ElliottWaveAnchorRegistry.AnchorSpec) toSpec.invoke(registryAnchor);
|
||||
} catch (InvocationTargetException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new AssertionError("Failed to invoke RegistryAnchor.toSpec", cause);
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new AssertionError("Failed to create raw registry anchor", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static ElliottWaveAnchorRegistry registry(ElliottWaveAnchorRegistry.AnchorSpec anchor) {
|
||||
try {
|
||||
Constructor<ElliottWaveAnchorRegistry> constructor = ElliottWaveAnchorRegistry.class
|
||||
.getDeclaredConstructor(String.class, String.class, String.class, List.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance("synthetic-registry", "synthetic-dataset", "synthetic-provenance",
|
||||
List.of(anchor));
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new AssertionError("Failed to instantiate synthetic registry", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1021
File diff suppressed because it is too large
Load Diff
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.backtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
|
||||
import ta4jexamples.analysis.elliottwave.support.OssifiedElliottWaveSeriesLoader;
|
||||
|
||||
/**
|
||||
* Locks down anchor-free macro-cycle detection against the committed BTC truth
|
||||
* set.
|
||||
*
|
||||
* <p>
|
||||
* The detector is allowed to infer its own anchor ids and provenance, but the
|
||||
* recovered macro turns must stay aligned with the committed BTC anchor
|
||||
* windows. The runtime historical report itself is now series-native and no
|
||||
* longer uses the detector as a front-end.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Tag("elliott-macro-cycle-replay")
|
||||
class ElliottWaveMacroCycleDetectorTest {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ElliottWaveMacroCycleDetectorTest.class);
|
||||
|
||||
@TempDir
|
||||
Path chartDirectory;
|
||||
|
||||
@Test
|
||||
void inferredBitcoinAnchorsRecoverCommittedMacroTurns() {
|
||||
final BarSeries series = loadBitcoinSeries();
|
||||
final ElliottWaveAnchorCalibrationHarness.AnchorRegistry expected = ElliottWaveAnchorCalibrationHarness
|
||||
.defaultBitcoinAnchors(series);
|
||||
final ElliottWaveAnchorCalibrationHarness.AnchorRegistry inferred = ElliottWaveMacroCycleDetector
|
||||
.inferAnchorRegistry(series);
|
||||
|
||||
assertEquals(expected.anchors().size(), inferred.anchors().size());
|
||||
|
||||
for (int index = 0; index < expected.anchors().size(); index++) {
|
||||
final ElliottWaveAnchorCalibrationHarness.Anchor expectedAnchor = expected.anchors().get(index);
|
||||
final ElliottWaveAnchorCalibrationHarness.Anchor inferredAnchor = inferred.anchors().get(index);
|
||||
assertEquals(expectedAnchor.type(), inferredAnchor.type());
|
||||
assertEquals(expectedAnchor.partition(), inferredAnchor.partition());
|
||||
assertEquals(expectedAnchor.expectedPhases(), inferredAnchor.expectedPhases());
|
||||
assertWithinDays(expectedAnchor.at(), inferredAnchor.at(), 21);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void seriesNativeHistoricalMacroDemoProducesCanonicalChronologicalCycles() throws Exception {
|
||||
final BarSeries series = loadBitcoinSeries();
|
||||
final ElliottWaveMacroCycleDemo.DemoReport seriesNative = ElliottWaveMacroCycleDemo
|
||||
.generateHistoricalReport(series, chartDirectory.resolve("series-native"));
|
||||
|
||||
assertEquals("canonical-structure", seriesNative.structureSource());
|
||||
assertChronologicalCycles(seriesNative.cycles(), "historical demo");
|
||||
assertTrue(Files.exists(Path.of(seriesNative.chartPath())));
|
||||
assertTrue(Files.exists(Path.of(seriesNative.summaryPath())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void seriesNativeHistoricalAndLiveReportsShareCanonicalProfileSelection() throws Exception {
|
||||
final BarSeries series = loadBitcoinSeries();
|
||||
|
||||
final ElliottWaveMacroCycleDemo.DemoReport historical = ElliottWaveMacroCycleDemo
|
||||
.generateHistoricalReport(series, chartDirectory.resolve("historical"));
|
||||
final ElliottWaveMacroCycleDemo.LivePresetReport live = ElliottWaveMacroCycleDemo
|
||||
.generateLivePresetReport(series, chartDirectory.resolve("live"));
|
||||
|
||||
assertEquals("canonical-structure", historical.structureSource());
|
||||
assertEquals("canonical-structure", live.structureSource());
|
||||
assertEquals(historical.selectedProfileId(), live.selectedProfileId());
|
||||
assertEquals(historical.selectedHypothesisId(), live.selectedHypothesisId());
|
||||
assertTrue(Files.exists(Path.of(live.chartPath())));
|
||||
assertTrue(Files.exists(Path.of(live.summaryPath())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalReplayAtMajorMacroTurnsPreservesHistoricalCurrentProfileCoherence() {
|
||||
final BarSeries fullSeries = loadBitcoinSeries();
|
||||
|
||||
assertReplay(fullSeries, "2013-11-30T00:00:00Z");
|
||||
assertReplay(fullSeries, "2015-08-19T00:00:00Z");
|
||||
assertReplay(fullSeries, "2017-12-18T00:00:00Z");
|
||||
assertReplay(fullSeries, "2018-12-16T00:00:00Z");
|
||||
assertReplay(fullSeries, "2021-11-11T00:00:00Z");
|
||||
assertReplay(fullSeries, "2022-11-22T00:00:00Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalReplayAt2018BottomRecoversTwoCompletedCycles() {
|
||||
final BarSeries fullSeries = loadBitcoinSeries();
|
||||
final BarSeries slicedSeries = sliceThrough(fullSeries, Instant.parse("2018-12-16T00:00:00Z"));
|
||||
final ElliottWaveMacroCycleDemo.CanonicalStructure structure = ElliottWaveMacroCycleDemo
|
||||
.analyzeCanonicalStructure(slicedSeries);
|
||||
final ElliottWaveMacroCycleDemo.MacroStudy study = structure.historicalStudy().orElseThrow();
|
||||
|
||||
assertEquals(2, study.cycles().size(), cycleDateSignatures(study.cycles()).toString());
|
||||
assertWithinDays(Instant.parse("2011-11-18T00:00:00Z"), Instant.parse(study.cycles().get(0).startTimeUtc()),
|
||||
21);
|
||||
assertWithinDays(Instant.parse("2013-11-30T00:00:00Z"), Instant.parse(study.cycles().get(0).peakTimeUtc()), 21);
|
||||
assertWithinDays(Instant.parse("2015-08-19T00:00:00Z"), Instant.parse(study.cycles().get(0).lowTimeUtc()), 21);
|
||||
assertWithinDays(Instant.parse("2015-08-19T00:00:00Z"), Instant.parse(study.cycles().get(1).startTimeUtc()),
|
||||
21);
|
||||
assertWithinDays(Instant.parse("2017-12-18T00:00:00Z"), Instant.parse(study.cycles().get(1).peakTimeUtc()), 21);
|
||||
assertWithinDays(Instant.parse("2018-12-16T00:00:00Z"), Instant.parse(study.cycles().get(1).lowTimeUtc()), 21);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalReplayAt2021TopUsesContinuationAdjustedSecondCycleLow() {
|
||||
final BarSeries fullSeries = loadBitcoinSeries();
|
||||
final BarSeries slicedSeries = sliceThrough(fullSeries, Instant.parse("2021-11-11T00:00:00Z"));
|
||||
final ElliottWaveMacroCycleDemo.CanonicalStructure structure = ElliottWaveMacroCycleDemo
|
||||
.analyzeCanonicalStructure(slicedSeries);
|
||||
final ElliottWaveMacroCycleDemo.MacroStudy study = structure.historicalStudy().orElseThrow();
|
||||
|
||||
assertEquals(2, study.cycles().size(), cycleDateSignatures(study.cycles()).toString());
|
||||
assertCycleWithinDays(study, 0, "2011-11-18T00:00:00Z", "2013-11-30T00:00:00Z", "2015-08-19T00:00:00Z");
|
||||
assertCycleWithinDays(study, 1, "2015-08-19T00:00:00Z", "2017-12-18T00:00:00Z", "2020-05-26T00:00:00Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalReplayAt2022BottomRecoversChronologicalContinuationCandidates() {
|
||||
final BarSeries fullSeries = loadBitcoinSeries();
|
||||
final BarSeries slicedSeries = sliceThrough(fullSeries, Instant.parse("2022-11-22T00:00:00Z"));
|
||||
final ElliottWaveMacroCycleDemo.CanonicalStructure structure = ElliottWaveMacroCycleDemo
|
||||
.analyzeCanonicalStructure(slicedSeries);
|
||||
final ElliottWaveMacroCycleDemo.MacroStudy study = structure.historicalStudy().orElseThrow();
|
||||
|
||||
assertEquals(4, study.cycles().size(), cycleDateSignatures(study.cycles()).toString());
|
||||
assertCycleWithinDays(study, 0, "2011-11-18T00:00:00Z", "2013-11-30T00:00:00Z", "2015-08-19T00:00:00Z");
|
||||
assertCycleWithinDays(study, 1, "2015-08-19T00:00:00Z", "2016-06-19T00:00:00Z", "2016-08-03T00:00:00Z");
|
||||
assertCycleWithinDays(study, 2, "2016-08-03T00:00:00Z", "2017-12-18T00:00:00Z", "2020-03-14T00:00:00Z");
|
||||
assertCycleWithinDays(study, 3, "2020-03-14T00:00:00Z", "2021-11-11T00:00:00Z", "2022-11-22T00:00:00Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sliceThroughPreservesSourceNumFactory() {
|
||||
final BarSeries fullSeries = loadBitcoinSeries();
|
||||
|
||||
final BarSeries slicedSeries = sliceThrough(fullSeries, Instant.parse("2022-11-22T00:00:00Z"));
|
||||
|
||||
assertEquals(fullSeries.numFactory(), slicedSeries.numFactory());
|
||||
}
|
||||
|
||||
private static BarSeries loadBitcoinSeries() {
|
||||
final BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(ElliottWaveMacroCycleDetectorTest.class,
|
||||
ElliottWaveAnchorCalibrationHarness.BTC_RESOURCE, ElliottWaveAnchorCalibrationHarness.BTC_SERIES_NAME,
|
||||
LOG);
|
||||
assertNotNull(series);
|
||||
return series;
|
||||
}
|
||||
|
||||
private static void assertWithinDays(final Instant expected, final Instant actual, final long maxDays) {
|
||||
final Duration delta = Duration.between(expected, actual).abs();
|
||||
assertTrue(delta.compareTo(Duration.ofDays(maxDays)) <= 0,
|
||||
() -> "expected " + actual + " to stay within " + maxDays + " days of " + expected);
|
||||
}
|
||||
|
||||
private static void assertCycleWithinDays(final ElliottWaveMacroCycleDemo.MacroStudy study, final int cycleIndex,
|
||||
final String expectedStartIso, final String expectedPeakIso, final String expectedLowIso) {
|
||||
final ElliottWaveMacroCycleDemo.DirectionalCycleSummary cycle = study.cycles().get(cycleIndex);
|
||||
assertWithinDays(Instant.parse(expectedStartIso), Instant.parse(cycle.startTimeUtc()), 21);
|
||||
assertWithinDays(Instant.parse(expectedPeakIso), Instant.parse(cycle.peakTimeUtc()), 21);
|
||||
assertWithinDays(Instant.parse(expectedLowIso), Instant.parse(cycle.lowTimeUtc()), 21);
|
||||
}
|
||||
|
||||
private static void assertReplay(final BarSeries fullSeries, final String cutoffIso) {
|
||||
final BarSeries slicedSeries = sliceThrough(fullSeries, Instant.parse(cutoffIso));
|
||||
final ElliottWaveMacroCycleDemo.CanonicalStructure structure = ElliottWaveMacroCycleDemo
|
||||
.analyzeCanonicalStructure(slicedSeries);
|
||||
final ElliottWaveMacroCycleDemo.MacroStudy study = structure.historicalStudy().orElseThrow();
|
||||
|
||||
assertChronologicalCycles(study.cycles(), cutoffIso);
|
||||
for (ElliottWaveMacroCycleDemo.DirectionalCycleSummary cycle : study.cycles()) {
|
||||
assertTrue(!Instant.parse(cycle.lowTimeUtc()).isAfter(Instant.parse(cutoffIso)));
|
||||
}
|
||||
assertEquals(study.selectedProfile().profile().id(), structure.currentCycle().summary().winningProfileId());
|
||||
}
|
||||
|
||||
private static BarSeries sliceThrough(final BarSeries fullSeries, final Instant cutoff) {
|
||||
final BarSeries slicedSeries = new BaseBarSeriesBuilder().withName(fullSeries.getName() + "@" + cutoff)
|
||||
.withNumFactory(fullSeries.numFactory())
|
||||
.build();
|
||||
for (int index = fullSeries.getBeginIndex(); index <= fullSeries.getEndIndex(); index++) {
|
||||
if (fullSeries.getBar(index).getEndTime().isAfter(cutoff)) {
|
||||
break;
|
||||
}
|
||||
slicedSeries.addBar(fullSeries.getBar(index));
|
||||
}
|
||||
assertTrue(slicedSeries.getBarCount() > 0, () -> "No bars found through cutoff " + cutoff);
|
||||
return slicedSeries;
|
||||
}
|
||||
|
||||
private static void assertChronologicalCycles(final List<ElliottWaveMacroCycleDemo.DirectionalCycleSummary> cycles,
|
||||
final String contextLabel) {
|
||||
assertTrue(!cycles.isEmpty(), () -> "Expected at least one completed cycle for " + contextLabel);
|
||||
for (ElliottWaveMacroCycleDemo.DirectionalCycleSummary cycle : cycles) {
|
||||
final Instant cycleStart = Instant.parse(cycle.startTimeUtc());
|
||||
final Instant cyclePeak = Instant.parse(cycle.peakTimeUtc());
|
||||
final Instant cycleLow = Instant.parse(cycle.lowTimeUtc());
|
||||
assertTrue(cycleStart.compareTo(cyclePeak) < 0, () -> "Non-chronological peak in " + cycleLabel(cycle));
|
||||
assertTrue(cyclePeak.compareTo(cycleLow) < 0, () -> "Non-chronological low in " + cycleLabel(cycle));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> cycleDateSignatures(
|
||||
final List<ElliottWaveMacroCycleDemo.DirectionalCycleSummary> cycles) {
|
||||
return cycles.stream().map(ElliottWaveMacroCycleDetectorTest::cycleLabel).toList();
|
||||
}
|
||||
|
||||
private static String cycleLabel(final ElliottWaveMacroCycleDemo.DirectionalCycleSummary cycle) {
|
||||
return String.join("|", cycle.startTimeUtc(), cycle.peakTimeUtc(), cycle.lowTimeUtc());
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.backtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.indicators.elliott.ElliottDegree;
|
||||
import org.ta4j.core.indicators.elliott.ElliottLogicProfile;
|
||||
|
||||
class ElliottWaveMacroCycleTruthTargetScoringTest {
|
||||
|
||||
@Test
|
||||
void truthTargetCoverageScorePenalizesMissingAndUnexpectedCycles() {
|
||||
ElliottWaveMacroCycleDemo.TruthTargetCoverage complete = new ElliottWaveMacroCycleDemo.TruthTargetCoverage(3, 3,
|
||||
0, 0, List.of(), List.of());
|
||||
ElliottWaveMacroCycleDemo.TruthTargetCoverage missingCycle = new ElliottWaveMacroCycleDemo.TruthTargetCoverage(
|
||||
3, 2, 1, 0, List.of("cycle-3"), List.of());
|
||||
ElliottWaveMacroCycleDemo.TruthTargetCoverage unexpectedCycle = new ElliottWaveMacroCycleDemo.TruthTargetCoverage(
|
||||
3, 3, 0, 1, List.of(), List.of("cycle-extra"));
|
||||
|
||||
assertEquals(1.0, complete.score());
|
||||
assertTrue(missingCycle.score() < complete.score());
|
||||
assertTrue(unexpectedCycle.score() < complete.score());
|
||||
assertTrue(missingCycle.score() < unexpectedCycle.score());
|
||||
}
|
||||
|
||||
@Test
|
||||
void profileComparatorPrefersCompleteTruthTargetCoverageOverHigherAggregateScore() {
|
||||
Comparator<ElliottWaveMacroCycleDemo.MacroProfileEvaluation> comparator = ElliottWaveMacroCycleDemo
|
||||
.profileEvaluationComparator();
|
||||
ElliottWaveMacroCycleDemo.MacroProfileEvaluation completeCoverage = evaluation("complete-coverage", 0, 0.62, 3,
|
||||
6, true, new ElliottWaveMacroCycleDemo.TruthTargetCoverage(3, 3, 0, 0, List.of(), List.of()));
|
||||
ElliottWaveMacroCycleDemo.MacroProfileEvaluation unexpectedCoverage = evaluation("unexpected-coverage", 1, 0.98,
|
||||
3, 8, false,
|
||||
new ElliottWaveMacroCycleDemo.TruthTargetCoverage(3, 3, 0, 1, List.of(), List.of("cycle-extra")));
|
||||
ElliottWaveMacroCycleDemo.MacroProfileEvaluation missingCoverage = evaluation("missing-coverage", 2, 0.99, 4, 8,
|
||||
false, new ElliottWaveMacroCycleDemo.TruthTargetCoverage(3, 2, 1, 0, List.of("cycle-3"), List.of()));
|
||||
|
||||
List<String> orderedProfileIds = List.of(missingCoverage, unexpectedCoverage, completeCoverage)
|
||||
.stream()
|
||||
.sorted(comparator)
|
||||
.map(evaluation -> evaluation.profile().id())
|
||||
.toList();
|
||||
|
||||
assertEquals(List.of("complete-coverage", "unexpected-coverage", "missing-coverage"), orderedProfileIds);
|
||||
}
|
||||
|
||||
private static ElliottWaveMacroCycleDemo.MacroProfileEvaluation evaluation(final String profileId,
|
||||
final int orthodoxyRank, final double aggregateScore, final int acceptedCycles, final int acceptedSegments,
|
||||
final boolean historicalFitPassed,
|
||||
final ElliottWaveMacroCycleDemo.TruthTargetCoverage truthTargetCoverage) {
|
||||
ElliottWaveMacroCycleDemo.MacroLogicProfile profile = new ElliottWaveMacroCycleDemo.MacroLogicProfile(profileId,
|
||||
"HX", "Test profile", orthodoxyRank, ElliottLogicProfile.ORTHODOX_CLASSICAL, ElliottDegree.MINUTE, null,
|
||||
null);
|
||||
return new ElliottWaveMacroCycleDemo.MacroProfileEvaluation(profile, aggregateScore, acceptedCycles,
|
||||
acceptedSegments, historicalFitPassed, truthTargetCoverage, List.of(), List.of());
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.demo;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.elliott.ElliottDegree;
|
||||
import org.ta4j.core.indicators.elliott.ElliottWaveAnalysisResult;
|
||||
import org.ta4j.core.indicators.elliott.ElliottWaveAnalysisRunner;
|
||||
import org.ta4j.core.indicators.elliott.swing.AdaptiveZigZagConfig;
|
||||
import org.ta4j.core.indicators.elliott.swing.SwingDetectors;
|
||||
|
||||
import ta4jexamples.analysis.elliottwave.ElliottWaveIndicatorSuiteDemo;
|
||||
import ta4jexamples.analysis.elliottwave.support.OssifiedElliottWaveSeriesLoader;
|
||||
|
||||
class ElliottWaveMultiDegreeAnalysisDemoTest {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ElliottWaveMultiDegreeAnalysisDemoTest.class);
|
||||
private static final String OSSIFIED_OHLCV_RESOURCE = "Coinbase-BTC-USD-PT1D-20230616_20231020.json";
|
||||
|
||||
@Test
|
||||
void ossifiedDatasetProducesRecommendedBaseScenario() {
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(ElliottWaveMultiDegreeAnalysisDemo.class,
|
||||
OSSIFIED_OHLCV_RESOURCE, "BTC-USD_PT1D@Coinbase (ossified)", LOG);
|
||||
assertNotNull(series, "Series should load from ossified classpath resource");
|
||||
assertFalse(series.isEmpty(), "Series should contain bars");
|
||||
ElliottDegree baseDegree = ElliottWaveIndicatorSuiteDemo.autoSelectDegree(series);
|
||||
|
||||
ElliottWaveAnalysisRunner analyzer = ElliottWaveAnalysisRunner.builder()
|
||||
.degree(baseDegree)
|
||||
.higherDegrees(1)
|
||||
.lowerDegrees(1)
|
||||
.swingDetector(SwingDetectors.adaptiveZigZag(new AdaptiveZigZagConfig(14, 1.0, 0.0, 0.0, 1)))
|
||||
.build();
|
||||
|
||||
ElliottWaveAnalysisResult result = analyzer.analyze(series);
|
||||
assertTrue(result.recommendedScenario().isPresent(),
|
||||
"Multi-degree analysis should produce a recommended base scenario");
|
||||
|
||||
Optional<ElliottWaveAnalysisResult.DegreeAnalysis> primary = result.analyses()
|
||||
.stream()
|
||||
.filter(analysis -> analysis.degree() == baseDegree)
|
||||
.findFirst();
|
||||
assertTrue(primary.isPresent(), "Base degree analysis should be available");
|
||||
assertFalse(primary.orElseThrow().analysis().rawSwings().isEmpty(),
|
||||
"Primary degree should have detected swings");
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.analysis.elliottwave.support;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
class OssifiedElliottWaveSeriesLoaderTest {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(OssifiedElliottWaveSeriesLoaderTest.class);
|
||||
private static final String RESOURCE = "Binance-ETH-USD-PT5M-20230313_20230315.json";
|
||||
|
||||
@Test
|
||||
void loadSeries_shouldResolveClasspathRootWithoutLeadingSlash() {
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(OssifiedElliottWaveSeriesLoaderTest.class,
|
||||
RESOURCE, "ETH-USD_PT5M@Binance (ossified)", LOG);
|
||||
|
||||
assertNotNull(series);
|
||||
assertEquals("ETH-USD_PT5M@Binance (ossified)", series.getName());
|
||||
assertTrue(series.getBarCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSeries_shouldResolveClasspathRootWithLeadingSlash() {
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(OssifiedElliottWaveSeriesLoaderTest.class,
|
||||
"/" + RESOURCE, "ETH-USD_PT5M@Binance (ossified)", LOG);
|
||||
|
||||
assertNotNull(series);
|
||||
assertEquals("ETH-USD_PT5M@Binance (ossified)", series.getName());
|
||||
assertTrue(series.getBarCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSeries_shouldReturnNullWhenResourceIsMissing() {
|
||||
BarSeries series = OssifiedElliottWaveSeriesLoader.loadSeries(OssifiedElliottWaveSeriesLoaderTest.class,
|
||||
"does-not-exist.json", "missing", LOG);
|
||||
|
||||
assertNull(series);
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.backtesting;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.indicators.NetMomentumIndicator;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.rules.CrossedDownIndicatorRule;
|
||||
import org.ta4j.core.rules.CrossedUpIndicatorRule;
|
||||
|
||||
class BacktestPerformanceTuningHarnessTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void sliceToLastBarsReturnsSameInstanceWhenNoTruncationRequested() {
|
||||
BarSeries series = buildSeries(50);
|
||||
|
||||
assertSame(series, BacktestPerformanceTuningHarness.sliceToLastBars(series, 0),
|
||||
"barCount=0 should keep full series");
|
||||
assertSame(series, BacktestPerformanceTuningHarness.sliceToLastBars(series, 50),
|
||||
"barCount==available bars should keep full series");
|
||||
assertSame(series, BacktestPerformanceTuningHarness.sliceToLastBars(series, 500),
|
||||
"barCount>available bars should keep full series");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sliceToLastBarsTruncatesToRequestedSizeAndPreservesLastBar() {
|
||||
BarSeries series = buildSeries(100);
|
||||
Num expectedLastClose = series.getLastBar().getClosePrice();
|
||||
|
||||
BarSeries sliced = BacktestPerformanceTuningHarness.sliceToLastBars(series, 10);
|
||||
|
||||
assertNotSame(series, sliced, "Sliced series should be a new instance");
|
||||
assertEquals(10, sliced.getBarCount(), "Sliced series should expose requested number of bars");
|
||||
assertEquals(expectedLastClose, sliced.getLastBar().getClosePrice(), "Sliced series should preserve last bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMaximumBarCountHintOverridesMaximumBarCountWithoutMutatingDelegate() {
|
||||
BarSeries series = buildSeries(20);
|
||||
|
||||
assertSame(series, BacktestPerformanceTuningHarness.applyMaximumBarCountHint(series, 0),
|
||||
"hint=0 should be a no-op");
|
||||
|
||||
BarSeries wrapped = BacktestPerformanceTuningHarness.applyMaximumBarCountHint(series, 123);
|
||||
assertEquals(123, wrapped.getMaximumBarCount(), "Wrapper should override BarSeries.getMaximumBarCount");
|
||||
assertEquals(series.getBarCount(), wrapped.getBarCount(), "Wrapper should preserve bar count");
|
||||
assertEquals(series.getLastBar().getClosePrice(), wrapped.getLastBar().getClosePrice(),
|
||||
"Wrapper should delegate bar access");
|
||||
assertThrows(UnsupportedOperationException.class, () -> wrapped.setMaximumBarCount(1),
|
||||
"Hint wrapper should not allow mutating maximum bar count");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createStrategiesRespectsRequestedCount() {
|
||||
BarSeries series = buildSeries(500);
|
||||
|
||||
List<Strategy> strategies = BacktestPerformanceTuningHarness.createStrategies(series, 5);
|
||||
|
||||
assertEquals(5, strategies.size(), "Should only create the requested number of strategies");
|
||||
assertTrue(strategies.stream().allMatch(strategy -> strategy != null),
|
||||
"Strategies should not contain null entries");
|
||||
assertThrows(IllegalArgumentException.class, () -> BacktestPerformanceTuningHarness.createStrategies(series, 0),
|
||||
"Zero strategies is not a meaningful request");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createStrategiesReusesEquivalentNetMomentumIndicatorGraphs() {
|
||||
BarSeries series = buildSeries(500);
|
||||
|
||||
List<Strategy> strategies = BacktestPerformanceTuningHarness.createStrategies(series, 64);
|
||||
|
||||
NetMomentumIndicator firstIndicator = entryIndicator(strategies.get(0));
|
||||
assertSame(firstIndicator, exitIndicator(strategies.get(0)),
|
||||
"Entry and exit thresholds should share one momentum graph within each strategy");
|
||||
assertTrue(strategies.stream().skip(1).anyMatch(strategy -> entryIndicator(strategy) == firstIndicator),
|
||||
"Strategies with the same RSI/timeframe/decay inputs should reuse cached indicator work");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNonLinearReturnsFalseForRoughlyLinearScaling() {
|
||||
Thresholds thresholds = new Thresholds(0.25d, 1.25d);
|
||||
|
||||
RunResult previous = sampleRun(1_000L, Duration.ofSeconds(10), Duration.ofSeconds(1));
|
||||
RunResult current = sampleRun(1_300L, Duration.ofSeconds(13), Duration.ofSeconds(1));
|
||||
|
||||
assertFalse(BacktestPerformanceTuningHarness.isNonLinear(previous, current, thresholds),
|
||||
"Near-linear scaling should not be flagged as non-linear");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNonLinearFlagsGcDominatedRuns() {
|
||||
Thresholds thresholds = new Thresholds(0.25d, 1.25d);
|
||||
|
||||
RunResult previous = sampleRun(1_000L, Duration.ofSeconds(10), Duration.ofSeconds(1));
|
||||
RunResult current = sampleRun(1_300L, Duration.ofSeconds(13), Duration.ofSeconds(4));
|
||||
|
||||
assertTrue(BacktestPerformanceTuningHarness.isNonLinear(previous, current, thresholds),
|
||||
"Runs dominated by GC time should be flagged as non-linear");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNonLinearFlagsSlowdownBeyondThreshold() {
|
||||
Thresholds thresholds = new Thresholds(0.25d, 1.25d);
|
||||
|
||||
RunResult previous = sampleRun(1_000L, Duration.ofSeconds(10), Duration.ofSeconds(1));
|
||||
RunResult current = sampleRun(1_300L, Duration.ofSeconds(25), Duration.ofSeconds(1));
|
||||
|
||||
assertTrue(BacktestPerformanceTuningHarness.isNonLinear(previous, current, thresholds),
|
||||
"Runs that slow down beyond the threshold should be flagged as non-linear");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectBestRecommendationPrefersHighestWorkUnits() {
|
||||
RunResult smaller = sampleRun(1_000L, Duration.ofSeconds(10), Duration.ofSeconds(1));
|
||||
RunResult larger = sampleRun(5_000L, Duration.ofSeconds(20), Duration.ofSeconds(2));
|
||||
|
||||
List<VariantTuningResult> results = List.of(new VariantTuningResult(new SeriesVariant(500, 0), smaller, null),
|
||||
new VariantTuningResult(new SeriesVariant(2000, 0), larger, null));
|
||||
|
||||
assertEquals(larger, BacktestPerformanceTuningHarness.selectBestRecommendation(results),
|
||||
"Best recommendation should be the last linear run with highest work units");
|
||||
}
|
||||
|
||||
@Test
|
||||
void throughputPlanBuildsDeterministicMatrixCellsAndAcceptsFullBarCount() {
|
||||
HarnessCli cli = HarnessCli.parse(new String[] { "--throughputControl", "--throughputOutputDir",
|
||||
tempDir.resolve("matrix").toString(), "--matrixStrategyCounts", "2,3", "--matrixBarCounts", "5,full",
|
||||
"--matrixMaxBarCountHints", "0,4", "--executionMode", "topK", "--topK", "1", "--parallelism", "auto" });
|
||||
|
||||
ThroughputControlPlan plan = ThroughputControlPlan.fromCli(cli, 10);
|
||||
|
||||
assertEquals(8, plan.cells().size());
|
||||
assertEquals("s2-b5-m0", plan.cells().get(0).cellId());
|
||||
assertEquals(0, plan.cells().get(2).barCount(), "full should be represented as barCount=0");
|
||||
assertTrue(plan.resolvedParallelism() >= 1);
|
||||
assertEquals(tempDir.resolve("matrix").toAbsolutePath().normalize(), plan.outputDir());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throughputPlanDeduplicatesRepeatedMatrixInputs() {
|
||||
HarnessCli cli = HarnessCli.parse(new String[] { "--throughputControl", "--matrixStrategyCounts", "2,2",
|
||||
"--matrixBarCounts", "5,5,full,full", "--matrixMaxBarCountHints", "0,0" });
|
||||
|
||||
ThroughputControlPlan plan = ThroughputControlPlan.fromCli(cli, 10);
|
||||
|
||||
assertEquals(2, plan.cells().size(), "Duplicate matrix input values should not duplicate output cells");
|
||||
long uniqueCellIds = plan.cells().stream().map(ThroughputMatrixCell::cellId).distinct().count();
|
||||
assertEquals(plan.cells().size(), uniqueCellIds, "cellId values should remain unambiguous");
|
||||
}
|
||||
|
||||
@Test
|
||||
void throughputPlanFingerprintIncludesExecutionKnobs() {
|
||||
HarnessCli base = HarnessCli.parse(new String[] { "--throughputControl", "--matrixStrategyCounts", "2",
|
||||
"--matrixBarCounts", "5", "--matrixMaxBarCountHints", "0", "--parallelism", "1" });
|
||||
HarnessCli withProgress = HarnessCli.parse(new String[] { "--throughputControl", "--matrixStrategyCounts", "2",
|
||||
"--matrixBarCounts", "5", "--matrixMaxBarCountHints", "0", "--parallelism", "1", "--progress" });
|
||||
HarnessCli withoutGc = HarnessCli.parse(new String[] { "--throughputControl", "--matrixStrategyCounts", "2",
|
||||
"--matrixBarCounts", "5", "--matrixMaxBarCountHints", "0", "--parallelism", "1", "--noGcBetweenRuns" });
|
||||
|
||||
ThroughputControlPlan basePlan = ThroughputControlPlan.fromCli(base, 10);
|
||||
|
||||
assertNotEquals(basePlan.specFingerprint(), ThroughputControlPlan.fromCli(withProgress, 10).specFingerprint());
|
||||
assertNotEquals(basePlan.specFingerprint(), ThroughputControlPlan.fromCli(withoutGc, 10).specFingerprint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throughputPlanRejectsNegativeBarCounts() {
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> HarnessCli.parse(new String[] { "--matrixBarCounts", "-5" }));
|
||||
|
||||
assertEquals("--matrixBarCounts values must be >= 0 or full", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void matrixPerformanceTelemetryReportsCellsAndHypothesesPerMinute() {
|
||||
ThroughputMatrixPerformanceTracker tracker = new ThroughputMatrixPerformanceTracker();
|
||||
ThroughputMatrixCell first = new ThroughputMatrixCell("first", 2, 10, 0, ExecutionMode.FULL_RESULT, 1);
|
||||
ThroughputMatrixCell second = new ThroughputMatrixCell("second", 3, 10, 0, ExecutionMode.FULL_RESULT, 1);
|
||||
tracker.record(new ThroughputCellResult(first, sampleRun(2, 20L, Duration.ofMillis(10), Duration.ZERO), 10L));
|
||||
tracker.record(new ThroughputCellResult(second, sampleRun(3, 30L, Duration.ofMillis(20), Duration.ZERO), 20L));
|
||||
HarnessCli cli = HarnessCli
|
||||
.parse(new String[] { "--throughputOutputDir", tempDir.resolve("matrix").toString() });
|
||||
ThroughputControlPlan plan = ThroughputControlPlan.fromCli(cli, 10);
|
||||
|
||||
JsonObject telemetry = tracker.toJson(30_000L, plan, HostTelemetry.capture());
|
||||
|
||||
assertEquals(2, telemetry.get("cellCount").getAsInt());
|
||||
assertFalse(telemetry.getAsJsonObject("host").has("hostname"),
|
||||
"Shared benchmark artifacts should not expose raw hostnames");
|
||||
assertTrue(telemetry.getAsJsonObject("host").get("hostId").getAsString().equals("unknown")
|
||||
|| telemetry.getAsJsonObject("host").get("hostId").getAsString().startsWith("sha256:"));
|
||||
assertFalse(telemetry.get("progress").getAsBoolean());
|
||||
assertTrue(telemetry.get("gcBetweenRuns").getAsBoolean());
|
||||
assertEquals(4.0d, telemetry.get("cellsPerMinute").getAsDouble());
|
||||
assertEquals(5, telemetry.get("hypothesisCount").getAsInt());
|
||||
assertEquals(10.0d, telemetry.get("hypothesesPerMinute").getAsDouble());
|
||||
assertEquals("strategy", telemetry.get("hypothesisKind").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throughputControlWritesManifestCellsAndMatrixPerformance() throws Exception {
|
||||
HarnessCli cli = HarnessCli.parse(new String[] { "--throughputControl", "--throughputOutputDir",
|
||||
tempDir.resolve("output").toString(), "--matrixStrategyCounts", "2", "--matrixBarCounts", "10",
|
||||
"--matrixMaxBarCountHints", "0", "--executionMode", "topK", "--topK", "1", "--parallelism", "1" });
|
||||
|
||||
BacktestPerformanceTuningHarness.runThroughputControl(buildSeries(30), cli);
|
||||
|
||||
Path performancePath = tempDir.resolve("output")
|
||||
.resolve(BacktestPerformanceTuningHarness.MATRIX_PERFORMANCE_FILE);
|
||||
Path manifestPath = tempDir.resolve("output")
|
||||
.resolve(BacktestPerformanceTuningHarness.THROUGHPUT_MANIFEST_FILE);
|
||||
Path cellsPath = tempDir.resolve("output").resolve(BacktestPerformanceTuningHarness.MATRIX_CELLS_FILE);
|
||||
assertTrue(Files.isRegularFile(performancePath));
|
||||
assertTrue(Files.isRegularFile(manifestPath));
|
||||
assertTrue(Files.isRegularFile(cellsPath));
|
||||
JsonObject performance = JsonParser.parseString(Files.readString(performancePath, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
assertEquals(1, performance.get("cellCount").getAsInt());
|
||||
assertEquals(2, performance.get("hypothesisCount").getAsInt());
|
||||
assertTrue(performance.get("cellsPerMinute").getAsDouble() > 0.0d);
|
||||
assertTrue(performance.get("hypothesesPerMinute").getAsDouble() > 0.0d);
|
||||
assertTrue(performance.getAsJsonObject("phases").has("backtest"));
|
||||
}
|
||||
|
||||
private RunResult sampleRun(long workUnits, Duration runtime, Duration gcTime) {
|
||||
return sampleRun(1, workUnits, runtime, gcTime);
|
||||
}
|
||||
|
||||
private RunResult sampleRun(int strategyCount, long workUnits, Duration runtime, Duration gcTime) {
|
||||
BacktestRuntimeStats runtimeStats = new BacktestRuntimeStats(runtime, Duration.ZERO, Duration.ZERO,
|
||||
Duration.ZERO, Duration.ZERO, "{}");
|
||||
HeapSnapshot heap = new HeapSnapshot(1024L * 1024L, 1024L * 1024L, 512L * 1024L);
|
||||
GcSnapshot gcDelta = new GcSnapshot(0L, gcTime);
|
||||
return new RunResult(ExecutionMode.FULL_RESULT, strategyCount, 1, 0, Integer.MAX_VALUE, 0, Duration.ZERO,
|
||||
runtimeStats, workUnits, gcDelta, heap, heap, "MockNumFactory");
|
||||
}
|
||||
|
||||
private BarSeries buildSeries(int barCount) {
|
||||
double[] data = new double[barCount];
|
||||
for (int i = 0; i < barCount; i++) {
|
||||
data[i] = i + 1d;
|
||||
}
|
||||
return new MockBarSeriesBuilder().withData(data).build();
|
||||
}
|
||||
|
||||
private NetMomentumIndicator entryIndicator(Strategy strategy) {
|
||||
CrossedUpIndicatorRule rule = (CrossedUpIndicatorRule) strategy.getEntryRule();
|
||||
Indicator<Num> indicator = rule.getLow();
|
||||
return (NetMomentumIndicator) indicator;
|
||||
}
|
||||
|
||||
private NetMomentumIndicator exitIndicator(Strategy strategy) {
|
||||
CrossedDownIndicatorRule rule = (CrossedDownIndicatorRule) strategy.getExitRule();
|
||||
Indicator<Num> indicator = rule.getUp();
|
||||
return (NetMomentumIndicator) indicator;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.backtesting;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleMovingAverageBacktestTest {
|
||||
|
||||
@Test
|
||||
public void test() throws InterruptedException {
|
||||
SimpleMovingAverageBacktest.main(null);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.backtesting;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.backtest.BacktestExecutionResult;
|
||||
import org.ta4j.core.backtest.BacktestExecutor;
|
||||
import org.ta4j.core.backtest.TradingStatementExecutionResult.WeightedCriterion;
|
||||
import org.ta4j.core.criteria.drawdown.ReturnOverMaxDrawdownCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class SimpleMovingAverageRangeBacktestTest {
|
||||
|
||||
@Test
|
||||
public void test() throws InterruptedException {
|
||||
SimpleMovingAverageRangeBacktest.main(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectTopStrategiesUsesWeightedRankingConvenienceApiAndPreservesCriterionScores() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
|
||||
List<TradingStatement> expected = result.getTopStrategiesWeighted(2,
|
||||
WeightedCriterion.of(new NetProfitCriterion(), 7.0),
|
||||
WeightedCriterion.of(new ReturnOverMaxDrawdownCriterion(), 3.0));
|
||||
List<TradingStatement> actual = SimpleMovingAverageRangeBacktest.selectTopStrategies(result, 2);
|
||||
|
||||
assertEquals(expected.size(), actual.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertEquals(expected.get(i).getStrategy().getName(), actual.get(i).getStrategy().getName());
|
||||
assertEquals(2, actual.get(i).getCriterionScores().size());
|
||||
|
||||
Set<String> criterionTypes = new HashSet<>();
|
||||
actual.get(i)
|
||||
.getCriterionScores()
|
||||
.keySet()
|
||||
.forEach(criterion -> criterionTypes.add(criterion.getClass().getName()));
|
||||
assertTrue(criterionTypes.contains(NetProfitCriterion.class.getName()));
|
||||
assertTrue(criterionTypes.contains(ReturnOverMaxDrawdownCriterion.class.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private BacktestExecutionResult createBacktestResult() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withData(100d, 120d, 90d, 140d, 115d, 130d).build();
|
||||
List<Strategy> strategies = List.of(new BaseStrategy("Hold to finish", new FixedRule(0), new FixedRule(5)),
|
||||
new BaseStrategy("Buy the dip", new FixedRule(2), new FixedRule(3)),
|
||||
new BaseStrategy("Early loss", new FixedRule(1), new FixedRule(2)));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
return executor.executeWithRuntimeReport(strategies, series.numFactory().numOf(50), Trade.TradeType.BUY);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.backtesting;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class TradeFillRecordingExampleTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
TradeFillRecordingExample.main(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void streamingTradeFillsMatchGroupedTradeRecording() {
|
||||
BaseTradingRecord streamingRecord = TradeFillRecordingExample.buildStreamingRecord();
|
||||
BaseTradingRecord groupedTradeRecord = TradeFillRecordingExample.buildGroupedTradeRecord();
|
||||
|
||||
assertEquivalent(streamingRecord, groupedTradeRecord, "example parity");
|
||||
|
||||
assertEquals(4, streamingRecord.getTrades().size());
|
||||
assertEquals(2, streamingRecord.getPositionCount());
|
||||
assertEquals(0.41, streamingRecord.getRecordedTotalFees().doubleValue(), 1.0e-10);
|
||||
assertEquals(29.59, TradeFillRecordingExample.closedNetProfit(streamingRecord).doubleValue(), 1.0e-10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exampleProducesTwoClosedWinningPositions() {
|
||||
BaseTradingRecord streamingRecord = TradeFillRecordingExample.buildStreamingRecord();
|
||||
|
||||
assertTrue(streamingRecord.isClosed());
|
||||
assertEquals(0, streamingRecord.getOpenPositions().size());
|
||||
assertEquals(2, streamingRecord.getPositions().size());
|
||||
assertEquals(9.85, streamingRecord.getPositions().get(0).getProfit().doubleValue(), 1.0e-10);
|
||||
assertEquals(19.74, streamingRecord.getPositions().get(1).getProfit().doubleValue(), 1.0e-10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingPoliciesShowHowPartialExitsCloseDifferentLots() {
|
||||
BaseTradingRecord fifoRecord = TradeFillRecordingExample.buildMatchingPolicyRecord(ExecutionMatchPolicy.FIFO);
|
||||
BaseTradingRecord lifoRecord = TradeFillRecordingExample.buildMatchingPolicyRecord(ExecutionMatchPolicy.LIFO);
|
||||
BaseTradingRecord averageCostRecord = TradeFillRecordingExample
|
||||
.buildMatchingPolicyRecord(ExecutionMatchPolicy.AVG_COST);
|
||||
BaseTradingRecord specificIdRecord = TradeFillRecordingExample
|
||||
.buildMatchingPolicyRecord(ExecutionMatchPolicy.SPECIFIC_ID);
|
||||
|
||||
assertPosition(fifoRecord.getPositions().get(0), "lot-a", 100.0, 1.0, 20.0);
|
||||
assertPosition(lifoRecord.getPositions().get(0), "lot-b", 106.0, 1.0, 14.0);
|
||||
assertPosition(averageCostRecord.getPositions().get(0), null, 102.0, 1.0, 18.0);
|
||||
assertPosition(specificIdRecord.getPositions().get(0), "lot-b", 106.0, 1.0, 14.0);
|
||||
|
||||
assertOpenExposure(fifoRecord, 2, 103.0);
|
||||
assertOpenExposure(lifoRecord, 2, 100.0);
|
||||
assertOpenExposure(averageCostRecord, 2, 102.0);
|
||||
assertOpenExposure(specificIdRecord, 2, 100.0);
|
||||
|
||||
assertEquals(2, fifoRecord.getOpenPositions().size());
|
||||
assertEquals(1, lifoRecord.getOpenPositions().size());
|
||||
assertEquals(1, averageCostRecord.getOpenPositions().size());
|
||||
assertEquals(1, specificIdRecord.getOpenPositions().size());
|
||||
|
||||
assertOpenLot(fifoRecord.getOpenPositions().get(0), "lot-a", 100.0, 1.0);
|
||||
assertOpenLot(fifoRecord.getOpenPositions().get(1), "lot-b", 106.0, 1.0);
|
||||
assertOpenLot(lifoRecord.getOpenPositions().get(0), "lot-a", 100.0, 2.0);
|
||||
assertOpenLot(averageCostRecord.getOpenPositions().get(0), null, 102.0, 2.0);
|
||||
assertOpenLot(specificIdRecord.getOpenPositions().get(0), "lot-a", 100.0, 2.0);
|
||||
}
|
||||
|
||||
private static void assertEquivalent(TradingRecord expected, TradingRecord actual, String label) {
|
||||
require(expected.getTrades().size() == actual.getTrades().size(), () -> label + ": trade count mismatch");
|
||||
require(expected.getPositionCount() == actual.getPositionCount(), () -> label + ": position count mismatch");
|
||||
assertNum(expected.getRecordedTotalFees(), actual.getRecordedTotalFees(), label + ": fee mismatch");
|
||||
|
||||
for (int i = 0; i < expected.getTrades().size(); i++) {
|
||||
Trade left = expected.getTrades().get(i);
|
||||
Trade right = actual.getTrades().get(i);
|
||||
assertTrade(left, right, label + ": trade[" + i + "]");
|
||||
}
|
||||
|
||||
for (int i = 0; i < expected.getPositions().size(); i++) {
|
||||
Position left = expected.getPositions().get(i);
|
||||
Position right = actual.getPositions().get(i);
|
||||
assertTrade(left.getEntry(), right.getEntry(), label + ": position[" + i + "].entry");
|
||||
assertTrade(left.getExit(), right.getExit(), label + ": position[" + i + "].exit");
|
||||
assertNum(left.getProfit(), right.getProfit(), label + ": position[" + i + "].profit");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTrade(Trade expected, Trade actual, String label) {
|
||||
require(expected.getType() == actual.getType(), () -> label + ": type mismatch");
|
||||
require(expected.getIndex() == actual.getIndex(), () -> label + ": index mismatch");
|
||||
assertNum(expected.getPricePerAsset(), actual.getPricePerAsset(), label + ": price mismatch");
|
||||
assertNum(expected.getAmount(), actual.getAmount(), label + ": amount mismatch");
|
||||
assertNum(expected.getCost(), actual.getCost(), label + ": fee mismatch");
|
||||
require(Objects.equals(expected.getOrderId(), actual.getOrderId()), () -> label + ": orderId mismatch");
|
||||
require(Objects.equals(expected.getCorrelationId(), actual.getCorrelationId()),
|
||||
() -> label + ": correlationId mismatch");
|
||||
}
|
||||
|
||||
private static void assertPosition(Position position, String expectedCorrelationId, double expectedPrice,
|
||||
double expectedAmount, double expectedProfit) {
|
||||
require(Objects.equals(expectedCorrelationId, position.getEntry().getCorrelationId()),
|
||||
() -> "Unexpected closed lot correlationId");
|
||||
assertEquals(expectedPrice, position.getEntry().getPricePerAsset().doubleValue(), 1.0e-10);
|
||||
assertEquals(expectedAmount, position.getEntry().getAmount().doubleValue(), 1.0e-10);
|
||||
assertEquals(expectedProfit, position.getProfit().doubleValue(), 1.0e-10);
|
||||
}
|
||||
|
||||
private static void assertOpenExposure(TradingRecord record, double expectedAmount,
|
||||
double expectedAverageEntryPrice) {
|
||||
assertEquals(expectedAmount, record.getCurrentPosition().amount().doubleValue(), 1.0e-10);
|
||||
assertEquals(expectedAverageEntryPrice, record.getCurrentPosition().averageEntryPrice().doubleValue(), 1.0e-10);
|
||||
}
|
||||
|
||||
private static void assertOpenLot(Position position, String expectedCorrelationId, double expectedPrice,
|
||||
double expectedAmount) {
|
||||
require(Objects.equals(expectedCorrelationId, position.getEntry().getCorrelationId()),
|
||||
() -> "Unexpected open lot correlationId");
|
||||
assertEquals(expectedPrice, position.averageEntryPrice().doubleValue(), 1.0e-10);
|
||||
assertEquals(expectedAmount, position.amount().doubleValue(), 1.0e-10);
|
||||
}
|
||||
|
||||
private static void assertNum(Num expected, Num actual, String message) {
|
||||
if (expected == null || actual == null) {
|
||||
require(Objects.equals(expected, actual), () -> message);
|
||||
return;
|
||||
}
|
||||
require(expected.isEqual(actual), () -> message + " expected=" + expected + ", actual=" + actual);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, Supplier<String> messageSupplier) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(messageSupplier.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.backtesting;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.backtest.SlippageExecutionModel;
|
||||
import org.ta4j.core.backtest.TradeExecutionModel;
|
||||
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
|
||||
import org.ta4j.core.backtest.TradeOnNextOpenModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class TradingRecordParityBacktestTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
TradingRecordParityBacktest.main(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionModelsDemonstrateTimingAndSlippageDifferences() {
|
||||
BarSeries series = TradingRecordParityBacktest.createSeries();
|
||||
Strategy strategy = TradingRecordParityBacktest.createStrategy();
|
||||
|
||||
TradingRecord nextOpenRecord = TradingRecordParityBacktest.runWithExecutionModel(series, strategy,
|
||||
new TradeOnNextOpenModel());
|
||||
TradingRecord currentCloseRecord = TradingRecordParityBacktest.runWithExecutionModel(series, strategy,
|
||||
new TradeOnCurrentCloseModel());
|
||||
TradingRecord slippageRecord = TradingRecordParityBacktest.runWithExecutionModel(series, strategy,
|
||||
new SlippageExecutionModel(series.numFactory().numOf(0.01),
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE));
|
||||
|
||||
Position nextOpenPosition = nextOpenRecord.getPositions().get(0);
|
||||
Position currentClosePosition = currentCloseRecord.getPositions().get(0);
|
||||
Position slippagePosition = slippageRecord.getPositions().get(0);
|
||||
|
||||
assertEquals(2, nextOpenPosition.getEntry().getIndex());
|
||||
assertNumEquals(series.numFactory().numOf(109), nextOpenPosition.getEntry().getPricePerAsset());
|
||||
assertEquals(4, nextOpenPosition.getExit().getIndex());
|
||||
assertNumEquals(series.numFactory().numOf(99), nextOpenPosition.getExit().getPricePerAsset());
|
||||
|
||||
assertEquals(1, currentClosePosition.getEntry().getIndex());
|
||||
assertNumEquals(series.numFactory().numOf(104), currentClosePosition.getEntry().getPricePerAsset());
|
||||
assertEquals(3, currentClosePosition.getExit().getIndex());
|
||||
assertNumEquals(series.numFactory().numOf(103), currentClosePosition.getExit().getPricePerAsset());
|
||||
|
||||
Num onePercent = series.numFactory().numOf(1.01);
|
||||
Num ninetyNinePercent = series.numFactory().numOf(0.99);
|
||||
assertNumEquals(series.numFactory().numOf(104).multipliedBy(onePercent),
|
||||
slippagePosition.getEntry().getPricePerAsset());
|
||||
assertNumEquals(series.numFactory().numOf(103).multipliedBy(ninetyNinePercent),
|
||||
slippagePosition.getExit().getPricePerAsset());
|
||||
|
||||
assertTrue(currentClosePosition.getGrossProfit().isGreaterThan(nextOpenPosition.getGrossProfit()));
|
||||
assertTrue(slippagePosition.getGrossProfit().isLessThan(currentClosePosition.getGrossProfit()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentCloseParityHoldsAcrossProvidedAndFactoryRecordFlows() {
|
||||
BarSeries series = TradingRecordParityBacktest.createSeries();
|
||||
Strategy strategy = TradingRecordParityBacktest.createStrategy();
|
||||
|
||||
TradingRecord defaultRecord = TradingRecordParityBacktest.runWithExecutionModel(series, strategy,
|
||||
new TradeOnCurrentCloseModel());
|
||||
TradingRecord providedRecord = TradingRecordParityBacktest.runWithProvidedRecord(series, strategy,
|
||||
new TradeOnCurrentCloseModel());
|
||||
TradingRecord factoryConfiguredRecord = TradingRecordParityBacktest.runWithFactoryConfiguredRecord(series,
|
||||
strategy, new TradeOnCurrentCloseModel());
|
||||
|
||||
TradingRecordParityBacktest.assertEquivalent(defaultRecord, providedRecord, "provided record");
|
||||
TradingRecordParityBacktest.assertEquivalent(defaultRecord, factoryConfiguredRecord,
|
||||
"factory-configured record");
|
||||
}
|
||||
|
||||
private static void assertNumEquals(Num expected, Num actual) {
|
||||
assertTrue("expected=" + expected + " actual=" + actual, actual.isEqual(expected));
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.barSeries;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BuildBarSeriesTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
BuildBarSeries.main(null);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.bots;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class TradingBotOnMovingBarSeriesTest {
|
||||
|
||||
@Test
|
||||
public void test() throws InterruptedException {
|
||||
TradingBotOnMovingBarSeries.main(null);
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.criteria.NumberOfPositionsCriterion;
|
||||
import org.ta4j.core.criteria.ExpectancyCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnalysisCriterionIndicator}.
|
||||
*/
|
||||
class AnalysisCriterionIndicatorTest {
|
||||
|
||||
private BarSeries barSeries;
|
||||
private TradingRecord tradingRecord;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
barSeries = ChartingTestFixtures.standardDailySeries();
|
||||
tradingRecord = ChartingTestFixtures.completedTradeRecord(barSeries);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorNullSeries() {
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
assertThrows(NullPointerException.class, () -> new AnalysisCriterionIndicator(null, criterion, tradingRecord),
|
||||
"Should throw exception for null series");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorNullCriterion() {
|
||||
assertThrows(NullPointerException.class, () -> new AnalysisCriterionIndicator(barSeries, null, tradingRecord),
|
||||
"Should throw exception for null criterion");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorNullTradingRecord() {
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
assertThrows(NullPointerException.class, () -> new AnalysisCriterionIndicator(barSeries, criterion, null),
|
||||
"Should throw exception for null trading record");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCountOfUnstableBars() {
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, tradingRecord);
|
||||
|
||||
assertEquals(0, indicator.getCountOfUnstableBars(), "Analysis criteria should have no unstable bars");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNumberOfPositionsCriterionCalculation() {
|
||||
// Create a trading record with multiple completed positions
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
// First position: buy at index 1, sell at index 3
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
// Second position: buy at index 5, sell at index 7
|
||||
record.enter(5, barSeries.getBar(5).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(7, barSeries.getBar(7).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// Before any trades
|
||||
assertEquals(barSeries.numFactory().numOf(0), indicator.getValue(0), "Should be 0 positions before any trades");
|
||||
|
||||
// After first buy (position opened but not closed)
|
||||
assertEquals(barSeries.numFactory().numOf(0), indicator.getValue(1),
|
||||
"Should be 0 positions (position not yet closed)");
|
||||
|
||||
// After first position closed
|
||||
assertEquals(barSeries.numFactory().numOf(1), indicator.getValue(3),
|
||||
"Should be 1 position after first position closed");
|
||||
|
||||
// After second buy
|
||||
assertEquals(barSeries.numFactory().numOf(1), indicator.getValue(5),
|
||||
"Should still be 1 position (second position not yet closed)");
|
||||
|
||||
// After second position closed
|
||||
assertEquals(barSeries.numFactory().numOf(2), indicator.getValue(7),
|
||||
"Should be 2 positions after second position closed");
|
||||
|
||||
// At end
|
||||
assertEquals(barSeries.numFactory().numOf(2), indicator.getValue(barSeries.getEndIndex()),
|
||||
"Should be 2 positions at end");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialTradingRecordFiltering() {
|
||||
// Create a trading record with trades at different indices
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.enter(5, barSeries.getBar(5).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(7, barSeries.getBar(7).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// At index 2, only the first buy should be included (no sell yet)
|
||||
Num valueAt2 = indicator.getValue(2);
|
||||
// Create a partial record manually to verify
|
||||
TradingRecord partialAt2 = new BaseTradingRecord();
|
||||
partialAt2.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
Num expectedAt2 = criterion.calculate(barSeries, partialAt2);
|
||||
assertEquals(expectedAt2, valueAt2, "Value at index 2 should match partial record calculation");
|
||||
|
||||
// At index 4, first position should be complete, second not started
|
||||
Num valueAt4 = indicator.getValue(4);
|
||||
TradingRecord partialAt4 = new BaseTradingRecord();
|
||||
partialAt4.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
partialAt4.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
Num expectedAt4 = criterion.calculate(barSeries, partialAt4);
|
||||
assertEquals(expectedAt4, valueAt4, "Value at index 4 should match partial record calculation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyTradingRecord() {
|
||||
TradingRecord emptyRecord = new BaseTradingRecord();
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, emptyRecord);
|
||||
|
||||
// Should return the criterion value for an empty record at all indices
|
||||
for (int i = barSeries.getBeginIndex(); i <= barSeries.getEndIndex(); i++) {
|
||||
Num value = indicator.getValue(i);
|
||||
Num expected = criterion.calculate(barSeries, emptyRecord);
|
||||
assertEquals(expected, value, "Value at index " + i + " should match empty record calculation");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExpectancyCriterionCalculation() {
|
||||
// Create a trading record with one completed position
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new ExpectancyCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// Before any trades
|
||||
Num valueBefore = indicator.getValue(0);
|
||||
TradingRecord emptyRecord = new BaseTradingRecord();
|
||||
Num expectedBefore = criterion.calculate(barSeries, emptyRecord);
|
||||
assertEquals(expectedBefore, valueBefore, "Expectancy before trades should match empty record");
|
||||
|
||||
// After position closed
|
||||
Num valueAfter = indicator.getValue(3);
|
||||
Num expectedAfter = criterion.calculate(barSeries, record);
|
||||
assertEquals(expectedAfter, valueAfter, "Expectancy after position closed should match full record");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNetProfitCriterionCalculation() {
|
||||
// Create a trading record with one completed position
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NetProfitCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// Before any trades
|
||||
Num valueBefore = indicator.getValue(0);
|
||||
TradingRecord emptyRecord = new BaseTradingRecord();
|
||||
Num expectedBefore = criterion.calculate(barSeries, emptyRecord);
|
||||
assertEquals(expectedBefore, valueBefore, "Net profit before trades should be zero or match empty record");
|
||||
|
||||
// After position closed
|
||||
Num valueAfter = indicator.getValue(3);
|
||||
Num expectedAfter = criterion.calculate(barSeries, record);
|
||||
assertEquals(expectedAfter, valueAfter, "Net profit after position closed should match full record");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCachingBehavior() {
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, tradingRecord);
|
||||
|
||||
// Get value multiple times - should return same result (cached)
|
||||
Num value1 = indicator.getValue(3);
|
||||
Num value2 = indicator.getValue(3);
|
||||
Num value3 = indicator.getValue(3);
|
||||
|
||||
assertEquals(value1, value2, "Cached values should be equal");
|
||||
assertEquals(value2, value3, "Cached values should be equal");
|
||||
assertSame(value1, value2, "Cached values should be the same instance");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiplePositionsAtDifferentIndices() {
|
||||
// Create a trading record with three completed positions
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
// Position 1: indices 0-2
|
||||
record.enter(0, barSeries.getBar(0).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(2, barSeries.getBar(2).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
// Position 2: indices 3-5
|
||||
record.enter(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(5, barSeries.getBar(5).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
// Position 3: indices 6-8
|
||||
record.enter(6, barSeries.getBar(6).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(8, barSeries.getBar(8).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// Verify progressive counting
|
||||
assertEquals(barSeries.numFactory().numOf(0), indicator.getValue(0), "Should be 0 positions at index 0");
|
||||
assertEquals(barSeries.numFactory().numOf(1), indicator.getValue(2), "Should be 1 position after first closed");
|
||||
assertEquals(barSeries.numFactory().numOf(1), indicator.getValue(3),
|
||||
"Should still be 1 position (second not yet closed)");
|
||||
assertEquals(barSeries.numFactory().numOf(2), indicator.getValue(5),
|
||||
"Should be 2 positions after second closed");
|
||||
assertEquals(barSeries.numFactory().numOf(3), indicator.getValue(8),
|
||||
"Should be 3 positions after third closed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOpenPositionNotCounted() {
|
||||
// Create a trading record with one open position (not closed)
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
// No exit
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// Open positions should not be counted
|
||||
Num value = indicator.getValue(barSeries.getEndIndex());
|
||||
TradingRecord emptyRecord = new BaseTradingRecord();
|
||||
Num expected = criterion.calculate(barSeries, emptyRecord);
|
||||
assertEquals(expected, value, "Open position should not be counted in number of positions");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndexBeforeFirstTrade() {
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(5, barSeries.getBar(5).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(7, barSeries.getBar(7).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// At index 4, before any trades
|
||||
Num value = indicator.getValue(4);
|
||||
TradingRecord emptyRecord = new BaseTradingRecord();
|
||||
Num expected = criterion.calculate(barSeries, emptyRecord);
|
||||
assertEquals(expected, value, "Value before first trade should match empty record");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndexAfterLastTrade() {
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
record.enter(1, barSeries.getBar(1).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
record.exit(3, barSeries.getBar(3).getClosePrice(), barSeries.numFactory().numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterionIndicator indicator = new AnalysisCriterionIndicator(barSeries, criterion, record);
|
||||
|
||||
// At index after last trade, should include all trades
|
||||
Num valueAtEnd = indicator.getValue(barSeries.getEndIndex());
|
||||
Num expectedAtEnd = criterion.calculate(barSeries, record);
|
||||
assertEquals(expectedAtEnd, valueAtEnd, "Value after last trade should match full record");
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.indicators.PriceChannel;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ChannelBoundaryIndicator}.
|
||||
*/
|
||||
class ChannelBoundaryIndicatorTest {
|
||||
|
||||
private BarSeries series;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
series = ChartingTestFixtures.standardDailySeries();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passesThroughNumValuesFromDelegateIndicator() {
|
||||
Num expected = series.numFactory().numOf(42);
|
||||
ConstantNumIndicator delegate = new ConstantNumIndicator(series, expected, 3);
|
||||
|
||||
ChannelBoundaryIndicator indicator = new ChannelBoundaryIndicator(delegate, PriceChannel.Boundary.UPPER);
|
||||
|
||||
assertEquals(expected, indicator.getValue(series.getBeginIndex()));
|
||||
assertEquals(3, indicator.getCountOfUnstableBars(), "Should delegate unstable bar count");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsBoundaryValuesFromPriceChannelIndicator() {
|
||||
Num upper = series.numFactory().numOf(15);
|
||||
Num lower = series.numFactory().numOf(5);
|
||||
Num median = series.numFactory().numOf(10);
|
||||
PriceChannel channel = new TestChannel(upper, lower, median);
|
||||
ConstantChannelIndicator delegate = new ConstantChannelIndicator(series, channel, 2);
|
||||
|
||||
ChannelBoundaryIndicator upperIndicator = new ChannelBoundaryIndicator(delegate, PriceChannel.Boundary.UPPER);
|
||||
ChannelBoundaryIndicator lowerIndicator = new ChannelBoundaryIndicator(delegate, PriceChannel.Boundary.LOWER);
|
||||
ChannelBoundaryIndicator medianIndicator = new ChannelBoundaryIndicator(delegate, PriceChannel.Boundary.MEDIAN);
|
||||
|
||||
assertEquals(upper, upperIndicator.getValue(series.getBeginIndex()));
|
||||
assertEquals(lower, lowerIndicator.getValue(series.getBeginIndex()));
|
||||
assertEquals(median, medianIndicator.getValue(series.getBeginIndex()));
|
||||
assertEquals(2, upperIndicator.getCountOfUnstableBars(), "Should delegate unstable bar count");
|
||||
}
|
||||
|
||||
private record TestChannel(Num upper, Num lower, Num median) implements PriceChannel {
|
||||
}
|
||||
|
||||
private static final class ConstantNumIndicator implements Indicator<Num> {
|
||||
private final Num value;
|
||||
private final int unstableBars;
|
||||
private final BarSeries series;
|
||||
|
||||
private ConstantNumIndicator(BarSeries series, Num value, int unstableBars) {
|
||||
this.series = series;
|
||||
this.value = value;
|
||||
this.unstableBars = unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ConstantChannelIndicator implements Indicator<PriceChannel> {
|
||||
private final PriceChannel channel;
|
||||
private final int unstableBars;
|
||||
private final BarSeries series;
|
||||
|
||||
private ConstantChannelIndicator(BarSeries series, PriceChannel channel, int unstableBars) {
|
||||
this.series = series;
|
||||
this.channel = channel;
|
||||
this.unstableBars = unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceChannel getValue(int index) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.jfree.data.xy.DefaultOHLCDataset;
|
||||
import org.jfree.data.xy.OHLCDataItem;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
|
||||
/**
|
||||
* Shared fixtures for charting tests. Relies on the {@code Mock*} utilities
|
||||
* from {@code ta4j-core} to guarantee deterministic datasets.
|
||||
*/
|
||||
public final class ChartingTestFixtures {
|
||||
|
||||
private static final Instant START_TIME = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
private static final Duration DAILY_PERIOD = Duration.ofDays(1);
|
||||
private static final Duration HOURLY_PERIOD = Duration.ofHours(1);
|
||||
|
||||
private ChartingTestFixtures() {
|
||||
throw new AssertionError("No instances");
|
||||
}
|
||||
|
||||
public static BarSeries standardDailySeries() {
|
||||
return linearSeries("Test Series", DAILY_PERIOD, 10, 100.0, 1.0, 1.0, 1000.0, 100.0);
|
||||
}
|
||||
|
||||
public static BarSeries dailySeries(final String name) {
|
||||
return linearSeries(name, DAILY_PERIOD, 10, 100.0, 1.0, 1.0, 1000.0, 100.0);
|
||||
}
|
||||
|
||||
public static BarSeries hourlySeries(final String name) {
|
||||
return linearSeries(name, HOURLY_PERIOD, 5, 100.0, 0.5, 0.5, 500.0, 50.0);
|
||||
}
|
||||
|
||||
public static BarSeries dailySeriesWithWeekendGap(final String name) {
|
||||
final var series = new MockBarSeriesBuilder().withName(name).build();
|
||||
addBar(series, START_TIME, DAILY_PERIOD, 100.0, 102.0, 99.0, 101.0, 1000.0);
|
||||
addBar(series, START_TIME.plus(Duration.ofDays(3)), DAILY_PERIOD, 101.0, 103.0, 100.0, 102.0, 1100.0);
|
||||
return series;
|
||||
}
|
||||
|
||||
public static BarSeries problematicSeries() {
|
||||
final var series = new MockBarSeriesBuilder().withName("Problematic Series").build();
|
||||
addBar(series, START_TIME, DAILY_PERIOD, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
return series;
|
||||
}
|
||||
|
||||
public static BarSeries seriesWithSpecialChars() {
|
||||
final var series = new MockBarSeriesBuilder().withName("Test:Series/With\\Special?Chars*<>|\"").build();
|
||||
addBar(series, START_TIME, DAILY_PERIOD, 100.0, 105.0, 99.0, 104.0, 1000.0);
|
||||
return series;
|
||||
}
|
||||
|
||||
public static TradingRecord completedTradeRecord(final BarSeries series) {
|
||||
final TradingRecord record = new BaseTradingRecord();
|
||||
|
||||
if (series.getBarCount() >= 6) {
|
||||
final Trade buy = Trade.buyAt(2, series);
|
||||
final Trade sell = Trade.sellAt(5, series);
|
||||
|
||||
record.enter(2, buy.getPricePerAsset(), buy.getAmount());
|
||||
record.exit(5, sell.getPricePerAsset(), sell.getAmount());
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
public static TradingRecord emptyRecord() {
|
||||
return new BaseTradingRecord();
|
||||
}
|
||||
|
||||
public static TradingRecord openPositionRecord(final BarSeries series) {
|
||||
final TradingRecord record = new BaseTradingRecord();
|
||||
if (!series.isEmpty()) {
|
||||
final Bar firstBar = series.getBar(series.getBeginIndex());
|
||||
record.enter(0, firstBar.getClosePrice(), series.numFactory().numOf(1));
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
public static DefaultOHLCDataset singleCandleDataset(final boolean upCandle) {
|
||||
final double close = upCandle ? 104.0 : 96.0;
|
||||
return buildDataset("Test", List.of(ohlcItem(100.0, 105.0, 99.0, close, 1000.0, START_TIME)));
|
||||
}
|
||||
|
||||
public static DefaultOHLCDataset candleDatasetWithZeros() {
|
||||
return buildDataset("Test", List.of(ohlcItem(0.0, 0.0, 0.0, 0.0, 0.0, START_TIME)));
|
||||
}
|
||||
|
||||
public static DefaultOHLCDataset linearOhlcDataset(final String name, final int count) {
|
||||
final var items = IntStream.range(0, count).mapToObj(i -> {
|
||||
final Instant endTime = START_TIME.plus(DAILY_PERIOD.multipliedBy(i));
|
||||
final double open = 100.0 + i;
|
||||
final double high = open + 5.0;
|
||||
final double low = open - 1.0;
|
||||
final double close = open + 4.0;
|
||||
final double volume = 1000.0 + i * 100.0;
|
||||
return ohlcItem(open, high, low, close, volume, endTime);
|
||||
}).toList();
|
||||
return buildDataset(name, items);
|
||||
}
|
||||
|
||||
public static DefaultOHLCDataset seriesToDataset(final BarSeries barSeries) {
|
||||
final var items = IntStream.rangeClosed(barSeries.getBeginIndex(), barSeries.getEndIndex()).mapToObj(i -> {
|
||||
final Bar bar = barSeries.getBar(i);
|
||||
return new OHLCDataItem(Date.from(bar.getEndTime()), bar.getOpenPrice().doubleValue(),
|
||||
bar.getHighPrice().doubleValue(), bar.getLowPrice().doubleValue(),
|
||||
bar.getClosePrice().doubleValue(), bar.getVolume().doubleValue());
|
||||
}).toArray(OHLCDataItem[]::new);
|
||||
|
||||
final String datasetName = Objects.requireNonNullElse(barSeries.getName(), "Series");
|
||||
return new DefaultOHLCDataset(datasetName, items);
|
||||
}
|
||||
|
||||
private static BarSeries linearSeries(final String name, final Duration period, final int count,
|
||||
final double baseOpen, final double openIncrement, final double closeOffset, final double baseVolume,
|
||||
final double volumeIncrement) {
|
||||
final var series = new MockBarSeriesBuilder().withName(name).build();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final Instant endTime = START_TIME.plus(period.multipliedBy(i));
|
||||
final double open = baseOpen + i * openIncrement;
|
||||
final double high = open + 2.0;
|
||||
final double low = open - 1.0;
|
||||
final double close = open + closeOffset;
|
||||
final double volume = baseVolume + i * volumeIncrement;
|
||||
addBar(series, endTime, period, open, high, low, close, volume);
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
private static void addBar(final BarSeries series, final Instant endTime, final Duration period, final double open,
|
||||
final double high, final double low, final double close, final double volume) {
|
||||
series.barBuilder()
|
||||
.timePeriod(period)
|
||||
.endTime(endTime)
|
||||
.openPrice(open)
|
||||
.highPrice(high)
|
||||
.lowPrice(low)
|
||||
.closePrice(close)
|
||||
.volume(volume)
|
||||
.add();
|
||||
}
|
||||
|
||||
private static DefaultOHLCDataset buildDataset(final String name, final List<OHLCDataItem> items) {
|
||||
return new DefaultOHLCDataset(name, items.toArray(OHLCDataItem[]::new));
|
||||
}
|
||||
|
||||
private static OHLCDataItem ohlcItem(final double open, final double high, final double low, final double close,
|
||||
final double volume, final Instant endTime) {
|
||||
return new OHLCDataItem(Date.from(endTime), open, high, low, close, volume);
|
||||
}
|
||||
}
|
||||
+698
@@ -0,0 +1,698 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.annotation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import ta4jexamples.charting.annotation.BarSeriesLabelIndicator.BarLabel;
|
||||
import ta4jexamples.charting.annotation.BarSeriesLabelIndicator.LabelPlacement;
|
||||
|
||||
/**
|
||||
* Comprehensive unit tests for {@link BarSeriesLabelIndicator}.
|
||||
*/
|
||||
class BarSeriesLabelIndicatorTest {
|
||||
|
||||
private BarSeries series;
|
||||
private NumFactory numFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
numFactory = org.ta4j.core.num.DecimalNumFactory.getInstance();
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ========== Constructor Tests ==========
|
||||
|
||||
@Test
|
||||
void testConstructorWithValidInputs() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(0, numFactory.numOf(100.0), "Label 0", LabelPlacement.CENTER),
|
||||
new BarLabel(5, numFactory.numOf(105.0), "Label 5", LabelPlacement.ABOVE));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
assertNotNull(indicator);
|
||||
assertSame(series, indicator.getBarSeries());
|
||||
assertEquals(2, indicator.labels().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorThrowsWhenSeriesIsNull() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(0, numFactory.numOf(100.0), "Label", LabelPlacement.CENTER));
|
||||
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BarSeriesLabelIndicator(null, labels);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorThrowsWhenLabelsIsNull() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BarSeriesLabelIndicator(series, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorFiltersNullLabels() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(0, numFactory.numOf(100.0), "Label 0", LabelPlacement.CENTER));
|
||||
labels.add(null);
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "Label 5", LabelPlacement.ABOVE));
|
||||
labels.add(null);
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
assertEquals(2, indicator.labels().size());
|
||||
assertEquals(0, indicator.labels().get(0).barIndex());
|
||||
assertEquals(5, indicator.labels().get(1).barIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithEmptyLabels() {
|
||||
List<BarLabel> emptyLabels = Collections.emptyList();
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, emptyLabels);
|
||||
|
||||
assertNotNull(indicator);
|
||||
assertTrue(indicator.labels().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorSortsLabelsByBarIndex() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "Label 5", LabelPlacement.ABOVE));
|
||||
labels.add(new BarLabel(2, numFactory.numOf(102.0), "Label 2", LabelPlacement.BELOW));
|
||||
labels.add(new BarLabel(0, numFactory.numOf(100.0), "Label 0", LabelPlacement.CENTER));
|
||||
labels.add(new BarLabel(8, numFactory.numOf(108.0), "Label 8", LabelPlacement.ABOVE));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> sortedLabels = indicator.labels();
|
||||
assertEquals(4, sortedLabels.size());
|
||||
assertEquals(0, sortedLabels.get(0).barIndex());
|
||||
assertEquals(2, sortedLabels.get(1).barIndex());
|
||||
assertEquals(5, sortedLabels.get(2).barIndex());
|
||||
assertEquals(8, sortedLabels.get(3).barIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorHandlesDuplicateBarIndices() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(3, numFactory.numOf(103.0), "First", LabelPlacement.CENTER));
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "Middle", LabelPlacement.ABOVE));
|
||||
labels.add(new BarLabel(3, numFactory.numOf(303.0), "Second", LabelPlacement.BELOW)); // Duplicate index
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> resultLabels = indicator.labels();
|
||||
// The labels list keeps all labels (including duplicates), but sorted
|
||||
assertEquals(3, resultLabels.size());
|
||||
assertEquals(3, resultLabels.get(0).barIndex());
|
||||
assertEquals(3, resultLabels.get(1).barIndex());
|
||||
assertEquals(5, resultLabels.get(2).barIndex());
|
||||
|
||||
// getValue() returns the last value for duplicate indices
|
||||
assertEquals(303.0, indicator.getValue(3).doubleValue(), 0.001);
|
||||
}
|
||||
|
||||
// ========== BarLabel Record Tests ==========
|
||||
|
||||
@Test
|
||||
void testBarLabelCreationWithValidInputs() {
|
||||
BarLabel label = new BarLabel(5, numFactory.numOf(105.0), "Test", LabelPlacement.ABOVE);
|
||||
|
||||
assertEquals(5, label.barIndex());
|
||||
assertEquals(105.0, label.yValue().doubleValue(), 0.001);
|
||||
assertEquals("Test", label.text());
|
||||
assertEquals(LabelPlacement.ABOVE, label.placement());
|
||||
assertNull(label.color());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelCreationRetainsExplicitColor() {
|
||||
BarLabel label = new BarLabel(5, numFactory.numOf(105.0), "Test", LabelPlacement.ABOVE, Color.GREEN);
|
||||
|
||||
assertEquals(Color.GREEN, label.color());
|
||||
assertEquals(1.0, label.fontScale());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelCreationRetainsExplicitFontScale() {
|
||||
BarLabel label = new BarLabel(5, numFactory.numOf(105.0), "Test", LabelPlacement.ABOVE, Color.GREEN, 3.0);
|
||||
|
||||
assertEquals(Color.GREEN, label.color());
|
||||
assertEquals(3.0, label.fontScale());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelThrowsWhenBarIndexIsNegative() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new BarLabel(-1, numFactory.numOf(100.0), "Test", LabelPlacement.CENTER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelThrowsWhenYValueIsNull() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BarLabel(0, null, "Test", LabelPlacement.CENTER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelThrowsWhenTextIsNull() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BarLabel(0, numFactory.numOf(100.0), null, LabelPlacement.CENTER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelThrowsWhenPlacementIsNull() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BarLabel(0, numFactory.numOf(100.0), "Test", null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelThrowsWhenFontScaleIsNotPositive() {
|
||||
List<Double> invalidFontScales = List.of(-1.0, 0.0, Double.NaN, Double.NEGATIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY);
|
||||
|
||||
for (Double invalidFontScale : invalidFontScales) {
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> new BarLabel(0,
|
||||
numFactory.numOf(100.0), "Test", LabelPlacement.CENTER, null, invalidFontScale));
|
||||
assertEquals("fontScale must be positive and finite", thrown.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelAcceptsZeroBarIndex() {
|
||||
BarLabel label = new BarLabel(0, numFactory.numOf(100.0), "Test", LabelPlacement.CENTER);
|
||||
assertEquals(0, label.barIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelAcceptsEmptyText() {
|
||||
BarLabel label = new BarLabel(0, numFactory.numOf(100.0), "", LabelPlacement.CENTER);
|
||||
assertEquals("", label.text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBarLabelAllPlacementValues() {
|
||||
BarLabel above = new BarLabel(0, numFactory.numOf(100.0), "Above", LabelPlacement.ABOVE);
|
||||
BarLabel below = new BarLabel(1, numFactory.numOf(101.0), "Below", LabelPlacement.BELOW);
|
||||
BarLabel center = new BarLabel(2, numFactory.numOf(102.0), "Center", LabelPlacement.CENTER);
|
||||
|
||||
assertEquals(LabelPlacement.ABOVE, above.placement());
|
||||
assertEquals(LabelPlacement.BELOW, below.placement());
|
||||
assertEquals(LabelPlacement.CENTER, center.placement());
|
||||
}
|
||||
|
||||
// ========== getValue() Tests ==========
|
||||
|
||||
@Test
|
||||
void testGetValueReturnsYValueForLabeledIndex() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER),
|
||||
new BarLabel(7, numFactory.numOf(200.0), "Label 7", LabelPlacement.ABOVE));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value3 = indicator.getValue(3);
|
||||
assertEquals(150.0, value3.doubleValue(), 0.001);
|
||||
|
||||
Num value7 = indicator.getValue(7);
|
||||
assertEquals(200.0, value7.doubleValue(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueReturnsNaNForUnlabeledIndex() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value0 = indicator.getValue(0);
|
||||
assertTrue(value0.isNaN());
|
||||
|
||||
Num value5 = indicator.getValue(5);
|
||||
assertTrue(value5.isNaN());
|
||||
|
||||
Num value9 = indicator.getValue(9);
|
||||
assertTrue(value9.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueReturnsNaNForIndexBeforeSeries() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
// Even if we query an index that doesn't exist in the series,
|
||||
// we should get NaN if it's not labeled
|
||||
Num value = indicator.getValue(-1);
|
||||
assertTrue(value.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueReturnsNaNForIndexAfterSeries() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value = indicator.getValue(100);
|
||||
assertTrue(value.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueWithEmptyLabels() {
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, Collections.emptyList());
|
||||
|
||||
for (int i = 0; i <= series.getEndIndex(); i++) {
|
||||
Num value = indicator.getValue(i);
|
||||
assertTrue(value.isNaN());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueWithSingleLabel() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(5, numFactory.numOf(250.0), "Single", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value5 = indicator.getValue(5);
|
||||
assertEquals(250.0, value5.doubleValue(), 0.001);
|
||||
|
||||
Num value0 = indicator.getValue(0);
|
||||
assertTrue(value0.isNaN());
|
||||
|
||||
Num value9 = indicator.getValue(9);
|
||||
assertTrue(value9.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueWithLabelsAtBoundaries() {
|
||||
int beginIndex = series.getBeginIndex();
|
||||
int endIndex = series.getEndIndex();
|
||||
|
||||
List<BarLabel> labels = List.of(
|
||||
new BarLabel(beginIndex, numFactory.numOf(100.0), "Begin", LabelPlacement.CENTER),
|
||||
new BarLabel(endIndex, numFactory.numOf(109.0), "End", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num beginValue = indicator.getValue(beginIndex);
|
||||
assertEquals(100.0, beginValue.doubleValue(), 0.001);
|
||||
|
||||
Num endValue = indicator.getValue(endIndex);
|
||||
assertEquals(109.0, endValue.doubleValue(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetValueCachingBehavior() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value1 = indicator.getValue(3);
|
||||
Num value2 = indicator.getValue(3);
|
||||
Num value3 = indicator.getValue(3);
|
||||
|
||||
// Values should be equal (cached)
|
||||
assertEquals(value1, value2);
|
||||
assertEquals(value2, value3);
|
||||
// Since it's a CachedIndicator, same instance should be returned
|
||||
assertSame(value1, value2);
|
||||
assertSame(value2, value3);
|
||||
}
|
||||
|
||||
// ========== labels() Tests ==========
|
||||
|
||||
@Test
|
||||
void testLabelsReturnsOrderedList() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(8, numFactory.numOf(108.0), "Label 8", LabelPlacement.ABOVE));
|
||||
labels.add(new BarLabel(1, numFactory.numOf(101.0), "Label 1", LabelPlacement.BELOW));
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "Label 5", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertEquals(3, result.size());
|
||||
assertEquals(1, result.get(0).barIndex());
|
||||
assertEquals(5, result.get(1).barIndex());
|
||||
assertEquals(8, result.get(2).barIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLabelsReturnsImmutableList() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
result.add(new BarLabel(5, numFactory.numOf(105.0), "New", LabelPlacement.ABOVE));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
result.remove(0);
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
result.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLabelsProtectsInternalRepresentation() {
|
||||
// Create indicator with multiple labels
|
||||
List<BarLabel> originalLabels = new ArrayList<>();
|
||||
originalLabels.add(new BarLabel(2, numFactory.numOf(102.0), "Label 2", LabelPlacement.CENTER));
|
||||
originalLabels.add(new BarLabel(5, numFactory.numOf(105.0), "Label 5", LabelPlacement.ABOVE));
|
||||
originalLabels.add(new BarLabel(8, numFactory.numOf(108.0), "Label 8", LabelPlacement.BELOW));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, originalLabels);
|
||||
|
||||
// Get the list and verify initial state
|
||||
List<BarLabel> returnedList1 = indicator.labels();
|
||||
assertEquals(3, returnedList1.size());
|
||||
assertEquals(2, returnedList1.get(0).barIndex());
|
||||
assertEquals(5, returnedList1.get(1).barIndex());
|
||||
assertEquals(8, returnedList1.get(2).barIndex());
|
||||
|
||||
// Verify getValue() works correctly
|
||||
assertEquals(102.0, indicator.getValue(2).doubleValue(), 0.001);
|
||||
assertEquals(105.0, indicator.getValue(5).doubleValue(), 0.001);
|
||||
assertEquals(108.0, indicator.getValue(8).doubleValue(), 0.001);
|
||||
|
||||
// Attempt to modify the returned list - all should fail
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.add(new BarLabel(10, numFactory.numOf(110.0), "New Label", LabelPlacement.CENTER));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.add(0, new BarLabel(1, numFactory.numOf(101.0), "Inserted", LabelPlacement.CENTER));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.remove(0);
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.remove(returnedList1.get(0));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.set(0, new BarLabel(99, numFactory.numOf(999.0), "Replaced", LabelPlacement.CENTER));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.clear();
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.addAll(List.of(new BarLabel(10, numFactory.numOf(110.0), "New", LabelPlacement.CENTER)));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.removeAll(List.of(returnedList1.get(0)));
|
||||
});
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> {
|
||||
returnedList1.retainAll(Collections.emptyList());
|
||||
});
|
||||
|
||||
// Verify internal state is unchanged - get the list again
|
||||
List<BarLabel> returnedList2 = indicator.labels();
|
||||
assertEquals(3, returnedList2.size(), "Internal list size should remain unchanged");
|
||||
assertEquals(2, returnedList2.get(0).barIndex(), "First label should be unchanged");
|
||||
assertEquals(5, returnedList2.get(1).barIndex(), "Second label should be unchanged");
|
||||
assertEquals(8, returnedList2.get(2).barIndex(), "Third label should be unchanged");
|
||||
|
||||
// Verify getValue() still works correctly - internal state is protected
|
||||
assertEquals(102.0, indicator.getValue(2).doubleValue(), 0.001, "getValue(2) should still work");
|
||||
assertEquals(105.0, indicator.getValue(5).doubleValue(), 0.001, "getValue(5) should still work");
|
||||
assertEquals(108.0, indicator.getValue(8).doubleValue(), 0.001, "getValue(8) should still work");
|
||||
|
||||
// Verify unlabeled indices still return NaN
|
||||
assertTrue(indicator.getValue(0).isNaN(), "Unlabeled index should return NaN");
|
||||
assertTrue(indicator.getValue(9).isNaN(), "Unlabeled index should return NaN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLabelsReturnsCachedInstance() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result1 = indicator.labels();
|
||||
List<BarLabel> result2 = indicator.labels();
|
||||
|
||||
// Should return the same list instance (cached unmodifiable list)
|
||||
assertSame(result1, result2, "Each call should return the same cached instance");
|
||||
assertEquals(result1, result2, "Both should contain the same content");
|
||||
assertEquals(result1.size(), result2.size(), "Both should have the same size");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLabelsWithEmptyList() {
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, Collections.emptyList());
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLabelsPreservesAllLabelProperties() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Test Label", LabelPlacement.ABOVE),
|
||||
new BarLabel(7, numFactory.numOf(200.0), "Another Label", LabelPlacement.BELOW));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertEquals(2, result.size());
|
||||
|
||||
BarLabel label0 = result.get(0);
|
||||
assertEquals(3, label0.barIndex());
|
||||
assertEquals(150.0, label0.yValue().doubleValue(), 0.001);
|
||||
assertEquals("Test Label", label0.text());
|
||||
assertEquals(LabelPlacement.ABOVE, label0.placement());
|
||||
|
||||
BarLabel label1 = result.get(1);
|
||||
assertEquals(7, label1.barIndex());
|
||||
assertEquals(200.0, label1.yValue().doubleValue(), 0.001);
|
||||
assertEquals("Another Label", label1.text());
|
||||
assertEquals(LabelPlacement.BELOW, label1.placement());
|
||||
}
|
||||
|
||||
// ========== getCountOfUnstableBars() Tests ==========
|
||||
|
||||
@Test
|
||||
void testGetCountOfUnstableBarsReturnsZero() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(3, numFactory.numOf(150.0), "Label 3", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
assertEquals(0, indicator.getCountOfUnstableBars());
|
||||
}
|
||||
|
||||
// ========== Integration Tests ==========
|
||||
|
||||
@Test
|
||||
void testIndicatorWithManyLabels() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
for (int i = 0; i <= series.getEndIndex(); i++) {
|
||||
labels.add(new BarLabel(i, numFactory.numOf(100.0 + i), "Label " + i, LabelPlacement.CENTER));
|
||||
}
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
assertEquals(series.getBarCount(), indicator.labels().size());
|
||||
|
||||
for (int i = 0; i <= series.getEndIndex(); i++) {
|
||||
Num value = indicator.getValue(i);
|
||||
assertEquals(100.0 + i, value.doubleValue(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithSparseLabels() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(0, numFactory.numOf(100.0), "Start", LabelPlacement.ABOVE),
|
||||
new BarLabel(5, numFactory.numOf(105.0), "Middle", LabelPlacement.CENTER),
|
||||
new BarLabel(9, numFactory.numOf(109.0), "End", LabelPlacement.BELOW));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
// Check labeled indices
|
||||
assertEquals(100.0, indicator.getValue(0).doubleValue(), 0.001);
|
||||
assertEquals(105.0, indicator.getValue(5).doubleValue(), 0.001);
|
||||
assertEquals(109.0, indicator.getValue(9).doubleValue(), 0.001);
|
||||
|
||||
// Check unlabeled indices
|
||||
assertTrue(indicator.getValue(1).isNaN());
|
||||
assertTrue(indicator.getValue(2).isNaN());
|
||||
assertTrue(indicator.getValue(3).isNaN());
|
||||
assertTrue(indicator.getValue(4).isNaN());
|
||||
assertTrue(indicator.getValue(6).isNaN());
|
||||
assertTrue(indicator.getValue(7).isNaN());
|
||||
assertTrue(indicator.getValue(8).isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithAllPlacementTypes() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(0, numFactory.numOf(100.0), "Above", LabelPlacement.ABOVE),
|
||||
new BarLabel(1, numFactory.numOf(101.0), "Below", LabelPlacement.BELOW),
|
||||
new BarLabel(2, numFactory.numOf(102.0), "Center", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertEquals(3, result.size());
|
||||
assertEquals(LabelPlacement.ABOVE, result.get(0).placement());
|
||||
assertEquals(LabelPlacement.BELOW, result.get(1).placement());
|
||||
assertEquals(LabelPlacement.CENTER, result.get(2).placement());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithDuplicateIndicesKeepsLast() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "First", LabelPlacement.ABOVE));
|
||||
labels.add(new BarLabel(3, numFactory.numOf(103.0), "Second", LabelPlacement.BELOW));
|
||||
labels.add(new BarLabel(5, numFactory.numOf(205.0), "Third", LabelPlacement.CENTER)); // Duplicate
|
||||
labels.add(new BarLabel(7, numFactory.numOf(107.0), "Fourth", LabelPlacement.ABOVE));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
// The labels list keeps all labels (including duplicates), but sorted
|
||||
assertEquals(4, result.size());
|
||||
assertEquals(3, result.get(0).barIndex());
|
||||
assertEquals(5, result.get(1).barIndex());
|
||||
assertEquals(5, result.get(2).barIndex());
|
||||
assertEquals(7, result.get(3).barIndex());
|
||||
|
||||
// Check that getValue returns the last value for duplicate indices
|
||||
assertEquals(205.0, indicator.getValue(5).doubleValue(), 0.001);
|
||||
|
||||
// The labels list contains both labels for index 5, but getValue uses the map
|
||||
// which keeps the last
|
||||
List<BarLabel> labelsAt5 = result.stream().filter(l -> l.barIndex() == 5).toList();
|
||||
assertEquals(2, labelsAt5.size());
|
||||
assertEquals("First", labelsAt5.get(0).text());
|
||||
assertEquals("Third", labelsAt5.get(1).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithNullLabelsFiltered() {
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
labels.add(new BarLabel(0, numFactory.numOf(100.0), "Valid 1", LabelPlacement.CENTER));
|
||||
labels.add(null);
|
||||
labels.add(new BarLabel(5, numFactory.numOf(105.0), "Valid 2", LabelPlacement.ABOVE));
|
||||
labels.add(null);
|
||||
labels.add(null);
|
||||
labels.add(new BarLabel(9, numFactory.numOf(109.0), "Valid 3", LabelPlacement.BELOW));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertEquals(3, result.size());
|
||||
assertEquals(0, result.get(0).barIndex());
|
||||
assertEquals(5, result.get(1).barIndex());
|
||||
assertEquals(9, result.get(2).barIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithUnsortedLabels() {
|
||||
// Create labels in reverse order
|
||||
List<BarLabel> labels = new ArrayList<>();
|
||||
for (int i = series.getEndIndex(); i >= 0; i--) {
|
||||
labels.add(new BarLabel(i, numFactory.numOf(100.0 + i), "Label " + i, LabelPlacement.CENTER));
|
||||
}
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
List<BarLabel> result = indicator.labels();
|
||||
assertEquals(series.getBarCount(), result.size());
|
||||
|
||||
// Verify they are sorted
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(i, result.get(i).barIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithLargeBarIndex() {
|
||||
// Test with a bar index that's larger than the series size
|
||||
List<BarLabel> labels = List
|
||||
.of(new BarLabel(100, numFactory.numOf(1000.0), "Large Index", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
// Should still work - the indicator doesn't validate bar index bounds
|
||||
assertEquals(1, indicator.labels().size());
|
||||
assertEquals(1000.0, indicator.getValue(100).doubleValue(), 0.001);
|
||||
assertTrue(indicator.getValue(0).isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithZeroValue() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(5, numFactory.numOf(0.0), "Zero", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value = indicator.getValue(5);
|
||||
assertEquals(0.0, value.doubleValue(), 0.001);
|
||||
assertFalse(value.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithNegativeValue() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(5, numFactory.numOf(-50.0), "Negative", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value = indicator.getValue(5);
|
||||
assertEquals(-50.0, value.doubleValue(), 0.001);
|
||||
assertFalse(value.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithVeryLargeValue() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(5, numFactory.numOf(1e10), "Large", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value = indicator.getValue(5);
|
||||
assertEquals(1e10, value.doubleValue(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIndicatorWithVerySmallValue() {
|
||||
List<BarLabel> labels = List.of(new BarLabel(5, numFactory.numOf(1e-10), "Small", LabelPlacement.CENTER));
|
||||
|
||||
BarSeriesLabelIndicator indicator = new BarSeriesLabelIndicator(series, labels);
|
||||
|
||||
Num value = indicator.getValue(5);
|
||||
assertEquals(1e-10, value.doubleValue(), 0.0001);
|
||||
}
|
||||
}
|
||||
+965
@@ -0,0 +1,965 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.builder;
|
||||
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.jfree.chart.axis.NumberAxis;
|
||||
import org.jfree.chart.plot.CombinedDomainXYPlot;
|
||||
import org.jfree.chart.plot.XYPlot;
|
||||
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
|
||||
import org.jfree.chart.renderer.xy.XYDifferenceRenderer;
|
||||
import org.jfree.chart.ui.Layer;
|
||||
import org.jfree.data.time.TimeSeriesCollection;
|
||||
import org.jfree.data.xy.OHLCDataset;
|
||||
import org.jfree.data.xy.XYDataset;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
|
||||
import org.ta4j.core.indicators.RSIIndicator;
|
||||
import org.ta4j.core.indicators.PriceChannel;
|
||||
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ta4jexamples.charting.ChannelBoundaryIndicator;
|
||||
import ta4jexamples.charting.ChartingTestFixtures;
|
||||
import ta4jexamples.charting.compose.TradingChartFactory;
|
||||
import ta4jexamples.charting.workflow.ChartWorkflow;
|
||||
|
||||
class ChartBuilderTest {
|
||||
|
||||
private ChartWorkflow chartWorkflow;
|
||||
private BarSeries series;
|
||||
private TradingRecord tradingRecord;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
chartWorkflow = new ChartWorkflow();
|
||||
series = ChartingTestFixtures.standardDailySeries();
|
||||
tradingRecord = ChartingTestFixtures.completedTradeRecord(series);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorRejectsNullWorkflow() {
|
||||
TradingChartFactory chartFactory = new TradingChartFactory();
|
||||
|
||||
assertThrows(NullPointerException.class, () -> new ChartBuilder(null, chartFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorRejectsNullFactory() {
|
||||
assertThrows(NullPointerException.class, () -> new ChartBuilder(new ChartWorkflow(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsCandlestickChartWithIndicatorOverlay() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.CYAN)
|
||||
.toChart();
|
||||
|
||||
assertNotNull(chart);
|
||||
assertInstanceOf(CombinedDomainXYPlot.class, chart.getPlot());
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
assertEquals(1, combined.getSubplots().size());
|
||||
XYPlot basePlot = combined.getSubplots().get(0);
|
||||
// Candle dataset + overlay dataset
|
||||
assertEquals(2, basePlot.getDatasetCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void barIndexTimeAxisModeBuildsNumberAxis() {
|
||||
BarSeries gapSeries = ChartingTestFixtures.dailySeriesWithWeekendGap("Gap Series");
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withTimeAxisMode(TimeAxisMode.BAR_INDEX)
|
||||
.withSeries(gapSeries)
|
||||
.toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
assertInstanceOf(NumberAxis.class, combined.getDomainAxis());
|
||||
|
||||
XYPlot basePlot = combined.getSubplots().get(0);
|
||||
OHLCDataset dataset = (OHLCDataset) basePlot.getDataset(0);
|
||||
assertEquals(gapSeries.getBeginIndex(), dataset.getXValue(0, 0), 0.0);
|
||||
assertEquals(gapSeries.getBeginIndex() + 1, dataset.getXValue(0, 1), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withChannelOverlayAddsUpperMedianLowerOverlays() {
|
||||
ConstantIndicator upper = constantIndicator(110, "upper");
|
||||
ConstantIndicator median = constantIndicator(100, "median");
|
||||
ConstantIndicator lower = constantIndicator(90, "lower");
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withChannelOverlay(upper, median, lower)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertEquals(5, basePlot.getDatasetCount(), "Channel overlays should add fill plus three datasets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withChannelOverlayAddsUpperAndLowerOverlays() {
|
||||
ConstantIndicator upper = constantIndicator(110, "upper");
|
||||
ConstantIndicator lower = constantIndicator(90, "lower");
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder().withSeries(series).withChannelOverlay(upper, lower).toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertEquals(4, basePlot.getDatasetCount(), "Channel overlays should add fill plus two datasets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withChannelOverlayWrapsPriceChannelIndicator() {
|
||||
ConstantChannelIndicator channelIndicator = new ConstantChannelIndicator(series, 110, 90, 100, "Test Channel");
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder().withSeries(series).withChannelOverlay(channelIndicator).toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertEquals(5, basePlot.getDatasetCount(),
|
||||
"Channel overlays should add fill plus upper, median, and lower datasets when wrapping a channel indicator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void channelOverlayMedianDefaultsToDashedLowerOpacity() {
|
||||
ConstantIndicator upper = constantIndicator(110, "upper");
|
||||
ConstantIndicator median = constantIndicator(100, "median");
|
||||
ConstantIndicator lower = constantIndicator(90, "lower");
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withChannelOverlay(upper, median, lower)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
int medianDatasetIndex = -1;
|
||||
for (int i = 0; i < basePlot.getDatasetCount(); i++) {
|
||||
if (basePlot.getDataset(i) instanceof TimeSeriesCollection collection && collection.getSeriesCount() > 0
|
||||
&& "median".equals(collection.getSeriesKey(0))) {
|
||||
medianDatasetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(medianDatasetIndex >= 0, "Median dataset should be present");
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(medianDatasetIndex);
|
||||
BasicStroke stroke = (BasicStroke) renderer.getSeriesStroke(0);
|
||||
assertNotNull(stroke.getDashArray(), "Median stroke should be dashed by default");
|
||||
assertTrue(stroke.getLineWidth() < 1.2f, "Median stroke should be thinner than default channel lines");
|
||||
}
|
||||
|
||||
@Test
|
||||
void channelOverlayStylingUpdatesLinesAndFill() {
|
||||
ConstantIndicator upper = constantIndicator(110, "upper");
|
||||
ConstantIndicator lower = constantIndicator(90, "lower");
|
||||
Color lineColor = new Color(0x607D8B);
|
||||
Color fillColor = new Color(0x90A4AE);
|
||||
float lineOpacity = 0.6f;
|
||||
float fillOpacity = 0.2f;
|
||||
float lineWidth = 2.2f;
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withChannelOverlay(upper, lower)
|
||||
.withLineColor(lineColor)
|
||||
.withOpacity(lineOpacity)
|
||||
.withLineWidth(lineWidth)
|
||||
.withFillColor(fillColor)
|
||||
.withFillOpacity(fillOpacity)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
XYDifferenceRenderer fillRenderer = null;
|
||||
int standardRendererCount = 0;
|
||||
for (int i = 0; i < basePlot.getRendererCount(); i++) {
|
||||
if (basePlot.getRenderer(i) instanceof XYDifferenceRenderer differenceRenderer) {
|
||||
fillRenderer = differenceRenderer;
|
||||
} else if (basePlot.getRenderer(i) instanceof StandardXYItemRenderer standardRenderer) {
|
||||
Color paint = (Color) standardRenderer.getSeriesPaint(0);
|
||||
assertEquals(lineColor.getRed(), paint.getRed(), "Channel line red should match");
|
||||
assertEquals(lineColor.getGreen(), paint.getGreen(), "Channel line green should match");
|
||||
assertEquals(lineColor.getBlue(), paint.getBlue(), "Channel line blue should match");
|
||||
assertEquals(Math.round(lineOpacity * 255), paint.getAlpha(), "Channel line opacity should match");
|
||||
BasicStroke stroke = (BasicStroke) standardRenderer.getSeriesStroke(0);
|
||||
assertEquals(lineWidth, stroke.getLineWidth(), "Channel line width should match");
|
||||
standardRendererCount++;
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(fillRenderer, "Channel fill renderer should be present");
|
||||
Color fillPaint = (Color) fillRenderer.getPositivePaint();
|
||||
assertEquals(fillColor.getRed(), fillPaint.getRed(), "Channel fill red should match");
|
||||
assertEquals(fillColor.getGreen(), fillPaint.getGreen(), "Channel fill green should match");
|
||||
assertEquals(fillColor.getBlue(), fillPaint.getBlue(), "Channel fill blue should match");
|
||||
assertEquals(Math.round(fillOpacity * 255), fillPaint.getAlpha(), "Channel fill opacity should match");
|
||||
assertEquals(2, standardRendererCount, "Channel should render two line overlays");
|
||||
}
|
||||
|
||||
@Test
|
||||
void channelOverlaysRequireUpperAndLowerBoundaries() {
|
||||
ConstantChannelIndicator channelIndicator = new ConstantChannelIndicator(series, 110, 90, 100,
|
||||
"Partial Channel");
|
||||
ChannelBoundaryIndicator upperBoundary = new ChannelBoundaryIndicator(channelIndicator,
|
||||
PriceChannel.Boundary.UPPER);
|
||||
|
||||
ChartBuilder.ChartStage stage = chartWorkflow.builder().withSeries(series).withIndicatorOverlay(upperBoundary);
|
||||
|
||||
assertThrows(IllegalStateException.class, stage::toChart,
|
||||
"Channel overlays should require both upper and lower boundaries before charting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void channelOverlaysAllowUpperAndLowerBoundariesTogether() {
|
||||
ConstantChannelIndicator channelIndicator = new ConstantChannelIndicator(series, 110, 90, 100, "Full Channel");
|
||||
ChannelBoundaryIndicator upperBoundary = new ChannelBoundaryIndicator(channelIndicator,
|
||||
PriceChannel.Boundary.UPPER);
|
||||
ChannelBoundaryIndicator lowerBoundary = new ChannelBoundaryIndicator(channelIndicator,
|
||||
PriceChannel.Boundary.LOWER);
|
||||
|
||||
ChartBuilder.ChartStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(upperBoundary)
|
||||
.withIndicatorOverlay(lowerBoundary);
|
||||
|
||||
assertDoesNotThrow(stage::toChart, "Upper and lower boundaries should satisfy channel overlay validation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsIndicatorBaseChartsWithSubplot() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
JFreeChart chart = chartWorkflow.builder().withIndicator(closePrice).withSubChart(rsiIndicator).toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
assertEquals(2, combined.getSubplots().size(), "Indicator chart should contain base plot plus sub-plot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tradingRecordOverlayAddsMarkers() {
|
||||
JFreeChart chart = chartWorkflow.builder().withSeries(series).withTradingRecordOverlay(tradingRecord).toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
XYPlot basePlot = combined.getSubplots().get(0);
|
||||
assertTrue(basePlot.getRendererCount() >= 2,
|
||||
"Trading record overlay should install an additional renderer for markers");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayStylingAppliesCustomColorAndStroke() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.ORANGE)
|
||||
.withLineWidth(3f)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
assertEquals(Color.ORANGE, renderer.getSeriesPaint(0));
|
||||
BasicStroke stroke = (BasicStroke) renderer.getSeriesStroke(0);
|
||||
assertEquals(3f, stroke.getLineWidth());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSubChartInvalidatesPreviousStage() {
|
||||
ChartBuilder.ChartStage baseStage = chartWorkflow.builder().withSeries(series);
|
||||
ChartBuilder.ChartStage subStage = baseStage.withSubChart(new ClosePriceIndicator(series));
|
||||
assertThrows(IllegalStateException.class, baseStage::toChart,
|
||||
"Parent stage should be unusable after withSubChart is invoked");
|
||||
|
||||
assertNotNull(subStage.toChart(), "Sub stage should still be able to build the chart");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerAddsReferenceLine() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0)
|
||||
.toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
XYPlot indicatorPlot = combined.getSubplots().get(1);
|
||||
Collection<?> rangeMarkers = indicatorPlot.getRangeMarkers(Layer.FOREGROUND);
|
||||
assertNotNull(rangeMarkers, "Should have range markers");
|
||||
assertFalse(rangeMarkers.isEmpty(), "Should have horizontal marker at 50");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerSupportsStyling() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
Color customColor = Color.RED;
|
||||
float customWidth = 2.5f;
|
||||
float customOpacity = 0.7f;
|
||||
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0)
|
||||
.withLineColor(customColor)
|
||||
.withLineWidth(customWidth)
|
||||
.withOpacity(customOpacity)
|
||||
.toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
XYPlot indicatorPlot = combined.getSubplots().get(1);
|
||||
Collection<?> rangeMarkers = indicatorPlot.getRangeMarkers(Layer.FOREGROUND);
|
||||
assertFalse(rangeMarkers.isEmpty(), "Should have horizontal marker");
|
||||
// Verify marker exists (styling is applied during rendering)
|
||||
assertTrue(rangeMarkers.size() >= 1, "Should have at least one marker");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMultipleHorizontalMarkers() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(30.0)
|
||||
.withHorizontalMarker(50.0)
|
||||
.withHorizontalMarker(70.0)
|
||||
.toChart();
|
||||
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
XYPlot indicatorPlot = combined.getSubplots().get(1);
|
||||
Collection<?> rangeMarkers = indicatorPlot.getRangeMarkers(Layer.FOREGROUND);
|
||||
assertNotNull(rangeMarkers, "Should have range markers");
|
||||
assertTrue(rangeMarkers.size() >= 3, "Should have at least 3 horizontal markers");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerRejectsInvalidLineWidth() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
ChartBuilder.StyledMarkerStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withLineWidth(0.0f),
|
||||
"Line width of 0 should be rejected");
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withLineWidth(-1.0f),
|
||||
"Negative line width should be rejected");
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withLineWidth(0.05f),
|
||||
"Line width of 0.05 should be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerRejectsInvalidOpacity() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
ChartBuilder.StyledMarkerStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withOpacity(-0.1f),
|
||||
"Negative opacity should be rejected");
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withOpacity(1.1f),
|
||||
"Opacity greater than 1.0 should be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerAcceptsValidOpacity() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
ChartBuilder.StyledMarkerStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0);
|
||||
|
||||
// These should not throw
|
||||
assertDoesNotThrow(() -> stage.withOpacity(0.0f), "Opacity of 0.0 should be valid");
|
||||
assertDoesNotThrow(() -> stage.withOpacity(0.5f), "Opacity of 0.5 should be valid");
|
||||
assertDoesNotThrow(() -> stage.withOpacity(1.0f), "Opacity of 1.0 should be valid");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerRejectsNullColor() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
ChartBuilder.StyledMarkerStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0);
|
||||
|
||||
assertThrows(NullPointerException.class, () -> stage.withLineColor(null), "Null color should be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHorizontalMarkerChainingWorks() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
RSIIndicator rsiIndicator = new RSIIndicator(closePrice, 14);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withSubChart(rsiIndicator)
|
||||
.withHorizontalMarker(50.0)
|
||||
.withLineColor(Color.BLUE)
|
||||
.withLineWidth(2.0f)
|
||||
.withOpacity(0.8f)
|
||||
.withHorizontalMarker(30.0)
|
||||
.withLineColor(Color.GREEN)
|
||||
.toChart();
|
||||
|
||||
assertNotNull(chart, "Chart should be created with chained marker styling");
|
||||
CombinedDomainXYPlot combined = (CombinedDomainXYPlot) chart.getPlot();
|
||||
XYPlot indicatorPlot = combined.getSubplots().get(1);
|
||||
Collection<?> rangeMarkers = indicatorPlot.getRangeMarkers(Layer.FOREGROUND);
|
||||
assertTrue(rangeMarkers.size() >= 2, "Should have at least 2 markers with chained styling");
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalOperationConsumesBuilder() {
|
||||
ChartBuilder.ChartStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withTradingRecordOverlay(tradingRecord);
|
||||
stage.toChart();
|
||||
assertThrows(IllegalStateException.class, stage::toChart,
|
||||
"All terminal operations should fail after the builder is consumed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesChartPlanForManualExecution() {
|
||||
ChartPlan plan = chartWorkflow.builder().withSeries(series).toPlan();
|
||||
JFreeChart chart = chartWorkflow.render(plan);
|
||||
assertNotNull(chart, "Rendered chart should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPlanAlsoConsumesBuilder() {
|
||||
ChartBuilder.ChartStage stage = chartWorkflow.builder().withSeries(series);
|
||||
stage.toPlan();
|
||||
assertThrows(IllegalStateException.class, stage::toChart,
|
||||
"Invoking toPlan should consume the builder like other terminal operations");
|
||||
}
|
||||
|
||||
@Test
|
||||
void analysisCriterionOverlayCreatesSecondaryAxis() {
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withTradingRecordOverlay(tradingRecord)
|
||||
.withAnalysisCriterionOverlay(new NetProfitCriterion(), tradingRecord)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertNotNull(basePlot.getRangeAxis(1), "Analysis overlays should create a secondary axis");
|
||||
assertEquals("NetProfit", basePlot.getRangeAxis(1).getLabel(), "Secondary axis should reflect criterion label");
|
||||
assertTrue(plotContainsSeries(basePlot, "NetProfit"),
|
||||
"Criterion overlay should contribute a dataset with its label");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tradingRecordOverlayIgnoredForIndicatorCharts() {
|
||||
ConstantIndicator baseIndicator = constantIndicator(1, "base-indicator");
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withIndicator(baseIndicator)
|
||||
.withTradingRecordOverlay(tradingRecord)
|
||||
.toChart();
|
||||
|
||||
XYPlot plot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertEquals(1, plot.getDatasetCount(),
|
||||
"Indicator charts should ignore trading record overlays due to incompatible axes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOverlaysWhenBothAxesOccupied() {
|
||||
ConstantIndicator baseIndicator = constantIndicator(5, "base");
|
||||
ConstantIndicator near = constantIndicator(6, "near");
|
||||
ConstantIndicator far = constantIndicator(500, "far");
|
||||
ConstantIndicator rejected = constantIndicator(-500, "rejected");
|
||||
|
||||
ChartBuilder.ChartStage dualAxisStage = chartWorkflow.builder()
|
||||
.withIndicator(baseIndicator)
|
||||
.withIndicatorOverlay(near)
|
||||
.withIndicatorOverlay(far);
|
||||
JFreeChart chartBefore = dualAxisStage.toChart();
|
||||
XYPlot plotBefore = ((CombinedDomainXYPlot) chartBefore.getPlot()).getSubplots().get(0);
|
||||
int datasetCountBefore = plotBefore.getDatasetCount();
|
||||
assertNotNull(plotBefore.getRangeAxis(1), "Secondary axis should be created for the far overlay");
|
||||
|
||||
ChartBuilder.StyledOverlayStage rejectedStage = chartWorkflow.builder()
|
||||
.withIndicator(baseIndicator)
|
||||
.withIndicatorOverlay(near)
|
||||
.withIndicatorOverlay(far)
|
||||
.withIndicatorOverlay(rejected);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> rejectedStage.withLineColor(Color.RED),
|
||||
"Styling an invalid overlay should fail fast");
|
||||
|
||||
JFreeChart chart = rejectedStage.toChart();
|
||||
XYPlot plot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertNotNull(plot.getRangeAxis(1), "Secondary axis should persist after rejecting the overlay");
|
||||
assertEquals(datasetCountBefore, plot.getDatasetCount(),
|
||||
"Rejected overlay must not mutate the dataset collection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayLineWidthValidation() {
|
||||
ChartBuilder.StyledOverlayStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(new ClosePriceIndicator(series));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withLineWidth(0.01f),
|
||||
"Line width must be positive and above the minimum threshold");
|
||||
assertDoesNotThrow(stage::toChart);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayStylingAppliesCustomOpacity() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
float opacity = 0.5f;
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.BLUE)
|
||||
.withOpacity(opacity)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
Color paintColor = (Color) renderer.getSeriesPaint(0);
|
||||
assertEquals(Color.BLUE.getRed(), paintColor.getRed());
|
||||
assertEquals(Color.BLUE.getGreen(), paintColor.getGreen());
|
||||
assertEquals(Color.BLUE.getBlue(), paintColor.getBlue());
|
||||
assertEquals(Math.round(opacity * 255), paintColor.getAlpha(),
|
||||
"Opacity should be applied to the color's alpha channel");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayOpacityDefaultsToFullyOpaque() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.RED)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
Color paintColor = (Color) renderer.getSeriesPaint(0);
|
||||
assertEquals(255, paintColor.getAlpha(), "Default opacity should be 1.0 (fully opaque)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayOpacityValidation() {
|
||||
ChartBuilder.StyledOverlayStage stage = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(new ClosePriceIndicator(series));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withOpacity(-0.1f), "Opacity must be >= 0.0");
|
||||
assertThrows(IllegalArgumentException.class, () -> stage.withOpacity(1.1f), "Opacity must be <= 1.0");
|
||||
assertDoesNotThrow(() -> stage.withOpacity(0.0f), "Opacity 0.0 should be valid");
|
||||
assertDoesNotThrow(() -> stage.withOpacity(1.0f), "Opacity 1.0 should be valid");
|
||||
assertDoesNotThrow(() -> stage.withOpacity(0.5f), "Opacity 0.5 should be valid");
|
||||
assertDoesNotThrow(stage::toChart);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayOpacityCanBeChainedWithOtherStylingMethods() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
float opacity = 0.75f;
|
||||
Color customColor = new Color(128, 0, 128); // Purple
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withOpacity(opacity)
|
||||
.withLineColor(customColor)
|
||||
.withLineWidth(2.5f)
|
||||
.withConnectAcrossNaN(true)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
Color paintColor = (Color) renderer.getSeriesPaint(0);
|
||||
assertEquals(customColor.getRed(), paintColor.getRed());
|
||||
assertEquals(customColor.getGreen(), paintColor.getGreen());
|
||||
assertEquals(customColor.getBlue(), paintColor.getBlue());
|
||||
assertEquals(Math.round(opacity * 255), paintColor.getAlpha(),
|
||||
"Opacity should be preserved when chained with other styling methods");
|
||||
|
||||
BasicStroke stroke = (BasicStroke) renderer.getSeriesStroke(0);
|
||||
assertEquals(2.5f, stroke.getLineWidth(), "Line width should still be applied");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayOpacityCanBeSetAfterOtherStylingMethods() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
float opacity = 0.6f;
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.CYAN)
|
||||
.withLineWidth(3.0f)
|
||||
.withOpacity(opacity)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
Color paintColor = (Color) renderer.getSeriesPaint(0);
|
||||
assertEquals(Math.round(opacity * 255), paintColor.getAlpha(),
|
||||
"Opacity should work when set after other styling methods");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlayOpacityEdgeCases() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
|
||||
// Test fully transparent (0.0)
|
||||
JFreeChart chartTransparent = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.GREEN)
|
||||
.withOpacity(0.0f)
|
||||
.toChart();
|
||||
XYPlot basePlotTransparent = ((CombinedDomainXYPlot) chartTransparent.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer rendererTransparent = (StandardXYItemRenderer) basePlotTransparent.getRenderer(1);
|
||||
Color paintColorTransparent = (Color) rendererTransparent.getSeriesPaint(0);
|
||||
assertEquals(0, paintColorTransparent.getAlpha(), "Opacity 0.0 should result in fully transparent color");
|
||||
|
||||
// Test fully opaque (1.0)
|
||||
JFreeChart chartOpaque = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.GREEN)
|
||||
.withOpacity(1.0f)
|
||||
.toChart();
|
||||
XYPlot basePlotOpaque = ((CombinedDomainXYPlot) chartOpaque.getPlot()).getSubplots().get(0);
|
||||
StandardXYItemRenderer rendererOpaque = (StandardXYItemRenderer) basePlotOpaque.getRenderer(1);
|
||||
Color paintColorOpaque = (Color) rendererOpaque.getSeriesPaint(0);
|
||||
assertEquals(255, paintColorOpaque.getAlpha(), "Opacity 1.0 should result in fully opaque color");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNFalseCreatesMultipleSegmentsForNaNValues() {
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(indicator)
|
||||
.withConnectAcrossNaN(false)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertTrue(dataset.getSeriesCount() > 1,
|
||||
"When connectGaps is false, NaN values should create multiple series segments");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNTrueCreatesSingleSegmentForNaNValues() {
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(indicator)
|
||||
.withConnectAcrossNaN(true)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(1, dataset.getSeriesCount(),
|
||||
"When connectGaps is true, NaN values should be skipped and non-NaN values connected in a single segment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNDefaultsToFalse() {
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder().withSeries(series).withIndicatorOverlay(indicator).toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertTrue(dataset.getSeriesCount() > 1,
|
||||
"By default (connectGaps not set), NaN values should create multiple series segments");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNCanBeChainedWithOtherStyling() {
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(indicator)
|
||||
.withLineColor(Color.MAGENTA)
|
||||
.withLineWidth(2.5f)
|
||||
.withConnectAcrossNaN(true)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(1, dataset.getSeriesCount(),
|
||||
"withConnectAcrossNaN should work when chained with other styling methods");
|
||||
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
assertEquals(Color.MAGENTA, renderer.getSeriesPaint(0), "Color should still be applied");
|
||||
BasicStroke stroke = (BasicStroke) renderer.getSeriesStroke(0);
|
||||
assertEquals(2.5f, stroke.getLineWidth(), "Line width should still be applied");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNTrueSkipsNaNButConnectsValidValues() {
|
||||
// Create indicator with valid values at indices 0,1, NaN at 2,3,4, then valid
|
||||
// at 5,6
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(indicator)
|
||||
.withConnectAcrossNaN(true)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(1, dataset.getSeriesCount(),
|
||||
"All valid values should be in a single connected segment when connectGaps is true");
|
||||
|
||||
// Verify the segment contains values from before and after the NaN gap
|
||||
org.jfree.data.time.TimeSeries timeSeries = dataset.getSeries(0);
|
||||
assertTrue(timeSeries.getItemCount() > 0, "The connected segment should contain valid values");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withConnectAcrossNaNFalseSplitsOnNaN() {
|
||||
// Create indicator with valid values at indices 0,1, NaN at 2,3,4, then valid
|
||||
// at 5,6
|
||||
IndicatorWithNaN indicator = new IndicatorWithNaN(series, new int[] { 2, 3, 4 });
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(indicator)
|
||||
.withConnectAcrossNaN(false)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(2, dataset.getSeriesCount(),
|
||||
"NaN values should split the series into two segments (before and after the gap)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLabelSetsCustomLabelInChartLegend() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
String customLabel = "Custom Close Price Label";
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLabel(customLabel)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(customLabel, dataset.getSeriesKey(0), "Custom label should appear in the chart legend");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLabelDefaultsToIndicatorToStringWhenNotSet() {
|
||||
ConstantIndicator indicator = constantIndicator(100.0, "TestIndicator");
|
||||
JFreeChart chart = chartWorkflow.builder().withSeries(series).withIndicatorOverlay(indicator).toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals("TestIndicator", dataset.getSeriesKey(0),
|
||||
"When no label is set, should fall back to indicator.toString()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLabelCanBeChainedWithOtherStylingMethods() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
String customLabel = "Styled Indicator";
|
||||
Color customColor = new Color(128, 0, 128); // Purple color
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLabel(customLabel)
|
||||
.withLineColor(customColor)
|
||||
.withLineWidth(2.5f)
|
||||
.withConnectAcrossNaN(true)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(customLabel, dataset.getSeriesKey(0),
|
||||
"Label should be preserved when chained with other styling methods");
|
||||
|
||||
StandardXYItemRenderer renderer = (StandardXYItemRenderer) basePlot.getRenderer(1);
|
||||
assertEquals(customColor, renderer.getSeriesPaint(0), "Color should still be applied");
|
||||
BasicStroke stroke = (BasicStroke) renderer.getSeriesStroke(0);
|
||||
assertEquals(2.5f, stroke.getLineWidth(), "Line width should still be applied");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLabelUsedForSecondaryAxisWhenApplicable() {
|
||||
ConstantIndicator baseIndicator = constantIndicator(5, "base");
|
||||
ConstantIndicator farIndicator = constantIndicator(500, "far-indicator");
|
||||
String customLabel = "Custom Secondary Axis Label";
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withIndicator(baseIndicator)
|
||||
.withIndicatorOverlay(farIndicator)
|
||||
.withLabel(customLabel)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
assertNotNull(basePlot.getRangeAxis(1), "Secondary axis should be created for the far overlay");
|
||||
assertEquals(customLabel, basePlot.getRangeAxis(1).getLabel(),
|
||||
"Custom label should be used for the secondary axis label");
|
||||
assertTrue(plotContainsSeries(basePlot, customLabel), "Custom label should appear in the dataset");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLabelCanBeSetAfterOtherStylingMethods() {
|
||||
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
|
||||
String customLabel = "Label Set Last";
|
||||
JFreeChart chart = chartWorkflow.builder()
|
||||
.withSeries(series)
|
||||
.withIndicatorOverlay(closePrice)
|
||||
.withLineColor(Color.CYAN)
|
||||
.withLineWidth(3.0f)
|
||||
.withLabel(customLabel)
|
||||
.toChart();
|
||||
|
||||
XYPlot basePlot = ((CombinedDomainXYPlot) chart.getPlot()).getSubplots().get(0);
|
||||
TimeSeriesCollection dataset = (TimeSeriesCollection) basePlot.getDataset(1);
|
||||
assertEquals(customLabel, dataset.getSeriesKey(0), "Label should work when set after other styling methods");
|
||||
}
|
||||
|
||||
private ConstantIndicator constantIndicator(double value, String name) {
|
||||
return new ConstantIndicator(series, value, name);
|
||||
}
|
||||
|
||||
private boolean plotContainsSeries(XYPlot plot, String seriesName) {
|
||||
for (int i = 0; i < plot.getDatasetCount(); i++) {
|
||||
XYDataset dataset = plot.getDataset(i);
|
||||
if (dataset instanceof TimeSeriesCollection collection) {
|
||||
for (int j = 0; j < collection.getSeriesCount(); j++) {
|
||||
if (seriesName.equals(collection.getSeriesKey(j))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class ConstantIndicator implements Indicator<Num> {
|
||||
private final Num value;
|
||||
private final String name;
|
||||
private final BarSeries series;
|
||||
|
||||
private ConstantIndicator(BarSeries series, double value, String name) {
|
||||
this.series = series;
|
||||
this.value = series.numFactory().numOf(value);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test indicator that returns NaN for specified indices and valid values for
|
||||
* others.
|
||||
*/
|
||||
private static final class IndicatorWithNaN implements Indicator<Num> {
|
||||
private final BarSeries series;
|
||||
private final int[] nanIndices;
|
||||
private final Num validValue;
|
||||
|
||||
IndicatorWithNaN(BarSeries series, int[] nanIndices) {
|
||||
this.series = series;
|
||||
this.nanIndices = nanIndices.clone();
|
||||
this.validValue = series.numFactory().numOf(100.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
for (int nanIndex : nanIndices) {
|
||||
if (index == nanIndex) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
}
|
||||
return validValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestIndicatorWithNaN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
private record TestChannel(Num upper, Num lower, Num median) implements PriceChannel {
|
||||
}
|
||||
|
||||
private static final class ConstantChannelIndicator implements Indicator<PriceChannel> {
|
||||
private final BarSeries series;
|
||||
private final PriceChannel channel;
|
||||
private final String name;
|
||||
|
||||
private ConstantChannelIndicator(BarSeries series, double upper, double lower, double median, String name) {
|
||||
this.series = series;
|
||||
this.channel = new TestChannel(series.numFactory().numOf(upper), series.numFactory().numOf(lower),
|
||||
series.numFactory().numOf(median));
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceChannel getValue(int index) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2158
File diff suppressed because it is too large
Load Diff
+150
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.display;
|
||||
|
||||
import org.jfree.data.time.Minute;
|
||||
import org.jfree.data.time.TimeSeries;
|
||||
import org.jfree.data.time.TimeSeriesCollection;
|
||||
import org.jfree.data.xy.DefaultOHLCDataset;
|
||||
import org.jfree.data.xy.XYSeries;
|
||||
import org.jfree.data.xy.XYSeriesCollection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ta4jexamples.charting.ChartingTestFixtures;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ChartDataExtractor}.
|
||||
*/
|
||||
class ChartDataExtractorTest {
|
||||
|
||||
private final ChartDataExtractor extractor = new ChartDataExtractor();
|
||||
|
||||
@Test
|
||||
void testExtractDataTextFromTimeSeriesCollection() {
|
||||
// Create a TimeSeriesCollection with test data
|
||||
TimeSeries timeSeries = new TimeSeries("Test Indicator");
|
||||
Date baseDate = new Date();
|
||||
timeSeries.add(new Minute(baseDate), 100.0);
|
||||
timeSeries.add(new Minute(new Date(baseDate.getTime() + 60000)), 101.5);
|
||||
timeSeries.add(new Minute(new Date(baseDate.getTime() + 120000)), 102.0);
|
||||
|
||||
TimeSeriesCollection dataset = new TimeSeriesCollection();
|
||||
dataset.addSeries(timeSeries);
|
||||
|
||||
// Test extraction from TimeSeriesCollection
|
||||
String result = extractor.extractDataText(dataset, 0, 0);
|
||||
|
||||
assertNotNull(result, "Extracted text should not be null");
|
||||
assertFalse(result.isEmpty(), "Extracted text should not be empty");
|
||||
assertTrue(result.contains("Test Indicator"), "Extracted text should contain series name");
|
||||
assertTrue(result.contains("100"), "Extracted text should contain value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextFromTimeSeriesCollectionWithMultipleSeries() {
|
||||
// Create a TimeSeriesCollection with multiple series
|
||||
TimeSeries series1 = new TimeSeries("Series 1");
|
||||
TimeSeries series2 = new TimeSeries("Series 2");
|
||||
Date baseDate = new Date();
|
||||
series1.add(new Minute(baseDate), 100.0);
|
||||
series2.add(new Minute(baseDate), 200.0);
|
||||
|
||||
TimeSeriesCollection dataset = new TimeSeriesCollection();
|
||||
dataset.addSeries(series1);
|
||||
dataset.addSeries(series2);
|
||||
|
||||
// Test extraction from first series
|
||||
String result1 = extractor.extractDataText(dataset, 0, 0);
|
||||
assertNotNull(result1, "Extracted text from first series should not be null");
|
||||
assertTrue(result1.contains("Series 1"), "Extracted text should contain first series name");
|
||||
assertTrue(result1.contains("100"), "Extracted text should contain first series value");
|
||||
|
||||
// Test extraction from second series
|
||||
String result2 = extractor.extractDataText(dataset, 1, 0);
|
||||
assertNotNull(result2, "Extracted text from second series should not be null");
|
||||
assertTrue(result2.contains("Series 2"), "Extracted text should contain second series name");
|
||||
assertTrue(result2.contains("200"), "Extracted text should contain second series value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextFromTimeSeriesCollectionHandlesInvalidIndex() {
|
||||
TimeSeries timeSeries = new TimeSeries("Test Indicator");
|
||||
Date baseDate = new Date();
|
||||
timeSeries.add(new Minute(baseDate), 100.0);
|
||||
|
||||
TimeSeriesCollection dataset = new TimeSeriesCollection();
|
||||
dataset.addSeries(timeSeries);
|
||||
|
||||
// Test with invalid item index (should handle gracefully)
|
||||
String result = extractor.extractDataText(dataset, 0, 999);
|
||||
// Should return null for invalid index
|
||||
assertNull(result, "Should return null for invalid index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextFromDefaultOHLCDataset() {
|
||||
// Create a DefaultOHLCDataset
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
|
||||
// Test extraction from DefaultOHLCDataset
|
||||
String result = extractor.extractDataText(dataset, 0, 0);
|
||||
|
||||
assertNotNull(result, "Extracted text should not be null");
|
||||
assertFalse(result.isEmpty(), "Extracted text should not be empty");
|
||||
assertTrue(result.contains("O:"), "Extracted text should contain Open price");
|
||||
assertTrue(result.contains("H:"), "Extracted text should contain High price");
|
||||
assertTrue(result.contains("L:"), "Extracted text should contain Low price");
|
||||
assertTrue(result.contains("C:"), "Extracted text should contain Close price");
|
||||
assertTrue(result.contains("V:"), "Extracted text should contain Volume");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextFromXYSeriesCollection() {
|
||||
// Create an XYSeriesCollection
|
||||
XYSeries series = new XYSeries("Test Series");
|
||||
long baseTime = System.currentTimeMillis();
|
||||
series.add(baseTime, 100.0);
|
||||
series.add(baseTime + 60000, 101.5);
|
||||
|
||||
XYSeriesCollection dataset = new XYSeriesCollection();
|
||||
dataset.addSeries(series);
|
||||
|
||||
// Test extraction from XYSeriesCollection
|
||||
String result = extractor.extractDataText(dataset, 0, 0);
|
||||
|
||||
assertNotNull(result, "Extracted text should not be null");
|
||||
assertFalse(result.isEmpty(), "Extracted text should not be empty");
|
||||
assertTrue(result.contains("100"), "Extracted text should contain value");
|
||||
assertTrue(result.contains("Date:"), "Extracted text should contain date label");
|
||||
assertTrue(result.contains("Value:"), "Extracted text should contain value label");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextReturnsNullForEmptyDataset() {
|
||||
// Create an empty XYSeriesCollection
|
||||
XYSeriesCollection emptyDataset = new XYSeriesCollection();
|
||||
|
||||
// Test with empty dataset (no series)
|
||||
String result = extractor.extractDataText(emptyDataset, 0, 0);
|
||||
assertNull(result, "Should return null for empty dataset");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDataTextReturnsNullForInvalidSeriesIndex() {
|
||||
TimeSeries timeSeries = new TimeSeries("Test Indicator");
|
||||
Date baseDate = new Date();
|
||||
timeSeries.add(new Minute(baseDate), 100.0);
|
||||
|
||||
TimeSeriesCollection dataset = new TimeSeriesCollection();
|
||||
dataset.addSeries(timeSeries);
|
||||
|
||||
// Test with invalid series index
|
||||
String result = extractor.extractDataText(dataset, 999, 0);
|
||||
assertNull(result, "Should return null for invalid series index");
|
||||
}
|
||||
}
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.display;
|
||||
|
||||
import org.jfree.chart.ChartFactory;
|
||||
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.Test;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SwingChartDisplayer}.
|
||||
*/
|
||||
class SwingChartDisplayerTest {
|
||||
|
||||
private SwingChartDisplayer displayer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
displayer = new SwingChartDisplayer();
|
||||
// Clear any existing properties
|
||||
System.clearProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY);
|
||||
System.clearProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY);
|
||||
// Always disable display in tests to prevent actual windows from being created
|
||||
// which would call System.exit(0) when closed
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// Keep display disabled while frames are torn down.
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
Frame[] frames = Frame.getFrames();
|
||||
for (Frame frame : frames) {
|
||||
frame.dispose();
|
||||
}
|
||||
// Clean up properties
|
||||
System.clearProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY);
|
||||
System.clearProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY);
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultDisplayScale() {
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_DISPLAY_SCALE, scale, 1e-9,
|
||||
"Default scale should be " + SwingChartDisplayer.DEFAULT_DISPLAY_SCALE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleFromValidProperty() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.5");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(0.5, scale, 1e-9, "Scale should reflect configured property");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleFromValidPropertyAtMax() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "1.0");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(1.0, scale, 1e-9, "Scale should accept max value of 1.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleFromValidPropertyNearMin() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.11");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(0.11, scale, 1e-9, "Scale should accept values just above 0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleIgnoresValueBelowThreshold() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.05");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_DISPLAY_SCALE, scale, 1e-9, "Values <= 0.1 should be ignored");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleIgnoresValueAboveMax() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "2.0");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_DISPLAY_SCALE, scale, 1e-9, "Values > 1.0 should be ignored");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleHandlesInvalidPropertyValue() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "invalid");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_DISPLAY_SCALE, scale, 1e-9,
|
||||
"Invalid property value should fall back to default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayScaleHandlesEmptyPropertyValue() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "");
|
||||
double scale = displayer.resolveDisplayScale();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_DISPLAY_SCALE, scale, 1e-9,
|
||||
"Empty property should fall back to default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineDisplaySizeReturnsDimension() {
|
||||
Dimension size = displayer.determineDisplaySize();
|
||||
assertNotNull(size, "Display size should not be null");
|
||||
assertTrue(size.width > 0, "Width should be positive");
|
||||
assertTrue(size.height > 0, "Height should be positive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineDisplaySizeMeetsMinimumDimensions() {
|
||||
Dimension size = displayer.determineDisplaySize();
|
||||
assertTrue(size.width >= 800, "Width should meet minimum of 800");
|
||||
assertTrue(size.height >= 600, "Height should meet minimum of 600");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineDisplaySizeScalesCorrectly() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.5");
|
||||
Dimension size1 = displayer.determineDisplaySize();
|
||||
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.75");
|
||||
Dimension size2 = displayer.determineDisplaySize();
|
||||
|
||||
assertTrue(size2.width >= size1.width, "Larger scale should produce larger width");
|
||||
assertTrue(size2.height >= size1.height, "Larger scale should produce larger height");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayWithNullChart() {
|
||||
assertThrows(Exception.class, () -> displayer.display(null), "Display should throw exception for null chart");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayCreatesNonFocusableWindowWhenEnabled() {
|
||||
Assume.assumeFalse("Headless environment", GraphicsEnvironment.isHeadless());
|
||||
|
||||
String title = "Focusability Test " + UUID.randomUUID();
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
displayer.display(chart, title);
|
||||
|
||||
JFrame frame = findFrameByTitle(title);
|
||||
assertNotNull(frame, "Expected frame created by public display API");
|
||||
assertFalse(frame.getFocusableWindowState(), "Displayed chart frame should not be focusable");
|
||||
|
||||
// Keep cleanup deterministic in tests and avoid exit-on-close behavior.
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
frame.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that display gracefully handles headless environments.
|
||||
* <p>
|
||||
* <b>Note:</b> This test is intentionally skipped when running in a
|
||||
* non-headless environment (e.g., when a display is available). The test uses
|
||||
* {@code Assume.assumeTrue()} to skip when a display is present, as it
|
||||
* specifically tests behavior in headless environments where GUI operations
|
||||
* should fail gracefully.
|
||||
* <p>
|
||||
* This skip is expected and intentional - the test validates headless
|
||||
* environment handling, which only applies when no display is available.
|
||||
*/
|
||||
@Test
|
||||
void testDisplayHandlesHeadlessEnvironment() {
|
||||
// This test only runs in headless environments
|
||||
Assume.assumeTrue("Test requires headless environment", GraphicsEnvironment.isHeadless());
|
||||
|
||||
// Clear the disable display property so we can test actual headless behavior
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
try {
|
||||
// In headless environment, display should fail gracefully
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// This will throw HeadlessException in headless environment
|
||||
// but we should handle it gracefully
|
||||
assertThrows(Exception.class, () -> displayer.display(chart));
|
||||
} finally {
|
||||
// Restore the property for cleanup
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayRespectsDisableDisplayProperty() {
|
||||
// Set property to disable display
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// Display should return immediately without showing a window
|
||||
displayer.display(chart);
|
||||
|
||||
// If we get here without exception, the property worked
|
||||
assertTrue(true, "Display should be disabled when property is set");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineDisplaySizeWithCustomScale() {
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.8");
|
||||
Dimension size = displayer.determineDisplaySize();
|
||||
assertNotNull(size);
|
||||
// Should use custom scale or fall back to defaults
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineDisplaySizeBoundsHandling() {
|
||||
// Test with various property values to ensure bounds are respected
|
||||
System.setProperty(SwingChartDisplayer.DISPLAY_SCALE_PROPERTY, "0.5");
|
||||
Dimension size = displayer.determineDisplaySize();
|
||||
assertTrue(size.width >= 800 && size.height >= 600, "Dimensions should respect minimum bounds");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveDisplaySizeUsesExplicitPreferredSize() {
|
||||
Dimension requested = new Dimension(1440, 900);
|
||||
|
||||
Dimension resolved = displayer.resolveDisplaySize(requested);
|
||||
|
||||
assertEquals(requested, resolved);
|
||||
assertNotSame(requested, resolved, "Resolved size should be a defensive copy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveDisplaySizeRejectsNonPositivePreferredDimensions() {
|
||||
assertThrows(IllegalArgumentException.class, () -> displayer.resolveDisplaySize(new Dimension(0, 900)));
|
||||
assertThrows(IllegalArgumentException.class, () -> displayer.resolveDisplaySize(new Dimension(1440, -1)));
|
||||
}
|
||||
|
||||
// ========== Mouseover functionality tests ==========
|
||||
|
||||
@Test
|
||||
void testDefaultHoverDelay() {
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_HOVER_DELAY_MS, delay,
|
||||
"Default hover delay should be " + SwingChartDisplayer.DEFAULT_HOVER_DELAY_MS + "ms");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayFromValidProperty() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "500");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(500, delay, "Hover delay should reflect configured property");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayFromZeroProperty() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "0");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(0, delay, "Hover delay should accept zero value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayFromLargeProperty() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "5000");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(5000, delay, "Hover delay should accept large values");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayIgnoresNegativeValue() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "-100");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_HOVER_DELAY_MS, delay,
|
||||
"Negative values should be ignored and fall back to default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayHandlesInvalidPropertyValue() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "invalid");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_HOVER_DELAY_MS, delay,
|
||||
"Invalid property value should fall back to default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHoverDelayHandlesEmptyPropertyValue() {
|
||||
System.setProperty(SwingChartDisplayer.HOVER_DELAY_PROPERTY, "");
|
||||
int delay = displayer.resolveHoverDelay();
|
||||
assertEquals(SwingChartDisplayer.DEFAULT_HOVER_DELAY_MS, delay, "Empty property should fall back to default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChartMouseoverListenerCreation() {
|
||||
javax.swing.JLabel infoLabel = new javax.swing.JLabel(" ");
|
||||
int hoverDelay = 1000;
|
||||
|
||||
SwingChartDisplayer.ChartMouseoverListener listener = new SwingChartDisplayer.ChartMouseoverListener(infoLabel,
|
||||
hoverDelay);
|
||||
assertNotNull(listener, "Listener should be created successfully");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChartMouseoverListenerCreationWithDifferentDelays() {
|
||||
javax.swing.JLabel infoLabel = new javax.swing.JLabel(" ");
|
||||
|
||||
// Test with different delay values
|
||||
SwingChartDisplayer.ChartMouseoverListener listener1 = new SwingChartDisplayer.ChartMouseoverListener(infoLabel,
|
||||
0);
|
||||
assertNotNull(listener1, "Listener should be created with zero delay");
|
||||
|
||||
SwingChartDisplayer.ChartMouseoverListener listener2 = new SwingChartDisplayer.ChartMouseoverListener(infoLabel,
|
||||
5000);
|
||||
assertNotNull(listener2, "Listener should be created with large delay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChartMouseoverListenerHandlesClick() {
|
||||
javax.swing.JLabel infoLabel = new javax.swing.JLabel(" ");
|
||||
SwingChartDisplayer.ChartMouseoverListener listener = new SwingChartDisplayer.ChartMouseoverListener(infoLabel,
|
||||
1000);
|
||||
|
||||
// Verify listener was created
|
||||
assertNotNull(listener, "Listener should be created successfully");
|
||||
// The click handler should not throw - this tests the method signature
|
||||
// Note: We can't easily test with actual ChartMouseEvent without complex
|
||||
// mocking,
|
||||
// but the method exists and is part of the ChartMouseListener interface
|
||||
}
|
||||
|
||||
// ========== Window cascading functionality tests ==========
|
||||
|
||||
@Test
|
||||
void testMultipleDisplaysHandleCascadingGracefully() {
|
||||
// Set property to disable display to avoid actually showing windows
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart1 = ChartFactory.createLineChart("Test 1", "X", "Y", null);
|
||||
JFreeChart chart2 = ChartFactory.createLineChart("Test 2", "X", "Y", null);
|
||||
JFreeChart chart3 = ChartFactory.createLineChart("Test 3", "X", "Y", null);
|
||||
|
||||
// Multiple displays should not throw exceptions
|
||||
// The cascading logic should handle positioning gracefully
|
||||
assertDoesNotThrow(() -> {
|
||||
displayer.display(chart1, "Window 1");
|
||||
displayer.display(chart2, "Window 2");
|
||||
displayer.display(chart3, "Window 3");
|
||||
}, "Multiple displays should handle cascading without exceptions");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayWithWindowTitleHandlesCascading() {
|
||||
// Set property to disable display
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// Display with custom window title should work
|
||||
assertDoesNotThrow(() -> displayer.display(chart, "Custom Window Title"),
|
||||
"Display with window title should handle cascading gracefully");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisplayHandlesCascadingInHeadlessEnvironment() {
|
||||
// This test only runs in headless environments
|
||||
Assume.assumeTrue("Test requires headless environment", GraphicsEnvironment.isHeadless());
|
||||
|
||||
// Clear the disable display property so we can test actual headless behavior
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// In headless environment, cascading logic should fail gracefully
|
||||
// The exception handling in the cascading code should catch any errors
|
||||
assertThrows(Exception.class, () -> displayer.display(chart, "Test Window"),
|
||||
"Display should throw exception in headless environment, but cascading logic should be attempted");
|
||||
} finally {
|
||||
// Restore the property for cleanup
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleDisplaysWithDifferentTitles() {
|
||||
// Set property to disable display
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// Multiple displays with different titles should all work
|
||||
assertDoesNotThrow(() -> {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
displayer.display(chart, "Window " + i);
|
||||
}
|
||||
}, "Multiple displays with different titles should handle cascading correctly");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Window tracking tests ==========
|
||||
|
||||
@Test
|
||||
void testWindowTrackingWhenDisplayed() {
|
||||
// Set property to disable actual display (works in both headless and
|
||||
// non-headless)
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// Display should not throw
|
||||
assertDoesNotThrow(() -> displayer.display(chart, "Test Window"),
|
||||
"Display should track window without throwing");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleWindowsTrackedIndependently() {
|
||||
// Set property to disable actual display (works in both headless and
|
||||
// non-headless)
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// Display multiple windows
|
||||
assertDoesNotThrow(() -> {
|
||||
displayer.display(chart, "Window 1");
|
||||
displayer.display(chart, "Window 2");
|
||||
displayer.display(chart, "Window 3");
|
||||
}, "Multiple windows should be tracked independently");
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWindowTrackingWithDisabledDisplay() {
|
||||
// Set property to disable display
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
JFreeChart chart = ChartFactory.createLineChart("Test", "X", "Y", null);
|
||||
|
||||
// When display is disabled, no windows should be created or tracked
|
||||
assertDoesNotThrow(() -> {
|
||||
displayer.display(chart, "Test Window");
|
||||
displayer.display(chart, "Test Window 2");
|
||||
}, "Display with disabled property should not throw");
|
||||
|
||||
// Since display is disabled, no windows are created, so no exit behavior
|
||||
// This test just verifies the code path doesn't crash
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
|
||||
private JFrame findFrameByTitle(String title) {
|
||||
Frame[] frames = Frame.getFrames();
|
||||
for (Frame frame : frames) {
|
||||
if (frame instanceof JFrame jFrame && title.equals(jFrame.getTitle())) {
|
||||
return jFrame;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.renderer;
|
||||
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.jfree.chart.ChartFactory;
|
||||
import org.jfree.chart.plot.XYPlot;
|
||||
import org.jfree.chart.renderer.xy.CandlestickRenderer;
|
||||
import org.jfree.data.xy.DefaultOHLCDataset;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import ta4jexamples.charting.ChartingTestFixtures;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BaseCandleStickRenderer}.
|
||||
*/
|
||||
public class BaseCandleStickRendererTest {
|
||||
|
||||
@Test
|
||||
public void testConstructor() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
assertNotNull("Renderer should not be null", renderer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintUpCandle() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create a chart with up candle data (close > open)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Test up candle paint
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
assertTrue("Paint should be green for up candle", paint instanceof Color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintDownCandle() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create a chart with down candle data (close < open)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(false);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Test down candle paint
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
assertTrue("Paint should be red for down candle", paint instanceof Color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintWithNullDataset() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create a chart with null dataset
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", null, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Should handle null dataset gracefully
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null even with null dataset", paint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintWithNonOHLCDataset() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create a chart with non-OHLC dataset
|
||||
JFreeChart chart = ChartFactory.createXYLineChart("Test", "Time", "Price", null);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Should handle non-OHLC dataset gracefully
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null even with non-OHLC dataset", paint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintWithNullValues() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create a dataset with null values
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.candleDatasetWithZeros();
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Should handle null values gracefully
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null even with null values", paint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetItemPaintWithInvalidIndices() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Test with invalid indices - should not throw exception
|
||||
try {
|
||||
Paint paint1 = renderer.getItemPaint(-1, 0);
|
||||
Paint paint2 = renderer.getItemPaint(0, -1);
|
||||
Paint paint3 = renderer.getItemPaint(100, 100);
|
||||
// Verify paints are not null
|
||||
assertNotNull("Paint for invalid row should not be null", paint1);
|
||||
assertNotNull("Paint for invalid column should not be null", paint2);
|
||||
assertNotNull("Paint for out of bounds should not be null", paint3);
|
||||
} catch (Exception e) {
|
||||
fail("Should not throw exception with invalid indices: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorConstants() {
|
||||
// Test that the color constants are accessible
|
||||
// Note: These are private in the actual class, so we test them indirectly
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
assertNotNull("Renderer should be created successfully", renderer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInheritance() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Test that it extends CandlestickRenderer
|
||||
assertTrue("Should extend CandlestickRenderer", renderer instanceof CandlestickRenderer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleCalls() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
// Test multiple calls to getItemPaint
|
||||
Paint paint1 = renderer.getItemPaint(0, 0);
|
||||
Paint paint2 = renderer.getItemPaint(0, 0);
|
||||
|
||||
assertNotNull("First call should return non-null paint", paint1);
|
||||
assertNotNull("Second call should return non-null paint", paint2);
|
||||
assertEquals("Multiple calls should return same paint", paint1, paint2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpCandleLogic() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create dataset where close > open (up candle)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
// The paint should be green for up candle
|
||||
assertTrue("Should return a Color object", paint instanceof Color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownCandleLogic() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create dataset where close < open (down candle)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(false);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
// The paint should be red for down candle
|
||||
assertTrue("Should return a Color object", paint instanceof Color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpCandleColorMatchesTradingView() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create dataset with up candle (close > open)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(true);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
assertTrue("Paint should be a Color", paint instanceof Color);
|
||||
|
||||
Color color = (Color) paint;
|
||||
// TradingView's default bullish candle color: #26A69A (RGB: 38, 166, 154)
|
||||
assertEquals("Up candle red component should match TradingView", 38, color.getRed());
|
||||
assertEquals("Up candle green component should match TradingView", 166, color.getGreen());
|
||||
assertEquals("Up candle blue component should match TradingView", 154, color.getBlue());
|
||||
assertEquals("Up candle color should match DEFAULT_UP_COLOR", BaseCandleStickRenderer.DEFAULT_UP_COLOR, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownCandleColorMatchesTradingView() {
|
||||
BaseCandleStickRenderer renderer = new BaseCandleStickRenderer();
|
||||
|
||||
// Create dataset with down candle (close < open)
|
||||
DefaultOHLCDataset dataset = ChartingTestFixtures.singleCandleDataset(false);
|
||||
JFreeChart chart = ChartFactory.createCandlestickChart("Test", "Time", "Price", dataset, true);
|
||||
XYPlot plot = chart.getXYPlot();
|
||||
plot.setRenderer(renderer);
|
||||
|
||||
Paint paint = renderer.getItemPaint(0, 0);
|
||||
assertNotNull("Paint should not be null", paint);
|
||||
assertTrue("Paint should be a Color", paint instanceof Color);
|
||||
|
||||
Color color = (Color) paint;
|
||||
// TradingView's default bearish candle color: #EF5350 (RGB: 239, 83, 80)
|
||||
assertEquals("Down candle red component should match TradingView", 239, color.getRed());
|
||||
assertEquals("Down candle green component should match TradingView", 83, color.getGreen());
|
||||
assertEquals("Down candle blue component should match TradingView", 80, color.getBlue());
|
||||
assertEquals("Down candle color should match DEFAULT_DOWN_COLOR", BaseCandleStickRenderer.DEFAULT_DOWN_COLOR,
|
||||
color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorConstantsAreTradingViewColors() {
|
||||
// Verify the color constants match TradingView's exact colors
|
||||
Color upColor = BaseCandleStickRenderer.DEFAULT_UP_COLOR;
|
||||
Color downColor = BaseCandleStickRenderer.DEFAULT_DOWN_COLOR;
|
||||
|
||||
// TradingView's default bullish candle color: #26A69A
|
||||
assertEquals("DEFAULT_UP_COLOR red should be 38", 38, upColor.getRed());
|
||||
assertEquals("DEFAULT_UP_COLOR green should be 166", 166, upColor.getGreen());
|
||||
assertEquals("DEFAULT_UP_COLOR blue should be 154", 154, upColor.getBlue());
|
||||
|
||||
// TradingView's default bearish candle color: #EF5350
|
||||
assertEquals("DEFAULT_DOWN_COLOR red should be 239", 239, downColor.getRed());
|
||||
assertEquals("DEFAULT_DOWN_COLOR green should be 83", 83, downColor.getGreen());
|
||||
assertEquals("DEFAULT_DOWN_COLOR blue should be 80", 80, downColor.getBlue());
|
||||
}
|
||||
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.charting.storage;
|
||||
|
||||
import org.jfree.chart.ChartFactory;
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ta4jexamples.charting.ChartingTestFixtures;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link FileSystemChartStorage}.
|
||||
*/
|
||||
class FileSystemChartStorageTest {
|
||||
|
||||
private FileSystemChartStorage storage;
|
||||
private BarSeries barSeries;
|
||||
private JFreeChart chart;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
storage = new FileSystemChartStorage(tempDir);
|
||||
barSeries = ChartingTestFixtures.standardDailySeries();
|
||||
chart = ChartFactory.createCandlestickChart("Test Chart", "Date", "Price",
|
||||
ChartingTestFixtures.seriesToDataset(barSeries), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithNullPath() {
|
||||
assertThrows(NullPointerException.class, () -> new FileSystemChartStorage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithValidInput() {
|
||||
Optional<Path> result = storage.save(chart, barSeries, "Test Strategy", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent(), "Save should return a path");
|
||||
assertTrue(Files.exists(result.get()), "Chart file should exist");
|
||||
assertTrue(Files.isRegularFile(result.get()), "Saved path should be a file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveCreatesDirectories() {
|
||||
Optional<Path> result = storage.save(chart, barSeries, "Test Strategy", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
Path parent = result.get().getParent();
|
||||
assertTrue(Files.exists(parent), "Parent directories should be created");
|
||||
assertTrue(Files.isDirectory(parent), "Parent should be a directory");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithNullChart() {
|
||||
assertThrows(NullPointerException.class, () -> storage.save(null, barSeries, "Test Strategy", 800, 600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithNullSeries() {
|
||||
assertThrows(NullPointerException.class, () -> storage.save(chart, null, "Test Strategy", 800, 600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathSanitization() {
|
||||
BarSeries seriesWithSpecialChars = ChartingTestFixtures.seriesWithSpecialChars();
|
||||
|
||||
Optional<Path> result = storage.save(chart, seriesWithSpecialChars, "Test:Strategy/Path", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
// Note: path may contain file system separators, but not special chars in
|
||||
// component names
|
||||
// The path components themselves should be sanitized
|
||||
String fileName = result.get().getFileName().toString();
|
||||
assertFalse(fileName.contains(":"), "File name should not contain ':'");
|
||||
assertFalse(fileName.contains("?"), "File name should not contain '?'");
|
||||
assertFalse(fileName.contains("*"), "File name should not contain '*'");
|
||||
assertFalse(fileName.contains("<") || fileName.contains(">"), "File name should not contain angle brackets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathSanitizationWithNullSeriesName() {
|
||||
BarSeries seriesWithNullName = new BaseBarSeriesBuilder().build();
|
||||
|
||||
String chartTitle = "Test Strategy";
|
||||
Optional<Path> result = storage.save(chart, seriesWithNullName, chartTitle, 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertTrue(result.get().toString().endsWith(chartTitle.replaceAll(" ", "_") + ".jpg"),
|
||||
"Should use chart title for null series name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathSanitizationWithEmptySeriesName() {
|
||||
BarSeries seriesWithEmptyName = new BaseBarSeriesBuilder().withName("").build();
|
||||
|
||||
String chartTitle = "Test Strategy";
|
||||
Optional<Path> result = storage.save(chart, seriesWithEmptyName, chartTitle, 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertTrue(result.get().toString().endsWith(chartTitle.replaceAll(" ", "_") + ".jpg"),
|
||||
"Should use chart title for empty series name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathSanitizationWhitespaceHandling() {
|
||||
BarSeries seriesWithWhitespace = new BaseBarSeriesBuilder().withName("Series Name With Spaces").build();
|
||||
|
||||
Optional<Path> result = storage.save(chart, seriesWithWhitespace, "Strategy Name", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
String pathString = result.get().toString();
|
||||
// Whitespace should be replaced with underscores
|
||||
assertFalse(pathString.contains(" "), "Multiple consecutive spaces should be collapsed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathSanitizationRemovesLeadingAndTrailingDots() {
|
||||
BarSeries seriesWithDots = new BaseBarSeriesBuilder().withName("...Series...").build();
|
||||
|
||||
Optional<Path> result = storage.save(chart, seriesWithDots, "...Strategy...", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
String pathString = result.get().toString();
|
||||
assertFalse(pathString.contains("..."), "Leading and trailing dots should be removed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithDifferentChartSizes() {
|
||||
Optional<Path> result1 = storage.save(chart, barSeries, "Test Strategy", 1024, 768);
|
||||
assertTrue(result1.isPresent(), "Should save with 1024x768");
|
||||
|
||||
Optional<Path> result2 = storage.save(chart, barSeries, "Test Strategy 2", 1920, 1080);
|
||||
assertTrue(result2.isPresent(), "Should save with 1920x1080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithNullChartTitleUsesSeriesName() {
|
||||
Optional<Path> result = storage.save(chart, barSeries, null, 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveWithLongChartTitle() {
|
||||
String longTitle = "Test Strategy with a very long name that might cause issues "
|
||||
+ "with path length limits on some file systems";
|
||||
Optional<Path> result = storage.save(chart, barSeries, longTitle, 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveIncludesSeriesPeriodDescription() {
|
||||
Optional<Path> result = storage.save(chart, barSeries, "Test Strategy", 800, 600);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
// Path should have multiple levels (series/period/title)
|
||||
Path parentDir = result.get().getParent();
|
||||
assertNotNull(parentDir, "Should have parent directory");
|
||||
}
|
||||
|
||||
}
|
||||
+1481
File diff suppressed because it is too large
Load Diff
+216
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.datasources;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.Appender;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.WriterAppender;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.hamcrest.core.IsNull.notNullValue;
|
||||
import static org.junit.Assume.assumeThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link BitStampCsvTradesFileBarSeriesDataSource} class.
|
||||
*/
|
||||
public class BitStampCSVTradesBarSeriesDataSourceTest {
|
||||
|
||||
private StringWriter logOutput;
|
||||
private Appender appender;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
logOutput = new StringWriter();
|
||||
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = context.getConfiguration();
|
||||
PatternLayout layout = PatternLayout.newBuilder().withPattern("%level %msg%n").build();
|
||||
appender = WriterAppender.newBuilder().setTarget(logOutput).setLayout(layout).setName("TestAppender").build();
|
||||
appender.start();
|
||||
config.addAppender(appender);
|
||||
org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) LogManager
|
||||
.getLogger(BitStampCsvTradesFileBarSeriesDataSource.class);
|
||||
logger.addAppender(appender);
|
||||
logger.setLevel(org.apache.logging.log4j.Level.WARN);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (appender != null) {
|
||||
org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) LogManager
|
||||
.getLogger(BitStampCsvTradesFileBarSeriesDataSource.class);
|
||||
logger.removeAppender(appender);
|
||||
appender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMain() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource.main(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithStandardNamingPattern() {
|
||||
// Test loading Bitstamp BTC data using domain-driven interface with standard
|
||||
// naming pattern
|
||||
// Pattern: Bitstamp-{ticker}-{interval}-{startDate}_{endDate}.csv
|
||||
String expectedFile = "Bitstamp-BTC-USD-PT5M-20131125_20131201.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("BTC-USD", Duration.ofMinutes(5), start, end);
|
||||
|
||||
assertNotNull(series, "Should load series using standard naming pattern with Bitstamp prefix");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals(expectedFile, series.getName(), "Series name should match the filename with Bitstamp prefix");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithDirectFilename() {
|
||||
// Test loading by direct filename (backward compatibility)
|
||||
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource
|
||||
.loadBitstampSeries("Bitstamp-BTC-USD-PT5M-20131125_20131201.csv");
|
||||
|
||||
assertNotNull(series, "Should load series from direct filename with Bitstamp prefix");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithDefaultFile() {
|
||||
// Test loading default Bitstamp file
|
||||
BarSeries series = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
|
||||
|
||||
assertNotNull(series, "Should load default Bitstamp series");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNonExistentTicker() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("NONEXISTENT-USD", Duration.ofMinutes(5), start, end);
|
||||
|
||||
assertNull(series, "Should return null for non-existent ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidDateRange() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-12-01T23:59:59Z");
|
||||
Instant end = Instant.parse("2013-11-25T00:00:00Z"); // End before start
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("BTC-USD", Duration.ofMinutes(5), start, end);
|
||||
}, "Should throw IllegalArgumentException when end date is before start date");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNullTicker() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries(null, Duration.ofMinutes(5), start, end);
|
||||
}, "Should throw IllegalArgumentException for null ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithEmptyTicker() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("", Duration.ofMinutes(5), start, end);
|
||||
}, "Should throw IllegalArgumentException for empty ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidInterval() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("BTC-USD", Duration.ZERO, start, end);
|
||||
}, "Should throw IllegalArgumentException for zero interval");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("BTC-USD", Duration.ofSeconds(-1), start, end);
|
||||
}, "Should throw IllegalArgumentException for negative interval");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceName() {
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
assertEquals("Bitstamp", dataSource.getSourceName(), "Should return 'Bitstamp' as source name");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceNameUsedInFileSearchPattern() {
|
||||
// Verify that getSourceName() is used in file search patterns
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
String sourceName = dataSource.getSourceName();
|
||||
assertFalse(sourceName.isEmpty(), "Source name should not be empty");
|
||||
assertEquals("Bitstamp", sourceName, "Source name should be 'Bitstamp'");
|
||||
|
||||
// Test that file search uses the source name prefix
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
String expectedFile = "Bitstamp-BTC-USD-PT5M-20131125_20131201.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
BarSeries series = dataSource.loadSeries("BTC-USD", Duration.ofMinutes(5), start, end);
|
||||
assertNotNull(series, "Should find file using source name prefix");
|
||||
assertTrue(expectedFile.startsWith(sourceName + "-"), "Expected file should start with source name prefix");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithMismatchedIntervalReturnsNull() {
|
||||
// Test that when requesting a different interval than what's in the file,
|
||||
// null is returned (file search won't find files with mismatched intervals).
|
||||
//
|
||||
// Note: The fix in filterAndAggregateSeries ensures that if a file is ever
|
||||
// found but has a mismatched interval, a warning will be logged instead of
|
||||
// failing silently. However, the current file search logic prevents this
|
||||
// scenario from occurring through the public API, so we test the expected
|
||||
// behavior (null return) rather than the warning.
|
||||
String expectedFile = "Bitstamp-BTC-USD-PT5M-20131125_20131201.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
BitStampCsvTradesFileBarSeriesDataSource dataSource = new BitStampCsvTradesFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-11-25T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-01T23:59:59Z");
|
||||
|
||||
// First verify the file can be loaded with matching interval
|
||||
BarSeries seriesWithMatchingInterval = dataSource.loadSeries("BTC-USD", Duration.ofMinutes(5), start, end);
|
||||
assertNotNull(seriesWithMatchingInterval, "File should be loadable with matching 5-minute interval");
|
||||
|
||||
// Request 1-hour bars - file search won't find the 5-minute file, so returns
|
||||
// null
|
||||
BarSeries series = dataSource.loadSeries("BTC-USD", Duration.ofHours(1), start, end);
|
||||
assertNull(series, "Should return null when no file matches the requested interval");
|
||||
}
|
||||
}
|
||||
+1435
File diff suppressed because it is too large
Load Diff
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.datasources;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.hamcrest.core.IsNull.notNullValue;
|
||||
import static org.junit.Assume.assumeThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link CsvFileBarSeriesDataSource} class.
|
||||
*/
|
||||
public class CsvBarSeriesDataSourceTest {
|
||||
|
||||
@Test
|
||||
public void testMain() {
|
||||
CsvFileBarSeriesDataSource.main(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithStandardNamingPattern() {
|
||||
// Test loading AAPL data using domain-driven interface with standard naming
|
||||
// pattern
|
||||
// Pattern: {ticker}-{interval}-{startDate}_{endDate}.csv
|
||||
String expectedFile = "AAPL-PT1D-20130102_20131231.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("AAPL", Duration.ofDays(1), start, end);
|
||||
|
||||
assertNotNull(series, "Should load series using standard naming pattern");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals(expectedFile, series.getName(), "Series name should match the filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithDirectFilename() {
|
||||
// Test loading by direct filename (backward compatibility)
|
||||
BarSeries series = CsvFileBarSeriesDataSource.loadCsvSeries("AAPL-PT1D-20130102_20131231.csv");
|
||||
|
||||
assertNotNull(series, "Should load series from direct filename");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNonExistentTicker() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("NONEXISTENT", Duration.ofDays(1), start, end);
|
||||
|
||||
assertNull(series, "Should return null for non-existent ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidDateRange() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-12-31T23:59:59Z");
|
||||
Instant end = Instant.parse("2013-01-02T00:00:00Z"); // End before start
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("AAPL", Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException when end date is before start date");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNullTicker() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries(null, Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException for null ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithEmptyTicker() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("", Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException for empty ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidInterval() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("AAPL", Duration.ZERO, start, end);
|
||||
}, "Should throw IllegalArgumentException for zero interval");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("AAPL", Duration.ofSeconds(-1), start, end);
|
||||
}, "Should throw IllegalArgumentException for negative interval");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceName() {
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
assertEquals("", dataSource.getSourceName(), "Should return empty string as CSV files don't use source prefix");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceNameUsedInFileSearchPattern() {
|
||||
// Verify that getSourceName() returns empty string and files don't use prefix
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
String sourceName = dataSource.getSourceName();
|
||||
assertTrue(sourceName.isEmpty(), "Source name should be empty for generic CSV files");
|
||||
|
||||
// Test that file search works without source name prefix
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
String expectedFile = "AAPL-PT1D-20130102_20131231.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
BarSeries series = dataSource.loadSeries("AAPL", Duration.ofDays(1), start, end);
|
||||
assertNotNull(series, "Should find file without source name prefix");
|
||||
// Verify file doesn't start with a source prefix (like "Bitstamp-" or
|
||||
// "YahooFinance-")
|
||||
assertFalse(expectedFile.startsWith("Bitstamp-"), "Expected file should not start with Bitstamp prefix");
|
||||
assertFalse(expectedFile.startsWith("YahooFinance-"),
|
||||
"Expected file should not start with YahooFinance prefix");
|
||||
assertTrue(expectedFile.startsWith("AAPL-"), "Expected file should start with ticker directly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithBroaderPatternWildcardInterval() {
|
||||
// Test that broader pattern search works when exact interval doesn't match
|
||||
// This exercises the wildcard pattern: AAPL-*-20130102_*.csv
|
||||
// The file exists as AAPL-PT1D-20130102_20131231.csv
|
||||
// Request with PT5M interval (which doesn't match) but same dates
|
||||
// Should find the file via broader pattern by trying PT1D for the interval
|
||||
// wildcard
|
||||
String expectedFile = "AAPL-PT1D-20130102_20131231.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
// Request with PT5M interval (file has PT1D) - exact pattern won't match
|
||||
// Broader pattern AAPL-*-20130102_*.csv should find it
|
||||
BarSeries series = dataSource.loadSeries("AAPL", Duration.ofMinutes(5), start, end);
|
||||
|
||||
assertNotNull(series, "Should find file via broader pattern when interval doesn't match exactly");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals(expectedFile, series.getName(), "Series name should match the found filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithBroaderPatternWildcardEndDate() {
|
||||
// Test that broader pattern search correctly handles multiple wildcards
|
||||
// Pattern: AAPL-*-20130102_*.csv
|
||||
// First wildcard should be replaced with interval (PT1D), second with end date
|
||||
// This verifies the fix where both wildcards were being replaced with the same
|
||||
// value
|
||||
String expectedFile = "AAPL-PT1D-20130102_20131231.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
// Use a slightly different end date that won't match exact pattern
|
||||
// but the broader pattern should still find the file
|
||||
Instant end = Instant.parse("2013-12-31T12:00:00Z"); // Different time, same date
|
||||
|
||||
// Request with PT1H interval (file has PT1D) - exact pattern won't match
|
||||
// Broader pattern should find it by trying PT1D for first wildcard and
|
||||
// 20131231 for second wildcard (from end date parameter)
|
||||
BarSeries series = dataSource.loadSeries("AAPL", Duration.ofHours(1), start, end);
|
||||
|
||||
assertNotNull(series,
|
||||
"Should find file via broader pattern when exact pattern doesn't match, verifying wildcard fix");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals(expectedFile, series.getName(), "Series name should match the found filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithBroaderPatternDateOnlyFormat() {
|
||||
// Test that broader pattern search works with date-only format
|
||||
// Pattern: AAPL-*-20130102_*.csv (date-only version)
|
||||
// This exercises the date-only broader pattern fallback
|
||||
String expectedFile = "AAPL-PT1D-20130102_20131231.csv";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
CsvFileBarSeriesDataSource dataSource = new CsvFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2013-01-02T00:00:00Z");
|
||||
Instant end = Instant.parse("2013-12-31T23:59:59Z");
|
||||
|
||||
// Request with PT1H interval (file has PT1D) - exact pattern won't match
|
||||
// The date-only broader pattern AAPL-*-20130102_*.csv should find it
|
||||
// by trying PT1D for first wildcard and 20131231 for second wildcard
|
||||
BarSeries series = dataSource.loadSeries("AAPL", Duration.ofHours(1), start, end);
|
||||
|
||||
assertNotNull(series, "Should find file via date-only broader pattern when exact pattern doesn't match");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals(expectedFile, series.getName(), "Series name should match the found filename");
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.datasources;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.hamcrest.core.IsNull.notNullValue;
|
||||
import static org.junit.Assume.assumeThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link JsonFileBarSeriesDataSource} class.
|
||||
* <p>
|
||||
* This test class verifies the behavior of the
|
||||
* {@code JsonFileBarSeriesDataSource} when loading bar series data from various
|
||||
* JSON input streams, including valid Coinbase and Binance formatted data, as
|
||||
* well as edge cases such as a null input stream.
|
||||
* </p>
|
||||
*/
|
||||
public class JsonBarSeriesDataSourceTest {
|
||||
|
||||
@Test
|
||||
public void testLoadCoinbaseInputStream() {
|
||||
String jsonFile = "Coinbase-ETH-USD-PT1D-20241105_20251020.json";
|
||||
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(jsonFile);
|
||||
assumeThat("File " + jsonFile + " does not exist", inputStream, is(notNullValue()));
|
||||
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(inputStream);
|
||||
|
||||
assertNotNull(series, "BarSeries should be loaded successfully with deserializer");
|
||||
assertTrue(series.getBarCount() > 0, "BarSeries should contain bars");
|
||||
assertEquals("CoinbaseData", series.getName(), "Series name should be set correctly");
|
||||
|
||||
// Verify first bar data
|
||||
var firstBar = series.getBar(0);
|
||||
assertNotNull(firstBar, "First bar should not be null");
|
||||
assertTrue(firstBar.getClosePrice().doubleValue() > 0, "Close price should be positive");
|
||||
assertTrue(firstBar.getVolume().doubleValue() > 0, "Volume should be positive");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadBinanceInputStream() {
|
||||
String jsonFile = "Binance-ETH-USD-PT5M-20230313_20230315.json";
|
||||
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(jsonFile);
|
||||
assumeThat("File " + jsonFile + " does not exist", inputStream, is(notNullValue()));
|
||||
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(inputStream);
|
||||
|
||||
assertNotNull(series, "BarSeries should be loaded successfully");
|
||||
assertTrue(series.getBarCount() > 0, "BarSeries should contain bars");
|
||||
|
||||
// Verify first bar data
|
||||
var firstBar = series.getBar(0);
|
||||
assertNotNull(firstBar, "First bar should not be null");
|
||||
assertTrue(firstBar.getClosePrice().doubleValue() > 0, "Close price should be positive");
|
||||
assertTrue(firstBar.getVolume().doubleValue() > 0, "Volume should be positive");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadNullInputStream() {
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries((InputStream) null);
|
||||
assertNull(series, "Should return null for null input stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesFromValidFile() {
|
||||
String jsonFile = "Coinbase-ETH-USD-PT1D-20241105_20251020.json";
|
||||
String resourcePath = Objects.requireNonNull(getClass().getClassLoader().getResource(jsonFile)).getPath();
|
||||
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(resourcePath);
|
||||
|
||||
assertNotNull(series, "BarSeries should be loaded successfully from file");
|
||||
assertTrue(series.getBarCount() > 0, "BarSeries should contain bars");
|
||||
assertEquals("CoinbaseData", series.getName(), "Series name should be set correctly");
|
||||
|
||||
// Verify first bar data
|
||||
var firstBar = series.getBar(0);
|
||||
assertNotNull(firstBar, "First bar should not be null");
|
||||
assertTrue(firstBar.getClosePrice().doubleValue() > 0, "Close price should be positive");
|
||||
assertTrue(firstBar.getVolume().doubleValue() > 0, "Volume should be positive");
|
||||
|
||||
// Verify last bar data
|
||||
var lastBar = series.getBar(series.getBarCount() - 1);
|
||||
assertNotNull(lastBar, "Last bar should not be null");
|
||||
assertTrue(lastBar.getClosePrice().doubleValue() > 0, "Last bar close price should be positive");
|
||||
assertTrue(lastBar.getVolume().doubleValue() > 0, "Last bar volume should be positive");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesFromNonExistentFile() {
|
||||
String nonExistentPath = "non-existent-file.json";
|
||||
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(nonExistentPath);
|
||||
|
||||
assertNull(series, "Should return null for non-existent file");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesFromNullFilename() {
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries((String) null);
|
||||
|
||||
assertNull(series, "Should return null for null filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesFromEmptyFilename() {
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries("");
|
||||
|
||||
assertNull(series, "Should return null for empty filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesFromInvalidJsonFile() {
|
||||
// Create a temporary file with invalid JSON content in temp/ directory
|
||||
String invalidJsonContent = "invalid json content";
|
||||
Path tempDir = Paths.get("temp");
|
||||
Path tempFile = null;
|
||||
|
||||
try {
|
||||
// Ensure temp directory exists
|
||||
Files.createDirectories(tempDir);
|
||||
|
||||
// Create temp file in temp/ directory
|
||||
tempFile = tempDir.resolve("invalid-" + System.currentTimeMillis() + ".json");
|
||||
Files.write(tempFile, invalidJsonContent.getBytes());
|
||||
|
||||
BarSeries series = JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(tempFile.toString());
|
||||
|
||||
assertNull(series, "Should return null for invalid JSON file");
|
||||
} catch (Exception e) {
|
||||
fail("Unexpected exception during test setup: " + e.getMessage());
|
||||
} finally {
|
||||
// Clean up temporary file
|
||||
if (tempFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (Exception e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithStandardNamingPatternCoinbase() {
|
||||
// Test loading Coinbase data using domain-driven interface with standard naming
|
||||
// pattern
|
||||
// Pattern: {Exchange}-{ticker}-{interval}-{startDate}_{endDate}.json
|
||||
String expectedFile = "Coinbase-ETH-USD-PT1D-20241105_20251020.json";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-11-05T00:00:00Z");
|
||||
Instant end = Instant.parse("2025-10-20T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("ETH-USD", Duration.ofDays(1), start, end);
|
||||
|
||||
assertNotNull(series, "Should load series using standard naming pattern with Coinbase prefix");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
assertEquals("CoinbaseData", series.getName(), "Series name should be set correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithStandardNamingPatternBinance() {
|
||||
// Test loading Binance data using domain-driven interface with standard naming
|
||||
// pattern
|
||||
String expectedFile = "Binance-ETH-USD-PT5M-20230313_20230315.json";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2023-03-13T00:00:00Z");
|
||||
Instant end = Instant.parse("2023-03-15T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("ETH-USD", Duration.ofMinutes(5), start, end);
|
||||
|
||||
assertNotNull(series, "Should load series using standard naming pattern with Binance prefix");
|
||||
assertTrue(series.getBarCount() > 0, "Series should contain bars");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNonExistentTicker() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
Instant end = Instant.parse("2024-12-31T23:59:59Z");
|
||||
|
||||
BarSeries series = dataSource.loadSeries("NONEXISTENT-USD", Duration.ofDays(1), start, end);
|
||||
|
||||
assertNull(series, "Should return null for non-existent ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidDateRange() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-12-31T23:59:59Z");
|
||||
Instant end = Instant.parse("2024-01-01T00:00:00Z"); // End before start
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("ETH-USD", Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException when end date is before start date");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNullTicker() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
Instant end = Instant.parse("2024-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries(null, Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException for null ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithEmptyTicker() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
Instant end = Instant.parse("2024-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("", Duration.ofDays(1), start, end);
|
||||
}, "Should throw IllegalArgumentException for empty ticker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithInvalidInterval() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
Instant end = Instant.parse("2024-12-31T23:59:59Z");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("ETH-USD", Duration.ZERO, start, end);
|
||||
}, "Should throw IllegalArgumentException for zero interval");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("ETH-USD", Duration.ofSeconds(-1), start, end);
|
||||
}, "Should throw IllegalArgumentException for negative interval");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSeriesWithNullDates() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("ETH-USD", Duration.ofDays(1), null, Instant.now());
|
||||
}, "Should throw IllegalArgumentException for null start date");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dataSource.loadSeries("ETH-USD", Duration.ofDays(1), Instant.now(), null);
|
||||
}, "Should throw IllegalArgumentException for null end date");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceName() {
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
assertEquals("", dataSource.getSourceName(),
|
||||
"Should return empty string as JSON files use exchange-specific prefixes (Coinbase-, Binance-)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceNameUsedInFileSearchPattern() {
|
||||
// Verify that getSourceName() returns empty string and files use exchange
|
||||
// prefixes
|
||||
JsonFileBarSeriesDataSource dataSource = new JsonFileBarSeriesDataSource();
|
||||
String sourceName = dataSource.getSourceName();
|
||||
assertTrue(sourceName.isEmpty(),
|
||||
"Source name should be empty for JSON files (exchange-specific prefixes used)");
|
||||
|
||||
// Test that file search works with exchange prefixes (not source name prefix)
|
||||
Instant start = Instant.parse("2024-11-05T00:00:00Z");
|
||||
Instant end = Instant.parse("2025-10-20T23:59:59Z");
|
||||
String expectedFile = "Coinbase-ETH-USD-PT1D-20241105_20251020.json";
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(expectedFile);
|
||||
assumeThat("File " + expectedFile + " does not exist", resourceStream, is(notNullValue()));
|
||||
|
||||
BarSeries series = dataSource.loadSeries("ETH-USD", Duration.ofDays(1), start, end);
|
||||
assertNotNull(series, "Should find file using exchange prefix (not source name prefix)");
|
||||
assertTrue(expectedFile.startsWith("Coinbase-") || expectedFile.startsWith("Binance-"),
|
||||
"Expected file should use exchange prefix, not source name prefix");
|
||||
}
|
||||
}
|
||||
+1408
File diff suppressed because it is too large
Load Diff
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.datasources.json;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class AdaptiveBarSeriesTypeAdapterTest {
|
||||
|
||||
private final Gson gson = new GsonBuilder().registerTypeAdapter(BarSeries.class, new AdaptiveBarSeriesTypeAdapter())
|
||||
.create();
|
||||
|
||||
@Test
|
||||
void coinbaseCandlesUseActualInterval() {
|
||||
String coinbaseJson = """
|
||||
{
|
||||
"candles": [
|
||||
{"start":"1700000600","open":"2.0","high":"2.5","low":"1.8","close":"2.1","volume":"300"},
|
||||
{"start":"1700000000","open":"1.0","high":"1.5","low":"0.9","close":"1.4","volume":"100"},
|
||||
{"start":"1700000300","open":"1.4","high":"1.8","low":"1.2","close":"1.7","volume":"200"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
BarSeries series = gson.fromJson(coinbaseJson, BarSeries.class);
|
||||
|
||||
assertNotNull(series, "Series should be created");
|
||||
assertEquals(3, series.getBarCount(), "Three bars expected");
|
||||
|
||||
Duration expectedDuration = Duration.ofMinutes(5);
|
||||
long[] expectedStartEpochSeconds = { 1700000000L, 1700000300L, 1700000600L };
|
||||
|
||||
for (int i = 0; i < series.getBarCount(); i++) {
|
||||
var bar = series.getBar(i);
|
||||
Instant expectedStart = Instant.ofEpochSecond(expectedStartEpochSeconds[i]);
|
||||
Instant expectedEnd = expectedStart.plus(expectedDuration);
|
||||
|
||||
assertEquals(expectedDuration, bar.getTimePeriod(), "Bar " + i + " should preserve the 5-minute interval");
|
||||
assertEquals(expectedStart, bar.getBeginTime(), "Bar " + i + " should begin at the candle start timestamp");
|
||||
assertEquals(expectedEnd, bar.getEndTime(), "Bar " + i + " should end at start plus the detected interval");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void binanceCandlesUseActualInterval() {
|
||||
String binanceJson = """
|
||||
{
|
||||
"name": "ETH/USD_PT5M@BinanceUS",
|
||||
"ohlc": [
|
||||
{"endTime":1700000600000,"openPrice":102.0,"highPrice":105.0,"lowPrice":101.0,"closePrice":104.0,"volume":20.0,"amount":0.1},
|
||||
{"endTime":1700000000000,"openPrice":100.0,"highPrice":103.0,"lowPrice":99.0,"closePrice":102.0,"volume":10.0,"amount":0.05},
|
||||
{"endTime":1700000300000,"openPrice":102.0,"highPrice":104.0,"lowPrice":101.0,"closePrice":103.0,"volume":15.0,"amount":0.08}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
BarSeries series = gson.fromJson(binanceJson, BarSeries.class);
|
||||
|
||||
assertNotNull(series, "Series should be created");
|
||||
assertEquals("ETH/USD_PT5M@BinanceUS", series.getName(), "Series name should be preserved");
|
||||
assertEquals(3, series.getBarCount(), "Three bars expected");
|
||||
|
||||
Duration expectedDuration = Duration.ofMinutes(5);
|
||||
long[] expectedEndEpochMillis = { 1700000000000L, 1700000300000L, 1700000600000L };
|
||||
|
||||
for (int i = 0; i < series.getBarCount(); i++) {
|
||||
var bar = series.getBar(i);
|
||||
Instant expectedEnd = Instant.ofEpochMilli(expectedEndEpochMillis[i]);
|
||||
Instant expectedStart = expectedEnd.minus(expectedDuration);
|
||||
|
||||
assertEquals(expectedDuration, bar.getTimePeriod(), "Bar " + i + " should preserve the 5-minute interval");
|
||||
assertEquals(expectedStart, bar.getBeginTime(),
|
||||
"Bar " + i + " should begin at end minus the detected interval");
|
||||
assertEquals(expectedEnd, bar.getEndTime(), "Bar " + i + " should end at the candle timestamp");
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.datasources.json;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.Appender;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.WriterAppender;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
class DeprecatedJsonDataSourcesNotifierTest {
|
||||
|
||||
private LoggerContext loggerContext;
|
||||
private LoggerConfig loggerConfig;
|
||||
private Level originalLevel;
|
||||
private Appender appender;
|
||||
private StringWriter logOutput;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
DeprecationNotifier.resetForTests();
|
||||
|
||||
loggerContext = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
loggerConfig = config.getLoggerConfig("org.ta4j.core.utils.DeprecationNotifier");
|
||||
originalLevel = loggerConfig.getLevel();
|
||||
|
||||
logOutput = new StringWriter();
|
||||
PatternLayout layout = PatternLayout.newBuilder().withPattern("%msg%n").build();
|
||||
appender = WriterAppender.newBuilder()
|
||||
.setTarget(logOutput)
|
||||
.setLayout(layout)
|
||||
.setName("DeprecatedJsonDataSourcesNotifierAppender")
|
||||
.build();
|
||||
appender.start();
|
||||
|
||||
loggerConfig.addAppender(appender, Level.WARN, null);
|
||||
loggerConfig.setLevel(Level.WARN);
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (loggerConfig != null && appender != null) {
|
||||
loggerConfig.removeAppender(appender.getName());
|
||||
appender.stop();
|
||||
}
|
||||
if (loggerConfig != null && originalLevel != null) {
|
||||
loggerConfig.setLevel(originalLevel);
|
||||
}
|
||||
if (loggerContext != null) {
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
DeprecationNotifier.resetForTests();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deprecatedJsonHelpersEmitMigrationWarnings() {
|
||||
BarSeries series = buildSeries();
|
||||
|
||||
JsonBarsSerializer.loadSeries((java.io.InputStream) null);
|
||||
GsonBarSeries.from(series);
|
||||
GsonBarData.from(series.getBar(0));
|
||||
|
||||
String logContent = logOutput.toString();
|
||||
|
||||
assertTrue(logContent.contains("ta4jexamples.datasources.json.JsonBarsSerializer is deprecated"));
|
||||
assertTrue(logContent.contains("ta4jexamples.datasources.json.GsonBarSeries is deprecated"));
|
||||
assertTrue(logContent.contains("ta4jexamples.datasources.json.GsonBarData is deprecated"));
|
||||
assertTrue(logContent.contains("Use ta4jexamples.datasources.json.JsonFileBarSeriesDataSource instead."));
|
||||
assertTrue(logContent.contains("scheduled for removal in 0.24.0"));
|
||||
|
||||
assertEquals(1, countOccurrences(logContent, "ta4jexamples.datasources.json.JsonBarsSerializer is deprecated"));
|
||||
assertEquals(1, countOccurrences(logContent, "ta4jexamples.datasources.json.GsonBarSeries is deprecated"));
|
||||
assertEquals(1, countOccurrences(logContent, "ta4jexamples.datasources.json.GsonBarData is deprecated"));
|
||||
}
|
||||
|
||||
private static BarSeries buildSeries() {
|
||||
BaseBarSeriesBuilder builder = new BaseBarSeriesBuilder().withName("deprecated-json-series");
|
||||
BarSeries series = builder.build();
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-01T00:00:00Z"))
|
||||
.openPrice(100)
|
||||
.highPrice(110)
|
||||
.lowPrice(90)
|
||||
.closePrice(105)
|
||||
.volume(1000)
|
||||
.amount(105000)
|
||||
.add();
|
||||
return series;
|
||||
}
|
||||
|
||||
private static int countOccurrences(String text, String pattern) {
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
while ((index = text.indexOf(pattern, index)) >= 0) {
|
||||
count++;
|
||||
index += pattern.length();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.doc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/**
|
||||
* Regression tests for {@link ReadmeContentManager} line-ending behavior.
|
||||
*
|
||||
* <p>
|
||||
* The README updater should preserve the dominant line separator already used
|
||||
* in the target README file, regardless of source snippet line endings.
|
||||
* </p>
|
||||
*/
|
||||
public class ReadmeContentManagerTest {
|
||||
|
||||
private static final String[] SNIPPET_IDS = { "ema-crossover", "rsi-strategy", "strategy-performance",
|
||||
"advanced-strategy", "serialize-indicator", "serialize-rule", "serialize-strategy" };
|
||||
|
||||
@TempDir
|
||||
private Path tempDir;
|
||||
|
||||
@Test
|
||||
public void testUpdateReadmeSnippetsPreservesLfLineEndings() throws IOException {
|
||||
String lineSeparator = "\n";
|
||||
Path sourceFile = tempDir.resolve("ReadmeContentManager.java");
|
||||
Path readmeFile = tempDir.resolve("README.md");
|
||||
|
||||
Files.writeString(sourceFile, buildSourceSnippets(lineSeparator), StandardCharsets.UTF_8);
|
||||
Files.writeString(readmeFile, buildReadmeTemplate(lineSeparator), StandardCharsets.UTF_8);
|
||||
|
||||
boolean updated = ReadmeContentManager.updateReadmeSnippets(readmeFile, sourceFile);
|
||||
assertTrue(updated);
|
||||
|
||||
String updatedContent = Files.readString(readmeFile, StandardCharsets.UTF_8);
|
||||
LineEndingCounts counts = countLineEndings(updatedContent);
|
||||
assertTrue(counts.lf() > 0);
|
||||
assertEquals(0, counts.crlf());
|
||||
assertTrue(updatedContent.contains(snippetBlock("ema-crossover", "ema_crossover", 1, lineSeparator)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateReadmeSnippetsPreservesCrLfLineEndings() throws IOException {
|
||||
String lineSeparator = "\r\n";
|
||||
Path sourceFile = tempDir.resolve("ReadmeContentManager.java");
|
||||
Path readmeFile = tempDir.resolve("README.md");
|
||||
|
||||
Files.writeString(sourceFile, buildSourceSnippets("\n"), StandardCharsets.UTF_8);
|
||||
Files.writeString(readmeFile, buildReadmeTemplate(lineSeparator), StandardCharsets.UTF_8);
|
||||
|
||||
boolean updated = ReadmeContentManager.updateReadmeSnippets(readmeFile, sourceFile);
|
||||
assertTrue(updated);
|
||||
|
||||
String updatedContent = Files.readString(readmeFile, StandardCharsets.UTF_8);
|
||||
LineEndingCounts counts = countLineEndings(updatedContent);
|
||||
assertTrue(counts.crlf() > 0);
|
||||
assertEquals(0, counts.lf());
|
||||
assertTrue(updatedContent.contains(snippetBlock("ema-crossover", "ema_crossover", 1, lineSeparator)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepositoryJavaBaselineDocumentationAndWorkflowsStayAligned() throws IOException {
|
||||
Path repositoryRoot = findRepositoryRoot();
|
||||
String pom = readString(repositoryRoot.resolve("pom.xml"));
|
||||
String readme = readString(repositoryRoot.resolve("README.md"));
|
||||
String contributing = readString(repositoryRoot.resolve(".github").resolve("CONTRIBUTING.md"));
|
||||
Map<String, String> expectedActionPins = Map.ofEntries(
|
||||
Map.entry("actions/setup-java@", "actions/setup-java@v5"),
|
||||
Map.entry("actions/checkout@", "actions/checkout@v6"), Map.entry("actions/cache@", "actions/cache@v5"),
|
||||
Map.entry("actions/upload-artifact@", "actions/upload-artifact@v7"),
|
||||
Map.entry("actions/github-script@", "actions/github-script@v9"),
|
||||
Map.entry("softprops/action-gh-release@", "softprops/action-gh-release@v3"),
|
||||
Map.entry("rhysd/actionlint@", "rhysd/actionlint@v1.7.12"));
|
||||
List<String> forbiddenActionPins = List.of("actions/checkout@v5", "actions/cache@v4",
|
||||
"actions/upload-artifact@v4", "actions/github-script@v8", "softprops/action-gh-release@v2",
|
||||
"rhysd/actionlint@v1.7.9");
|
||||
List<Path> setupJavaWorkflows = new ArrayList<>();
|
||||
|
||||
assertTrue(pom.contains("<maven.compiler.release>25</maven.compiler.release>"));
|
||||
assertTrue(pom.contains("<requireJavaVersion>"));
|
||||
assertTrue(pom.contains("<version>[25,)</version>"));
|
||||
assertTrue(readme.contains("JDK-25%2B"));
|
||||
assertTrue(readme.contains("Java 25+"));
|
||||
assertFalse(readme.contains("./mvnw"));
|
||||
assertFalse(readme.contains("mvnw.cmd"));
|
||||
assertTrue(contributing.contains("Java 25+"));
|
||||
|
||||
try (Stream<Path> workflowPaths = Files.list(repositoryRoot.resolve(".github").resolve("workflows"))) {
|
||||
workflowPaths.filter(path -> path.getFileName().toString().endsWith(".yml")).forEach(path -> {
|
||||
String workflow = readString(path);
|
||||
if (workflow.contains("actions/setup-java")) {
|
||||
setupJavaWorkflows.add(path);
|
||||
assertTrue(workflow.contains("java-version: 25"), path + " should set up Java 25");
|
||||
assertFalse(workflow.contains("java-version: 21"), path + " should not set up Java 21");
|
||||
}
|
||||
expectedActionPins.forEach((actionPrefix, expectedPin) -> {
|
||||
if (workflow.contains(actionPrefix)) {
|
||||
assertTrue(workflow.contains(expectedPin), path + " should use " + expectedPin);
|
||||
}
|
||||
});
|
||||
forbiddenActionPins.forEach((forbiddenPin) -> assertFalse(workflow.contains(forbiddenPin),
|
||||
path + " should not pin " + forbiddenPin));
|
||||
if (path.getFileName().toString().equals("github-release.yml")) {
|
||||
assertTrue(workflow.contains("path: workflow-support"),
|
||||
path + " should stage workflow support files separately from the release tag checkout");
|
||||
assertTrue(workflow.contains("workflow-support/scripts/release/release_helpers.py"), path
|
||||
+ " should validate artifacts with workflow support files, not the checked-out tag tree");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assertFalse(setupJavaWorkflows.isEmpty());
|
||||
}
|
||||
|
||||
private static String buildSourceSnippets(String lineSeparator) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < SNIPPET_IDS.length; i++) {
|
||||
String snippetId = SNIPPET_IDS[i];
|
||||
String variableName = snippetId.replace('-', '_');
|
||||
builder.append("// START_SNIPPET: ").append(snippetId).append(lineSeparator);
|
||||
builder.append(" int ")
|
||||
.append(variableName)
|
||||
.append(" = ")
|
||||
.append(i + 1)
|
||||
.append(";")
|
||||
.append(lineSeparator);
|
||||
builder.append("// END_SNIPPET: ").append(snippetId).append(lineSeparator).append(lineSeparator);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String buildReadmeTemplate(String lineSeparator) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("# README").append(lineSeparator).append(lineSeparator);
|
||||
for (String snippetId : SNIPPET_IDS) {
|
||||
builder.append("<!-- START_SNIPPET: ").append(snippetId).append(" -->").append(lineSeparator);
|
||||
builder.append("```java").append(lineSeparator);
|
||||
builder.append("old").append(lineSeparator);
|
||||
builder.append("```").append(lineSeparator);
|
||||
builder.append("<!-- END_SNIPPET: ")
|
||||
.append(snippetId)
|
||||
.append(" -->")
|
||||
.append(lineSeparator)
|
||||
.append(lineSeparator);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String snippetBlock(String snippetId, String variableName, int value, String lineSeparator) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("<!-- START_SNIPPET: ").append(snippetId).append(" -->").append(lineSeparator);
|
||||
builder.append("```java").append(lineSeparator);
|
||||
builder.append("int ").append(variableName).append(" = ").append(value).append(";").append(lineSeparator);
|
||||
builder.append("```").append(lineSeparator);
|
||||
builder.append("<!-- END_SNIPPET: ").append(snippetId).append(" -->");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static LineEndingCounts countLineEndings(String content) {
|
||||
int crlf = 0;
|
||||
int lf = 0;
|
||||
for (int i = 0; i < content.length(); i++) {
|
||||
if (content.charAt(i) == '\n') {
|
||||
if (i > 0 && content.charAt(i - 1) == '\r') {
|
||||
crlf++;
|
||||
} else {
|
||||
lf++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new LineEndingCounts(crlf, lf);
|
||||
}
|
||||
|
||||
private static Path findRepositoryRoot() throws IOException {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
Path candidate = current;
|
||||
while (candidate != null) {
|
||||
if (Files.exists(candidate.resolve("pom.xml")) && Files.exists(candidate.resolve("README.md"))
|
||||
&& Files.isDirectory(candidate.resolve(".github"))) {
|
||||
return candidate;
|
||||
}
|
||||
candidate = candidate.getParent();
|
||||
}
|
||||
throw new IOException("Unable to locate repository root from " + current);
|
||||
}
|
||||
|
||||
private static String readString(Path path) {
|
||||
try {
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private record LineEndingCounts(int crlf, int lf) {
|
||||
}
|
||||
}
|
||||
+623
@@ -0,0 +1,623 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.doc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Regression coverage for {@link RemovalReadyDeprecationScanner}.
|
||||
*/
|
||||
public class RemovalReadyDeprecationScannerTest {
|
||||
|
||||
@TempDir
|
||||
private Path tempDir;
|
||||
|
||||
@Test
|
||||
public void testMatchingSnapshotDetection() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class LegacyBridge {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public static void bridge() {
|
||||
}
|
||||
}
|
||||
""");
|
||||
writeJava("ta4j-examples/src/main/java/ta4jexamples/legacy/FutureBridge.java", """
|
||||
package ta4jexamples.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.25.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class FutureBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(1, report.get("schemaVersion").getAsInt());
|
||||
assertEquals("ta4j:deprecation-removal", report.get("automationNamespace").getAsString());
|
||||
assertEquals("0.24.0-SNAPSHOT", report.get("snapshotVersion").getAsString());
|
||||
assertEquals(2, report.get("findingCount").getAsInt());
|
||||
assertEquals(1, report.get("issuePlanCount").getAsInt());
|
||||
|
||||
JsonObject plan = report.getAsJsonArray("issuePlans").get(0).getAsJsonObject();
|
||||
assertEquals("grouped-file-removal", plan.get("planKind").getAsString());
|
||||
assertEquals("ta4j-core", plan.get("module").getAsString());
|
||||
assertEquals("ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyBridge.java",
|
||||
plan.get("filePath").getAsString());
|
||||
assertEquals("Remove 0.24.0-ready deprecations in LegacyBridge.java", plan.get("issueTitle").getAsString());
|
||||
assertFalse(plan.get("issueBody").getAsString().contains(":symbol:v1"));
|
||||
JsonArray symbols = plan.getAsJsonArray("symbols");
|
||||
assertEquals(
|
||||
"ta4j:deprecation-removal:symbol:v1|0.24.0|ta4j-core|"
|
||||
+ "ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyBridge.java|class|LegacyBridge",
|
||||
symbols.get(0).getAsJsonObject().get("trackingKey").getAsString());
|
||||
assertEquals("LegacyBridge", symbols.get(0).getAsJsonObject().get("signature").getAsString());
|
||||
assertEquals(
|
||||
"ta4j:deprecation-removal:symbol:v1|0.24.0|ta4j-core|"
|
||||
+ "ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyBridge.java|method|bridge()",
|
||||
symbols.get(1).getAsJsonObject().get("trackingKey").getAsString());
|
||||
assertEquals("bridge()", symbols.get(1).getAsJsonObject().get("signature").getAsString());
|
||||
assertSymbolNames(plan.getAsJsonArray("symbols"), "LegacyBridge", "bridge");
|
||||
assertTrue(Files.readString(tempDir.resolve("report.md")).contains("LegacyBridge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifierVersionDetection() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/NotifierBridge.java",
|
||||
"""
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class NotifierBridge {
|
||||
|
||||
{
|
||||
DeprecationNotifier.warnOnce(NotifierBridge.class, "org.ta4j.core.modern.NotifierBridge", "0.24.0");
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(1, report.get("findingCount").getAsInt());
|
||||
JsonObject plan = report.getAsJsonArray("issuePlans").get(0).getAsJsonObject();
|
||||
assertSymbolNames(plan.getAsJsonArray("symbols"), "NotifierBridge");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMixedVersionsInOneFile() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/MixedRemovalBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
public class MixedRemovalBridge {
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public void removeNow() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.25.0.
|
||||
*/
|
||||
@Deprecated(since = "0.21.0", forRemoval = true)
|
||||
public void removeLater() {
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(1, report.get("findingCount").getAsInt());
|
||||
JsonObject plan = report.getAsJsonArray("issuePlans").get(0).getAsJsonObject();
|
||||
assertSymbolNames(plan.getAsJsonArray("symbols"), "removeNow");
|
||||
assertFalse(Files.readString(tempDir.resolve("report.md")).contains("removeLater"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNextJavadocDoesNotLeakBackwards() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/AdjacentJavadocBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
public class AdjacentJavadocBridge {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public void unscheduled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.21.0", forRemoval = true)
|
||||
public void removeNow() {
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
JsonObject plan = firstPlan(report);
|
||||
assertSymbolNames(plan.getAsJsonArray("symbols"), "removeNow");
|
||||
assertSymbolNames(report.getAsJsonArray("unscheduledSymbols"), "unscheduled");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTypeInheritanceResetsBetweenTypes() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class LegacyBridge {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public void removeNow() {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(since = "0.21.0", forRemoval = true)
|
||||
class UnscheduledBridge {
|
||||
|
||||
@Deprecated(since = "0.21.0", forRemoval = true)
|
||||
void keepAround() {
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(2, report.get("findingCount").getAsInt());
|
||||
JsonObject plan = firstPlan(report);
|
||||
assertSymbolNames(plan.getAsJsonArray("symbols"), "LegacyBridge", "removeNow");
|
||||
assertSymbolNames(report.getAsJsonArray("unscheduledSymbols"), "UnscheduledBridge", "keepAround");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializedFieldIsClassifiedAsField() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/LegacyFieldHolder.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class LegacyFieldHolder {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public static final LegacyFieldHolder LEGACY = createLegacy();
|
||||
|
||||
private static LegacyFieldHolder createLegacy() {
|
||||
return new LegacyFieldHolder();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject plan = firstPlan(runScanner());
|
||||
JsonArray symbols = plan.getAsJsonArray("symbols");
|
||||
JsonObject field = symbols.get(1).getAsJsonObject();
|
||||
assertEquals("LEGACY", field.get("name").getAsString());
|
||||
assertEquals("field", field.get("kind").getAsString());
|
||||
assertFalse(Files.readString(tempDir.resolve("report.md")).contains("createLegacy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTargetVersionIncludesOverdueFindingsWhenRequested() throws IOException {
|
||||
writePom("<version>0.24.1-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/OverdueBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class OverdueBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner("--target-removal-version", "0.24.1", "--include-overdue");
|
||||
assertEquals("0.24.1", report.get("removalVersion").getAsString());
|
||||
assertEquals(1, report.get("overdueFindingCount").getAsInt());
|
||||
assertEquals(1, report.get("findingCount").getAsInt());
|
||||
|
||||
JsonObject symbol = firstPlan(report).getAsJsonArray("symbols").get(0).getAsJsonObject();
|
||||
assertEquals("overdue", symbol.get("status").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTargetVersionIncludesOverdueFindingsAcrossSemanticVersionJumps() throws IOException {
|
||||
writePom("<version>1.2.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/MajorJumpBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.99.9.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class MajorJumpBridge {
|
||||
}
|
||||
""");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/MinorJumpBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 1.1.5.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class MinorJumpBridge {
|
||||
}
|
||||
""");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/CurrentBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 1.2.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class CurrentBridge {
|
||||
}
|
||||
""");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/FutureBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 2.0.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class FutureBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner("--target-removal-version", "1.2.0", "--include-overdue");
|
||||
assertEquals(3, report.get("findingCount").getAsInt());
|
||||
assertEquals(1, report.get("dueFindingCount").getAsInt());
|
||||
assertEquals(2, report.get("overdueFindingCount").getAsInt());
|
||||
assertEquals(1, report.get("futureFindingCount").getAsInt());
|
||||
assertEquals(3, report.get("issuePlanCount").getAsInt());
|
||||
|
||||
List<String> statuses = report.getAsJsonArray("issuePlans")
|
||||
.asList()
|
||||
.stream()
|
||||
.flatMap(plan -> plan.getAsJsonObject().getAsJsonArray("symbols").asList().stream())
|
||||
.map(symbol -> symbol.getAsJsonObject().get("status").getAsString())
|
||||
.toList();
|
||||
assertEquals(List.of("due", "overdue", "overdue"), statuses);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTargetVersionExcludesOverdueFindingsByDefault() throws IOException {
|
||||
writePom("<version>0.24.1-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/OverdueBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class OverdueBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner("--target-removal-version", "0.24.1");
|
||||
assertEquals(1, report.get("overdueFindingCount").getAsInt());
|
||||
assertEquals(0, report.get("findingCount").getAsInt());
|
||||
assertEquals(0, report.get("issuePlanCount").getAsInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailOnDueReturnsNonZeroAfterWritingReport() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/DueBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class DueBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
Path outputJson = tempDir.resolve("report.json");
|
||||
Path outputMarkdown = tempDir.resolve("report.md");
|
||||
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(
|
||||
scannerArgs(outputJson, outputMarkdown, "--target-removal-version", "0.24.0", "--fail-on-due"),
|
||||
new PrintStream(new ByteArrayOutputStream()), new PrintStream(stderr));
|
||||
|
||||
assertEquals(2, exitCode);
|
||||
assertTrue(Files.exists(outputJson));
|
||||
assertTrue(Files.exists(outputMarkdown));
|
||||
assertTrue(stderr.toString(StandardCharsets.UTF_8).contains("deprecation gate failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStdoutSummaryIncludesLifecycleCounts() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/DueBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class DueBridge {
|
||||
}
|
||||
""");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/FutureBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.25.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class FutureBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(
|
||||
scannerArgs(tempDir.resolve("report.json"), tempDir.resolve("report.md"), "--fail-on-due"),
|
||||
new PrintStream(stdout), new PrintStream(new ByteArrayOutputStream()));
|
||||
|
||||
assertEquals(2, exitCode);
|
||||
String summaryText = stdout.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(summaryText.startsWith("{\"schemaVersion\""));
|
||||
JsonObject summary = JsonParser.parseString(summaryText).getAsJsonObject();
|
||||
assertEquals(1, summary.get("schemaVersion").getAsInt());
|
||||
assertEquals("ta4j:deprecation-removal", summary.get("automationNamespace").getAsString());
|
||||
assertEquals(1, summary.get("findingCount").getAsInt());
|
||||
assertEquals(1, summary.get("dueFindingCount").getAsInt());
|
||||
assertEquals(1, summary.get("futureFindingCount").getAsInt());
|
||||
assertEquals(1, summary.get("blockingFindingCount").getAsInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnscheduledForRemovalIsReportedButNotPlanned() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/UnscheduledBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
@Deprecated(
|
||||
since = "0.20.0",
|
||||
forRemoval = true
|
||||
)
|
||||
public class UnscheduledBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(0, report.get("issuePlanCount").getAsInt());
|
||||
assertEquals(1, report.get("unscheduledFindingCount").getAsInt());
|
||||
assertEquals(
|
||||
"ta4j:deprecation-removal:symbol:v1|unscheduled|ta4j-core|"
|
||||
+ "ta4j-core/src/main/java/org/ta4j/core/legacy/UnscheduledBridge.java|class|UnscheduledBridge",
|
||||
report.getAsJsonArray("unscheduledSymbols").get(0).getAsJsonObject().get("trackingKey").getAsString());
|
||||
assertSymbolNames(report.getAsJsonArray("unscheduledSymbols"), "UnscheduledBridge");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultilineAnnotationAndCommentFalsePositives() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/MultilineBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
public class MultilineBridge {
|
||||
// @Deprecated(since = "0.20.0", forRemoval = true)
|
||||
// Scheduled for removal in 0.24.0.
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(
|
||||
since = "0.20.0",
|
||||
forRemoval =
|
||||
true
|
||||
)
|
||||
public void removeNow() {
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(1, report.get("findingCount").getAsInt());
|
||||
assertSymbolNames(firstPlan(report).getAsJsonArray("symbols"), "removeNow");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedDeprecatedTypeResetsInheritedVersions() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/NestedBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class NestedBridge {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public void removeOuterMember() {
|
||||
}
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
static class UnscheduledNestedBridge {
|
||||
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
void keepNestedMember() {
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject report = runScanner();
|
||||
assertEquals(2, report.get("findingCount").getAsInt());
|
||||
assertEquals(2, report.get("unscheduledFindingCount").getAsInt());
|
||||
assertSymbolNames(firstPlan(report).getAsJsonArray("symbols"), "NestedBridge", "removeOuterMember");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIssuePlanIncludesVersionMarkerAndReplacementHint() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/ReplacementBridge.java",
|
||||
"""
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ModernBridge}. Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class ReplacementBridge {
|
||||
{
|
||||
DeprecationNotifier.warnOnce(ReplacementBridge.class, "org.ta4j.core.legacy.ModernBridge", "0.24.0");
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
JsonObject plan = firstPlan(runScanner());
|
||||
assertTrue(plan.get("issueMarker").getAsString().contains("version=0.24.0"));
|
||||
assertTrue(plan.get("issueBody").getAsString().contains("org.ta4j.core.legacy.ModernBridge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorktreePathPrefixDoesNotHideRepoSources() throws IOException {
|
||||
Path repoRoot = tempDir.resolve(".agents/worktrees/scanner-checkout");
|
||||
Files.createDirectories(repoRoot);
|
||||
Files.writeString(repoRoot.resolve("pom.xml"), "<project><version>0.24.0-SNAPSHOT</version></project>",
|
||||
StandardCharsets.UTF_8);
|
||||
Path javaFile = repoRoot.resolve("ta4j-core/src/main/java/org/ta4j/core/legacy/WorktreeBridge.java");
|
||||
Files.createDirectories(javaFile.getParent());
|
||||
Files.writeString(javaFile, """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 0.24.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class WorktreeBridge {
|
||||
}
|
||||
""", StandardCharsets.UTF_8);
|
||||
|
||||
Path outputJson = repoRoot.resolve("report.json");
|
||||
Path outputMarkdown = repoRoot.resolve("report.md");
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(
|
||||
new String[] { "--repo-root", repoRoot.toString(), "--output-json", outputJson.toString(),
|
||||
"--output-md", outputMarkdown.toString() },
|
||||
new PrintStream(new ByteArrayOutputStream()), new PrintStream(new ByteArrayOutputStream()));
|
||||
|
||||
assertEquals(0, exitCode);
|
||||
JsonObject report = JsonParser.parseString(Files.readString(outputJson)).getAsJsonObject();
|
||||
assertEquals(1, report.get("findingCount").getAsInt());
|
||||
assertTrue(Files.readString(outputMarkdown).contains("WorktreeBridge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRejectsNonSnapshotVersion() throws IOException {
|
||||
writePom("<version>0.24.0</version>");
|
||||
|
||||
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(
|
||||
new String[] { "--repo-root", tempDir.toString(), "--output-json",
|
||||
tempDir.resolve("report.json").toString(), "--output-md",
|
||||
tempDir.resolve("report.md").toString() },
|
||||
new PrintStream(new ByteArrayOutputStream()), new PrintStream(stderr));
|
||||
|
||||
assertEquals(1, exitCode);
|
||||
assertTrue(stderr.toString(StandardCharsets.UTF_8).contains("expected a SNAPSHOT version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRejectsOutOfRangeVersionComponentsWithClearError() throws IOException {
|
||||
writePom("<version>0.24.0-SNAPSHOT</version>");
|
||||
writeJava("ta4j-core/src/main/java/org/ta4j/core/legacy/LargeVersionBridge.java", """
|
||||
package org.ta4j.core.legacy;
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in 999999999999.0.0.
|
||||
*/
|
||||
@Deprecated(since = "0.20.0", forRemoval = true)
|
||||
public class LargeVersionBridge {
|
||||
}
|
||||
""");
|
||||
|
||||
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(scannerArgs(tempDir.resolve("report.json"),
|
||||
tempDir.resolve("report.md"), "--target-removal-version", "0.24.0"),
|
||||
new PrintStream(new ByteArrayOutputStream()), new PrintStream(stderr));
|
||||
|
||||
assertEquals(1, exitCode);
|
||||
assertTrue(stderr.toString(StandardCharsets.UTF_8).contains("version components must fit in an integer"));
|
||||
}
|
||||
|
||||
private JsonObject runScanner(String... extraArgs) throws IOException {
|
||||
Path outputJson = tempDir.resolve("report.json");
|
||||
Path outputMarkdown = tempDir.resolve("report.md");
|
||||
int exitCode = RemovalReadyDeprecationScanner.run(scannerArgs(outputJson, outputMarkdown, extraArgs),
|
||||
new PrintStream(new ByteArrayOutputStream()), new PrintStream(new ByteArrayOutputStream()));
|
||||
assertEquals(0, exitCode);
|
||||
return JsonParser.parseString(Files.readString(outputJson, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
}
|
||||
|
||||
private String[] scannerArgs(Path outputJson, Path outputMarkdown, String... extraArgs) {
|
||||
String[] baseArgs = { "--repo-root", tempDir.toString(), "--output-json", outputJson.toString(), "--output-md",
|
||||
outputMarkdown.toString() };
|
||||
String[] args = new String[baseArgs.length + extraArgs.length];
|
||||
System.arraycopy(baseArgs, 0, args, 0, baseArgs.length);
|
||||
System.arraycopy(extraArgs, 0, args, baseArgs.length, extraArgs.length);
|
||||
return args;
|
||||
}
|
||||
|
||||
private JsonObject firstPlan(JsonObject report) {
|
||||
return report.getAsJsonArray("issuePlans").get(0).getAsJsonObject();
|
||||
}
|
||||
|
||||
private void assertSymbolNames(JsonArray symbols, String... expectedNames) {
|
||||
assertEquals(expectedNames.length, symbols.size());
|
||||
for (int index = 0; index < expectedNames.length; index++) {
|
||||
assertEquals(expectedNames[index], symbols.get(index).getAsJsonObject().get("name").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private void writePom(String versionLine) throws IOException {
|
||||
Files.writeString(tempDir.resolve("pom.xml"), "<project>" + System.lineSeparator() + " " + versionLine
|
||||
+ System.lineSeparator() + "</project>" + System.lineSeparator(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String content) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.indicators;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.ta4j.core.indicators.averages.SMAIndicator;
|
||||
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
|
||||
|
||||
/**
|
||||
* Regression coverage + opt-in perf harness for
|
||||
* {@link CachedIndicatorBenchmark}.
|
||||
*/
|
||||
class CachedIndicatorBenchmarkTest {
|
||||
|
||||
private static final String BENCHMARK_PROPERTY = "ta4j.runBenchmarks";
|
||||
|
||||
private final CachedIndicatorBenchmark benchmark = new CachedIndicatorBenchmark();
|
||||
|
||||
@Test
|
||||
void boundedEvictionScenarioProducesExpectedChecksums() {
|
||||
int barCount = 64;
|
||||
int maximumBarCountHint = 12;
|
||||
|
||||
CachedIndicatorBenchmark.ScenarioResult result = benchmark.runBoundedEvictionScenario(barCount,
|
||||
maximumBarCountHint);
|
||||
|
||||
int endIndex = barCount - 1;
|
||||
long expectedOperations = endIndex;
|
||||
long expectedChecksum = expectedBoundedEvictionChecksum(endIndex);
|
||||
|
||||
assertEquals(expectedOperations, result.getOperations(), "Operations should match processed indices");
|
||||
assertEquals(expectedChecksum, result.getChecksum(), "Checksum should reflect monotonic index values");
|
||||
assertTrue(result.getThroughputOpsPerSecond() > 0d, "Throughput should be positive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentCacheHitsScenarioReturnsStableCachedValue() {
|
||||
int barCount = 32;
|
||||
int threads = 2;
|
||||
int readsPerThread = 500;
|
||||
|
||||
CachedIndicatorBenchmark.ScenarioResult result = benchmark.runConcurrentCacheHitsScenario(barCount, threads,
|
||||
readsPerThread);
|
||||
|
||||
int hitIndex = Math.max(0, barCount - 2);
|
||||
long expectedOperations = (long) threads * readsPerThread;
|
||||
long expectedChecksum = expectedOperations * hitIndex;
|
||||
|
||||
assertEquals(expectedOperations, result.getOperations(), "All concurrent reads should be counted");
|
||||
assertEquals(expectedChecksum, result.getChecksum(), "Cached indicator should always return the hit index");
|
||||
assertTrue(result.getThroughputOpsPerSecond() > 0d, "Concurrent scenario should report throughput");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastBarHotReadsScenarioKeepsSmaStableAcrossHits() {
|
||||
int barCount = 48;
|
||||
int smaPeriod = 8;
|
||||
int reads = 750;
|
||||
|
||||
CachedIndicatorBenchmark.ScenarioResult result = benchmark.runLastBarHotReadsScenario(barCount, smaPeriod,
|
||||
reads);
|
||||
|
||||
var series = CachedIndicatorBenchmark.buildSeries(barCount);
|
||||
var closePrice = new ClosePriceIndicator(series);
|
||||
var sma = new SMAIndicator(closePrice, smaPeriod);
|
||||
int endIndex = series.getEndIndex();
|
||||
|
||||
long expectedChecksum = (long) reads * sma.getValue(endIndex).hashCode();
|
||||
|
||||
assertEquals(reads, result.getOperations(), "Each SMA read should be counted");
|
||||
assertEquals(expectedChecksum, result.getChecksum(), "Hot reads should return a stable cached SMA value");
|
||||
assertTrue(result.getThroughputOpsPerSecond() > 0d, "Hot-read scenario should report throughput");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("benchmark")
|
||||
@EnabledIfSystemProperty(named = BENCHMARK_PROPERTY, matches = "true")
|
||||
void benchmarksRunWhenExplicitlyEnabled() throws Exception {
|
||||
int threads = intProperty("ta4j.bench.cachedIndicator.threads", 8);
|
||||
int batches = intProperty("ta4j.bench.cachedIndicator.batches", 1);
|
||||
int evictionBars = intProperty("ta4j.bench.cachedIndicator.evictionBars", 50_000);
|
||||
int cacheHitsPerThread = intProperty("ta4j.bench.cachedIndicator.cacheHitsPerThread", 100_000);
|
||||
int lastBarReads = intProperty("ta4j.bench.cachedIndicator.lastBarReads", 100_000);
|
||||
int maximumBarCountHint = intProperty("ta4j.bench.cachedIndicator.maxBarCountHint", 256);
|
||||
int lastBarSmaPeriod = intProperty("ta4j.bench.cachedIndicator.lastBarSmaPeriod", 32);
|
||||
|
||||
CachedIndicatorBenchmark.main(new String[] { Integer.toString(threads), Integer.toString(batches),
|
||||
Integer.toString(evictionBars), Integer.toString(cacheHitsPerThread), Integer.toString(lastBarReads),
|
||||
Integer.toString(maximumBarCountHint), Integer.toString(lastBarSmaPeriod) });
|
||||
}
|
||||
|
||||
private long expectedBoundedEvictionChecksum(int endIndex) {
|
||||
// Sum of indices [0, endIndex)
|
||||
return (long) (endIndex - 1) * endIndex / 2;
|
||||
}
|
||||
|
||||
private static int intProperty(String key, int defaultValue) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.logging;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class StrategyExecutionLoggingTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
StrategyExecutionLogging.main(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.num;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class CompareNumTypesTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
CompareNumTypes.main(null);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.rules;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.ta4j.core.rules.AndRule;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
/**
|
||||
* Regression coverage + opt-in perf harness for {@link RuleNameBenchmark}.
|
||||
*/
|
||||
class RuleNameBenchmarkTest {
|
||||
|
||||
private static final String BENCHMARK_PROPERTY = "ta4j.runBenchmarks";
|
||||
|
||||
@Test
|
||||
void eagerAndRuleTouchesChildNamesOnceAndCachesResult() {
|
||||
CountingRule left = new CountingRule("left");
|
||||
CountingRule right = new CountingRule("right");
|
||||
|
||||
AndRule andRule = new AndRule(left, right);
|
||||
|
||||
assertEquals(1, left.nameRequests(), "AndRule constructor should read left child name exactly once");
|
||||
assertEquals(1, right.nameRequests(), "AndRule constructor should read right child name exactly once");
|
||||
|
||||
assertEquals("AndRule(left,right)", andRule.getName(), "AndRule name should include both child names");
|
||||
andRule.getName();
|
||||
assertEquals(1, left.nameRequests(), "AndRule should not re-query child names once cached");
|
||||
assertEquals(1, right.nameRequests(), "AndRule should not re-query child names once cached");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyAndRuleDefersChildNameResolutionUntilGetName() {
|
||||
CountingRule left = new CountingRule("lazyLeft");
|
||||
CountingRule right = new CountingRule("lazyRight");
|
||||
|
||||
RuleNameBenchmark.LazyAndRule lazyAndRule = new RuleNameBenchmark.LazyAndRule(left, right);
|
||||
|
||||
assertEquals(0, left.nameRequests(), "LazyAndRule should not touch child names in constructor");
|
||||
assertEquals(0, right.nameRequests(), "LazyAndRule should not touch child names in constructor");
|
||||
|
||||
assertEquals("LazyAndRule(lazyLeft,lazyRight)", lazyAndRule.getName(),
|
||||
"LazyAndRule should build name lazily on first access");
|
||||
assertEquals(1, left.nameRequests(), "LazyAndRule should resolve left name once when needed");
|
||||
assertEquals(1, right.nameRequests(), "LazyAndRule should resolve right name once when needed");
|
||||
lazyAndRule.getName();
|
||||
assertEquals(1, left.nameRequests(), "LazyAndRule should not re-resolve after caching the name");
|
||||
assertEquals(1, right.nameRequests(), "LazyAndRule should not re-resolve after caching the name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noNameLeakAndRuleAvoidsChildNameResolutionEntirely() {
|
||||
CountingRule left = new CountingRule("ignoredLeft");
|
||||
CountingRule right = new CountingRule("ignoredRight");
|
||||
|
||||
RuleNameBenchmark.NoNameLeakAndRule noLeakRule = new RuleNameBenchmark.NoNameLeakAndRule(left, right);
|
||||
|
||||
assertEquals("NoNameLeakAndRule(CountingRule,CountingRule)", noLeakRule.getName(),
|
||||
"NoNameLeakAndRule should use child types without touching getName()");
|
||||
assertEquals(0, left.nameRequests(), "Child names should not be accessed for no-leak variant");
|
||||
assertEquals(0, right.nameRequests(), "Child names should not be accessed for no-leak variant");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("benchmark")
|
||||
@EnabledIfSystemProperty(named = BENCHMARK_PROPERTY, matches = "true")
|
||||
void benchmarksRunWhenExplicitlyEnabled() throws Exception {
|
||||
int threads = Math.max(8, Runtime.getRuntime().availableProcessors());
|
||||
RuleNameBenchmark.main(new String[] { Integer.toString(threads), "2", "20000" });
|
||||
}
|
||||
|
||||
private static final class CountingRule extends FixedRule {
|
||||
|
||||
private final AtomicInteger nameRequests = new AtomicInteger();
|
||||
private final String label;
|
||||
|
||||
private CountingRule(String label) {
|
||||
super(0);
|
||||
this.label = label;
|
||||
setName(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
nameRequests.incrementAndGet();
|
||||
return super.getName();
|
||||
}
|
||||
|
||||
int nameRequests() {
|
||||
return nameRequests.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Test;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
public class CCICorrectionStrategyTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// Disable chart display during tests to prevent windows from popping up
|
||||
// This works in both headless and non-headless environments
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
CCICorrectionStrategy.main(null);
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class DayOfWeekStrategyTest {
|
||||
|
||||
private BarSeries series;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
NumFactory numFactory = DecimalNumFactory.getInstance();
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
// Create bars for each day of the week starting from Monday
|
||||
// 2019-09-16 is a Monday
|
||||
// Note: DateTimeIndicator default constructor uses Bar::getBeginTime
|
||||
// We set beginTime explicitly to match the day we want to test
|
||||
// Use Duration.between to calculate the exact timePeriod
|
||||
Instant mondayBegin = Instant.parse("2019-09-16T00:00:00Z");
|
||||
Instant mondayEnd = Instant.parse("2019-09-16T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(mondayBegin, mondayEnd))
|
||||
.beginTime(mondayBegin)
|
||||
.endTime(mondayEnd)
|
||||
.closePrice(100d)
|
||||
.add(); // Monday
|
||||
Instant tuesdayBegin = Instant.parse("2019-09-17T00:00:00Z");
|
||||
Instant tuesdayEnd = Instant.parse("2019-09-17T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(tuesdayBegin, tuesdayEnd))
|
||||
.beginTime(tuesdayBegin)
|
||||
.endTime(tuesdayEnd)
|
||||
.closePrice(101d)
|
||||
.add(); // Tuesday
|
||||
Instant wednesdayBegin = Instant.parse("2019-09-18T00:00:00Z");
|
||||
Instant wednesdayEnd = Instant.parse("2019-09-18T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(wednesdayBegin, wednesdayEnd))
|
||||
.beginTime(wednesdayBegin)
|
||||
.endTime(wednesdayEnd)
|
||||
.closePrice(102d)
|
||||
.add(); // Wednesday
|
||||
Instant thursdayBegin = Instant.parse("2019-09-19T00:00:00Z");
|
||||
Instant thursdayEnd = Instant.parse("2019-09-19T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(thursdayBegin, thursdayEnd))
|
||||
.beginTime(thursdayBegin)
|
||||
.endTime(thursdayEnd)
|
||||
.closePrice(103d)
|
||||
.add(); // Thursday
|
||||
Instant fridayBegin = Instant.parse("2019-09-20T00:00:00Z");
|
||||
Instant fridayEnd = Instant.parse("2019-09-20T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(fridayBegin, fridayEnd))
|
||||
.beginTime(fridayBegin)
|
||||
.endTime(fridayEnd)
|
||||
.closePrice(104d)
|
||||
.add(); // Friday
|
||||
Instant saturdayBegin = Instant.parse("2019-09-21T00:00:00Z");
|
||||
Instant saturdayEnd = Instant.parse("2019-09-21T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(saturdayBegin, saturdayEnd))
|
||||
.beginTime(saturdayBegin)
|
||||
.endTime(saturdayEnd)
|
||||
.closePrice(105d)
|
||||
.add(); // Saturday
|
||||
Instant sundayBegin = Instant.parse("2019-09-22T00:00:00Z");
|
||||
Instant sundayEnd = Instant.parse("2019-09-22T23:59:59Z");
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(sundayBegin, sundayEnd))
|
||||
.beginTime(sundayBegin)
|
||||
.endTime(sundayEnd)
|
||||
.closePrice(106d)
|
||||
.add(); // Sunday
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithDayOfWeek() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("DayOfWeekStrategy_MONDAY_FRIDAY", strategy.getName());
|
||||
assertEquals("{\"type\":\"NamedStrategy\",\"label\":\"DayOfWeekStrategy_MONDAY_FRIDAY\"}", strategy.toJson());
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParams() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, "MONDAY", "FRIDAY");
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("DayOfWeekStrategy_MONDAY_FRIDAY", strategy.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParamsAllDays() {
|
||||
for (DayOfWeek entryDay : DayOfWeek.values()) {
|
||||
for (DayOfWeek exitDay : DayOfWeek.values()) {
|
||||
if (entryDay != exitDay) {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, entryDay.name(), exitDay.name());
|
||||
assertNotNull(strategy);
|
||||
assertEquals("DayOfWeekStrategy_" + entryDay + "_" + exitDay, strategy.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithNullParams() {
|
||||
new DayOfWeekStrategy(series, (String[]) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithEmptyParams() {
|
||||
new DayOfWeekStrategy(series);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInsufficientParams() {
|
||||
new DayOfWeekStrategy(series, "MONDAY");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidEntryDay() {
|
||||
new DayOfWeekStrategy(series, "INVALID_DAY", "FRIDAY");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidExitDay() {
|
||||
new DayOfWeekStrategy(series, "MONDAY", "INVALID_DAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithCaseSensitiveDayNames() {
|
||||
try {
|
||||
new DayOfWeekStrategy(series, "monday", "friday");
|
||||
fail("Expected IllegalArgumentException for lowercase day names");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry DayOfWeek value"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryRuleSatisfiedOnCorrectDay() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 0 is Monday - entry rule should be satisfied
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(0, tradingRecord));
|
||||
|
||||
// Index 1 is Tuesday - entry rule should not be satisfied
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(1, tradingRecord));
|
||||
|
||||
// Index 4 is Friday - entry rule should not be satisfied (it's exit day)
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(4, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExitRuleSatisfiedOnCorrectDay() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 0 is Monday - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(0, tradingRecord));
|
||||
|
||||
// Index 4 is Friday - exit rule should be satisfied
|
||||
assertTrue(strategy.getExitRule().isSatisfied(4, tradingRecord));
|
||||
|
||||
// Index 1 is Tuesday - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(1, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutations() {
|
||||
List<Strategy> strategies = DayOfWeekStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
assertNotNull(strategies);
|
||||
// Should have 7 * 6 = 42 strategies (all combinations except where entry ==
|
||||
// exit)
|
||||
assertEquals(42, strategies.size());
|
||||
|
||||
// Verify all strategies have unique names
|
||||
long uniqueNames = strategies.stream().map(Strategy::getName).distinct().count();
|
||||
assertEquals(42, uniqueNames);
|
||||
|
||||
// Verify all strategies are DayOfWeekStrategy instances
|
||||
for (Strategy strategy : strategies) {
|
||||
assertTrue(strategy instanceof DayOfWeekStrategy);
|
||||
assertNotNull(strategy.getName());
|
||||
assertTrue(strategy.getName().startsWith("DayOfWeekStrategy_"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutationsNoDuplicateEntryExit() {
|
||||
List<Strategy> strategies = DayOfWeekStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
// Verify no strategy has the same entry and exit day
|
||||
for (Strategy strategy : strategies) {
|
||||
String name = strategy.getName();
|
||||
String[] parts = name.split("_");
|
||||
assertNotEquals("Strategy should not have same entry and exit day: " + name, parts[1], parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyNameFormat() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.TUESDAY, DayOfWeek.THURSDAY);
|
||||
String name = strategy.getName();
|
||||
|
||||
assertTrue(name.startsWith("DayOfWeekStrategy_"));
|
||||
assertTrue(name.contains("TUESDAY"));
|
||||
assertTrue(name.contains("THURSDAY"));
|
||||
assertEquals("DayOfWeekStrategy_TUESDAY_THURSDAY", name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithWeekendDays() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("DayOfWeekStrategy_SATURDAY_SUNDAY", strategy.getName());
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
// Index 5 is Saturday
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(5, tradingRecord));
|
||||
// Index 6 is Sunday
|
||||
assertTrue(strategy.getExitRule().isSatisfied(6, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithAllDayCombinations() {
|
||||
for (DayOfWeek entryDay : DayOfWeek.values()) {
|
||||
for (DayOfWeek exitDay : DayOfWeek.values()) {
|
||||
if (entryDay != exitDay) {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, entryDay, exitDay);
|
||||
assertNotNull(strategy);
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyRulesAreNotNull() {
|
||||
DayOfWeekStrategy strategy = new DayOfWeekStrategy(series, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY);
|
||||
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithNullSeries() {
|
||||
try {
|
||||
new DayOfWeekStrategy(null, DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
|
||||
fail("Expected NullPointerException or IllegalArgumentException for null series");
|
||||
} catch (Exception e) {
|
||||
// Expected - either NullPointerException or IllegalArgumentException
|
||||
assertTrue(e instanceof NullPointerException || e instanceof IllegalArgumentException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEntryDayOfWeekErrorMessage() {
|
||||
try {
|
||||
new DayOfWeekStrategy(series, "INVALID", "FRIDAY");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry DayOfWeek value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are:"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExitDayOfWeekErrorMessage() {
|
||||
try {
|
||||
new DayOfWeekStrategy(series, "MONDAY", "INVALID");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit DayOfWeek value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are:"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsufficientParamsErrorMessage() {
|
||||
try {
|
||||
new DayOfWeekStrategy(series, "MONDAY");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("At least 2 parameters required"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Test;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
public class GlobalExtremaStrategyTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// Disable chart display during tests to prevent windows from popping up
|
||||
// This works in both headless and non-headless environments
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
GlobalExtremaStrategy.main(null);
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.ConcurrentBarSeries;
|
||||
import org.ta4j.core.ConcurrentBarSeriesBuilder;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.elliott.ElliottConfidence;
|
||||
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.ElliottScenarioSet;
|
||||
import org.ta4j.core.indicators.elliott.ElliottSwing;
|
||||
import org.ta4j.core.indicators.elliott.ScenarioType;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HighRewardElliottWaveStrategyTest {
|
||||
|
||||
private ConcurrentBarSeries series;
|
||||
private NumFactory numFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
numFactory = DecimalNumFactory.getInstance();
|
||||
series = new ConcurrentBarSeriesBuilder().withName("elliott-test").withNumFactory(numFactory).build();
|
||||
Instant start = Instant.parse("2025-01-01T00:00:00Z");
|
||||
for (int i = 0; i < 30; i++) {
|
||||
BigDecimal close = BigDecimal.valueOf(100.0 + i);
|
||||
addBar(series, start.plusSeconds(i * 60L), close);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultLabelContainsDirectionAndDegree() {
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series);
|
||||
String[] parts = strategy.getName().split("_");
|
||||
assertEquals("HighRewardElliottWaveStrategy", parts[0]);
|
||||
assertEquals("BULLISH", parts[1]);
|
||||
assertEquals("PRIMARY", parts[2]);
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithParamsBuildsExpectedLabel() {
|
||||
String[] params = new String[] { "BULLISH", "PRIMARY", "0.7", "3", "1.5", "0.2", "5", "2", "50", "2", "4",
|
||||
"0.2" };
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, params);
|
||||
String[] parts = strategy.getName().split("_");
|
||||
assertEquals("HighRewardElliottWaveStrategy", parts[0]);
|
||||
assertEquals("BULLISH", parts[1]);
|
||||
assertEquals("PRIMARY", parts[2]);
|
||||
assertEquals("0.7", parts[3]);
|
||||
assertEquals("3", parts[4]);
|
||||
assertEquals("1.5", parts[5]);
|
||||
assertEquals("0.2", parts[6]);
|
||||
assertEquals("5", parts[7]);
|
||||
assertEquals("2", parts[8]);
|
||||
assertEquals("50", parts[9]);
|
||||
assertEquals("2", parts[10]);
|
||||
assertEquals("4", parts[11]);
|
||||
assertEquals("0.2", parts[12]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorRejectsInvalidDirection() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new HighRewardElliottWaveStrategy(series, "SIDEWAYS",
|
||||
"PRIMARY", "0.7", "3", "1.5", "0.2", "5", "2", "50", "2", "4", "0.2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorRejectsWrongParamCount() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HighRewardElliottWaveStrategy(series, "BULLISH", "PRIMARY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEntryRuleSatisfiedForHighConfidenceImpulse() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
ElliottScenario scenario = buildScenario(numFactory.numOf(120), numFactory.numOf(200));
|
||||
ElliottScenarioSet scenarioSet = buildScenarioSet(series, scenario);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEntryRuleRejectedWhenRiskRewardTooLow() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
ElliottScenario scenario = buildScenario(numFactory.numOf(120), numFactory.numOf(140));
|
||||
ElliottScenarioSet scenarioSet = buildScenarioSet(series, scenario);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEntryRuleRejectedWhenNoScenario() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
ElliottScenarioSet scenarioSet = buildEmptyScenarioSet(series);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExitRuleTriggersOnInvalidation() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
ElliottScenario scenario = buildScenario(numFactory.numOf(130), numFactory.numOf(200));
|
||||
ElliottScenarioSet scenarioSet = buildScenarioSet(series, scenario);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
BaseTradingRecord record = new BaseTradingRecord();
|
||||
int entryIndex = series.getEndIndex() - 1;
|
||||
record.enter(entryIndex);
|
||||
|
||||
assertTrue(strategy.getExitRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExitRuleTriggersOnCorrectiveStopViolation() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
List<ElliottSwing> swings = List.of(
|
||||
new ElliottSwing(0, 4, numFactory.numOf(100), numFactory.numOf(150), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(4, 6, numFactory.numOf(150), numFactory.numOf(140), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(6, 12, numFactory.numOf(140), numFactory.numOf(160), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(12, 16, numFactory.numOf(160), numFactory.numOf(150), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(16, 20, numFactory.numOf(150), numFactory.numOf(170), ElliottDegree.PRIMARY));
|
||||
|
||||
ElliottScenario scenario = ElliottScenario.builder()
|
||||
.id("test-stop")
|
||||
.currentPhase(ElliottPhase.WAVE3)
|
||||
.swings(swings)
|
||||
.confidence(buildConfidence(numFactory, 0.8))
|
||||
.degree(ElliottDegree.PRIMARY)
|
||||
.invalidationPrice(numFactory.numOf(120))
|
||||
.primaryTarget(numFactory.numOf(200))
|
||||
.fibonacciTargets(List.of(numFactory.numOf(200)))
|
||||
.type(ScenarioType.IMPULSE)
|
||||
.startIndex(0)
|
||||
.build();
|
||||
|
||||
ElliottScenarioSet scenarioSet = buildScenarioSet(series, scenario);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
BaseTradingRecord record = new BaseTradingRecord();
|
||||
int entryIndex = series.getEndIndex() - 1;
|
||||
record.enter(entryIndex);
|
||||
|
||||
assertTrue(strategy.getExitRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExitRuleTriggersWhenNoScenario() {
|
||||
HighRewardElliottWaveStrategy.Config config = new HighRewardElliottWaveStrategy.Config(
|
||||
HighRewardElliottWaveStrategy.SignalDirection.BULLISH, ElliottDegree.PRIMARY, 0.7, 3.0, 1.5, 0.2, 5, 2,
|
||||
50.0, 2, 4, 0.2);
|
||||
|
||||
ElliottScenarioSet scenarioSet = buildEmptyScenarioSet(series);
|
||||
Indicator<ElliottScenarioSet> indicator = new FixedScenarioIndicator(series, scenarioSet);
|
||||
|
||||
HighRewardElliottWaveStrategy strategy = new HighRewardElliottWaveStrategy(series, config, indicator);
|
||||
BaseTradingRecord record = new BaseTradingRecord();
|
||||
int entryIndex = series.getEndIndex() - 1;
|
||||
record.enter(entryIndex);
|
||||
|
||||
assertTrue(strategy.getExitRule().isSatisfied(series.getEndIndex(), record));
|
||||
}
|
||||
|
||||
private ElliottScenario buildScenario(Num invalidation, Num target) {
|
||||
List<ElliottSwing> swings = List.of(
|
||||
new ElliottSwing(0, 4, numFactory.numOf(100), numFactory.numOf(120), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(4, 6, numFactory.numOf(120), numFactory.numOf(110), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(6, 12, numFactory.numOf(110), numFactory.numOf(145), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(12, 16, numFactory.numOf(145), numFactory.numOf(130), ElliottDegree.PRIMARY),
|
||||
new ElliottSwing(16, 20, numFactory.numOf(130), numFactory.numOf(160), ElliottDegree.PRIMARY));
|
||||
|
||||
ElliottConfidence confidence = buildConfidence(numFactory, 0.8);
|
||||
|
||||
return ElliottScenario.builder()
|
||||
.id("test")
|
||||
.currentPhase(ElliottPhase.WAVE3)
|
||||
.swings(swings)
|
||||
.confidence(confidence)
|
||||
.degree(ElliottDegree.PRIMARY)
|
||||
.invalidationPrice(invalidation)
|
||||
.primaryTarget(target)
|
||||
.fibonacciTargets(List.of(target))
|
||||
.type(ScenarioType.IMPULSE)
|
||||
.startIndex(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ElliottScenarioSet buildScenarioSet(BarSeries series, ElliottScenario scenario) {
|
||||
return ElliottScenarioSet.of(List.of(scenario), series.getEndIndex());
|
||||
}
|
||||
|
||||
private ElliottScenarioSet buildEmptyScenarioSet(BarSeries series) {
|
||||
return ElliottScenarioSet.empty(series.getEndIndex());
|
||||
}
|
||||
|
||||
private ElliottConfidence buildConfidence(NumFactory factory, double overall) {
|
||||
Num score = factory.numOf(overall);
|
||||
return new ElliottConfidence(score, score, score, score, score, score, "test");
|
||||
}
|
||||
|
||||
private static void addBar(ConcurrentBarSeries series, Instant start, BigDecimal close) {
|
||||
Instant end = start.plusSeconds(60L);
|
||||
BigDecimal high = close.add(new BigDecimal("0.5"));
|
||||
BigDecimal low = close.subtract(new BigDecimal("0.5"));
|
||||
series.addBar(buildBar(series, start, end, close, high, low, close));
|
||||
}
|
||||
|
||||
private static Bar buildBar(ConcurrentBarSeries series, Instant start, Instant end, BigDecimal open,
|
||||
BigDecimal high, BigDecimal low, BigDecimal close) {
|
||||
return series.barBuilder()
|
||||
.timePeriod(Duration.between(start, end))
|
||||
.beginTime(start)
|
||||
.endTime(end)
|
||||
.openPrice(open)
|
||||
.highPrice(high)
|
||||
.lowPrice(low)
|
||||
.closePrice(close)
|
||||
.volume(new BigDecimal("1"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final class FixedScenarioIndicator implements Indicator<ElliottScenarioSet> {
|
||||
|
||||
private final BarSeries series;
|
||||
private final ElliottScenarioSet scenarioSet;
|
||||
|
||||
private FixedScenarioIndicator(BarSeries series, ElliottScenarioSet scenarioSet) {
|
||||
this.series = series;
|
||||
this.scenarioSet = scenarioSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElliottScenarioSet getValue(int index) {
|
||||
return scenarioSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class HourOfDayStrategyTest {
|
||||
|
||||
private BarSeries series;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
NumFactory numFactory = DecimalNumFactory.getInstance();
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
// Create bars for different hours of the day
|
||||
// Use the same day (2019-09-16) but different hours
|
||||
for (int hour = 0; hour < 24; hour++) {
|
||||
Instant beginTime = Instant.parse("2019-09-16T" + String.format("%02d:00:00Z", hour));
|
||||
Instant endTime = Instant.parse("2019-09-16T" + String.format("%02d:59:59Z", hour));
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(beginTime, endTime))
|
||||
.beginTime(beginTime)
|
||||
.endTime(endTime)
|
||||
.closePrice(100d + hour)
|
||||
.add();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithHours() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 9, 17);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("HourOfDayStrategy_9_17", strategy.getName());
|
||||
assertEquals("{\"type\":\"NamedStrategy\",\"label\":\"HourOfDayStrategy_9_17\"}", strategy.toJson());
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParams() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, "9", "17");
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("HourOfDayStrategy_9_17", strategy.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParamsAllHours() {
|
||||
for (int entryHour = 0; entryHour < 24; entryHour++) {
|
||||
for (int exitHour = 0; exitHour < 24; exitHour++) {
|
||||
if (entryHour != exitHour) {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, String.valueOf(entryHour),
|
||||
String.valueOf(exitHour));
|
||||
assertNotNull(strategy);
|
||||
assertEquals("HourOfDayStrategy_" + entryHour + "_" + exitHour, strategy.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithNullParams() {
|
||||
new HourOfDayStrategy(series, (String[]) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithEmptyParams() {
|
||||
new HourOfDayStrategy(series);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInsufficientParams() {
|
||||
new HourOfDayStrategy(series, "9");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidEntryHourNegative() {
|
||||
new HourOfDayStrategy(series, "-1", "17");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidEntryHourTooLarge() {
|
||||
new HourOfDayStrategy(series, "24", "17");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidExitHourNegative() {
|
||||
new HourOfDayStrategy(series, "9", "-1");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidExitHourTooLarge() {
|
||||
new HourOfDayStrategy(series, "9", "24");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithSameEntryAndExitHour() {
|
||||
new HourOfDayStrategy(series, 12, 12);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithSameEntryAndExitHourString() {
|
||||
new HourOfDayStrategy(series, "12", "12");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithNonNumericEntryHour() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "abc", "17");
|
||||
fail("Expected IllegalArgumentException for non-numeric entry hour");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithNonNumericExitHour() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "9", "xyz");
|
||||
fail("Expected IllegalArgumentException for non-numeric exit hour");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryRuleSatisfiedOnCorrectHour() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 9, 17);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 9 is hour 9 - entry rule should be satisfied
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(9, tradingRecord));
|
||||
|
||||
// Index 10 is hour 10 - entry rule should not be satisfied
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(10, tradingRecord));
|
||||
|
||||
// Index 17 is hour 17 - entry rule should not be satisfied (it's exit hour)
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(17, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExitRuleSatisfiedOnCorrectHour() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 9, 17);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 9 is hour 9 - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(9, tradingRecord));
|
||||
|
||||
// Index 17 is hour 17 - exit rule should be satisfied
|
||||
assertTrue(strategy.getExitRule().isSatisfied(17, tradingRecord));
|
||||
|
||||
// Index 10 is hour 10 - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(10, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutations() {
|
||||
List<Strategy> strategies = HourOfDayStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
assertNotNull(strategies);
|
||||
// Should have 24 * 23 = 552 strategies (all combinations except where entry ==
|
||||
// exit)
|
||||
assertEquals(552, strategies.size());
|
||||
|
||||
// Verify all strategies have unique names
|
||||
long uniqueNames = strategies.stream().map(Strategy::getName).distinct().count();
|
||||
assertEquals(552, uniqueNames);
|
||||
|
||||
// Verify all strategies are HourOfDayStrategy instances
|
||||
for (Strategy strategy : strategies) {
|
||||
assertTrue(strategy instanceof HourOfDayStrategy);
|
||||
assertNotNull(strategy.getName());
|
||||
assertTrue(strategy.getName().startsWith("HourOfDayStrategy_"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutationsNoDuplicateEntryExit() {
|
||||
List<Strategy> strategies = HourOfDayStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
// Verify no strategy has the same entry and exit hour
|
||||
for (Strategy strategy : strategies) {
|
||||
String name = strategy.getName();
|
||||
String[] parts = name.split("_");
|
||||
assertNotEquals("Strategy should not have same entry and exit hour: " + name, parts[1], parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyNameFormat() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 10, 15);
|
||||
String name = strategy.getName();
|
||||
|
||||
assertTrue(name.startsWith("HourOfDayStrategy_"));
|
||||
assertTrue(name.contains("10"));
|
||||
assertTrue(name.contains("15"));
|
||||
assertEquals("HourOfDayStrategy_10_15", name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithBoundaryHours() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 0, 23);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("HourOfDayStrategy_0_23", strategy.getName());
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
// Index 0 is hour 0
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(0, tradingRecord));
|
||||
// Index 23 is hour 23
|
||||
assertTrue(strategy.getExitRule().isSatisfied(23, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithAllHourCombinations() {
|
||||
for (int entryHour = 0; entryHour < 24; entryHour++) {
|
||||
for (int exitHour = 0; exitHour < 24; exitHour++) {
|
||||
if (entryHour != exitHour) {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, entryHour, exitHour);
|
||||
assertNotNull(strategy);
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyRulesAreNotNull() {
|
||||
HourOfDayStrategy strategy = new HourOfDayStrategy(series, 12, 18);
|
||||
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithNullSeries() {
|
||||
try {
|
||||
new HourOfDayStrategy(null, 9, 17);
|
||||
fail("Expected NullPointerException or IllegalArgumentException for null series");
|
||||
} catch (Exception e) {
|
||||
// Expected - either NullPointerException or IllegalArgumentException
|
||||
assertTrue(e instanceof NullPointerException || e instanceof IllegalArgumentException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEntryHourErrorMessage() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "INVALID", "17");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExitHourErrorMessage() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "9", "INVALID");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsufficientParamsErrorMessage() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "9");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("At least 2 parameters required"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryHourOutOfRangeErrorMessage() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "25", "17");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExitHourOutOfRangeErrorMessage() {
|
||||
try {
|
||||
new HourOfDayStrategy(series, "9", "25");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit hour value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-23"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class MinuteOfHourStrategyTest {
|
||||
|
||||
private BarSeries series;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
NumFactory numFactory = DecimalNumFactory.getInstance();
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
// Create bars for different minutes of the hour
|
||||
// Use the same hour (12:00) but different minutes
|
||||
for (int minute = 0; minute < 60; minute++) {
|
||||
Instant beginTime = Instant.parse("2019-09-16T12:" + String.format("%02d:00Z", minute));
|
||||
Instant endTime = Instant.parse("2019-09-16T12:" + String.format("%02d:59Z", minute));
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.between(beginTime, endTime))
|
||||
.beginTime(beginTime)
|
||||
.endTime(endTime)
|
||||
.closePrice(100d + minute)
|
||||
.add();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithMinutes() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 15, 45);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("MinuteOfHourStrategy_15_45", strategy.getName());
|
||||
assertEquals("{\"type\":\"NamedStrategy\",\"label\":\"MinuteOfHourStrategy_15_45\"}", strategy.toJson());
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParams() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, "15", "45");
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("MinuteOfHourStrategy_15_45", strategy.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithStringParamsAllMinutes() {
|
||||
for (int entryMinute = 0; entryMinute < 60; entryMinute++) {
|
||||
for (int exitMinute = 0; exitMinute < 60; exitMinute++) {
|
||||
if (entryMinute != exitMinute) {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, String.valueOf(entryMinute),
|
||||
String.valueOf(exitMinute));
|
||||
assertNotNull(strategy);
|
||||
assertEquals("MinuteOfHourStrategy_" + entryMinute + "_" + exitMinute, strategy.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithNullParams() {
|
||||
new MinuteOfHourStrategy(series, (String[]) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithEmptyParams() {
|
||||
new MinuteOfHourStrategy(series);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInsufficientParams() {
|
||||
new MinuteOfHourStrategy(series, "15");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidEntryMinuteNegative() {
|
||||
new MinuteOfHourStrategy(series, "-1", "45");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidEntryMinuteTooLarge() {
|
||||
new MinuteOfHourStrategy(series, "60", "45");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidExitMinuteNegative() {
|
||||
new MinuteOfHourStrategy(series, "15", "-1");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithInvalidExitMinuteTooLarge() {
|
||||
new MinuteOfHourStrategy(series, "15", "60");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithSameEntryAndExitMinute() {
|
||||
new MinuteOfHourStrategy(series, 30, 30);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConstructorWithSameEntryAndExitMinuteString() {
|
||||
new MinuteOfHourStrategy(series, "30", "30");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithNonNumericEntryMinute() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "abc", "45");
|
||||
fail("Expected IllegalArgumentException for non-numeric entry minute");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithNonNumericExitMinute() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "15", "xyz");
|
||||
fail("Expected IllegalArgumentException for non-numeric exit minute");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryRuleSatisfiedOnCorrectMinute() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 15, 45);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 15 is minute 15 - entry rule should be satisfied
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(15, tradingRecord));
|
||||
|
||||
// Index 20 is minute 20 - entry rule should not be satisfied
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(20, tradingRecord));
|
||||
|
||||
// Index 45 is minute 45 - entry rule should not be satisfied (it's exit minute)
|
||||
assertFalse(strategy.getEntryRule().isSatisfied(45, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExitRuleSatisfiedOnCorrectMinute() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 15, 45);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
// Index 15 is minute 15 - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(15, tradingRecord));
|
||||
|
||||
// Index 45 is minute 45 - exit rule should be satisfied
|
||||
assertTrue(strategy.getExitRule().isSatisfied(45, tradingRecord));
|
||||
|
||||
// Index 20 is minute 20 - exit rule should not be satisfied
|
||||
assertFalse(strategy.getExitRule().isSatisfied(20, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutations() {
|
||||
List<Strategy> strategies = MinuteOfHourStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
assertNotNull(strategies);
|
||||
// Should have 60 * 59 = 3540 strategies (all combinations except where entry ==
|
||||
// exit)
|
||||
assertEquals(3540, strategies.size());
|
||||
|
||||
// Verify all strategies have unique names
|
||||
long uniqueNames = strategies.stream().map(Strategy::getName).distinct().count();
|
||||
assertEquals(3540, uniqueNames);
|
||||
|
||||
// Verify all strategies are MinuteOfHourStrategy instances
|
||||
for (Strategy strategy : strategies) {
|
||||
assertTrue(strategy instanceof MinuteOfHourStrategy);
|
||||
assertNotNull(strategy.getName());
|
||||
assertTrue(strategy.getName().startsWith("MinuteOfHourStrategy_"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllStrategyPermutationsNoDuplicateEntryExit() {
|
||||
List<Strategy> strategies = MinuteOfHourStrategy.buildAllStrategyPermutations(series);
|
||||
|
||||
// Verify no strategy has the same entry and exit minute
|
||||
for (Strategy strategy : strategies) {
|
||||
String name = strategy.getName();
|
||||
String[] parts = name.split("_");
|
||||
assertNotEquals("Strategy should not have same entry and exit minute: " + name, parts[1], parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyNameFormat() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 20, 40);
|
||||
String name = strategy.getName();
|
||||
|
||||
assertTrue(name.startsWith("MinuteOfHourStrategy_"));
|
||||
assertTrue(name.contains("20"));
|
||||
assertTrue(name.contains("40"));
|
||||
assertEquals("MinuteOfHourStrategy_20_40", name);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithBoundaryMinutes() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 0, 59);
|
||||
|
||||
assertNotNull(strategy);
|
||||
assertEquals("MinuteOfHourStrategy_0_59", strategy.getName());
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
// Index 0 is minute 0
|
||||
assertTrue(strategy.getEntryRule().isSatisfied(0, tradingRecord));
|
||||
// Index 59 is minute 59
|
||||
assertTrue(strategy.getExitRule().isSatisfied(59, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithAllMinuteCombinations() {
|
||||
for (int entryMinute = 0; entryMinute < 60; entryMinute++) {
|
||||
for (int exitMinute = 0; exitMinute < 60; exitMinute++) {
|
||||
if (entryMinute != exitMinute) {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, entryMinute, exitMinute);
|
||||
assertNotNull(strategy);
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyRulesAreNotNull() {
|
||||
MinuteOfHourStrategy strategy = new MinuteOfHourStrategy(series, 20, 40);
|
||||
|
||||
assertNotNull(strategy.getEntryRule());
|
||||
assertNotNull(strategy.getExitRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrategyWithNullSeries() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(null, 15, 45);
|
||||
fail("Expected NullPointerException or IllegalArgumentException for null series");
|
||||
} catch (Exception e) {
|
||||
// Expected - either NullPointerException or IllegalArgumentException
|
||||
assertTrue(e instanceof NullPointerException || e instanceof IllegalArgumentException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEntryMinuteErrorMessage() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "INVALID", "45");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExitMinuteErrorMessage() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "15", "INVALID");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsufficientParamsErrorMessage() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "15");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("At least 2 parameters required"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryMinuteOutOfRangeErrorMessage() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "60", "45");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid entry minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExitMinuteOutOfRangeErrorMessage() {
|
||||
try {
|
||||
new MinuteOfHourStrategy(series, "15", "60");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Invalid exit minute value"));
|
||||
assertTrue(e.getMessage().contains("Valid values are integers in the range 0-59"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Test;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
public class MovingMomentumStrategyTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// Disable chart display during tests to prevent windows from popping up
|
||||
// This works in both headless and non-headless environments
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
MovingMomentumStrategy.main(null);
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.strategies;
|
||||
|
||||
import org.junit.Test;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
public class RSI2StrategyTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// Disable chart display during tests to prevent windows from popping up
|
||||
// This works in both headless and non-headless environments
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
RSI2Strategy.main(null);
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package ta4jexamples.walkforward;
|
||||
|
||||
import org.junit.Test;
|
||||
import ta4jexamples.charting.display.SwingChartDisplayer;
|
||||
|
||||
public class WalkForwardTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// Disable chart display during tests to prevent windows from popping up
|
||||
System.setProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY, "true");
|
||||
try {
|
||||
WalkForward.main(null);
|
||||
} finally {
|
||||
System.clearProperty(SwingChartDisplayer.DISABLE_DISPLAY_PROPERTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN" monitorInterval="0">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="ta4jexamples.charting" level="off" additivity="false"/>
|
||||
<Logger name="ta4jexamples.datasources" level="off" additivity="false"/>
|
||||
<Logger name="ta4jexamples.analysis" level="off" additivity="false"/>
|
||||
<Logger name="ta4jexamples.rules" level="off" additivity="false"/>
|
||||
<Logger name="ta4jexamples.indicators" level="off" additivity="false"/>
|
||||
<Logger name="ta4jexamples.backtesting" level="off" additivity="false"/>
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"registryId": "test-registry-missing-anchors",
|
||||
"datasetResource": "test-dataset.csv",
|
||||
"provenance": "unit test fixture"
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"registryId": "test-registry-null-anchor",
|
||||
"datasetResource": "test-dataset.csv",
|
||||
"provenance": "unit test fixture",
|
||||
"anchors": [
|
||||
null
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user