goldenChat base source add
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# AGENTS instructions for `ta4j-core/src/test/java`
|
||||
|
||||
These are cross-cutting test rules for ta4j-core. Deeper package guides can add stricter local conventions.
|
||||
|
||||
## Non-negotiable test policy
|
||||
|
||||
- Never skip tests without explicit user approval.
|
||||
- Never hide failures with `@Ignore`, unconditional assumptions, or silent try/catch blocks.
|
||||
- Treat failing tests as defects to fix or escalate, not as noise.
|
||||
|
||||
## Test design conventions
|
||||
|
||||
- Use `assertThrows` for exception assertions (avoid `@Test(expected = ...)` and manual try/catch assertions).
|
||||
- Avoid reflection-based access to private APIs; test through public behavior or refactor for dependency injection.
|
||||
- Prefer dependency injection when production code is hard to test.
|
||||
- Keep assertion intent explicit and deterministic; avoid flaky timing assumptions.
|
||||
|
||||
## Editing hygiene
|
||||
|
||||
- Keep diffs surgical in test files (touch only lines relevant to the behavior change).
|
||||
- Add imports incrementally rather than replacing entire import blocks.
|
||||
- Match production typing standards in tests: prefer explicit local variable types.
|
||||
- Use `var` in tests only when the type is immediately obvious from a constructor or literal.
|
||||
@@ -0,0 +1,9 @@
|
||||
# AGENTS instructions for `org.ta4j.core` tests
|
||||
|
||||
Follow `ta4j-core/src/test/java/AGENTS.md` for global test policy; this file adds package-specific guidance.
|
||||
|
||||
- Prefer deterministic concurrency tests using `CountDownLatch` / `AtomicBoolean` and bounded executors.
|
||||
- Always apply timeouts on `Future#get` to prevent hanging builds.
|
||||
- Use `MockBarBuilderFactory` for lightweight bar creation when time semantics are not under test.
|
||||
- Assert snapshot semantics on `getBarData()` by verifying returned lists are unmodifiable.
|
||||
- Prefer `NumFactory` convenience methods (`zero()`, `one()`, `two()`, etc.) over `numOf(...)` for common constants.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.backtest.BarSeriesManager;
|
||||
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class BarSeriesManagerConstrainedSeriesTest {
|
||||
|
||||
@Test
|
||||
public void currentCloseModelClosesOpenPositionUsingRawBarsBeyondConstrainedEndIndex() {
|
||||
BarSeries sourceSeries = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(10d, 20d, 30d)
|
||||
.build();
|
||||
NumFactory numFactory = sourceSeries.numFactory();
|
||||
BaseBarSeries constrainedSeries = new BaseBarSeries("constrained-series",
|
||||
List.copyOf(sourceSeries.getBarData()), 0, 1, true, numFactory, new TimeBarBuilderFactory());
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(2));
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(constrainedSeries, new TradeOnCurrentCloseModel())
|
||||
.run(strategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositionCount());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
assertEquals(0, position.getEntry().getIndex());
|
||||
assertEquals(2, position.getExit().getIndex());
|
||||
assertEquals(constrainedSeries.getBar(2).getClosePrice(), position.getExit().getPricePerAsset());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
|
||||
import org.ta4j.core.indicators.helpers.HighPriceIndicator;
|
||||
import org.ta4j.core.indicators.helpers.LowPriceIndicator;
|
||||
import org.ta4j.core.indicators.helpers.PreviousValueIndicator;
|
||||
import org.ta4j.core.mocks.MockBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class BarSeriesTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private BarSeries defaultSeries;
|
||||
|
||||
private BarSeries subSeries;
|
||||
|
||||
private BarSeries emptySeries;
|
||||
|
||||
private String defaultName;
|
||||
|
||||
public BarSeriesTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
defaultName = "Series Name";
|
||||
|
||||
defaultSeries = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withName(defaultName)
|
||||
.withBarBuilderFactory(new MockBarBuilderFactory())
|
||||
.build();
|
||||
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-13T00:00:00Z"))
|
||||
.closePrice(1d)
|
||||
.add();
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-14T00:00:00Z"))
|
||||
.closePrice(2d)
|
||||
.add();
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-15T00:00:00Z"))
|
||||
.closePrice(3d)
|
||||
.add();
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-20T00:00:00Z"))
|
||||
.closePrice(4d)
|
||||
.add();
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-25T00:00:00Z"))
|
||||
.closePrice(5d)
|
||||
.add();
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2014-06-30T00:00:00Z"))
|
||||
.closePrice(6d)
|
||||
.add();
|
||||
|
||||
subSeries = defaultSeries.getSubSeries(2, 5);
|
||||
emptySeries = new BaseBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0, 2, 3, 6), new FixedRule(1, 4, 7, 8));
|
||||
strategy.setUnstableBars(2); // Strategy would need a real test class
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the addBar(bar, boolean) function works correct.
|
||||
*/
|
||||
@Test
|
||||
public void replaceBarTest() {
|
||||
var now = Instant.now();
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(new MockBarBuilderFactory())
|
||||
.build();
|
||||
series.addBar(series.barBuilder().timePeriod(Duration.ofDays(1)).endTime(now).closePrice(1d).build(), true);
|
||||
assertEquals(1, series.getBarCount());
|
||||
assertNumEquals(series.getLastBar().getClosePrice(), series.numFactory().one());
|
||||
|
||||
series.addBar(series.barBuilder().endTime(now.plus(Duration.ofMinutes(1))).closePrice(2d).build(), false);
|
||||
series.addBar(series.barBuilder().endTime(now.plus(Duration.ofMinutes(2))).closePrice(3d).build(), false);
|
||||
assertEquals(3, series.getBarCount());
|
||||
|
||||
assertNumEquals(series.getLastBar().getClosePrice(), series.numFactory().numOf(3));
|
||||
series.addBar(series.barBuilder().endTime(now.plus(Duration.ofMinutes(3))).closePrice(4d).build(), true);
|
||||
series.addBar(series.barBuilder().endTime(now.plus(Duration.ofMinutes(4))).closePrice(5d).build(), true);
|
||||
assertEquals(3, series.getBarCount());
|
||||
|
||||
assertNumEquals(series.getLastBar().getClosePrice(), series.numFactory().numOf(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getEndGetBeginGetBarCountIsEmptyTest() {
|
||||
|
||||
// Default series
|
||||
assertEquals(0, defaultSeries.getBeginIndex());
|
||||
assertEquals(defaultSeries.getBarData().size() - 1, defaultSeries.getEndIndex());
|
||||
assertEquals(defaultSeries.getBarData().size(), defaultSeries.getBarCount());
|
||||
assertFalse(defaultSeries.isEmpty());
|
||||
// Constrained series
|
||||
assertEquals(0, subSeries.getBeginIndex());
|
||||
assertEquals(2, subSeries.getEndIndex());
|
||||
assertEquals(3, subSeries.getBarCount());
|
||||
assertFalse(subSeries.isEmpty());
|
||||
// Empty series
|
||||
assertEquals(-1, emptySeries.getBeginIndex());
|
||||
assertEquals(-1, emptySeries.getEndIndex());
|
||||
assertEquals(0, emptySeries.getBarCount());
|
||||
assertTrue(emptySeries.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBarDataTest() {
|
||||
// Constrained series
|
||||
assertNotEquals(defaultSeries.getBarData(), subSeries.getBarData());
|
||||
// Empty series
|
||||
assertEquals(0, emptySeries.getBarData().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSeriesPeriodDescriptionTest() {
|
||||
var formatter = DateTimeFormatter.ISO_INSTANT;
|
||||
|
||||
// Default series
|
||||
var barData = defaultSeries.getBarData();
|
||||
var defaultDescription = defaultSeries.getSeriesPeriodDescription();
|
||||
var lastEndTimeDefault = barData.get(defaultSeries.getEndIndex()).getEndTime();
|
||||
var firstEndTimeDefault = barData.get(defaultSeries.getBeginIndex()).getEndTime();
|
||||
assertTrue(defaultDescription.endsWith(formatter.format(lastEndTimeDefault)));
|
||||
assertTrue(defaultDescription.startsWith(formatter.format(firstEndTimeDefault)));
|
||||
|
||||
// Constrained series
|
||||
var subSeries = defaultSeries.getSubSeries(2, 4);
|
||||
var subDescription = subSeries.getSeriesPeriodDescription();
|
||||
var lastEndTimeConstrained = subSeries.getLastBar().getEndTime();
|
||||
var firstEndTimeConstrained = subSeries.getFirstBar().getEndTime();
|
||||
assertTrue(subDescription.endsWith(formatter.format(lastEndTimeConstrained)));
|
||||
assertTrue(subDescription.startsWith(formatter.format(firstEndTimeConstrained)));
|
||||
|
||||
// Empty series
|
||||
assertEquals("", emptySeries.getSeriesPeriodDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSeriesPeriodDescriptionTestInSystemTimeZone() {
|
||||
var formatter = DateTimeFormatter.ISO_DATE_TIME;
|
||||
|
||||
// Default series
|
||||
var barData = defaultSeries.getBarData();
|
||||
var defaultDescription = defaultSeries.getSeriesPeriodDescriptionInSystemTimeZone();
|
||||
var lastEndTimeDefault = barData.get(defaultSeries.getEndIndex()).getSystemZonedEndTime();
|
||||
var firstEndTimeDefault = barData.get(defaultSeries.getBeginIndex()).getSystemZonedEndTime();
|
||||
assertTrue(defaultDescription.endsWith(formatter.format(lastEndTimeDefault)));
|
||||
assertTrue(defaultDescription.startsWith(formatter.format(firstEndTimeDefault)));
|
||||
|
||||
// Constrained series
|
||||
var subSeries = defaultSeries.getSubSeries(2, 4);
|
||||
var subDescription = subSeries.getSeriesPeriodDescriptionInSystemTimeZone();
|
||||
var lastEndTimeConstrained = subSeries.getLastBar().getSystemZonedEndTime();
|
||||
var firstEndTimeConstrained = subSeries.getFirstBar().getSystemZonedEndTime();
|
||||
assertTrue(subDescription.endsWith(formatter.format(lastEndTimeConstrained)));
|
||||
assertTrue(subDescription.startsWith(formatter.format(firstEndTimeConstrained)));
|
||||
|
||||
// Empty series
|
||||
assertEquals("", emptySeries.getSeriesPeriodDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNameTest() {
|
||||
assertEquals(defaultName, defaultSeries.getName());
|
||||
assertEquals(defaultName, subSeries.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBarWithRemovedIndexOnMovingSeriesShouldReturnFirstRemainingBarTest() {
|
||||
Bar bar = defaultSeries.getBar(4);
|
||||
defaultSeries.setMaximumBarCount(2);
|
||||
|
||||
assertSame(bar, defaultSeries.getBar(0));
|
||||
assertSame(bar, defaultSeries.getBar(1));
|
||||
assertSame(bar, defaultSeries.getBar(2));
|
||||
assertSame(bar, defaultSeries.getBar(3));
|
||||
assertSame(bar, defaultSeries.getBar(4));
|
||||
assertNotSame(bar, defaultSeries.getBar(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modificationsOnOriginalListShouldNotAffectBarSeries() {
|
||||
defaultSeries.setMaximumBarCount(2);
|
||||
assertEquals(2, defaultSeries.getBarCount());
|
||||
assertNumEquals(5, defaultSeries.getBar(1).getClosePrice());
|
||||
}
|
||||
|
||||
@Test(expected = IndexOutOfBoundsException.class)
|
||||
public void getBarWithNegativeIndexShouldThrowExceptionTest() {
|
||||
defaultSeries.getBar(-1);
|
||||
}
|
||||
|
||||
@Test(expected = IndexOutOfBoundsException.class)
|
||||
public void getBarWithIndexGreaterThanBarCountShouldThrowExceptionTest() {
|
||||
defaultSeries.getBar(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBarOnMovingSeriesTest() {
|
||||
Bar bar = defaultSeries.getBar(4);
|
||||
defaultSeries.setMaximumBarCount(2);
|
||||
assertEquals(bar, defaultSeries.getBar(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSeriesCreationTest() {
|
||||
BarSeries subSeries = defaultSeries.getSubSeries(2, 5);
|
||||
assertEquals(3, subSeries.getBarCount());
|
||||
assertEquals(defaultSeries.getName(), subSeries.getName());
|
||||
assertEquals(0, subSeries.getBeginIndex());
|
||||
assertEquals(defaultSeries.getBeginIndex(), subSeries.getBeginIndex());
|
||||
assertEquals(2, subSeries.getEndIndex());
|
||||
assertNotEquals(defaultSeries.getEndIndex(), subSeries.getEndIndex());
|
||||
assertEquals(3, subSeries.getBarCount());
|
||||
|
||||
subSeries = defaultSeries.getSubSeries(0, 1000);
|
||||
assertEquals(0, subSeries.getBeginIndex());
|
||||
assertEquals(defaultSeries.getBarCount(), subSeries.getBarCount());
|
||||
assertEquals(defaultSeries.getEndIndex(), subSeries.getEndIndex());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void subSeriesCreationWithNegativeIndexTest() {
|
||||
defaultSeries.getSubSeries(-1000, 1000);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void subSeriesWithWrongArgumentsTest() {
|
||||
defaultSeries.getSubSeries(10, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maximumBarCountOnConstrainedSeriesShouldNotThrowExceptionTest() {
|
||||
try {
|
||||
subSeries.setMaximumBarCount(10);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("setMaximumBarCount onConstrained series should not throw Exception");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void negativeMaximumBarCountShouldThrowExceptionTest() {
|
||||
defaultSeries.setMaximumBarCount(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMaximumBarCountTest() {
|
||||
// Before
|
||||
assertEquals(0, defaultSeries.getBeginIndex());
|
||||
assertEquals(defaultSeries.getBarData().size() - 1, defaultSeries.getEndIndex());
|
||||
assertEquals(defaultSeries.getBarData().size(), defaultSeries.getBarCount());
|
||||
|
||||
defaultSeries.setMaximumBarCount(3);
|
||||
|
||||
// After
|
||||
assertEquals(3, defaultSeries.getBeginIndex());
|
||||
assertEquals(5, defaultSeries.getEndIndex());
|
||||
assertEquals(3, defaultSeries.getBarCount());
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void addNullBarShouldThrowExceptionTest() {
|
||||
defaultSeries.addBar(null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void addBarWithEndTimePriorToSeriesEndTimeShouldThrowExceptionTest() {
|
||||
defaultSeries.addBar(
|
||||
defaultSeries.barBuilder().endTime(Instant.parse("2000-01-01T00:00:00Z")).closePrice(99d).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addBarTest() {
|
||||
defaultSeries = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(new MockBarBuilderFactory())
|
||||
.build();
|
||||
|
||||
Bar bar1 = defaultSeries.barBuilder().endTime(Instant.parse("2014-06-13T00:00:00Z")).closePrice(1d).build();
|
||||
|
||||
Bar bar2 = defaultSeries.barBuilder().endTime(Instant.parse("2014-06-14T00:00:00Z")).closePrice(2d).build();
|
||||
|
||||
assertEquals(0, defaultSeries.getBarCount());
|
||||
assertEquals(-1, defaultSeries.getBeginIndex());
|
||||
assertEquals(-1, defaultSeries.getEndIndex());
|
||||
|
||||
defaultSeries.addBar(bar1);
|
||||
assertEquals(1, defaultSeries.getBarCount());
|
||||
assertEquals(0, defaultSeries.getBeginIndex());
|
||||
assertEquals(0, defaultSeries.getEndIndex());
|
||||
|
||||
defaultSeries.addBar(bar2);
|
||||
assertEquals(2, defaultSeries.getBarCount());
|
||||
assertEquals(0, defaultSeries.getBeginIndex());
|
||||
assertEquals(1, defaultSeries.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPriceTest() {
|
||||
var cp = new ClosePriceIndicator(defaultSeries);
|
||||
var mxPrice = new HighPriceIndicator(defaultSeries);
|
||||
var mnPrice = new LowPriceIndicator(defaultSeries);
|
||||
var prevValue = new PreviousValueIndicator(cp, 1);
|
||||
|
||||
Num adding1 = numOf(100);
|
||||
Num prevClose = defaultSeries.getBar(defaultSeries.getEndIndex() - 1).getClosePrice();
|
||||
Num currentMax = mxPrice.getValue(defaultSeries.getEndIndex());
|
||||
Num currentMin = mnPrice.getValue(defaultSeries.getEndIndex());
|
||||
Num currentClose = cp.getValue(defaultSeries.getEndIndex());
|
||||
|
||||
assertNumEquals(currentClose, defaultSeries.getLastBar().getClosePrice());
|
||||
defaultSeries.addPrice(adding1);
|
||||
assertNumEquals(adding1, cp.getValue(defaultSeries.getEndIndex())); // adding1 is new close
|
||||
assertNull(currentMax);
|
||||
assertNumEquals(adding1, mxPrice.getValue(defaultSeries.getEndIndex())); // adding1 also new max
|
||||
assertNull(currentMin);
|
||||
assertNumEquals(adding1, mnPrice.getValue(defaultSeries.getEndIndex())); // adding1 also new min
|
||||
assertNumEquals(prevClose, prevValue.getValue(defaultSeries.getEndIndex())); // previous close stays
|
||||
|
||||
Num adding2 = numOf(0);
|
||||
defaultSeries.addPrice(adding2);
|
||||
assertNumEquals(adding2, cp.getValue(defaultSeries.getEndIndex())); // adding2 is new close
|
||||
assertNumEquals(adding1, mxPrice.getValue(defaultSeries.getEndIndex())); // max stays 100
|
||||
assertNumEquals(adding2, mnPrice.getValue(defaultSeries.getEndIndex())); // min is new adding2
|
||||
assertNumEquals(prevClose, prevValue.getValue(defaultSeries.getEndIndex())); // previous close stays
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the {@link BaseBarSeries#addTrade(Number, Number)} method works
|
||||
* correct.
|
||||
*/
|
||||
@Test
|
||||
public void addTradeTest() {
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(new MockBarBuilderFactory())
|
||||
.build();
|
||||
series.barBuilder().closePrice(1d).volume(0).amount(0).add();
|
||||
series.addTrade(200, 11.5);
|
||||
assertNumEquals(series.numFactory().numOf(200), series.getLastBar().getVolume());
|
||||
assertNumEquals(series.numFactory().numOf(11.5), series.getLastBar().getClosePrice());
|
||||
series.addTrade(BigDecimal.valueOf(200), BigDecimal.valueOf(100));
|
||||
assertNumEquals(series.numFactory().numOf(400), series.getLastBar().getVolume());
|
||||
assertNumEquals(series.numFactory().numOf(100), series.getLastBar().getClosePrice());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void wrongBarTypeDoubleTest() {
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance()).build();
|
||||
series.addBar(new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.now())
|
||||
.closePrice(DecimalNumFactory.getInstance().one())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void wrongBarTypeBigDecimalTest() {
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance()).build();
|
||||
series.addBar(new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.now())
|
||||
.closePrice(DoubleNumFactory.getInstance().one())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSeriesOfMaxBarCountSeriesTest() {
|
||||
var maxBarCount = 20;
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withName("Series with maxBar count")
|
||||
.withMaxBarCount(maxBarCount)
|
||||
.build();
|
||||
|
||||
var timespan = 5;
|
||||
var now = Instant.now();
|
||||
|
||||
IntStream.range(0, 100).forEach(i -> {
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(now.plus(Duration.ofMinutes(i)))
|
||||
.openPrice(5)
|
||||
.highPrice(7)
|
||||
.lowPrice(1)
|
||||
.closePrice(5)
|
||||
.volume(i)
|
||||
.add();
|
||||
var startIndex = Math.max(series.getBeginIndex(), series.getEndIndex() - timespan + 1);
|
||||
var endIndex = i + 1;
|
||||
var subSeries = series.getSubSeries(startIndex, endIndex);
|
||||
assertEquals(subSeries.getBarCount(), endIndex - startIndex);
|
||||
assertEquals(maxBarCount, subSeries.getMaximumBarCount());
|
||||
|
||||
var subSeriesLastBar = subSeries.getLastBar();
|
||||
var seriesLastBar = series.getLastBar();
|
||||
assertEquals(subSeriesLastBar.getVolume(), seriesLastBar.getVolume());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSeriesOfMaxBarCountSeriesNoBarsTest() {
|
||||
var empty = "empty";
|
||||
var maximumBarCount = 42;
|
||||
var series = new BaseBarSeriesBuilder().withName(empty)
|
||||
.withNumFactory(numFactory)
|
||||
.withMaxBarCount(maximumBarCount)
|
||||
.build();
|
||||
var subSeries = series.getSubSeries(0, 1);
|
||||
|
||||
assertEquals(empty, subSeries.getName());
|
||||
assertSame(numFactory, subSeries.numFactory());
|
||||
assertEquals(0, subSeries.getBarCount());
|
||||
assertEquals(maximumBarCount, subSeries.getMaximumBarCount());
|
||||
assertEquals(maximumBarCount, series.getMaximumBarCount());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class BarTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private Bar bar;
|
||||
|
||||
private Instant beginTime;
|
||||
|
||||
private Instant endTime;
|
||||
|
||||
public BarTest(final NumFactory numFactory) {
|
||||
super(null, numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.beginTime = Instant.parse("2014-06-25T00:00:00Z");
|
||||
this.endTime = Instant.parse("2014-06-25T01:00:00Z");
|
||||
this.bar = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBars() {
|
||||
var barByBeginTime = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.beginTime(this.beginTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
|
||||
var barByEndTime = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
|
||||
var barByBeginTimeAndEndTime = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.beginTime(this.beginTime)
|
||||
.endTime(this.endTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
|
||||
var barWithoutTimePeriod = new TimeBarBuilder(this.numFactory).beginTime(this.beginTime)
|
||||
.endTime(this.endTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
|
||||
assertEquals(barByBeginTime.getBeginTime(), barByEndTime.getBeginTime());
|
||||
assertEquals(barByBeginTime.getEndTime(), barByEndTime.getEndTime());
|
||||
assertEquals(barByBeginTimeAndEndTime.getTimePeriod(), barWithoutTimePeriod.getTimePeriod());
|
||||
assertEquals(barByBeginTimeAndEndTime.getTimePeriod(), Duration.between(beginTime, endTime));
|
||||
assertNotEquals(barByBeginTimeAndEndTime.getTimePeriod(), Duration.between(endTime, beginTime));
|
||||
assertEquals(barWithoutTimePeriod.getTimePeriod(), Duration.between(beginTime, endTime));
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
@SuppressWarnings("unused")
|
||||
public void createBarsWithMissingBeginTime() {
|
||||
// TimePeriod is not given and cannot be computed due to missing beginTime.
|
||||
var bar = new TimeBarBuilder(this.numFactory).endTime(endTime).volume(0).amount(0).build();
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
@SuppressWarnings("unused")
|
||||
public void createBarsWithMissingEndTime() {
|
||||
// TimePeriod is not given and cannot be computed due to missing endTime.
|
||||
var bar = new TimeBarBuilder(this.numFactory).beginTime(beginTime).volume(0).amount(0).build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@SuppressWarnings("unused")
|
||||
public void createBarsWithInvalidTimePeriod() {
|
||||
var barByBeginTime = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(2))
|
||||
.beginTime(this.beginTime)
|
||||
.endTime(this.endTime)
|
||||
.volume(0)
|
||||
.amount(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrades() {
|
||||
|
||||
this.bar.addTrade(numOf(3.0), numOf(200.0));
|
||||
this.bar.addTrade(numOf(4.0), numOf(201.0));
|
||||
this.bar.addTrade(numOf(2.0), numOf(198.0));
|
||||
|
||||
assertEquals(3, this.bar.getTrades());
|
||||
assertEquals(numOf(3 * 200 + 4 * 201 + 2 * 198), this.bar.getAmount());
|
||||
assertEquals(numOf(200), this.bar.getOpenPrice());
|
||||
assertEquals(numOf(198), this.bar.getClosePrice());
|
||||
assertEquals(numOf(198), this.bar.getLowPrice());
|
||||
assertEquals(numOf(201), this.bar.getHighPrice());
|
||||
assertEquals(numOf(9), this.bar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTimePeriod() {
|
||||
assertEquals(this.beginTime, this.bar.getEndTime().minus(this.bar.getTimePeriod()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBeginTime() {
|
||||
assertEquals(this.beginTime, this.bar.getBeginTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDateName() {
|
||||
assertNotNull(bar.getDateName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSimpleDateName() {
|
||||
assertNotNull(bar.getSimpleDateName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inPeriod() {
|
||||
assertFalse(this.bar.inPeriod(null));
|
||||
|
||||
ZonedDateTime zonedBeginTime = beginTime.atZone(ZoneOffset.UTC);
|
||||
assertFalse(bar.inPeriod(zonedBeginTime.withDayOfMonth(24).toInstant()));
|
||||
assertFalse(bar.inPeriod(zonedBeginTime.withDayOfMonth(26).toInstant()));
|
||||
assertTrue(bar.inPeriod(zonedBeginTime.withMinute(30).toInstant()));
|
||||
|
||||
assertTrue(this.bar.inPeriod(this.beginTime));
|
||||
assertFalse(this.bar.inPeriod(this.endTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotThrowNullPointerException() {
|
||||
var bar = new TimeBarBuilder().timePeriod(Duration.ofHours(1)).endTime(endTime).build();
|
||||
// TODO use Junit5: org.junit.jupiter.api.Assertions.assertDoesNotThrow instead:
|
||||
assertNotNull(bar.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals() {
|
||||
final Bar bar1 = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.build();
|
||||
final Bar bar2 = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.build();
|
||||
|
||||
assertEquals(bar1, bar2);
|
||||
assertNotSame(bar1, bar2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCode2() {
|
||||
final Bar bar1 = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.build();
|
||||
final Bar bar2 = new TimeBarBuilder(this.numFactory).timePeriod(Duration.ofHours(1))
|
||||
.endTime(this.endTime)
|
||||
.build();
|
||||
|
||||
assertEquals(bar1.hashCode(), bar2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numFactoryPrefersOpenPrice() {
|
||||
var bar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofSeconds(1))
|
||||
.beginTime(Instant.now())
|
||||
.openPrice(1)
|
||||
.closePrice(2)
|
||||
.build();
|
||||
|
||||
assertSame(bar.getOpenPrice().getClass(), bar.numFactory().one().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numFactoryFallsBackToClosePrice() {
|
||||
var bar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofSeconds(1))
|
||||
.beginTime(Instant.now())
|
||||
.closePrice(2)
|
||||
.build();
|
||||
|
||||
assertSame(bar.getClosePrice().getClass(), bar.numFactory().one().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numFactoryThrowsWhenNoPricesAvailable() {
|
||||
var bar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofSeconds(1)).beginTime(Instant.now()).build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, bar::numFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class BaseBarSeriesBuilderTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public BaseBarSeriesBuilderTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildBigDecimal() {
|
||||
|
||||
final Instant beginTime = Instant.parse("2014-06-25T00:00:00Z");
|
||||
final Instant endTime = Instant.parse("2014-06-25T01:00:00Z");
|
||||
final Duration duration = Duration.between(beginTime, endTime);
|
||||
|
||||
final var series = new BaseBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
final var bar = series.barBuilder()
|
||||
.timePeriod(duration)
|
||||
.endTime(endTime)
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
assertEquals(duration, bar.getTimePeriod());
|
||||
assertEquals(beginTime, bar.getBeginTime());
|
||||
assertEquals(endTime, bar.getEndTime());
|
||||
assertNumEquals(numOf(101.0), bar.getOpenPrice());
|
||||
assertNumEquals(numOf(103), bar.getHighPrice());
|
||||
assertNumEquals(numOf(100), bar.getLowPrice());
|
||||
assertNumEquals(numOf(102), bar.getClosePrice());
|
||||
assertEquals(4, bar.getTrades());
|
||||
assertNumEquals(numOf(40), bar.getVolume());
|
||||
assertNumEquals(numOf(4020), bar.getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithBars() {
|
||||
|
||||
// When we create a series with predefined bars, we need to make sure that the
|
||||
// NumFactory of the series and the NumFactory of the bars are the same.
|
||||
|
||||
final NumFactory doubleNumFactory = DoubleNumFactory.getInstance();
|
||||
|
||||
final Instant beginTime = Instant.parse("2014-06-25T00:00:00Z");
|
||||
final Instant endTime = Instant.parse("2014-06-25T01:00:00Z");
|
||||
final Duration duration = Duration.between(beginTime, endTime);
|
||||
|
||||
// we build bars with DoubleNumFactory
|
||||
final var bar1 = new TimeBarBuilder(doubleNumFactory).timePeriod(duration)
|
||||
.endTime(endTime)
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
final var bars = new ArrayList<Bar>();
|
||||
bars.add(bar1);
|
||||
|
||||
// User does not assign a numFactory, therefore use the numFactory of the bars
|
||||
// instead of the default
|
||||
final var series = new BaseBarSeriesBuilder().withBars(bars).build();
|
||||
|
||||
// We add another bar with NumFactory assigned to series
|
||||
final var bar2 = series.barBuilder()
|
||||
.timePeriod(duration)
|
||||
.endTime(endTime.plus(duration))
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
series.addBar(bar2);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
|
||||
assertEquals(doubleNumFactory, series.numFactory());
|
||||
assertEquals(doubleNumFactory, bar1.getClosePrice().getNumFactory());
|
||||
assertEquals(doubleNumFactory, bar2.getClosePrice().getNumFactory());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@SuppressWarnings("unused")
|
||||
public void testBuildWithBarsAndWithNumFactory() {
|
||||
|
||||
// When we create a series with predefined bars, we need to make sure that the
|
||||
// NumFactory of the series and the NumFactory of the bars are the same.
|
||||
|
||||
final NumFactory doubleNumFactory = DoubleNumFactory.getInstance();
|
||||
final NumFactory decimalNumFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
final Instant beginTime = Instant.parse("2014-06-25T00:00:00Z");
|
||||
final Instant endTime = Instant.parse("2014-06-25T01:00:00Z");
|
||||
final Duration duration = Duration.between(beginTime, endTime);
|
||||
|
||||
// we build bars with DoubleNumFactory
|
||||
final var bar1 = new TimeBarBuilder(doubleNumFactory).timePeriod(duration)
|
||||
.endTime(endTime)
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
final var bars = new ArrayList<Bar>();
|
||||
bars.add(bar1);
|
||||
|
||||
// The user explicitly assigns DecimalNumFactory to the series, but the bar
|
||||
// uses DoubleNumFactory, therefore throw an exception.
|
||||
final var series = new BaseBarSeriesBuilder().withNumFactory(decimalNumFactory).withBars(bars).build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@SuppressWarnings("unused")
|
||||
public void testBuildWithBarsWithDifferentNumFactory() {
|
||||
|
||||
// When we create a series with predefined bars, we need to make sure that the
|
||||
// NumFactory of all the bars are the same.
|
||||
|
||||
final NumFactory doubleNumFactory = DoubleNumFactory.getInstance();
|
||||
final NumFactory decimalNumFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
final Instant beginTime = Instant.parse("2014-06-25T00:00:00Z");
|
||||
final Instant endTime = Instant.parse("2014-06-25T01:00:00Z");
|
||||
final Duration duration = Duration.between(beginTime, endTime);
|
||||
|
||||
// we build bars with DoubleNumFactory
|
||||
final var bar1 = new TimeBarBuilder(doubleNumFactory).timePeriod(duration)
|
||||
.endTime(endTime)
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
// we build bars with DecimalNumFactory
|
||||
final var bar2 = new TimeBarBuilder(decimalNumFactory).timePeriod(duration)
|
||||
.endTime(endTime)
|
||||
.openPrice(BigDecimal.valueOf(101.0))
|
||||
.highPrice(BigDecimal.valueOf(103))
|
||||
.lowPrice(BigDecimal.valueOf(100))
|
||||
.closePrice(BigDecimal.valueOf(102))
|
||||
.trades(4)
|
||||
.volume(BigDecimal.valueOf(40))
|
||||
.amount(BigDecimal.valueOf(4020))
|
||||
.build();
|
||||
|
||||
final var bars = new ArrayList<Bar>();
|
||||
bars.add(bar1);
|
||||
bars.add(bar2);
|
||||
|
||||
// bar1 and bar2 have different numFactories, therefore throw an exception.
|
||||
final var series = new BaseBarSeriesBuilder().withBars(bars).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BaseBarSeries}.
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public class BaseBarSeriesTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private BarBuilderFactory barBuilderFactory;
|
||||
private List<Bar> testBars;
|
||||
private BaseBarSeries emptySeries;
|
||||
private BaseBarSeries seriesWithBars;
|
||||
private BaseBarSeries constrainedSeries;
|
||||
|
||||
public BaseBarSeriesTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
barBuilderFactory = new MockBarBuilderFactory();
|
||||
|
||||
// Create test bars
|
||||
testBars = new ArrayList<>();
|
||||
Instant baseTime = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Bar bar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(baseTime.plus(Duration.ofDays(i)))
|
||||
.openPrice(numOf(i + 1))
|
||||
.highPrice(numOf(i + 2))
|
||||
.lowPrice(numOf(i))
|
||||
.closePrice(numOf(i + 1.5))
|
||||
.volume(numOf(i * 100))
|
||||
.amount(numOf(i * 1000))
|
||||
.trades(i * 10)
|
||||
.build();
|
||||
testBars.add(bar);
|
||||
}
|
||||
|
||||
// Create test series
|
||||
emptySeries = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withName("EmptySeries")
|
||||
.build();
|
||||
|
||||
seriesWithBars = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withName("TestSeries")
|
||||
.withBars(new ArrayList<>(testBars))
|
||||
.build();
|
||||
|
||||
// Create constrained series directly using constructor to avoid builder issue
|
||||
constrainedSeries = new BaseBarSeries("ConstrainedSeries", new ArrayList<>(testBars), 0, testBars.size() - 1,
|
||||
true, numFactory, barBuilderFactory);
|
||||
}
|
||||
|
||||
// ==================== Constructor Tests ====================
|
||||
|
||||
@Test
|
||||
public void testConvenienceConstructor() {
|
||||
BaseBarSeries series = new BaseBarSeries("TestName", testBars);
|
||||
|
||||
assertEquals("TestName", series.getName());
|
||||
assertEquals(5, series.getBarCount());
|
||||
assertEquals(0, series.getBeginIndex());
|
||||
assertEquals(4, series.getEndIndex());
|
||||
assertEquals(DecimalNumFactory.getInstance().getClass(), series.numFactory().getClass());
|
||||
assertFalse(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvenienceConstructorWithEmptyBars() {
|
||||
BaseBarSeries series = new BaseBarSeries("TestName", Collections.emptyList());
|
||||
|
||||
assertEquals("TestName", series.getName());
|
||||
assertEquals(0, series.getBarCount());
|
||||
assertEquals(-1, series.getBeginIndex());
|
||||
assertEquals(-1, series.getEndIndex());
|
||||
assertTrue(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullConstructor() {
|
||||
BaseBarSeries series = new BaseBarSeries("TestName", testBars, 1, 3, true, numFactory, barBuilderFactory);
|
||||
|
||||
assertEquals("TestName", series.getName());
|
||||
assertEquals(3, series.getBarCount());
|
||||
assertEquals(1, series.getBeginIndex());
|
||||
assertEquals(3, series.getEndIndex());
|
||||
assertSame(numFactory, series.numFactory());
|
||||
assertFalse(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullConstructorWithEmptyBars() {
|
||||
BaseBarSeries series = new BaseBarSeries("TestName", Collections.emptyList(), 0, 0, false, numFactory,
|
||||
barBuilderFactory);
|
||||
|
||||
assertEquals("TestName", series.getName());
|
||||
assertEquals(0, series.getBarCount());
|
||||
assertEquals(-1, series.getBeginIndex());
|
||||
assertEquals(-1, series.getEndIndex());
|
||||
assertTrue(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullConstructorWithInvalidEndIndex() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new BaseBarSeries("TestName", testBars, 3, 1, // endIndex < beginIndex - 1 (1 < 3 - 1 = 1 < 2)
|
||||
false, numFactory, barBuilderFactory);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullConstructorWithEndIndexTooLarge() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new BaseBarSeries("TestName", testBars, 0, 10, // endIndex >= bars.size()
|
||||
false, numFactory, barBuilderFactory);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullConstructorWithNullBarBuilderFactory() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
new BaseBarSeries("TestName", testBars, 0, 4, false, numFactory, null);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Basic Property Tests ====================
|
||||
|
||||
@Test
|
||||
public void testGetName() {
|
||||
assertEquals("EmptySeries", emptySeries.getName());
|
||||
assertEquals("TestSeries", seriesWithBars.getName());
|
||||
assertEquals("ConstrainedSeries", constrainedSeries.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumFactory() {
|
||||
assertEquals(numFactory.getClass(), seriesWithBars.numFactory().getClass());
|
||||
assertEquals(numFactory.getClass(), emptySeries.numFactory().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBarBuilder() {
|
||||
BarBuilder builder = seriesWithBars.barBuilder();
|
||||
assertNotNull(builder);
|
||||
assertTrue(builder instanceof TimeBarBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBarData() {
|
||||
List<Bar> barData = seriesWithBars.getBarData();
|
||||
assertNotNull(barData);
|
||||
assertEquals(5, barData.size());
|
||||
|
||||
// Test that it returns a copy (immutability)
|
||||
List<Bar> originalData = seriesWithBars.getBarData();
|
||||
int originalSize = originalData.size();
|
||||
List<Bar> modifiedData = new ArrayList<>(originalData);
|
||||
modifiedData.add(testBars.get(0));
|
||||
|
||||
// The copy should be modified, while the original series data remains unchanged
|
||||
assertEquals(originalSize + 1, modifiedData.size());
|
||||
assertEquals(originalSize, seriesWithBars.getBarData().size());
|
||||
}
|
||||
|
||||
// ==================== Bar Access Tests ====================
|
||||
|
||||
@Test
|
||||
public void testGetBarValidIndex() {
|
||||
Bar bar = seriesWithBars.getBar(0);
|
||||
assertNotNull(bar);
|
||||
assertNumEquals(1.5, bar.getClosePrice());
|
||||
|
||||
bar = seriesWithBars.getBar(4);
|
||||
assertNotNull(bar);
|
||||
assertNumEquals(5.5, bar.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBarNegativeIndex() {
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> {
|
||||
seriesWithBars.getBar(-1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBarIndexTooLarge() {
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> {
|
||||
seriesWithBars.getBar(10);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBarOnEmptySeries() {
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> {
|
||||
emptySeries.getBar(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBarWithRemovedBars() {
|
||||
// Set maximum bar count to trigger removal
|
||||
seriesWithBars.setMaximumBarCount(3);
|
||||
|
||||
// After removal, we should have 3 bars remaining
|
||||
assertEquals(3, seriesWithBars.getBarCount());
|
||||
assertEquals(2, seriesWithBars.getRemovedBarsCount());
|
||||
|
||||
// The remaining bars should be the last 3 bars from the original series
|
||||
Bar firstRemainingBar = seriesWithBars.getBar(2); // This should be the 3rd bar (index 2)
|
||||
Bar secondRemainingBar = seriesWithBars.getBar(3); // This should be the 4th bar (index 3)
|
||||
Bar thirdRemainingBar = seriesWithBars.getBar(4); // This should be the 5th bar (index 4)
|
||||
|
||||
// Verify we get different bars for different indices
|
||||
assertNotSame(firstRemainingBar, secondRemainingBar);
|
||||
assertNotSame(secondRemainingBar, thirdRemainingBar);
|
||||
assertNotSame(firstRemainingBar, thirdRemainingBar);
|
||||
|
||||
// Verify the bars are the correct ones from the original series
|
||||
assertSame(testBars.get(2), firstRemainingBar);
|
||||
assertSame(testBars.get(3), secondRemainingBar);
|
||||
assertSame(testBars.get(4), thirdRemainingBar);
|
||||
}
|
||||
|
||||
// ==================== Bar Counting Tests ====================
|
||||
|
||||
@Test
|
||||
public void testGetBarCount() {
|
||||
assertEquals(0, emptySeries.getBarCount());
|
||||
assertEquals(5, seriesWithBars.getBarCount());
|
||||
assertEquals(5, constrainedSeries.getBarCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBeginIndex() {
|
||||
assertEquals(-1, emptySeries.getBeginIndex());
|
||||
assertEquals(0, seriesWithBars.getBeginIndex());
|
||||
assertEquals(0, constrainedSeries.getBeginIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEndIndex() {
|
||||
assertEquals(-1, emptySeries.getEndIndex());
|
||||
assertEquals(4, seriesWithBars.getEndIndex());
|
||||
assertEquals(4, constrainedSeries.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsEmpty() {
|
||||
assertTrue(emptySeries.isEmpty());
|
||||
assertFalse(seriesWithBars.isEmpty());
|
||||
assertFalse(constrainedSeries.isEmpty());
|
||||
}
|
||||
|
||||
// ==================== SubSeries Tests ====================
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesValidRange() {
|
||||
BaseBarSeries subSeries = seriesWithBars.getSubSeries(1, 4);
|
||||
|
||||
assertEquals("TestSeries", subSeries.getName());
|
||||
assertEquals(3, subSeries.getBarCount());
|
||||
assertEquals(0, subSeries.getBeginIndex());
|
||||
assertEquals(2, subSeries.getEndIndex());
|
||||
|
||||
// Verify the bars are correct
|
||||
assertNumEquals(2.5, subSeries.getBar(0).getClosePrice());
|
||||
assertNumEquals(3.5, subSeries.getBar(1).getClosePrice());
|
||||
assertNumEquals(4.5, subSeries.getBar(2).getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesFromEmptySeries() {
|
||||
BaseBarSeries subSeries = emptySeries.getSubSeries(0, 1);
|
||||
|
||||
assertEquals("EmptySeries", subSeries.getName());
|
||||
assertEquals(0, subSeries.getBarCount());
|
||||
assertEquals(-1, subSeries.getBeginIndex());
|
||||
assertEquals(-1, subSeries.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesWithLargeEndIndex() {
|
||||
BaseBarSeries subSeries = seriesWithBars.getSubSeries(2, 1000);
|
||||
|
||||
assertEquals(3, subSeries.getBarCount());
|
||||
assertEquals(0, subSeries.getBeginIndex());
|
||||
assertEquals(2, subSeries.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesNegativeStartIndex() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
seriesWithBars.getSubSeries(-1, 3);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesStartIndexGreaterThanEndIndex() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
seriesWithBars.getSubSeries(3, 2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubSeriesStartIndexEqualsEndIndex() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
seriesWithBars.getSubSeries(2, 2);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Add Bar Tests ====================
|
||||
|
||||
@Test
|
||||
public void testAddBarToEmptySeries() {
|
||||
Bar newBar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-06T00:00:00Z"))
|
||||
.closePrice(numOf(10.0))
|
||||
.build();
|
||||
|
||||
emptySeries.addBar(newBar);
|
||||
|
||||
assertEquals(1, emptySeries.getBarCount());
|
||||
assertEquals(0, emptySeries.getBeginIndex());
|
||||
assertEquals(0, emptySeries.getEndIndex());
|
||||
assertSame(newBar, emptySeries.getBar(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBarToNonEmptySeries() {
|
||||
Bar newBar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-06T00:00:00Z"))
|
||||
.closePrice(numOf(10.0))
|
||||
.build();
|
||||
|
||||
int originalCount = seriesWithBars.getBarCount();
|
||||
seriesWithBars.addBar(newBar);
|
||||
|
||||
assertEquals(originalCount + 1, seriesWithBars.getBarCount());
|
||||
assertEquals(0, seriesWithBars.getBeginIndex());
|
||||
assertEquals(5, seriesWithBars.getEndIndex());
|
||||
assertSame(newBar, seriesWithBars.getBar(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBarWithReplace() {
|
||||
Bar originalBar = seriesWithBars.getBar(4);
|
||||
Bar replacementBar = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-05T00:00:00Z"))
|
||||
.closePrice(numOf(99.0))
|
||||
.build();
|
||||
|
||||
int originalCount = seriesWithBars.getBarCount();
|
||||
seriesWithBars.addBar(replacementBar, true);
|
||||
|
||||
assertEquals(originalCount, seriesWithBars.getBarCount());
|
||||
assertSame(replacementBar, seriesWithBars.getBar(4));
|
||||
assertNotSame(originalBar, seriesWithBars.getBar(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNullBar() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
seriesWithBars.addBar(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBarWithWrongNumType() {
|
||||
// Use the opposite NumFactory to ensure type mismatch
|
||||
NumFactory wrongFactory = (numFactory instanceof DoubleNumFactory) ? DecimalNumFactory.getInstance()
|
||||
: DoubleNumFactory.getInstance();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
Bar barWithWrongType = new TimeBarBuilder(wrongFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-06T00:00:00Z"))
|
||||
.closePrice(wrongFactory.numOf(10.0))
|
||||
.build();
|
||||
|
||||
seriesWithBars.addBar(barWithWrongType);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBarWithEndTimeNotAfterSeriesEndTime() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
Bar barWithOldTime = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2023-12-31T00:00:00Z")) // Before series end time
|
||||
.closePrice(numOf(10.0))
|
||||
.build();
|
||||
|
||||
seriesWithBars.addBar(barWithOldTime);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBarWithEndTimeEqualToSeriesEndTime() {
|
||||
Bar barWithSameTime = new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(Instant.parse("2024-01-05T00:00:00Z")) // Same as last bar's end time
|
||||
.closePrice(numOf(10.0))
|
||||
.build();
|
||||
|
||||
try {
|
||||
seriesWithBars.addBar(barWithSameTime);
|
||||
fail("Should have thrown IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("Cannot add a bar with end time"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Add Trade Tests ====================
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithNumbers() {
|
||||
seriesWithBars.addTrade(100, 50.5);
|
||||
|
||||
Bar lastBar = seriesWithBars.getLastBar();
|
||||
assertNumEquals(500, lastBar.getVolume()); // Original volume (4*100) + 100
|
||||
assertNumEquals(50.5, lastBar.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithNums() {
|
||||
Num volume = numOf(200);
|
||||
Num price = numOf(75.25);
|
||||
|
||||
seriesWithBars.addTrade(volume, price);
|
||||
|
||||
Bar lastBar = seriesWithBars.getLastBar();
|
||||
assertNumEquals(600, lastBar.getVolume()); // Original volume (4*100) + 200
|
||||
assertNumEquals(75.25, lastBar.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithBigDecimal() {
|
||||
// Create a fresh series for this test to avoid interference from other tests
|
||||
BaseBarSeries freshSeries = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withName("FreshSeries")
|
||||
.withBars(new ArrayList<>(testBars))
|
||||
.build();
|
||||
|
||||
freshSeries.addTrade(BigDecimal.valueOf(150), BigDecimal.valueOf(60.75));
|
||||
|
||||
Bar lastBar = freshSeries.getLastBar();
|
||||
assertNumEquals(550, lastBar.getVolume()); // Original volume (4*100) + 150
|
||||
assertNumEquals(60.75, lastBar.getClosePrice());
|
||||
}
|
||||
|
||||
// ==================== Add Price Tests ====================
|
||||
|
||||
@Test
|
||||
public void testAddPrice() {
|
||||
Num newPrice = numOf(99.99);
|
||||
|
||||
seriesWithBars.addPrice(newPrice);
|
||||
|
||||
Bar lastBar = seriesWithBars.getLastBar();
|
||||
assertNumEquals(99.99, lastBar.getClosePrice());
|
||||
}
|
||||
|
||||
// ==================== Maximum Bar Count Tests ====================
|
||||
|
||||
@Test
|
||||
public void testSetMaximumBarCount() {
|
||||
seriesWithBars.setMaximumBarCount(3);
|
||||
|
||||
assertEquals(3, seriesWithBars.getMaximumBarCount());
|
||||
assertEquals(3, seriesWithBars.getBarCount());
|
||||
assertEquals(2, seriesWithBars.getBeginIndex());
|
||||
assertEquals(4, seriesWithBars.getEndIndex());
|
||||
assertEquals(2, seriesWithBars.getRemovedBarsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMaximumBarCountLargerThanCurrent() {
|
||||
seriesWithBars.setMaximumBarCount(10);
|
||||
|
||||
assertEquals(10, seriesWithBars.getMaximumBarCount());
|
||||
assertEquals(5, seriesWithBars.getBarCount());
|
||||
assertEquals(0, seriesWithBars.getBeginIndex());
|
||||
assertEquals(4, seriesWithBars.getEndIndex());
|
||||
assertEquals(0, seriesWithBars.getRemovedBarsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMaximumBarCountZero() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
seriesWithBars.setMaximumBarCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMaximumBarCountNegative() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
seriesWithBars.setMaximumBarCount(-1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMaximumBarCountOnConstrainedSeries() {
|
||||
assertThrows(IllegalStateException.class, () -> {
|
||||
constrainedSeries.setMaximumBarCount(3);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMaximumBarCount() {
|
||||
assertEquals(Integer.MAX_VALUE, seriesWithBars.getMaximumBarCount());
|
||||
assertEquals(Integer.MAX_VALUE, emptySeries.getMaximumBarCount());
|
||||
assertEquals(Integer.MAX_VALUE, constrainedSeries.getMaximumBarCount());
|
||||
}
|
||||
|
||||
// ==================== Removed Bars Tests ====================
|
||||
|
||||
@Test
|
||||
public void testGetRemovedBarsCount() {
|
||||
assertEquals(0, seriesWithBars.getRemovedBarsCount());
|
||||
assertEquals(0, emptySeries.getRemovedBarsCount());
|
||||
|
||||
seriesWithBars.setMaximumBarCount(3);
|
||||
assertEquals(2, seriesWithBars.getRemovedBarsCount());
|
||||
}
|
||||
|
||||
// ==================== Edge Cases Tests ====================
|
||||
|
||||
@Test
|
||||
public void testSeriesWithSingleBar() {
|
||||
List<Bar> singleBar = Arrays.asList(testBars.get(0));
|
||||
BaseBarSeries singleBarSeries = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withBars(singleBar)
|
||||
.build();
|
||||
|
||||
assertEquals(1, singleBarSeries.getBarCount());
|
||||
assertEquals(0, singleBarSeries.getBeginIndex());
|
||||
assertEquals(0, singleBarSeries.getEndIndex());
|
||||
assertFalse(singleBarSeries.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSeriesWithNullName() {
|
||||
BaseBarSeries series = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withName(null)
|
||||
.build();
|
||||
|
||||
assertEquals("unnamed_series", series.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSeriesWithEmptyName() {
|
||||
BaseBarSeries series = new BaseBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(barBuilderFactory)
|
||||
.withName("")
|
||||
.build();
|
||||
|
||||
assertEquals("", series.getName());
|
||||
}
|
||||
|
||||
// ==================== Utility Methods Tests ====================
|
||||
|
||||
@Test
|
||||
public void testCutMethod() {
|
||||
List<Bar> result = BaseBarSeriesTest.cut(testBars, 1, 4);
|
||||
|
||||
assertEquals(3, result.size());
|
||||
assertSame(testBars.get(1), result.get(0));
|
||||
assertSame(testBars.get(2), result.get(1));
|
||||
assertSame(testBars.get(3), result.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCutMethodWithEmptyRange() {
|
||||
List<Bar> result = BaseBarSeriesTest.cut(testBars, 2, 2);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildOutOfBoundsMessage() {
|
||||
String message = BaseBarSeriesTest.buildOutOfBoundsMessage(seriesWithBars, 10);
|
||||
|
||||
assertTrue(message.contains("Size of series: 5 bars"));
|
||||
assertTrue(message.contains("0 bars removed"));
|
||||
assertTrue(message.contains("index = 10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildOutOfBoundsMessageWithRemovedBars() {
|
||||
seriesWithBars.setMaximumBarCount(3);
|
||||
String message = BaseBarSeriesTest.buildOutOfBoundsMessage(seriesWithBars, 10);
|
||||
|
||||
assertTrue(message.contains("Size of series: 3 bars"));
|
||||
assertTrue(message.contains("2 bars removed"));
|
||||
assertTrue(message.contains("index = 10"));
|
||||
}
|
||||
|
||||
// ==================== Helper Methods for Testing ====================
|
||||
|
||||
/**
|
||||
* Helper method to access the private cut method for testing.
|
||||
*/
|
||||
private static List<Bar> cut(List<Bar> bars, int startIndex, int endIndex) {
|
||||
return new ArrayList<>(bars.subList(startIndex, endIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to access the private buildOutOfBoundsMessage method for
|
||||
* testing.
|
||||
*/
|
||||
private static String buildOutOfBoundsMessage(BaseBarSeries series, int index) {
|
||||
return String.format("Size of series: %s bars, %s bars removed, index = %s", series.getBarData().size(),
|
||||
series.getRemovedBarsCount(), index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class BaseRealtimeBarTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public BaseRealtimeBarTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeUpdatesSideAndLiquidityBreakdowns() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(2), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
bar.addTrade(numOf(1), numOf(90), RealtimeBar.Side.SELL, RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
assertTrue(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(numOf(3), bar.getVolume());
|
||||
assertEquals(numOf(290), bar.getAmount());
|
||||
assertEquals(2, bar.getTrades());
|
||||
assertEquals(numOf(2), bar.getBuyVolume());
|
||||
assertEquals(numOf(1), bar.getSellVolume());
|
||||
assertEquals(numOf(200), bar.getBuyAmount());
|
||||
assertEquals(numOf(90), bar.getSellAmount());
|
||||
assertEquals(1, bar.getBuyTrades());
|
||||
assertEquals(1, bar.getSellTrades());
|
||||
assertEquals(numOf(2), bar.getMakerVolume());
|
||||
assertEquals(numOf(1), bar.getTakerVolume());
|
||||
assertEquals(numOf(200), bar.getMakerAmount());
|
||||
assertEquals(numOf(90), bar.getTakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
assertEquals(1, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeSupportsOptionalSideAndLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), null, null);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(numOf(0), bar.getBuyVolume());
|
||||
assertEquals(numOf(0), bar.getSellVolume());
|
||||
assertEquals(numOf(0), bar.getMakerVolume());
|
||||
assertEquals(numOf(0), bar.getTakerVolume());
|
||||
|
||||
bar.addTrade(numOf(2), numOf(110), RealtimeBar.Side.SELL, null);
|
||||
assertTrue(bar.hasSideData());
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(numOf(2), bar.getSellVolume());
|
||||
assertEquals(numOf(220), bar.getSellAmount());
|
||||
assertEquals(1, bar.getSellTrades());
|
||||
|
||||
bar.addTrade(numOf(1), numOf(120), null, RealtimeBar.Liquidity.MAKER);
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(numOf(1), bar.getMakerVolume());
|
||||
assertEquals(numOf(120), bar.getMakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testConstructorRejectsNullNumFactory() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0, null, null, null,
|
||||
null, 0, 0, null, null, null, null, 0, 0, false, false, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithAllParameters() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var openPrice = numOf(100);
|
||||
final var highPrice = numOf(110);
|
||||
final var lowPrice = numOf(90);
|
||||
final var closePrice = numOf(105);
|
||||
final var volume = numOf(1000);
|
||||
final var amount = numOf(105000);
|
||||
final var buyVolume = numOf(600);
|
||||
final var sellVolume = numOf(400);
|
||||
final var buyAmount = numOf(63000);
|
||||
final var sellAmount = numOf(42000);
|
||||
final var makerVolume = numOf(700);
|
||||
final var takerVolume = numOf(300);
|
||||
final var makerAmount = numOf(73500);
|
||||
final var takerAmount = numOf(31500);
|
||||
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), openPrice, highPrice, lowPrice,
|
||||
closePrice, volume, amount, 100, buyVolume, sellVolume, buyAmount, sellAmount, 60, 40, makerVolume,
|
||||
takerVolume, makerAmount, takerAmount, 70, 30, true, true, numFactory);
|
||||
|
||||
assertEquals(openPrice, bar.getOpenPrice());
|
||||
assertEquals(highPrice, bar.getHighPrice());
|
||||
assertEquals(lowPrice, bar.getLowPrice());
|
||||
assertEquals(closePrice, bar.getClosePrice());
|
||||
assertEquals(volume, bar.getVolume());
|
||||
assertEquals(amount, bar.getAmount());
|
||||
assertEquals(100, bar.getTrades());
|
||||
assertTrue(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(buyVolume, bar.getBuyVolume());
|
||||
assertEquals(sellVolume, bar.getSellVolume());
|
||||
assertEquals(buyAmount, bar.getBuyAmount());
|
||||
assertEquals(sellAmount, bar.getSellAmount());
|
||||
assertEquals(60, bar.getBuyTrades());
|
||||
assertEquals(40, bar.getSellTrades());
|
||||
assertEquals(makerVolume, bar.getMakerVolume());
|
||||
assertEquals(takerVolume, bar.getTakerVolume());
|
||||
assertEquals(makerAmount, bar.getMakerAmount());
|
||||
assertEquals(takerAmount, bar.getTakerAmount());
|
||||
assertEquals(70, bar.getMakerTrades());
|
||||
assertEquals(30, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGettersReturnZeroWhenFieldsAreNull() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
assertEquals(zero, bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getSellVolume());
|
||||
assertEquals(zero, bar.getBuyAmount());
|
||||
assertEquals(zero, bar.getSellAmount());
|
||||
assertEquals(zero, bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getTakerVolume());
|
||||
assertEquals(zero, bar.getMakerAmount());
|
||||
assertEquals(zero, bar.getTakerAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGettersReturnZeroWhenHasSideDataIsFalse() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
assertEquals(zero, bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getSellVolume());
|
||||
assertEquals(zero, bar.getBuyAmount());
|
||||
assertEquals(zero, bar.getSellAmount());
|
||||
assertEquals(0, bar.getBuyTrades());
|
||||
assertEquals(0, bar.getSellTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGettersReturnZeroWhenHasLiquidityDataIsFalse() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(zero, bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getTakerVolume());
|
||||
assertEquals(zero, bar.getMakerAmount());
|
||||
assertEquals(zero, bar.getTakerAmount());
|
||||
assertEquals(0, bar.getMakerTrades());
|
||||
assertEquals(0, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasSideDataInitialState() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasSideDataAfterAddingTradeWithSide() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, null);
|
||||
assertTrue(bar.hasSideData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasLiquidityDataInitialState() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasLiquidityDataAfterAddingTradeWithLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), null, RealtimeBar.Liquidity.MAKER);
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasSideDataWithPrePopulatedData() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var buyVolume = numOf(100);
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
buyVolume, null, null, null, 1, 0, null, null, null, null, 0, 0, true, false, numFactory);
|
||||
|
||||
assertTrue(bar.hasSideData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasLiquidityDataWithPrePopulatedData() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var makerVolume = numOf(100);
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, makerVolume, null, null, null, 1, 0, false, true, numFactory);
|
||||
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesMultipleBuyTrades() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, null);
|
||||
bar.addTrade(numOf(2), numOf(110), RealtimeBar.Side.BUY, null);
|
||||
bar.addTrade(numOf(3), numOf(120), RealtimeBar.Side.BUY, null);
|
||||
|
||||
assertEquals(numOf(6), bar.getBuyVolume());
|
||||
assertEquals(numOf(680), bar.getBuyAmount()); // 100 + 220 + 360
|
||||
assertEquals(3, bar.getBuyTrades());
|
||||
assertEquals(numOf(0), bar.getSellVolume());
|
||||
assertEquals(0, bar.getSellTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesMultipleSellTrades() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.SELL, null);
|
||||
bar.addTrade(numOf(2), numOf(110), RealtimeBar.Side.SELL, null);
|
||||
bar.addTrade(numOf(3), numOf(120), RealtimeBar.Side.SELL, null);
|
||||
|
||||
assertEquals(numOf(6), bar.getSellVolume());
|
||||
assertEquals(numOf(680), bar.getSellAmount()); // 100 + 220 + 360
|
||||
assertEquals(3, bar.getSellTrades());
|
||||
assertEquals(numOf(0), bar.getBuyVolume());
|
||||
assertEquals(0, bar.getBuyTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesMultipleMakerTrades() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), null, RealtimeBar.Liquidity.MAKER);
|
||||
bar.addTrade(numOf(2), numOf(110), null, RealtimeBar.Liquidity.MAKER);
|
||||
bar.addTrade(numOf(3), numOf(120), null, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(numOf(6), bar.getMakerVolume());
|
||||
assertEquals(numOf(680), bar.getMakerAmount()); // 100 + 220 + 360
|
||||
assertEquals(3, bar.getMakerTrades());
|
||||
assertEquals(numOf(0), bar.getTakerVolume());
|
||||
assertEquals(0, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesMultipleTakerTrades() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(1), numOf(100), null, RealtimeBar.Liquidity.TAKER);
|
||||
bar.addTrade(numOf(2), numOf(110), null, RealtimeBar.Liquidity.TAKER);
|
||||
bar.addTrade(numOf(3), numOf(120), null, RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
assertEquals(numOf(6), bar.getTakerVolume());
|
||||
assertEquals(numOf(680), bar.getTakerAmount()); // 100 + 220 + 360
|
||||
assertEquals(3, bar.getTakerTrades());
|
||||
assertEquals(numOf(0), bar.getMakerVolume());
|
||||
assertEquals(0, bar.getMakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesMixedSideAndLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
// Buy + Maker
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
// Buy + Taker
|
||||
bar.addTrade(numOf(2), numOf(110), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.TAKER);
|
||||
// Sell + Maker
|
||||
bar.addTrade(numOf(3), numOf(120), RealtimeBar.Side.SELL, RealtimeBar.Liquidity.MAKER);
|
||||
// Sell + Taker
|
||||
bar.addTrade(numOf(4), numOf(130), RealtimeBar.Side.SELL, RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
assertEquals(numOf(3), bar.getBuyVolume()); // 1 + 2
|
||||
assertEquals(numOf(320), bar.getBuyAmount()); // 100 + 220
|
||||
assertEquals(2, bar.getBuyTrades());
|
||||
assertEquals(numOf(7), bar.getSellVolume()); // 3 + 4
|
||||
assertEquals(numOf(880), bar.getSellAmount()); // 360 + 520
|
||||
assertEquals(2, bar.getSellTrades());
|
||||
assertEquals(numOf(4), bar.getMakerVolume()); // 1 + 3
|
||||
assertEquals(numOf(460), bar.getMakerAmount()); // 100 + 360
|
||||
assertEquals(2, bar.getMakerTrades());
|
||||
assertEquals(numOf(6), bar.getTakerVolume()); // 2 + 4
|
||||
assertEquals(numOf(740), bar.getTakerAmount()); // 220 + 520
|
||||
assertEquals(2, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeUpdatesBaseBarVolume() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(5), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(numOf(5), bar.getVolume());
|
||||
assertEquals(numOf(500), bar.getAmount());
|
||||
assertEquals(1, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeAccumulatesBaseBarVolume() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(2), numOf(100), RealtimeBar.Side.BUY, null);
|
||||
bar.addTrade(numOf(3), numOf(110), RealtimeBar.Side.SELL, null);
|
||||
bar.addTrade(numOf(1), numOf(120), null, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(numOf(6), bar.getVolume());
|
||||
assertEquals(numOf(650), bar.getAmount()); // 200 + 330 + 120
|
||||
assertEquals(3, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithZeroVolume() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(zero, numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(zero, bar.getVolume());
|
||||
assertEquals(zero, bar.getAmount());
|
||||
assertEquals(1, bar.getTrades());
|
||||
assertEquals(zero, bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getBuyAmount());
|
||||
assertEquals(1, bar.getBuyTrades());
|
||||
assertEquals(zero, bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getMakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithZeroPrice() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(5), zero, RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(numOf(5), bar.getVolume());
|
||||
assertEquals(zero, bar.getAmount());
|
||||
assertEquals(1, bar.getTrades());
|
||||
assertEquals(numOf(5), bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getBuyAmount());
|
||||
assertEquals(1, bar.getBuyTrades());
|
||||
assertEquals(numOf(5), bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getMakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeWithLargeTradeCounts() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
// Add many trades
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
bar.addTrade(numOf(1), numOf(100 + i), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
}
|
||||
|
||||
assertEquals(numOf(1000), bar.getBuyVolume());
|
||||
assertEquals(1000, bar.getBuyTrades());
|
||||
assertEquals(1000, bar.getMakerTrades());
|
||||
assertEquals(1000, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeOnlySideNoLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(5), numOf(100), RealtimeBar.Side.BUY, null);
|
||||
|
||||
assertTrue(bar.hasSideData());
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(numOf(5), bar.getBuyVolume());
|
||||
assertEquals(numOf(500), bar.getBuyAmount());
|
||||
assertEquals(1, bar.getBuyTrades());
|
||||
assertEquals(zero, bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getTakerVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeOnlyLiquidityNoSide() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(5), numOf(100), null, RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(zero, bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getSellVolume());
|
||||
assertEquals(numOf(5), bar.getMakerVolume());
|
||||
assertEquals(numOf(500), bar.getMakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTradeNeitherSideNorLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
bar.addTrade(numOf(5), numOf(100), null, null);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(zero, bar.getBuyVolume());
|
||||
assertEquals(zero, bar.getSellVolume());
|
||||
assertEquals(zero, bar.getMakerVolume());
|
||||
assertEquals(zero, bar.getTakerVolume());
|
||||
// Base bar should still be updated
|
||||
assertEquals(numOf(5), bar.getVolume());
|
||||
assertEquals(numOf(500), bar.getAmount());
|
||||
assertEquals(1, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithPrePopulatedSideData() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var buyVolume = numOf(100);
|
||||
final var sellVolume = numOf(200);
|
||||
final var buyAmount = numOf(10000);
|
||||
final var sellAmount = numOf(22000);
|
||||
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
buyVolume, sellVolume, buyAmount, sellAmount, 10, 20, null, null, null, null, 0, 0, true, false,
|
||||
numFactory);
|
||||
|
||||
assertTrue(bar.hasSideData());
|
||||
assertFalse(bar.hasLiquidityData());
|
||||
assertEquals(buyVolume, bar.getBuyVolume());
|
||||
assertEquals(sellVolume, bar.getSellVolume());
|
||||
assertEquals(buyAmount, bar.getBuyAmount());
|
||||
assertEquals(sellAmount, bar.getSellAmount());
|
||||
assertEquals(10, bar.getBuyTrades());
|
||||
assertEquals(20, bar.getSellTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithPrePopulatedLiquidityData() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var makerVolume = numOf(150);
|
||||
final var takerVolume = numOf(250);
|
||||
final var makerAmount = numOf(15000);
|
||||
final var takerAmount = numOf(27500);
|
||||
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, makerVolume, takerVolume, makerAmount, takerAmount, 15, 25, false, true,
|
||||
numFactory);
|
||||
|
||||
assertFalse(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(makerVolume, bar.getMakerVolume());
|
||||
assertEquals(takerVolume, bar.getTakerVolume());
|
||||
assertEquals(makerAmount, bar.getMakerAmount());
|
||||
assertEquals(takerAmount, bar.getTakerAmount());
|
||||
assertEquals(15, bar.getMakerTrades());
|
||||
assertEquals(25, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConstructorWithAllPrePopulatedData() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var buyVolume = numOf(100);
|
||||
final var sellVolume = numOf(200);
|
||||
final var buyAmount = numOf(10000);
|
||||
final var sellAmount = numOf(22000);
|
||||
final var makerVolume = numOf(150);
|
||||
final var takerVolume = numOf(250);
|
||||
final var makerAmount = numOf(15000);
|
||||
final var takerAmount = numOf(27500);
|
||||
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
buyVolume, sellVolume, buyAmount, sellAmount, 10, 20, makerVolume, takerVolume, makerAmount,
|
||||
takerAmount, 15, 25, true, true, numFactory);
|
||||
|
||||
assertTrue(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertEquals(buyVolume, bar.getBuyVolume());
|
||||
assertEquals(sellVolume, bar.getSellVolume());
|
||||
assertEquals(makerVolume, bar.getMakerVolume());
|
||||
assertEquals(takerVolume, bar.getTakerVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTradeCountsAccumulateCorrectly() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
// Add 5 buy trades
|
||||
for (int i = 0; i < 5; i++) {
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, null);
|
||||
}
|
||||
|
||||
// Add 3 sell trades
|
||||
for (int i = 0; i < 3; i++) {
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.SELL, null);
|
||||
}
|
||||
|
||||
// Add 4 maker trades (these are separate trades, so they add to total count)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
bar.addTrade(numOf(1), numOf(100), null, RealtimeBar.Liquidity.MAKER);
|
||||
}
|
||||
|
||||
// Add 2 taker trades (these are separate trades, so they add to total count)
|
||||
for (int i = 0; i < 2; i++) {
|
||||
bar.addTrade(numOf(1), numOf(100), null, RealtimeBar.Liquidity.TAKER);
|
||||
}
|
||||
|
||||
// Each addTrade() increments the total trade count
|
||||
assertEquals(14, bar.getTrades()); // 5 + 3 + 4 + 2
|
||||
assertEquals(5, bar.getBuyTrades());
|
||||
assertEquals(3, bar.getSellTrades());
|
||||
assertEquals(4, bar.getMakerTrades());
|
||||
assertEquals(2, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTradeCountsWithOverlappingSideAndLiquidity() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
// Add trades with both side and liquidity (each trade counts once)
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.TAKER);
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.SELL, RealtimeBar.Liquidity.MAKER);
|
||||
bar.addTrade(numOf(1), numOf(100), RealtimeBar.Side.SELL, RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
assertEquals(4, bar.getTrades()); // 4 total trades
|
||||
assertEquals(2, bar.getBuyTrades());
|
||||
assertEquals(2, bar.getSellTrades());
|
||||
assertEquals(2, bar.getMakerTrades());
|
||||
assertEquals(2, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAmountCalculationIsCorrect() {
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var period = Duration.ofMinutes(1);
|
||||
final var zero = numFactory.zero();
|
||||
final var bar = new BaseRealtimeBar(period, start, start.plus(period), null, null, null, null, zero, zero, 0,
|
||||
null, null, null, null, 0, 0, null, null, null, null, 0, 0, false, false, numFactory);
|
||||
|
||||
// Trade: volume=2, price=100 -> amount=200
|
||||
bar.addTrade(numOf(2), numOf(100), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
assertEquals(numOf(200), bar.getBuyAmount());
|
||||
assertEquals(numOf(200), bar.getMakerAmount());
|
||||
|
||||
// Trade: volume=3, price=110 -> amount=330
|
||||
bar.addTrade(numOf(3), numOf(110), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
assertEquals(numOf(530), bar.getBuyAmount()); // 200 + 330
|
||||
assertEquals(numOf(530), bar.getMakerAmount()); // 200 + 330
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
class BaseTradeTest {
|
||||
|
||||
private static final DoubleNumFactory NUM_FACTORY = DoubleNumFactory.getInstance();
|
||||
|
||||
@Test
|
||||
void liveConstructorCarriesMetadataAndFeeIntoTradeAndFill() {
|
||||
Instant time = Instant.parse("2025-01-01T00:00:00Z");
|
||||
Num price = NUM_FACTORY.hundred();
|
||||
Num amount = NUM_FACTORY.two();
|
||||
Num fee = NUM_FACTORY.numOf(0.4);
|
||||
|
||||
BaseTrade trade = new BaseTrade(3, time, price, amount, fee, ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertEquals(3, trade.getIndex());
|
||||
assertEquals(time, trade.getTime());
|
||||
assertEquals("order-1", trade.getOrderId());
|
||||
assertEquals("corr-1", trade.getCorrelationId());
|
||||
assertNumEquals(price, trade.getPricePerAsset());
|
||||
assertNumEquals(amount, trade.getAmount());
|
||||
assertNumEquals(fee, trade.getCost());
|
||||
assertNumEquals(NUM_FACTORY.numOf(100.2), trade.getNetPrice());
|
||||
assertEquals(1, trade.getFills().size());
|
||||
assertEquals(3, trade.getFills().getFirst().index());
|
||||
assertEquals(time, trade.getFills().getFirst().time());
|
||||
assertEquals(ExecutionSide.BUY, trade.getFills().getFirst().side());
|
||||
assertNumEquals(fee, trade.getFills().getFirst().fee());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compatibilityAccessorsMirrorLegacyRecordContract() {
|
||||
Instant time = Instant.parse("2025-01-01T00:00:00Z");
|
||||
Num price = NUM_FACTORY.hundred();
|
||||
Num amount = NUM_FACTORY.two();
|
||||
Num fee = NUM_FACTORY.numOf(0.2);
|
||||
|
||||
BaseTrade trade = new BaseTrade(5, time, price, amount, fee, ExecutionSide.BUY, "order-5", "corr-5");
|
||||
|
||||
assertEquals(5, trade.getIndex());
|
||||
assertEquals(time, trade.time());
|
||||
assertNumEquals(price, trade.price());
|
||||
assertNumEquals(amount, trade.amount());
|
||||
assertNumEquals(fee, trade.fee());
|
||||
assertEquals(ExecutionSide.BUY, trade.side());
|
||||
assertEquals("order-5", trade.orderId());
|
||||
assertEquals("corr-5", trade.correlationId());
|
||||
assertTrue(trade.getCostModel().equals(RecordedTradeCostModel.INSTANCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void liveConstructorDefaultsNullFeeToZero() {
|
||||
BaseTrade trade = new BaseTrade(1, Instant.EPOCH, NUM_FACTORY.hundred(), NUM_FACTORY.one(), null,
|
||||
ExecutionSide.SELL, null, null);
|
||||
|
||||
assertNumEquals(NUM_FACTORY.zero(), trade.getCost());
|
||||
assertNumEquals(NUM_FACTORY.hundred(), trade.getNetPrice());
|
||||
assertNumEquals(NUM_FACTORY.zero(), trade.getFills().getFirst().fee());
|
||||
assertNumEquals(NUM_FACTORY.zero(), trade.fee());
|
||||
assertTrue(trade.fee().isZero());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withIndexCopiesMetadataAndPreservesFee() {
|
||||
BaseTrade original = new BaseTrade(2, Instant.parse("2025-01-01T00:00:00Z"), NUM_FACTORY.hundred(),
|
||||
NUM_FACTORY.two(), NUM_FACTORY.numOf(0.2), ExecutionSide.BUY, "order-2", "corr-2");
|
||||
|
||||
BaseTrade reindexed = original.withIndex(9);
|
||||
|
||||
assertEquals(9, reindexed.getIndex());
|
||||
assertEquals(original.getTime(), reindexed.getTime());
|
||||
assertEquals(original.getOrderId(), reindexed.getOrderId());
|
||||
assertEquals(original.getCorrelationId(), reindexed.getCorrelationId());
|
||||
assertEquals(1, reindexed.getFills().size());
|
||||
assertEquals(9, reindexed.getFills().getFirst().index());
|
||||
assertNumEquals(original.getCost(), reindexed.getCost());
|
||||
assertEquals(original.time(), reindexed.time());
|
||||
assertNumEquals(original.price(), reindexed.price());
|
||||
assertNumEquals(original.amount(), reindexed.amount());
|
||||
assertNumEquals(original.fee(), reindexed.fee());
|
||||
assertEquals(original.side(), reindexed.side());
|
||||
assertEquals(original.orderId(), reindexed.orderId());
|
||||
assertEquals(original.correlationId(), reindexed.correlationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withIndexPreservesFillOffsetsForMultiFillTrades() {
|
||||
TradeFill firstFill = new TradeFill(2, Instant.EPOCH, NUM_FACTORY.hundred(), NUM_FACTORY.one(),
|
||||
NUM_FACTORY.numOf(0.1), ExecutionSide.BUY, "order-1", "corr-1");
|
||||
TradeFill secondFill = new TradeFill(4, Instant.EPOCH.plusSeconds(60), NUM_FACTORY.numOf(102),
|
||||
NUM_FACTORY.one(), NUM_FACTORY.numOf(0.2), ExecutionSide.BUY, "order-1", "corr-1");
|
||||
BaseTrade original = new BaseTrade(TradeType.BUY, List.of(firstFill, secondFill),
|
||||
RecordedTradeCostModel.INSTANCE);
|
||||
|
||||
BaseTrade reindexed = original.withIndex(10);
|
||||
|
||||
assertEquals(10, reindexed.getIndex());
|
||||
assertEquals(List.of(10, 12), reindexed.getFills().stream().map(TradeFill::index).toList());
|
||||
assertNumEquals(original.getCost(), reindexed.getCost());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withIndexAfterSerializationPreservesRecordedFee() throws Exception {
|
||||
BaseTrade original = new BaseTrade(4, Instant.parse("2025-01-01T00:00:00Z"), NUM_FACTORY.hundred(),
|
||||
NUM_FACTORY.one(), NUM_FACTORY.numOf(0.3), ExecutionSide.BUY, "order-4", "corr-4");
|
||||
byte[] serialized;
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(original);
|
||||
objectOutput.flush();
|
||||
serialized = output.toByteArray();
|
||||
}
|
||||
|
||||
BaseTrade restored;
|
||||
try (ByteArrayInputStream input = new ByteArrayInputStream(serialized);
|
||||
ObjectInputStream objectInput = new ObjectInputStream(input)) {
|
||||
restored = (BaseTrade) objectInput.readObject();
|
||||
}
|
||||
|
||||
BaseTrade reindexed = restored.withIndex(10);
|
||||
|
||||
assertEquals(10, reindexed.getIndex());
|
||||
assertNumEquals(NUM_FACTORY.numOf(0.3), reindexed.getCost());
|
||||
assertNumEquals(NUM_FACTORY.numOf(100.3), reindexed.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withIndexRejectsNegativeIndex() {
|
||||
BaseTrade trade = new BaseTrade(0, Instant.EPOCH, NUM_FACTORY.hundred(), NUM_FACTORY.one(), NUM_FACTORY.zero(),
|
||||
ExecutionSide.BUY, null, null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> trade.withIndex(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromFillsWithRecordedCostModelUsesFillFees() {
|
||||
TradeFill firstFill = new TradeFill(1, Instant.EPOCH, NUM_FACTORY.hundred(), NUM_FACTORY.one(),
|
||||
NUM_FACTORY.numOf(0.1), ExecutionSide.BUY, "order-1", "corr-1");
|
||||
TradeFill secondFill = new TradeFill(2, Instant.EPOCH, NUM_FACTORY.numOf(102), NUM_FACTORY.one(),
|
||||
NUM_FACTORY.numOf(0.2), ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(firstFill, secondFill), RecordedTradeCostModel.INSTANCE);
|
||||
|
||||
assertNumEquals(NUM_FACTORY.numOf(0.3), trade.getCost());
|
||||
assertNumEquals(NUM_FACTORY.numOf(101.15), trade.getNetPrice());
|
||||
assertEquals(2, trade.getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromFillsRejectsMismatchedFillSide() {
|
||||
TradeFill sellFill = new TradeFill(1, Instant.EPOCH, NUM_FACTORY.hundred(), NUM_FACTORY.one(),
|
||||
NUM_FACTORY.zero(), ExecutionSide.SELL, null, null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> Trade.fromFills(TradeType.BUY, List.of(sellFill), RecordedTradeCostModel.INSTANCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromFillsUsesEarliestFillMetadataAsIdentityAnchor() {
|
||||
Instant laterTime = Instant.parse("2025-01-01T00:05:00Z");
|
||||
Instant earlierTime = Instant.parse("2025-01-01T00:01:00Z");
|
||||
TradeFill laterFill = new TradeFill(5, laterTime, NUM_FACTORY.hundred(), NUM_FACTORY.one(), NUM_FACTORY.zero(),
|
||||
ExecutionSide.BUY, "order-later", "corr-later");
|
||||
TradeFill earlierFill = new TradeFill(2, earlierTime, NUM_FACTORY.numOf(101), NUM_FACTORY.one(),
|
||||
NUM_FACTORY.zero(), ExecutionSide.BUY, "order-earlier", "corr-earlier");
|
||||
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(laterFill, earlierFill), RecordedTradeCostModel.INSTANCE);
|
||||
|
||||
assertEquals(2, trade.getIndex());
|
||||
assertEquals(earlierTime, trade.getTime());
|
||||
assertEquals("order-earlier", trade.getOrderId());
|
||||
assertEquals("corr-earlier", trade.getCorrelationId());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+507
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Comprehensive unit tests for {@link ConcurrentBarSeriesBuilder}.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ConcurrentBarSeriesBuilderTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public ConcurrentBarSeriesBuilderTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
// ==================== Constructor and Defaults Tests ====================
|
||||
|
||||
@Test
|
||||
public void testDefaultConstructor() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
assertNotNull(builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithDefaults() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().build();
|
||||
|
||||
assertEquals("unnamed_series", series.getName());
|
||||
assertEquals(0, series.getBarCount());
|
||||
assertEquals(-1, series.getBeginIndex());
|
||||
assertEquals(-1, series.getEndIndex());
|
||||
assertTrue(series.isEmpty());
|
||||
assertEquals(Integer.MAX_VALUE, series.getMaximumBarCount());
|
||||
assertTrue(series.numFactory() instanceof DecimalNumFactory);
|
||||
assertTrue(series.barBuilderFactory() instanceof TimeBarBuilderFactory);
|
||||
}
|
||||
|
||||
// ==================== withName() Tests ====================
|
||||
|
||||
@Test
|
||||
public void testWithName() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName("MySeries").build();
|
||||
|
||||
assertEquals("MySeries", series.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNameNull() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName(null).build();
|
||||
|
||||
assertEquals("unnamed_series", series.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNameEmptyString() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName("").build();
|
||||
|
||||
assertEquals("", series.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNameChaining() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
ConcurrentBarSeriesBuilder result = builder.withName("Test");
|
||||
assertSame(builder, result);
|
||||
}
|
||||
|
||||
// ==================== withBars() Tests ====================
|
||||
|
||||
@Test
|
||||
public void testWithBars() {
|
||||
List<Bar> bars = createTestBars(3);
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(bars).build();
|
||||
|
||||
assertEquals(3, series.getBarCount());
|
||||
assertEquals(0, series.getBeginIndex());
|
||||
assertEquals(2, series.getEndIndex());
|
||||
assertFalse(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBarsEmptyList() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(Collections.emptyList()).build();
|
||||
|
||||
assertEquals(0, series.getBarCount());
|
||||
assertEquals(-1, series.getBeginIndex());
|
||||
assertEquals(-1, series.getEndIndex());
|
||||
assertTrue(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBarsCreatesCopy() {
|
||||
List<Bar> originalBars = createTestBars(3);
|
||||
List<Bar> barsCopy = new ArrayList<>(originalBars);
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder().withBars(barsCopy);
|
||||
|
||||
// Modify original list
|
||||
barsCopy.add(createTestBar(3));
|
||||
|
||||
ConcurrentBarSeries series = builder.build();
|
||||
assertEquals(3, series.getBarCount()); // Should still be 3, not 4
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBarsChaining() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
ConcurrentBarSeriesBuilder result = builder.withBars(Collections.emptyList());
|
||||
assertSame(builder, result);
|
||||
}
|
||||
|
||||
// ==================== withNumFactory() Tests ====================
|
||||
|
||||
@Test
|
||||
public void testWithNumFactory() {
|
||||
NumFactory customFactory = numFactory instanceof DoubleNumFactory ? DecimalNumFactory.getInstance()
|
||||
: DoubleNumFactory.getInstance();
|
||||
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withNumFactory(customFactory).build();
|
||||
|
||||
assertSame(customFactory, series.numFactory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNumFactoryChaining() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
ConcurrentBarSeriesBuilder result = builder.withNumFactory(numFactory);
|
||||
assertSame(builder, result);
|
||||
}
|
||||
|
||||
// ==================== withBarBuilderFactory() Tests ====================
|
||||
|
||||
@Test
|
||||
public void testWithBarBuilderFactory() {
|
||||
BarBuilderFactory customFactory = new MockBarBuilderFactory();
|
||||
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBarBuilderFactory(customFactory).build();
|
||||
|
||||
assertSame(customFactory, series.barBuilderFactory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBarBuilderFactoryChaining() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
BarBuilderFactory factory = new MockBarBuilderFactory();
|
||||
ConcurrentBarSeriesBuilder result = builder.withBarBuilderFactory(factory);
|
||||
assertSame(builder, result);
|
||||
}
|
||||
|
||||
// ==================== withMaxBarCount() Tests ====================
|
||||
|
||||
@Test
|
||||
public void testWithMaxBarCount() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withMaxBarCount(100).build();
|
||||
|
||||
assertEquals(100, series.getMaximumBarCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMaxBarCountAtMaxValueKeepsSeriesUnconstrained() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withMaxBarCount(Integer.MAX_VALUE).build();
|
||||
|
||||
series.setMaximumBarCount(100);
|
||||
assertEquals(100, series.getMaximumBarCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMaxBarCountZero() {
|
||||
// Maximum bar count must be strictly positive
|
||||
assertThrows(IllegalArgumentException.class, () -> new ConcurrentBarSeriesBuilder().withMaxBarCount(0).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMaxBarCountNegative() {
|
||||
// Maximum bar count must be strictly positive
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new ConcurrentBarSeriesBuilder().withMaxBarCount(-1).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithMaxBarCountChaining() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
ConcurrentBarSeriesBuilder result = builder.withMaxBarCount(100);
|
||||
assertSame(builder, result);
|
||||
}
|
||||
|
||||
// ==================== Builder Pattern (Fluent Interface) Tests
|
||||
// ====================
|
||||
|
||||
@Test
|
||||
public void testFluentInterfaceChaining() {
|
||||
List<Bar> bars = createTestBars(2);
|
||||
BarBuilderFactory factory = new MockBarBuilderFactory();
|
||||
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName("FluentTest")
|
||||
.withBars(bars)
|
||||
.withNumFactory(numFactory)
|
||||
.withBarBuilderFactory(factory)
|
||||
.withMaxBarCount(50)
|
||||
.build();
|
||||
|
||||
assertEquals("FluentTest", series.getName());
|
||||
assertEquals(2, series.getBarCount());
|
||||
assertSame(numFactory, series.numFactory());
|
||||
assertSame(factory, series.barBuilderFactory());
|
||||
assertEquals(50, series.getMaximumBarCount());
|
||||
// Constrained state is internal and not exposed via public API
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithoutMaxBarCountIsConstrained() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(createTestBars(1)).build();
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> series.setMaximumBarCount(50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithMaxBarCountAllowsUpdates() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(createTestBars(1))
|
||||
.withMaxBarCount(50)
|
||||
.build();
|
||||
|
||||
series.setMaximumBarCount(75);
|
||||
assertEquals(75, series.getMaximumBarCount());
|
||||
}
|
||||
|
||||
// ==================== Builder Reset After Build Tests ====================
|
||||
|
||||
@Test
|
||||
public void testBuilderResetsAfterBuild() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder().withName("First").withMaxBarCount(100);
|
||||
|
||||
ConcurrentBarSeries first = builder.build();
|
||||
assertEquals("First", first.getName());
|
||||
assertEquals(100, first.getMaximumBarCount());
|
||||
// Constrained state is internal and not exposed via public API
|
||||
|
||||
// Builder should be reset, so second build should use defaults
|
||||
ConcurrentBarSeries second = builder.build();
|
||||
assertEquals("unnamed_series", second.getName());
|
||||
assertEquals(Integer.MAX_VALUE, second.getMaximumBarCount());
|
||||
// Constrained state is internal and not exposed via public API
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderResetsBarsAfterBuild() {
|
||||
List<Bar> bars1 = createTestBars(3);
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder().withBars(bars1);
|
||||
|
||||
ConcurrentBarSeries first = builder.build();
|
||||
assertEquals(3, first.getBarCount());
|
||||
|
||||
// Builder should be reset, so second build should be empty
|
||||
ConcurrentBarSeries second = builder.build();
|
||||
assertEquals(0, second.getBarCount());
|
||||
}
|
||||
|
||||
// ==================== Build with All Configurations Tests ====================
|
||||
|
||||
@Test
|
||||
public void testBuildWithAllConfigurations() {
|
||||
List<Bar> bars = createTestBars(5);
|
||||
NumFactory customNumFactory = numFactory instanceof DoubleNumFactory ? DecimalNumFactory.getInstance()
|
||||
: DoubleNumFactory.getInstance();
|
||||
BarBuilderFactory customBarFactory = new MockBarBuilderFactory();
|
||||
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName("CompleteTest")
|
||||
.withBars(bars)
|
||||
.withNumFactory(customNumFactory)
|
||||
.withBarBuilderFactory(customBarFactory)
|
||||
.withMaxBarCount(200)
|
||||
.build();
|
||||
|
||||
assertEquals("CompleteTest", series.getName());
|
||||
assertEquals(5, series.getBarCount());
|
||||
assertEquals(0, series.getBeginIndex());
|
||||
assertEquals(4, series.getEndIndex());
|
||||
assertSame(customNumFactory, series.numFactory());
|
||||
assertSame(customBarFactory, series.barBuilderFactory());
|
||||
assertEquals(200, series.getMaximumBarCount());
|
||||
// Constrained state is internal and not exposed via public API
|
||||
assertFalse(series.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithEmptyBarsAndAllConfigurations() {
|
||||
NumFactory customNumFactory = numFactory instanceof DoubleNumFactory ? DecimalNumFactory.getInstance()
|
||||
: DoubleNumFactory.getInstance();
|
||||
BarBuilderFactory customBarFactory = new MockBarBuilderFactory();
|
||||
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withName("EmptyBarsTest")
|
||||
.withBars(Collections.emptyList())
|
||||
.withNumFactory(customNumFactory)
|
||||
.withBarBuilderFactory(customBarFactory)
|
||||
.withMaxBarCount(50)
|
||||
.build();
|
||||
|
||||
assertEquals("EmptyBarsTest", series.getName());
|
||||
assertEquals(0, series.getBarCount());
|
||||
assertEquals(-1, series.getBeginIndex());
|
||||
assertEquals(-1, series.getEndIndex());
|
||||
assertSame(customNumFactory, series.numFactory());
|
||||
assertSame(customBarFactory, series.barBuilderFactory());
|
||||
assertEquals(50, series.getMaximumBarCount());
|
||||
// Constrained state is internal and not exposed via public API
|
||||
assertTrue(series.isEmpty());
|
||||
}
|
||||
|
||||
// ==================== Multiple Builds Tests ====================
|
||||
|
||||
@Test
|
||||
public void testMultipleBuilds() {
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
|
||||
ConcurrentBarSeries series1 = builder.withName("Series1").build();
|
||||
ConcurrentBarSeries series2 = builder.withName("Series2").withMaxBarCount(100).build();
|
||||
ConcurrentBarSeries series3 = builder.withName("Series3").withBars(createTestBars(2)).build();
|
||||
|
||||
assertEquals("Series1", series1.getName());
|
||||
assertEquals("Series2", series2.getName());
|
||||
assertEquals(100, series2.getMaximumBarCount());
|
||||
assertEquals("Series3", series3.getName());
|
||||
assertEquals(2, series3.getBarCount());
|
||||
}
|
||||
|
||||
// ==================== Edge Cases Tests ====================
|
||||
|
||||
@Test
|
||||
public void testBuildWithSingleBar() {
|
||||
List<Bar> singleBar = createTestBars(1);
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(singleBar).build();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
assertEquals(0, series.getBeginIndex());
|
||||
assertEquals(0, series.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithLargeBarCount() {
|
||||
List<Bar> manyBars = createTestBars(1000);
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(manyBars).build();
|
||||
|
||||
assertEquals(1000, series.getBarCount());
|
||||
assertEquals(0, series.getBeginIndex());
|
||||
assertEquals(999, series.getEndIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildWithMaxBarCountApplied() {
|
||||
List<Bar> bars = createTestBars(10);
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().withBars(bars).withMaxBarCount(5).build();
|
||||
|
||||
// Max bar count is applied immediately, so only 5 bars should remain
|
||||
// When bars are removed, beginIndex is adjusted based on removedBarsCount
|
||||
assertEquals(5, series.getBarCount());
|
||||
assertEquals(5, series.getMaximumBarCount());
|
||||
assertEquals(5, series.getBeginIndex()); // Adjusted after removing 5 bars
|
||||
assertEquals(9, series.getEndIndex()); // End index remains at 9 (original last bar)
|
||||
assertEquals(5, series.getRemovedBarsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildReturnsConcurrentBarSeries() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().build();
|
||||
assertTrue(series instanceof ConcurrentBarSeries);
|
||||
}
|
||||
|
||||
// ==================== Null Parameter Handling Tests ====================
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testWithBarsNull() {
|
||||
new ConcurrentBarSeriesBuilder().withBars(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNumFactoryNull() {
|
||||
// Null numFactory should be accepted (will use default or fail at build time)
|
||||
// Actually, looking at the implementation, it just assigns null
|
||||
// Let's verify it doesn't throw immediately
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
builder.withNumFactory(null);
|
||||
// Build will likely fail, but builder accepts null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithBarBuilderFactoryNull() {
|
||||
// Null barBuilderFactory should be accepted (will use default or fail at build
|
||||
// time)
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder();
|
||||
builder.withBarBuilderFactory(null);
|
||||
// Build will likely fail, but builder accepts null
|
||||
}
|
||||
|
||||
// ==================== Builder State Isolation Tests ====================
|
||||
|
||||
@Test
|
||||
public void testBuilderStateIsolation() {
|
||||
List<Bar> bars1 = createTestBars(3);
|
||||
List<Bar> bars2 = createTestBars(5);
|
||||
|
||||
ConcurrentBarSeriesBuilder builder = new ConcurrentBarSeriesBuilder().withName("Test1")
|
||||
.withBars(bars1)
|
||||
.withMaxBarCount(100);
|
||||
|
||||
ConcurrentBarSeries series1 = builder.build();
|
||||
|
||||
// Modify builder for second series
|
||||
builder.withName("Test2").withBars(bars2).withMaxBarCount(200);
|
||||
|
||||
ConcurrentBarSeries series2 = builder.build();
|
||||
|
||||
// First series should be unaffected
|
||||
assertEquals("Test1", series1.getName());
|
||||
assertEquals(3, series1.getBarCount());
|
||||
assertEquals(100, series1.getMaximumBarCount());
|
||||
|
||||
// Second series should have new values
|
||||
assertEquals("Test2", series2.getName());
|
||||
assertEquals(5, series2.getBarCount());
|
||||
assertEquals(200, series2.getMaximumBarCount());
|
||||
}
|
||||
|
||||
// ==================== Default BarBuilderFactory Tests ====================
|
||||
|
||||
@Test
|
||||
public void testDefaultBarBuilderFactory() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().build();
|
||||
BarBuilderFactory factory = series.barBuilderFactory();
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof TimeBarBuilderFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultBarBuilderFactoryCreatesRealtimeBars() {
|
||||
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder().build();
|
||||
BarBuilderFactory factory = series.barBuilderFactory();
|
||||
|
||||
// Default factory should create realtime bars (TimeBarBuilderFactory(true))
|
||||
BarBuilder builder = factory.createBarBuilder(series);
|
||||
assertNotNull(builder);
|
||||
}
|
||||
|
||||
// ==================== UNNAMED_SERIES_NAME Constant Tests ====================
|
||||
|
||||
@Test
|
||||
public void testUnnamedSeriesNameConstant() {
|
||||
ConcurrentBarSeries series1 = new ConcurrentBarSeriesBuilder().build();
|
||||
ConcurrentBarSeries series2 = new ConcurrentBarSeriesBuilder().withName(null).build();
|
||||
|
||||
assertEquals("unnamed_series", series1.getName());
|
||||
assertEquals("unnamed_series", series2.getName());
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
private List<Bar> createTestBars(int count) {
|
||||
List<Bar> bars = new ArrayList<>();
|
||||
Instant baseTime = Instant.parse("2024-01-01T00:00:00Z");
|
||||
for (int i = 0; i < count; i++) {
|
||||
bars.add(createTestBar(i, baseTime));
|
||||
}
|
||||
return bars;
|
||||
}
|
||||
|
||||
private Bar createTestBar(int index) {
|
||||
return createTestBar(index, Instant.parse("2024-01-01T00:00:00Z"));
|
||||
}
|
||||
|
||||
private Bar createTestBar(int index, Instant baseTime) {
|
||||
return new TimeBarBuilder(numFactory).timePeriod(Duration.ofDays(1))
|
||||
.endTime(baseTime.plus(Duration.ofDays(index)))
|
||||
.openPrice(numOf(index + 1))
|
||||
.highPrice(numOf(index + 2))
|
||||
.lowPrice(numOf(index))
|
||||
.closePrice(numOf(index + 1.5))
|
||||
.volume(numOf(index * 100))
|
||||
.amount(numOf(index * 1000))
|
||||
.trades(index * 10L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CriterionFactory {
|
||||
|
||||
/**
|
||||
* Applies parameters to a CriterionFactory and returns the AnalysisCriterion.
|
||||
*
|
||||
* @param params criteria parameters
|
||||
* @return AnalysisCriterion with the parameters applied
|
||||
*/
|
||||
AnalysisCriterion getCriterion(Object... params);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.mocks.MockIndicator;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import com.opencsv.CSVParserBuilder;
|
||||
import com.opencsv.CSVReader;
|
||||
import com.opencsv.CSVReaderBuilder;
|
||||
import com.opencsv.exceptions.CsvValidationException;
|
||||
|
||||
public class CsvTestUtils {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(CsvTestUtils.class.getName());
|
||||
|
||||
public static MockIndicator getCsvFile(Class<?> clazz, String fileName, NumFactory numFactory) {
|
||||
InputStream inputStream = clazz.getResourceAsStream(fileName);
|
||||
if (inputStream == null) {
|
||||
throw new RuntimeException("Null InputStream for file " + fileName);
|
||||
}
|
||||
try (CSVReader csvReader = new CSVReaderBuilder(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||
.withCSVParser(new CSVParserBuilder().withSeparator(',').build())
|
||||
.withSkipLines(1)
|
||||
.build()) {
|
||||
String[] line;
|
||||
|
||||
BarSeries series = new MockBarSeriesBuilder().withName("CVS series").withNumFactory(numFactory).build();
|
||||
List<Num> values = new ArrayList<>();
|
||||
|
||||
while ((line = csvReader.readNext()) != null) {
|
||||
|
||||
LocalDateTime dateTime = parseDate(line[0]);
|
||||
double open = Double.parseDouble(line[1]);
|
||||
double high = Double.parseDouble(line[2]);
|
||||
double low = Double.parseDouble(line[3]);
|
||||
double close = Double.parseDouble(line[4]);
|
||||
double volume = Double.parseDouble(line[5]);
|
||||
double ma = Double.parseDouble(line[6]);
|
||||
|
||||
Instant instant = dateTime.toInstant(ZoneOffset.UTC);
|
||||
Bar bar = series.barBuilder()
|
||||
.timePeriod(Duration.ofMinutes(1))
|
||||
.endTime(instant)
|
||||
.openPrice(numFactory.numOf(open))
|
||||
.highPrice(numFactory.numOf(high))
|
||||
.lowPrice(numFactory.numOf(low))
|
||||
.closePrice(numFactory.numOf(close))
|
||||
.volume(numFactory.numOf(volume))
|
||||
.build();
|
||||
|
||||
series.addBar(bar);
|
||||
values.add(numFactory.numOf(ma));
|
||||
}
|
||||
|
||||
return new MockIndicator(series, values);
|
||||
} catch (CsvValidationException | IOException e) {
|
||||
log.error("Error while reading CSV file", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static LocalDateTime parseDate(String dateString) {
|
||||
List<DateTimeFormatter> formatters = Arrays.asList(DateTimeFormatter.ISO_OFFSET_DATE_TIME,
|
||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME, DateTimeFormatter.ISO_LOCAL_DATE,
|
||||
DateTimeFormatter.ofPattern("MM/d/yyyy HH:mm:ss"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd"), DateTimeFormatter.ofPattern("MM/dd/yy"),
|
||||
DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||
|
||||
for (DateTimeFormatter formatter : formatters) {
|
||||
try {
|
||||
return LocalDateTime.parse(dateString, formatter);
|
||||
} catch (DateTimeParseException e) {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Could not parse date: " + dateString);
|
||||
}
|
||||
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
class DeprecatedTradeCompatibilityTest {
|
||||
|
||||
private final NumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
|
||||
@Test
|
||||
void liveTradeAndLiveTradingRecordRemainUsable() {
|
||||
LiveTradingRecord record = new LiveTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
LiveTrade trade = new LiveTrade(7, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.one(), numFactory.numOf("0.25"), ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
record.recordFill(trade);
|
||||
|
||||
Trade recordedTrade = record.getLastTrade();
|
||||
assertNotNull(recordedTrade);
|
||||
assertEquals(0, recordedTrade.getIndex());
|
||||
assertEquals(TradeType.BUY, recordedTrade.getType());
|
||||
assertEquals(numFactory.numOf("0.25"), record.getTotalFees());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executionFillContractStillRoutesThroughLiveTradingRecord() {
|
||||
LiveTradingRecord record = new LiveTradingRecord();
|
||||
ExecutionFill fill = new ExecutionFill() {
|
||||
@Override
|
||||
public Instant time() {
|
||||
return Instant.parse("2025-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num price() {
|
||||
return numFactory.hundred();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num amount() {
|
||||
return numFactory.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num fee() {
|
||||
return numFactory.numOf("0.10");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionSide side() {
|
||||
return ExecutionSide.BUY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String orderId() {
|
||||
return "order-2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String correlationId() {
|
||||
return "corr-2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int index() {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
record.recordExecutionFill(fill);
|
||||
|
||||
Trade recordedTrade = record.getLastTrade();
|
||||
assertNotNull(recordedTrade);
|
||||
assertEquals(0, recordedTrade.getIndex());
|
||||
assertEquals("order-2", recordedTrade.getOrderId());
|
||||
assertEquals(numFactory.numOf("0.10"), record.getRecordedTotalFees());
|
||||
}
|
||||
|
||||
@Test
|
||||
void simulatedTradeFactoriesStillExist() {
|
||||
SimulatedTrade trade = SimulatedTrade.buyAt(3, numFactory.hundred(), numFactory.numOf(2));
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertEquals(3, trade.getIndex());
|
||||
assertEquals(numFactory.numOf(2), trade.getAmount());
|
||||
assertEquals(numFactory.zero(), trade.getCost());
|
||||
}
|
||||
|
||||
@Test
|
||||
void positionLedgerContractStillAvailable() {
|
||||
PositionLedger ledger = new LiveTradingRecord();
|
||||
assertTrue(ledger.getPositions().isEmpty());
|
||||
assertTrue(ledger.getOpenPositions().isEmpty());
|
||||
assertNull(ledger.getNetOpenPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
void liveTradeStillImplementsExecutionFill() {
|
||||
ExecutionFill fill = new LiveTrade(4, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.one(), numFactory.numOf("0.05"), ExecutionSide.SELL, "order-3", "corr-3");
|
||||
|
||||
assertTrue(fill.hasFee());
|
||||
assertEquals(4, fill.index());
|
||||
assertEquals("corr-3", fill.intentId());
|
||||
assertEquals(ExecutionSide.SELL, fill.side());
|
||||
}
|
||||
|
||||
@Test
|
||||
void liveTradingRecordPreservesRecordedFeeSemantics() {
|
||||
LiveTradingRecord record = new LiveTradingRecord();
|
||||
record.recordFill(new LiveTrade(0, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.one(), numFactory.numOf("0.20"), ExecutionSide.BUY, null, null));
|
||||
record.recordFill(new LiveTrade(1, Instant.parse("2025-01-01T00:00:01Z"), numFactory.numOf(110),
|
||||
numFactory.one(), numFactory.numOf("0.30"), ExecutionSide.SELL, null, null));
|
||||
|
||||
assertEquals(RecordedTradeCostModel.INSTANCE, record.getTransactionCostModel());
|
||||
assertEquals(numFactory.numOf("0.50"), record.getRecordedTotalFees());
|
||||
assertFalse(record.getPositions().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ExecutionIntentTest {
|
||||
|
||||
@Test
|
||||
void rejectsBlankIntentId() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new ExecutionIntent(" ", ExecutionSide.BUY, Instant.now(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNullSide() {
|
||||
assertThrows(NullPointerException.class, () -> new ExecutionIntent("intent-1", null, Instant.now(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNullCreatedAt() {
|
||||
assertThrows(NullPointerException.class, () -> new ExecutionIntent("intent-1", ExecutionSide.BUY, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBlankCorrelationId() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new ExecutionIntent("intent-1", ExecutionSide.BUY, Instant.now(), " "));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public interface ExternalCriterionTest {
|
||||
|
||||
/**
|
||||
* Gets the BarSeries used by an external criterion calculator.
|
||||
*
|
||||
* @return BarSeries from the external criterion calculator
|
||||
* @throws Exception if the external calculator throws an Exception
|
||||
*/
|
||||
BarSeries getSeries() throws Exception;
|
||||
|
||||
/**
|
||||
* Sends criterion parameters to an external criterion calculator and returns
|
||||
* the final value of the externally calculated criterion.
|
||||
*
|
||||
* @param params criterion parameters
|
||||
* @return Num final criterion value
|
||||
* @throws Exception if the external calculator throws an Exception
|
||||
*/
|
||||
Num getFinalCriterionValue(Object... params) throws Exception;
|
||||
|
||||
/**
|
||||
* Gets the trading record used by an external criterion calculator.
|
||||
*
|
||||
* @return TradingRecord from the external criterion calculator
|
||||
* @throws Exception if the external calculator throws an Exception
|
||||
*/
|
||||
TradingRecord getTradingRecord() throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public interface ExternalIndicatorTest {
|
||||
|
||||
/**
|
||||
* Gets the BarSeries used by an external indicator calculator.
|
||||
*
|
||||
* @return BarSeries from the external indicator calculator
|
||||
* @throws Exception if the external calculator throws an Exception
|
||||
*/
|
||||
BarSeries getSeries() throws Exception;
|
||||
|
||||
/**
|
||||
* Sends indicator parameters to an external indicator calculator and returns
|
||||
* the externally calculated indicator.
|
||||
*
|
||||
* @param params indicator parameters
|
||||
* @return Indicator<Num> from the external indicator calculator
|
||||
* @throws Exception if the external calculator throws an Exception
|
||||
*/
|
||||
Indicator<Num> getIndicator(Object... params) throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class FillBackedTradeTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public FillBackedTradeTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computesWeightedAveragePriceAndTotalAmount() {
|
||||
TradeFill firstFill = new TradeFill(2, numFactory.hundred(), numFactory.two());
|
||||
TradeFill secondFill = new TradeFill(3, numFactory.numOf(110), numFactory.one());
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(firstFill, secondFill));
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertEquals(2, trade.getIndex());
|
||||
assertNumEquals(numFactory.three(), trade.getAmount());
|
||||
assertNumEquals(numFactory.numOf(103.3333333333), trade.getPricePerAsset(), 0.0001);
|
||||
assertEquals(List.of(firstFill, secondFill), trade.getFills());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsEmptyFillCollections() {
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY, List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNanFillPrice() {
|
||||
TradeFill fill = new TradeFill(1, NaN.NaN, numFactory.one());
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY, List.of(fill)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonPositiveFillAmount() {
|
||||
TradeFill zeroAmountFill = new TradeFill(1, numFactory.hundred(), numFactory.zero());
|
||||
TradeFill negativeAmountFill = new TradeFill(1, numFactory.hundred(), numFactory.minusOne());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY, List.of(zeroAmountFill)));
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY, List.of(negativeAmountFill)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesEarliestFillIndexWhenFillsAreUnordered() {
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(new TradeFill(5, numFactory.hundred(), numFactory.one()),
|
||||
new TradeFill(2, numFactory.numOf(101), numFactory.one())));
|
||||
|
||||
assertEquals(2, trade.getIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializationPreservesFills() throws Exception {
|
||||
Trade original = Trade.fromFills(TradeType.BUY,
|
||||
List.of(new TradeFill(1, numFactory.hundred(), numFactory.one()),
|
||||
new TradeFill(2, numFactory.numOf(101), numFactory.one())));
|
||||
|
||||
byte[] data;
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(original);
|
||||
objectOutput.flush();
|
||||
data = output.toByteArray();
|
||||
}
|
||||
|
||||
Trade restored;
|
||||
try (ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
ObjectInputStream objectInput = new ObjectInputStream(input)) {
|
||||
restored = (Trade) objectInput.readObject();
|
||||
}
|
||||
|
||||
assertEquals(2, restored.getFills().size());
|
||||
assertNumEquals(original.getPricePerAsset(), restored.getPricePerAsset());
|
||||
assertNumEquals(original.getAmount(), restored.getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleFillMetadataIsPreserved() {
|
||||
Instant fillTime = Instant.parse("2025-01-01T00:00:00Z");
|
||||
TradeFill fill = new TradeFill(3, fillTime, numFactory.hundred(), numFactory.one(), numFactory.numOf(0.1),
|
||||
ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(fill));
|
||||
|
||||
assertEquals(1, trade.getFills().size());
|
||||
assertEquals(fill, trade.getFills().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesRecordedFeesByDefault() {
|
||||
TradeFill firstFill = new TradeFill(1, null, numFactory.hundred(), numFactory.one(), numFactory.numOf(0.2),
|
||||
null, null, null);
|
||||
TradeFill secondFill = new TradeFill(2, null, numFactory.numOf(110), numFactory.two(), numFactory.numOf(0.3),
|
||||
null, null, null);
|
||||
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(firstFill, secondFill));
|
||||
|
||||
assertNumEquals(numFactory.numOf(0.5), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(106.8333333333), trade.getNetPrice(), 0.0001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IndicatorFactory<D, I> {
|
||||
|
||||
/**
|
||||
* Applies parameters and data to an IndicatorFactory and returns the Indicator.
|
||||
*
|
||||
* @param data source data for building the indicator
|
||||
* @param params indicator parameters
|
||||
* @return Indicator<I> with the indicator parameters applied
|
||||
*/
|
||||
Indicator<I> getIndicator(D data, Object... params);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.mocks.MockIndicator;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class IndicatorTest extends AbstractIndicatorTest<Indicator<Num>, Num> {
|
||||
|
||||
double[] typicalPrices = { 23.98, 23.92, 23.79, 23.67, 23.54, 23.36, 23.65, 23.72, 24.16, 23.91, 23.81, 23.92,
|
||||
23.74, 24.68, 24.94, 24.93, 25.10, 25.12, 25.20, 25.06, 24.50, 24.31, 24.57, 24.62, 24.49, 24.37, 24.41,
|
||||
24.35, 23.75, 24.09 };
|
||||
BarSeries data;
|
||||
|
||||
public IndicatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
data = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(typicalPrices).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toDouble() {
|
||||
List<Num> expectedValues = Arrays.stream(typicalPrices)
|
||||
.mapToObj(numFactory::numOf)
|
||||
.collect(Collectors.toList());
|
||||
MockIndicator closePriceMockIndicator = new MockIndicator(data, expectedValues);
|
||||
|
||||
int barCount = 10, index = 20;
|
||||
Double[] doubles = Indicator.toDouble(closePriceMockIndicator, index, barCount);
|
||||
assertTrue(doubles.length == barCount);
|
||||
|
||||
for (int i = 0; i < barCount; i++) {
|
||||
assertTrue(typicalPrices[i + 11] == doubles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldProvideStream() {
|
||||
List<Num> expectedValues = Arrays.stream(typicalPrices)
|
||||
.mapToObj(numFactory::numOf)
|
||||
.collect(Collectors.toList());
|
||||
MockIndicator closePriceMockIndicator = new MockIndicator(data, expectedValues);
|
||||
|
||||
Stream<Num> stream = closePriceMockIndicator.stream();
|
||||
List<Num> collectedValues = stream.collect(Collectors.toList());
|
||||
|
||||
Assert.assertNotNull(stream);
|
||||
Assert.assertNotNull(collectedValues);
|
||||
assertEquals(30, collectedValues.size());
|
||||
for (int i = 0; i < data.getBarCount(); i++) {
|
||||
assertNumEquals(typicalPrices[i], collectedValues.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.ta4j.core.num.NaN.NaN;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.LinearBorrowingCostModel;
|
||||
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class PositionTest {
|
||||
|
||||
private Position newPosition, uncoveredPosition, posEquals1, posEquals2, posNotEquals1, posNotEquals2;
|
||||
|
||||
private CostModel transactionModel;
|
||||
private CostModel holdingModel;
|
||||
private Trade enter;
|
||||
private Trade exitSameType;
|
||||
private Trade exitDifferentType;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.newPosition = new Position();
|
||||
this.uncoveredPosition = new Position(TradeType.SELL);
|
||||
|
||||
posEquals1 = new Position();
|
||||
posEquals1.operate(1);
|
||||
posEquals1.operate(2);
|
||||
|
||||
posEquals2 = new Position();
|
||||
posEquals2.operate(1);
|
||||
posEquals2.operate(2);
|
||||
|
||||
posNotEquals1 = new Position(TradeType.SELL);
|
||||
posNotEquals1.operate(1);
|
||||
posNotEquals1.operate(2);
|
||||
|
||||
posNotEquals2 = new Position(TradeType.SELL);
|
||||
posNotEquals2.operate(1);
|
||||
posNotEquals2.operate(2);
|
||||
|
||||
transactionModel = new LinearTransactionCostModel(0.01);
|
||||
holdingModel = new LinearBorrowingCostModel(0.001);
|
||||
|
||||
enter = Trade.buyAt(1, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
exitSameType = Trade.sellAt(2, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
exitDifferentType = Trade.buyAt(2, DoubleNum.valueOf(2), DoubleNum.valueOf(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenNewShouldCreateBuyOrderWhenEntering() {
|
||||
newPosition.operate(0);
|
||||
assertEquals(Trade.buyAt(0, NaN, NaN), newPosition.getEntry());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenNewShouldNotExit() {
|
||||
assertFalse(newPosition.isOpened());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenOpenedShouldCreateSellOrderWhenExiting() {
|
||||
newPosition.operate(0);
|
||||
newPosition.operate(1);
|
||||
assertEquals(Trade.sellAt(1, NaN, NaN), newPosition.getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenClosedShouldNotEnter() {
|
||||
newPosition.operate(0);
|
||||
newPosition.operate(1);
|
||||
assertTrue(newPosition.isClosed());
|
||||
newPosition.operate(2);
|
||||
assertTrue(newPosition.isClosed());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void whenExitIndexIsLessThanEntryIndexShouldThrowException() {
|
||||
newPosition.operate(3);
|
||||
newPosition.operate(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldClosePositionOnSameIndex() {
|
||||
newPosition.operate(3);
|
||||
newPosition.operate(3);
|
||||
assertTrue(newPosition.isClosed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradesSupportsEntryAndExit() {
|
||||
Position position = new Position(TradeType.BUY, RecordedTradeCostModel.INSTANCE, new ZeroCostModel());
|
||||
Trade entry = Trade.fromFills(TradeType.BUY,
|
||||
List.of(new TradeFill(1, DoubleNum.valueOf(100), DoubleNum.valueOf(1)),
|
||||
new TradeFill(2, DoubleNum.valueOf(101), DoubleNum.valueOf(1))),
|
||||
RecordedTradeCostModel.INSTANCE);
|
||||
Trade exit = Trade.fromFills(TradeType.SELL,
|
||||
List.of(new TradeFill(3, DoubleNum.valueOf(110), DoubleNum.valueOf(2))),
|
||||
RecordedTradeCostModel.INSTANCE);
|
||||
|
||||
position.operate(entry);
|
||||
position.operate(exit);
|
||||
|
||||
assertEquals(entry, position.getEntry());
|
||||
assertEquals(exit, position.getExit());
|
||||
assertTrue(position.isClosed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradeRejectsMismatchedEntryType() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
Trade entry = Trade.sellAt(1, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> position.operate(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradeRejectsMismatchedExitType() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
position.operate(1, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
Trade exit = Trade.buyAt(2, DoubleNum.valueOf(110), DoubleNum.valueOf(1));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> position.operate(exit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradeRejectsExitBeforeEntryIndex() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
position.operate(3, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
Trade exit = Trade.sellAt(2, DoubleNum.valueOf(110), DoubleNum.valueOf(1));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> position.operate(exit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradeRejectsMismatchedCostModel() {
|
||||
Position position = new Position(TradeType.BUY, transactionModel, holdingModel);
|
||||
Trade entry = Trade.fromFills(TradeType.BUY,
|
||||
List.of(new TradeFill(1, DoubleNum.valueOf(100), DoubleNum.valueOf(1))), new ZeroCostModel());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> position.operate(entry));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldThrowIllegalArgumentExceptionWhenOrderTypeIsNull() {
|
||||
new Position(null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldThrowIllegalArgumentExceptionWhenOrdersHaveSameType() {
|
||||
new Position(Trade.buyAt(0, NaN, NaN), Trade.buyAt(1, NaN, NaN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenNewShouldCreateSellOrderWhenEnteringUncovered() {
|
||||
uncoveredPosition.operate(0);
|
||||
assertEquals(Trade.sellAt(0, NaN, NaN), uncoveredPosition.getEntry());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenOpenedShouldCreateBuyOrderWhenExitingUncovered() {
|
||||
uncoveredPosition.operate(0);
|
||||
uncoveredPosition.operate(1);
|
||||
assertEquals(Trade.buyAt(1, NaN, NaN), uncoveredPosition.getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overrideToString() {
|
||||
assertEquals(posEquals1.toString(), posEquals2.toString());
|
||||
assertNotEquals(posEquals1.toString(), posNotEquals1.toString());
|
||||
assertNotEquals(posEquals1.toString(), posNotEquals2.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsForNewPositions() {
|
||||
assertEquals(newPosition, new Position());
|
||||
assertNotEquals(newPosition, new Object());
|
||||
assertNotEquals(newPosition, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsForEntryOrders() {
|
||||
Position trLeft = newPosition;
|
||||
Position trRightEquals = new Position();
|
||||
Position trRightNotEquals = new Position();
|
||||
|
||||
assertEquals(TradeType.BUY, trRightNotEquals.operate(2).getType());
|
||||
assertNotEquals(trLeft, trRightNotEquals);
|
||||
|
||||
assertEquals(TradeType.BUY, trLeft.operate(1).getType());
|
||||
assertEquals(TradeType.BUY, trRightEquals.operate(1).getType());
|
||||
assertEquals(trLeft, trRightEquals);
|
||||
|
||||
assertNotEquals(trLeft, trRightNotEquals);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsForExitOrders() {
|
||||
Position trLeft = newPosition;
|
||||
Position trRightEquals = new Position();
|
||||
Position trRightNotEquals = new Position();
|
||||
|
||||
assertEquals(TradeType.BUY, trLeft.operate(1).getType());
|
||||
assertEquals(TradeType.BUY, trRightEquals.operate(1).getType());
|
||||
assertEquals(TradeType.BUY, trRightNotEquals.operate(1).getType());
|
||||
|
||||
assertEquals(TradeType.SELL, trRightNotEquals.operate(3).getType());
|
||||
assertNotEquals(trLeft, trRightNotEquals);
|
||||
|
||||
assertEquals(TradeType.SELL, trLeft.operate(2).getType());
|
||||
assertEquals(TradeType.SELL, trRightEquals.operate(2).getType());
|
||||
assertEquals(trLeft, trRightEquals);
|
||||
|
||||
assertNotEquals(trLeft, trRightNotEquals);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetProfitForLongPositions() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
|
||||
position.operate(0, DoubleNum.valueOf(10.00), DoubleNum.valueOf(2));
|
||||
position.operate(0, DoubleNum.valueOf(12.00), DoubleNum.valueOf(2));
|
||||
|
||||
final Num profit = position.getProfit();
|
||||
|
||||
assertEquals(DoubleNum.valueOf(4.0), profit);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetProfitForShortPositions() {
|
||||
Position position = new Position(TradeType.SELL);
|
||||
|
||||
position.operate(0, DoubleNum.valueOf(12.00), DoubleNum.valueOf(2));
|
||||
position.operate(0, DoubleNum.valueOf(10.00), DoubleNum.valueOf(2));
|
||||
|
||||
final Num profit = position.getProfit();
|
||||
|
||||
assertEquals(DoubleNum.valueOf(4.0), profit);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetGrossReturnForLongPositions() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
|
||||
position.operate(0, DoubleNum.valueOf(10.00), DoubleNum.valueOf(2));
|
||||
position.operate(0, DoubleNum.valueOf(12.00), DoubleNum.valueOf(2));
|
||||
|
||||
final Num profit = position.getGrossReturn();
|
||||
|
||||
assertEquals(DoubleNum.valueOf(1.2), profit);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetGrossReturnForShortPositions() {
|
||||
Position position = new Position(TradeType.SELL);
|
||||
|
||||
position.operate(0, DoubleNum.valueOf(10.00), DoubleNum.valueOf(2));
|
||||
position.operate(0, DoubleNum.valueOf(8.00), DoubleNum.valueOf(2));
|
||||
|
||||
final Num profit = position.getGrossReturn();
|
||||
|
||||
assertEquals(DoubleNum.valueOf(1.2), profit);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetGrossReturnForLongPositionsUsingBarCloseOnNaN() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(100, 105)
|
||||
.build();
|
||||
Position position = new Position(new BaseTrade(0, TradeType.BUY, NaN, NaN),
|
||||
new BaseTrade(1, TradeType.SELL, NaN, NaN));
|
||||
assertNumEquals(DoubleNum.valueOf(1.05), position.getGrossReturn(series));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetGrossReturnForShortPositionsUsingBarCloseOnNaN() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(100, 95)
|
||||
.build();
|
||||
Position position = new Position(new BaseTrade(0, TradeType.SELL, NaN, NaN),
|
||||
new BaseTrade(1, TradeType.BUY, NaN, NaN));
|
||||
assertNumEquals(DoubleNum.valueOf(1.05), position.getGrossReturn(series));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCostModelConsistencyTrue() {
|
||||
new Position(enter, exitSameType, transactionModel, holdingModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesTransactionCostModel() {
|
||||
Position position = new Position(TradeType.BUY, transactionModel, holdingModel);
|
||||
|
||||
assertSame(transactionModel, position.getTransactionCostModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesHoldingCostModel() {
|
||||
Position position = new Position(TradeType.BUY, transactionModel, holdingModel);
|
||||
|
||||
assertSame(holdingModel, position.getHoldingCostModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openViewAccessorsExposeEntryDerivedValues() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
Trade entry = new BaseTrade(5, Instant.EPOCH, numFactory.numOf(123), numFactory.numOf(2), numFactory.numOf(0.3),
|
||||
ExecutionSide.BUY, "order-5", "corr-5");
|
||||
Position position = new Position(entry, RecordedTradeCostModel.INSTANCE, new ZeroCostModel());
|
||||
|
||||
assertEquals(ExecutionSide.BUY, position.side());
|
||||
assertNumEquals(numFactory.numOf(2), position.amount());
|
||||
assertNumEquals(numFactory.numOf(123), position.averageEntryPrice());
|
||||
assertNumEquals(numFactory.numOf(246), position.totalEntryCost());
|
||||
assertNumEquals(numFactory.numOf(0.3), position.totalFees());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openViewAccessorsReturnNullWhenPositionHasNoEntry() {
|
||||
Position position = new Position(TradeType.BUY);
|
||||
|
||||
assertNull(position.side());
|
||||
assertNull(position.amount());
|
||||
assertNull(position.averageEntryPrice());
|
||||
assertNull(position.totalEntryCost());
|
||||
assertNull(position.totalFees());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCostModelEntryInconsistent() {
|
||||
new Position(enter, exitDifferentType, new ZeroCostModel(), holdingModel);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCostModelExitInconsistent() {
|
||||
new Position(enter, exitDifferentType, transactionModel, holdingModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProfitLongNoFinalBarTest() {
|
||||
Position closedPosition = new Position(enter, exitSameType, transactionModel, holdingModel);
|
||||
Position openPosition = new Position(TradeType.BUY, transactionModel, holdingModel);
|
||||
openPosition.operate(5, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
Num profitOfClosedPosition = closedPosition.getProfit();
|
||||
Num proftOfOpenPosition = openPosition.getProfit();
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(-0.04), profitOfClosedPosition);
|
||||
assertNumEquals(DoubleNum.valueOf(0), proftOfOpenPosition);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProfitLongWithFinalBarTest() {
|
||||
Position closedPosition = new Position(enter, exitSameType, transactionModel, holdingModel);
|
||||
Position openPosition = new Position(TradeType.BUY, transactionModel, holdingModel);
|
||||
openPosition.operate(5, DoubleNum.valueOf(2), DoubleNum.valueOf(1));
|
||||
|
||||
Num profitOfClosedPosition = closedPosition.getProfit(10, DoubleNum.valueOf(12));
|
||||
Num profitOfOpenPosition = openPosition.getProfit(10, DoubleNum.valueOf(12));
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(9.98), profitOfOpenPosition);
|
||||
assertNumEquals(DoubleNum.valueOf(-0.04), profitOfClosedPosition);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProfitShortNoFinalBarTest() {
|
||||
Trade sell = Trade.sellAt(1, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
Trade buyBack = Trade.buyAt(10, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
|
||||
Position closedPosition = new Position(sell, buyBack, transactionModel, holdingModel);
|
||||
Position openPosition = new Position(TradeType.SELL, transactionModel, holdingModel);
|
||||
openPosition.operate(5, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
Num profitOfClosedPosition = closedPosition.getProfit();
|
||||
Num proftOfOpenPosition = openPosition.getProfit();
|
||||
|
||||
Num expectedHoldingCosts = DoubleNum.valueOf(2.0 * 9.0 * 0.001);
|
||||
Num expectedProfitOfClosedPosition = DoubleNum.valueOf(-0.04).minus(expectedHoldingCosts);
|
||||
|
||||
assertNumEquals(expectedProfitOfClosedPosition, profitOfClosedPosition);
|
||||
assertNumEquals(DoubleNum.valueOf(0), proftOfOpenPosition);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProfitShortWithFinalBarTest() {
|
||||
Trade sell = Trade.sellAt(1, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
Trade buyBack = Trade.buyAt(10, DoubleNum.valueOf(2), DoubleNum.valueOf(1), transactionModel);
|
||||
|
||||
Position closedPosition = new Position(sell, buyBack, transactionModel, holdingModel);
|
||||
Position openPosition = new Position(TradeType.SELL, transactionModel, holdingModel);
|
||||
openPosition.operate(5, DoubleNum.valueOf(2), DoubleNum.valueOf(1));
|
||||
|
||||
Num profitOfClosedPositionFinalAfter = closedPosition.getProfit(20, DoubleNum.valueOf(3));
|
||||
Num profitOfOpenPositionFinalAfter = openPosition.getProfit(20, DoubleNum.valueOf(3));
|
||||
Num profitOfClosedPositionFinalBefore = closedPosition.getProfit(5, DoubleNum.valueOf(3));
|
||||
Num profitOfOpenPositionFinalBefore = openPosition.getProfit(5, DoubleNum.valueOf(3));
|
||||
|
||||
Num expectedHoldingCosts = DoubleNum.valueOf(2.0 * 9.0 * 0.001);
|
||||
Num expectedProfitOfClosedPosition = DoubleNum.valueOf(-0.04).minus(expectedHoldingCosts);
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(-1.05), profitOfOpenPositionFinalAfter);
|
||||
assertNumEquals(DoubleNum.valueOf(-1.02), profitOfOpenPositionFinalBefore);
|
||||
assertNumEquals(expectedProfitOfClosedPosition, profitOfClosedPositionFinalAfter);
|
||||
assertNumEquals(expectedProfitOfClosedPosition, profitOfClosedPositionFinalBefore);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.num.DecimalNum;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
|
||||
public class SeriesBuilderTest {
|
||||
|
||||
private final BaseBarSeriesBuilder seriesBuilder = new BaseBarSeriesBuilder()
|
||||
.withNumFactory(DecimalNumFactory.getInstance());
|
||||
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
|
||||
// build a new empty unnamed bar series
|
||||
BarSeries defaultSeries = seriesBuilder.build();
|
||||
|
||||
// build a new empty bar series using BigDecimal as delegate
|
||||
BarSeries defaultSeriesName = seriesBuilder.withName("default").build();
|
||||
|
||||
BarSeries doubleSeries = seriesBuilder.withMaxBarCount(100)
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withName("useDoubleNum")
|
||||
.build();
|
||||
BarSeries precisionSeries = seriesBuilder.withMaxBarCount(100)
|
||||
.withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withName("usePrecisionNum")
|
||||
.build();
|
||||
|
||||
var now = Instant.now();
|
||||
for (int i = 1000; i >= 0; i--) {
|
||||
defaultSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(now.minusSeconds(i))
|
||||
.openPrice(i)
|
||||
.closePrice(i)
|
||||
.highPrice(i)
|
||||
.lowPrice(i)
|
||||
.volume(i)
|
||||
.add();
|
||||
defaultSeriesName.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(now.minusSeconds(i))
|
||||
.openPrice(i)
|
||||
.closePrice(i)
|
||||
.highPrice(i)
|
||||
.lowPrice(i)
|
||||
.volume(i)
|
||||
.add();
|
||||
doubleSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(now.minusSeconds(i))
|
||||
.openPrice(i)
|
||||
.closePrice(i)
|
||||
.highPrice(i)
|
||||
.lowPrice(i)
|
||||
.volume(i)
|
||||
.add();
|
||||
precisionSeries.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(now.minusSeconds(i))
|
||||
.openPrice(i)
|
||||
.closePrice(i)
|
||||
.highPrice(i)
|
||||
.lowPrice(i)
|
||||
.volume(i)
|
||||
.add();
|
||||
}
|
||||
|
||||
assertNumEquals(0, defaultSeries.getBar(1000).getClosePrice());
|
||||
assertNumEquals(1000, defaultSeries.getBar(0).getClosePrice());
|
||||
assertEquals(defaultSeriesName.getName(), "default");
|
||||
assertNumEquals(99, doubleSeries.getBar(0).getClosePrice());
|
||||
assertNumEquals(99, precisionSeries.getBar(0).getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumFunctions() {
|
||||
BarSeries series = seriesBuilder.withNumFactory(DoubleNumFactory.getInstance()).build();
|
||||
assertNumEquals(series.numFactory().numOf(12), DoubleNum.valueOf(12));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongNumType() {
|
||||
BarSeries series = seriesBuilder.withNumFactory(DecimalNumFactory.getInstance()).build();
|
||||
assertNumEquals(series.numFactory().numOf(12), DecimalNum.valueOf(12));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class StrategyStartingTypeTest {
|
||||
|
||||
@Test
|
||||
void defaultsToLong() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
assertEquals(TradeType.BUY, strategy.getStartingType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void canOverrideStartingType() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1)) {
|
||||
@Override
|
||||
public TradeType getStartingType() {
|
||||
return TradeType.SELL;
|
||||
}
|
||||
};
|
||||
assertEquals(TradeType.SELL, strategy.getStartingType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void canSetStartingTypeViaConstructor() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1), TradeType.SELL);
|
||||
assertEquals(TradeType.SELL, strategy.getStartingType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedStrategiesPreserveStartingType() {
|
||||
Strategy shortStrategy = new BaseStrategy("short", new FixedRule(0), new FixedRule(1), 0, TradeType.SELL);
|
||||
Strategy other = new BaseStrategy("other", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
assertEquals(TradeType.SELL, shortStrategy.and(other).getStartingType());
|
||||
assertEquals(TradeType.SELL, shortStrategy.or(other).getStartingType());
|
||||
assertEquals(TradeType.SELL, shortStrategy.opposite().getStartingType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.rules.AndRule;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
/**
|
||||
* Tests for trace logging in strategies, verifying that custom names are used
|
||||
* in trace logs when set, and class names are used as fallback.
|
||||
*/
|
||||
public class StrategyTraceLoggingTest {
|
||||
|
||||
private TraceTestLogger traceTestLogger;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
traceTestLogger = new TraceTestLogger();
|
||||
traceTestLogger.open();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
traceTestLogger.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingUsesClassNameWhenNoCustomNameSet() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldEnter(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should contain class name when no custom name is set",
|
||||
logContent.contains("BaseStrategy#shouldEnter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingExplainsUnstableStrategyDecision() {
|
||||
Strategy strategy = new BaseStrategy("Unstable Strategy", new FixedRule(1), new FixedRule(2), 3);
|
||||
traceTestLogger.clear();
|
||||
|
||||
assertFalse(strategy.shouldEnter(1, new BaseTradingRecord()));
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Strategy trace should include the false entry decision",
|
||||
logContent.contains(">>> Unstable Strategy#shouldEnter(1): false"));
|
||||
assertTrue("Strategy trace should explain unstable-bar suppression", logContent.contains("reason=unstable"));
|
||||
assertTrue("Strategy trace should include the unstable bar count", logContent.contains("unstableBars=3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingUsesCustomNameWhenSet() {
|
||||
Strategy strategy = new BaseStrategy("My Custom Strategy", new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldEnter(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should contain custom name when set",
|
||||
logContent.contains("My Custom Strategy#shouldEnter"));
|
||||
assertFalse("Trace log should not contain class name when custom name is set",
|
||||
logContent.contains("BaseStrategy#shouldEnter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingUsesCustomNameForShouldExit() {
|
||||
Strategy strategy = new BaseStrategy("5min Entry Strategy", new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldExit(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should contain custom name for shouldExit",
|
||||
logContent.contains("5min Entry Strategy#shouldExit"));
|
||||
assertFalse("Trace log should not contain class name when custom name is set",
|
||||
logContent.contains("BaseStrategy#shouldExit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingUsesClassNameForShouldExitWhenNoCustomName() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldExit(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should contain class name for shouldExit when no custom name is set",
|
||||
logContent.contains("BaseStrategy#shouldExit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingWorksForBothShouldEnterAndShouldExit() {
|
||||
Strategy strategy = new BaseStrategy("Multi-Timeframe Strategy", new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldEnter(0, new BaseTradingRecord());
|
||||
strategy.shouldExit(1, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should contain custom name for shouldEnter",
|
||||
logContent.contains("Multi-Timeframe Strategy#shouldEnter"));
|
||||
assertTrue("Trace log should contain custom name for shouldExit",
|
||||
logContent.contains("Multi-Timeframe Strategy#shouldExit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingWorksForMultipleStrategiesWithDifferentNames() {
|
||||
Strategy strategy1 = new BaseStrategy("Strategy 5min", new FixedRule(1), new FixedRule(2));
|
||||
Strategy strategy2 = new BaseStrategy("Strategy 15min", new FixedRule(3), new FixedRule(4));
|
||||
|
||||
traceTestLogger.clear();
|
||||
strategy1.shouldEnter(0, new BaseTradingRecord());
|
||||
strategy2.shouldEnter(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("First strategy should use its custom name in trace log",
|
||||
logContent.contains("Strategy 5min#shouldEnter"));
|
||||
assertTrue("Second strategy should use its custom name in trace log",
|
||||
logContent.contains("Strategy 15min#shouldEnter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingIncludesPrefixForStrategyTraces() {
|
||||
Strategy strategy = new BaseStrategy("Test Strategy", new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldEnter(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Trace log should include >>> prefix for strategy traces", logContent.contains(">>>"));
|
||||
assertTrue("Trace log should contain custom name after prefix",
|
||||
logContent.contains(">>> Test Strategy#shouldEnter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingFollowsStrategyLoggerTraceByDefault() {
|
||||
Strategy strategy = new BaseStrategy("Default Trace Strategy", new FixedRule(1), new FixedRule(2));
|
||||
traceTestLogger.clear();
|
||||
|
||||
strategy.shouldEnter(0, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("TRACE logging should emit strategy decisions without mutable trace state",
|
||||
logContent.contains("Default Trace Strategy#shouldEnter"));
|
||||
assertTrue("Default strategy traces should use verbose mode", logContent.contains("mode=VERBOSE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingSummaryModeEvaluatesEntryRuleWithScopedTracePolicy() {
|
||||
FixedRule child1 = new FixedRule(1);
|
||||
child1.setName("Entry Child 1");
|
||||
FixedRule child2 = new FixedRule(1);
|
||||
child2.setName("Entry Child 2");
|
||||
AndRule entryRule = new AndRule(child1, child2);
|
||||
entryRule.setName("Entry Composite");
|
||||
Strategy strategy = new BaseStrategy("Trace Strategy", entryRule, new FixedRule(2));
|
||||
|
||||
traceTestLogger.clear();
|
||||
strategy.shouldEnterWithTraceMode(1, new BaseTradingRecord(), Rule.TraceMode.SUMMARY);
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Summary mode should log the strategy decision",
|
||||
logContent.contains(">>> Trace Strategy#shouldEnter"));
|
||||
assertTrue("Summary mode should log the entry rule", logContent.contains("Entry Composite#isSatisfied"));
|
||||
assertTrue("Summary mode should mark the scoped root path",
|
||||
logContent.contains("mode=SUMMARY ruleType=AndRule path=root depth=0"));
|
||||
assertFalse("Summary mode should suppress first child logs", logContent.contains("Entry Child 1#isSatisfied"));
|
||||
assertFalse("Summary mode should suppress second child logs", logContent.contains("Entry Child 2#isSatisfied"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingVerboseModeEvaluatesExitRuleWithScopedTracePolicy() {
|
||||
FixedRule child1 = new FixedRule(2);
|
||||
child1.setName("Exit Child 1");
|
||||
FixedRule child2 = new FixedRule(2);
|
||||
child2.setName("Exit Child 2");
|
||||
AndRule exitRule = new AndRule(child1, child2);
|
||||
exitRule.setName("Exit Composite");
|
||||
Strategy strategy = new BaseStrategy("Trace Strategy", new FixedRule(1), exitRule);
|
||||
|
||||
traceTestLogger.clear();
|
||||
strategy.shouldExit(2, new BaseTradingRecord());
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Verbose mode should log the strategy exit decision",
|
||||
logContent.contains(">>> Trace Strategy#shouldExit"));
|
||||
assertTrue("Verbose mode should log the exit rule", logContent.contains("Exit Composite#isSatisfied"));
|
||||
assertTrue("Verbose mode should log first exit child", logContent.contains("Exit Child 1#isSatisfied"));
|
||||
assertTrue("Verbose mode should log second exit child", logContent.contains("Exit Child 2#isSatisfied"));
|
||||
assertTrue("Verbose mode should attribute the first child path",
|
||||
logContent.contains("path=root.rule1 depth=1"));
|
||||
assertTrue("Verbose mode should attribute the second child path",
|
||||
logContent.contains("path=root.rule2 depth=1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingDoesNotCreateStrategyScopeWhenStrategyLoggerTraceIsDisabled() {
|
||||
FixedRule entryRule = new FixedRule(1);
|
||||
entryRule.setName("Entry Child");
|
||||
Strategy strategy = new BaseStrategy("Trace Strategy", entryRule, new FixedRule(2));
|
||||
|
||||
traceTestLogger.setLoggerLevel(BaseStrategy.class, Level.INFO);
|
||||
traceTestLogger.setLoggerLevel(FixedRule.class, Level.TRACE);
|
||||
traceTestLogger.clear();
|
||||
try {
|
||||
strategy.shouldEnter(1, new BaseTradingRecord());
|
||||
} finally {
|
||||
traceTestLogger.clearLoggerLevel(FixedRule.class);
|
||||
traceTestLogger.clearLoggerLevel(BaseStrategy.class);
|
||||
}
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertFalse("Strategy should not emit strategy logs when the strategy logger is not tracing",
|
||||
logContent.contains(">>> Trace Strategy#shouldEnter"));
|
||||
assertTrue("A TRACE-enabled child logger should still emit its own default trace",
|
||||
logContent.contains("Entry Child#isSatisfied"));
|
||||
assertTrue("Child trace should not inherit a strategy parent frame", logContent.contains("path=root depth=0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traceLoggingCanBeScopedToSingleStrategyEvaluationWithoutMutatingStrategyMode() {
|
||||
FixedRule child1 = new FixedRule(1);
|
||||
child1.setName("Scoped Entry Child 1");
|
||||
FixedRule child2 = new FixedRule(1);
|
||||
child2.setName("Scoped Entry Child 2");
|
||||
AndRule entryRule = new AndRule(child1, child2);
|
||||
entryRule.setName("Scoped Entry Composite");
|
||||
Strategy strategy = new BaseStrategy("Scoped Strategy", entryRule, new FixedRule(2));
|
||||
|
||||
traceTestLogger.clear();
|
||||
assertTrue(strategy.shouldEnterWithTraceMode(1, new BaseTradingRecord(), Rule.TraceMode.VERBOSE));
|
||||
|
||||
String logContent = traceTestLogger.getLogOutput();
|
||||
assertTrue("Scoped verbose evaluation should log the strategy decision",
|
||||
logContent.contains(">>> Scoped Strategy#shouldEnter"));
|
||||
assertTrue("Scoped verbose evaluation should log the entry composite",
|
||||
logContent.contains("Scoped Entry Composite#isSatisfied"));
|
||||
assertTrue("Scoped verbose evaluation should log first child",
|
||||
logContent.contains("Scoped Entry Child 1#isSatisfied"));
|
||||
assertTrue("Scoped verbose evaluation should log second child",
|
||||
logContent.contains("Scoped Entry Child 2#isSatisfied"));
|
||||
|
||||
traceTestLogger.clear();
|
||||
assertTrue(strategy.shouldEnter(1, new BaseTradingRecord()));
|
||||
assertTrue("A scoped strategy evaluation should not suppress later default TRACE behavior",
|
||||
traceTestLogger.getLogOutput().contains("mode=VERBOSE"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.num.DecimalNum;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Utility class for {@code Num} tests.
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
/** Offset for double equality checking */
|
||||
public static final double GENERAL_OFFSET = 0.0001;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TestUtils.class);
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is equal to the given
|
||||
* {@code String} representation.
|
||||
*
|
||||
* @param expected the given {@code String} representation to compare the actual
|
||||
* value to
|
||||
* @param actual the actual {@code Num} value
|
||||
* @throws AssertionError if the actual value is not equal to the given
|
||||
* {@code String} representation
|
||||
*/
|
||||
public static void assertNumEquals(String expected, Num actual) {
|
||||
assertEquals(actual.getNumFactory().numOf(new BigDecimal(expected)), actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is equal to the given {@code Num}.
|
||||
*
|
||||
* @param expected the given {@code Num} representation to compare the actual
|
||||
* value to
|
||||
* @param actual the actual {@code Num} value
|
||||
* @throws AssertionError if the actual value is not equal to the given
|
||||
* {@code Num} representation
|
||||
*/
|
||||
public static void assertNumEquals(Num expected, Num actual) {
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is equal (within a positive
|
||||
* offset) to the given {@code Num} representation.
|
||||
*
|
||||
* @param expected the given {@code Num} representation to compare the actual
|
||||
* value to
|
||||
* @param actual the actual {@code Num} value
|
||||
* @param offset the allowed difference between expected and actual
|
||||
* @throws AssertionError if the actual value is not equal to the given
|
||||
* {@code Num} representation within the offset
|
||||
*/
|
||||
public static void assertNumEquals(Num expected, Num actual, double offset) {
|
||||
if (Num.isNaNOrNull(expected) || Num.isNaNOrNull(actual)) {
|
||||
boolean expectedIsNaN = Num.isNaNOrNull(expected);
|
||||
boolean actualIsNaN = Num.isNaNOrNull(actual);
|
||||
if (expectedIsNaN != actualIsNaN) {
|
||||
throw new AssertionError("Expected: " + expected + " Actual: " + actual);
|
||||
}
|
||||
return;
|
||||
}
|
||||
assertEquals(expected.doubleValue(), actual.doubleValue(), offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is equal to the given {@code int}
|
||||
* representation.
|
||||
*
|
||||
*
|
||||
* @param expected the given {@code int} representation to compare the actual
|
||||
* value to
|
||||
* @param actual the actual {@code Num} value
|
||||
* @throws AssertionError if the actual value is not equal to the given
|
||||
* {@code int} representation
|
||||
*/
|
||||
public static void assertNumEquals(int expected, Num actual) {
|
||||
if (actual.isNaN()) {
|
||||
throw new AssertionError("Expected: " + expected + " Actual: " + actual);
|
||||
}
|
||||
assertEquals(actual.getNumFactory().numOf(expected), actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is equal (within a positive
|
||||
* offset) to the given {@code double} representation.
|
||||
*
|
||||
* @param expected the given {@code double} representation to compare the actual
|
||||
* value to
|
||||
* @param actual the actual {@code Num} value
|
||||
* @throws AssertionError if the actual value is not equal to the given
|
||||
* {@code double} representation
|
||||
*/
|
||||
public static void assertNumEquals(double expected, Num actual) {
|
||||
assertEquals(expected, actual.doubleValue(), GENERAL_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is not equal to the given
|
||||
* {@code int} representation.
|
||||
*
|
||||
* @param actual the actual {@code Num} value
|
||||
* @param unexpected the given {@code int} representation to compare the actual
|
||||
* value to
|
||||
* @throws AssertionError if the actual value is equal to the given {@code int}
|
||||
* representation
|
||||
*/
|
||||
public static void assertNumNotEquals(int unexpected, Num actual) {
|
||||
assertNotEquals(actual.getNumFactory().numOf(unexpected), actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that two indicators have the same size and values to an offset
|
||||
*
|
||||
* @param expected indicator of expected values
|
||||
* @param actual indicator of actual values
|
||||
*/
|
||||
public static void assertIndicatorEquals(Indicator<Num> expected, Indicator<Num> actual) {
|
||||
org.junit.Assert.assertEquals("Size does not match,", expected.getBarSeries().getBarCount(),
|
||||
actual.getBarSeries().getBarCount());
|
||||
for (int i = 0; i < expected.getBarSeries().getBarCount(); i++) {
|
||||
Num expectedValue = expected.getValue(i);
|
||||
Num actualValue = actual.getValue(i);
|
||||
|
||||
// Handle NaN values - if both are NaN, they match; if only one is NaN, they
|
||||
// don't match
|
||||
if (Num.isNaNOrNull(expectedValue) || Num.isNaNOrNull(actualValue)) {
|
||||
boolean expectedIsNaN = Num.isNaNOrNull(expectedValue);
|
||||
boolean actualIsNaN = Num.isNaNOrNull(actualValue);
|
||||
if (expectedIsNaN != actualIsNaN) {
|
||||
throw new AssertionError(String.format("Failed at index %s: %s expected %s but actual was %s", i,
|
||||
actual.toString(), expectedValue, actualValue));
|
||||
}
|
||||
// Both are NaN, continue to next index
|
||||
continue;
|
||||
}
|
||||
|
||||
assertEquals(String.format("Failed at index %s: %s", i, actual.toString()), expectedValue.doubleValue(),
|
||||
actualValue.doubleValue(), GENERAL_OFFSET);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that two indicators have either different size or different values
|
||||
* to an offset
|
||||
*
|
||||
* @param expected indicator of expected values
|
||||
* @param actual indicator of actual values
|
||||
*/
|
||||
public static void assertIndicatorNotEquals(Indicator<Num> expected, Indicator<Num> actual) {
|
||||
if (expected.getBarSeries().getBarCount() != actual.getBarSeries().getBarCount())
|
||||
return;
|
||||
for (int i = 0; i < expected.getBarSeries().getBarCount(); i++) {
|
||||
if (Math.abs(expected.getValue(i).doubleValue() - actual.getValue(i).doubleValue()) > GENERAL_OFFSET)
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Indicators match to " + GENERAL_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is not equal to the given
|
||||
* {@code String} representation.
|
||||
*
|
||||
* @param actual the actual {@code Num} value
|
||||
* @param expected the given {@code String} representation to compare the actual
|
||||
* value to
|
||||
* @throws AssertionError if the actual value is equal to the given
|
||||
* {@code String} representation
|
||||
*/
|
||||
public static void assertNumNotEquals(String expected, Num actual) {
|
||||
assertNotEquals(actual.getNumFactory().numOf(new BigDecimal(expected)), actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is not equal to the given
|
||||
* {@code Num}.
|
||||
*
|
||||
* @param actual the actual {@code Num} value
|
||||
* @param expected the given {@code Num} representation to compare the actual
|
||||
* value to
|
||||
* @throws AssertionError if the actual value is equal to the given {@code Num}
|
||||
* representation
|
||||
*/
|
||||
public static void assertNumNotEquals(Num expected, Num actual) {
|
||||
assertNotEquals(expected, actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@code Num} value is not equal (within a positive
|
||||
* offset) to the given {@code double} representation.
|
||||
*
|
||||
* @param actual the actual {@code Num} value
|
||||
* @param expected the given {@code double} representation to compare the actual
|
||||
* value to
|
||||
* @throws AssertionError if the actual value is equal to the given
|
||||
* {@code double} representation
|
||||
*/
|
||||
public static void assertNumNotEquals(double expected, Num actual) {
|
||||
assertNotEquals(expected, actual.doubleValue(), GENERAL_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that two indicators have the same size and values
|
||||
*
|
||||
* @param expected indicator of expected values
|
||||
* @param actual indicator of actual values
|
||||
*/
|
||||
public static void assertIndicatorEquals(Indicator<Num> expected, Indicator<Num> actual, Num delta) {
|
||||
org.junit.Assert.assertEquals("Size does not match,", expected.getBarSeries().getBarCount(),
|
||||
actual.getBarSeries().getBarCount());
|
||||
for (int i = expected.getBarSeries().getBeginIndex(); i < expected.getBarSeries().getEndIndex(); i++) {
|
||||
Num expectedValue = expected.getValue(i);
|
||||
Num actualValue = actual.getValue(i);
|
||||
|
||||
if (expectedValue.isNaN() || actualValue.isNaN()) {
|
||||
if (expectedValue.isNaN() && actualValue.isNaN()) {
|
||||
continue;
|
||||
}
|
||||
throw new AssertionError(String.format("Failed at index %s: expected %s but actual was %s", i,
|
||||
expectedValue, actualValue));
|
||||
}
|
||||
|
||||
// convert to DecimalNum via String (auto-precision) avoids Cast Class
|
||||
// Exception
|
||||
Num exp = DecimalNum.valueOf(expectedValue.toString());
|
||||
Num act = DecimalNum.valueOf(actualValue.toString());
|
||||
Num result = exp.minus(act).abs();
|
||||
if (result.isGreaterThan(delta)) {
|
||||
log.debug("{} expected does not match", exp);
|
||||
log.debug("{} actual", act);
|
||||
log.debug("{} offset", delta);
|
||||
String expString = exp.toString();
|
||||
String actString = act.toString();
|
||||
int minLen = Math.min(expString.length(), actString.length());
|
||||
if (expString.length() > minLen)
|
||||
expString = expString.substring(0, minLen) + "..";
|
||||
if (actString.length() > minLen)
|
||||
actString = actString.substring(0, minLen) + "..";
|
||||
throw new AssertionError(
|
||||
String.format("Failed at index %s: expected %s but actual was %s", i, expString, actString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that two indicators have either different size or different values
|
||||
* to an offset
|
||||
*
|
||||
* @param expected indicator of expected values
|
||||
* @param actual indicator of actual values
|
||||
* @param delta num offset to which the indicators must be different
|
||||
*/
|
||||
public static void assertIndicatorNotEquals(Indicator<Num> expected, Indicator<Num> actual, Num delta) {
|
||||
if (expected.getBarSeries().getBarCount() != actual.getBarSeries().getBarCount()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < expected.getBarSeries().getBarCount(); i++) {
|
||||
Num expectedValue = expected.getValue(i);
|
||||
Num actualValue = actual.getValue(i);
|
||||
|
||||
// Handle potential NaN values in double representations
|
||||
if (expectedValue.isNaN() || actualValue.isNaN()) {
|
||||
if (!expectedValue.isNaN() || !actualValue.isNaN()) {
|
||||
return; // Found a NaN mismatch - test passes
|
||||
}
|
||||
continue; // Both NaNs, continue checking other values
|
||||
}
|
||||
|
||||
Num exp = DecimalNum.valueOf(expectedValue.toString());
|
||||
Num act = DecimalNum.valueOf(actualValue.toString());
|
||||
Num result = exp.minus(act).abs();
|
||||
if (result.isGreaterThan(delta)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Indicators match to " + delta);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.ta4j.core.TestUtils.assertIndicatorEquals;
|
||||
import static org.ta4j.core.TestUtils.assertIndicatorNotEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumNotEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class TestUtilsTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private static final String stringDouble = "1234567890.12345";
|
||||
private static final String diffStringDouble = "1234567890.12346";
|
||||
private static final BigDecimal bigDecimalDouble = new BigDecimal(stringDouble);
|
||||
private static final BigDecimal diffBigDecimalDouble = new BigDecimal(diffStringDouble);
|
||||
private static final int aInt = 1234567890;
|
||||
private static final int diffInt = 1234567891;
|
||||
private static final double aDouble = 1234567890.1234;
|
||||
private static final double diffDouble = 1234567890.1235;
|
||||
private static Num numStringDouble;
|
||||
private static Num diffNumStringDouble;
|
||||
private static Num numInt;
|
||||
private static Num diffNumInt;
|
||||
private static Num numDouble;
|
||||
private static Num diffNumDouble;
|
||||
private static Indicator<Num> indicator;
|
||||
private static Indicator<Num> diffIndicator;
|
||||
|
||||
public TestUtilsTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
numStringDouble = numOf(bigDecimalDouble);
|
||||
diffNumStringDouble = numOf(diffBigDecimalDouble);
|
||||
numInt = numOf(aInt);
|
||||
diffNumInt = numOf(diffInt);
|
||||
numDouble = numOf(aDouble);
|
||||
diffNumDouble = numOf(diffDouble);
|
||||
BarSeries series = randomSeries();
|
||||
BarSeries diffSeries = randomSeries();
|
||||
indicator = new ClosePriceIndicator(series);
|
||||
diffIndicator = new ClosePriceIndicator(diffSeries);
|
||||
}
|
||||
|
||||
private BarSeries randomSeries() {
|
||||
var series = new BaseBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
var time = Instant.parse("1970-01-01T01:01:01Z");
|
||||
double random;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
random = Math.random();
|
||||
time = time.plus(Duration.ofDays(i));
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(time)
|
||||
.openPrice(random)
|
||||
.closePrice(random)
|
||||
.highPrice(random)
|
||||
.lowPrice(random)
|
||||
.amount(random)
|
||||
.volume(random)
|
||||
.trades(0)
|
||||
.add();
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringNum() {
|
||||
assertNumEquals(stringDouble, numStringDouble);
|
||||
assertNumNotEquals(stringDouble, diffNumStringDouble);
|
||||
assertNumNotEquals(diffStringDouble, numStringDouble);
|
||||
assertNumEquals(diffStringDouble, diffNumStringDouble);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumNum() {
|
||||
assertNumEquals(numStringDouble, numStringDouble);
|
||||
assertNumNotEquals(numStringDouble, diffNumStringDouble);
|
||||
assertNumNotEquals(diffNumStringDouble, numStringDouble);
|
||||
assertNumEquals(diffNumStringDouble, diffNumStringDouble);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntNum() {
|
||||
assertNumEquals(aInt, numInt);
|
||||
assertNumNotEquals(aInt, diffNumInt);
|
||||
assertNumNotEquals(diffInt, numInt);
|
||||
assertNumEquals(diffInt, diffNumInt);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleNum() {
|
||||
assertNumEquals(aDouble, numDouble);
|
||||
assertNumNotEquals(aDouble, diffNumDouble);
|
||||
assertNumNotEquals(diffDouble, numDouble);
|
||||
assertNumEquals(diffDouble, diffNumDouble);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndicator() {
|
||||
assertIndicatorEquals(indicator, indicator);
|
||||
assertIndicatorNotEquals(indicator, diffIndicator);
|
||||
assertIndicatorNotEquals(diffIndicator, indicator);
|
||||
assertIndicatorEquals(diffIndicator, diffIndicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Shared support for capturing trace logs in black-box tests.
|
||||
*/
|
||||
public final class TraceTestLogger {
|
||||
|
||||
private final Set<String> configuredLoggerNames = new HashSet<>();
|
||||
private LoggerContext loggerContext;
|
||||
private StringWriter logOutput;
|
||||
private Appender appender;
|
||||
private Appender consoleAppender;
|
||||
private Level originalLevel;
|
||||
private LoggerConfig rootLoggerConfig;
|
||||
|
||||
public void open() {
|
||||
loggerContext = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
rootLoggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
|
||||
originalLevel = rootLoggerConfig.getLevel();
|
||||
|
||||
consoleAppender = rootLoggerConfig.getAppenders().get("Console");
|
||||
if (consoleAppender != null) {
|
||||
rootLoggerConfig.removeAppender("Console");
|
||||
}
|
||||
|
||||
rootLoggerConfig.setLevel(Level.TRACE);
|
||||
loggerContext.updateLoggers();
|
||||
|
||||
logOutput = new StringWriter();
|
||||
PatternLayout layout = PatternLayout.newBuilder().withPattern("%msg%n").build();
|
||||
appender = WriterAppender.newBuilder().setTarget(logOutput).setLayout(layout).setName("TestAppender").build();
|
||||
appender.start();
|
||||
rootLoggerConfig.addAppender(appender, Level.TRACE, null);
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (loggerContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appender != null) {
|
||||
appender.stop();
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
|
||||
loggerConfig.removeAppender(appender.getName());
|
||||
}
|
||||
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
for (String loggerName : configuredLoggerNames) {
|
||||
config.removeLogger(loggerName);
|
||||
}
|
||||
configuredLoggerNames.clear();
|
||||
|
||||
if (consoleAppender != null) {
|
||||
rootLoggerConfig.addAppender(consoleAppender, null, null);
|
||||
}
|
||||
|
||||
if (originalLevel != null) {
|
||||
LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
|
||||
loggerConfig.setLevel(originalLevel);
|
||||
}
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
|
||||
public void setLoggerLevel(Class<?> loggerClass, Level level) {
|
||||
String loggerName = loggerClass.getName();
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
config.removeLogger(loggerName);
|
||||
config.addLogger(loggerName, new LoggerConfig(loggerName, level, true));
|
||||
configuredLoggerNames.add(loggerName);
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
|
||||
public void clearLoggerLevel(Class<?> loggerClass) {
|
||||
String loggerName = loggerClass.getName();
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
config.removeLogger(loggerName);
|
||||
configuredLoggerNames.remove(loggerName);
|
||||
loggerContext.updateLoggers();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
logOutput.getBuffer().setLength(0);
|
||||
}
|
||||
|
||||
public String getLogOutput() {
|
||||
return logOutput.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
class TradeFillExecutionTest {
|
||||
|
||||
@Test
|
||||
void operateAcceptsTradeFillDirectly() {
|
||||
Num price = DoubleNumFactory.getInstance().numOf(100);
|
||||
Num amount = DoubleNumFactory.getInstance().numOf(1);
|
||||
TradeFill fill = new TradeFill(5, Instant.parse("2025-01-01T00:00:00Z"), price, amount, null, ExecutionSide.BUY,
|
||||
"order-1", "corr-1");
|
||||
|
||||
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
record.operate(fill);
|
||||
|
||||
assertEquals(1, record.getTrades().size());
|
||||
assertEquals("corr-1", record.getTrades().get(0).getCorrelationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void operateFillKeepsExplicitIndexFromTradeFill() {
|
||||
Num price = DoubleNumFactory.getInstance().numOf(101);
|
||||
Num amount = DoubleNumFactory.getInstance().numOf(2);
|
||||
TradeFill fill = new TradeFill(7, Instant.parse("2025-01-01T00:00:00Z"), price, amount, ExecutionSide.BUY);
|
||||
|
||||
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
record.operate(fill);
|
||||
|
||||
assertEquals(1, record.getTrades().size());
|
||||
assertEquals(7, record.getTrades().getFirst().getIndex());
|
||||
assertEquals(price, record.getTrades().getFirst().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
void operateFillRequiresExplicitSide() {
|
||||
Num price = DoubleNumFactory.getInstance().numOf(100);
|
||||
Num amount = DoubleNumFactory.getInstance().numOf(1);
|
||||
TradeFill fillWithoutSide = new TradeFill(-1, null, price, amount, null, null, null, null);
|
||||
|
||||
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> record.operate(fillWithoutSide));
|
||||
}
|
||||
|
||||
@Test
|
||||
void operateFillPreservesMissingTimeAsNull() {
|
||||
Num price = DoubleNumFactory.getInstance().numOf(100);
|
||||
Num amount = DoubleNumFactory.getInstance().numOf(1);
|
||||
TradeFill fillWithoutTime = new TradeFill(-1, null, price, amount, null, ExecutionSide.BUY, null, null);
|
||||
|
||||
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
record.operate(fillWithoutTime);
|
||||
|
||||
assertEquals(1, record.getTrades().size());
|
||||
assertNull(record.getTrades().getFirst().getTime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.time.Instant;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class TradeFillTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public TradeFillTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storesFillAttributes() {
|
||||
TradeFill fill = new TradeFill(3, numFactory.hundred(), numFactory.two());
|
||||
|
||||
assertEquals(3, fill.index());
|
||||
assertEquals(numFactory.hundred(), fill.price());
|
||||
assertEquals(numFactory.two(), fill.amount());
|
||||
assertEquals(numFactory.zero(), fill.fee());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storesOptionalMetadataWhenProvided() {
|
||||
Instant time = Instant.parse("2025-01-01T00:00:00Z");
|
||||
TradeFill fill = new TradeFill(4, time, numFactory.hundred(), numFactory.one(), numFactory.numOf(0.2),
|
||||
ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
assertEquals(time, fill.time());
|
||||
assertEquals(ExecutionSide.BUY, fill.side());
|
||||
assertEquals("order-1", fill.orderId());
|
||||
assertEquals("corr-1", fill.correlationId());
|
||||
assertEquals(numFactory.numOf(0.2), fill.fee());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sideAndTimeConstructorKeepsMetadataAndDefaultsFeeToZero() {
|
||||
Instant time = Instant.parse("2025-01-02T00:00:00Z");
|
||||
TradeFill fill = new TradeFill(5, time, numFactory.numOf(110), numFactory.one(), ExecutionSide.SELL);
|
||||
|
||||
assertEquals(time, fill.time());
|
||||
assertEquals(ExecutionSide.SELL, fill.side());
|
||||
assertEquals(numFactory.zero(), fill.fee());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsNegativeIndexForDeferredAssignment() {
|
||||
TradeFill fill = new TradeFill(-1, numFactory.hundred(), numFactory.one());
|
||||
|
||||
assertEquals(-1, fill.index());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsIndexBelowDeferredAssignmentSentinel() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new TradeFill(-2, numFactory.hundred(), numFactory.one()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNullPriceOrAmount() {
|
||||
assertThrows(NullPointerException.class, () -> new TradeFill(1, null, numFactory.one()));
|
||||
assertThrows(NullPointerException.class, () -> new TradeFill(1, numFactory.one(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsSerializationRoundTrip() throws Exception {
|
||||
TradeFill original = new TradeFill(3, numFactory.hundred(), numFactory.two());
|
||||
|
||||
byte[] data;
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(original);
|
||||
objectOutput.flush();
|
||||
data = output.toByteArray();
|
||||
}
|
||||
|
||||
TradeFill restored;
|
||||
try (ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
ObjectInputStream objectInput = new ObjectInputStream(input)) {
|
||||
restored = (TradeFill) objectInput.readObject();
|
||||
}
|
||||
|
||||
assertEquals(original, restored);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.ta4j.core.num.NaN.NaN;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.FixedTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class TradeTest {
|
||||
|
||||
Trade opEquals1, opEquals2, opNotEquals1, opNotEquals2;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
opEquals1 = Trade.buyAt(1, NaN, NaN);
|
||||
opEquals2 = Trade.buyAt(1, NaN, NaN);
|
||||
|
||||
opNotEquals1 = Trade.sellAt(1, NaN, NaN);
|
||||
opNotEquals2 = Trade.buyAt(2, NaN, NaN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void type() {
|
||||
assertEquals(TradeType.SELL, opNotEquals1.getType());
|
||||
assertFalse(opNotEquals1.isBuy());
|
||||
assertTrue(opNotEquals1.isSell());
|
||||
assertEquals(TradeType.BUY, opNotEquals2.getType());
|
||||
assertTrue(opNotEquals2.isBuy());
|
||||
assertFalse(opNotEquals2.isSell());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overrideToString() {
|
||||
assertEquals(opEquals1.toString(), opEquals2.toString());
|
||||
|
||||
assertNotEquals(opEquals1.toString(), opNotEquals1.toString());
|
||||
assertNotEquals(opEquals1.toString(), opNotEquals2.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeWithCostsTest() {
|
||||
var transactionCostModel = new LinearTransactionCostModel(0.05);
|
||||
var trade = new BaseTrade(0, TradeType.BUY, DoubleNum.valueOf(100), DoubleNum.valueOf(20),
|
||||
transactionCostModel);
|
||||
Num expectedCost = DoubleNum.valueOf(100);
|
||||
Num expectedValue = DoubleNum.valueOf(2000);
|
||||
Num expectedRawPrice = DoubleNum.valueOf(100);
|
||||
Num expectedNetPrice = DoubleNum.valueOf(105);
|
||||
|
||||
assertNumEquals(expectedCost, trade.getCost());
|
||||
assertNumEquals(expectedValue, trade.getValue());
|
||||
assertNumEquals(expectedRawPrice, trade.getPricePerAsset());
|
||||
assertNumEquals(expectedNetPrice, trade.getNetPrice());
|
||||
assertTrue(transactionCostModel.equals(trade.getCostModel()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simulatedTradeSerializationKeepsCostModelAccessible() throws Exception {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade original = Trade.buyAt(0, numFactory.hundred(), numFactory.one(), new FixedTransactionCostModel(1.0));
|
||||
|
||||
byte[] data;
|
||||
try (var output = new ByteArrayOutputStream(); var objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(original);
|
||||
objectOutput.flush();
|
||||
data = output.toByteArray();
|
||||
}
|
||||
|
||||
Trade restored;
|
||||
try (var input = new ByteArrayInputStream(data); var objectInput = new ObjectInputStream(input)) {
|
||||
restored = (Trade) objectInput.readObject();
|
||||
}
|
||||
|
||||
assertNotNull(restored.getCostModel());
|
||||
assertNumEquals(original.getCost(), restored.getCost());
|
||||
assertNumEquals(original.getNetPrice(), restored.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnBarSeriesCloseOnNaN() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(100, 95, 100, 80, 85, 130)
|
||||
.build();
|
||||
Trade trade = new BaseTrade(1, TradeType.BUY, NaN);
|
||||
assertNumEquals(DoubleNum.valueOf(95), trade.getPricePerAsset(series));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factoryBuyAtSeriesOverloads() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(100, 110)
|
||||
.build();
|
||||
NumFactory numFactory = series.numFactory();
|
||||
|
||||
Trade trade = Trade.buyAt(1, series);
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(110), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.one(), trade.getAmount());
|
||||
assertNumEquals(numFactory.numOf(110), trade.getNetPrice());
|
||||
assertEquals(series.getBar(1).getEndTime(), trade.getFills().getFirst().time());
|
||||
assertEquals(ExecutionSide.BUY, trade.getFills().getFirst().side());
|
||||
|
||||
Trade tradeWithAmount = Trade.buyAt(1, series, numFactory.two());
|
||||
assertEquals(TradeType.BUY, tradeWithAmount.getType());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithAmount.getPricePerAsset());
|
||||
assertNumEquals(numFactory.two(), tradeWithAmount.getAmount());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithAmount.getNetPrice());
|
||||
|
||||
var costModel = new FixedTransactionCostModel(1.0);
|
||||
Trade tradeWithCost = Trade.buyAt(1, series, numFactory.two(), costModel);
|
||||
assertEquals(TradeType.BUY, tradeWithCost.getType());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithCost.getPricePerAsset());
|
||||
assertNumEquals(numFactory.two(), tradeWithCost.getAmount());
|
||||
assertNumEquals(numFactory.one(), tradeWithCost.getCost());
|
||||
assertNumEquals(numFactory.numOf(110.5), tradeWithCost.getNetPrice());
|
||||
assertTrue(costModel.equals(tradeWithCost.getCostModel()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySellAtSeriesOverloads() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(100, 110)
|
||||
.build();
|
||||
NumFactory numFactory = series.numFactory();
|
||||
|
||||
Trade trade = Trade.sellAt(1, series);
|
||||
assertEquals(TradeType.SELL, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(110), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.one(), trade.getAmount());
|
||||
assertNumEquals(numFactory.numOf(110), trade.getNetPrice());
|
||||
assertEquals(series.getBar(1).getEndTime(), trade.getFills().getFirst().time());
|
||||
assertEquals(ExecutionSide.SELL, trade.getFills().getFirst().side());
|
||||
|
||||
Trade tradeWithAmount = Trade.sellAt(1, series, numFactory.two());
|
||||
assertEquals(TradeType.SELL, tradeWithAmount.getType());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithAmount.getPricePerAsset());
|
||||
assertNumEquals(numFactory.two(), tradeWithAmount.getAmount());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithAmount.getNetPrice());
|
||||
|
||||
var costModel = new FixedTransactionCostModel(1.0);
|
||||
Trade tradeWithCost = Trade.sellAt(1, series, numFactory.two(), costModel);
|
||||
assertEquals(TradeType.SELL, tradeWithCost.getType());
|
||||
assertNumEquals(numFactory.numOf(110), tradeWithCost.getPricePerAsset());
|
||||
assertNumEquals(numFactory.two(), tradeWithCost.getAmount());
|
||||
assertNumEquals(numFactory.one(), tradeWithCost.getCost());
|
||||
assertNumEquals(numFactory.numOf(109.5), tradeWithCost.getNetPrice());
|
||||
assertTrue(costModel.equals(tradeWithCost.getCostModel()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factoryBuyAtPriceAmountWithoutCostModel() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.buyAt(0, numFactory.numOf(200), numFactory.numOf(4));
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.numOf(4), trade.getAmount());
|
||||
assertNumEquals(numFactory.zero(), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySellAtPriceAmountWithoutCostModel() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.sellAt(0, numFactory.numOf(200), numFactory.numOf(4));
|
||||
|
||||
assertEquals(TradeType.SELL, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.numOf(4), trade.getAmount());
|
||||
assertNumEquals(numFactory.zero(), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factoryBuyAtPriceAmountWithCostModel() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
var costModel = new FixedTransactionCostModel(1.0);
|
||||
Trade trade = Trade.buyAt(0, numFactory.numOf(200), numFactory.numOf(4), costModel);
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.numOf(4), trade.getAmount());
|
||||
assertNumEquals(numFactory.one(), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(200.25), trade.getNetPrice());
|
||||
assertTrue(costModel.equals(trade.getCostModel()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySellAtPriceAmountWithCostModel() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
var costModel = new FixedTransactionCostModel(1.0);
|
||||
Trade trade = Trade.sellAt(0, numFactory.numOf(200), numFactory.numOf(4), costModel);
|
||||
|
||||
assertEquals(TradeType.SELL, trade.getType());
|
||||
assertNumEquals(numFactory.numOf(200), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.numOf(4), trade.getAmount());
|
||||
assertNumEquals(numFactory.one(), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(199.75), trade.getNetPrice());
|
||||
assertTrue(costModel.equals(trade.getCostModel()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultAccessorsReturnNull() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.buyAt(0, numFactory.hundred(), numFactory.one());
|
||||
|
||||
assertNull(trade.getTime());
|
||||
assertNull(trade.getId());
|
||||
assertNull(trade.getInstrument());
|
||||
assertNull(trade.getOrderId());
|
||||
assertNull(trade.getCorrelationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroAmountDoesNotAdjustNetPrice() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade buyTrade = Trade.buyAt(0, numFactory.hundred(), numFactory.zero());
|
||||
Trade sellTrade = Trade.sellAt(0, numFactory.hundred(), numFactory.zero());
|
||||
|
||||
assertNumEquals(numFactory.hundred(), buyTrade.getNetPrice());
|
||||
assertNumEquals(numFactory.hundred(), sellTrade.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultTradeFillsExposeSingleExecution() {
|
||||
var numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.buyAt(3, numFactory.hundred(), numFactory.two());
|
||||
|
||||
List<TradeFill> fills = trade.getFills();
|
||||
assertEquals(1, fills.size());
|
||||
assertEquals(3, fills.getFirst().index());
|
||||
assertNumEquals(numFactory.hundred(), fills.getFirst().price());
|
||||
assertNumEquals(numFactory.two(), fills.getFirst().amount());
|
||||
assertEquals(ExecutionSide.BUY, fills.getFirst().side());
|
||||
assertNull(fills.getFirst().time());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionFillsOfFallsBackToScalarTradeFields() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = new BaseTrade(3, TradeType.BUY, numFactory.hundred(), numFactory.two()) {
|
||||
@Override
|
||||
public List<TradeFill> getFills() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
|
||||
List<TradeFill> fills = Trade.executionFillsOf(trade);
|
||||
|
||||
assertEquals(1, fills.size());
|
||||
assertEquals(3, fills.getFirst().index());
|
||||
assertNumEquals(numFactory.hundred(), fills.getFirst().price());
|
||||
assertNumEquals(numFactory.two(), fills.getFirst().amount());
|
||||
assertEquals(ExecutionSide.BUY, fills.getFirst().side());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillsCreatesBaseTradeForSingleFill() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(new TradeFill(2, numFactory.hundred(), numFactory.one())),
|
||||
new FixedTransactionCostModel(1.0));
|
||||
|
||||
assertTrue(trade instanceof BaseTrade);
|
||||
assertEquals(1, trade.getFills().size());
|
||||
assertEquals(2, trade.getIndex());
|
||||
assertNumEquals(numFactory.hundred(), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.one(), trade.getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillsCreatesFillBackedBaseTradeForMultipleFills() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
Trade trade = Trade.fromFills(TradeType.BUY, List.of(new TradeFill(1, numFactory.hundred(), numFactory.one()),
|
||||
new TradeFill(2, numFactory.numOf(101), numFactory.one())));
|
||||
|
||||
assertTrue(trade instanceof BaseTrade);
|
||||
assertEquals(2, trade.getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillsRejectsInvalidSingleFill() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> Trade.fromFills(TradeType.BUY, List.of(new TradeFill(1, NaN, numFactory.one()))));
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY,
|
||||
List.of(new TradeFill(1, numFactory.hundred(), numFactory.zero()))));
|
||||
assertThrows(IllegalArgumentException.class, () -> Trade.fromFills(TradeType.BUY,
|
||||
List.of(new TradeFill(1, numFactory.hundred(), numFactory.minusOne()))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillInfersTypeFromSideAndUsesRecordedFees() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradeFill fill = new TradeFill(4, null, numFactory.hundred(), numFactory.two(), numFactory.numOf(0.5),
|
||||
ExecutionSide.SELL, null, null);
|
||||
|
||||
Trade trade = Trade.fromFill(fill);
|
||||
|
||||
assertEquals(TradeType.SELL, trade.getType());
|
||||
assertEquals(4, trade.getIndex());
|
||||
assertNumEquals(numFactory.hundred(), trade.getPricePerAsset());
|
||||
assertNumEquals(numFactory.two(), trade.getAmount());
|
||||
assertNumEquals(numFactory.numOf(0.5), trade.getCost());
|
||||
assertTrue(trade.getCostModel() instanceof RecordedTradeCostModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillWithExplicitTypeAcceptsMissingSide() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradeFill fill = new TradeFill(6, null, numFactory.numOf(105), numFactory.one(), numFactory.numOf(0.2), null,
|
||||
"order-6", "corr-6");
|
||||
|
||||
Trade trade = Trade.fromFill(TradeType.BUY, fill);
|
||||
|
||||
assertEquals(TradeType.BUY, trade.getType());
|
||||
assertEquals(6, trade.getIndex());
|
||||
assertEquals("order-6", trade.getOrderId());
|
||||
assertEquals("corr-6", trade.getCorrelationId());
|
||||
assertNumEquals(numFactory.numOf(0.2), trade.getCost());
|
||||
assertEquals(ExecutionSide.BUY, ((BaseTrade) trade).side());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillWithExplicitCostModelUsesProvidedCostModel() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
FixedTransactionCostModel costModel = new FixedTransactionCostModel(1.0);
|
||||
TradeFill fill = new TradeFill(8, null, numFactory.numOf(110), numFactory.numOf(4), ExecutionSide.BUY);
|
||||
|
||||
Trade trade = Trade.fromFill(fill, costModel);
|
||||
|
||||
assertSame(costModel, trade.getCostModel());
|
||||
assertNumEquals(numFactory.one(), trade.getCost());
|
||||
assertNumEquals(numFactory.numOf(110.25), trade.getNetPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromFillRejectsSideThatConflictsWithExplicitType() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradeFill fill = new TradeFill(9, null, numFactory.hundred(), numFactory.one(), ExecutionSide.SELL);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> Trade.fromFill(TradeType.BUY, fill));
|
||||
|
||||
assertEquals("fill side must match trade type at index 9", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void baseTradeToStringSupportsDecimalNum() {
|
||||
var decimalFactory = DecimalNumFactory.getInstance();
|
||||
Trade trade = Trade.buyAt(0, decimalFactory.hundred(), decimalFactory.one());
|
||||
|
||||
String json = trade.toString();
|
||||
assertTrue(json.contains("\"type\":\"BUY\""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.backtest.BarSeriesManager;
|
||||
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
class TradingRecordParityTest {
|
||||
|
||||
private final NumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
|
||||
@Test
|
||||
void operateParityLongFlow() {
|
||||
BaseTradingRecord baseRecord = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
|
||||
applySyntheticTrade(baseRecord, 1, numOf(100), numFactory.two());
|
||||
applySyntheticTrade(baseRecord, 3, numOf(120), numFactory.two());
|
||||
applySyntheticTrade(baseRecord, 5, numOf(90), numFactory.one());
|
||||
|
||||
applySyntheticTrade(liveRecord, 1, numOf(100), numFactory.two());
|
||||
applySyntheticTrade(liveRecord, 3, numOf(120), numFactory.two());
|
||||
applySyntheticTrade(liveRecord, 5, numOf(90), numFactory.one());
|
||||
|
||||
assertEquivalent(baseRecord, liveRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void operateParityShortFlow() {
|
||||
BaseTradingRecord baseRecord = new BaseTradingRecord(TradeType.SELL, new ZeroCostModel(), new ZeroCostModel());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.SELL, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
|
||||
applySyntheticTrade(baseRecord, 2, numOf(100), numFactory.one());
|
||||
applySyntheticTrade(baseRecord, 4, numOf(90), numFactory.one());
|
||||
applySyntheticTrade(baseRecord, 6, numOf(95), numFactory.one());
|
||||
applySyntheticTrade(baseRecord, 7, numOf(85), numFactory.one());
|
||||
|
||||
applySyntheticTrade(liveRecord, 2, numOf(100), numFactory.one());
|
||||
applySyntheticTrade(liveRecord, 4, numOf(90), numFactory.one());
|
||||
applySyntheticTrade(liveRecord, 6, numOf(95), numFactory.one());
|
||||
applySyntheticTrade(liveRecord, 7, numOf(85), numFactory.one());
|
||||
|
||||
assertEquivalent(baseRecord, liveRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void barSeriesManagerParityWithProvidedLiveRecord() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(10d, 20d, 30d, 15d, 25d, 35d)
|
||||
.build();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(1, 4), new FixedRule(2, 5));
|
||||
BarSeriesManager manager = new BarSeriesManager(series, new TradeOnCurrentCloseModel());
|
||||
Num unitAmount = series.numFactory().one();
|
||||
|
||||
TradingRecord baseRecord = manager.run(strategy, TradeType.BUY, unitAmount, 0, series.getEndIndex());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), 0, series.getEndIndex());
|
||||
TradingRecord liveBacktestRecord = manager.run(strategy, liveRecord, unitAmount, 0, series.getEndIndex());
|
||||
|
||||
assertEquivalent(baseRecord, liveBacktestRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialFillWeightedAverageParity() {
|
||||
BaseTradingRecord baseRecord = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
Trade aggregatedEntry = Trade.fromFills(TradeType.BUY, List.of(new TradeFill(1, numOf(100), numFactory.one()),
|
||||
new TradeFill(2, numOf(101), numFactory.two())));
|
||||
Num expectedAverage = numOf(302).dividedBy(numFactory.three());
|
||||
|
||||
baseRecord.operate(aggregatedEntry);
|
||||
liveRecord.operate(aggregatedEntry);
|
||||
|
||||
Position baseCurrent = baseRecord.getCurrentPosition();
|
||||
Position liveCurrent = liveRecord.getCurrentPosition();
|
||||
assertNotNull(baseCurrent.getEntry());
|
||||
assertNotNull(liveCurrent.getEntry());
|
||||
assertEquals(numFactory.three(), baseCurrent.getEntry().getAmount());
|
||||
assertEquals(numFactory.three(), liveCurrent.getEntry().getAmount());
|
||||
assertEquals(expectedAverage, baseCurrent.getEntry().getPricePerAsset());
|
||||
assertEquals(expectedAverage, liveCurrent.getEntry().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
void debugSnapshotParityForClosedFlow() {
|
||||
BaseTradingRecord baseRecord = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
|
||||
applySyntheticTrade(baseRecord, 1, numOf(100), numFactory.two());
|
||||
applySyntheticTrade(baseRecord, 3, numOf(120), numFactory.two());
|
||||
applySyntheticTrade(liveRecord, 1, numOf(100), numFactory.two());
|
||||
applySyntheticTrade(liveRecord, 3, numOf(120), numFactory.two());
|
||||
|
||||
BaseTradingRecord.DebugSnapshot baseSnapshot = baseRecord.debugSnapshot();
|
||||
BaseTradingRecord.DebugSnapshot liveSnapshot = liveRecord.debugSnapshot();
|
||||
|
||||
assertSnapshotEquivalent(baseSnapshot, liveSnapshot);
|
||||
assertThrows(UnsupportedOperationException.class, () -> baseSnapshot.trades().add(null));
|
||||
assertThrows(UnsupportedOperationException.class, () -> liveSnapshot.closedPositions().add(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void debugSnapshotCapturesOpenExposureParity() {
|
||||
BaseTradingRecord baseRecord = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
|
||||
applySyntheticTrade(baseRecord, 1, numOf(100), numFactory.two());
|
||||
applySyntheticTrade(liveRecord, 1, numOf(100), numFactory.two());
|
||||
|
||||
BaseTradingRecord.DebugSnapshot baseSnapshot = baseRecord.debugSnapshot();
|
||||
BaseTradingRecord.DebugSnapshot liveSnapshot = liveRecord.debugSnapshot();
|
||||
|
||||
assertSnapshotEquivalent(baseSnapshot, liveSnapshot);
|
||||
assertEquals(1, baseSnapshot.openPositions().size());
|
||||
assertEquals(numFactory.two(), baseSnapshot.netOpenPosition().amount());
|
||||
assertEquals(numOf(100), baseSnapshot.netOpenPosition().averageEntryPrice());
|
||||
}
|
||||
|
||||
private void applySyntheticTrade(TradingRecord record, int index, Num price, Num amount) {
|
||||
record.operate(index, price, amount);
|
||||
}
|
||||
|
||||
private void assertEquivalent(TradingRecord expected, TradingRecord actual) {
|
||||
assertEquals(expected.getStartingType(), actual.getStartingType());
|
||||
assertEquals(expected.isClosed(), actual.isClosed());
|
||||
assertNullableTradeEqual(expected.getLastTrade(), actual.getLastTrade());
|
||||
assertNullableTradeEqual(expected.getLastTrade(TradeType.BUY), actual.getLastTrade(TradeType.BUY));
|
||||
assertNullableTradeEqual(expected.getLastTrade(TradeType.SELL), actual.getLastTrade(TradeType.SELL));
|
||||
assertNullableTradeEqual(expected.getLastEntry(), actual.getLastEntry());
|
||||
assertNullableTradeEqual(expected.getLastExit(), actual.getLastExit());
|
||||
|
||||
assertEquals(expected.getTrades().size(), actual.getTrades().size());
|
||||
for (int i = 0; i < expected.getTrades().size(); i++) {
|
||||
Trade expectedTrade = expected.getTrades().get(i);
|
||||
Trade actualTrade = actual.getTrades().get(i);
|
||||
assertTradeEqual(expectedTrade, actualTrade);
|
||||
}
|
||||
|
||||
assertEquals(expected.getPositions().size(), actual.getPositions().size());
|
||||
for (int i = 0; i < expected.getPositions().size(); i++) {
|
||||
Position expectedPosition = expected.getPositions().get(i);
|
||||
Position actualPosition = actual.getPositions().get(i);
|
||||
assertNotNull(expectedPosition.getEntry());
|
||||
assertNotNull(actualPosition.getEntry());
|
||||
assertTradeEqual(expectedPosition.getEntry(), actualPosition.getEntry());
|
||||
if (expectedPosition.getExit() != null || actualPosition.getExit() != null) {
|
||||
assertNotNull(expectedPosition.getExit());
|
||||
assertNotNull(actualPosition.getExit());
|
||||
assertTradeEqual(expectedPosition.getExit(), actualPosition.getExit());
|
||||
}
|
||||
}
|
||||
|
||||
Position expectedCurrent = expected.getCurrentPosition();
|
||||
Position actualCurrent = actual.getCurrentPosition();
|
||||
assertEquals(expectedCurrent.isNew(), actualCurrent.isNew());
|
||||
assertEquals(expectedCurrent.isOpened(), actualCurrent.isOpened());
|
||||
assertEquals(expectedCurrent.isClosed(), actualCurrent.isClosed());
|
||||
if (expectedCurrent.getEntry() != null || actualCurrent.getEntry() != null) {
|
||||
assertNotNull(expectedCurrent.getEntry());
|
||||
assertNotNull(actualCurrent.getEntry());
|
||||
assertTradeEqual(expectedCurrent.getEntry(), actualCurrent.getEntry());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertNullableTradeEqual(Trade expected, Trade actual) {
|
||||
if (expected == null || actual == null) {
|
||||
assertEquals(expected, actual);
|
||||
return;
|
||||
}
|
||||
assertTradeEqual(expected, actual);
|
||||
}
|
||||
|
||||
private void assertTradeEqual(Trade expected, Trade actual) {
|
||||
assertEquals(expected.getType(), actual.getType());
|
||||
assertEquals(expected.getIndex(), actual.getIndex());
|
||||
assertEquals(expected.getPricePerAsset(), actual.getPricePerAsset());
|
||||
assertEquals(expected.getAmount(), actual.getAmount());
|
||||
}
|
||||
|
||||
private void assertSnapshotEquivalent(BaseTradingRecord.DebugSnapshot expected,
|
||||
BaseTradingRecord.DebugSnapshot actual) {
|
||||
assertEquals(expected.startingType(), actual.startingType());
|
||||
assertEquals(expected.totalFees(), actual.totalFees());
|
||||
assertEquals(expected.trades().size(), actual.trades().size());
|
||||
assertEquals(expected.closedPositions().size(), actual.closedPositions().size());
|
||||
assertEquals(expected.openPositions().size(), actual.openPositions().size());
|
||||
|
||||
for (int i = 0; i < expected.trades().size(); i++) {
|
||||
assertTradeEqual(expected.trades().get(i), actual.trades().get(i));
|
||||
}
|
||||
for (int i = 0; i < expected.closedPositions().size(); i++) {
|
||||
Position expectedPosition = expected.closedPositions().get(i);
|
||||
Position actualPosition = actual.closedPositions().get(i);
|
||||
assertTradeEqual(expectedPosition.getEntry(), actualPosition.getEntry());
|
||||
assertTradeEqual(expectedPosition.getExit(), actualPosition.getExit());
|
||||
}
|
||||
|
||||
Position expectedNet = expected.netOpenPosition();
|
||||
Position actualNet = actual.netOpenPosition();
|
||||
if (expectedNet == null || actualNet == null) {
|
||||
assertEquals(expectedNet, actualNet);
|
||||
return;
|
||||
}
|
||||
assertEquals(expectedNet.side(), actualNet.side());
|
||||
assertEquals(expectedNet.amount(), actualNet.amount());
|
||||
assertEquals(expectedNet.averageEntryPrice(), actualNet.averageEntryPrice());
|
||||
assertEquals(expectedNet.totalEntryCost(), actualNet.totalEntryCost());
|
||||
assertEquals(expectedNet.totalFees(), actualNet.totalFees());
|
||||
}
|
||||
|
||||
private Num numOf(Number value) {
|
||||
return numFactory.numOf(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
|
||||
public class TradingRecordTest {
|
||||
|
||||
private final DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
private TradingRecord emptyRecord, openedRecord, closedRecord;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
emptyRecord = new BaseTradingRecord();
|
||||
openedRecord = new BaseTradingRecord(buyAt(0), sellAt(3), buyAt(7));
|
||||
closedRecord = new BaseTradingRecord(buyAt(0), sellAt(3), buyAt(7), sellAt(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCurrentPosition() {
|
||||
assertTrue(emptyRecord.getCurrentPosition().isNew());
|
||||
assertTrue(openedRecord.getCurrentPosition().isOpened());
|
||||
assertTrue(closedRecord.getCurrentPosition().isNew());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operate() {
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
|
||||
record.operate(1, numFactory.hundred(), numFactory.one());
|
||||
assertTrue(record.getCurrentPosition().isOpened());
|
||||
assertEquals(0, record.getPositionCount());
|
||||
assertNull(record.getLastPosition());
|
||||
assertEquals(buyAt(1), record.getLastTrade());
|
||||
assertEquals(buyAt(1), record.getLastTrade(Trade.TradeType.BUY));
|
||||
assertNull(record.getLastTrade(Trade.TradeType.SELL));
|
||||
assertEquals(buyAt(1), record.getLastEntry());
|
||||
assertNull(record.getLastExit());
|
||||
|
||||
record.operate(3, numFactory.numOf(110), numFactory.one());
|
||||
assertTrue(record.getCurrentPosition().isNew());
|
||||
assertEquals(1, record.getPositionCount());
|
||||
assertEquals(new Position(buyAt(1), sellAt(3)), record.getLastPosition());
|
||||
assertEquals(sellAt(3), record.getLastTrade());
|
||||
assertEquals(buyAt(1), record.getLastTrade(Trade.TradeType.BUY));
|
||||
assertEquals(sellAt(3), record.getLastTrade(Trade.TradeType.SELL));
|
||||
assertEquals(buyAt(1), record.getLastEntry());
|
||||
assertEquals(sellAt(3), record.getLastExit());
|
||||
|
||||
record.operate(5, numFactory.hundred(), numFactory.one());
|
||||
assertTrue(record.getCurrentPosition().isOpened());
|
||||
assertEquals(1, record.getPositionCount());
|
||||
assertEquals(new Position(buyAt(1), sellAt(3)), record.getLastPosition());
|
||||
assertEquals(buyAt(5), record.getLastTrade());
|
||||
assertEquals(buyAt(5), record.getLastTrade(Trade.TradeType.BUY));
|
||||
assertEquals(sellAt(3), record.getLastTrade(Trade.TradeType.SELL));
|
||||
assertEquals(buyAt(5), record.getLastEntry());
|
||||
assertEquals(sellAt(3), record.getLastExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isClosed() {
|
||||
assertTrue(emptyRecord.isClosed());
|
||||
assertFalse(openedRecord.isClosed());
|
||||
assertTrue(closedRecord.isClosed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPositionCount() {
|
||||
assertEquals(0, emptyRecord.getPositionCount());
|
||||
assertEquals(1, openedRecord.getPositionCount());
|
||||
assertEquals(2, closedRecord.getPositionCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLastPosition() {
|
||||
assertNull(emptyRecord.getLastPosition());
|
||||
assertEquals(new Position(buyAt(0), sellAt(3)), openedRecord.getLastPosition());
|
||||
assertEquals(new Position(buyAt(7), sellAt(8)), closedRecord.getLastPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLastTrade() {
|
||||
// Last trade
|
||||
assertNull(emptyRecord.getLastTrade());
|
||||
assertEquals(buyAt(7), openedRecord.getLastTrade());
|
||||
assertEquals(sellAt(8), closedRecord.getLastTrade());
|
||||
// Last BUY trade
|
||||
assertNull(emptyRecord.getLastTrade(Trade.TradeType.BUY));
|
||||
assertEquals(buyAt(7), openedRecord.getLastTrade(Trade.TradeType.BUY));
|
||||
assertEquals(buyAt(7), closedRecord.getLastTrade(Trade.TradeType.BUY));
|
||||
// Last SELL trade
|
||||
assertNull(emptyRecord.getLastTrade(Trade.TradeType.SELL));
|
||||
assertEquals(sellAt(3), openedRecord.getLastTrade(Trade.TradeType.SELL));
|
||||
assertEquals(sellAt(8), closedRecord.getLastTrade(Trade.TradeType.SELL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLastEntryExit() {
|
||||
// Last entry
|
||||
assertNull(emptyRecord.getLastEntry());
|
||||
assertEquals(buyAt(7), openedRecord.getLastEntry());
|
||||
assertEquals(buyAt(7), closedRecord.getLastEntry());
|
||||
// Last exit
|
||||
assertNull(emptyRecord.getLastExit());
|
||||
assertEquals(sellAt(3), openedRecord.getLastExit());
|
||||
assertEquals(sellAt(8), closedRecord.getLastExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRecordFromSingleClosedPosition() {
|
||||
var position = new Position(buyAt(1), sellAt(4));
|
||||
|
||||
var record = new BaseTradingRecord(position);
|
||||
|
||||
assertTrue(record.getCurrentPosition().isNew());
|
||||
assertTrue(record.isClosed());
|
||||
assertEquals(1, record.getPositionCount());
|
||||
assertEquals(position, record.getLastPosition());
|
||||
assertEquals(sellAt(4), record.getLastTrade());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRecordFromSingleOpenPosition() {
|
||||
var position = new Position(Trade.TradeType.BUY);
|
||||
position.operate(2, numFactory.hundred(), numFactory.one());
|
||||
|
||||
var record = new BaseTradingRecord(position);
|
||||
|
||||
assertTrue(record.getCurrentPosition().isOpened());
|
||||
assertEquals(0, record.getPositionCount());
|
||||
assertNull(record.getLastPosition());
|
||||
assertEquals(buyAt(2), record.getLastEntry());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRecordFromMultiplePositions() {
|
||||
var first = new Position(buyAt(1), sellAt(3));
|
||||
var second = new Position(sellAt(5), buyAt(7));
|
||||
|
||||
var record = new BaseTradingRecord(List.of(first, second));
|
||||
|
||||
assertTrue(record.getCurrentPosition().isNew());
|
||||
assertEquals(2, record.getPositionCount());
|
||||
assertEquals(second, record.getLastPosition());
|
||||
assertEquals(4, record.getTrades().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRecordFromDeserializedPositionDefaultsCostModels() throws Exception {
|
||||
var position = new Position(buyAt(1), sellAt(2));
|
||||
|
||||
var deserialized = roundTrip(position);
|
||||
|
||||
assertTrue(deserialized.getTransactionCostModel() instanceof ZeroCostModel);
|
||||
assertTrue(deserialized.getHoldingCostModel() instanceof ZeroCostModel);
|
||||
|
||||
var record = new BaseTradingRecord(deserialized);
|
||||
|
||||
assertTrue(record.getTransactionCostModel() instanceof ZeroCostModel);
|
||||
assertTrue(record.getHoldingCostModel() instanceof ZeroCostModel);
|
||||
assertEquals(1, record.getPositionCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void baseTradingRecordAcceptsTradeBasedFills() {
|
||||
var trade = new BaseTrade(0, Instant.EPOCH, numFactory.hundred(), numFactory.one(), null, ExecutionSide.BUY,
|
||||
null, null);
|
||||
|
||||
var record = new BaseTradingRecord(trade);
|
||||
assertEquals(1, record.getTrades().size());
|
||||
assertEquals(trade.getType(), record.getLastTrade().getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void baseTradingRecordRejectsEmptyTradesArray() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new BaseTradingRecord(new Trade[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operateWithPrebuiltTradePreservesExecutionFills() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
Trade aggregatedEntry = Trade.fromFills(Trade.TradeType.BUY,
|
||||
List.of(new TradeFill(1, numFactory.hundred(), numFactory.one()),
|
||||
new TradeFill(2, numFactory.numOf(101), numFactory.one())));
|
||||
Trade aggregatedExit = Trade.fromFills(Trade.TradeType.SELL,
|
||||
List.of(new TradeFill(4, numFactory.numOf(110), numFactory.two())));
|
||||
|
||||
record.operate(aggregatedEntry);
|
||||
assertFalse(record.isClosed());
|
||||
assertEquals(2, record.getTrades().size());
|
||||
assertEquals(1, record.getLastTrade().getFills().size());
|
||||
|
||||
record.operate(aggregatedExit);
|
||||
assertTrue(record.isClosed());
|
||||
assertEquals(2, record.getPositionCount());
|
||||
int totalEntryFills = record.getPositions()
|
||||
.stream()
|
||||
.map(Position::getEntry)
|
||||
.mapToInt(trade -> trade.getFills().size())
|
||||
.sum();
|
||||
int totalExitFills = record.getPositions()
|
||||
.stream()
|
||||
.map(Position::getExit)
|
||||
.mapToInt(trade -> trade.getFills().size())
|
||||
.sum();
|
||||
assertEquals(2, totalEntryFills);
|
||||
assertEquals(2, totalExitFills);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enterAndExitWithTradeRespectOpenCloseState() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
Trade firstEntry = Trade.buyAt(1, numFactory.hundred(), numFactory.one());
|
||||
Trade duplicateEntry = Trade.buyAt(2, numFactory.numOf(101), numFactory.one());
|
||||
Trade exit = Trade.sellAt(3, numFactory.numOf(110), numFactory.one());
|
||||
|
||||
assertTrue(record.enter(firstEntry));
|
||||
assertFalse(record.enter(duplicateEntry));
|
||||
assertTrue(record.exit(exit));
|
||||
assertFalse(record.exit(Trade.sellAt(4, numFactory.numOf(120), numFactory.one())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enterAndExitWithTradeRejectWrongTradeTypes() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> record.enter(Trade.sellAt(1, numFactory.hundred(), numFactory.one())));
|
||||
|
||||
assertTrue(record.enter(Trade.buyAt(1, numFactory.hundred(), numFactory.one())));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> record.exit(Trade.buyAt(2, numFactory.numOf(110), numFactory.one())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tradeDefaultOperationsRejectNullTrades() {
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
|
||||
assertThrows(NullPointerException.class, () -> record.operate((Trade) null));
|
||||
assertThrows(NullPointerException.class, () -> record.operate((TradeFill) null));
|
||||
assertThrows(NullPointerException.class, () -> record.enter((Trade) null));
|
||||
assertThrows(NullPointerException.class, () -> record.exit((Trade) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOperateFillDelegatesThroughTradeFactory() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = newTradingRecordUsingDefaultOperateImplementation();
|
||||
TradeFill fill = new TradeFill(2, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(), numFactory.one(),
|
||||
null, ExecutionSide.BUY, "order-1", "corr-1");
|
||||
|
||||
record.operate(fill);
|
||||
|
||||
assertFalse(record.isClosed());
|
||||
assertEquals(2, record.getLastTrade().getIndex());
|
||||
assertEquals(numFactory.hundred(), record.getLastTrade().getPricePerAsset());
|
||||
assertEquals(numFactory.one(), record.getLastTrade().getAmount());
|
||||
assertEquals(1, record.getLastTrade().getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOperateFillRejectsMissingSide() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = newTradingRecordUsingDefaultOperateImplementation();
|
||||
TradeFill fillWithoutSide = new TradeFill(2, null, numFactory.hundred(), numFactory.one(), null, null, null,
|
||||
null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> record.operate(fillWithoutSide));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOperateTradeFallsBackToScalarOperateForSingleFillTrade() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = newTradingRecordUsingDefaultOperateImplementation();
|
||||
Trade singleFillTrade = Trade.fromFills(Trade.TradeType.BUY,
|
||||
List.of(new TradeFill(2, numFactory.hundred(), numFactory.one())));
|
||||
|
||||
record.operate(singleFillTrade);
|
||||
|
||||
assertFalse(record.isClosed());
|
||||
assertEquals(2, record.getLastTrade().getIndex());
|
||||
assertEquals(numFactory.hundred(), record.getLastTrade().getPricePerAsset());
|
||||
assertEquals(numFactory.one(), record.getLastTrade().getAmount());
|
||||
assertEquals(1, record.getLastTrade().getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOperateTradeFallsBackWhenCustomTradeProvidesNoFills() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = newTradingRecordUsingDefaultOperateImplementation();
|
||||
Trade customTradeWithoutFills = new BaseTrade(3, Trade.TradeType.BUY, numFactory.hundred(), numFactory.one()) {
|
||||
@Override
|
||||
public List<TradeFill> getFills() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
|
||||
record.operate(customTradeWithoutFills);
|
||||
|
||||
assertFalse(record.isClosed());
|
||||
assertEquals(3, record.getLastTrade().getIndex());
|
||||
assertEquals(numFactory.hundred(), record.getLastTrade().getPricePerAsset());
|
||||
assertEquals(numFactory.one(), record.getLastTrade().getAmount());
|
||||
assertEquals(1, record.getLastTrade().getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOperateTradeRejectsMultiFillTrade() {
|
||||
DoubleNumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
TradingRecord record = newTradingRecordUsingDefaultOperateImplementation();
|
||||
Trade multiFillTrade = Trade.fromFills(Trade.TradeType.BUY,
|
||||
List.of(new TradeFill(1, numFactory.hundred(), numFactory.one()),
|
||||
new TradeFill(2, numFactory.numOf(101), numFactory.one())));
|
||||
|
||||
UnsupportedOperationException exception = assertThrows(UnsupportedOperationException.class,
|
||||
() -> record.operate(multiFillTrade));
|
||||
|
||||
assertEquals("This TradingRecord implementation must override operate(Trade) to preserve multi-fill trades",
|
||||
exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOpenPositionMethodsRemainSafeForLegacyTradingRecordImplementations() {
|
||||
TradingRecord record = new LegacyTradingRecordStub();
|
||||
|
||||
record.operate(1, numFactory.hundred(), numFactory.one());
|
||||
|
||||
assertFalse(record.isClosed());
|
||||
assertTrue(record.getOpenPositions().isEmpty());
|
||||
assertNull(record.getNetOpenPosition());
|
||||
assertNull(record.getRecordedTotalFees());
|
||||
}
|
||||
|
||||
private Position roundTrip(Position position) throws Exception {
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
try (var objectOutputStream = new ObjectOutputStream(outputStream)) {
|
||||
objectOutputStream.writeObject(position);
|
||||
}
|
||||
|
||||
try (var objectInputStream = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()))) {
|
||||
return (Position) objectInputStream.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
private TradingRecord newTradingRecordUsingDefaultOperateImplementation() {
|
||||
return new DefaultOperateTradingRecordImpl();
|
||||
}
|
||||
|
||||
private Trade buyAt(int index) {
|
||||
return Trade.buyAt(index, numFactory.hundred(), numFactory.one());
|
||||
}
|
||||
|
||||
private Trade sellAt(int index) {
|
||||
return Trade.sellAt(index, numFactory.numOf(110), numFactory.one());
|
||||
}
|
||||
|
||||
private interface DefaultOperateTradingRecord extends TradingRecord {
|
||||
|
||||
@Override
|
||||
default void operate(Trade trade) {
|
||||
TradingRecord.super.operate(trade);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DefaultOperateTradingRecordImpl extends BaseTradingRecord
|
||||
implements DefaultOperateTradingRecord {
|
||||
|
||||
@Override
|
||||
public void operate(Trade trade) {
|
||||
DefaultOperateTradingRecord.super.operate(trade);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LegacyTradingRecordStub implements TradingRecord {
|
||||
|
||||
private final BaseTradingRecord delegate = new BaseTradingRecord();
|
||||
|
||||
@Override
|
||||
public Trade.TradeType getStartingType() {
|
||||
return delegate.getStartingType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operate(int index, Num price, Num amount) {
|
||||
delegate.operate(index, price, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enter(int index, Num price, Num amount) {
|
||||
return delegate.enter(index, price, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exit(int index, Num price, Num amount) {
|
||||
return delegate.exit(index, price, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostModel getTransactionCostModel() {
|
||||
return delegate.getTransactionCostModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostModel getHoldingCostModel() {
|
||||
return delegate.getHoldingCostModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Position> getPositions() {
|
||||
return delegate.getPositions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getCurrentPosition() {
|
||||
return delegate.getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trade> getTrades() {
|
||||
return delegate.getTrades();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getStartIndex() {
|
||||
return delegate.getStartIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getEndIndex() {
|
||||
return delegate.getEndIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.zip.DataFormatException;
|
||||
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.CellValue;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.FormulaEvaluator;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.ta4j.core.mocks.MockIndicator;
|
||||
import org.ta4j.core.mocks.MockTradingRecord;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class XlsTestsUtils {
|
||||
|
||||
/**
|
||||
* Returns the first Sheet (mutable) from a workbook with the file name in the
|
||||
* test class's resources.
|
||||
*
|
||||
* @param clazz class containing the file resources
|
||||
* @param fileName file name of the file containing the workbook
|
||||
* @return Sheet number zero from the workbook (mutable)
|
||||
* @throws IOException if inputStream returned by getResourceAsStream is null or
|
||||
* if HSSFWorkBook constructor throws IOException or if
|
||||
* close throws IOException
|
||||
*/
|
||||
private static Sheet getSheet(Class<?> clazz, String fileName) throws IOException {
|
||||
InputStream inputStream = clazz.getResourceAsStream(fileName);
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Null InputStream for file " + fileName);
|
||||
}
|
||||
HSSFWorkbook workbook = new HSSFWorkbook(inputStream);
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
workbook.close();
|
||||
return sheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the parameters into the second column of the parameters section of a
|
||||
* mutable sheet. The parameters section starts after the parameters section
|
||||
* header. There must be at least params.size() rows between the parameters
|
||||
* section header and the data section header or part of the data section will
|
||||
* be overwritten.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @param params parameters to write
|
||||
* @throws DataFormatException if the parameters section header is not found
|
||||
*/
|
||||
private static void setParams(Sheet sheet, Num... params) throws DataFormatException {
|
||||
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
|
||||
Iterator<Row> iterator = sheet.rowIterator();
|
||||
while (iterator.hasNext()) {
|
||||
Row row = iterator.next();
|
||||
// skip rows with an empty first cell
|
||||
if (row.getCell(0) == null) {
|
||||
continue;
|
||||
}
|
||||
// parameters section header is first row with "Param" in first cell
|
||||
if (evaluator.evaluate(row.getCell(0)).formatAsString().contains("Param")) {
|
||||
// stream parameters into the second column of subsequent rows
|
||||
// overwrites data section if there is not a large enough gap
|
||||
Arrays.stream(params)
|
||||
.mapToDouble(Num::doubleValue)
|
||||
.forEach(d -> iterator.next().getCell(1).setCellValue(d));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// the parameters section header was not found
|
||||
throw new DataFormatException("\"Param\" header row not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the BarSeries from a file.
|
||||
*
|
||||
* @param clazz class containing the file resources
|
||||
* @param fileName file name of the file resource
|
||||
* @param numFactory
|
||||
*
|
||||
* @return BarSeries of the data
|
||||
*
|
||||
* @throws IOException if getSheet throws IOException
|
||||
* @throws DataFormatException if getSeries throws DataFormatException
|
||||
*/
|
||||
public static BarSeries getSeries(Class<?> clazz, String fileName, NumFactory numFactory)
|
||||
throws IOException, DataFormatException {
|
||||
Sheet sheet = getSheet(clazz, fileName);
|
||||
return getSeries(sheet, numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a BarSeries from the data section of a mutable Sheet. Data follows a
|
||||
* data section header and appears in the first six columns to the end of the
|
||||
* file. Empty cells in the data are forbidden.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @param numFactory
|
||||
*
|
||||
* @return BarSeries of the data
|
||||
*
|
||||
* @throws DataFormatException if getData throws DataFormatException or if the
|
||||
* data contains empty cells
|
||||
*/
|
||||
private static BarSeries getSeries(Sheet sheet, NumFactory numFactory) throws DataFormatException {
|
||||
BarSeries series = new BaseBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
|
||||
List<Row> rows = getData(sheet);
|
||||
int minInterval = Integer.MAX_VALUE;
|
||||
int previousNumber = Integer.MAX_VALUE;
|
||||
// find the minimum interval in days
|
||||
for (Row row : rows) {
|
||||
int currentNumber = (int) evaluator.evaluate(row.getCell(0)).getNumberValue();
|
||||
if (previousNumber != Integer.MAX_VALUE) {
|
||||
int interval = currentNumber - previousNumber;
|
||||
if (interval < minInterval) {
|
||||
minInterval = interval;
|
||||
}
|
||||
}
|
||||
previousNumber = currentNumber;
|
||||
}
|
||||
Duration duration = Duration.ofDays(minInterval);
|
||||
// parse the bars from the data section
|
||||
for (Row row : rows) {
|
||||
CellValue[] cellValues = new CellValue[6];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
// empty cells in the data section are forbidden
|
||||
if (row.getCell(i) == null) {
|
||||
throw new DataFormatException("empty cell in xls bar series data");
|
||||
}
|
||||
cellValues[i] = evaluator.evaluate(row.getCell(i));
|
||||
}
|
||||
// add a bar to the series
|
||||
Date endDate = DateUtil.getJavaDate(cellValues[0].getNumberValue());
|
||||
Instant endDateTime = Instant.ofEpochMilli(endDate.getTime());
|
||||
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(duration)
|
||||
.endTime(endDateTime)
|
||||
.openPrice(new BigDecimal(cellValues[1].formatAsString()))
|
||||
.highPrice(new BigDecimal(cellValues[2].formatAsString()))
|
||||
.lowPrice(new BigDecimal(cellValues[3].formatAsString()))
|
||||
.closePrice(new BigDecimal(cellValues[4].formatAsString()))
|
||||
.volume(new BigDecimal(cellValues[5].formatAsString()))
|
||||
.amount(0)
|
||||
.build());
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Object parameters into Num parameters and calls getValues on a
|
||||
* column of a mutable sheet.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @param column column number of the values to get
|
||||
* @param params Object parameters to convert to Num
|
||||
* @return List<Num> of values from the column
|
||||
* @throws DataFormatException if getValues returns DataFormatException
|
||||
*/
|
||||
private static List<Num> getValues(Sheet sheet, int column, NumFactory numFactory, Object... params)
|
||||
throws DataFormatException {
|
||||
Num[] NumParams = Arrays.stream(params)
|
||||
.map(p -> numFactory.numOf(new BigDecimal(p.toString())))
|
||||
.toArray(Num[]::new);
|
||||
return getValues(sheet, column, numFactory, NumParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the parameters to a mutable Sheet then gets the values from the
|
||||
* column.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @param column column number of the values to get
|
||||
* @param params Num parameters to write to the Sheet
|
||||
* @return List<Num> of values from the column after the parameters have been
|
||||
* written
|
||||
* @throws DataFormatException if setParams or getValues throws
|
||||
* DataFormatException
|
||||
*/
|
||||
private static List<Num> getValues(Sheet sheet, int column, NumFactory numFactory, Num... params)
|
||||
throws DataFormatException {
|
||||
setParams(sheet, params);
|
||||
return getValues(sheet, column, numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values in a column of the data section of a sheet. Rows with an
|
||||
* empty first cell are ignored.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @param column column number of the values to get
|
||||
* @return List<Num> of values from the column
|
||||
* @throws DataFormatException if getData throws DataFormatException
|
||||
*/
|
||||
private static List<Num> getValues(Sheet sheet, int column, NumFactory numFactory) throws DataFormatException {
|
||||
List<Num> values = new ArrayList<>();
|
||||
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
|
||||
// get all of the data from the data section of the sheet
|
||||
List<Row> rows = getData(sheet);
|
||||
for (Row row : rows) {
|
||||
// skip rows where the first cell is empty
|
||||
if (row.getCell(column) == null) {
|
||||
continue;
|
||||
}
|
||||
String s = evaluator.evaluate(row.getCell(column)).formatAsString();
|
||||
if (s.equals("#DIV/0!")) {
|
||||
values.add(NaN.NaN);
|
||||
} else {
|
||||
values.add(numFactory.numOf(new BigDecimal(s)));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data rows in the data section, following the data section header to
|
||||
* the end of the sheet. Skips rows that start with "//" as data comments.
|
||||
*
|
||||
* @param sheet mutable Sheet
|
||||
* @return List<Row> of the data rows
|
||||
* @throws DataFormatException if the data section header is not found.
|
||||
*/
|
||||
private static List<Row> getData(Sheet sheet) throws DataFormatException {
|
||||
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
|
||||
Iterator<Row> iterator = sheet.rowIterator();
|
||||
boolean noHeader = true;
|
||||
List<Row> rows = new ArrayList<Row>();
|
||||
// iterate through all rows of the sheet
|
||||
while (iterator.hasNext()) {
|
||||
Row row = iterator.next();
|
||||
// skip rows with an empty first cell
|
||||
if (row.getCell(0) == null) {
|
||||
continue;
|
||||
}
|
||||
// after the data section header is found, add all rows that don't
|
||||
// have "//" in the first cell
|
||||
if (!noHeader) {
|
||||
if (evaluator.evaluate(row.getCell(0)).formatAsString().compareTo("\"//\"") != 0) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
// if the data section header is not found and this row has "Date"
|
||||
// in its first cell, then mark the header as found
|
||||
if (noHeader && evaluator.evaluate(row.getCell(0)).formatAsString().contains("Date")) {
|
||||
noHeader = false;
|
||||
}
|
||||
}
|
||||
// if the header was not found throw an exception
|
||||
if (noHeader) {
|
||||
throw new DataFormatException("\"Date\" header row not found");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an Indicator from a column of an XLS file parameters.
|
||||
*
|
||||
* @param clazz class containing the file resource
|
||||
* @param fileName file name of the file resource
|
||||
* @param column column number of the indicator values
|
||||
* @param params indicator parameters
|
||||
* @return Indicator<Num> as calculated by the XLS file given the parameters
|
||||
* @throws IOException if getSheet throws IOException
|
||||
* @throws DataFormatException if getSeries or getValues throws
|
||||
* DataFormatException
|
||||
*/
|
||||
public static Indicator<Num> getIndicator(Class<?> clazz, String fileName, int column, NumFactory numFactory,
|
||||
Object... params) throws IOException, DataFormatException {
|
||||
Sheet sheet = getSheet(clazz, fileName);
|
||||
return new MockIndicator(getSeries(sheet, numFactory), getValues(sheet, column, numFactory, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the final criterion value from a column of an XLS file given parameters.
|
||||
*
|
||||
* @param clazz test class containing the file resources
|
||||
* @param fileName file name of the file resource
|
||||
* @param column column number of the calculated criterion values
|
||||
* @param params criterion parameters
|
||||
* @return Num final criterion value as calculated by the XLS file given the
|
||||
* parameters
|
||||
* @throws IOException if getSheet throws IOException
|
||||
* @throws DataFormatException if getValues throws DataFormatException
|
||||
*/
|
||||
public static Num getFinalCriterionValue(Class<?> clazz, String fileName, int column, NumFactory numFactory,
|
||||
Object... params) throws IOException, DataFormatException {
|
||||
Sheet sheet = getSheet(clazz, fileName);
|
||||
List<Num> values = getValues(sheet, column, numFactory, params);
|
||||
return values.get(values.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trading record from an XLS file.
|
||||
*
|
||||
* @param clazz the test class containing the file resources
|
||||
* @param fileName file name of the file resource
|
||||
* @param column column number of the trading record
|
||||
* @return TradingRecord from the file
|
||||
* @throws IOException if getSheet throws IOException
|
||||
* @throws DataFormatException if getValues throws DataFormatException
|
||||
*/
|
||||
public static TradingRecord getTradingRecord(Class<?> clazz, String fileName, int column, NumFactory numFactory)
|
||||
throws IOException, DataFormatException {
|
||||
Sheet sheet = getSheet(clazz, fileName);
|
||||
return new MockTradingRecord(getValues(sheet, column, numFactory));
|
||||
}
|
||||
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
final class AggregatorTestFixtures {
|
||||
|
||||
private static final Duration SOURCE_PERIOD = Duration.ofMinutes(1);
|
||||
private static final Instant BASE_END_TIME = Instant.parse("2026-01-01T00:01:00Z");
|
||||
|
||||
private AggregatorTestFixtures() {
|
||||
}
|
||||
|
||||
static List<Bar> trendingBars(NumFactory numFactory) {
|
||||
double[][] rows = new double[][] { { 100d, 102d, 99d, 101d, 30d }, { 101d, 104d, 100d, 103d, 25d },
|
||||
{ 103d, 106d, 102d, 105d, 28d }, { 105d, 108d, 104d, 107d, 22d }, { 107d, 110d, 106d, 109d, 26d },
|
||||
{ 109d, 112d, 108d, 111d, 24d } };
|
||||
return fromRows(numFactory, rows, List.of(1, 2, 3, 4, 5, 6));
|
||||
}
|
||||
|
||||
static List<Bar> volatileBars(NumFactory numFactory) {
|
||||
double[][] rows = new double[][] { { 100d, 108d, 94d, 96d, 40d }, { 96d, 112d, 95d, 110d, 45d },
|
||||
{ 110d, 111d, 90d, 92d, 35d }, { 92d, 118d, 91d, 115d, 50d }, { 115d, 116d, 88d, 90d, 55d },
|
||||
{ 90d, 120d, 89d, 118d, 60d } };
|
||||
return fromRows(numFactory, rows, List.of(1, 2, 3, 4, 5, 6));
|
||||
}
|
||||
|
||||
static List<Bar> flatBars(NumFactory numFactory) {
|
||||
double[][] rows = new double[][] { { 100d, 101d, 99d, 100d, 20d }, { 100d, 101d, 99d, 100d, 20d },
|
||||
{ 100d, 101d, 99d, 100d, 20d }, { 100d, 101d, 99d, 100d, 20d }, { 100d, 101d, 99d, 100d, 20d },
|
||||
{ 100d, 101d, 99d, 100d, 20d } };
|
||||
return fromRows(numFactory, rows, List.of(1, 2, 3, 4, 5, 6));
|
||||
}
|
||||
|
||||
static List<Bar> unevenIntervalBars(NumFactory numFactory) {
|
||||
double[][] rows = new double[][] { { 100d, 102d, 99d, 101d, 10d }, { 101d, 103d, 100d, 102d, 10d },
|
||||
{ 102d, 104d, 101d, 103d, 10d } };
|
||||
return fromRows(numFactory, rows, List.of(1, 2, 5));
|
||||
}
|
||||
|
||||
static List<Bar> inconsistentPeriodBars(NumFactory numFactory) {
|
||||
BarSeries series = newSeries(numFactory);
|
||||
addBar(series, Duration.ofMinutes(1), BASE_END_TIME, 100d, 101d, 99d, 100d, 10d);
|
||||
addBar(series, Duration.ofMinutes(2), BASE_END_TIME.plus(Duration.ofMinutes(2)), 100d, 102d, 98d, 101d, 11d);
|
||||
addBar(series, Duration.ofMinutes(1), BASE_END_TIME.plus(Duration.ofMinutes(3)), 101d, 103d, 100d, 102d, 12d);
|
||||
return List.copyOf(series.getBarData());
|
||||
}
|
||||
|
||||
static List<Bar> barsFromClosePrices(NumFactory numFactory, double... closePrices) {
|
||||
BarSeries series = newSeries(numFactory);
|
||||
for (int i = 0; i < closePrices.length; i++) {
|
||||
double closePrice = closePrices[i];
|
||||
double openPrice = i == 0 ? closePrice : closePrices[i - 1];
|
||||
double highPrice = Math.max(openPrice, closePrice);
|
||||
double lowPrice = Math.min(openPrice, closePrice);
|
||||
double volume = 10d;
|
||||
Instant endTime = BASE_END_TIME.plus(Duration.ofMinutes(i));
|
||||
addBar(series, SOURCE_PERIOD, endTime, openPrice, highPrice, lowPrice, closePrice, volume);
|
||||
}
|
||||
return List.copyOf(series.getBarData());
|
||||
}
|
||||
|
||||
static List<Bar> barsWithMissingClosePrice(NumFactory numFactory) {
|
||||
BarSeries series = newSeries(numFactory);
|
||||
addBar(series, SOURCE_PERIOD, BASE_END_TIME, 100d, 101d, 99d, 100d, 10d);
|
||||
addBar(series, SOURCE_PERIOD, BASE_END_TIME.plus(Duration.ofMinutes(1)), 100d, 102d, 98d, null, 10d);
|
||||
return List.copyOf(series.getBarData());
|
||||
}
|
||||
|
||||
static List<Bar> barsWithNullBeginTime(NumFactory numFactory) {
|
||||
List<Bar> bars = new ArrayList<>(trendingBars(numFactory));
|
||||
Bar targetBar = bars.get(1);
|
||||
bars.set(1, new TimestampOverrideBar(targetBar, null, targetBar.getEndTime()));
|
||||
return List.copyOf(bars);
|
||||
}
|
||||
|
||||
static List<Bar> barsWithNullEndTime(NumFactory numFactory) {
|
||||
List<Bar> bars = new ArrayList<>(trendingBars(numFactory));
|
||||
Bar targetBar = bars.get(1);
|
||||
bars.set(1, new TimestampOverrideBar(targetBar, targetBar.getBeginTime(), null));
|
||||
return List.copyOf(bars);
|
||||
}
|
||||
|
||||
private static List<Bar> fromRows(NumFactory numFactory, double[][] rows, List<Integer> endOffsetsInMinutes) {
|
||||
BarSeries series = newSeries(numFactory);
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
double[] row = rows[i];
|
||||
Instant endTime = BASE_END_TIME.plus(Duration.ofMinutes(endOffsetsInMinutes.get(i) - 1L));
|
||||
addBar(series, SOURCE_PERIOD, endTime, row[0], row[1], row[2], row[3], row[4]);
|
||||
}
|
||||
return List.copyOf(series.getBarData());
|
||||
}
|
||||
|
||||
private static BarSeries newSeries(NumFactory numFactory) {
|
||||
return new MockBarSeriesBuilder().withNumFactory(numFactory).withName("aggregator-fixtures").build();
|
||||
}
|
||||
|
||||
private static void addBar(BarSeries series, Duration period, Instant endTime, double open, double high, double low,
|
||||
Double close, double volume) {
|
||||
long trades = Math.max(1L, Math.round(volume / 5d));
|
||||
Num amount = close == null ? series.numFactory().zero() : series.numFactory().numOf(close * volume);
|
||||
BarBuilder barBuilder = series.barBuilder()
|
||||
.timePeriod(period)
|
||||
.endTime(endTime)
|
||||
.openPrice(open)
|
||||
.highPrice(high)
|
||||
.lowPrice(low);
|
||||
if (close == null) {
|
||||
barBuilder.closePrice((Num) null);
|
||||
} else {
|
||||
barBuilder.closePrice(close);
|
||||
}
|
||||
barBuilder.volume(volume).amount(amount).trades(trades).add();
|
||||
}
|
||||
|
||||
private static final class TimestampOverrideBar implements Bar {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final Bar delegate;
|
||||
private final Instant beginTime;
|
||||
private final Instant endTime;
|
||||
|
||||
private TimestampOverrideBar(Bar delegate, Instant beginTime, Instant endTime) {
|
||||
this.delegate = delegate;
|
||||
this.beginTime = beginTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getTimePeriod() {
|
||||
return delegate.getTimePeriod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getBeginTime() {
|
||||
return beginTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getOpenPrice() {
|
||||
return delegate.getOpenPrice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getHighPrice() {
|
||||
return delegate.getHighPrice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getLowPrice() {
|
||||
return delegate.getLowPrice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getClosePrice() {
|
||||
return delegate.getClosePrice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getVolume() {
|
||||
return delegate.getVolume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getAmount() {
|
||||
return delegate.getAmount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTrades() {
|
||||
return delegate.getTrades();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(Num tradeVolume, Num tradePrice) {
|
||||
delegate.addTrade(tradeVolume, tradePrice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrice(Num price) {
|
||||
delegate.addPrice(price);
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class BaseBarSeriesAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private final BaseBarSeriesAggregator baseBarSeriesAggregator = new BaseBarSeriesAggregator(
|
||||
new BarAggregatorForTest());
|
||||
|
||||
public BaseBarSeriesAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregateWithNewName() {
|
||||
final BarSeries barSeries = new MockBarSeriesBuilder().withName("name").build();
|
||||
final Instant time = Instant.parse("2019-06-12T04:01:00Z");
|
||||
|
||||
var bar0 = barSeries.barBuilder()
|
||||
.endTime(time)
|
||||
.openPrice(1d)
|
||||
.closePrice(2d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.volume(5d)
|
||||
.amount(6d)
|
||||
.trades(7)
|
||||
.build();
|
||||
var bar1 = barSeries.barBuilder()
|
||||
.endTime(time.plus(Duration.ofDays(1)))
|
||||
.openPrice(2d)
|
||||
.closePrice(3d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.volume(5d)
|
||||
.amount(6d)
|
||||
.trades(7)
|
||||
.build();
|
||||
var bar2 = barSeries.barBuilder()
|
||||
.endTime(time.plus(Duration.ofDays(2)))
|
||||
.openPrice(3d)
|
||||
.closePrice(4d)
|
||||
.highPrice(4d)
|
||||
.lowPrice(5d)
|
||||
.volume(6d)
|
||||
.amount(7d)
|
||||
.trades(7)
|
||||
.build();
|
||||
barSeries.addBar(bar0);
|
||||
barSeries.addBar(bar1);
|
||||
barSeries.addBar(bar2);
|
||||
|
||||
final BarSeries aggregated = baseBarSeriesAggregator.aggregate(barSeries, "newName");
|
||||
|
||||
assertEquals("newName", aggregated.getName());
|
||||
assertEquals(2, aggregated.getBarCount());
|
||||
assertSame(bar0, aggregated.getBar(0));
|
||||
assertSame(bar2, aggregated.getBar(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregateWithTheSameName() {
|
||||
final BarSeries barSeries = new MockBarSeriesBuilder().withName("name").build();
|
||||
final Instant time = Instant.parse("2019-06-12T04:01:00Z");
|
||||
|
||||
var bar0 = barSeries.barBuilder()
|
||||
.endTime(time)
|
||||
.openPrice(1d)
|
||||
.closePrice(2d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.volume(5d)
|
||||
.amount(6d)
|
||||
.trades(7)
|
||||
.build();
|
||||
var bar1 = barSeries.barBuilder()
|
||||
.endTime(time.plus(Duration.ofDays(1)))
|
||||
.openPrice(2d)
|
||||
.closePrice(3d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.volume(5d)
|
||||
.amount(6d)
|
||||
.trades(7)
|
||||
.build();
|
||||
var bar2 = barSeries.barBuilder()
|
||||
.endTime(time.plus(Duration.ofDays(2)))
|
||||
.openPrice(3d)
|
||||
.closePrice(4d)
|
||||
.highPrice(4d)
|
||||
.lowPrice(5d)
|
||||
.volume(6d)
|
||||
.amount(7d)
|
||||
.trades(7)
|
||||
.build();
|
||||
barSeries.addBar(bar0);
|
||||
barSeries.addBar(bar1);
|
||||
barSeries.addBar(bar2);
|
||||
|
||||
final BarSeries aggregated = baseBarSeriesAggregator.aggregate(barSeries);
|
||||
|
||||
assertEquals("name", aggregated.getName());
|
||||
assertEquals(2, aggregated.getBarCount());
|
||||
assertSame(bar0, aggregated.getBar(0));
|
||||
assertSame(bar2, aggregated.getBar(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* This bar aggregator created only for test purposes is returning first and
|
||||
* last bar.
|
||||
*/
|
||||
private static class BarAggregatorForTest implements BarAggregator {
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> bars) {
|
||||
final List<Bar> aggregated = new ArrayList<>();
|
||||
if (bars.isEmpty()) {
|
||||
return aggregated;
|
||||
}
|
||||
int lastBarIndex = bars.size() - 1;
|
||||
|
||||
aggregated.add(bars.get(0));
|
||||
aggregated.add(bars.get(lastBarIndex));
|
||||
return aggregated;
|
||||
}
|
||||
}
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class DurationBarAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public DurationBarAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
private List<Bar> getOneDayBars() {
|
||||
var bars = new LinkedList<Bar>();
|
||||
var time = Instant.parse("2019-06-12T04:01:00Z");
|
||||
|
||||
// days 1 - 5
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time)
|
||||
.openPrice(1d)
|
||||
.closePrice(2d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.amount(5d)
|
||||
.volume(6d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(1)))
|
||||
.openPrice(2d)
|
||||
.closePrice(3d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(4d)
|
||||
.amount(5d)
|
||||
.volume(6d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(2)))
|
||||
.openPrice(3d)
|
||||
.closePrice(4d)
|
||||
.highPrice(4d)
|
||||
.lowPrice(5d)
|
||||
.amount(6d)
|
||||
.volume(7d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(3)))
|
||||
.openPrice(4d)
|
||||
.closePrice(5d)
|
||||
.highPrice(6d)
|
||||
.lowPrice(5d)
|
||||
.amount(7d)
|
||||
.volume(8d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(4)))
|
||||
.openPrice(5d)
|
||||
.closePrice(9d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(11d)
|
||||
.amount(2d)
|
||||
.volume(6d)
|
||||
.trades(7)
|
||||
.build());
|
||||
|
||||
// days 6 - 10
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(5)))
|
||||
.openPrice(6d)
|
||||
.closePrice(10d)
|
||||
.highPrice(9d)
|
||||
.lowPrice(4d)
|
||||
.amount(8d)
|
||||
.volume(3d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(6)))
|
||||
.openPrice(3d)
|
||||
.closePrice(3d)
|
||||
.highPrice(4d)
|
||||
.lowPrice(95d)
|
||||
.amount(21d)
|
||||
.volume(74d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(7)))
|
||||
.openPrice(4d)
|
||||
.closePrice(7d)
|
||||
.highPrice(63d)
|
||||
.lowPrice(59d)
|
||||
.amount(56d)
|
||||
.volume(89d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(8)))
|
||||
.openPrice(5d)
|
||||
.closePrice(93d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(21d)
|
||||
.amount(29d)
|
||||
.volume(62d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(9)))
|
||||
.openPrice(6d)
|
||||
.closePrice(10d)
|
||||
.highPrice(91d)
|
||||
.lowPrice(43d)
|
||||
.amount(84d)
|
||||
.volume(32d)
|
||||
.trades(7)
|
||||
.build());
|
||||
|
||||
// days 11 - 15
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(10)))
|
||||
.openPrice(4d)
|
||||
.closePrice(10d)
|
||||
.highPrice(943d)
|
||||
.lowPrice(49d)
|
||||
.amount(8d)
|
||||
.volume(43d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(11)))
|
||||
.openPrice(3d)
|
||||
.closePrice(3d)
|
||||
.highPrice(43d)
|
||||
.lowPrice(92d)
|
||||
.amount(21d)
|
||||
.volume(784d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(12)))
|
||||
.openPrice(4d)
|
||||
.closePrice(74d)
|
||||
.highPrice(53d)
|
||||
.lowPrice(52d)
|
||||
.amount(56d)
|
||||
.volume(89d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(13)))
|
||||
.openPrice(5d)
|
||||
.closePrice(93d)
|
||||
.highPrice(31d)
|
||||
.lowPrice(221d)
|
||||
.amount(29d)
|
||||
.volume(62d)
|
||||
.trades(7)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(14)))
|
||||
.openPrice(6d)
|
||||
.closePrice(10d)
|
||||
.highPrice(991d)
|
||||
.lowPrice(43d)
|
||||
.amount(84d)
|
||||
.volume(32d)
|
||||
.trades(7)
|
||||
.build());
|
||||
|
||||
// day 16
|
||||
bars.add(new MockBarBuilder(numFactory).endTime(time.plus(Duration.ofDays(15)))
|
||||
.openPrice(6d)
|
||||
.closePrice(108d)
|
||||
.highPrice(1991d)
|
||||
.lowPrice(433d)
|
||||
.amount(847d)
|
||||
.volume(322d)
|
||||
.trades(7)
|
||||
.build());
|
||||
return bars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the bars are upscaled correctly from 1day to 5day
|
||||
*/
|
||||
@Test
|
||||
public void upscaledTo5DayBars() {
|
||||
var barAggregator = new DurationBarAggregator(Duration.ofDays(5), true);
|
||||
|
||||
var bars = barAggregator.aggregate(getOneDayBars());
|
||||
|
||||
// must be 3 bars
|
||||
assertEquals(3, bars.size());
|
||||
|
||||
// bar 1 must have ohlcv (1, 6, 4, 9, 25)
|
||||
var bar1 = bars.getFirst();
|
||||
var num1 = bar1.getOpenPrice();
|
||||
assertNumEquals(num1.getNumFactory().numOf(1), bar1.getOpenPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(6), bar1.getHighPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(4), bar1.getLowPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(9), bar1.getClosePrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(33), bar1.getVolume());
|
||||
|
||||
// bar 2 must have ohlcv (6, 91, 4, 10, 260)
|
||||
var bar2 = bars.get(1);
|
||||
var num2 = bar2.getOpenPrice();
|
||||
assertNumEquals(num2.getNumFactory().numOf(6), bar2.getOpenPrice());
|
||||
assertNumEquals(num2.getNumFactory().numOf(91), bar2.getHighPrice());
|
||||
assertNumEquals(num2.getNumFactory().numOf(4), bar2.getLowPrice());
|
||||
assertNumEquals(num2.getNumFactory().numOf(10), bar2.getClosePrice());
|
||||
assertNumEquals(num2.getNumFactory().numOf(260), bar2.getVolume());
|
||||
|
||||
// bar 3 must have ohlcv (1d, 6d, 4d, 9d, 25)
|
||||
var bar3 = bars.get(2);
|
||||
var num3 = bar3.getOpenPrice();
|
||||
assertNumEquals(num3.getNumFactory().numOf(4), bar3.getOpenPrice());
|
||||
assertNumEquals(num3.getNumFactory().numOf(991), bar3.getHighPrice());
|
||||
assertNumEquals(num3.getNumFactory().numOf(43), bar3.getLowPrice());
|
||||
assertNumEquals(num3.getNumFactory().numOf(10), bar3.getClosePrice());
|
||||
assertNumEquals(num3.getNumFactory().numOf(1010), bar3.getVolume());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the bars are upscaled correctly from 1day to 10day
|
||||
*/
|
||||
@Test
|
||||
public void upscaledTo10DayBars() {
|
||||
var barAggregator = new DurationBarAggregator(Duration.ofDays(10), true);
|
||||
var bars = barAggregator.aggregate(getOneDayBars());
|
||||
|
||||
// must be 1 bars
|
||||
assertEquals(1, bars.size());
|
||||
|
||||
// bar 1 must have ohlcv (1, 91, 4, 10, 293)
|
||||
var bar1 = bars.getFirst();
|
||||
var num1 = bar1.getOpenPrice();
|
||||
assertNumEquals(num1.getNumFactory().numOf(1), bar1.getOpenPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(91), bar1.getHighPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(4), bar1.getLowPrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(10), bar1.getClosePrice());
|
||||
assertNumEquals(num1.getNumFactory().numOf(293), bar1.getVolume());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the bars are upscaled correctly from 1day to 10day, allowed not
|
||||
* final bars too
|
||||
*/
|
||||
@Test
|
||||
public void upscaledTo10DayBarsNotOnlyFinalBars() {
|
||||
var barAggregator = new DurationBarAggregator(Duration.ofDays(10), false);
|
||||
var bars = barAggregator.aggregate(getOneDayBars());
|
||||
|
||||
// must be 2 bars
|
||||
assertEquals(2, bars.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithGapsInSeries() {
|
||||
var now = Instant.now();
|
||||
var barSeries = new BaseBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
barSeries.barBuilder()
|
||||
.timePeriod(Duration.ofMinutes(1))
|
||||
.endTime(now.plus(Duration.ofMinutes(1)))
|
||||
.openPrice(1)
|
||||
.highPrice(1)
|
||||
.closePrice(2)
|
||||
.lowPrice(1)
|
||||
.volume(1)
|
||||
.add();
|
||||
barSeries.barBuilder()
|
||||
.timePeriod(Duration.ofMinutes(1))
|
||||
.endTime(now.plus(Duration.ofMinutes(2)))
|
||||
.openPrice(1)
|
||||
.highPrice(1)
|
||||
.closePrice(3)
|
||||
.lowPrice(1)
|
||||
.volume(1)
|
||||
.add();
|
||||
barSeries.barBuilder()
|
||||
.timePeriod(Duration.ofMinutes(1))
|
||||
.endTime(now.plus(Duration.ofMinutes(60)))
|
||||
.openPrice(1)
|
||||
.highPrice(1)
|
||||
.closePrice(1)
|
||||
.lowPrice(1)
|
||||
.volume(1)
|
||||
.add();
|
||||
|
||||
var aggregated2MinSeries = new BaseBarSeriesAggregator(new DurationBarAggregator(Duration.ofMinutes(2), false))
|
||||
.aggregate(barSeries, "");
|
||||
var aggregated4MinSeries = new BaseBarSeriesAggregator(new DurationBarAggregator(Duration.ofMinutes(4), false))
|
||||
.aggregate(barSeries, "");
|
||||
|
||||
assertEquals(2, aggregated2MinSeries.getBarCount());
|
||||
assertEquals(2, aggregated4MinSeries.getBarCount());
|
||||
|
||||
assertNumEquals(3, aggregated2MinSeries.getBar(0).getClosePrice());
|
||||
assertNumEquals(3, aggregated4MinSeries.getBar(0).getClosePrice());
|
||||
|
||||
assertNumEquals(2, aggregated2MinSeries.getBar(0).getVolume());
|
||||
assertNumEquals(2, aggregated4MinSeries.getBar(0).getVolume());
|
||||
|
||||
assertNumEquals(1, aggregated2MinSeries.getBar(1).getClosePrice());
|
||||
assertNumEquals(1, aggregated4MinSeries.getBar(1).getClosePrice());
|
||||
|
||||
assertNumEquals(1, aggregated2MinSeries.getBar(1).getVolume());
|
||||
assertNumEquals(1, aggregated4MinSeries.getBar(1).getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateUsesAvailablePriceForNumFactory() {
|
||||
var barAggregator = new DurationBarAggregator(Duration.ofDays(1), true);
|
||||
|
||||
var bars = new LinkedList<Bar>();
|
||||
bars.add(new MockBarBuilder(numFactory).openPrice((Num) null)
|
||||
.closePrice(2d)
|
||||
.highPrice(3d)
|
||||
.lowPrice(1d)
|
||||
.volume(4d)
|
||||
.build());
|
||||
bars.add(new MockBarBuilder(numFactory).openPrice((Num) null)
|
||||
.closePrice(4d)
|
||||
.highPrice(5d)
|
||||
.lowPrice(2d)
|
||||
.volume(6d)
|
||||
.build());
|
||||
|
||||
var aggregated = barAggregator.aggregate(bars);
|
||||
|
||||
assertEquals(2, aggregated.size());
|
||||
assertNumEquals(4d, aggregated.getFirst().getVolume());
|
||||
assertSame(numFactory.getClass(), aggregated.getFirst().getVolume().getNumFactory().getClass());
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class HeikinAshiBarAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public HeikinAshiBarAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
private final HeikinAshiBarAggregator unit = new HeikinAshiBarAggregator();
|
||||
|
||||
@Test
|
||||
public void testAggregate() {
|
||||
var endTime = Instant.parse("2024-01-01T01:00:00Z");
|
||||
var timePeriod = Duration.ofHours(1);
|
||||
|
||||
Bar bar1 = new BaseBar(timePeriod, null, endTime, numFactory.numOf(100), numFactory.numOf(105),
|
||||
numFactory.numOf(95), numFactory.numOf(100), numFactory.numOf(10), numFactory.numOf(50), 1);
|
||||
var bar2 = new BaseBar(timePeriod, null, endTime.plus(1, ChronoUnit.HOURS), numFactory.numOf(100),
|
||||
numFactory.numOf(110), numFactory.numOf(98), numFactory.numOf(105), numFactory.numOf(20),
|
||||
numFactory.numOf(100), 2);
|
||||
|
||||
var ohlcBars = List.of(bar1, bar2);
|
||||
var haBars = unit.aggregate(ohlcBars);
|
||||
assertEquals(2, haBars.size());
|
||||
|
||||
// First HA bar should be identical to the original bar, since no previous HA
|
||||
// data
|
||||
var firstHA = haBars.getFirst();
|
||||
assertEquals(bar1.getOpenPrice(), firstHA.getOpenPrice());
|
||||
assertEquals(bar1.getHighPrice(), firstHA.getHighPrice());
|
||||
assertEquals(bar1.getLowPrice(), firstHA.getLowPrice());
|
||||
assertEquals(bar1.getClosePrice(), firstHA.getClosePrice());
|
||||
|
||||
// Second HA bar uses first bar’s HA open/close in the formula
|
||||
var secondHA = haBars.get(1);
|
||||
var haClose2Expected = bar2.getOpenPrice()
|
||||
.plus(bar2.getHighPrice())
|
||||
.plus(bar2.getLowPrice())
|
||||
.plus(bar2.getClosePrice())
|
||||
.dividedBy(numFactory.numOf(4));
|
||||
var haOpen2Expected = firstHA.getOpenPrice().plus(firstHA.getClosePrice()).dividedBy(numFactory.numOf(2));
|
||||
assertEquals(haOpen2Expected, secondHA.getOpenPrice());
|
||||
assertEquals(haClose2Expected, secondHA.getClosePrice());
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class RangeBarAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public RangeBarAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateTrendingSeriesByRange() {
|
||||
List<Bar> bars = AggregatorTestFixtures.trendingBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(6d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(2, aggregated.size());
|
||||
Bar firstBar = aggregated.getFirst();
|
||||
assertNumEquals(100d, firstBar.getOpenPrice());
|
||||
assertNumEquals(106d, firstBar.getHighPrice());
|
||||
assertNumEquals(99d, firstBar.getLowPrice());
|
||||
assertNumEquals(105d, firstBar.getClosePrice());
|
||||
assertNumEquals(83d, firstBar.getVolume());
|
||||
assertEquals(Duration.ofMinutes(3), firstBar.getTimePeriod());
|
||||
|
||||
Bar secondBar = aggregated.get(1);
|
||||
assertNumEquals(105d, secondBar.getOpenPrice());
|
||||
assertNumEquals(110d, secondBar.getHighPrice());
|
||||
assertNumEquals(104d, secondBar.getLowPrice());
|
||||
assertNumEquals(109d, secondBar.getClosePrice());
|
||||
assertNumEquals(48d, secondBar.getVolume());
|
||||
assertEquals(Duration.ofMinutes(2), secondBar.getTimePeriod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateVolatileSeriesByRange() {
|
||||
List<Bar> bars = AggregatorTestFixtures.volatileBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(10d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(6, aggregated.size());
|
||||
assertNumEquals(100d, aggregated.getFirst().getOpenPrice());
|
||||
assertNumEquals(96d, aggregated.getFirst().getClosePrice());
|
||||
assertNumEquals(118d, aggregated.getLast().getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateFlatSeriesKeepsPendingBarWhenConfigured() {
|
||||
List<Bar> bars = AggregatorTestFixtures.flatBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(4d, false);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(1, aggregated.size());
|
||||
Bar pendingBar = aggregated.getFirst();
|
||||
assertNumEquals(100d, pendingBar.getOpenPrice());
|
||||
assertNumEquals(101d, pendingBar.getHighPrice());
|
||||
assertNumEquals(99d, pendingBar.getLowPrice());
|
||||
assertNumEquals(100d, pendingBar.getClosePrice());
|
||||
assertNumEquals(120d, pendingBar.getVolume());
|
||||
assertEquals(Duration.ofMinutes(6), pendingBar.getTimePeriod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateFlatSeriesDropsPendingBarByDefault() {
|
||||
List<Bar> bars = AggregatorTestFixtures.flatBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(4d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsUnevenIntervals() {
|
||||
List<Bar> bars = AggregatorTestFixtures.unevenIntervalBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(2d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsInconsistentSourceTimePeriods() {
|
||||
List<Bar> bars = AggregatorTestFixtures.inconsistentPeriodBars(numFactory);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(2d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceTimestamps() {
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(2d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullBeginTime(numFactory)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullEndTime(numFactory)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceBars() {
|
||||
List<Bar> bars = new ArrayList<>(AggregatorTestFixtures.trendingBars(numFactory));
|
||||
bars.set(2, null);
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(2d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateEmptyBarsReturnsEmptyList() {
|
||||
RangeBarAggregator aggregator = new RangeBarAggregator(2d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(List.of());
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorRejectsInvalidRangeSize() {
|
||||
assertThrows(NullPointerException.class, () -> new RangeBarAggregator(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RangeBarAggregator(0d));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RangeBarAggregator(-1d));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RangeBarAggregator(Double.NaN));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RangeBarAggregator(Double.POSITIVE_INFINITY));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new RangeBarAggregator(new NonDecimalNumberRepresentation()));
|
||||
}
|
||||
|
||||
private static final class NonDecimalNumberRepresentation extends Number {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float floatValue() {
|
||||
return 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double doubleValue() {
|
||||
return 1d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "non-decimal";
|
||||
}
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class RenkoBarAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public RenkoBarAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateTrendingSeriesCreatesAscendingBricks() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 101d, 103d, 105d, 107d, 109d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(4, aggregated.size());
|
||||
assertNumEquals(100d, aggregated.get(0).getOpenPrice());
|
||||
assertNumEquals(102d, aggregated.get(0).getClosePrice());
|
||||
assertNumEquals(102d, aggregated.get(1).getOpenPrice());
|
||||
assertNumEquals(104d, aggregated.get(1).getClosePrice());
|
||||
assertNumEquals(104d, aggregated.get(2).getOpenPrice());
|
||||
assertNumEquals(106d, aggregated.get(2).getClosePrice());
|
||||
assertNumEquals(106d, aggregated.get(3).getOpenPrice());
|
||||
assertNumEquals(108d, aggregated.get(3).getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateInitialDowntrendCreatesDescendingBricks() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 99d, 97d, 95d, 93d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(3, aggregated.size());
|
||||
assertNumEquals(98d, aggregated.get(0).getClosePrice());
|
||||
assertNumEquals(96d, aggregated.get(1).getClosePrice());
|
||||
assertNumEquals(94d, aggregated.get(2).getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateVolatileSeriesHandlesReversalAmount() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 104d, 99d, 95d, 101d, 107d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
double[] expectedCloses = new double[] { 102d, 104d, 102d, 100d, 98d, 96d, 98d, 100d, 102d, 104d, 106d };
|
||||
|
||||
assertEquals(expectedCloses.length, aggregated.size());
|
||||
for (int i = 0; i < expectedCloses.length; i++) {
|
||||
assertNumEquals(expectedCloses[i], aggregated.get(i).getClosePrice());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRequiresConfiguredReversalDistanceBeforeDirectionChange() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 104d, 99d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 3);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(2, aggregated.size());
|
||||
assertNumEquals(102d, aggregated.get(0).getClosePrice());
|
||||
assertNumEquals(104d, aggregated.get(1).getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateAssignsMetricsToFirstBrickWhenMultipleBricksComeFromSingleSourceBar() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 106d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(3, aggregated.size());
|
||||
assertNumEquals(20d, aggregated.get(0).getVolume());
|
||||
assertNumEquals(numFactory.zero(), aggregated.get(1).getVolume());
|
||||
assertNumEquals(numFactory.zero(), aggregated.get(2).getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateFlatSeriesCreatesNoBricks() {
|
||||
List<Bar> bars = AggregatorTestFixtures.barsFromClosePrices(numFactory, 100d, 100.5d, 99.5d, 100d, 100.2d);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsUnevenIntervals() {
|
||||
List<Bar> bars = AggregatorTestFixtures.unevenIntervalBars(numFactory);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsInconsistentSourceTimePeriods() {
|
||||
List<Bar> bars = AggregatorTestFixtures.inconsistentPeriodBars(numFactory);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceTimestamps() {
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullBeginTime(numFactory)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullEndTime(numFactory)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceBars() {
|
||||
List<Bar> bars = new ArrayList<>(AggregatorTestFixtures.trendingBars(numFactory));
|
||||
bars.set(3, null);
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsMissingClosePrice() {
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithMissingClosePrice(numFactory)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateEmptyBarsReturnsEmptyList() {
|
||||
RenkoBarAggregator aggregator = new RenkoBarAggregator(2d, 2);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(List.of());
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorRejectsInvalidParameters() {
|
||||
assertThrows(NullPointerException.class, () -> new RenkoBarAggregator(null, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RenkoBarAggregator(0d, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RenkoBarAggregator(-1d, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RenkoBarAggregator(Double.NaN, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RenkoBarAggregator(1d, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new RenkoBarAggregator(1d, -1));
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class VolumeBarAggregatorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public VolumeBarAggregatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateTrendingSeriesByVolumeThreshold() {
|
||||
List<Bar> bars = AggregatorTestFixtures.trendingBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(100d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(1, aggregated.size());
|
||||
Bar firstBar = aggregated.getFirst();
|
||||
assertNumEquals(100d, firstBar.getOpenPrice());
|
||||
assertNumEquals(108d, firstBar.getHighPrice());
|
||||
assertNumEquals(99d, firstBar.getLowPrice());
|
||||
assertNumEquals(107d, firstBar.getClosePrice());
|
||||
assertNumEquals(105d, firstBar.getVolume());
|
||||
assertEquals(Duration.ofMinutes(4), firstBar.getTimePeriod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateVolatileSeriesByVolumeThreshold() {
|
||||
List<Bar> bars = AggregatorTestFixtures.volatileBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(100d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(2, aggregated.size());
|
||||
Bar firstBar = aggregated.getFirst();
|
||||
assertNumEquals(100d, firstBar.getOpenPrice());
|
||||
assertNumEquals(112d, firstBar.getHighPrice());
|
||||
assertNumEquals(90d, firstBar.getLowPrice());
|
||||
assertNumEquals(92d, firstBar.getClosePrice());
|
||||
assertNumEquals(120d, firstBar.getVolume());
|
||||
|
||||
Bar secondBar = aggregated.get(1);
|
||||
assertNumEquals(92d, secondBar.getOpenPrice());
|
||||
assertNumEquals(118d, secondBar.getHighPrice());
|
||||
assertNumEquals(88d, secondBar.getLowPrice());
|
||||
assertNumEquals(90d, secondBar.getClosePrice());
|
||||
assertNumEquals(105d, secondBar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateFlatSeriesKeepsPendingBarWhenConfigured() {
|
||||
List<Bar> bars = AggregatorTestFixtures.flatBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(200d, false);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(1, aggregated.size());
|
||||
Bar pendingBar = aggregated.getFirst();
|
||||
assertNumEquals(100d, pendingBar.getOpenPrice());
|
||||
assertNumEquals(101d, pendingBar.getHighPrice());
|
||||
assertNumEquals(99d, pendingBar.getLowPrice());
|
||||
assertNumEquals(100d, pendingBar.getClosePrice());
|
||||
assertNumEquals(120d, pendingBar.getVolume());
|
||||
assertEquals(Duration.ofMinutes(6), pendingBar.getTimePeriod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateFlatSeriesDropsPendingBarByDefault() {
|
||||
List<Bar> bars = AggregatorTestFixtures.flatBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(200d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(bars);
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsUnevenIntervals() {
|
||||
List<Bar> bars = AggregatorTestFixtures.unevenIntervalBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(20d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsInconsistentSourceTimePeriods() {
|
||||
List<Bar> bars = AggregatorTestFixtures.inconsistentPeriodBars(numFactory);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(20d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceTimestamps() {
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(20d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullBeginTime(numFactory)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> aggregator.aggregate(AggregatorTestFixtures.barsWithNullEndTime(numFactory)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateRejectsNullSourceBars() {
|
||||
List<Bar> bars = new ArrayList<>(AggregatorTestFixtures.trendingBars(numFactory));
|
||||
bars.set(1, null);
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(20d);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> aggregator.aggregate(bars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregateEmptyBarsReturnsEmptyList() {
|
||||
VolumeBarAggregator aggregator = new VolumeBarAggregator(20d);
|
||||
|
||||
List<Bar> aggregated = aggregator.aggregate(List.of());
|
||||
|
||||
assertEquals(0, aggregated.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorRejectsInvalidVolumeThreshold() {
|
||||
assertThrows(NullPointerException.class, () -> new VolumeBarAggregator(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new VolumeBarAggregator(0d));
|
||||
assertThrows(IllegalArgumentException.class, () -> new VolumeBarAggregator(-1d));
|
||||
assertThrows(IllegalArgumentException.class, () -> new VolumeBarAggregator(Double.NaN));
|
||||
assertThrows(IllegalArgumentException.class, () -> new VolumeBarAggregator(Double.NEGATIVE_INFINITY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.analysis.AnalysisContext.MissingHistoryPolicy;
|
||||
import org.ta4j.core.analysis.AnalysisContext.PositionInclusionPolicy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnalysisContext}.
|
||||
*/
|
||||
public class AnalysisContextTest {
|
||||
|
||||
@Test
|
||||
public void defaultsUseConservativePolicies() {
|
||||
AnalysisContext defaults = AnalysisContext.defaults();
|
||||
|
||||
assertEquals(MissingHistoryPolicy.STRICT, defaults.missingHistoryPolicy());
|
||||
assertEquals(PositionInclusionPolicy.EXIT_IN_WINDOW, defaults.positionInclusionPolicy());
|
||||
assertEquals(OpenPositionHandling.IGNORE, defaults.openPositionHandling());
|
||||
assertNull(defaults.asOf());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorRejectsNullPolicies() {
|
||||
assertThrows(NullPointerException.class, () -> new AnalysisContext(null, PositionInclusionPolicy.EXIT_IN_WINDOW,
|
||||
OpenPositionHandling.IGNORE, null));
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AnalysisContext(MissingHistoryPolicy.STRICT, null, OpenPositionHandling.IGNORE, null));
|
||||
assertThrows(NullPointerException.class, () -> new AnalysisContext(MissingHistoryPolicy.STRICT,
|
||||
PositionInclusionPolicy.EXIT_IN_WINDOW, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withMethodsReturnUpdatedCopies() {
|
||||
Instant asOf = Instant.parse("2026-02-14T00:00:00Z");
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withMissingHistoryPolicy(MissingHistoryPolicy.CLAMP)
|
||||
.withPositionInclusionPolicy(PositionInclusionPolicy.FULLY_CONTAINED)
|
||||
.withOpenPositionHandling(OpenPositionHandling.MARK_TO_MARKET)
|
||||
.withAsOf(asOf);
|
||||
|
||||
assertEquals(MissingHistoryPolicy.CLAMP, context.missingHistoryPolicy());
|
||||
assertEquals(PositionInclusionPolicy.FULLY_CONTAINED, context.positionInclusionPolicy());
|
||||
assertEquals(OpenPositionHandling.MARK_TO_MARKET, context.openPositionHandling());
|
||||
assertEquals(asOf, context.asOf());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.criteria.NumberOfPositionsCriterion;
|
||||
import org.ta4j.core.criteria.ReturnRepresentation;
|
||||
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;
|
||||
import org.ta4j.core.criteria.helpers.AverageCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnalysisWindow} and window-aware
|
||||
* {@link AnalysisCriterion} calculations.
|
||||
*/
|
||||
public class AnalysisWindowTest {
|
||||
|
||||
@Test
|
||||
public void barRangeFactoryCreatesBarRangeWindow() {
|
||||
AnalysisWindow window = AnalysisWindow.barRange(5, 12);
|
||||
|
||||
assertTrue(window instanceof AnalysisWindow.BarRange);
|
||||
AnalysisWindow.BarRange barRange = (AnalysisWindow.BarRange) window;
|
||||
assertEquals(5, barRange.startIndexInclusive());
|
||||
assertEquals(12, barRange.endIndexInclusive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void barRangeRejectsNegativeStart() {
|
||||
assertThrows(IllegalArgumentException.class, () -> AnalysisWindow.barRange(-1, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void barRangeRejectsEndBeforeStart() {
|
||||
assertThrows(IllegalArgumentException.class, () -> AnalysisWindow.barRange(10, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackBarsFactoryCreatesLookbackBarsWindow() {
|
||||
AnalysisWindow window = AnalysisWindow.lookbackBars(30);
|
||||
|
||||
assertTrue(window instanceof AnalysisWindow.LookbackBars);
|
||||
assertEquals(30, ((AnalysisWindow.LookbackBars) window).barCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackBarsRejectsNonPositiveCount() {
|
||||
assertThrows(IllegalArgumentException.class, () -> AnalysisWindow.lookbackBars(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeRangeFactoryCreatesTimeRangeWindow() {
|
||||
Instant start = Instant.parse("2026-02-10T00:00:00Z");
|
||||
Instant end = Instant.parse("2026-02-14T00:00:00Z");
|
||||
AnalysisWindow window = AnalysisWindow.timeRange(start, end);
|
||||
|
||||
assertTrue(window instanceof AnalysisWindow.TimeRange);
|
||||
AnalysisWindow.TimeRange timeRange = (AnalysisWindow.TimeRange) window;
|
||||
assertEquals(start, timeRange.startInclusive());
|
||||
assertEquals(end, timeRange.endExclusive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeRangeRejectsStartAtOrAfterEnd() {
|
||||
Instant instant = Instant.parse("2026-02-10T00:00:00Z");
|
||||
assertThrows(IllegalArgumentException.class, () -> AnalysisWindow.timeRange(instant, instant));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackDurationFactoryCreatesLookbackDurationWindow() {
|
||||
Duration duration = Duration.ofDays(7);
|
||||
AnalysisWindow window = AnalysisWindow.lookbackDuration(duration);
|
||||
|
||||
assertTrue(window instanceof AnalysisWindow.LookbackDuration);
|
||||
assertEquals(duration, ((AnalysisWindow.LookbackDuration) window).duration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackDurationRejectsNonPositiveDuration() {
|
||||
assertThrows(IllegalArgumentException.class, () -> AnalysisWindow.lookbackDuration(Duration.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculateOverloadMatchesExplicitDefaultContext() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(6, series));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
AnalysisWindow window = AnalysisWindow.barRange(4, 8);
|
||||
|
||||
Num fromOverload = criterion.calculate(series, record, window);
|
||||
Num fromExplicitDefaults = criterion.calculate(series, record, window, AnalysisContext.defaults());
|
||||
|
||||
assertNumEquals(fromExplicitDefaults, fromOverload);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculateRejectsNullSeries() {
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
AnalysisWindow window = AnalysisWindow.barRange(0, 1);
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> criterion.calculate(null, record, window, AnalysisContext.defaults()));
|
||||
assertThrows(NullPointerException.class, () -> criterion.calculate(null, record, window));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculateRejectsNullTradingRecord() {
|
||||
BarSeries series = buildSeries(2);
|
||||
AnalysisWindow window = AnalysisWindow.barRange(0, 1);
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> criterion.calculate(series, null, window, AnalysisContext.defaults()));
|
||||
assertThrows(NullPointerException.class, () -> criterion.calculate(series, null, window));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculateRejectsNullWindow() {
|
||||
BarSeries series = buildSeries(2);
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> criterion.calculate(series, record, null, AnalysisContext.defaults()));
|
||||
assertThrows(NullPointerException.class, () -> criterion.calculate(series, record, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculateRejectsNullContext() {
|
||||
BarSeries series = buildSeries(2);
|
||||
TradingRecord record = new BaseTradingRecord();
|
||||
AnalysisWindow window = AnalysisWindow.barRange(0, 1);
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
assertThrows(NullPointerException.class, () -> criterion.calculate(series, record, window, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultWindowedCalculateUsesStrictHistoryPolicy() {
|
||||
BarSeries series = buildSeries(10);
|
||||
series.setMaximumBarCount(5);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(5, series), Trade.sellAt(6, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> criterion.calculate(series, record, AnalysisWindow.barRange(2, 6)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clampModeIntersectsWindowWithAvailableMovingSeriesRange() {
|
||||
BarSeries series = buildSeries(10);
|
||||
series.setMaximumBarCount(5); // available logical indices: 5..9
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(5, series), Trade.sellAt(6, series),
|
||||
Trade.buyAt(7, series), Trade.sellAt(8, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withMissingHistoryPolicy(AnalysisContext.MissingHistoryPolicy.CLAMP);
|
||||
|
||||
Num positions = criterion.calculate(series, record, AnalysisWindow.barRange(2, 8), context);
|
||||
assertNumEquals(2, positions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeRangeUsesStartInclusiveEndExclusiveMembership() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(6, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
Instant startInclusive = series.getBar(4).getEndTime();
|
||||
Instant endExclusive = series.getBar(7).getEndTime();
|
||||
|
||||
Num positions = criterion.calculate(series, record, AnalysisWindow.timeRange(startInclusive, endExclusive));
|
||||
assertNumEquals(1, positions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackDurationUsesSeriesEndAnchorByDefault() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(3, series), Trade.buyAt(8, series), Trade.sellAt(9, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
Num positions = criterion.calculate(series, record, AnalysisWindow.lookbackDuration(Duration.ofDays(7)));
|
||||
assertNumEquals(2, positions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackBarsUsesSeriesEndAnchorByDefault() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(5, series), Trade.sellAt(7, series), Trade.buyAt(8, series), Trade.sellAt(9, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
Num positions = criterion.calculate(series, record, AnalysisWindow.lookbackBars(3));
|
||||
assertNumEquals(2, positions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackBarsHonorsExplicitAsOfAnchor() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(5, series), Trade.buyAt(5, series), Trade.sellAt(6, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
Instant asOf = series.getBar(6).getEndTime();
|
||||
AnalysisContext context = AnalysisContext.defaults().withAsOf(asOf);
|
||||
|
||||
Num positions = criterion.calculate(series, record, AnalysisWindow.lookbackBars(3), context);
|
||||
assertNumEquals(2, positions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookbackBarsStrictModeThrowsWhenAsOfIsOutsideAvailableHistory() {
|
||||
BarSeries series = buildSeries(10);
|
||||
series.setMaximumBarCount(5); // available logical indices: 5..9
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(5, series), Trade.sellAt(6, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
Instant asOf = series.getBar(5).getEndTime().minus(Duration.ofDays(3));
|
||||
AnalysisContext context = AnalysisContext.defaults().withAsOf(asOf);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> criterion.calculate(series, record, AnalysisWindow.lookbackBars(3), context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullyContainedPolicyExcludesBoundaryCrossingPosition() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(2, series), Trade.sellAt(5, series),
|
||||
Trade.buyAt(6, series), Trade.sellAt(8, series));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withPositionInclusionPolicy(AnalysisContext.PositionInclusionPolicy.FULLY_CONTAINED);
|
||||
|
||||
Num pnl = criterion.calculate(series, record, AnalysisWindow.barRange(4, 8), context);
|
||||
assertNumEquals(2, pnl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markToMarketPolicyIncludesOpenPositionAtWindowEnd() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(6, series));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
|
||||
Num excluded = criterion.calculate(series, record, AnalysisWindow.barRange(4, 8));
|
||||
assertNumEquals(0, excluded);
|
||||
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withOpenPositionHandling(OpenPositionHandling.MARK_TO_MARKET);
|
||||
Num included = criterion.calculate(series, record, AnalysisWindow.barRange(4, 8), context);
|
||||
assertNumEquals(2, included);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullyContainedPolicyExcludesMarkToMarketWhenEntryIsBeforeWindow() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(2, series));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withPositionInclusionPolicy(AnalysisContext.PositionInclusionPolicy.FULLY_CONTAINED)
|
||||
.withOpenPositionHandling(OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
Num result = criterion.calculate(series, record, AnalysisWindow.barRange(4, 8), context);
|
||||
assertNumEquals(0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullyContainedPolicyIncludesEligibleLiveOpenLotsWithMarkToMarket() {
|
||||
BarSeries series = buildSeries(10);
|
||||
BaseTradingRecord record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
record.recordFill(6, new BaseTrade(6, series.getBar(6).getEndTime(), series.getBar(6).getClosePrice(),
|
||||
series.numFactory().one(), null, ExecutionSide.BUY, null, null));
|
||||
record.recordFill(7, new BaseTrade(7, series.getBar(7).getEndTime(), series.getBar(7).getClosePrice(),
|
||||
series.numFactory().one(), null, ExecutionSide.BUY, null, null));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withPositionInclusionPolicy(AnalysisContext.PositionInclusionPolicy.FULLY_CONTAINED)
|
||||
.withOpenPositionHandling(OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
Num result = criterion.calculate(series, record, AnalysisWindow.barRange(7, 8), context);
|
||||
assertNumEquals(1, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clampModeWithNoIntersectionReturnsEmptyProjectedResult() {
|
||||
BarSeries series = buildSeries(10);
|
||||
series.setMaximumBarCount(5); // available logical indices: 5..9
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(5, series), Trade.sellAt(6, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withMissingHistoryPolicy(AnalysisContext.MissingHistoryPolicy.CLAMP);
|
||||
|
||||
Num result = criterion.calculate(series, record, AnalysisWindow.barRange(20, 25), context);
|
||||
assertNumEquals(0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clampModeWithNoIntersectionReturnsReturnNeutralValue() {
|
||||
BarSeries series = buildSeries(10);
|
||||
series.setMaximumBarCount(5); // available logical indices: 5..9
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(5, series), Trade.sellAt(6, series));
|
||||
NetReturnCriterion criterion = new NetReturnCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withMissingHistoryPolicy(AnalysisContext.MissingHistoryPolicy.CLAMP);
|
||||
|
||||
Num result = criterion.calculate(series, record, AnalysisWindow.barRange(20, 25), context);
|
||||
assertNumEquals(1, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedCalculationDoesNotMutateSourceRecord() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(2, series));
|
||||
NumberOfPositionsCriterion criterion = new NumberOfPositionsCriterion();
|
||||
AnalysisContext context = AnalysisContext.defaults()
|
||||
.withMissingHistoryPolicy(AnalysisContext.MissingHistoryPolicy.CLAMP);
|
||||
int originalTradeCount = record.getTrades().size();
|
||||
int originalPositionCount = record.getPositionCount();
|
||||
|
||||
Num result = criterion.calculate(series, record, AnalysisWindow.barRange(0, 3), context);
|
||||
|
||||
assertNumEquals(1, result);
|
||||
assertEquals(originalTradeCount, record.getTrades().size());
|
||||
assertEquals(originalPositionCount, record.getPositionCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wholeWindowMatchesLegacyPnlCalculation() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(8, series));
|
||||
NetProfitLossCriterion criterion = new NetProfitLossCriterion();
|
||||
AnalysisWindow fullWindow = AnalysisWindow.barRange(series.getBeginIndex(), series.getEndIndex());
|
||||
|
||||
Num legacy = criterion.calculate(series, record);
|
||||
Num windowed = criterion.calculate(series, record, fullWindow);
|
||||
assertNumEquals(legacy, windowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wholeWindowMatchesLegacyReturnCalculation() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(8, series));
|
||||
NetReturnCriterion criterion = new NetReturnCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
AnalysisWindow fullWindow = AnalysisWindow.barRange(series.getBeginIndex(), series.getEndIndex());
|
||||
|
||||
Num legacy = criterion.calculate(series, record);
|
||||
Num windowed = criterion.calculate(series, record, fullWindow);
|
||||
assertNumEquals(legacy, windowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wholeWindowMatchesLegacyDrawdownCalculation() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(8, series));
|
||||
MaximumDrawdownCriterion criterion = new MaximumDrawdownCriterion();
|
||||
AnalysisWindow fullWindow = AnalysisWindow.barRange(series.getBeginIndex(), series.getEndIndex());
|
||||
|
||||
Num legacy = criterion.calculate(series, record);
|
||||
Num windowed = criterion.calculate(series, record, fullWindow);
|
||||
assertNumEquals(legacy, windowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowedDrawdownDoesNotScaleWithTrailingBars() {
|
||||
CountingNumFactory numFactory = new CountingNumFactory();
|
||||
BarSeries fullSeries = buildSeries(30, numFactory);
|
||||
TradingRecord fullRecord = new BaseTradingRecord(Trade.buyAt(0, fullSeries), Trade.sellAt(1, fullSeries));
|
||||
MaximumDrawdownCriterion criterion = new MaximumDrawdownCriterion();
|
||||
AnalysisWindow window = AnalysisWindow.barRange(0, 2);
|
||||
|
||||
numFactory.resetMultiplicationCount();
|
||||
Num fullWindowed = criterion.calculate(fullSeries, fullRecord, window);
|
||||
long fullWindowedMultiplications = numFactory.multiplicationCount();
|
||||
|
||||
BarSeries slicedSeries = fullSeries.getSubSeries(0, 3);
|
||||
TradingRecord slicedRecord = new BaseTradingRecord(Trade.buyAt(0, slicedSeries), Trade.sellAt(1, slicedSeries));
|
||||
|
||||
numFactory.resetMultiplicationCount();
|
||||
Num slicedWindowed = criterion.calculate(slicedSeries, slicedRecord, window);
|
||||
long slicedWindowedMultiplications = numFactory.multiplicationCount();
|
||||
|
||||
assertNumEquals(slicedWindowed, fullWindowed);
|
||||
assertEquals(slicedWindowedMultiplications, fullWindowedMultiplications);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wholeWindowMatchesLegacyHelperCalculation() {
|
||||
BarSeries series = buildSeries(10);
|
||||
TradingRecord record = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(4, series), Trade.sellAt(8, series));
|
||||
AverageCriterion criterion = new AverageCriterion(new NetProfitLossCriterion());
|
||||
AnalysisWindow fullWindow = AnalysisWindow.barRange(series.getBeginIndex(), series.getEndIndex());
|
||||
|
||||
Num legacy = criterion.calculate(series, record);
|
||||
Num windowed = criterion.calculate(series, record, fullWindow);
|
||||
assertNumEquals(legacy, windowed);
|
||||
}
|
||||
|
||||
private static BarSeries buildSeries(int barCount) {
|
||||
return buildSeries(barCount, null);
|
||||
}
|
||||
|
||||
private static BarSeries buildSeries(int barCount, NumFactory numFactory) {
|
||||
BarSeries series = new BaseBarSeriesBuilder().withName("windowed-series").build();
|
||||
if (numFactory != null) {
|
||||
series = new BaseBarSeriesBuilder().withName("windowed-series").withNumFactory(numFactory).build();
|
||||
}
|
||||
Instant base = Instant.parse("2026-02-01T00:00:00Z");
|
||||
for (int i = 0; i < barCount; i++) {
|
||||
series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(base.plus(Duration.ofDays(i)))
|
||||
.openPrice(100 + i)
|
||||
.highPrice(101 + i)
|
||||
.lowPrice(99 + i)
|
||||
.closePrice(100 + i)
|
||||
.volume(1)
|
||||
.amount(100 + i)
|
||||
.trades(1)
|
||||
.add();
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
private static final class CountingNumFactory implements NumFactory {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DoubleNumFactory delegate = DoubleNumFactory.getInstance();
|
||||
private long multiplicationCount;
|
||||
|
||||
@Override
|
||||
public Num minusOne() {
|
||||
return wrap(delegate.minusOne());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num zero() {
|
||||
return wrap(delegate.zero());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num one() {
|
||||
return wrap(delegate.one());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num two() {
|
||||
return wrap(delegate.two());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num three() {
|
||||
return wrap(delegate.three());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num hundred() {
|
||||
return wrap(delegate.hundred());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num thousand() {
|
||||
return wrap(delegate.thousand());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num numOf(Number number) {
|
||||
return wrap(delegate.numOf(number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num numOf(String number) {
|
||||
return wrap(delegate.numOf(number));
|
||||
}
|
||||
|
||||
long multiplicationCount() {
|
||||
return multiplicationCount;
|
||||
}
|
||||
|
||||
void resetMultiplicationCount() {
|
||||
multiplicationCount = 0;
|
||||
}
|
||||
|
||||
private Num wrap(Num value) {
|
||||
if (value instanceof CountingNum countingNum && countingNum.factory == this) {
|
||||
return value;
|
||||
}
|
||||
return new CountingNum(this, value);
|
||||
}
|
||||
|
||||
private Num unwrap(Num value) {
|
||||
if (value instanceof CountingNum countingNum) {
|
||||
return countingNum.delegate;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void incrementMultiplicationCount() {
|
||||
multiplicationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingNum implements Num {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final CountingNumFactory factory;
|
||||
private final Num delegate;
|
||||
|
||||
private CountingNum(CountingNumFactory factory, Num delegate) {
|
||||
this.factory = factory;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getDelegate() {
|
||||
return delegate.getDelegate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumFactory getNumFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num plus(Num augend) {
|
||||
return factory.wrap(delegate.plus(factory.unwrap(augend)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num minus(Num subtrahend) {
|
||||
return factory.wrap(delegate.minus(factory.unwrap(subtrahend)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num multipliedBy(Num multiplicand) {
|
||||
factory.incrementMultiplicationCount();
|
||||
return factory.wrap(delegate.multipliedBy(factory.unwrap(multiplicand)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num dividedBy(Num divisor) {
|
||||
return factory.wrap(delegate.dividedBy(factory.unwrap(divisor)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num remainder(Num divisor) {
|
||||
return factory.wrap(delegate.remainder(factory.unwrap(divisor)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num floor() {
|
||||
return factory.wrap(delegate.floor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num ceil() {
|
||||
return factory.wrap(delegate.ceil());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num pow(int n) {
|
||||
return factory.wrap(delegate.pow(n));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num pow(Num n) {
|
||||
return factory.wrap(delegate.pow(factory.unwrap(n)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num log() {
|
||||
return factory.wrap(delegate.log());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num exp() {
|
||||
return factory.wrap(delegate.exp());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num sqrt() {
|
||||
return factory.wrap(delegate.sqrt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num sqrt(MathContext mathContext) {
|
||||
return factory.wrap(delegate.sqrt(mathContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num abs() {
|
||||
return factory.wrap(delegate.abs());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num negate() {
|
||||
return factory.wrap(delegate.negate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isZero() {
|
||||
return delegate.isZero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPositive() {
|
||||
return delegate.isPositive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPositiveOrZero() {
|
||||
return delegate.isPositiveOrZero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNegative() {
|
||||
return delegate.isNegative();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNegativeOrZero() {
|
||||
return delegate.isNegativeOrZero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEqual(Num other) {
|
||||
return delegate.isEqual(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGreaterThan(Num other) {
|
||||
return delegate.isGreaterThan(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGreaterThanOrEqual(Num other) {
|
||||
return delegate.isGreaterThanOrEqual(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLessThan(Num other) {
|
||||
return delegate.isLessThan(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLessThanOrEqual(Num other) {
|
||||
return delegate.isLessThanOrEqual(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num min(Num other) {
|
||||
return factory.wrap(delegate.min(factory.unwrap(other)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num max(Num other) {
|
||||
return factory.wrap(delegate.max(factory.unwrap(other)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNaN() {
|
||||
return delegate.isNaN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal bigDecimalValue() {
|
||||
return delegate.bigDecimalValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Num other) {
|
||||
return delegate.compareTo(factory.unwrap(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof CountingNum other) {
|
||||
return delegate.equals(other.delegate);
|
||||
}
|
||||
if (obj instanceof Num otherNum) {
|
||||
return delegate.equals(factory.unwrap(otherNum));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.FixedTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class CashFlowTest extends AbstractIndicatorTest<Indicator<Num>, Num> {
|
||||
|
||||
public CashFlowTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowSize() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1d, 2d, 3d, 4d, 5d)
|
||||
.build();
|
||||
var cashFlow = new CashFlow(sampleBarSeries, new BaseTradingRecord());
|
||||
assertEquals(5, cashFlow.getSize());
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(1, cashFlow.getValue(2));
|
||||
assertNumEquals(1, cashFlow.getValue(3));
|
||||
assertNumEquals(1, cashFlow.getValue(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowBuyWithOnlyOnePosition() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowRealizedKeepsEntryValueUntilExit() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, EquityCurveMode.REALIZED);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(3, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowRealizedIgnoresOpenPositions() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, EquityCurveMode.REALIZED);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(1, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowMarkToMarketOpenPositionRespectsFinalIndexAndPadsAfterwards() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, 1, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
assertNumEquals(2, cashFlow.getValue(2)); // padded with last computed value at finalIndex
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowWindowedMarkToMarketSeedsWindowStartForOpenPosition() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 120d, 110d, 90d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(3, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, 1, 3, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(1.2d, cashFlow.getValue(1));
|
||||
assertNumEquals(1.1d, cashFlow.getValue(2));
|
||||
assertNumEquals(0.9d, cashFlow.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowWindowedRealizedKeepsWindowStartFlatForOpenPosition() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 120d, 110d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, 1, 2, EquityCurveMode.REALIZED,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(1d, cashFlow.getValue(1));
|
||||
assertNumEquals(1.1d, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowMarkToMarketCanIgnoreOpenPositions() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(1, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowMarkToMarketIncludesOpenPositionsByDefault() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord, EquityCurveMode.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
assertNumEquals(3, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowWithSellAndBuyTrades() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(2, 1, 3, 5, 6, 3, 20)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries),
|
||||
Trade.buyAt(3, sampleBarSeries), Trade.sellAt(4, sampleBarSeries), Trade.sellAt(5, sampleBarSeries),
|
||||
Trade.buyAt(6, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals("0.5", cashFlow.getValue(1));
|
||||
assertNumEquals("0.5", cashFlow.getValue(2));
|
||||
assertNumEquals("0.5", cashFlow.getValue(3));
|
||||
assertNumEquals("0.6", cashFlow.getValue(4));
|
||||
assertNumEquals("0.6", cashFlow.getValue(5));
|
||||
assertNumEquals("-2.8", cashFlow.getValue(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowSell() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1, 2, 4, 8, 16, 32)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(2, sampleBarSeries), Trade.buyAt(3, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(1, cashFlow.getValue(2));
|
||||
assertNumEquals(0, cashFlow.getValue(3));
|
||||
assertNumEquals(0, cashFlow.getValue(4));
|
||||
assertNumEquals(0, cashFlow.getValue(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSell() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1, 2, 4, 8, 16, 32)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries),
|
||||
Trade.sellAt(2, sampleBarSeries), Trade.buyAt(4, sampleBarSeries), Trade.buyAt(4, sampleBarSeries),
|
||||
Trade.sellAt(5, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
assertNumEquals(4, cashFlow.getValue(2));
|
||||
assertNumEquals(0, cashFlow.getValue(3));
|
||||
assertNumEquals(-8, cashFlow.getValue(4));
|
||||
assertNumEquals(-8, cashFlow.getValue(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSellWith20PercentGain() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(110, 100, 90, 80).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(1, sampleBarSeries), Trade.buyAt(3, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(1.1, cashFlow.getValue(2));
|
||||
assertNumEquals(1.2, cashFlow.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSellWith20PercentLoss() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(90, 100, 110, 120).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(1, sampleBarSeries), Trade.buyAt(3, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(0.9, cashFlow.getValue(2));
|
||||
assertNumEquals(0.8, cashFlow.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSellWith100PercentLoss() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(1, sampleBarSeries), Trade.buyAt(11, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(0.9, cashFlow.getValue(2));
|
||||
assertNumEquals(0.8, cashFlow.getValue(3));
|
||||
assertNumEquals(0.7, cashFlow.getValue(4));
|
||||
assertNumEquals(0.6, cashFlow.getValue(5));
|
||||
assertNumEquals(0.5, cashFlow.getValue(6));
|
||||
assertNumEquals(0.4, cashFlow.getValue(7));
|
||||
assertNumEquals(0.3, cashFlow.getValue(8));
|
||||
assertNumEquals(0.2, cashFlow.getValue(9));
|
||||
assertNumEquals(0.1, cashFlow.getValue(10));
|
||||
assertNumEquals(0.0, cashFlow.getValue(11));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSellWithOver100PercentLoss() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 150, 200, 210)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(0, sampleBarSeries), Trade.buyAt(3, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(0.5, cashFlow.getValue(1));
|
||||
assertNumEquals(0.0, cashFlow.getValue(2));
|
||||
assertNumEquals(-0.1, cashFlow.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowShortSellBigLossWithNegativeCashFlow() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(3, 20).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(0, sampleBarSeries), Trade.buyAt(1, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(-4.6667, cashFlow.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowValueWithOnlyOnePositionAndAGapBefore() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 1d, 2d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(1, sampleBarSeries), Trade.sellAt(2, sampleBarSeries));
|
||||
|
||||
CashFlow cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(2, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowValueWithOnlyOnePositionAndAGapAfter() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 2d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertEquals(3, cashFlow.getSize());
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
assertNumEquals(2, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowValueWithTwoPositionsAndLongTimeWithoutTrades() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1d, 2d, 4d, 8d, 16d, 32d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(1, sampleBarSeries), Trade.sellAt(2, sampleBarSeries),
|
||||
Trade.buyAt(4, sampleBarSeries), Trade.sellAt(5, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(2, cashFlow.getValue(2));
|
||||
assertNumEquals(2, cashFlow.getValue(3));
|
||||
assertNumEquals(2, cashFlow.getValue(4));
|
||||
assertNumEquals(4, cashFlow.getValue(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowValue() {
|
||||
// First sample series
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(3d, 2d, 5d, 1000d, 5000d, 0.0001d, 4d, 7d, 6d, 7d, 8d, 5d, 6d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries),
|
||||
Trade.buyAt(6, sampleBarSeries), Trade.sellAt(8, sampleBarSeries), Trade.buyAt(9, sampleBarSeries),
|
||||
Trade.sellAt(11, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2d / 3, cashFlow.getValue(1));
|
||||
assertNumEquals(5d / 3, cashFlow.getValue(2));
|
||||
assertNumEquals(5d / 3, cashFlow.getValue(3));
|
||||
assertNumEquals(5d / 3, cashFlow.getValue(4));
|
||||
assertNumEquals(5d / 3, cashFlow.getValue(5));
|
||||
assertNumEquals(5d / 3, cashFlow.getValue(6));
|
||||
assertNumEquals(5d / 3 * 7d / 4, cashFlow.getValue(7));
|
||||
assertNumEquals(5d / 3 * 6d / 4, cashFlow.getValue(8));
|
||||
assertNumEquals(5d / 3 * 6d / 4, cashFlow.getValue(9));
|
||||
assertNumEquals(5d / 3 * 6d / 4 * 8d / 7, cashFlow.getValue(10));
|
||||
assertNumEquals(5d / 3 * 6d / 4 * 5d / 7, cashFlow.getValue(11));
|
||||
assertNumEquals(5d / 3 * 6d / 4 * 5d / 7, cashFlow.getValue(12));
|
||||
|
||||
// Second sample series
|
||||
sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(5d, 6d, 3d, 7d, 8d, 6d, 10d, 15d, 6d)
|
||||
.build();
|
||||
tradingRecord = new BaseTradingRecord(Trade.buyAt(4, sampleBarSeries), Trade.sellAt(5, sampleBarSeries),
|
||||
Trade.buyAt(6, sampleBarSeries), Trade.sellAt(8, sampleBarSeries));
|
||||
|
||||
var flow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
assertNumEquals(1, flow.getValue(0));
|
||||
assertNumEquals(1, flow.getValue(1));
|
||||
assertNumEquals(1, flow.getValue(2));
|
||||
assertNumEquals(1, flow.getValue(3));
|
||||
assertNumEquals(1, flow.getValue(4));
|
||||
assertNumEquals("0.75", flow.getValue(5));
|
||||
assertNumEquals("0.75", flow.getValue(6));
|
||||
assertNumEquals("1.125", flow.getValue(7));
|
||||
assertNumEquals("0.45", flow.getValue(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowValueWithNoPositions() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(3d, 2d, 5d, 4d, 7d, 6d, 7d, 8d, 5d, 6d)
|
||||
.build();
|
||||
var cashFlow = new CashFlow(sampleBarSeries, new BaseTradingRecord());
|
||||
assertNumEquals(1, cashFlow.getValue(4));
|
||||
assertNumEquals(1, cashFlow.getValue(7));
|
||||
assertNumEquals(1, cashFlow.getValue(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowWithZeroCostsProducesConsistentValuesForCompressedSeries() {
|
||||
double[] originalPrices = { 100, 105, 110, 115, 120 };
|
||||
double[] compressedPrices = { 100, 110, 120 };
|
||||
|
||||
var originalSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(originalPrices).build();
|
||||
var compressedSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(compressedPrices).build();
|
||||
|
||||
var originalRecord = new BaseTradingRecord(Trade.buyAt(0, originalSeries),
|
||||
Trade.sellAt(originalSeries.getEndIndex(), originalSeries));
|
||||
var compressedRecord = new BaseTradingRecord(Trade.buyAt(0, compressedSeries),
|
||||
Trade.sellAt(compressedSeries.getEndIndex(), compressedSeries));
|
||||
|
||||
var originalCashFlow = new CashFlow(originalSeries, originalRecord);
|
||||
var compressedCashFlow = new CashFlow(compressedSeries, compressedRecord);
|
||||
|
||||
assertNumEquals(originalCashFlow.getValue(2), compressedCashFlow.getValue(1));
|
||||
assertNumEquals(originalCashFlow.getValue(4), compressedCashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reallyLongCashFlow() {
|
||||
int size = 1000000;
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(Collections.nCopies(size, 10d))
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries),
|
||||
Trade.sellAt(size - 1, sampleBarSeries));
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
assertNumEquals(1, cashFlow.getValue(size - 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowBuyExitSameBarShouldNotReturnNaN() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 100d).build();
|
||||
|
||||
var entryPrice = numFactory.hundred();
|
||||
var exitPrice = numFactory.numOf(90);
|
||||
var amount = numFactory.one();
|
||||
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, entryPrice, amount),
|
||||
Trade.sellAt(0, exitPrice, amount));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(0.9, cashFlow.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowIgnoresOpenPositionWhenConfigured() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 120d, 180d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries),
|
||||
Trade.buyAt(1, sampleBarSeries));
|
||||
|
||||
var markToMarket = new CashFlow(sampleBarSeries, tradingRecord, OpenPositionHandling.MARK_TO_MARKET);
|
||||
var ignore = new CashFlow(sampleBarSeries, tradingRecord, OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(1.8, markToMarket.getValue(2));
|
||||
assertNumEquals(1.2, ignore.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowFromPositionUsesMarkToMarketCurve() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var position = new Position(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, position);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(2, cashFlow.getValue(1));
|
||||
assertNumEquals(3, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowFromPositionPreservesCostModels() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 100d, 100d).build();
|
||||
var transactionCost = new FixedTransactionCostModel(1d);
|
||||
var holdingCost = new FixedHoldingCostModel(4d);
|
||||
var amount = numFactory.one();
|
||||
var entry = Trade.buyAt(0, sampleBarSeries.getBar(0).getClosePrice(), amount, transactionCost);
|
||||
var exit = Trade.sellAt(2, sampleBarSeries.getBar(2).getClosePrice(), amount, transactionCost);
|
||||
var position = new Position(entry, exit, transactionCost, holdingCost);
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, position);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(98d / 101d, cashFlow.getValue(1));
|
||||
assertNumEquals(97d / 101d, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowFromPositionUsesRealizedCurve() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d, 3d).build();
|
||||
var position = new Position(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(2, sampleBarSeries));
|
||||
|
||||
var cashFlow = new CashFlow(sampleBarSeries, position, EquityCurveMode.REALIZED);
|
||||
|
||||
assertNumEquals(1, cashFlow.getValue(0));
|
||||
assertNumEquals(1, cashFlow.getValue(1));
|
||||
assertNumEquals(3, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowMarkToMarketDoesNotUseFutureExitPriceWhenExitAfterFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), numFactory.one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), numFactory.one());
|
||||
|
||||
var cashFlow = new CashFlow(series, tradingRecord, 2, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var expected = series.getBar(2).getClosePrice().dividedBy(series.getBar(0).getClosePrice());
|
||||
assertTrue(cashFlow.getValue(2).isEqual(expected));
|
||||
assertNumEquals(expected, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowIgnoreSkipsPositionsThatAreOpenAtFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), numFactory.one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), numFactory.one());
|
||||
|
||||
var cashFlow = new CashFlow(series, tradingRecord, 2, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(series.numFactory().one(), cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashFlowIncludesMultipleOpenLotsFromBaseTradingRecord() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 12d, 14d).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(0, new BaseTrade(0, Instant.EPOCH, series.getBar(0).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
record.recordFill(1, new BaseTrade(1, Instant.EPOCH, series.getBar(1).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
|
||||
var cashFlow = new CashFlow(series, record, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var expectedAt1 = series.getBar(1).getClosePrice().dividedBy(series.getBar(0).getClosePrice());
|
||||
var ratioFirst = series.getBar(2).getClosePrice().dividedBy(series.getBar(0).getClosePrice());
|
||||
var ratioSecond = series.getBar(2).getClosePrice().dividedBy(series.getBar(1).getClosePrice());
|
||||
var expectedAt2 = ratioFirst.multipliedBy(ratioSecond);
|
||||
|
||||
assertNumEquals(expectedAt1, cashFlow.getValue(1));
|
||||
assertNumEquals(expectedAt2, cashFlow.getValue(2));
|
||||
}
|
||||
|
||||
private record FixedHoldingCostModel(double fee) implements CostModel {
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position, int finalIndex) {
|
||||
return cost(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
return cost(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return price.getNumFactory().numOf(fee);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
if (otherModel instanceof FixedHoldingCostModel(double fee1)) {
|
||||
return fee1 == fee;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Num cost(Position position) {
|
||||
return position.getEntry().getPricePerAsset().getNumFactory().numOf(fee);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class CumulativePnLTest extends AbstractIndicatorTest<org.ta4j.core.Indicator<Num>, Num> {
|
||||
|
||||
public CumulativePnLTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sizeWithoutTrades() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1, 2, 3, 4, 5).build();
|
||||
var pnl = new CumulativePnL(series, new BaseTradingRecord());
|
||||
|
||||
assertEquals(5, pnl.getSize());
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longAndShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 95, 90).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(5, pnl.getValue(1));
|
||||
assertNumEquals(5, pnl.getValue(2));
|
||||
assertNumEquals(10, pnl.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionUsesFinalPrice() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 102).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(5, pnl.getValue(1));
|
||||
assertNumEquals(2, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realizedModeUsesExitOnly() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, EquityCurveMode.REALIZED);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(1));
|
||||
assertNumEquals(5, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realizedModeIgnoresOpenPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 102).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, EquityCurveMode.REALIZED);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(1));
|
||||
assertNumEquals(0, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markToMarketCanIgnoreOpenPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 102).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.IGNORE);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(1));
|
||||
assertNumEquals(0, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realizedModeIgnoresOpenPositionEvenWithMarkToMarketHandling() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 102).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, EquityCurveMode.REALIZED, OpenPositionHandling.MARK_TO_MARKET);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(1));
|
||||
assertNumEquals(0, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markToMarketRespectsFinalIndexForOpenPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 120).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, 1, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(10, pnl.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openShortPositionMarkToMarketAndRealized() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 90).build();
|
||||
var record = new BaseTradingRecord(Trade.sellAt(0, series));
|
||||
|
||||
var markToMarket = new CumulativePnL(series, record);
|
||||
assertNumEquals(0, markToMarket.getValue(0));
|
||||
assertNumEquals(5, markToMarket.getValue(1));
|
||||
assertNumEquals(10, markToMarket.getValue(2));
|
||||
|
||||
var realized = new CumulativePnL(series, record, EquityCurveMode.REALIZED);
|
||||
assertNumEquals(0, realized.getValue(0));
|
||||
assertNumEquals(0, realized.getValue(1));
|
||||
assertNumEquals(0, realized.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positionConstructorUsesMarkToMarket() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var position = new Position(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, position);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(10, pnl.getValue(1));
|
||||
assertNumEquals(5, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positionConstructorUsesRealizedMode() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var position = new Position(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
var pnl = new CumulativePnL(series, position, EquityCurveMode.REALIZED);
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(0, pnl.getValue(1));
|
||||
assertNumEquals(5, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cumulativePnL_markToMarket_doesNotUseFutureExitPriceWhenExitAfterFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), series.numFactory().one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), series.numFactory().one());
|
||||
|
||||
var cumulativePnL = new CumulativePnL(series, tradingRecord, 2, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var expected = series.getBar(2).getClosePrice().minus(series.getBar(0).getClosePrice());
|
||||
assertNumEquals(cumulativePnL.getValue(2), expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cumulativePnL_ignore_skipsPositionsThatAreOpenAtFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), series.numFactory().one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), series.numFactory().one());
|
||||
|
||||
var cumulativePnL = new CumulativePnL(series, tradingRecord, 2, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(cumulativePnL.getValue(2), series.numFactory().zero());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cumulativePnLIncludesMultipleOpenLotsFromBaseTradingRecord() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 12d, 14d).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(0, new BaseTrade(0, Instant.EPOCH, series.getBar(0).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
record.recordFill(1, new BaseTrade(1, Instant.EPOCH, series.getBar(1).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
|
||||
var pnl = new CumulativePnL(series, record, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(0, pnl.getValue(0));
|
||||
assertNumEquals(2, pnl.getValue(1));
|
||||
assertNumEquals(6, pnl.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithFinalIndexDelegatesToMain() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
int typicalIndex = series.getEndIndex();
|
||||
int beyondEnd = series.getEndIndex() + 2;
|
||||
|
||||
var expectedTypical = new CumulativePnL(series, record, typicalIndex, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
var actualTypical = new CumulativePnL(series, record, typicalIndex);
|
||||
assertSameValues(expectedTypical, actualTypical);
|
||||
|
||||
var expectedBeyond = new CumulativePnL(series, record, beyondEnd, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
var actualBeyond = new CumulativePnL(series, record, beyondEnd);
|
||||
assertSameValues(expectedBeyond, actualBeyond);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithFinalIndexAndModeDelegatesToMain() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
int typicalIndex = series.getEndIndex();
|
||||
int beyondEnd = series.getEndIndex() + 2;
|
||||
|
||||
var expectedTypical = new CumulativePnL(series, record, typicalIndex, EquityCurveMode.REALIZED,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
var actualTypical = new CumulativePnL(series, record, typicalIndex, EquityCurveMode.REALIZED);
|
||||
assertSameValues(expectedTypical, actualTypical);
|
||||
|
||||
var expectedBeyond = new CumulativePnL(series, record, beyondEnd, EquityCurveMode.REALIZED,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
var actualBeyond = new CumulativePnL(series, record, beyondEnd, EquityCurveMode.REALIZED);
|
||||
assertSameValues(expectedBeyond, actualBeyond);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithOpenPositionHandlingDelegatesToMain() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 105).build();
|
||||
var record = new BaseTradingRecord(Trade.buyAt(0, series));
|
||||
|
||||
var expected = new CumulativePnL(series, record, record.getEndIndex(series), EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
var actual = new CumulativePnL(series, record, OpenPositionHandling.IGNORE);
|
||||
|
||||
assertSameValues(expected, actual);
|
||||
}
|
||||
|
||||
private void assertSameValues(CumulativePnL expected, CumulativePnL actual) {
|
||||
assertEquals(expected.getSize(), actual.getSize());
|
||||
for (int i = 0; i < expected.getSize(); i++) {
|
||||
assertNumEquals(expected.getValue(i), actual.getValue(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.stream.IntStream;
|
||||
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.BaseTradingRecord;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.analysis.ExcessReturns.CashReturnPolicy;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.utils.TimeConstants;
|
||||
|
||||
public class ExcessReturnsTest extends AbstractIndicatorTest<Indicator<Num>, Num> {
|
||||
|
||||
public ExcessReturnsTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashReturnPolicyControlsFlatIntervalExcessGrowth() {
|
||||
var series = getBarSeries("excess_returns_series");
|
||||
var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
var closes = new double[] { 100d, 110d, 110d, 121d };
|
||||
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
var endTime = start.plus(Duration.ofDays(i + 1L));
|
||||
var close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var one = numFactory.one();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), one);
|
||||
tradingRecord.exit(1, series.getBar(1).getClosePrice(), one);
|
||||
tradingRecord.enter(2, series.getBar(2).getClosePrice(), one);
|
||||
tradingRecord.exit(3, series.getBar(3).getClosePrice(), one);
|
||||
|
||||
var annualRate = numFactory.numOf(0.05d);
|
||||
var perBarRiskFree = Math.pow(1.0 + annualRate.doubleValue(),
|
||||
Duration.ofDays(1).getSeconds() / TimeConstants.SECONDS_PER_YEAR);
|
||||
|
||||
var earnsRiskFree = new ExcessReturns(series, annualRate, CashReturnPolicy.CASH_EARNS_RISK_FREE, tradingRecord)
|
||||
.excessReturn(0, 3)
|
||||
.doubleValue();
|
||||
var earnsZero = new ExcessReturns(series, annualRate, CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord)
|
||||
.excessReturn(0, 3)
|
||||
.doubleValue();
|
||||
|
||||
var expectedEarnsRiskFree = (1.21d / (perBarRiskFree * perBarRiskFree)) - 1.0d;
|
||||
var expectedEarnsZero = (1.21d / (perBarRiskFree * perBarRiskFree * perBarRiskFree)) - 1.0d;
|
||||
|
||||
assertEquals(expectedEarnsRiskFree, earnsRiskFree, 1e-12);
|
||||
assertEquals(expectedEarnsZero, earnsZero, 1e-12);
|
||||
assertTrue(earnsZero < earnsRiskFree);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPolicyKeepsFlatCashNeutralWhenRiskFreeIsZero() {
|
||||
var series = buildDailySeries(new double[] { 100d, 100d, 100d });
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
|
||||
var zero = numFactory.zero();
|
||||
var actual = new ExcessReturns(series, zero, CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord).excessReturn(0,
|
||||
2);
|
||||
|
||||
assertEquals(zero, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashEarnsZeroPenalizesFlatCashAgainstPositiveRiskFree() {
|
||||
var series = buildDailySeries(new double[] { 100d, 100d });
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var annualRate = numFactory.numOf(0.1d);
|
||||
var perBarRiskFree = Math.pow(1.0 + annualRate.doubleValue(),
|
||||
Duration.ofDays(1).getSeconds() / TimeConstants.SECONDS_PER_YEAR);
|
||||
|
||||
var actual = new ExcessReturns(series, annualRate, CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord)
|
||||
.excessReturn(0, 1)
|
||||
.doubleValue();
|
||||
var expected = (1.0d / perBarRiskFree) - 1.0d;
|
||||
|
||||
assertEquals(expected, actual, 1e-12);
|
||||
assertTrue(actual < 0.0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionHandlingControlsExcessReturnForOpenPositions() {
|
||||
var series = buildDailySeries(new double[] { 100d, 120d, 180d });
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var amount = numFactory.one();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), amount);
|
||||
tradingRecord.exit(1, series.getBar(1).getClosePrice(), amount);
|
||||
tradingRecord.enter(1, series.getBar(1).getClosePrice(), amount);
|
||||
|
||||
var markToMarket = new ExcessReturns(series, numFactory.zero(), CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord,
|
||||
OpenPositionHandling.MARK_TO_MARKET).excessReturn(0, 2).doubleValue();
|
||||
var ignore = new ExcessReturns(series, numFactory.zero(), CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord,
|
||||
OpenPositionHandling.IGNORE).excessReturn(0, 2).doubleValue();
|
||||
|
||||
assertEquals(0.8d, markToMarket, 1e-12);
|
||||
assertEquals(0.2d, ignore, 1e-12);
|
||||
assertTrue(markToMarket > ignore);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroPreviousEquityDoesNotBreakExcessReturn() {
|
||||
var series = buildDailySeries(new double[] { 1d, 0d, 0d });
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var one = numFactory.one();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), one);
|
||||
tradingRecord.exit(1, series.getBar(1).getClosePrice(), one);
|
||||
|
||||
var actual = new ExcessReturns(series, numFactory.zero(), CashReturnPolicy.CASH_EARNS_ZERO, tradingRecord)
|
||||
.excessReturn(0, 2);
|
||||
|
||||
assertEquals(one.negate(), actual);
|
||||
}
|
||||
|
||||
private BarSeries buildDailySeries(double[] closes) {
|
||||
var series = getBarSeries("excess_returns_series");
|
||||
var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
var endTime = start.plus(Duration.ofDays(i + 1L));
|
||||
var close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.junit.Test;
|
||||
|
||||
public class InvestedIntervalTest extends AbstractIndicatorTest<Indicator<Boolean>, Num> {
|
||||
|
||||
public InvestedIntervalTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void marksIntervalsForClosedAndOpenPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1, 1, 1, 1, 1, 1).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var price = series.numFactory().numOf(1);
|
||||
var amount = series.numFactory().numOf(1);
|
||||
|
||||
tradingRecord.enter(1, price, amount);
|
||||
tradingRecord.exit(3, price, amount);
|
||||
tradingRecord.enter(4, price, amount);
|
||||
|
||||
var indicator = new InvestedInterval(series, tradingRecord);
|
||||
|
||||
assertThat(indicator.getValue(0)).as("first bar interval").isFalse();
|
||||
assertThat(indicator.getValue(1)).as("entry bar interval").isFalse();
|
||||
assertThat(indicator.getValue(2)).as("between entry and exit").isTrue();
|
||||
assertThat(indicator.getValue(3)).as("exit interval").isTrue();
|
||||
assertThat(indicator.getValue(4)).as("open position entry interval").isFalse();
|
||||
assertThat(indicator.getValue(5)).as("open position following interval").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsFalseWhenNoPositionsExist() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1, 1, 1).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
|
||||
var indicator = new InvestedInterval(series, tradingRecord);
|
||||
|
||||
assertThat(indicator.getValue(0)).isFalse();
|
||||
assertThat(indicator.getValue(1)).isFalse();
|
||||
assertThat(indicator.getValue(2)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresOpenPositionsWhenConfigured() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1, 1, 1, 1, 1, 1).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var price = series.numFactory().numOf(1);
|
||||
var amount = series.numFactory().numOf(1);
|
||||
|
||||
tradingRecord.enter(1, price, amount);
|
||||
tradingRecord.exit(3, price, amount);
|
||||
tradingRecord.enter(4, price, amount);
|
||||
|
||||
var indicator = new InvestedInterval(series, tradingRecord, OpenPositionHandling.IGNORE);
|
||||
|
||||
assertThat(indicator.getValue(2)).as("between entry and exit").isTrue();
|
||||
assertThat(indicator.getValue(3)).as("exit interval").isTrue();
|
||||
assertThat(indicator.getValue(4)).as("open position entry interval").isFalse();
|
||||
assertThat(indicator.getValue(5)).as("open position following interval").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handlesEmptySeriesWithoutIntervals() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData().build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
|
||||
var indicator = new InvestedInterval(series, tradingRecord);
|
||||
|
||||
assertThat(series.getEndIndex()).isEqualTo(-1);
|
||||
assertThat(series.getBarCount()).isEqualTo(0);
|
||||
assertThat(indicator.getValue(0)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void respectsNonZeroBeginIndexWhenMarkingIntervals() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1, 1, 1, 1, 1)
|
||||
.withMaxBarCount(2)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
var price = series.numFactory().one();
|
||||
var amount = series.numFactory().one();
|
||||
|
||||
tradingRecord.enter(0, price, amount);
|
||||
|
||||
var indicator = new InvestedInterval(series, tradingRecord, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
int beginIndex = series.getBeginIndex();
|
||||
assertThat(beginIndex).isGreaterThan(0);
|
||||
assertThat(indicator.getValue(beginIndex)).as("begin index interval").isFalse();
|
||||
assertThat(indicator.getValue(beginIndex + 1)).as("first invested interval after begin index").isTrue();
|
||||
|
||||
var ignoreIndicator = new InvestedInterval(series, tradingRecord, OpenPositionHandling.IGNORE);
|
||||
assertThat(ignoreIndicator.getValue(beginIndex + 1)).as("ignored open position interval").isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class NamedScoreFunctionTest {
|
||||
|
||||
@Test
|
||||
public void applyDelegatesToScore() {
|
||||
NamedScoreFunction<String, Integer> scoreFunction = new NamedScoreFunction<>() {
|
||||
@Override
|
||||
public String name() {
|
||||
return "length";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer score(String input) {
|
||||
return input == null ? 0 : input.length();
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(scoreFunction.name()).isEqualTo("length");
|
||||
assertThat(scoreFunction.apply("ta4j")).isEqualTo(4);
|
||||
assertThat(scoreFunction.score("wave")).isEqualTo(4);
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class PerformanceIndicatorTest extends AbstractIndicatorTest<PerformanceIndicator, Num> {
|
||||
|
||||
public PerformanceIndicatorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recalculationWithEmptyRecordKeepsValuesStable() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
|
||||
var cashFlow = new CashFlow(series, tradingRecord);
|
||||
assertUnchanged(cashFlow, tradingRecord, series.getEndIndex(), numFactory.one());
|
||||
|
||||
var cumulativePnL = new CumulativePnL(series, tradingRecord);
|
||||
assertUnchanged(cumulativePnL, tradingRecord, series.getEndIndex(), numFactory.zero());
|
||||
|
||||
var returnsIndicator = new Returns(series, tradingRecord);
|
||||
assertUnchanged(returnsIndicator, tradingRecord, series.getEndIndex(),
|
||||
returnsIndicator.getValue(series.getEndIndex()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineEndIndexUsesExitIndexWhenClosed() {
|
||||
var position = new Position();
|
||||
position.operate(0);
|
||||
position.operate(5);
|
||||
|
||||
var result = testIndicator().determineEndIndex(position, 10, 20);
|
||||
assertEquals(5, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineEndIndexUsesFinalIndexBeforeExit() {
|
||||
var position = new Position();
|
||||
position.operate(0);
|
||||
position.operate(10);
|
||||
|
||||
var result = testIndicator().determineEndIndex(position, 8, 20);
|
||||
assertEquals(8, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineEndIndexForOpenPositionClampsToMax() {
|
||||
var position = new Position();
|
||||
position.operate(0);
|
||||
|
||||
var result = testIndicator().determineEndIndex(position, 15, 12);
|
||||
assertEquals(12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineEndIndexForClosedPositionClampsToMax() {
|
||||
var position = new Position();
|
||||
position.operate(0);
|
||||
position.operate(15);
|
||||
|
||||
var result = testIndicator().determineEndIndex(position, 20, 10);
|
||||
assertEquals(10, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCostSubtractsHoldingCostForLongTrades() {
|
||||
var raw = numFactory.hundred();
|
||||
var cost = numFactory.numOf(1.5);
|
||||
|
||||
var result = testIndicator().addCost(raw, cost, true);
|
||||
assertNumEquals("98.5", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCostAddsHoldingCostForShortTrades() {
|
||||
var raw = numFactory.hundred();
|
||||
var cost = numFactory.numOf(1.5);
|
||||
|
||||
var result = testIndicator().addCost(raw, cost, false);
|
||||
assertNumEquals("101.5", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markToMarketSkipsFutureOpenPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d, 11d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(10, numFactory.one(), numFactory.one());
|
||||
|
||||
var calls = new AtomicInteger();
|
||||
var indicator = new PerformanceIndicator() {
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return numFactory.zero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EquityCurveMode getEquityCurveMode() {
|
||||
return EquityCurveMode.MARK_TO_MARKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculatePosition(Position position, int finalIndex) {
|
||||
calls.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
indicator.calculate(tradingRecord, 5, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertEquals(0, calls.get());
|
||||
}
|
||||
|
||||
private void assertUnchanged(PerformanceIndicator indicator, TradingRecord tradingRecord, int finalIndex,
|
||||
Num expectedValue) {
|
||||
indicator.calculate(tradingRecord, finalIndex, OpenPositionHandling.MARK_TO_MARKET);
|
||||
assertNumEquals(expectedValue, indicator.getValue(finalIndex));
|
||||
}
|
||||
|
||||
private PerformanceIndicator testIndicator() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d).build();
|
||||
return new PerformanceIndicator() {
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return numFactory.zero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EquityCurveMode getEquityCurveMode() {
|
||||
return EquityCurveMode.MARK_TO_MARKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculatePosition(Position position, int finalIndex) {
|
||||
// no-op for testing default helpers
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.criteria.ReturnRepresentation;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNum;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class ReturnsTest extends AbstractIndicatorTest<Indicator<Num>, Num> {
|
||||
|
||||
public ReturnsTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnSize() {
|
||||
// Test with both LOG and DECIMAL representations
|
||||
ReturnRepresentation[] representations = { ReturnRepresentation.LOG, ReturnRepresentation.DECIMAL };
|
||||
for (var representation : representations) {
|
||||
// No return at index 0
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1d, 2d, 3d, 4d, 5d)
|
||||
.build();
|
||||
var returns = new Returns(sampleBarSeries, new BaseTradingRecord(), representation);
|
||||
assertEquals(4, returns.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleReturnPositionArith() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL);
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(1.0, returns.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsWithSellAndBuyTrades() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(2, 1, 3, 5, 6, 3, 20)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries),
|
||||
Trade.buyAt(3, sampleBarSeries), Trade.sellAt(4, sampleBarSeries), Trade.sellAt(5, sampleBarSeries),
|
||||
Trade.buyAt(6, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(-0.5, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
assertNumEquals(0, returns.getValue(3));
|
||||
assertNumEquals(1d / 5, returns.getValue(4));
|
||||
assertNumEquals(0, returns.getValue(5));
|
||||
assertNumEquals(1 - (20d / 3), returns.getValue(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsRealizedModeUsesExitOnly() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(10d, 12d, 11d, 13d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(3, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL,
|
||||
EquityCurveMode.REALIZED);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
assertNumEquals(0.3, returns.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMarkToMarketIncludesOpenPosition() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 105d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0.1, returns.getValue(1));
|
||||
assertNumEquals((105d / 110d) - 1d, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsCanIgnoreOpenPosition() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 105d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL,
|
||||
OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsWithGaps() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d, 11d, 12d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(2, sampleBarSeries), Trade.buyAt(5, sampleBarSeries),
|
||||
Trade.buyAt(8, sampleBarSeries), Trade.sellAt(10, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.LOG);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
assertNumEquals(-0.28768207245178085, returns.getValue(3));
|
||||
assertNumEquals(-0.22314355131420976, returns.getValue(4));
|
||||
assertNumEquals(-0.1823215567939546, returns.getValue(5));
|
||||
assertNumEquals(0, returns.getValue(6));
|
||||
assertNumEquals(0, returns.getValue(7));
|
||||
assertNumEquals(0, returns.getValue(8));
|
||||
assertNumEquals(0.10536051565782635, returns.getValue(9));
|
||||
assertNumEquals(0.09531017980432493, returns.getValue(10));
|
||||
assertNumEquals(0, returns.getValue(11));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsWithNoPositions() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(3d, 2d, 5d, 4d, 7d, 6d, 7d, 8d, 5d, 6d)
|
||||
.build();
|
||||
var returns = new Returns(sampleBarSeries, new BaseTradingRecord(), ReturnRepresentation.LOG);
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0, returns.getValue(4));
|
||||
assertNumEquals(0, returns.getValue(7));
|
||||
assertNumEquals(0, returns.getValue(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsPrecision() {
|
||||
var doubleNumSeries = new MockBarSeriesBuilder().withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withData(1.2d, 1.1d)
|
||||
.build();
|
||||
|
||||
var highPrecisionContext = new MathContext(32, RoundingMode.HALF_UP);
|
||||
var precisionFactory = DecimalNumFactory.getInstance(highPrecisionContext);
|
||||
var precisionSeries = new MockBarSeriesBuilder().withNumFactory(precisionFactory).withData(1.2d, 1.1d).build();
|
||||
|
||||
var fullRecordDouble = new BaseTradingRecord();
|
||||
fullRecordDouble.enter(doubleNumSeries.getBeginIndex(), doubleNumSeries.getBar(0).getClosePrice(),
|
||||
doubleNumSeries.numFactory().one());
|
||||
fullRecordDouble.exit(doubleNumSeries.getEndIndex(), doubleNumSeries.getBar(1).getClosePrice(),
|
||||
doubleNumSeries.numFactory().one());
|
||||
|
||||
var fullRecordPrecision = new BaseTradingRecord();
|
||||
fullRecordPrecision.enter(precisionSeries.getBeginIndex(), precisionSeries.getBar(0).getClosePrice(),
|
||||
precisionSeries.numFactory().one());
|
||||
fullRecordPrecision.exit(precisionSeries.getEndIndex(), precisionSeries.getBar(1).getClosePrice(),
|
||||
precisionSeries.numFactory().one());
|
||||
|
||||
var arithDouble = new Returns(doubleNumSeries, fullRecordDouble, ReturnRepresentation.DECIMAL).getValue(1);
|
||||
var arithPrecision = new Returns(precisionSeries, fullRecordPrecision, ReturnRepresentation.DECIMAL)
|
||||
.getValue(1);
|
||||
var logDouble = new Returns(doubleNumSeries, fullRecordDouble, ReturnRepresentation.LOG).getValue(1);
|
||||
var logPrecision = new Returns(precisionSeries, fullRecordPrecision, ReturnRepresentation.LOG).getValue(1);
|
||||
|
||||
assertFalse(arithDouble.isNaN());
|
||||
assertFalse(arithPrecision.isNaN());
|
||||
assertFalse(logDouble.isNaN());
|
||||
assertFalse(logPrecision.isNaN());
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(-0.08333333333333326), arithDouble);
|
||||
|
||||
var expectedArithmetic = DecimalNum.valueOf("1.1", highPrecisionContext)
|
||||
.dividedBy(DecimalNum.valueOf("1.2", highPrecisionContext))
|
||||
.minus(DecimalNum.valueOf(1, highPrecisionContext));
|
||||
assertNumEquals(expectedArithmetic, arithPrecision);
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(-0.08701137698962969), logDouble);
|
||||
assertNumEquals(DecimalNum.valueOf("-0.087011376989629766167765901873746", highPrecisionContext), logPrecision);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsRealizedModeUsesRepresentationForFlatPeriods() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(10d, 12d, 11d, 13d)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(3, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.MULTIPLICATIVE,
|
||||
EquityCurveMode.REALIZED);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(1, returns.getValue(1));
|
||||
assertNumEquals(1, returns.getValue(2));
|
||||
assertNumEquals(1.3, returns.getValue(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realizedModeIgnoresOpenPositionEvenWithMarkToMarketHandling() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 105d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, sampleBarSeries.getEndIndex(),
|
||||
ReturnRepresentation.DECIMAL, EquityCurveMode.REALIZED, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsRespectFinalIndexForOpenPositions() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 120d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, 1, ReturnRepresentation.DECIMAL,
|
||||
EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(0.1, returns.getValue(1));
|
||||
assertNumEquals(0, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMarkToMarketIncludesOpenPositionMultiplicative() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 105d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.MULTIPLICATIVE);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(1.1, returns.getValue(1));
|
||||
assertNumEquals(105d / 110d, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsCanIgnoreOpenPositionMultiplicative() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 110d, 105d).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries));
|
||||
|
||||
var returns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.MULTIPLICATIVE,
|
||||
OpenPositionHandling.IGNORE);
|
||||
|
||||
assertNumEquals(NaN.NaN, returns.getValue(0));
|
||||
assertNumEquals(1, returns.getValue(1));
|
||||
assertNumEquals(1, returns.getValue(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsFromPositionDefaultRepresentationMatchesTradingRecord() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d).build();
|
||||
var position = new Position(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
|
||||
var positionReturns = new Returns(sampleBarSeries, position);
|
||||
var tradingRecordReturns = new Returns(sampleBarSeries, tradingRecord);
|
||||
|
||||
assertNumEquals(tradingRecordReturns.getValue(0), positionReturns.getValue(0));
|
||||
assertNumEquals(tradingRecordReturns.getValue(1), positionReturns.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsFromPositionDecimalMatchesTradingRecord() {
|
||||
var sampleBarSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 2d).build();
|
||||
var position = new Position(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, sampleBarSeries), Trade.sellAt(1, sampleBarSeries));
|
||||
|
||||
var positionReturns = new Returns(sampleBarSeries, position, ReturnRepresentation.DECIMAL);
|
||||
var tradingRecordReturns = new Returns(sampleBarSeries, tradingRecord, ReturnRepresentation.DECIMAL);
|
||||
|
||||
assertNumEquals(NaN.NaN, positionReturns.getValue(0));
|
||||
assertNumEquals(1.0, positionReturns.getValue(1));
|
||||
assertNumEquals(tradingRecordReturns.getValue(1), positionReturns.getValue(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionOpenedOnFinalBarYieldsZeroReturn() {
|
||||
var barSeries = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(1d, 1d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
|
||||
var endIndex = barSeries.getEndIndex();
|
||||
tradingRecord.enter(endIndex, barSeries.getBar(endIndex).getClosePrice(), barSeries.numFactory().one());
|
||||
|
||||
var returns = new Returns(barSeries, tradingRecord, endIndex, ReturnRepresentation.DECIMAL,
|
||||
EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var lastReturn = returns.getValue(endIndex);
|
||||
assertFalse(lastReturn.isNaN());
|
||||
assertNumEquals(0, lastReturn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returns_markToMarket_doesNotUseFutureExitPriceWhenExitAfterFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), series.numFactory().one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), series.numFactory().one());
|
||||
|
||||
var returns = new Returns(series, tradingRecord, 2, ReturnRepresentation.DECIMAL,
|
||||
EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var one = series.numFactory().one();
|
||||
var expectedAt2 = series.getBar(2).getClosePrice().dividedBy(series.getBar(1).getClosePrice()).minus(one);
|
||||
|
||||
assertNumEquals(returns.getRawValues().get(2), expectedAt2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returns_ignore_skipsPositionsThatAreOpenAtFinalIndex() {
|
||||
var series = new MockBarSeriesBuilder().withData(10d, 11d, 12d, 13d, 100d).build();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(0, series.getBar(0).getClosePrice(), series.numFactory().one());
|
||||
tradingRecord.exit(4, series.getBar(4).getClosePrice(), series.numFactory().one());
|
||||
|
||||
var returns = new Returns(series, tradingRecord, 2, ReturnRepresentation.DECIMAL,
|
||||
EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.IGNORE);
|
||||
|
||||
var zero = series.numFactory().zero();
|
||||
assertNumEquals(returns.getRawValues().get(1), zero);
|
||||
assertNumEquals(returns.getRawValues().get(2), zero);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsIncludeMultipleOpenLotsFromBaseTradingRecord() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 12d, 14d).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(0, new BaseTrade(0, Instant.EPOCH, series.getBar(0).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
record.recordFill(1, new BaseTrade(1, Instant.EPOCH, series.getBar(1).getClosePrice(), numFactory.one(), null,
|
||||
ExecutionSide.BUY, null, null));
|
||||
|
||||
var returns = new Returns(series, record, ReturnRepresentation.DECIMAL, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var expectedAt1 = (12d / 10d) - 1d;
|
||||
var stepFactor = 14d / 12d;
|
||||
var expectedAt2 = (stepFactor * stepFactor) - 1d;
|
||||
|
||||
assertNumEquals(expectedAt1, returns.getValue(1));
|
||||
assertNumEquals(expectedAt2, returns.getValue(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class WeightedValueTest {
|
||||
|
||||
private static final NumFactory NUM_FACTORY = DoubleNumFactory.getInstance();
|
||||
|
||||
@Test
|
||||
public void constructorRejectsInvalidWeight() {
|
||||
assertThrows(NullPointerException.class, () -> new WeightedValue<>(null, NUM_FACTORY.one()));
|
||||
assertThrows(NullPointerException.class, () -> new WeightedValue<>("a", null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new WeightedValue<>("a", NaN.NaN));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new WeightedValue<>("a", NUM_FACTORY.numOf(Double.POSITIVE_INFINITY)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorAllowsNegativeWeightForPenaltyUseCases() {
|
||||
WeightedValue<String> weightedValue = new WeightedValue<>("penaltyMetric", NUM_FACTORY.minusOne());
|
||||
assertThat(weightedValue.weight()).isEqualByComparingTo(NUM_FACTORY.minusOne());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizeWeightsReturnsUnitSumInOriginalOrder() {
|
||||
List<WeightedValue<String>> normalized = WeightedValue
|
||||
.normalizeWeights(List.of(new WeightedValue<>("alpha", NUM_FACTORY.numOf(3.0)),
|
||||
new WeightedValue<>("beta", NUM_FACTORY.numOf(1.0))), NUM_FACTORY);
|
||||
|
||||
assertThat(normalized).hasSize(2);
|
||||
assertThat(normalized.get(0).value()).isEqualTo("alpha");
|
||||
assertThat(normalized.get(1).value()).isEqualTo("beta");
|
||||
assertThat(normalized.get(0).weight()).isEqualByComparingTo(NUM_FACTORY.numOf(0.75));
|
||||
assertThat(normalized.get(1).weight()).isEqualByComparingTo(NUM_FACTORY.numOf(0.25));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizeWeightsRejectsEmptyAndZeroTotals() {
|
||||
assertThrows(IllegalArgumentException.class, () -> WeightedValue.normalizeWeights(List.of(), NUM_FACTORY));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> WeightedValue.normalizeWeights(List.of(new WeightedValue<>("alpha", NUM_FACTORY.zero()),
|
||||
new WeightedValue<>("beta", NUM_FACTORY.zero())), NUM_FACTORY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedSumSkipsMissingValues() {
|
||||
List<WeightedValue<String>> weights = List.of(new WeightedValue<>("alpha", NUM_FACTORY.numOf(2.0)),
|
||||
new WeightedValue<>("beta", NUM_FACTORY.numOf(1.0)));
|
||||
|
||||
Num weightedSum = WeightedValue.weightedSum(weights, key -> {
|
||||
if ("alpha".equals(key)) {
|
||||
return NUM_FACTORY.numOf(3.0);
|
||||
}
|
||||
return NaN.NaN;
|
||||
}, NUM_FACTORY);
|
||||
|
||||
assertThat(weightedSum).isEqualByComparingTo(NUM_FACTORY.numOf(6.0));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class FixedTransactionCostModelTest {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final Num PRICE = DoubleNum.valueOf(100);
|
||||
|
||||
private static final Num AMOUNT = DoubleNum.valueOf(5);
|
||||
|
||||
@Test
|
||||
public void calculatePerPositionWhenPositionIsOpen() {
|
||||
double positionTrades = 1;
|
||||
double feePerTrade = RANDOM.nextDouble();
|
||||
FixedTransactionCostModel model = new FixedTransactionCostModel(feePerTrade);
|
||||
|
||||
Position position = new Position(TradeType.BUY, model, null);
|
||||
position.operate(0, PRICE, AMOUNT);
|
||||
Num cost = model.calculate(position);
|
||||
|
||||
assertNumEquals(cost, DoubleNum.valueOf(feePerTrade * positionTrades));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatePerPositionWhenPositionIsClosed() {
|
||||
double positionTrades = 2;
|
||||
double feePerTrade = RANDOM.nextDouble();
|
||||
FixedTransactionCostModel model = new FixedTransactionCostModel(feePerTrade);
|
||||
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.buyAt(0, PRICE, AMOUNT, model);
|
||||
Trade exit = Trade.sellAt(holdingPeriod, PRICE, AMOUNT, model);
|
||||
|
||||
Position position = new Position(entry, exit, model, model);
|
||||
Num cost = model.calculate(position, RANDOM.nextInt());
|
||||
|
||||
assertNumEquals(cost, DoubleNum.valueOf(feePerTrade * positionTrades));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatePerPrice() {
|
||||
double feePerTrade = RANDOM.nextDouble();
|
||||
FixedTransactionCostModel model = new FixedTransactionCostModel(feePerTrade);
|
||||
Num cost = model.calculate(PRICE, AMOUNT);
|
||||
|
||||
assertNumEquals(cost, DoubleNum.valueOf(feePerTrade));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquality() {
|
||||
double randomFee = RANDOM.nextDouble();
|
||||
FixedTransactionCostModel model = new FixedTransactionCostModel(randomFee);
|
||||
CostModel modelSame = new FixedTransactionCostModel(randomFee);
|
||||
CostModel modelOther = new LinearTransactionCostModel(randomFee);
|
||||
boolean equality = model.equals(modelSame);
|
||||
boolean inequality = model.equals(modelOther);
|
||||
|
||||
assertTrue(equality);
|
||||
assertFalse(inequality);
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class LinearBorrowingCostModelTest {
|
||||
|
||||
private CostModel borrowingModel;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
borrowingModel = new LinearBorrowingCostModel(0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateZeroTest() {
|
||||
// Price - Amount calculation Test
|
||||
Num price = DoubleNum.valueOf(100);
|
||||
Num amount = DoubleNum.valueOf(2);
|
||||
Num cost = borrowingModel.calculate(price, amount);
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(0), cost);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateBuyPosition() {
|
||||
// Holding a bought asset should not incur borrowing costs
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.buyAt(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
Trade exit = Trade.sellAt(holdingPeriod, DoubleNum.valueOf(110), DoubleNum.valueOf(1));
|
||||
|
||||
Position position = new Position(entry, exit, new ZeroCostModel(), borrowingModel);
|
||||
|
||||
Num costsFromPosition = position.getHoldingCost();
|
||||
Num costsFromModel = borrowingModel.calculate(position, holdingPeriod);
|
||||
|
||||
assertNumEquals(costsFromModel, costsFromPosition);
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateSellPosition() {
|
||||
// Short selling incurs borrowing costs
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.sellAt(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
Trade exit = Trade.buyAt(holdingPeriod, DoubleNum.valueOf(110), DoubleNum.valueOf(1));
|
||||
|
||||
Position position = new Position(entry, exit, new ZeroCostModel(), borrowingModel);
|
||||
|
||||
Num costsFromPosition = position.getHoldingCost();
|
||||
Num costsFromModel = borrowingModel.calculate(position, holdingPeriod);
|
||||
|
||||
assertNumEquals(costsFromModel, costsFromPosition);
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOpenSellPosition() {
|
||||
// Short selling incurs borrowing costs. Since position is still open, accounted
|
||||
// for until current index
|
||||
int currentIndex = 4;
|
||||
Position position = new Position(Trade.TradeType.SELL, new ZeroCostModel(), borrowingModel);
|
||||
position.operate(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
Num costsFromPosition = position.getHoldingCost(currentIndex);
|
||||
Num costsFromModel = borrowingModel.calculate(position, currentIndex);
|
||||
|
||||
assertNumEquals(costsFromModel, costsFromPosition);
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquality() {
|
||||
LinearBorrowingCostModel model = new LinearBorrowingCostModel(0.1);
|
||||
CostModel modelSameClass = new LinearBorrowingCostModel(0.2);
|
||||
CostModel modelSameFee = new LinearBorrowingCostModel(0.1);
|
||||
CostModel modelOther = new ZeroCostModel();
|
||||
|
||||
boolean equality = model.equals(modelSameFee);
|
||||
boolean inequality1 = model.equals(modelSameClass);
|
||||
boolean inequality2 = model.equals(modelOther);
|
||||
|
||||
assertTrue(equality);
|
||||
assertFalse(inequality1);
|
||||
assertFalse(inequality2);
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.*;
|
||||
import org.ta4j.core.backtest.BacktestExecutor;
|
||||
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
|
||||
import org.ta4j.core.mocks.MockBarBuilderFactory;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
public class LinearTransactionCostModelTest {
|
||||
|
||||
private CostModel transactionModel;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
transactionModel = new LinearTransactionCostModel(0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateSingleTradeCost() {
|
||||
// Price - Amount calculation Test
|
||||
Num price = DoubleNum.valueOf(100);
|
||||
Num amount = DoubleNum.valueOf(2);
|
||||
Num cost = transactionModel.calculate(price, amount);
|
||||
|
||||
assertNumEquals(DoubleNum.valueOf(2), cost);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateBuyPosition() {
|
||||
// Calculate the transaction costs of a closed long position
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.buyAt(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1), transactionModel);
|
||||
Trade exit = Trade.sellAt(holdingPeriod, DoubleNum.valueOf(110), DoubleNum.valueOf(1), transactionModel);
|
||||
|
||||
Position position = new Position(entry, exit, transactionModel, new ZeroCostModel());
|
||||
|
||||
Num costFromBuy = entry.getCost();
|
||||
Num costFromSell = exit.getCost();
|
||||
Num costsFromModel = transactionModel.calculate(position, holdingPeriod);
|
||||
|
||||
assertNumEquals(costsFromModel, costFromBuy.plus(costFromSell));
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(2.1));
|
||||
assertNumEquals(costFromBuy, DoubleNum.valueOf(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateSellPosition() {
|
||||
// Calculate the transaction costs of a closed short position
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.sellAt(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1), transactionModel);
|
||||
Trade exit = Trade.buyAt(holdingPeriod, DoubleNum.valueOf(110), DoubleNum.valueOf(1), transactionModel);
|
||||
|
||||
Position position = new Position(entry, exit, transactionModel, new ZeroCostModel());
|
||||
|
||||
Num costFromBuy = entry.getCost();
|
||||
Num costFromSell = exit.getCost();
|
||||
Num costsFromModel = transactionModel.calculate(position, holdingPeriod);
|
||||
|
||||
assertNumEquals(costsFromModel, costFromBuy.plus(costFromSell));
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(2.1));
|
||||
assertNumEquals(costFromBuy, DoubleNum.valueOf(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOpenSellPosition() {
|
||||
// Calculate the transaction costs of an open position
|
||||
int currentIndex = 4;
|
||||
Position position = new Position(Trade.TradeType.BUY, transactionModel, new ZeroCostModel());
|
||||
position.operate(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
Num costsFromModel = transactionModel.calculate(position, currentIndex);
|
||||
|
||||
assertNumEquals(costsFromModel, DoubleNum.valueOf(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquality() {
|
||||
LinearTransactionCostModel model = new LinearTransactionCostModel(0.1);
|
||||
CostModel modelSameClass = new LinearTransactionCostModel(0.2);
|
||||
CostModel modelSameFee = new LinearTransactionCostModel(0.1);
|
||||
CostModel modelOther = new ZeroCostModel();
|
||||
|
||||
boolean equality = model.equals(modelSameFee);
|
||||
boolean inequality1 = model.equals(modelSameClass);
|
||||
boolean inequality2 = model.equals(modelOther);
|
||||
|
||||
assertTrue(equality);
|
||||
assertFalse(inequality1);
|
||||
assertFalse(inequality2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBacktesting() {
|
||||
BaseBarSeries series = new BaseBarSeriesBuilder().withName("CostModel test")
|
||||
.withBarBuilderFactory(new MockBarBuilderFactory())
|
||||
.build();
|
||||
Instant now = Instant.now();
|
||||
Num one = series.numFactory().one();
|
||||
Num two = series.numFactory().numOf(2);
|
||||
Num three = series.numFactory().numOf(3);
|
||||
Num four = series.numFactory().numOf(4);
|
||||
series.barBuilder().endTime(now).openPrice(one).closePrice(one).highPrice(one).lowPrice(one).add();
|
||||
series.barBuilder()
|
||||
.endTime(now.plusSeconds(1))
|
||||
.openPrice(two)
|
||||
.closePrice(two)
|
||||
.highPrice(two)
|
||||
.lowPrice(two)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.endTime(now.plusSeconds(2))
|
||||
.openPrice(three)
|
||||
.closePrice(three)
|
||||
.highPrice(three)
|
||||
.lowPrice(three)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.endTime(now.plusSeconds(3))
|
||||
.openPrice(four)
|
||||
.closePrice(four)
|
||||
.highPrice(four)
|
||||
.lowPrice(four)
|
||||
.add();
|
||||
|
||||
Rule entryRule = new FixedRule(0, 2);
|
||||
Rule exitRule = new FixedRule(1, 3);
|
||||
List<Strategy> strategies = new LinkedList<>();
|
||||
strategies.add(new BaseStrategy("Cost model test strategy", entryRule, exitRule));
|
||||
|
||||
Num orderFee = series.numFactory().numOf(new BigDecimal("0.0026"));
|
||||
BacktestExecutor executor = new BacktestExecutor(series, new LinearTransactionCostModel(orderFee.doubleValue()),
|
||||
new ZeroCostModel(), new TradeOnCurrentCloseModel());
|
||||
|
||||
Num amount = series.numFactory().numOf(25);
|
||||
TradingStatement strategyResult = executor.execute(strategies, amount).get(0);
|
||||
|
||||
Num firstPositionBuy = one.plus(one.multipliedBy(orderFee));
|
||||
Num firstPositionSell = two.minus(two.multipliedBy(orderFee));
|
||||
Num firstPositionProfit = firstPositionSell.minus(firstPositionBuy).multipliedBy(amount);
|
||||
|
||||
Num secondPositionBuy = three.plus(three.multipliedBy(orderFee));
|
||||
Num secondPositionSell = four.minus(four.multipliedBy(orderFee));
|
||||
Num secondPositionProfit = secondPositionSell.minus(secondPositionBuy).multipliedBy(amount);
|
||||
|
||||
Num overallProfit = firstPositionProfit.plus(secondPositionProfit);
|
||||
|
||||
assertEquals(overallProfit, strategyResult.getPerformanceReport().getPerformanceMetric());
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.DoubleNum;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class ZeroCostModelTest {
|
||||
|
||||
@Test
|
||||
public void calculatePerPosition() {
|
||||
// calculate costs per position
|
||||
ZeroCostModel model = new ZeroCostModel();
|
||||
|
||||
int holdingPeriod = 2;
|
||||
Trade entry = Trade.buyAt(0, DoubleNum.valueOf(100), DoubleNum.valueOf(1), model);
|
||||
Trade exit = Trade.sellAt(holdingPeriod, DoubleNum.valueOf(110), DoubleNum.valueOf(1), model);
|
||||
|
||||
Position position = new Position(entry, exit, model, model);
|
||||
Num cost = model.calculate(position, holdingPeriod);
|
||||
|
||||
assertNumEquals(cost, DoubleNum.valueOf(0));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatePerPrice() {
|
||||
// calculate costs per position
|
||||
ZeroCostModel model = new ZeroCostModel();
|
||||
Num cost = model.calculate(DoubleNum.valueOf(100), DoubleNum.valueOf(1));
|
||||
|
||||
assertNumEquals(cost, DoubleNum.valueOf(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquality() {
|
||||
ZeroCostModel model = new ZeroCostModel();
|
||||
CostModel modelSame = new ZeroCostModel();
|
||||
CostModel modelOther = new LinearTransactionCostModel(0.1);
|
||||
boolean equality = model.equals(modelSame);
|
||||
boolean inequality = model.equals(modelOther);
|
||||
|
||||
assertTrue(equality);
|
||||
assertFalse(inequality);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.sampling;
|
||||
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import org.ta4j.core.analysis.frequency.Sample;
|
||||
import org.ta4j.core.analysis.frequency.SampleSummary;
|
||||
import org.ta4j.core.criteria.AbstractCriterionTest;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SampleSummaryTest extends AbstractCriterionTest {
|
||||
|
||||
public SampleSummaryTest(NumFactory numFactory) {
|
||||
super(params -> null, numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computesMomentsForDeterministicSeries() {
|
||||
var returns = new double[] { 0.05d, -0.02d, 0.04d, 0.01d, 0.03d };
|
||||
var deltas = new double[] { 0.5d, 0.25d, 0.25d, 0.5d, 1.0d };
|
||||
|
||||
var summary = SampleSummary.fromSamples(
|
||||
IntStream.range(0, returns.length).mapToObj(i -> new Sample(numOf(returns[i]), numOf(deltas[i]))),
|
||||
numFactory);
|
||||
|
||||
var expectedMean = DoubleStream.of(returns).average().orElseThrow();
|
||||
var expectedM2 = 0d;
|
||||
var expectedM3 = 0d;
|
||||
var expectedM4 = 0d;
|
||||
for (double value : returns) {
|
||||
var diff = value - expectedMean;
|
||||
var diff2 = diff * diff;
|
||||
expectedM2 += diff2;
|
||||
expectedM3 += diff2 * diff;
|
||||
expectedM4 += diff2 * diff2;
|
||||
}
|
||||
var expectedVariance = expectedM2 / (returns.length - 1);
|
||||
var expectedSkewness = Math.sqrt(returns.length * (returns.length - 1d)) / (returns.length - 2d)
|
||||
* (expectedM3 / Math.pow(expectedM2, 1.5d));
|
||||
var expectedKurtosis = (returns.length - 1d) / ((returns.length - 2d) * (returns.length - 3d))
|
||||
* (((returns.length + 1d) * expectedM4 / (expectedM2 * expectedM2)) - (3d * (returns.length - 1d)));
|
||||
var expectedAnnualization = Math.sqrt(deltas.length / DoubleStream.of(deltas).sum());
|
||||
|
||||
assertEquals(returns.length, summary.count());
|
||||
assertEquals(expectedMean, summary.mean().doubleValue(), 1e-12);
|
||||
assertEquals(expectedM2, summary.m2().doubleValue(), 1e-12);
|
||||
assertEquals(expectedM3, summary.m3().doubleValue(), 1e-12);
|
||||
assertEquals(expectedM4, summary.m4().doubleValue(), 1e-12);
|
||||
assertEquals(expectedVariance, summary.sampleVariance(numFactory).doubleValue(), 1e-12);
|
||||
assertEquals(expectedSkewness, summary.sampleSkewness(numFactory).doubleValue(), 1e-12);
|
||||
assertEquals(expectedKurtosis, summary.sampleKurtosis(numFactory).doubleValue(), 1e-12);
|
||||
assertEquals(expectedAnnualization, summary.annualizationFactor(numFactory).orElseThrow().doubleValue(), 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromValuesTreatsAnnualizationAsEmpty() {
|
||||
var values = new double[] { 0.1d, 0.2d, 0.3d };
|
||||
|
||||
var summary = SampleSummary.fromValues(DoubleStream.of(values).mapToObj(this::numOf), numFactory);
|
||||
|
||||
var expectedMean = DoubleStream.of(values).average().orElseThrow();
|
||||
|
||||
assertEquals(values.length, summary.count());
|
||||
assertEquals(expectedMean, summary.mean().doubleValue(), 1e-12);
|
||||
assertTrue(summary.annualizationFactor(numFactory).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skewnessAndKurtosisRemainZeroWithInsufficientCount() {
|
||||
var summary = SampleSummary.fromSamples(IntStream.range(0, 2).mapToObj(i -> new Sample(numOf(i), numOf(0.5d))),
|
||||
numFactory);
|
||||
|
||||
assertEquals(2, summary.count());
|
||||
assertEquals(0d, summary.sampleSkewness(numFactory).doubleValue(), 0d);
|
||||
assertEquals(0d, summary.sampleKurtosis(numFactory).doubleValue(), 0d);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.sampling;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import org.ta4j.core.analysis.frequency.IndexPair;
|
||||
import org.ta4j.core.analysis.frequency.SamplingFrequencyIndexes;
|
||||
import org.ta4j.core.analysis.frequency.SamplingFrequency;
|
||||
|
||||
public class SamplingFrequencyIndexesTest {
|
||||
|
||||
@Test
|
||||
public void samplePerBarUsesConsecutivePairs() {
|
||||
var series = buildDailySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.BAR, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 1, 3).toList();
|
||||
|
||||
var expected = List.of(new IndexPair(0, 1), new IndexPair(1, 2), new IndexPair(2, 3));
|
||||
|
||||
assertEquals(expected, pairs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleDailyUsesPeriodEnds() {
|
||||
var series = buildDailySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.DAY, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 1, 3).toList();
|
||||
|
||||
var expected = Stream.of(new IndexPair(0, 1), new IndexPair(1, 2), new IndexPair(2, 3)).toList();
|
||||
|
||||
assertEquals(expected, pairs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleDailyAnchorsAtExplicitIndex() {
|
||||
var series = buildIntradaySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.DAY, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 1, 3).toList();
|
||||
|
||||
var expected = List.of(new IndexPair(0, 1), new IndexPair(1, 3));
|
||||
|
||||
assertEquals(expected, pairs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleMonthlyUsesMonthBoundary() {
|
||||
var series = buildMonthlyBoundarySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.MONTH, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 1, 2).toList();
|
||||
|
||||
var expected = List.of(new IndexPair(0, 1), new IndexPair(1, 2));
|
||||
|
||||
assertEquals(expected, pairs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleReturnsEmptyWhenRangeHasSingleIndex() {
|
||||
var series = buildDailySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.BAR, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 2, 2).toList();
|
||||
|
||||
assertEquals(List.of(), pairs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleReturnsEmptyWhenRangeIsReversed() {
|
||||
var series = buildDailySeries();
|
||||
var sampler = new SamplingFrequencyIndexes(SamplingFrequency.DAY, ZoneOffset.UTC);
|
||||
|
||||
var pairs = sampler.sample(series, 0, 3, 1).toList();
|
||||
|
||||
assertEquals(List.of(), pairs);
|
||||
}
|
||||
|
||||
private static BarSeries buildDailySeries() {
|
||||
var series = new BaseBarSeriesBuilder().withName("sampler_series").build();
|
||||
var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
var closes = new double[] { 100d, 110d, 120d, 130d };
|
||||
|
||||
for (var i = 0; i < closes.length; i++) {
|
||||
var endTime = start.plus(Duration.ofDays(i + 1L));
|
||||
var close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
private static BarSeries buildIntradaySeries() {
|
||||
var series = new BaseBarSeriesBuilder().withName("intraday_sampler_series").build();
|
||||
var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
var closes = new double[] { 100d, 105d, 110d, 120d };
|
||||
var offsets = new long[] { 6, 20, 30, 44 };
|
||||
|
||||
for (var i = 0; i < closes.length; i++) {
|
||||
var endTime = start.plus(Duration.ofHours(offsets[i]));
|
||||
var close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
private static BarSeries buildMonthlyBoundarySeries() {
|
||||
var series = new BaseBarSeriesBuilder().withName("monthly_sampler_series").build();
|
||||
var endTimes = new Instant[] { Instant.parse("2024-01-30T00:00:00Z"), Instant.parse("2024-01-31T00:00:00Z"),
|
||||
Instant.parse("2024-02-01T00:00:00Z") };
|
||||
var closes = new double[] { 100d, 103d, 104d };
|
||||
|
||||
for (var i = 0; i < closes.length; i++) {
|
||||
var close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTimes[i])
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
|
||||
import org.ta4j.core.criteria.ExpectancyCriterion;
|
||||
import org.ta4j.core.criteria.NumberOfPositionsCriterion;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BacktestExecutionResultTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public BacktestExecutionResultTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringReturnsJsonWithCountAndRuntimeReport() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
Strategy strategyOne = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategyTwo = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
|
||||
List<Strategy> strategies = List.of(strategyOne, strategyTwo);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
String jsonString = result.toString();
|
||||
|
||||
assertNotNull("toString() should not return null", jsonString);
|
||||
assertFalse("toString() should return non-empty JSON", jsonString.isEmpty());
|
||||
|
||||
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
assertTrue("JSON should contain barSeriesName", json.has("barSeriesName"));
|
||||
assertEquals("barSeriesName should match series name", series.getName(),
|
||||
json.get("barSeriesName").getAsString());
|
||||
|
||||
assertTrue("JSON should contain tradingStatementsCount", json.has("tradingStatementsCount"));
|
||||
assertEquals("tradingStatementsCount should match actual count", strategies.size(),
|
||||
json.get("tradingStatementsCount").getAsInt());
|
||||
|
||||
assertTrue("JSON should contain runtimeReport", json.has("runtimeReport"));
|
||||
assertNotNull("runtimeReport should not be null", json.get("runtimeReport"));
|
||||
assertTrue("runtimeReport should be a JSON object", json.get("runtimeReport").isJsonObject());
|
||||
|
||||
JsonObject runtimeReportJson = json.get("runtimeReport").getAsJsonObject();
|
||||
assertTrue("runtimeReport should contain overallRuntime", runtimeReportJson.has("overallRuntime"));
|
||||
assertTrue("runtimeReport should contain minStrategyRuntime", runtimeReportJson.has("minStrategyRuntime"));
|
||||
assertTrue("runtimeReport should contain maxStrategyRuntime", runtimeReportJson.has("maxStrategyRuntime"));
|
||||
assertTrue("runtimeReport should contain averageStrategyRuntime",
|
||||
runtimeReportJson.has("averageStrategyRuntime"));
|
||||
assertTrue("runtimeReport should contain medianStrategyRuntime",
|
||||
runtimeReportJson.has("medianStrategyRuntime"));
|
||||
assertFalse("runtimeReport should NOT contain strategyRuntimes", runtimeReportJson.has("strategyRuntimes"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringHandlesEmptyTradingStatements() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(5, 6, 7).build();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(), numOf(1));
|
||||
|
||||
String jsonString = result.toString();
|
||||
|
||||
assertNotNull("toString() should not return null", jsonString);
|
||||
assertFalse("toString() should return non-empty JSON", jsonString.isEmpty());
|
||||
|
||||
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
assertEquals("tradingStatementsCount should be 0 for empty list", 0,
|
||||
json.get("tradingStatementsCount").getAsInt());
|
||||
assertTrue("JSON should contain runtimeReport", json.has("runtimeReport"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesWithSingleCriterionSortsCorrectly() {
|
||||
// Create a bar series with price movement
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 115, 120, 125, 130, 135, 140, 145)
|
||||
.build();
|
||||
|
||||
// Strategy 1: Buy at 0, sell at 5 (profit from 100 to 125)
|
||||
Strategy strategy1 = new BaseStrategy("Strategy1", new FixedRule(0), new FixedRule(5));
|
||||
|
||||
// Strategy 2: Buy at 2, sell at 7 (profit from 110 to 135)
|
||||
Strategy strategy2 = new BaseStrategy("Strategy2", new FixedRule(2), new FixedRule(7));
|
||||
|
||||
// Strategy 3: Buy at 4, sell at 9 (profit from 120 to 145)
|
||||
Strategy strategy3 = new BaseStrategy("Strategy3", new FixedRule(4), new FixedRule(9));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
// Get top 2 strategies by net profit
|
||||
AnalysisCriterion netProfitCriterion = new NetProfitCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(2, netProfitCriterion);
|
||||
|
||||
assertEquals("Should return 2 strategies", 2, topStrategies.size());
|
||||
|
||||
// Verify the strategies are sorted by profit (strategy3 should be best, then
|
||||
// strategy2)
|
||||
Num profit1 = netProfitCriterion.calculate(result.barSeries(), topStrategies.get(0).getTradingRecord());
|
||||
Num profit2 = netProfitCriterion.calculate(result.barSeries(), topStrategies.get(1).getTradingRecord());
|
||||
assertTrue("First strategy should have better or equal profit than second",
|
||||
netProfitCriterion.betterThan(profit1, profit2) || profit1.equals(profit2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesWithMultipleCriteriaSortsByPriorityAndTieBreaks() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 115, 120, 125, 130, 135, 140, 145)
|
||||
.build();
|
||||
|
||||
// Create strategies with different trading patterns
|
||||
Strategy strategy1 = new BaseStrategy("Strategy1", new FixedRule(0, 5), new FixedRule(2, 7));
|
||||
Strategy strategy2 = new BaseStrategy("Strategy2", new FixedRule(1, 6), new FixedRule(3, 8));
|
||||
Strategy strategy3 = new BaseStrategy("Strategy3", new FixedRule(2), new FixedRule(7));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
// Sort by number of positions first, then by expectancy for ties
|
||||
AnalysisCriterion positionsCriterion = new NumberOfPositionsCriterion();
|
||||
AnalysisCriterion expectancyCriterion = new ExpectancyCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(3, positionsCriterion, expectancyCriterion);
|
||||
|
||||
assertEquals("Should return all 3 strategies", 3, topStrategies.size());
|
||||
|
||||
// Verify ordering by primary criterion
|
||||
for (int i = 0; i < topStrategies.size() - 1; i++) {
|
||||
Num positions1 = positionsCriterion.calculate(result.barSeries(), topStrategies.get(i).getTradingRecord());
|
||||
Num positions2 = positionsCriterion.calculate(result.barSeries(),
|
||||
topStrategies.get(i + 1).getTradingRecord());
|
||||
|
||||
// First criterion should be better or equal
|
||||
assertTrue("Strategies should be sorted by primary criterion",
|
||||
positionsCriterion.betterThan(positions1, positions2) || positions1.equals(positions2));
|
||||
|
||||
// If equal on first criterion, second criterion should be better or equal
|
||||
if (positions1.equals(positions2)) {
|
||||
Num expectancy1 = expectancyCriterion.calculate(result.barSeries(),
|
||||
topStrategies.get(i).getTradingRecord());
|
||||
Num expectancy2 = expectancyCriterion.calculate(result.barSeries(),
|
||||
topStrategies.get(i + 1).getTradingRecord());
|
||||
assertTrue("Strategies with equal primary criterion should be sorted by secondary criterion",
|
||||
expectancyCriterion.betterThan(expectancy1, expectancy2) || expectancy1.equals(expectancy2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesRespectsLimit() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 120, 130, 140).build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
strategies.add(new BaseStrategy("Strategy" + i, new FixedRule(0), new FixedRule(2)));
|
||||
}
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NetProfitCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(5, criterion);
|
||||
|
||||
assertEquals("Should return only 5 strategies even though 10 were provided", 5, topStrategies.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesWithLimitLargerThanResultsReturnsAll() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 120).build();
|
||||
|
||||
Strategy strategy1 = new BaseStrategy("Strategy1", new FixedRule(0), new FixedRule(1));
|
||||
Strategy strategy2 = new BaseStrategy("Strategy2", new FixedRule(0), new FixedRule(2));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NetProfitCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(100, criterion);
|
||||
|
||||
assertEquals("Should return all available strategies when limit exceeds count", 2, topStrategies.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesWithZeroLimitReturnsEmpty() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110, 120).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy("Strategy", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NetProfitCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(0, criterion);
|
||||
|
||||
assertTrue("Should return empty list when limit is 0", topStrategies.isEmpty());
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void getTopStrategiesThrowsExceptionWhenCriteriaVarargsIsNull() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy("Strategy", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numOf(1));
|
||||
|
||||
AnalysisCriterion[] nullCriteria = null;
|
||||
result.getTopStrategies(1, nullCriteria);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void getTopStrategiesThrowsExceptionWhenCriteriaListIsNull() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy("Strategy", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numOf(1));
|
||||
|
||||
List<AnalysisCriterion> nullCriteria = null;
|
||||
result.getTopStrategies(1, nullCriteria);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void getTopStrategiesThrowsExceptionWhenCriteriaIsEmpty() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy("Strategy", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numOf(1));
|
||||
|
||||
result.getTopStrategies(1, new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void getTopStrategiesThrowsExceptionWhenLimitIsNegative() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
Strategy strategy = new BaseStrategy("Strategy", new FixedRule(0), new FixedRule(1));
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numOf(1));
|
||||
|
||||
result.getTopStrategies(-1, new NetProfitCriterion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesVarargsAndListMethodsProduceSameResults() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 115, 120, 125, 130, 135)
|
||||
.build();
|
||||
|
||||
Strategy strategy1 = new BaseStrategy("Strategy1", new FixedRule(0), new FixedRule(4));
|
||||
Strategy strategy2 = new BaseStrategy("Strategy2", new FixedRule(1), new FixedRule(5));
|
||||
Strategy strategy3 = new BaseStrategy("Strategy3", new FixedRule(2), new FixedRule(6));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
AnalysisCriterion netProfitCriterion = new NetProfitCriterion();
|
||||
AnalysisCriterion expectancyCriterion = new ExpectancyCriterion();
|
||||
|
||||
// Call with varargs
|
||||
List<TradingStatement> varargsResult = result.getTopStrategies(2, netProfitCriterion, expectancyCriterion);
|
||||
|
||||
// Call with List
|
||||
List<TradingStatement> listResult = result.getTopStrategies(2,
|
||||
Arrays.asList(netProfitCriterion, expectancyCriterion));
|
||||
|
||||
assertEquals("Varargs and List methods should return same number of results", varargsResult.size(),
|
||||
listResult.size());
|
||||
|
||||
// Verify same strategies in same order
|
||||
for (int i = 0; i < varargsResult.size(); i++) {
|
||||
assertEquals("Varargs and List methods should return same strategies in same order",
|
||||
varargsResult.get(i).getStrategy().getName(), listResult.get(i).getStrategy().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesHandlesEmptyTradingStatements() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(), numOf(1));
|
||||
|
||||
AnalysisCriterion criterion = new NetProfitCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(10, criterion);
|
||||
|
||||
assertTrue("Should return empty list when no trading statements exist", topStrategies.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTopStrategiesStoresCriterionScores() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 115, 120, 125, 130, 135, 140, 145)
|
||||
.build();
|
||||
|
||||
Strategy strategy1 = new BaseStrategy("Strategy1", new FixedRule(0), new FixedRule(5));
|
||||
Strategy strategy2 = new BaseStrategy("Strategy2", new FixedRule(2), new FixedRule(7));
|
||||
Strategy strategy3 = new BaseStrategy("Strategy3", new FixedRule(4), new FixedRule(9));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
AnalysisCriterion netProfitCriterion = new NetProfitCriterion();
|
||||
AnalysisCriterion expectancyCriterion = new ExpectancyCriterion();
|
||||
List<TradingStatement> topStrategies = result.getTopStrategies(3, netProfitCriterion, expectancyCriterion);
|
||||
|
||||
assertEquals("Should return all 3 strategies", 3, topStrategies.size());
|
||||
|
||||
// Verify that criterion scores are stored and accessible
|
||||
for (TradingStatement statement : topStrategies) {
|
||||
// Check that scores are available via getCriterionScore
|
||||
assertTrue("Net profit score should be available",
|
||||
statement.getCriterionScore(netProfitCriterion).isPresent());
|
||||
assertTrue("Expectancy score should be available",
|
||||
statement.getCriterionScore(expectancyCriterion).isPresent());
|
||||
|
||||
// Verify the scores match what we would calculate
|
||||
Num storedNetProfit = statement.getCriterionScore(netProfitCriterion).get();
|
||||
Num calculatedNetProfit = netProfitCriterion.calculate(result.barSeries(), statement.getTradingRecord());
|
||||
assertEquals("Stored net profit should match calculated value", storedNetProfit, calculatedNetProfit);
|
||||
|
||||
Num storedExpectancy = statement.getCriterionScore(expectancyCriterion).get();
|
||||
Num calculatedExpectancy = expectancyCriterion.calculate(result.barSeries(), statement.getTradingRecord());
|
||||
assertEquals("Stored expectancy should match calculated value", storedExpectancy, calculatedExpectancy);
|
||||
|
||||
// Check that all scores are available via getCriterionScores
|
||||
var allScores = statement.getCriterionScores();
|
||||
assertEquals("Should have 2 criterion scores stored", 2, allScores.size());
|
||||
assertTrue("Should contain net profit criterion", allScores.containsKey(netProfitCriterion));
|
||||
assertTrue("Should contain expectancy criterion", allScores.containsKey(expectancyCriterion));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.criteria.NumberOfBarsCriterion;
|
||||
import org.ta4j.core.criteria.commissions.CommissionsCriterion;
|
||||
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
|
||||
public class BacktestExecutorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public BacktestExecutorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithRuntimeReportCollectsMetrics() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
Strategy strategyOne = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategyTwo = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
Strategy strategyThree = new BaseStrategy(new FixedRule(0, 4), new FixedRule(1, 2));
|
||||
|
||||
List<Strategy> strategies = List.of(strategyOne, strategyTwo, strategyThree);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1));
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
assertEquals(strategies.size(), result.runtimeReport().strategyCount());
|
||||
assertEquals(strategies.size(), result.runtimeReport().strategyRuntimes().size());
|
||||
|
||||
for (int i = 0; i < strategies.size(); i++) {
|
||||
assertSame(strategies.get(i), result.runtimeReport().strategyRuntimes().get(i).strategy());
|
||||
}
|
||||
|
||||
assertFalse(result.runtimeReport()
|
||||
.strategyRuntimes()
|
||||
.stream()
|
||||
.anyMatch(strategyRuntime -> strategyRuntime.runtime().isNegative()));
|
||||
assertFalse(result.runtimeReport().overallRuntime().isNegative());
|
||||
|
||||
assertTrue(result.runtimeReport()
|
||||
.maxStrategyRuntime()
|
||||
.compareTo(result.runtimeReport().minStrategyRuntime()) >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithRuntimeReportHandlesEmptyStrategies() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(5, 6, 7).build();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(), numOf(1));
|
||||
|
||||
assertTrue(result.tradingStatements().isEmpty());
|
||||
assertEquals(0, result.runtimeReport().strategyCount());
|
||||
assertTrue(result.runtimeReport().strategyRuntimes().isEmpty());
|
||||
assertEquals(result.runtimeReport().overallRuntime(), result.runtimeReport().minStrategyRuntime());
|
||||
assertEquals(result.runtimeReport().overallRuntime(), result.runtimeReport().maxStrategyRuntime());
|
||||
assertEquals(result.runtimeReport().overallRuntime(), result.runtimeReport().averageStrategyRuntime());
|
||||
assertEquals(result.runtimeReport().overallRuntime(), result.runtimeReport().medianStrategyRuntime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithProgressCallback() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
Strategy strategyOne = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategyTwo = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
Strategy strategyThree = new BaseStrategy(new FixedRule(0, 4), new FixedRule(1, 2));
|
||||
|
||||
List<Strategy> strategies = List.of(strategyOne, strategyTwo, strategyThree);
|
||||
AtomicInteger callbackCount = new AtomicInteger(0);
|
||||
AtomicInteger lastCompletedCount = new AtomicInteger(0);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
completed -> {
|
||||
callbackCount.incrementAndGet();
|
||||
if (completed == 2) {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Interrupted while simulating callback delay", e);
|
||||
}
|
||||
}
|
||||
lastCompletedCount.set(completed);
|
||||
});
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
assertEquals(strategies.size(), callbackCount.get());
|
||||
assertEquals(strategies.size(), lastCompletedCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithLargeStrategyCountUsesBatchProcessing() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
// Create more than PARALLEL_THRESHOLD (1000) strategies to trigger batched
|
||||
// processing
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 1500; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
AtomicInteger progressUpdateCount = new AtomicInteger(0);
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
completed -> progressUpdateCount.incrementAndGet());
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
assertEquals(strategies.size(), result.runtimeReport().strategyCount());
|
||||
assertEquals(strategies.size(), progressUpdateCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithCustomBatchSize() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
// Create more than PARALLEL_THRESHOLD strategies
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 1500; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
int customBatchSize = 250;
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
null, customBatchSize);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
assertEquals(strategies.size(), result.runtimeReport().strategyCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithTradeExecutionModelUsesConfiguredExecutionPrices() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12).build();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
BacktestExecutor executor = new BacktestExecutor(series, new TradeOnCurrentCloseModel());
|
||||
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numFactory.one());
|
||||
|
||||
TradingRecord tradingRecord = result.tradingStatements().getFirst().getTradingRecord();
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
assertEquals(series.getBar(0).getClosePrice(), position.getEntry().getPricePerAsset());
|
||||
assertEquals(series.getBar(1).getClosePrice(), position.getExit().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithCostModelsAndTradeExecutionModelUsesBoth() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12).build();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
LinearTransactionCostModel costModel = new LinearTransactionCostModel(0.1);
|
||||
BacktestExecutor executor = new BacktestExecutor(series, costModel, new ZeroCostModel(),
|
||||
new TradeOnCurrentCloseModel());
|
||||
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategy), numFactory.one());
|
||||
|
||||
TradingRecord tradingRecord = result.tradingStatements().getFirst().getTradingRecord();
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
Trade entry = position.getEntry();
|
||||
Trade exit = position.getExit();
|
||||
|
||||
assertSame(costModel, tradingRecord.getTransactionCostModel());
|
||||
assertEquals(series.getBar(0).getClosePrice(), entry.getPricePerAsset());
|
||||
assertEquals(series.getBar(1).getClosePrice(), exit.getPricePerAsset());
|
||||
assertEquals(costModel.calculate(entry.getPricePerAsset(), entry.getAmount()), entry.getCost());
|
||||
assertEquals(costModel.calculate(exit.getPricePerAsset(), exit.getAmount()), exit.getCost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWithSeriesManagerUsesItsTradingRecordFactory() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
Strategy strategyOne = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
Strategy strategyTwo = new BaseStrategy(new FixedRule(2), new FixedRule(3));
|
||||
AtomicInteger createdRecords = new AtomicInteger();
|
||||
BarSeriesManager.TradingRecordFactory tradingRecordFactory = (tradeType, startIndex, endIndex,
|
||||
transactionCostModel, holdingCostModel) -> {
|
||||
createdRecords.incrementAndGet();
|
||||
return new TrackingTradingRecord(tradeType, startIndex, endIndex, transactionCostModel, holdingCostModel);
|
||||
};
|
||||
BarSeriesManager seriesManager = new BarSeriesManager(series, new ZeroCostModel(), new ZeroCostModel(),
|
||||
new TradeOnCurrentCloseModel(), tradingRecordFactory);
|
||||
BacktestExecutor executor = new BacktestExecutor(seriesManager);
|
||||
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(List.of(strategyOne, strategyTwo),
|
||||
numFactory.one());
|
||||
|
||||
assertEquals(2, createdRecords.get());
|
||||
assertEquals(2, result.tradingStatements().size());
|
||||
assertTrue(result.tradingStatements()
|
||||
.stream()
|
||||
.map(statement -> statement.getTradingRecord())
|
||||
.allMatch(TrackingTradingRecord.class::isInstance));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeAndKeepTopKWithLowerIsBetterCriterion() {
|
||||
// Create a series with enough bars for different holding periods
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14, 15, 16).build();
|
||||
|
||||
// Create strategies with different holding periods (number of bars)
|
||||
// Strategy 1: Buy at 0, sell at 1 -> 2 bars
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
// Strategy 2: Buy at 0, sell at 2 -> 3 bars
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(0), new FixedRule(2));
|
||||
// Strategy 3: Buy at 0, sell at 3 -> 4 bars
|
||||
Strategy strategy3 = new BaseStrategy(new FixedRule(0), new FixedRule(3));
|
||||
// Strategy 4: Buy at 0, sell at 4 -> 5 bars
|
||||
Strategy strategy4 = new BaseStrategy(new FixedRule(0), new FixedRule(4));
|
||||
// Strategy 5: Buy at 0, sell at 5 -> 6 bars
|
||||
Strategy strategy5 = new BaseStrategy(new FixedRule(0), new FixedRule(5));
|
||||
// Strategy 6: Buy at 0, sell at 6 -> 7 bars (worst for NumberOfBarsCriterion)
|
||||
Strategy strategy6 = new BaseStrategy(new FixedRule(0), new FixedRule(6));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3, strategy4, strategy5, strategy6);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
NumberOfBarsCriterion criterion = new NumberOfBarsCriterion();
|
||||
int topK = 3;
|
||||
|
||||
BacktestExecutionResult result = executor.executeAndKeepTopK(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
criterion, topK, null);
|
||||
|
||||
// Should return top 3 strategies
|
||||
assertEquals(topK, result.tradingStatements().size());
|
||||
|
||||
// Verify ordering: best (lowest number of bars) should be first
|
||||
var statements = result.tradingStatements();
|
||||
Num bars1 = criterion.calculate(series, statements.get(0).getTradingRecord());
|
||||
Num bars2 = criterion.calculate(series, statements.get(1).getTradingRecord());
|
||||
Num bars3 = criterion.calculate(series, statements.get(2).getTradingRecord());
|
||||
|
||||
// Verify ascending order (best/lowest first) for lower-is-better criterion
|
||||
assertTrue("First strategy should have lowest number of bars", bars1.isLessThanOrEqual(bars2));
|
||||
assertTrue("Second strategy should have fewer bars than third", bars2.isLessThanOrEqual(bars3));
|
||||
|
||||
// Verify we got the actual top performers (lowest bars)
|
||||
assertTrue("Top strategy should have <= 4 bars", bars1.isLessThanOrEqual(numOf(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeAndKeepTopKWithCommissionsCriterion() {
|
||||
// Create a series with enough bars for different numbers of trades
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14, 15, 16).build();
|
||||
|
||||
// Create strategies with different numbers of trades (more trades = more
|
||||
// commissions)
|
||||
// Strategy 1: Single trade (buy at 0, sell at 6) -> 2 trades (entry + exit) =
|
||||
// lowest commissions
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0), new FixedRule(6));
|
||||
// Strategy 2: Two trades (buy at 0, sell at 3, buy at 4, sell at 6) -> 4 trades
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(0, 4), new FixedRule(3, 6));
|
||||
// Strategy 3: Three trades (buy at 0, sell at 2, buy at 3, sell at 4, buy at 5,
|
||||
// sell at 6) -> 6 trades
|
||||
Strategy strategy3 = new BaseStrategy(new FixedRule(0, 3, 5), new FixedRule(2, 4, 6));
|
||||
// Strategy 4: Four trades -> 8 trades = highest commissions
|
||||
Strategy strategy4 = new BaseStrategy(new FixedRule(0, 2, 4, 5), new FixedRule(1, 3, 5, 6));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3, strategy4);
|
||||
|
||||
// Use transaction costs so commissions are non-zero
|
||||
double transactionFee = 0.01; // 1% fee
|
||||
BacktestExecutor executor = new BacktestExecutor(series, new LinearTransactionCostModel(transactionFee),
|
||||
new ZeroCostModel(), new TradeOnNextOpenModel());
|
||||
|
||||
CommissionsCriterion criterion = new CommissionsCriterion();
|
||||
int topK = 2;
|
||||
|
||||
BacktestExecutionResult result = executor.executeAndKeepTopK(strategies, numOf(100), Trade.TradeType.BUY,
|
||||
criterion, topK, null);
|
||||
|
||||
// Should return top 2 strategies
|
||||
assertEquals(topK, result.tradingStatements().size());
|
||||
|
||||
// Verify ordering: best (lowest commissions) should be first
|
||||
var statements = result.tradingStatements();
|
||||
Num commissions1 = criterion.calculate(series, statements.get(0).getTradingRecord());
|
||||
Num commissions2 = criterion.calculate(series, statements.get(1).getTradingRecord());
|
||||
|
||||
// Verify ascending order (best/lowest first) for lower-is-better criterion
|
||||
assertTrue("First strategy should have lowest commissions", commissions1.isLessThanOrEqual(commissions2));
|
||||
|
||||
// Verify we got the actual top performers (lowest commissions)
|
||||
// Strategy 1 should have the lowest commissions (only 2 trades)
|
||||
assertTrue("Top strategy should have lowest commissions", commissions1.isLessThanOrEqual(commissions2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeAndKeepTopKWithHigherIsBetterCriterion() {
|
||||
// Create a series with increasing prices to generate different returns
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
// Create strategies using the same pattern as
|
||||
// executeWithRuntimeReportCollectsMetrics
|
||||
// which we know produces trades
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
Strategy strategy3 = new BaseStrategy(new FixedRule(0, 4), new FixedRule(1, 2));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
GrossReturnCriterion criterion = new GrossReturnCriterion();
|
||||
|
||||
// First, execute all strategies to get their returns
|
||||
BacktestExecutionResult fullResult = executor.executeWithRuntimeReport(strategies, numOf(1),
|
||||
Trade.TradeType.BUY, null);
|
||||
List<Num> allReturns = new ArrayList<>();
|
||||
for (var statement : fullResult.tradingStatements()) {
|
||||
Num returnValue = criterion.calculate(series, statement.getTradingRecord());
|
||||
if (!returnValue.isNaN()) {
|
||||
allReturns.add(returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip test if no strategies produced trades
|
||||
if (allReturns.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now test executeAndKeepTopK
|
||||
int topK = Math.min(2, allReturns.size());
|
||||
BacktestExecutionResult result = executor.executeAndKeepTopK(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
criterion, topK, null);
|
||||
|
||||
// Should return top K strategies
|
||||
assertEquals(topK, result.tradingStatements().size());
|
||||
|
||||
// Verify ordering: best (highest return) should be first
|
||||
var statements = result.tradingStatements();
|
||||
Num return1 = criterion.calculate(series, statements.get(0).getTradingRecord());
|
||||
Num return2 = criterion.calculate(series, statements.get(1).getTradingRecord());
|
||||
|
||||
// Verify descending order (best first) - this is the key test for the fix
|
||||
// This verifies that criterion.betterThan() is used correctly for
|
||||
// higher-is-better criteria
|
||||
assertFalse("First strategy should have executed trades", return1.isNaN());
|
||||
if (topK > 1) {
|
||||
assertFalse("Second strategy should have executed trades", return2.isNaN());
|
||||
assertTrue("First strategy should have highest return: " + return1 + " >= " + return2,
|
||||
return1.isGreaterThanOrEqual(return2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeAndKeepTopKWithTopKGreaterThanStrategies() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13, 14).build();
|
||||
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0), new FixedRule(2));
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(1), new FixedRule(3));
|
||||
Strategy strategy3 = new BaseStrategy(new FixedRule(0), new FixedRule(4));
|
||||
|
||||
List<Strategy> strategies = List.of(strategy1, strategy2, strategy3);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
GrossReturnCriterion criterion = new GrossReturnCriterion();
|
||||
int topK = 10; // More than number of strategies
|
||||
|
||||
BacktestExecutionResult result = executor.executeAndKeepTopK(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
criterion, topK, null);
|
||||
|
||||
// Should return all strategies (min of topK and strategy count)
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeAndKeepTopKSkipsNaNStrategies() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10, 11, 12, 13).build();
|
||||
|
||||
Strategy strategyWithOneTrade = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
Strategy strategyWithTwoTrades = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategyWithoutTrades = new BaseStrategy(new FixedRule(), new FixedRule());
|
||||
|
||||
List<Strategy> strategies = List.of(strategyWithOneTrade, strategyWithTwoTrades, strategyWithoutTrades);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
AnalysisCriterion criterion = new NaNPenalizingCriterion();
|
||||
|
||||
BacktestExecutionResult result = executor.executeAndKeepTopK(strategies, numOf(1), Trade.TradeType.BUY,
|
||||
criterion, 2, null);
|
||||
|
||||
assertEquals(2, result.tradingStatements().size());
|
||||
|
||||
Num firstScore = criterion.calculate(series, result.tradingStatements().get(0).getTradingRecord());
|
||||
Num secondScore = criterion.calculate(series, result.tradingStatements().get(1).getTradingRecord());
|
||||
|
||||
assertFalse(firstScore.isNaN());
|
||||
assertFalse(secondScore.isNaN());
|
||||
assertTrue(firstScore.isGreaterThanOrEqual(secondScore));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWalkForwardRunsStrategyAcrossFolds() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
|
||||
.build();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(4, 8, 12), new FixedRule(5, 9, 13));
|
||||
WalkForwardConfig config = new WalkForwardConfig(4, 4, 4, 0, 0, 4, 2, List.of(1), 1, List.of(1), 3L);
|
||||
BacktestExecutor executor = new BacktestExecutor(series, new ZeroCostModel(), new ZeroCostModel(),
|
||||
new TradeOnCurrentCloseModel());
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.executeWalkForward(strategy, numOf(1), Trade.TradeType.BUY,
|
||||
config);
|
||||
|
||||
assertSame(series, result.barSeries());
|
||||
assertFalse(result.folds().isEmpty());
|
||||
assertEquals(result.folds().size(), result.runtimeReport().foldRuntimes().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeWithWalkForwardReturnsCombinedBacktestAndWalkForwardOutputs() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
|
||||
.build();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(4, 8, 12), new FixedRule(5, 9, 13));
|
||||
WalkForwardConfig config = new WalkForwardConfig(4, 4, 4, 0, 0, 4, 2, List.of(1), 1, List.of(1), 3L);
|
||||
BacktestExecutor executor = new BacktestExecutor(series, new ZeroCostModel(), new ZeroCostModel(),
|
||||
new TradeOnCurrentCloseModel());
|
||||
|
||||
BacktestExecutor.BacktestAndWalkForwardResult result = executor.executeWithWalkForward(strategy, numOf(1),
|
||||
Trade.TradeType.BUY, config);
|
||||
|
||||
assertEquals(1, result.backtest().tradingStatements().size());
|
||||
assertFalse(result.walkForward().folds().isEmpty());
|
||||
assertSame(result.backtest().barSeries(), result.walkForward().barSeries());
|
||||
}
|
||||
|
||||
private static final class NaNPenalizingCriterion implements AnalysisCriterion {
|
||||
|
||||
@Override
|
||||
public Num calculate(BarSeries series, Position position) {
|
||||
if (!position.isClosed()) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
return series.numFactory().numOf(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(BarSeries series, TradingRecord tradingRecord) {
|
||||
int tradeCount = tradingRecord.getTrades().size();
|
||||
if (tradeCount == 0) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
return series.numFactory().numOf(tradeCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean betterThan(Num criterionValue1, Num criterionValue2) {
|
||||
if (criterionValue1.isNaN()) {
|
||||
return false;
|
||||
}
|
||||
if (criterionValue2.isNaN()) {
|
||||
return true;
|
||||
}
|
||||
return criterionValue1.isGreaterThan(criterionValue2);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TrackingTradingRecord extends BaseTradingRecord {
|
||||
|
||||
private TrackingTradingRecord(Trade.TradeType tradeType, int startIndex, int endIndex,
|
||||
CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
super(tradeType, startIndex, endIndex, transactionCostModel, holdingCostModel);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BacktestRuntimeReportTest {
|
||||
|
||||
@Test
|
||||
public void emptyCreatesReportWithZeroedStatistics() {
|
||||
BacktestRuntimeReport report = BacktestRuntimeReport.empty();
|
||||
|
||||
assertEquals(Duration.ZERO, report.overallRuntime());
|
||||
assertEquals(Duration.ZERO, report.minStrategyRuntime());
|
||||
assertEquals(Duration.ZERO, report.maxStrategyRuntime());
|
||||
assertEquals(Duration.ZERO, report.averageStrategyRuntime());
|
||||
assertEquals(Duration.ZERO, report.medianStrategyRuntime());
|
||||
assertTrue(report.strategyRuntimes().isEmpty());
|
||||
assertEquals(0, report.strategyCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void strategyCountReturnsCorrectSize() {
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
|
||||
List<BacktestRuntimeReport.StrategyRuntime> runtimes = List.of(
|
||||
new BacktestRuntimeReport.StrategyRuntime(strategy1, Duration.ofMillis(100)),
|
||||
new BacktestRuntimeReport.StrategyRuntime(strategy2, Duration.ofMillis(200)));
|
||||
|
||||
BacktestRuntimeReport report = new BacktestRuntimeReport(Duration.ofMillis(300), Duration.ofMillis(100),
|
||||
Duration.ofMillis(200), Duration.ofMillis(150), Duration.ofMillis(150), runtimes);
|
||||
|
||||
assertEquals(2, report.strategyCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorValidatesStrategyRuntimesNotNull() {
|
||||
try {
|
||||
new BacktestRuntimeReport(Duration.ZERO, Duration.ZERO, Duration.ZERO, Duration.ZERO, Duration.ZERO, null);
|
||||
fail("Expected NullPointerException");
|
||||
} catch (NullPointerException e) {
|
||||
assertEquals("strategyRuntimes must not be null", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorCreatesImmutableCopyOfStrategyRuntimes() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
List<BacktestRuntimeReport.StrategyRuntime> mutableList = new ArrayList<>();
|
||||
mutableList.add(new BacktestRuntimeReport.StrategyRuntime(strategy, Duration.ofMillis(100)));
|
||||
|
||||
BacktestRuntimeReport report = new BacktestRuntimeReport(Duration.ZERO, Duration.ZERO, Duration.ZERO,
|
||||
Duration.ZERO, Duration.ZERO, mutableList);
|
||||
|
||||
assertEquals(1, report.strategyRuntimes().size());
|
||||
|
||||
mutableList.add(new BacktestRuntimeReport.StrategyRuntime(strategy, Duration.ofMillis(200)));
|
||||
|
||||
assertEquals("strategyRuntimes should be immutable", 1, report.strategyRuntimes().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringExcludesStrategyRuntimes() {
|
||||
Strategy strategy1 = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Strategy strategy2 = new BaseStrategy(new FixedRule(1, 3), new FixedRule(2, 4));
|
||||
|
||||
List<BacktestRuntimeReport.StrategyRuntime> runtimes = List.of(
|
||||
new BacktestRuntimeReport.StrategyRuntime(strategy1, Duration.ofMillis(100)),
|
||||
new BacktestRuntimeReport.StrategyRuntime(strategy2, Duration.ofMillis(200)));
|
||||
|
||||
BacktestRuntimeReport report = new BacktestRuntimeReport(Duration.ofSeconds(5), Duration.ofMillis(100),
|
||||
Duration.ofMillis(200), Duration.ofMillis(150), Duration.ofMillis(150), runtimes);
|
||||
|
||||
String jsonString = report.toString();
|
||||
|
||||
assertNotNull("toString() should not return null", jsonString);
|
||||
assertFalse("toString() should return non-empty JSON", jsonString.isEmpty());
|
||||
|
||||
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
assertTrue("JSON should contain overallRuntime", json.has("overallRuntime"));
|
||||
assertTrue("JSON should contain minStrategyRuntime", json.has("minStrategyRuntime"));
|
||||
assertTrue("JSON should contain maxStrategyRuntime", json.has("maxStrategyRuntime"));
|
||||
assertTrue("JSON should contain averageStrategyRuntime", json.has("averageStrategyRuntime"));
|
||||
assertTrue("JSON should contain medianStrategyRuntime", json.has("medianStrategyRuntime"));
|
||||
|
||||
assertFalse("JSON should NOT contain strategyRuntimes", json.has("strategyRuntimes"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringSerializesDurationFields() {
|
||||
Duration overall = Duration.ofSeconds(5);
|
||||
Duration min = Duration.ofMillis(100);
|
||||
Duration max = Duration.ofMillis(200);
|
||||
Duration avg = Duration.ofMillis(150);
|
||||
Duration median = Duration.ofMillis(150);
|
||||
|
||||
BacktestRuntimeReport report = new BacktestRuntimeReport(overall, min, max, avg, median, List.of());
|
||||
|
||||
String jsonString = report.toString();
|
||||
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
assertEquals("PT5S", json.get("overallRuntime").getAsString());
|
||||
assertEquals("PT0.1S", json.get("minStrategyRuntime").getAsString());
|
||||
assertEquals("PT0.2S", json.get("maxStrategyRuntime").getAsString());
|
||||
assertEquals("PT0.15S", json.get("averageStrategyRuntime").getAsString());
|
||||
assertEquals("PT0.15S", json.get("medianStrategyRuntime").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringHandlesEmptyStrategyRuntimes() {
|
||||
BacktestRuntimeReport report = BacktestRuntimeReport.empty();
|
||||
|
||||
String jsonString = report.toString();
|
||||
|
||||
assertNotNull("toString() should not return null", jsonString);
|
||||
assertFalse("toString() should return non-empty JSON", jsonString.isEmpty());
|
||||
|
||||
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
assertFalse("JSON should NOT contain strategyRuntimes", json.has("strategyRuntimes"));
|
||||
assertEquals("PT0S", json.get("overallRuntime").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void strategyRuntimeRecordHoldsStrategyAndRuntime() {
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3));
|
||||
Duration runtime = Duration.ofMillis(150);
|
||||
|
||||
BacktestRuntimeReport.StrategyRuntime strategyRuntime = new BacktestRuntimeReport.StrategyRuntime(strategy,
|
||||
runtime);
|
||||
|
||||
assertSame(strategy, strategyRuntime.strategy());
|
||||
assertEquals(runtime, strategyRuntime.runtime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.walkforward.AnchoredExpandingWalkForwardSplitter;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
import org.ta4j.core.walkforward.WalkForwardSplit;
|
||||
|
||||
public class BarSeriesManagerTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
private BarSeries seriesForRun;
|
||||
|
||||
private BarSeriesManager manager;
|
||||
|
||||
private Strategy strategy;
|
||||
|
||||
private final Num HUNDRED = numOf(100);
|
||||
|
||||
public BarSeriesManagerTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
seriesForRun = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2013-01-01T05:00:00Z")).closePrice(1d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2013-08-01T05:00:00Z")).closePrice(2d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2013-10-01T05:00:00Z")).closePrice(3d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2013-12-01T05:00:00Z")).closePrice(4d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2014-02-01T05:00:00Z")).closePrice(5d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2015-01-01T05:00:00Z")).closePrice(6d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2015-08-01T05:00:00Z")).closePrice(7d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2015-10-01T05:00:00Z")).closePrice(8d).add();
|
||||
seriesForRun.barBuilder().endTime(Instant.parse("2015-12-01T05:00:00Z")).closePrice(7d).add();
|
||||
|
||||
manager = new BarSeriesManager(seriesForRun, new TradeOnCurrentCloseModel());
|
||||
|
||||
strategy = new BaseStrategy(new FixedRule(0, 2, 3, 6), new FixedRule(1, 4, 7, 8));
|
||||
strategy.setUnstableBars(2); // Strategy would need a real test class
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runOnWholeSeries() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(20d, 40d, 60d, 10d, 30d, 50d, 0d, 20d, 40d)
|
||||
.build();
|
||||
manager = new BarSeriesManager(series, new TradeOnCurrentCloseModel());
|
||||
List<Position> allPositions = manager.run(strategy).getPositions();
|
||||
assertEquals(2, allPositions.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runOnWholeSeriesWithAmount() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(20d, 40d, 60d, 10d, 30d, 50d, 0d, 20d, 40d)
|
||||
.build();
|
||||
manager = new BarSeriesManager(series, new TradeOnCurrentCloseModel());
|
||||
List<Position> allPositions = manager.run(strategy, TradeType.BUY, HUNDRED).getPositions();
|
||||
|
||||
assertEquals(2, allPositions.size());
|
||||
assertEquals(HUNDRED, allPositions.get(0).getEntry().getAmount());
|
||||
assertEquals(HUNDRED, allPositions.get(1).getEntry().getAmount());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runOnSeries() {
|
||||
List<Position> positions = manager.run(strategy).getPositions();
|
||||
assertEquals(2, positions.size());
|
||||
|
||||
assertEquals(buyAt(2, seriesForRun.getBar(2).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(sellAt(4, seriesForRun.getBar(4).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
|
||||
assertEquals(buyAt(6, seriesForRun.getBar(6).getClosePrice(), numOf(1)), positions.get(1).getEntry());
|
||||
assertEquals(sellAt(7, seriesForRun.getBar(7).getClosePrice(), numOf(1)), positions.get(1).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWithOpenEntryBuyLeft() {
|
||||
Strategy aStrategy = new BaseStrategy(new FixedRule(1), new FixedRule(3));
|
||||
List<Position> positions = manager.run(aStrategy, 0, 3).getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
|
||||
assertEquals(buyAt(1, seriesForRun.getBar(1).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(sellAt(3, seriesForRun.getBar(3).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWithOpenEntrySellLeft() {
|
||||
Strategy aStrategy = new BaseStrategy(new FixedRule(1), new FixedRule(3));
|
||||
List<Position> positions = manager.run(aStrategy, TradeType.SELL, 0, 3).getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
|
||||
assertEquals(sellAt(1, seriesForRun.getBar(1).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(buyAt(3, seriesForRun.getBar(3).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runUsesStrategyStartingTypeByDefault() {
|
||||
Strategy shortStrategy = new BaseStrategy(new FixedRule(0, 2, 3, 6), new FixedRule(1, 4, 7, 8), TradeType.SELL);
|
||||
shortStrategy.setUnstableBars(2);
|
||||
|
||||
List<Position> positions = manager.run(shortStrategy).getPositions();
|
||||
assertEquals(2, positions.size());
|
||||
assertEquals(sellAt(2, seriesForRun.getBar(2).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(buyAt(4, seriesForRun.getBar(4).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWithIndexesUsesStrategyStartingTypeByDefault() {
|
||||
Strategy shortStrategy = new BaseStrategy(new FixedRule(1), new FixedRule(3), TradeType.SELL);
|
||||
List<Position> positions = manager.run(shortStrategy, 0, 3).getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
|
||||
assertEquals(sellAt(1, seriesForRun.getBar(1).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(buyAt(3, seriesForRun.getBar(3).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runBetweenIndexes() {
|
||||
|
||||
// only 1 entry happened within [0-3]
|
||||
TradingRecord tradingRecord = manager.run(strategy, 0, 3);
|
||||
List<Position> positions = tradingRecord.getPositions();
|
||||
assertEquals(0, tradingRecord.getPositions().size());
|
||||
assertEquals(2, tradingRecord.getCurrentPosition().getEntry().getIndex());
|
||||
|
||||
// 1 entry and 1 exit happened within [0-4]
|
||||
tradingRecord = manager.run(strategy, 0, 4);
|
||||
positions = tradingRecord.getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
assertEquals(buyAt(2, seriesForRun.getBar(2).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(sellAt(4, seriesForRun.getBar(4).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
|
||||
// no trades happened within [4-4]
|
||||
tradingRecord = manager.run(strategy, 4, 4);
|
||||
positions = tradingRecord.getPositions();
|
||||
assertTrue(positions.isEmpty());
|
||||
|
||||
// 1 entry and 1 exit happened within [5-8]
|
||||
tradingRecord = manager.run(strategy, 5, 8);
|
||||
positions = tradingRecord.getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
assertEquals(buyAt(6, seriesForRun.getBar(6).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(sellAt(7, seriesForRun.getBar(7).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runOnSeriesSlices() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
series.barBuilder().closePrice(1d).add();
|
||||
series.barBuilder().closePrice(2d).add();
|
||||
series.barBuilder().closePrice(3d).add();
|
||||
series.barBuilder().closePrice(4d).add();
|
||||
series.barBuilder().closePrice(5d).add();
|
||||
series.barBuilder().closePrice(6d).add();
|
||||
series.barBuilder().closePrice(7d).add();
|
||||
series.barBuilder().closePrice(8d).add();
|
||||
series.barBuilder().closePrice(9d).add();
|
||||
series.barBuilder().closePrice(10d).add();
|
||||
|
||||
manager = new BarSeriesManager(series, new TradeOnCurrentCloseModel());
|
||||
|
||||
Strategy aStrategy = new BaseStrategy(new FixedRule(0, 3, 5, 7), new FixedRule(2, 4, 6, 9));
|
||||
|
||||
// only 1 entry happened within [0-1]
|
||||
TradingRecord tradingRecord = manager.run(aStrategy, 0, 1);
|
||||
List<Position> positions = tradingRecord.getPositions();
|
||||
assertEquals(0, positions.size());
|
||||
assertEquals(0, tradingRecord.getCurrentPosition().getEntry().getIndex());
|
||||
|
||||
// only 1 entry happened within [2-3]
|
||||
tradingRecord = manager.run(aStrategy, 2, 3);
|
||||
positions = tradingRecord.getPositions();
|
||||
assertEquals(0, positions.size());
|
||||
assertEquals(3, tradingRecord.getCurrentPosition().getEntry().getIndex());
|
||||
|
||||
// 1 entry and 1 exit happened within [4-6]
|
||||
positions = manager.run(aStrategy, 4, 6).getPositions();
|
||||
assertEquals(1, positions.size());
|
||||
assertEquals(buyAt(5, series.getBar(5).getClosePrice(), numOf(1)), positions.get(0).getEntry());
|
||||
assertEquals(sellAt(6, series.getBar(6).getClosePrice(), numOf(1)), positions.get(0).getExit());
|
||||
|
||||
// 1 entry happened within [7-7]
|
||||
tradingRecord = manager.run(aStrategy, 7, 7);
|
||||
positions = tradingRecord.getPositions();
|
||||
assertEquals(0, positions.size());
|
||||
assertEquals(7, tradingRecord.getCurrentPosition().getEntry().getIndex());
|
||||
|
||||
// no trade happened within [8-8]
|
||||
positions = manager.run(aStrategy, 8, 8).getPositions();
|
||||
assertTrue(positions.isEmpty());
|
||||
|
||||
// no trade happened within [9-9]
|
||||
positions = manager.run(aStrategy, 9, 9).getPositions();
|
||||
assertTrue(positions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokesExecutionModelOnBarForEachVisitedIndex() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 20d, 30d, 40d).build();
|
||||
List<Integer> visitedIndices = new ArrayList<>();
|
||||
TradeExecutionModel model = new TradeExecutionModel() {
|
||||
@Override
|
||||
public void onBar(int index, TradingRecord tradingRecord, BarSeries barSeries) {
|
||||
visitedIndices.add(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
BarSeriesManager localManager = new BarSeriesManager(series, model);
|
||||
Strategy noSignalStrategy = new BaseStrategy(new FixedRule(), new FixedRule());
|
||||
|
||||
localManager.run(noSignalStrategy, 1, 3);
|
||||
|
||||
assertEquals(List.of(1, 2, 3), visitedIndices);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBarCanOperateWithoutAnyStrategySignal() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(10d, 20d, 30d).build();
|
||||
TradeExecutionModel model = new TradeExecutionModel() {
|
||||
@Override
|
||||
public void onBar(int index, TradingRecord tradingRecord, BarSeries barSeries) {
|
||||
if (index == 1 && tradingRecord.isClosed()) {
|
||||
tradingRecord.operate(index, barSeries.getBar(index).getClosePrice(), barSeries.numFactory().one());
|
||||
} else if (index == 2 && !tradingRecord.isClosed()) {
|
||||
tradingRecord.operate(index, barSeries.getBar(index).getClosePrice(), barSeries.numFactory().one());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
BarSeriesManager localManager = new BarSeriesManager(series, model);
|
||||
Strategy noSignalStrategy = new BaseStrategy(new FixedRule(), new FixedRule());
|
||||
|
||||
TradingRecord tradingRecord = localManager.run(noSignalStrategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositionCount());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
assertEquals(1, position.getEntry().getIndex());
|
||||
assertEquals(2, position.getExit().getIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWithProvidedTradingRecordReturnsSameInstance() {
|
||||
TradingRecord providedRecord = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
|
||||
TradingRecord returnedRecord = manager.run(strategy, providedRecord, numOf(1), 0, 8);
|
||||
|
||||
assertSame(providedRecord, returnedRecord);
|
||||
assertEquals(2, returnedRecord.getPositionCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWithProvidedBaseTradingRecordSupportsLiveBacktestStack() {
|
||||
BaseTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), 0, 8);
|
||||
|
||||
TradingRecord returnedRecord = manager.run(strategy, liveRecord, numOf(1), 0, 8);
|
||||
|
||||
assertSame(liveRecord, returnedRecord);
|
||||
assertEquals(2, returnedRecord.getPositionCount());
|
||||
Position firstPosition = returnedRecord.getPositions().getFirst();
|
||||
assertEquals(2, firstPosition.getEntry().getIndex());
|
||||
assertEquals(4, firstPosition.getExit().getIndex());
|
||||
assertEquals(TradeType.BUY, firstPosition.getEntry().getType());
|
||||
assertEquals(TradeType.SELL, firstPosition.getExit().getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRunCanUseConfiguredTradingRecordFactory() {
|
||||
final int[] capturedBounds = new int[2];
|
||||
BarSeriesManager.TradingRecordFactory recordFactory = (tradeType, startIndex, endIndex, txCost, holdCost) -> {
|
||||
capturedBounds[0] = startIndex;
|
||||
capturedBounds[1] = endIndex;
|
||||
return new BaseTradingRecord(tradeType, ExecutionMatchPolicy.FIFO, txCost, holdCost, startIndex, endIndex);
|
||||
};
|
||||
BarSeriesManager localManager = new BarSeriesManager(seriesForRun, new ZeroCostModel(), new ZeroCostModel(),
|
||||
new TradeOnCurrentCloseModel(), recordFactory);
|
||||
|
||||
TradingRecord record = localManager.run(strategy, TradeType.BUY, numOf(1), -10, 99);
|
||||
|
||||
assertTrue(record instanceof BaseTradingRecord);
|
||||
assertEquals(seriesForRun.getBeginIndex(), capturedBounds[0]);
|
||||
assertEquals(seriesForRun.getEndIndex(), capturedBounds[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWalkForwardReturnsAllConfiguredSplits() {
|
||||
WalkForwardConfig config = new WalkForwardConfig(3, 2, 2, 0, 0, 2, 1, List.of(), 1, List.of(1), 7L);
|
||||
StrategyWalkForwardExecutionResult result = manager.runWalkForward(strategy, config);
|
||||
List<WalkForwardSplit> expectedSplits = new AnchoredExpandingWalkForwardSplitter().split(seriesForRun, config);
|
||||
|
||||
assertEquals(expectedSplits.size(), result.folds().size());
|
||||
assertEquals(expectedSplits.size(), result.runtimeReport().foldRuntimes().size());
|
||||
assertTrue(result.holdoutFold().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runWalkForwardRespectsExplicitTradeTypeAndAmount() {
|
||||
WalkForwardConfig config = new WalkForwardConfig(3, 2, 2, 0, 0, 2, 1, List.of(), 1, List.of(1), 7L);
|
||||
List<WalkForwardSplit> splits = new AnchoredExpandingWalkForwardSplitter().split(seriesForRun, config);
|
||||
WalkForwardSplit firstSplit = splits.getFirst();
|
||||
Strategy foldStrategy = new BaseStrategy(new FixedRule(firstSplit.testStart()),
|
||||
new FixedRule(firstSplit.testEnd()), TradeType.SELL);
|
||||
|
||||
StrategyWalkForwardExecutionResult result = manager.runWalkForward(foldStrategy, TradeType.SELL, HUNDRED,
|
||||
config);
|
||||
Trade entry = result.folds()
|
||||
.getFirst()
|
||||
.tradingStatement()
|
||||
.getTradingRecord()
|
||||
.getPositions()
|
||||
.getFirst()
|
||||
.getEntry();
|
||||
|
||||
assertEquals(TradeType.SELL, entry.getType());
|
||||
assertEquals(HUNDRED, entry.getAmount());
|
||||
}
|
||||
|
||||
private Trade buyAt(int index, Num price, Num amount) {
|
||||
return Trade.buyAt(index, price, amount);
|
||||
}
|
||||
|
||||
private Trade sellAt(int index, Num price, Num amount) {
|
||||
return Trade.sellAt(index, price, amount);
|
||||
}
|
||||
}
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DecimalNum;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class ProgressCompletionTest {
|
||||
|
||||
@Test
|
||||
public void noOpDoesNothing() {
|
||||
Consumer<Integer> callback = ProgressCompletion.noOp();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw any exceptions
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
callback.accept(1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetection() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw - should detect ProgressCompletionTest as the caller
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionAndCustomInterval() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw - should detect ProgressCompletionTest as the caller
|
||||
callback.accept(50);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionDetectsCorrectCaller() {
|
||||
// Test that auto-detection correctly identifies the calling class
|
||||
Consumer<Integer> callback = ProgressCompletion.logging();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Verify it works (doesn't throw)
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionFromNestedMethod() {
|
||||
// Test that auto-detection works when called from a nested method
|
||||
Consumer<Integer> callback = createCallbackFromNestedMethod();
|
||||
assertNotNull(callback);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionAndCustomIntervalDetectsCorrectCaller() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(75);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Verify it works with custom interval
|
||||
callback.accept(75);
|
||||
callback.accept(150);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionWorksWithTotalStrategies() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should work with withTotalStrategies
|
||||
Consumer<Integer> wrapped = ProgressCompletion.withTotalStrategies(callback, 500);
|
||||
assertNotNull(wrapped);
|
||||
|
||||
// Should not throw
|
||||
wrapped.accept(100);
|
||||
wrapped.accept(250); // 50% milestone
|
||||
wrapped.accept(500); // 100% milestone
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionAndIntervalWorksWithTotalStrategies() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should work with withTotalStrategies
|
||||
Consumer<Integer> wrapped = ProgressCompletion.withTotalStrategies(callback, 200);
|
||||
assertNotNull(wrapped);
|
||||
|
||||
// Should not throw - logs at interval (50, 100, 150, 200) and milestones
|
||||
wrapped.accept(50); // Interval
|
||||
wrapped.accept(100); // Interval and 50% milestone
|
||||
wrapped.accept(150); // Interval and 75% milestone
|
||||
wrapped.accept(200); // Interval and 100% milestone
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionWorksWithBacktestExecutor() {
|
||||
// Integration test to ensure auto-detection works with actual BacktestExecutor
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 150; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
// Use auto-detection convenience method
|
||||
Consumer<Integer> callback = ProgressCompletion.logging();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionAndIntervalWorksWithBacktestExecutor() {
|
||||
// Integration test with custom interval
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 200; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
// Use auto-detection with custom interval
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(50);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionInvalidIntervalThrowsException() {
|
||||
// Verify that invalid intervals still throw exceptions with auto-detection
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.logging(0));
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.logging(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionFromHelperClass() {
|
||||
// Test that auto-detection works when called from a helper class
|
||||
TestHelper helper = new TestHelper();
|
||||
Consumer<Integer> callback = helper.createCallback();
|
||||
assertNotNull(callback);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithAutoDetectionFromHelperClassWithInterval() {
|
||||
// Test that auto-detection works with interval when called from helper class
|
||||
TestHelper helper = new TestHelper();
|
||||
Consumer<Integer> callback = helper.createCallbackWithInterval(25);
|
||||
assertNotNull(callback);
|
||||
callback.accept(25);
|
||||
callback.accept(50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to test auto-detection from nested method calls.
|
||||
*/
|
||||
private Consumer<Integer> createCallbackFromNestedMethod() {
|
||||
return ProgressCompletion.logging();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to test auto-detection from different calling contexts.
|
||||
*/
|
||||
private static class TestHelper {
|
||||
Consumer<Integer> createCallback() {
|
||||
return ProgressCompletion.logging();
|
||||
}
|
||||
|
||||
Consumer<Integer> createCallbackWithInterval(int interval) {
|
||||
return ProgressCompletion.logging(interval);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithAutoDetection() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw - should detect ProgressCompletionTest as the caller
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithAutoDetectionAndCustomInterval() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw - should detect ProgressCompletionTest as the caller
|
||||
callback.accept(50);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithStringLoggerName() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory("test.logger");
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithClass() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(ProgressCompletionTest.class);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithLogger() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithNullLoggerThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.loggingWithMemory((Logger) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithInvalidIntervalThrowsException() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.loggingWithMemory(logger, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.loggingWithMemory(logger, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithCustomInterval() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger, 50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(50);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryLogsAtIntervalMilestones() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger, 100);
|
||||
ProgressCompletion.withTotalStrategies(callback, 500);
|
||||
|
||||
// Should not throw - logs at 100, 200, 300, etc. with memory stats
|
||||
callback.accept(100);
|
||||
callback.accept(200);
|
||||
callback.accept(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryLogsAtPercentageMilestones() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger, 1000); // Large interval
|
||||
ProgressCompletion.withTotalStrategies(callback, 400);
|
||||
|
||||
// Should not throw - logs at 25%, 50%, 75%, 100% with memory stats
|
||||
callback.accept(100); // 25%
|
||||
callback.accept(200); // 50%
|
||||
callback.accept(300); // 75%
|
||||
callback.accept(400); // 100%
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithoutTotalStrategiesLogsOnlyCount() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger, 100);
|
||||
|
||||
// Without total, should only log count at intervals with memory stats
|
||||
callback.accept(100);
|
||||
callback.accept(200);
|
||||
callback.accept(250); // Should not log (not an interval)
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWithTotalStrategiesLogsPercentage() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(logger, 100);
|
||||
ProgressCompletion.withTotalStrategies(callback, 1000);
|
||||
|
||||
// Should not throw - logs percentage with memory stats
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWorksWithTotalStrategies() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory();
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should work with withTotalStrategies
|
||||
Consumer<Integer> wrapped = ProgressCompletion.withTotalStrategies(callback, 500);
|
||||
assertNotNull(wrapped);
|
||||
|
||||
// Should not throw - logs with memory stats
|
||||
wrapped.accept(100);
|
||||
wrapped.accept(250); // 50% milestone
|
||||
wrapped.accept(500); // 100% milestone
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryAndIntervalWorksWithTotalStrategies() {
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should work with withTotalStrategies
|
||||
Consumer<Integer> wrapped = ProgressCompletion.withTotalStrategies(callback, 200);
|
||||
assertNotNull(wrapped);
|
||||
|
||||
// Should not throw - logs at interval and milestones with memory stats
|
||||
wrapped.accept(50); // Interval
|
||||
wrapped.accept(100); // Interval and 50% milestone
|
||||
wrapped.accept(150); // Interval and 75% milestone
|
||||
wrapped.accept(200); // Interval and 100% milestone
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryWorksWithBacktestExecutor() {
|
||||
// Integration test to ensure memory logging works with actual BacktestExecutor
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 150; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
// Use memory logging convenience method
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryAndIntervalWorksWithBacktestExecutor() {
|
||||
// Integration test with custom interval
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 200; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
// Use memory logging with custom interval
|
||||
Consumer<Integer> callback = ProgressCompletion.loggingWithMemory(50);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryFromHelperClass() {
|
||||
// Test that memory logging auto-detection works when called from helper class
|
||||
MemoryTestHelper helper = new MemoryTestHelper();
|
||||
Consumer<Integer> callback = helper.createCallback();
|
||||
assertNotNull(callback);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithMemoryFromHelperClassWithInterval() {
|
||||
// Test that memory logging auto-detection works with interval from helper class
|
||||
MemoryTestHelper helper = new MemoryTestHelper();
|
||||
Consumer<Integer> callback = helper.createCallbackWithInterval(25);
|
||||
assertNotNull(callback);
|
||||
callback.accept(25);
|
||||
callback.accept(50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to test memory logging auto-detection from different calling
|
||||
* contexts.
|
||||
*/
|
||||
private static class MemoryTestHelper {
|
||||
Consumer<Integer> createCallback() {
|
||||
return ProgressCompletion.loggingWithMemory();
|
||||
}
|
||||
|
||||
Consumer<Integer> createCallbackWithInterval(int interval) {
|
||||
return ProgressCompletion.loggingWithMemory(interval);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithStringLoggerName() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging("test.logger");
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithClass() {
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(ProgressCompletionTest.class);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(1);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithLogger() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithNullLoggerThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.logging((Logger) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithInvalidIntervalThrowsException() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.logging(logger, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> ProgressCompletion.logging(logger, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithCustomInterval() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 50);
|
||||
assertNotNull(callback);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(50);
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingLogsAtIntervalMilestones() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 100);
|
||||
ProgressCompletion.withTotalStrategies(callback, 500);
|
||||
|
||||
// Should not throw - logs at 100, 200, 300, etc.
|
||||
callback.accept(100);
|
||||
callback.accept(200);
|
||||
callback.accept(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingLogsAtPercentageMilestones() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 1000); // Large interval
|
||||
ProgressCompletion.withTotalStrategies(callback, 400);
|
||||
|
||||
// Should not throw - logs at 25%, 50%, 75%, 100%
|
||||
callback.accept(100); // 25%
|
||||
callback.accept(200); // 50%
|
||||
callback.accept(300); // 75%
|
||||
callback.accept(400); // 100%
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithoutTotalStrategiesLogsOnlyCount() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 100);
|
||||
|
||||
// Without total, should only log count at intervals
|
||||
callback.accept(100);
|
||||
callback.accept(200);
|
||||
callback.accept(250); // Should not log (not an interval)
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWithTotalStrategiesLogsPercentage() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 100);
|
||||
ProgressCompletion.withTotalStrategies(callback, 1000);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withTotalStrategiesThrowsForNullCallback() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ProgressCompletion.withTotalStrategies(null, 100));
|
||||
assertEquals("callback must not be null", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withTotalStrategiesReturnsSameCallbackForNonLoggingCallback() {
|
||||
AtomicInteger count = new AtomicInteger(0);
|
||||
Consumer<Integer> originalCallback = completed -> count.incrementAndGet();
|
||||
|
||||
Consumer<Integer> result = ProgressCompletion.withTotalStrategies(originalCallback, 100);
|
||||
assertNotNull(result);
|
||||
|
||||
// Should still work
|
||||
result.accept(1);
|
||||
assertEquals(1, count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingHandlesEdgeCases() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 100);
|
||||
ProgressCompletion.withTotalStrategies(callback, 1);
|
||||
|
||||
// Single strategy - should not throw
|
||||
callback.accept(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingHandlesExactIntervalAndMilestoneOverlap() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 250);
|
||||
ProgressCompletion.withTotalStrategies(callback, 1000);
|
||||
|
||||
// 250 is both an interval (250 % 250 == 0) and 25% milestone
|
||||
// Should not throw
|
||||
callback.accept(250);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingDoesNotLogBetweenMilestones() {
|
||||
Logger logger = LoggerFactory.getLogger("test.logger");
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(logger, 1000); // Large interval
|
||||
ProgressCompletion.withTotalStrategies(callback, 400);
|
||||
|
||||
// Should not throw
|
||||
callback.accept(100); // 25%
|
||||
callback.accept(150); // Between milestones
|
||||
callback.accept(199); // Between milestones
|
||||
callback.accept(200); // 50%
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loggingWorksWithBacktestExecutor() {
|
||||
// Integration test to ensure it works with actual BacktestExecutor
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 250; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
Consumer<Integer> callback = ProgressCompletion.logging(ProgressCompletionTest.class);
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noOpWorksWithBacktestExecutor() {
|
||||
// Integration test to ensure noOp works with actual BacktestExecutor
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
Consumer<Integer> callback = ProgressCompletion.noOp();
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, callback);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultNoOpWhenNullCallback() {
|
||||
// Integration test to verify default noOp behavior
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(DecimalNumFactory.getInstance())
|
||||
.withData(10, 11, 12, 13, 14)
|
||||
.build();
|
||||
|
||||
List<Strategy> strategies = new ArrayList<>();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
strategies.add(new BaseStrategy(new FixedRule(0, 2), new FixedRule(1, 3)));
|
||||
}
|
||||
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
// Pass null - should use default noOp
|
||||
BacktestExecutionResult result = executor.executeWithRuntimeReport(strategies, DecimalNum.valueOf(1),
|
||||
Trade.TradeType.BUY, null);
|
||||
|
||||
assertEquals(strategies.size(), result.tradingStatements().size());
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class SlippageExecutionModelTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public SlippageExecutionModelTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesConfigurableSlippageOnNextOpen() {
|
||||
BarSeries series = buildSeries();
|
||||
Num slippage = numOf(0.10);
|
||||
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series,
|
||||
new SlippageExecutionModel(slippage, TradeExecutionModel.PriceSource.NEXT_OPEN)).run(strategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositions().size());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
|
||||
Num one = series.numFactory().one();
|
||||
Num expectedEntryPrice = series.getBar(1).getOpenPrice().multipliedBy(one.plus(slippage));
|
||||
Num expectedExitPrice = series.getBar(2).getOpenPrice().multipliedBy(one.minus(slippage));
|
||||
|
||||
assertEquals(expectedEntryPrice, position.getEntry().getPricePerAsset());
|
||||
assertEquals(expectedExitPrice, position.getExit().getPricePerAsset());
|
||||
assertEquals(ExecutionSide.BUY, position.getEntry().getFills().getFirst().side());
|
||||
assertEquals(ExecutionSide.SELL, position.getExit().getFills().getFirst().side());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesConfigurableSlippageOnCurrentClose() {
|
||||
BarSeries series = buildSeries();
|
||||
Num slippage = numOf(0.05);
|
||||
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series,
|
||||
new SlippageExecutionModel(slippage, TradeExecutionModel.PriceSource.CURRENT_CLOSE)).run(strategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositions().size());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
|
||||
Num one = series.numFactory().one();
|
||||
Num expectedEntryPrice = series.getBar(0).getClosePrice().multipliedBy(one.plus(slippage));
|
||||
Num expectedExitPrice = series.getBar(1).getClosePrice().multipliedBy(one.minus(slippage));
|
||||
|
||||
assertEquals(expectedEntryPrice, position.getEntry().getPricePerAsset());
|
||||
assertEquals(expectedExitPrice, position.getExit().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsSignalWhenNoNextOpenBarExists() {
|
||||
BarSeries series = buildSeries();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(2), new FixedRule(2));
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series,
|
||||
new SlippageExecutionModel(numOf(0.01), TradeExecutionModel.PriceSource.NEXT_OPEN)).run(strategy);
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidSlippageRatios() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SlippageExecutionModel(numFactory.minusOne(), TradeExecutionModel.PriceSource.NEXT_OPEN));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SlippageExecutionModel(numFactory.one(), TradeExecutionModel.PriceSource.NEXT_OPEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesConfiguredParameters() {
|
||||
Num slippage = numFactory.numOf(0.02);
|
||||
SlippageExecutionModel model = new SlippageExecutionModel(slippage,
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
|
||||
assertEquals(slippage, model.getSlippageRatio());
|
||||
assertEquals(TradeExecutionModel.PriceSource.CURRENT_CLOSE, model.getPriceSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNullPriceSource() {
|
||||
assertThrows(NullPointerException.class, () -> new SlippageExecutionModel(numFactory.zero(), null));
|
||||
}
|
||||
|
||||
private BarSeries buildSeries() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
series.barBuilder().openPrice(100d).highPrice(105d).lowPrice(95d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(110d).highPrice(112d).lowPrice(108d).closePrice(109d).volume(10d).add();
|
||||
series.barBuilder().openPrice(120d).highPrice(122d).lowPrice(118d).closePrice(121d).volume(10d).add();
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradeFill;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class StopLimitExecutionModelTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public StopLimitExecutionModelTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUntriggeredOrderWhenItExpires() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(102d).lowPrice(98d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(103d).lowPrice(97d).closePrice(101d).volume(10d).add();
|
||||
series.barBuilder().openPrice(101d).highPrice(104d).lowPrice(98d).closePrice(100d).volume(10d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numOf(0.05), numOf(0.06), numOf(0.5), 2);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy);
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
assertFalse(model.getPendingOrder(tradingRecord).isPresent());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertTrue(rejection.reason().contains("expired"));
|
||||
assertEquals(series.numFactory().zero(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregatesPartialFillsAcrossBarsUntilRequestedAmountIsReached() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(100d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(6d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(8d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(6d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
4);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy, strategy.getStartingType(),
|
||||
numFactory.numOf(10));
|
||||
|
||||
assertEquals(3, tradingRecord.getTrades().size());
|
||||
Trade firstEntryFillTrade = tradingRecord.getTrades().get(0);
|
||||
Trade secondEntryFillTrade = tradingRecord.getTrades().get(1);
|
||||
Trade thirdEntryFillTrade = tradingRecord.getTrades().get(2);
|
||||
assertEquals(numFactory.numOf(3), firstEntryFillTrade.getAmount());
|
||||
assertEquals(numFactory.numOf(4), secondEntryFillTrade.getAmount());
|
||||
assertEquals(numFactory.numOf(3), thirdEntryFillTrade.getAmount());
|
||||
assertEquals(1, firstEntryFillTrade.getFills().size());
|
||||
assertEquals(1, secondEntryFillTrade.getFills().size());
|
||||
assertEquals(1, thirdEntryFillTrade.getFills().size());
|
||||
assertEquals(ExecutionSide.BUY, firstEntryFillTrade.getFills().getFirst().side());
|
||||
assertEquals(ExecutionSide.BUY, secondEntryFillTrade.getFills().getFirst().side());
|
||||
assertEquals(ExecutionSide.BUY, thirdEntryFillTrade.getFills().getFirst().side());
|
||||
assertEquals(series.getBar(1).getEndTime(), firstEntryFillTrade.getFills().getFirst().time());
|
||||
assertEquals(series.getBar(2).getEndTime(), secondEntryFillTrade.getFills().getFirst().time());
|
||||
assertEquals(series.getBar(3).getEndTime(), thirdEntryFillTrade.getFills().getFirst().time());
|
||||
assertFalse(model.getPendingOrder(tradingRecord).isPresent());
|
||||
assertTrue(model.getRejectedOrders(tradingRecord).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleFillStopLimitOrderUsesScalarTradeRepresentation() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(100d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(100d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 2);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy, strategy.getStartingType(),
|
||||
numFactory.one());
|
||||
|
||||
Trade entry = tradingRecord.getTrades().getFirst();
|
||||
assertTrue(entry instanceof BaseTrade);
|
||||
assertEquals(1, entry.getFills().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recordsFilledPortionAndRejectsRemainingAmountOnExpiry() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(100d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(4d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(2d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
2);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy, strategy.getStartingType(),
|
||||
numFactory.numOf(5));
|
||||
|
||||
assertEquals(2, tradingRecord.getTrades().size());
|
||||
Trade firstEntryFillTrade = tradingRecord.getTrades().get(0);
|
||||
Trade secondEntryFillTrade = tradingRecord.getTrades().get(1);
|
||||
assertEquals(numFactory.numOf(2), firstEntryFillTrade.getAmount());
|
||||
assertEquals(numFactory.one(), secondEntryFillTrade.getAmount());
|
||||
assertEquals(1, firstEntryFillTrade.getFills().size());
|
||||
assertEquals(1, secondEntryFillTrade.getFills().size());
|
||||
TradeFill firstFill = firstEntryFillTrade.getFills().getFirst();
|
||||
TradeFill secondFill = secondEntryFillTrade.getFills().getFirst();
|
||||
assertEquals(numFactory.numOf(2), firstFill.amount());
|
||||
assertEquals(numFactory.one(), secondFill.amount());
|
||||
assertEquals(ExecutionSide.BUY, firstFill.side());
|
||||
assertEquals(ExecutionSide.BUY, secondFill.side());
|
||||
assertEquals(series.getBar(1).getEndTime(), firstFill.time());
|
||||
assertEquals(series.getBar(2).getEndTime(), secondFill.time());
|
||||
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertEquals(numFactory.numOf(5), rejection.requestedAmount());
|
||||
assertEquals(numFactory.numOf(3), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidRequestedAmount() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 101d).build();
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
2);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.zero());
|
||||
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isEmpty());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertTrue(rejection.reason().contains("Invalid requested amount"));
|
||||
assertEquals(numFactory.zero(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSignalWhenAnotherOrderIsAlreadyPending() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 101d, 102d).build();
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
3);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.two());
|
||||
model.execute(0, tradingRecord, series, numFactory.numOf(3));
|
||||
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isPresent());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertTrue(rejection.reason().contains("another stop-limit order is pending"));
|
||||
assertEquals(numFactory.numOf(3), rejection.requestedAmount());
|
||||
assertEquals(numFactory.zero(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSignalWhenNextOpenReferenceCannotBeResolved() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d).build();
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 2);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isEmpty());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertTrue(rejection.reason().contains("Unable to resolve reference bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroVolumeBarsDoNotFillPendingOrders() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(0d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(0d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
2);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy, strategy.getStartingType(),
|
||||
numFactory.one());
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
assertEquals(numFactory.zero(), model.getRejectedOrders(tradingRecord).getFirst().filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesCurrentCloseReferenceInPendingOrder() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 2, TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
StopLimitExecutionModel.PendingOrderSnapshot pendingOrder = model.getPendingOrder(tradingRecord).orElseThrow();
|
||||
|
||||
assertEquals(1, pendingOrder.activationIndex());
|
||||
assertEquals(series.getBar(0).getClosePrice(), pendingOrder.stopPrice());
|
||||
assertEquals(series.getBar(0).getClosePrice(), pendingOrder.limitPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentCloseOrdersDoNotFillOnSignalBar() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(105d).lowPrice(95d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(120d).highPrice(120d).lowPrice(120d).closePrice(120d).volume(10d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 1, TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy);
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isEmpty());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runEndExpiresPendingEntryOrderAndCommitsFilledPortion() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(100d).lowPrice(100d).closePrice(100d).volume(100d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(100d).lowPrice(100d).closePrice(100d).volume(2d).add();
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5),
|
||||
3);
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule());
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, model).run(strategy, strategy.getStartingType(),
|
||||
numFactory.numOf(3));
|
||||
|
||||
assertEquals(1, tradingRecord.getTrades().size());
|
||||
assertEquals(numFactory.one(), tradingRecord.getTrades().getFirst().getAmount());
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isEmpty());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
assertEquals(numFactory.one(), model.getRejectedOrders(tradingRecord).getFirst().filledAmount());
|
||||
assertEquals(numFactory.numOf(3), model.getRejectedOrders(tradingRecord).getFirst().requestedAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exitOrderUsesCurrentOpenPositionAmount() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.operate(0, numFactory.hundred(), numFactory.numOf(5));
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 2, TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
|
||||
StopLimitExecutionModel.PendingOrderSnapshot pendingOrder = model.getPendingOrder(tradingRecord).orElseThrow();
|
||||
assertEquals(numFactory.numOf(5), pendingOrder.requestedAmount());
|
||||
assertEquals(1, pendingOrder.activationIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exitOrderUsesRemainingOpenExposureAfterPartialExit() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.operate(0, numFactory.hundred(), numFactory.numOf(5));
|
||||
tradingRecord.operate(1, numFactory.numOf(101), numFactory.one());
|
||||
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 2, TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
model.execute(1, tradingRecord, series, numFactory.one());
|
||||
|
||||
StopLimitExecutionModel.PendingOrderSnapshot pendingOrder = model.getPendingOrder(tradingRecord).orElseThrow();
|
||||
assertEquals(numFactory.numOf(4), pendingOrder.requestedAmount());
|
||||
assertEquals(2, pendingOrder.activationIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialExitOrderExpiryCommitsFilledPortionWhenRecordIsExposedAsTradingRecord() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(20d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(2d).add();
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.operate(0, numFactory.hundred(), numFactory.numOf(5));
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5), 1,
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
model.onBar(1, tradingRecord, series);
|
||||
|
||||
assertEquals(3, tradingRecord.getTrades().size());
|
||||
assertTrue(tradingRecord.getCurrentPosition().isOpened());
|
||||
assertEquals(numFactory.numOf(4), tradingRecord.getCurrentPosition().getEntry().getAmount());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertEquals(numFactory.one(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialExitOrderExpiryDoesNotCommitForLegacyRecordWithoutLotExposure() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(20d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(2d).add();
|
||||
|
||||
LegacyTradingRecordWithoutLotExposure tradingRecord = new LegacyTradingRecordWithoutLotExposure(numFactory);
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5), 1,
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
model.onBar(1, tradingRecord, series);
|
||||
|
||||
assertEquals(0, tradingRecord.recordedOperations().size());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertEquals(numFactory.one(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialExitOrderExpiryCommitsFilledPortionForBaseTradingRecord() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(20d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(2d).add();
|
||||
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord(Trade.TradeType.BUY, ExecutionMatchPolicy.FIFO,
|
||||
new ZeroCostModel(), new ZeroCostModel(), null, null);
|
||||
tradingRecord.operate(0, numFactory.hundred(), numFactory.numOf(5));
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numOf(0.5), 1,
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
model.onBar(1, tradingRecord, series);
|
||||
|
||||
assertEquals(3, tradingRecord.getTrades().size());
|
||||
assertTrue(tradingRecord.getCurrentPosition().isOpened());
|
||||
assertEquals(numFactory.numOf(4), tradingRecord.getCurrentPosition().getEntry().getAmount());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertEquals(numFactory.one(), rejection.filledAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stalePendingOrderExpiresWhenNextSignalArrives() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 101d, 102d, 103d)
|
||||
.build();
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numOf(0.5), numOf(0.6), numFactory.one(), 1);
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
assertTrue(model.getPendingOrder(tradingRecord).isPresent());
|
||||
|
||||
model.execute(2, tradingRecord, series, numFactory.one());
|
||||
|
||||
StopLimitExecutionModel.PendingOrderSnapshot pendingOrder = model.getPendingOrder(tradingRecord).orElseThrow();
|
||||
assertEquals(2, pendingOrder.signalIndex());
|
||||
assertEquals(1, model.getRejectedOrders(tradingRecord).size());
|
||||
StopLimitExecutionModel.RejectedOrder rejection = model.getRejectedOrders(tradingRecord).getFirst();
|
||||
assertTrue(rejection.reason().contains("expired"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidConstructorArguments() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new StopLimitExecutionModel(numFactory.zero(), numFactory.minusOne(), numFactory.one(), 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new StopLimitExecutionModel(numFactory.one(), numFactory.zero(), numFactory.one(), 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new StopLimitExecutionModel(numFactory.zero(), numFactory.one(), numFactory.one(), 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numFactory.zero(), 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(), numFactory.one(), 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sellOrdersTagFillsWithSellExecutionSide() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
series.barBuilder().openPrice(100d).highPrice(101d).lowPrice(99d).closePrice(100d).volume(10d).add();
|
||||
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.TradeType.SELL);
|
||||
StopLimitExecutionModel model = new StopLimitExecutionModel(numFactory.zero(), numFactory.zero(),
|
||||
numFactory.one(), 1);
|
||||
|
||||
model.execute(0, tradingRecord, series, numFactory.one());
|
||||
model.onBar(1, tradingRecord, series);
|
||||
|
||||
assertEquals(1, tradingRecord.getTrades().size());
|
||||
assertEquals(ExecutionSide.SELL, tradingRecord.getTrades().getFirst().getFills().getFirst().side());
|
||||
}
|
||||
|
||||
private static final class LegacyTradingRecordWithoutLotExposure implements TradingRecord {
|
||||
|
||||
private final List<Trade> recordedOperations = new ArrayList<>();
|
||||
private final Position openPosition;
|
||||
private final NumFactory numFactory;
|
||||
|
||||
private LegacyTradingRecordWithoutLotExposure(NumFactory numFactory) {
|
||||
this.numFactory = numFactory;
|
||||
Position position = new Position(Trade.TradeType.BUY);
|
||||
position.operate(0, numFactory.hundred(), numFactory.numOf(5));
|
||||
this.openPosition = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trade.TradeType getStartingType() {
|
||||
return Trade.TradeType.BUY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "legacy-record";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operate(int index, Num price, Num amount) {
|
||||
recordedOperations.add(Trade.sellAt(index, price, amount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostModel getTransactionCostModel() {
|
||||
return new ZeroCostModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostModel getHoldingCostModel() {
|
||||
return new ZeroCostModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Position> getPositions() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getCurrentPosition() {
|
||||
return openPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trade> getTrades() {
|
||||
return List.copyOf(recordedOperations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getStartIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getEndIndex() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enter(int index, Num price, Num amount) {
|
||||
throw new UnsupportedOperationException("Not used by this test");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exit(int index, Num price, Num amount) {
|
||||
throw new UnsupportedOperationException("Not used by this test");
|
||||
}
|
||||
|
||||
private List<Trade> recordedOperations() {
|
||||
return recordedOperations;
|
||||
}
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.criteria.NumberOfPositionsCriterion;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.BooleanRule;
|
||||
import org.ta4j.core.walkforward.AnchoredExpandingWalkForwardSplitter;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
import org.ta4j.core.walkforward.WalkForwardSplit;
|
||||
|
||||
public class StrategyWalkForwardExecutorTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public StrategyWalkForwardExecutorTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeProducesFoldResultsAndRuntimeReport() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
WalkForwardConfig config = walkForwardConfig();
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series, new ZeroCostModel(),
|
||||
new ZeroCostModel(), new TradeOnCurrentCloseModel());
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, Trade.TradeType.BUY, numOf(2), config);
|
||||
List<WalkForwardSplit> expectedSplits = new AnchoredExpandingWalkForwardSplitter().split(series, config);
|
||||
|
||||
assertEquals(expectedSplits.size(), result.folds().size());
|
||||
assertEquals(expectedSplits.size(), result.runtimeReport().foldRuntimes().size());
|
||||
assertTrue(result.holdoutFold().isPresent());
|
||||
assertFalse(result.inSampleFolds().isEmpty());
|
||||
assertFalse(result.outOfSampleFolds().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeReportsProgressPerFold() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
WalkForwardConfig config = walkForwardConfig();
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
|
||||
AtomicInteger callbackCount = new AtomicInteger(0);
|
||||
AtomicInteger lastCompleted = new AtomicInteger(0);
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, Trade.TradeType.BUY, numOf(1), config,
|
||||
completed -> {
|
||||
callbackCount.incrementAndGet();
|
||||
lastCompleted.set(completed);
|
||||
});
|
||||
|
||||
assertEquals(result.folds().size(), callbackCount.get());
|
||||
assertEquals(result.folds().size(), lastCompleted.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeUsesStartingTypeAndUnitAmountByDefault() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE, Trade.TradeType.SELL);
|
||||
WalkForwardConfig config = walkForwardConfig();
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series, new ZeroCostModel(),
|
||||
new ZeroCostModel(), new TradeOnCurrentCloseModel());
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, config);
|
||||
StrategyWalkForwardExecutionResult.FoldResult tradedFold = result.folds()
|
||||
.stream()
|
||||
.filter(fold -> !fold.tradingRecord().getPositions().isEmpty())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
assertNotNull(tradedFold);
|
||||
Trade entry = tradedFold.tradingRecord().getPositions().getFirst().getEntry();
|
||||
assertEquals(Trade.TradeType.SELL, entry.getType());
|
||||
assertEquals(series.numFactory().one(), entry.getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resultHelperViewsPartitionFolds() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
WalkForwardConfig config = walkForwardConfig();
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, config);
|
||||
int foldCount = result.folds().size();
|
||||
int inSampleCount = result.inSampleFolds().size();
|
||||
int outOfSampleCount = result.outOfSampleFolds().size();
|
||||
|
||||
assertEquals(foldCount, inSampleCount + outOfSampleCount);
|
||||
assertEquals(result.holdoutFold().isPresent(), outOfSampleCount > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resultExposesFoldTradingRecords() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
|
||||
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, walkForwardConfig());
|
||||
|
||||
assertEquals(result.folds().size(), result.tradingRecords().size());
|
||||
for (StrategyWalkForwardExecutionResult.FoldResult fold : result.folds()) {
|
||||
assertSame(fold.tradingRecord(), fold.tradingStatement().getTradingRecord());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resultCriterionHelpersUseAnalysisCriterionOnFoldTradingRecords() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
|
||||
StrategyWalkForwardExecutionResult result = executor.execute(strategy, walkForwardConfig());
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
List<Num> expectedAll = result.folds()
|
||||
.stream()
|
||||
.map(fold -> criterion.calculate(series, fold.tradingRecord()))
|
||||
.toList();
|
||||
|
||||
assertEquals(expectedAll, result.criterionValues(criterion));
|
||||
assertEquals(result.inSampleFolds().size(), result.inSampleCriterionValues(criterion).size());
|
||||
assertEquals(result.outOfSampleFolds().size(), result.outOfSampleCriterionValues(criterion).size());
|
||||
assertEquals(result.folds().size(), result.criterionValuesByFold(criterion).size());
|
||||
for (StrategyWalkForwardExecutionResult.FoldResult fold : result.folds()) {
|
||||
assertEquals(criterion.calculate(series, fold.tradingRecord()),
|
||||
result.criterionValuesByFold(criterion).get(fold.split().foldId()));
|
||||
}
|
||||
|
||||
if (result.holdoutFold().isPresent()) {
|
||||
Num expectedHoldout = criterion.calculate(series, result.holdoutFold().orElseThrow().tradingRecord());
|
||||
assertTrue(result.holdoutCriterionValue(criterion).isPresent());
|
||||
assertEquals(expectedHoldout, result.holdoutCriterionValue(criterion).orElseThrow());
|
||||
} else {
|
||||
assertFalse(result.holdoutCriterionValue(criterion).isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
private BarSeries buildSeries(int bars) {
|
||||
double[] data = new double[bars];
|
||||
for (int i = 0; i < bars; i++) {
|
||||
data[i] = 100 + (i * 0.5);
|
||||
}
|
||||
return new MockBarSeriesBuilder().withNumFactory(numFactory).withData(data).build();
|
||||
}
|
||||
|
||||
private static WalkForwardConfig walkForwardConfig() {
|
||||
return new WalkForwardConfig(12, 6, 6, 0, 0, 6, 3, List.of(2), 1, List.of(1), 42L);
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
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.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class TradeOnPriceExecutionModelTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public TradeOnPriceExecutionModelTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tradeOnCurrentCloseExecutesAtCurrentBarClose() {
|
||||
BarSeries series = buildSeries();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, new TradeOnCurrentCloseModel()).run(strategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositionCount());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
assertEquals(0, position.getEntry().getIndex());
|
||||
assertEquals(1, position.getExit().getIndex());
|
||||
assertEquals(series.getBar(0).getClosePrice(), position.getEntry().getPricePerAsset());
|
||||
assertEquals(series.getBar(1).getClosePrice(), position.getExit().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tradeOnNextOpenExecutesAtNextBarOpen() {
|
||||
BarSeries series = buildSeries();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(0), new FixedRule(1));
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, new TradeOnNextOpenModel()).run(strategy);
|
||||
|
||||
assertEquals(1, tradingRecord.getPositionCount());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
assertEquals(1, position.getEntry().getIndex());
|
||||
assertEquals(2, position.getExit().getIndex());
|
||||
assertEquals(series.getBar(1).getOpenPrice(), position.getEntry().getPricePerAsset());
|
||||
assertEquals(series.getBar(2).getOpenPrice(), position.getExit().getPricePerAsset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tradeOnNextOpenSkipsSignalWhenNoNextBarExists() {
|
||||
BarSeries series = buildSeries();
|
||||
Strategy strategy = new BaseStrategy(new FixedRule(2), new FixedRule(2));
|
||||
|
||||
TradingRecord tradingRecord = new BarSeriesManager(series, new TradeOnNextOpenModel()).run(strategy);
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tradeOnCurrentCloseSkipsSignalOutsideSeriesRange() {
|
||||
BarSeries series = buildSeries();
|
||||
TradeOnCurrentCloseModel model = new TradeOnCurrentCloseModel();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
|
||||
model.execute(series.getEndIndex() + 1, tradingRecord, series, numFactory.one());
|
||||
|
||||
assertTrue(tradingRecord.getTrades().isEmpty());
|
||||
}
|
||||
|
||||
private BarSeries buildSeries() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
|
||||
series.barBuilder().openPrice(100d).highPrice(105d).lowPrice(95d).closePrice(101d).volume(10d).add();
|
||||
series.barBuilder().openPrice(110d).highPrice(112d).lowPrice(108d).closePrice(109d).volume(10d).add();
|
||||
series.barBuilder().openPrice(120d).highPrice(122d).lowPrice(118d).closePrice(121d).volume(10d).add();
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.criteria.NumberOfPositionsCriterion;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.rules.BooleanRule;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
import org.ta4j.core.walkforward.WalkForwardRuntimeReport;
|
||||
|
||||
public class TradingStatementExecutionResultTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public TradingStatementExecutionResultTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backtestResultExposesSharedContractCriterionAndRecordViews() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
TradingStatementExecutionResult<BacktestRuntimeReport> executionResult = result;
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
List<TradingRecord> records = executionResult.tradingRecords();
|
||||
assertEquals(result.tradingStatements().size(), records.size());
|
||||
for (int i = 0; i < result.tradingStatements().size(); i++) {
|
||||
assertSame(result.tradingStatements().get(i).getTradingRecord(), records.get(i));
|
||||
}
|
||||
|
||||
List<Num> expectedValues = new ArrayList<>();
|
||||
for (TradingStatement statement : result.tradingStatements()) {
|
||||
expectedValues.add(criterion.calculate(result.barSeries(), statement.getTradingRecord()));
|
||||
}
|
||||
List<Num> actualValues = executionResult.criterionValues(criterion);
|
||||
assertEquals(expectedValues, actualValues);
|
||||
|
||||
Map<Integer, Num> valuesByIndex = executionResult.criterionValuesByIndex(criterion);
|
||||
assertEquals(actualValues.size(), valuesByIndex.size());
|
||||
for (int i = 0; i < actualValues.size(); i++) {
|
||||
assertEquals(actualValues.get(i), valuesByIndex.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void walkForwardResultExposesSharedContractCriterionAndRecordViews() {
|
||||
StrategyWalkForwardExecutionResult result = createWalkForwardResult();
|
||||
TradingStatementExecutionResult<WalkForwardRuntimeReport> executionResult = result;
|
||||
AnalysisCriterion criterion = new NumberOfPositionsCriterion();
|
||||
|
||||
List<TradingRecord> records = executionResult.tradingRecords();
|
||||
assertEquals(result.tradingStatements().size(), records.size());
|
||||
for (int i = 0; i < result.tradingStatements().size(); i++) {
|
||||
assertSame(result.tradingStatements().get(i).getTradingRecord(), records.get(i));
|
||||
}
|
||||
|
||||
List<Num> expectedValues = new ArrayList<>();
|
||||
for (TradingStatement statement : result.tradingStatements()) {
|
||||
expectedValues.add(criterion.calculate(result.barSeries(), statement.getTradingRecord()));
|
||||
}
|
||||
List<Num> actualValues = executionResult.criterionValues(criterion);
|
||||
assertEquals(expectedValues, actualValues);
|
||||
assertEquals(expectedValues, result.criterionValues(criterion));
|
||||
|
||||
Map<Integer, Num> valuesByIndex = executionResult.criterionValuesByIndex(criterion);
|
||||
assertEquals(actualValues.size(), valuesByIndex.size());
|
||||
for (int i = 0; i < actualValues.size(); i++) {
|
||||
assertEquals(actualValues.get(i), valuesByIndex.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingNormalizesEquivalentMultipliersToSameScores() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterionOne = mappedCriterion(result, true, 9.0, 6.0, 3.0);
|
||||
MappedCriterion criterionTwo = mappedCriterion(result, true, 2.0, 5.0, 8.0);
|
||||
MappedCriterion criterionThree = mappedCriterion(result, false, 9.0, 5.0, 1.0);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile profileA = TradingStatementExecutionResult.RankingProfile.of(
|
||||
weightedCriterion(criterionOne, 1.5), weightedCriterion(criterionTwo, 1.1),
|
||||
weightedCriterion(criterionThree, 0.8));
|
||||
TradingStatementExecutionResult.RankingProfile profileB = TradingStatementExecutionResult.RankingProfile.of(
|
||||
weightedCriterion(criterionOne, 15.0), weightedCriterion(criterionTwo, 11.0),
|
||||
weightedCriterion(criterionThree, 8.0));
|
||||
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> rankedA = result.rankTradingStatements(profileA);
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> rankedB = result.rankTradingStatements(profileB);
|
||||
|
||||
assertEquals(rankedA.size(), rankedB.size());
|
||||
for (int i = 0; i < rankedA.size(); i++) {
|
||||
assertEquals(rankedA.get(i).originalIndex(), rankedB.get(i).originalIndex());
|
||||
assertEquals(rankedA.get(i).compositeScore().doubleValue(), rankedB.get(i).compositeScore().doubleValue(),
|
||||
1.0e-12);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedFactoriesAndConvenienceAliasesPreserveDefaultBehavior() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterionOne = mappedCriterion(result, true, 3.0, 2.0, 1.0);
|
||||
MappedCriterion criterionTwo = mappedCriterion(result, false, 1.0, 2.0, 3.0);
|
||||
|
||||
TradingStatementExecutionResult.WeightedCriterion equalWeight = TradingStatementExecutionResult.WeightedCriterion
|
||||
.of(criterionOne);
|
||||
TradingStatementExecutionResult.WeightedCriterion explicitWeight = TradingStatementExecutionResult.WeightedCriterion
|
||||
.of(criterionTwo, 2.5);
|
||||
|
||||
assertEquals(1.0, equalWeight.multiplier().doubleValue(), 1.0e-12);
|
||||
assertEquals(2.5, explicitWeight.multiplier().doubleValue(), 1.0e-12);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile expectedProfile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(equalWeight, explicitWeight);
|
||||
TradingStatementExecutionResult.RankingProfile actualProfile = TradingStatementExecutionResult.RankingProfile
|
||||
.weighted(equalWeight, explicitWeight);
|
||||
|
||||
assertEquals(expectedProfile, actualProfile);
|
||||
assertSame(TradingStatementExecutionResult.DirectionAwareMinMaxNormalizer.INSTANCE, actualProfile.normalizer());
|
||||
assertEquals(TradingStatementExecutionResult.MissingValuePolicy.WORST_SCORE,
|
||||
actualProfile.missingValuePolicy());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingHandlesMixedCriterionDirections() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion rewardCriterion = mappedCriterion(result, true, 10.0, 7.0, 4.0);
|
||||
MappedCriterion drawdownCriterion = mappedCriterion(result, false, 10.0, 5.0, 1.0);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile profile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(rewardCriterion, 1.0), weightedCriterion(drawdownCriterion, 1.0));
|
||||
|
||||
List<Integer> rankedIndexes = rankedIndexes(result.rankTradingStatements(profile));
|
||||
assertEquals(List.of(1, 0, 2), rankedIndexes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingUsesWorstScoreForMissingValuesByDefault() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion missingCriterion = mappedCriterion(result, true, 3.0, Double.NaN, 1.0);
|
||||
MappedCriterion baselineCriterion = mappedCriterion(result, true, 3.0, 2.0, 1.0);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile profile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(missingCriterion, 1.0), weightedCriterion(baselineCriterion, 1.0));
|
||||
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> ranked = result.rankTradingStatements(profile);
|
||||
assertEquals(List.of(0, 1, 2), rankedIndexes(ranked));
|
||||
assertEquals(3, ranked.size());
|
||||
|
||||
TradingStatementExecutionResult.RankedTradingStatement middle = ranked.get(1);
|
||||
assertEquals(1, middle.originalIndex());
|
||||
assertEquals(0.25, middle.compositeScore().doubleValue(), 1.0e-12);
|
||||
assertTrue(middle.rawScores().get(missingCriterion).isNaN());
|
||||
assertEquals(0.0, middle.normalizedScores().get(missingCriterion).doubleValue(), 1.0e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingZeroSpreadCriterionDoesNotChangeOrdering() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion zeroSpreadCriterion = mappedCriterion(result, true, 5.0, 5.0, 5.0);
|
||||
MappedCriterion variableCriterion = mappedCriterion(result, true, 1.0, 2.0, 3.0);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile withZeroSpread = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(zeroSpreadCriterion, 1.0), weightedCriterion(variableCriterion, 1.0));
|
||||
TradingStatementExecutionResult.RankingProfile variableOnly = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(variableCriterion, 1.0));
|
||||
|
||||
List<Integer> withZeroSpreadOrder = rankedIndexes(result.rankTradingStatements(withZeroSpread));
|
||||
List<Integer> variableOnlyOrder = rankedIndexes(result.rankTradingStatements(variableOnly));
|
||||
|
||||
assertEquals(variableOnlyOrder, withZeroSpreadOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingUsesStableOriginalIndexTieBreaks() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion tieCriterion = mappedCriterion(result, true, 2.0, 2.0, 2.0);
|
||||
TradingStatementExecutionResult.RankingProfile profile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(tieCriterion, 1.0));
|
||||
|
||||
List<Integer> rankedIndexes = rankedIndexes(result.rankTradingStatements(profile));
|
||||
assertEquals(List.of(0, 1, 2), rankedIndexes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingSupportsPluggableNormalizer() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterion = mappedCriterion(result, true, 3.0, 2.0, 1.0);
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile defaultProfile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(criterion, 1.0));
|
||||
TradingStatementExecutionResult.CriterionNormalizer inverseNormalizer = (analysisCriterion, rawValue, bestValue,
|
||||
worstValue, factory) -> factory.one()
|
||||
.minus(TradingStatementExecutionResult.DirectionAwareMinMaxNormalizer.INSTANCE
|
||||
.normalize(analysisCriterion, rawValue, bestValue, worstValue, factory));
|
||||
TradingStatementExecutionResult.RankingProfile invertedProfile = new TradingStatementExecutionResult.RankingProfile(
|
||||
List.of(weightedCriterion(criterion, 1.0)), inverseNormalizer,
|
||||
TradingStatementExecutionResult.MissingValuePolicy.WORST_SCORE);
|
||||
|
||||
assertEquals(List.of(0, 1, 2), rankedIndexes(result.rankTradingStatements(defaultProfile)));
|
||||
assertEquals(List.of(2, 1, 0), rankedIndexes(result.rankTradingStatements(invertedProfile)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingWorksForWalkForwardResultThroughSharedContract() {
|
||||
StrategyWalkForwardExecutionResult result = createWalkForwardResult();
|
||||
int statementCount = result.tradingStatements().size();
|
||||
assertTrue(statementCount >= 2);
|
||||
|
||||
double[] descendingValues = new double[statementCount];
|
||||
for (int i = 0; i < statementCount; i++) {
|
||||
descendingValues[i] = statementCount - i;
|
||||
}
|
||||
MappedCriterion criterion = mappedCriterion(result, true, descendingValues);
|
||||
TradingStatementExecutionResult.RankingProfile profile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(weightedCriterion(criterion, 1.0));
|
||||
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> ranked = result.rankTradingStatements(profile);
|
||||
assertFalse(ranked.isEmpty());
|
||||
assertEquals(0, ranked.get(0).originalIndex());
|
||||
|
||||
List<TradingStatement> top = result.topTradingStatements(2, profile);
|
||||
assertEquals(Math.min(2, statementCount), top.size());
|
||||
assertSame(result.tradingStatements().getFirst(), top.getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingConvenienceOverloadsMatchProfileBehavior() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterionOne = mappedCriterion(result, true, 4.0, 3.0, 1.0);
|
||||
MappedCriterion criterionTwo = mappedCriterion(result, false, 5.0, 4.0, 2.0);
|
||||
TradingStatementExecutionResult.WeightedCriterion weightedOne = TradingStatementExecutionResult.WeightedCriterion
|
||||
.of(criterionOne, 3.0);
|
||||
TradingStatementExecutionResult.WeightedCriterion weightedTwo = TradingStatementExecutionResult.WeightedCriterion
|
||||
.of(criterionTwo, 1.0);
|
||||
TradingStatementExecutionResult.RankingProfile profile = TradingStatementExecutionResult.RankingProfile
|
||||
.weighted(weightedOne, weightedTwo);
|
||||
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> rankedFromProfile = result
|
||||
.rankTradingStatements(profile);
|
||||
List<TradingStatementExecutionResult.RankedTradingStatement> rankedFromVarargs = result
|
||||
.rankTradingStatements(weightedOne, weightedTwo);
|
||||
assertEquals(rankedIndexes(rankedFromProfile), rankedIndexes(rankedFromVarargs));
|
||||
|
||||
List<TradingStatement> topFromProfile = result.topTradingStatements(2, profile);
|
||||
List<TradingStatement> topFromVarargs = result.topTradingStatements(2, weightedOne, weightedTwo);
|
||||
assertEquals(topFromProfile.size(), topFromVarargs.size());
|
||||
for (int i = 0; i < topFromProfile.size(); i++) {
|
||||
assertSame(topFromProfile.get(i), topFromVarargs.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backtestWeightedTopStrategiesAttachRawCriterionScores() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterionOne = mappedCriterion(result, true, 3.0, 2.0, 1.0);
|
||||
MappedCriterion criterionTwo = mappedCriterion(result, false, 5.0, 4.0, 1.0);
|
||||
|
||||
List<TradingStatement> topStatements = result.getTopStrategiesWeighted(2,
|
||||
TradingStatementExecutionResult.WeightedCriterion.of(criterionOne, 1.0),
|
||||
TradingStatementExecutionResult.WeightedCriterion.of(criterionTwo, 1.0));
|
||||
assertEquals(2, topStatements.size());
|
||||
for (TradingStatement statement : topStatements) {
|
||||
Num expectedOne = criterionOne.calculate(result.barSeries(), statement.getTradingRecord());
|
||||
Num expectedTwo = criterionTwo.calculate(result.barSeries(), statement.getTradingRecord());
|
||||
assertEquals(expectedOne, statement.getCriterionScore(criterionOne).orElseThrow());
|
||||
assertEquals(expectedTwo, statement.getCriterionScore(criterionTwo).orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedRankingValidatesInvalidInputs() {
|
||||
BacktestExecutionResult result = createBacktestResult();
|
||||
MappedCriterion criterionOne = mappedCriterion(result, true, 3.0, 2.0, 1.0);
|
||||
MappedCriterion criterionTwo = mappedCriterion(result, true, 1.0, 2.0, 3.0);
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> result.rankTradingStatements((TradingStatementExecutionResult.RankingProfile) null));
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> result.rankTradingStatements((TradingStatementExecutionResult.WeightedCriterion[]) null));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new TradingStatementExecutionResult.RankingProfile(List.of(), null, null));
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new TradingStatementExecutionResult.WeightedCriterion(null, numFactory.one()));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new TradingStatementExecutionResult.WeightedCriterion(criterionOne, numFactory.minusOne()));
|
||||
assertThrows(NullPointerException.class, () -> TradingStatementExecutionResult.RankingProfile
|
||||
.of((TradingStatementExecutionResult.WeightedCriterion[]) null));
|
||||
|
||||
TradingStatementExecutionResult.RankingProfile zeroWeightProfile = TradingStatementExecutionResult.RankingProfile
|
||||
.of(new TradingStatementExecutionResult.WeightedCriterion(criterionOne, numFactory.zero()),
|
||||
new TradingStatementExecutionResult.WeightedCriterion(criterionTwo, numFactory.zero()));
|
||||
assertThrows(IllegalArgumentException.class, () -> result.rankTradingStatements(zeroWeightProfile));
|
||||
}
|
||||
|
||||
private TradingStatementExecutionResult.WeightedCriterion weightedCriterion(AnalysisCriterion criterion,
|
||||
double multiplier) {
|
||||
return TradingStatementExecutionResult.WeightedCriterion.of(criterion, numOf(multiplier));
|
||||
}
|
||||
|
||||
private MappedCriterion mappedCriterion(TradingStatementExecutionResult<?> result, boolean higherIsBetter,
|
||||
double... values) {
|
||||
List<TradingRecord> records = result.tradingRecords();
|
||||
if (values.length != records.size()) {
|
||||
throw new IllegalArgumentException("values length must match trading record count");
|
||||
}
|
||||
Map<TradingRecord, Num> valuesByRecord = new IdentityHashMap<>(records.size());
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Num value = Double.isNaN(values[i]) ? NaN.NaN : result.barSeries().numFactory().numOf(values[i]);
|
||||
valuesByRecord.put(records.get(i), value);
|
||||
}
|
||||
return new MappedCriterion(valuesByRecord, result.barSeries().numFactory(), higherIsBetter);
|
||||
}
|
||||
|
||||
private List<Integer> rankedIndexes(List<TradingStatementExecutionResult.RankedTradingStatement> rankedStatements) {
|
||||
List<Integer> indexes = new ArrayList<>(rankedStatements.size());
|
||||
for (TradingStatementExecutionResult.RankedTradingStatement ranked : rankedStatements) {
|
||||
indexes.add(ranked.originalIndex());
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
|
||||
private BacktestExecutionResult createBacktestResult() {
|
||||
BarSeries series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 102, 104, 106, 108, 110, 112, 114, 116, 118)
|
||||
.build();
|
||||
Strategy strategyOne = new BaseStrategy("strategy-1", new FixedRule(0), new FixedRule(4));
|
||||
Strategy strategyTwo = new BaseStrategy("strategy-2", new FixedRule(1), new FixedRule(5));
|
||||
Strategy strategyThree = new BaseStrategy("strategy-3", new FixedRule(2), new FixedRule(6));
|
||||
BacktestExecutor executor = new BacktestExecutor(series);
|
||||
return executor.executeWithRuntimeReport(List.of(strategyOne, strategyTwo, strategyThree), numFactory.one());
|
||||
}
|
||||
|
||||
private StrategyWalkForwardExecutionResult createWalkForwardResult() {
|
||||
BarSeries series = buildSeries(48);
|
||||
Strategy strategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
|
||||
return executor.execute(strategy, walkForwardConfig());
|
||||
}
|
||||
|
||||
private BarSeries buildSeries(int bars) {
|
||||
double[] data = new double[bars];
|
||||
for (int i = 0; i < bars; i++) {
|
||||
data[i] = 100 + (i * 0.5);
|
||||
}
|
||||
return new MockBarSeriesBuilder().withNumFactory(numFactory).withData(data).build();
|
||||
}
|
||||
|
||||
private static WalkForwardConfig walkForwardConfig() {
|
||||
return new WalkForwardConfig(12, 6, 6, 0, 0, 6, 3, List.of(2), 1, List.of(1), 42L);
|
||||
}
|
||||
|
||||
private static final class MappedCriterion implements AnalysisCriterion {
|
||||
|
||||
private final Map<TradingRecord, Num> valuesByRecord;
|
||||
private final NumFactory numFactory;
|
||||
private final boolean higherIsBetter;
|
||||
|
||||
private MappedCriterion(Map<TradingRecord, Num> valuesByRecord, NumFactory numFactory, boolean higherIsBetter) {
|
||||
this.valuesByRecord = valuesByRecord;
|
||||
this.numFactory = numFactory;
|
||||
this.higherIsBetter = higherIsBetter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(BarSeries series, Position position) {
|
||||
return numFactory.zero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(BarSeries series, TradingRecord tradingRecord) {
|
||||
return valuesByRecord.getOrDefault(tradingRecord, NaN.NaN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean betterThan(Num criterionValue1, Num criterionValue2) {
|
||||
if (Num.isNaNOrNull(criterionValue1)) {
|
||||
return false;
|
||||
}
|
||||
if (Num.isNaNOrNull(criterionValue2)) {
|
||||
return true;
|
||||
}
|
||||
if (higherIsBetter) {
|
||||
return criterionValue1.isGreaterThan(criterionValue2);
|
||||
}
|
||||
return criterionValue1.isLessThan(criterionValue2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
|
||||
class AmountBarBuilderTest {
|
||||
|
||||
@Test
|
||||
void createBarsWithSetAmountByVolume() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(12, true))
|
||||
.build();
|
||||
final var now = Instant.now();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var bar = series.barBuilder().timePeriod(oneDay).endTime(now).closePrice(1).volume(1).trades(3);
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> bar.amount(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addWithSetAmountByVolume() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(12, true))
|
||||
.build();
|
||||
final var now = Instant.now();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now).closePrice(1).volume(1).trades(3).add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(1))).closePrice(2).volume(1).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(2)))
|
||||
.closePrice(5)
|
||||
.volume(1)
|
||||
.trades(7)
|
||||
.add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(3))).closePrice(4).volume(2).add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(4, bar1.getVolume());
|
||||
assertNumEquals(1, bar1.getOpenPrice());
|
||||
assertNumEquals(4, bar1.getClosePrice());
|
||||
assertNumEquals(5, bar1.getHighPrice());
|
||||
assertNumEquals(1, bar1.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(4), bar1.getTimePeriod());
|
||||
final var beginTime0 = now.minus(oneDay);
|
||||
final var endTime4 = now.plus(Duration.ofDays(3));
|
||||
assertEquals(beginTime0, bar1.getBeginTime());
|
||||
assertEquals(endTime4, bar1.getEndTime());
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
assertEquals(numFactory.numOf(12), bar1.getAmount());
|
||||
assertEquals(10, bar1.getTrades());
|
||||
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(4))).closePrice(2).volume(1).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(5)))
|
||||
.closePrice(3)
|
||||
.volume(1)
|
||||
.trades(5)
|
||||
.add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(6))).closePrice(6).volume(1).add();
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var bar2 = series.getBar(1);
|
||||
assertNumEquals(3.5, bar2.getVolume());
|
||||
assertNumEquals(2, bar2.getOpenPrice());
|
||||
assertNumEquals(6, bar2.getClosePrice());
|
||||
assertNumEquals(6, bar2.getHighPrice());
|
||||
assertNumEquals(2, bar2.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(3), bar2.getTimePeriod());
|
||||
final var beginTime5 = now.plus(Duration.ofDays(4)).minus(oneDay);
|
||||
final var endTime7 = now.plus(Duration.ofDays(6));
|
||||
assertEquals(beginTime5, bar2.getBeginTime());
|
||||
assertEquals(endTime7, bar2.getEndTime());
|
||||
assertEquals(numFactory.numOf(12), bar2.getAmount());
|
||||
assertEquals(5, bar2.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addWithoutSetAmountByVolume() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(12, false))
|
||||
.build();
|
||||
final var now = Instant.now();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now).closePrice(1).volume(1).amount(1).trades(3).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(1)))
|
||||
.closePrice(2)
|
||||
.volume(1)
|
||||
.amount(2)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(2)))
|
||||
.closePrice(5)
|
||||
.volume(1)
|
||||
.amount(5)
|
||||
.trades(7)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(3)))
|
||||
.closePrice(4)
|
||||
.volume(2)
|
||||
.amount(8)
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(4, bar1.getVolume());
|
||||
assertNumEquals(1, bar1.getOpenPrice());
|
||||
assertNumEquals(4, bar1.getClosePrice());
|
||||
assertNumEquals(5, bar1.getHighPrice());
|
||||
assertNumEquals(1, bar1.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(4), bar1.getTimePeriod());
|
||||
final var beginTime0 = now.minus(oneDay);
|
||||
final var endTime4 = now.plus(Duration.ofDays(3));
|
||||
assertEquals(beginTime0, bar1.getBeginTime());
|
||||
assertEquals(endTime4, bar1.getEndTime());
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
assertEquals(numFactory.numOf(12), bar1.getAmount());
|
||||
assertEquals(10, bar1.getTrades());
|
||||
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(4)))
|
||||
.closePrice(2)
|
||||
.volume(1)
|
||||
.amount(2)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(5)))
|
||||
.closePrice(3)
|
||||
.volume(1)
|
||||
.amount(3)
|
||||
.trades(5)
|
||||
.add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(6)))
|
||||
.closePrice(6)
|
||||
.volume(1)
|
||||
.amount(6)
|
||||
.add();
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var bar2 = series.getBar(1);
|
||||
assertNumEquals(3.5, bar2.getVolume());
|
||||
assertNumEquals(2, bar2.getOpenPrice());
|
||||
assertNumEquals(6, bar2.getClosePrice());
|
||||
assertNumEquals(6, bar2.getHighPrice());
|
||||
assertNumEquals(2, bar2.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(3), bar2.getTimePeriod());
|
||||
final var beginTime5 = now.plus(Duration.ofDays(4)).minus(oneDay);
|
||||
final var endTime7 = now.plus(Duration.ofDays(6));
|
||||
assertEquals(beginTime5, bar2.getBeginTime());
|
||||
assertEquals(endTime7, bar2.getEndTime());
|
||||
assertEquals(numFactory.numOf(12), bar2.getAmount());
|
||||
assertEquals(5, bar2.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeBuildsAmountBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder().addTrade(start, numFactory.two(), numFactory.two());
|
||||
series.barBuilder().addTrade(start.plusSeconds(10), numFactory.one(), numFactory.three());
|
||||
assertEquals(0, series.getBarCount());
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(20), numFactory.one(), numFactory.three());
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
final var bar = series.getBar(0);
|
||||
assertEquals(start, bar.getBeginTime());
|
||||
assertEquals(start.plusSeconds(20), bar.getEndTime());
|
||||
assertNumEquals(4, bar.getVolume());
|
||||
assertNumEquals(2, bar.getOpenPrice());
|
||||
assertNumEquals(3, bar.getClosePrice());
|
||||
assertNumEquals(3, bar.getHighPrice());
|
||||
assertNumEquals(2, bar.getLowPrice());
|
||||
assertEquals(numFactory.numOf(10), bar.getAmount());
|
||||
assertEquals(3, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeBuildsRealtimeAmountBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.two(), numFactory.two(), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(10), numFactory.one(), numFactory.three(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(20), numFactory.one(), numFactory.three(), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
Assertions.assertEquals(1, series.getBarCount());
|
||||
final var bar = (RealtimeBar) series.getBar(0);
|
||||
Assertions.assertTrue(bar.hasSideData());
|
||||
Assertions.assertTrue(bar.hasLiquidityData());
|
||||
assertNumEquals(3, bar.getBuyVolume());
|
||||
assertNumEquals(1, bar.getSellVolume());
|
||||
assertNumEquals(7, bar.getBuyAmount());
|
||||
assertNumEquals(3, bar.getSellAmount());
|
||||
Assertions.assertEquals(2, bar.getBuyTrades());
|
||||
Assertions.assertEquals(1, bar.getSellTrades());
|
||||
assertNumEquals(3, bar.getMakerVolume());
|
||||
assertNumEquals(1, bar.getTakerVolume());
|
||||
assertNumEquals(7, bar.getMakerAmount());
|
||||
assertNumEquals(3, bar.getTakerAmount());
|
||||
Assertions.assertEquals(2, bar.getMakerTrades());
|
||||
Assertions.assertEquals(1, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeRejectsSideDataWhenRealtimeDisabled() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
Assertions.assertThrows(IllegalStateException.class, () -> series.barBuilder()
|
||||
.addTrade(start, numFactory.one(), numFactory.two(), RealtimeBar.Side.BUY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeResetsSideAndLiquidityAcrossBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.one(), numFactory.numOf(5), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(10), numFactory.one(), numFactory.numOf(5), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(20), numFactory.one(), numFactory.numOf(5), null, null);
|
||||
series.barBuilder().addTrade(start.plusSeconds(30), numFactory.one(), numFactory.numOf(5), null, null);
|
||||
|
||||
Assertions.assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
Assertions.assertTrue(first.hasSideData());
|
||||
Assertions.assertTrue(first.hasLiquidityData());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertNumEquals(0, second.getBuyVolume());
|
||||
assertNumEquals(0, second.getSellVolume());
|
||||
assertNumEquals(0, second.getMakerVolume());
|
||||
assertNumEquals(0, second.getTakerVolume());
|
||||
Assertions.assertEquals(0, second.getBuyTrades());
|
||||
Assertions.assertEquals(0, second.getMakerTrades());
|
||||
Assertions.assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeCarriesAmountRemainderAcrossBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two(), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.three(), numFactory.two(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.two(), numFactory.two(), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.two(), numFactory.two(), null, null);
|
||||
|
||||
Assertions.assertEquals(2, series.getBarCount());
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
Assertions.assertFalse(second.hasSideData());
|
||||
Assertions.assertFalse(second.hasLiquidityData());
|
||||
assertNumEquals(5, second.getVolume());
|
||||
assertNumEquals(10, second.getAmount());
|
||||
Assertions.assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeCarriesSideAndLiquidityProportionally() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(
|
||||
new AmountBarBuilderFactory(10, true, true, RemainderCarryOverPolicy.PROPORTIONAL))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two(), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.three(), numFactory.two(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.two(), numFactory.two(), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.two(), numFactory.two(), null, null);
|
||||
|
||||
Assertions.assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertNumEquals(3, first.getBuyVolume());
|
||||
assertNumEquals(2, first.getSellVolume());
|
||||
assertNumEquals(6, first.getBuyAmount());
|
||||
assertNumEquals(4, first.getSellAmount());
|
||||
assertNumEquals(3, first.getMakerVolume());
|
||||
assertNumEquals(2, first.getTakerVolume());
|
||||
assertNumEquals(6, first.getMakerAmount());
|
||||
assertNumEquals(4, first.getTakerAmount());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
Assertions.assertTrue(second.hasSideData());
|
||||
Assertions.assertTrue(second.hasLiquidityData());
|
||||
assertNumEquals(0, second.getBuyVolume());
|
||||
assertNumEquals(1, second.getSellVolume());
|
||||
assertNumEquals(0, second.getBuyAmount());
|
||||
assertNumEquals(2, second.getSellAmount());
|
||||
assertNumEquals(0, second.getMakerVolume());
|
||||
assertNumEquals(1, second.getTakerVolume());
|
||||
assertNumEquals(0, second.getMakerAmount());
|
||||
assertNumEquals(2, second.getTakerAmount());
|
||||
assertNumEquals(5, second.getVolume());
|
||||
assertNumEquals(10, second.getAmount());
|
||||
Assertions.assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeCarriesTradeCountsProportionally() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(new AmountBarBuilderFactory(5, true, true,
|
||||
RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.two(), numFactory.two(), RealtimeBar.Side.BUY, RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.two(), numFactory.two(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.one(), numFactory.two(), null, null);
|
||||
|
||||
Assertions.assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
Assertions.assertEquals(1, first.getTrades());
|
||||
Assertions.assertEquals(1, first.getBuyTrades());
|
||||
Assertions.assertEquals(0, first.getSellTrades());
|
||||
Assertions.assertEquals(1, first.getMakerTrades());
|
||||
Assertions.assertEquals(0, first.getTakerTrades());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
Assertions.assertEquals(2, second.getTrades());
|
||||
Assertions.assertEquals(0, second.getBuyTrades());
|
||||
Assertions.assertEquals(1, second.getSellTrades());
|
||||
Assertions.assertEquals(0, second.getMakerTrades());
|
||||
Assertions.assertEquals(1, second.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeRejectsOutOfOrderTimestamp() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:10Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.one(), numFactory.two());
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addTrade(start.minusSeconds(1), numFactory.one(), numFactory.two()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeWithZeroPriceUsesLastTradePriceForRemainder() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two());
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.two(), numFactory.two());
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addWithZeroPriceWhenThresholdExceededUsesLastTradePrice() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
assertNumEquals(2, bar1.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeThenBuilderWithZeroPriceHandlesRemainderCorrectly() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two());
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.two(), numFactory.two());
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
assertNumEquals(2, bar1.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeRejectsNullTime() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
Assertions.assertThrows(NullPointerException.class,
|
||||
() -> builder.addTrade(null, numFactory.one(), numFactory.two()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeRejectsNullVolume() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
Assertions.assertThrows(NullPointerException.class, () -> builder.addTrade(start, null, numFactory.two()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addTradeRejectsNullPrice() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
Assertions.assertThrows(NullPointerException.class, () -> builder.addTrade(start, numFactory.one(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindToRejectsNullSeries() {
|
||||
final var builder = new AmountBarBuilder(10, true);
|
||||
Assertions.assertThrows(NullPointerException.class, () -> builder.bindTo(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void openPriceSetterThrowsException() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var builder = series.barBuilder();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.openPrice(numFactory.one()));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.openPrice(1));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.openPrice("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void highPriceSetterThrowsException() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var builder = series.barBuilder();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.highPrice(numFactory.one()));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.highPrice(1));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.highPrice("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowPriceSetterThrowsException() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var builder = series.barBuilder();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.lowPrice(numFactory.one()));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.lowPrice(1));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.lowPrice("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void amountSetterThrowsWhenSetAmountByVolumeIsTrue() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var builder = series.barBuilder();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.amount(numFactory.one()));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.amount(1));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> builder.amount("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactThresholdMatchCreatesBar() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertNumEquals(10, bar.getAmount());
|
||||
assertNumEquals(5, bar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void priceTrackingHighLow() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(20, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.numOf(5)).volume(numFactory.one()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.numOf(10))
|
||||
.volume(numFactory.one())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.numOf(3))
|
||||
.volume(numFactory.one())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(30))
|
||||
.closePrice(numFactory.numOf(15))
|
||||
.volume(numFactory.one())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertNumEquals(5, bar.getOpenPrice());
|
||||
assertNumEquals(15, bar.getHighPrice());
|
||||
assertNumEquals(3, bar.getLowPrice());
|
||||
assertNumEquals(15, bar.getClosePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void timePeriodAccumulation() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(20, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.three())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(30))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertEquals(oneDay.multipliedBy(4), bar.getTimePeriod());
|
||||
}
|
||||
|
||||
@Test
|
||||
void volumeAccumulationAcrossMultipleAdds() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(20, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.one()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.three())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(30))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.numOf(4))
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertNumEquals(10, bar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleConsecutiveBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.three())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(30))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
assertEquals(2, series.getBarCount());
|
||||
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(40))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.three())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(50))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
assertEquals(3, series.getBarCount());
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final var bar = series.getBar(i);
|
||||
assertNumEquals(10, bar.getAmount());
|
||||
assertNumEquals(5, bar.getVolume());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void remainderCalculationAccuracy() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(30))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.one())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(40))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var bar2 = series.getBar(1);
|
||||
assertNumEquals(10, bar2.getAmount());
|
||||
assertNumEquals(5, bar2.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void builderApiWithNumberAndStringOverloads() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, false))
|
||||
.build();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(2).volume(3).amount(6).add();
|
||||
builder.timePeriod(oneDay).endTime(start.plusSeconds(10)).closePrice("2").volume("2").amount("4").add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertNumEquals(10, bar.getAmount());
|
||||
assertNumEquals(5, bar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tradesStringOverload() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start)
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.three())
|
||||
.trades("3")
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.trades("2")
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertEquals(5, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildMethodReturnsRealtimeBarWhenEnabled() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.addTrade(start, numFactory.one(), numFactory.two(), RealtimeBar.Side.BUY, null);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.one(), numFactory.two(), RealtimeBar.Side.SELL, null);
|
||||
|
||||
final var bar = builder.build();
|
||||
Assertions.assertInstanceOf(org.ta4j.core.BaseRealtimeBar.class, bar);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindToMethodBindsBuilderToSeries() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var builder = series.barBuilder();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroPriceTradeWithValidLastTradePrice() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two());
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.two(), numFactory.two());
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void carryOverPolicyNoneDoesNotCarrySideData() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(new AmountBarBuilderFactory(10, true, true, RemainderCarryOverPolicy.NONE))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two(), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.three(), numFactory.two(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.two(), numFactory.two(), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.two(), numFactory.two(), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
Assertions.assertFalse(second.hasSideData());
|
||||
Assertions.assertFalse(second.hasLiquidityData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void divisionByZeroFixWithDoubleNumFactory() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build();
|
||||
final var numFactory = DoubleNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two());
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.two(), numFactory.two());
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void remainderCalculationWithDoubleNumFactory() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build();
|
||||
final var numFactory = DoubleNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(20))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(10, bar1.getAmount());
|
||||
assertNumEquals(5, bar1.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void thresholdExceedanceWithDoubleNumFactory() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new AmountBarBuilderFactory(10, true))
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build();
|
||||
final var numFactory = DoubleNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
final var builder = series.barBuilder();
|
||||
builder.timePeriod(oneDay).endTime(start).closePrice(numFactory.two()).volume(numFactory.three()).add();
|
||||
builder.timePeriod(oneDay)
|
||||
.endTime(start.plusSeconds(10))
|
||||
.closePrice(numFactory.two())
|
||||
.volume(numFactory.two())
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = series.getBar(0);
|
||||
assertNumEquals(10, bar.getAmount());
|
||||
assertNumEquals(5, bar.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void carryOverProportionalWithDoubleNumFactory() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(
|
||||
new AmountBarBuilderFactory(10, true, true, RemainderCarryOverPolicy.PROPORTIONAL))
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build();
|
||||
final var numFactory = DoubleNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.three(), numFactory.two(), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.three(), numFactory.two(), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.two(), numFactory.two(), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.two(), numFactory.two(), null, null);
|
||||
|
||||
Assertions.assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertNumEquals(3, first.getBuyVolume());
|
||||
assertNumEquals(2, first.getSellVolume());
|
||||
assertNumEquals(6, first.getBuyAmount());
|
||||
assertNumEquals(4, first.getSellAmount());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
Assertions.assertTrue(second.hasSideData());
|
||||
Assertions.assertTrue(second.hasLiquidityData());
|
||||
assertNumEquals(0, second.getBuyVolume());
|
||||
assertNumEquals(1, second.getSellVolume());
|
||||
assertNumEquals(0, second.getBuyAmount());
|
||||
assertNumEquals(2, second.getSellAmount());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.indicators.AbstractIndicatorTest;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class HeikinAshiBarBuilderTest extends AbstractIndicatorTest<BarSeries, Num> {
|
||||
|
||||
public HeikinAshiBarBuilderTest(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
private final HeikinAshiBarBuilder unit = new HeikinAshiBarBuilder(numFactory);
|
||||
|
||||
@Test
|
||||
public void testBuild() {
|
||||
var inputBar = new BaseBar(Duration.ofHours(1), null, Instant.parse("2024-01-01T01:00:00Z"),
|
||||
numFactory.numOf(100), numFactory.numOf(110), numFactory.numOf(95), numFactory.numOf(105),
|
||||
numFactory.numOf(10), numFactory.numOf(1000), 1);
|
||||
|
||||
// No previous HA data: should return bar as-is.
|
||||
var resultBar = unit.timePeriod(inputBar.getTimePeriod())
|
||||
.endTime(inputBar.getEndTime())
|
||||
.openPrice(inputBar.getOpenPrice())
|
||||
.highPrice(inputBar.getHighPrice())
|
||||
.lowPrice(inputBar.getLowPrice())
|
||||
.closePrice(inputBar.getClosePrice())
|
||||
.volume(inputBar.getVolume())
|
||||
.amount(inputBar.getAmount())
|
||||
.trades(inputBar.getTrades())
|
||||
.build();
|
||||
|
||||
assertEquals(inputBar.getOpenPrice(), resultBar.getOpenPrice());
|
||||
assertEquals(inputBar.getHighPrice(), resultBar.getHighPrice());
|
||||
assertEquals(inputBar.getLowPrice(), resultBar.getLowPrice());
|
||||
assertEquals(inputBar.getClosePrice(), resultBar.getClosePrice());
|
||||
|
||||
// Setup for second bar with previous HA data
|
||||
var builderWithPrevious = new HeikinAshiBarBuilder().previousHeikinAshiOpenPrice(numFactory.numOf(100))
|
||||
.previousHeikinAshiClosePrice(numFactory.numOf(105))
|
||||
.timePeriod(inputBar.getTimePeriod())
|
||||
.endTime(inputBar.getEndTime())
|
||||
.openPrice(inputBar.getOpenPrice())
|
||||
.highPrice(inputBar.getHighPrice())
|
||||
.lowPrice(inputBar.getLowPrice())
|
||||
.closePrice(inputBar.getClosePrice())
|
||||
.volume(inputBar.getVolume())
|
||||
.amount(inputBar.getAmount())
|
||||
.trades(inputBar.getTrades());
|
||||
|
||||
var haBar = builderWithPrevious.build();
|
||||
|
||||
// Heikin-Ashi formula checks
|
||||
var haCloseExpected = (numFactory.numOf(100)
|
||||
.plus(numFactory.numOf(110))
|
||||
.plus(numFactory.numOf(95))
|
||||
.plus(numFactory.numOf(105))).dividedBy(numFactory.numOf(4));
|
||||
var haOpenExpected = (numFactory.numOf(100).plus(numFactory.numOf(105))).dividedBy(numFactory.numOf(2));
|
||||
|
||||
assertEquals(haOpenExpected, haBar.getOpenPrice());
|
||||
assertEquals(haCloseExpected, haBar.getClosePrice());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
|
||||
public class TickBarBuilderTest {
|
||||
|
||||
@Test
|
||||
public void add() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(5)).build();
|
||||
final var now = Instant.now();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now).closePrice(1).volume(1).add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(1))).closePrice(2).volume(1).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(2)))
|
||||
.closePrice(5)
|
||||
.volume(1)
|
||||
.trades(9)
|
||||
.add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(3))).closePrice(1).volume(1).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(4)))
|
||||
.closePrice(4)
|
||||
.volume(2)
|
||||
.trades(1)
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(6, bar1.getVolume());
|
||||
assertNumEquals(1, bar1.getOpenPrice());
|
||||
assertNumEquals(4, bar1.getClosePrice());
|
||||
assertNumEquals(5, bar1.getHighPrice());
|
||||
assertNumEquals(1, bar1.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(5), bar1.getTimePeriod());
|
||||
final var beginTime0 = now.minus(oneDay);
|
||||
final var endTime4 = now.plus(Duration.ofDays(4));
|
||||
assertEquals(beginTime0, bar1.getBeginTime());
|
||||
assertEquals(endTime4, bar1.getEndTime());
|
||||
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
assertEquals(numFactory.numOf(24), bar1.getAmount());
|
||||
assertEquals(10, bar1.getTrades());
|
||||
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(5)))
|
||||
.closePrice(2)
|
||||
.volume(1)
|
||||
.amount(24)
|
||||
.add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(6))).closePrice(3).volume(1).add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(7))).closePrice(6).volume(2).add();
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(8))).closePrice(2).volume(1).add();
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(9)))
|
||||
.closePrice(5)
|
||||
.volume(2)
|
||||
.trades(100)
|
||||
.add();
|
||||
assertEquals(2, series.getBarCount());
|
||||
|
||||
final var bar2 = series.getBar(1);
|
||||
assertNumEquals(7, bar2.getVolume());
|
||||
assertNumEquals(2, bar2.getOpenPrice());
|
||||
assertNumEquals(5, bar2.getClosePrice());
|
||||
assertNumEquals(6, bar2.getHighPrice());
|
||||
assertNumEquals(2, bar2.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(5), bar1.getTimePeriod());
|
||||
final var beginTime5 = now.plus(Duration.ofDays(5)).minus(oneDay);
|
||||
final var endTime9 = now.plus(Duration.ofDays(9));
|
||||
assertEquals(beginTime5, bar2.getBeginTime());
|
||||
assertEquals(endTime9, bar2.getEndTime());
|
||||
assertEquals(numFactory.numOf(24), bar2.getAmount());
|
||||
assertEquals(100, bar2.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeBuildsTickBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(2)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder().addTrade(start, numFactory.numOf(1), numFactory.numOf(100));
|
||||
assertEquals(0, series.getBarCount());
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(30), numFactory.numOf(2), numFactory.numOf(110));
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
final var bar = series.getBar(0);
|
||||
assertEquals(start, bar.getBeginTime());
|
||||
assertEquals(start.plusSeconds(30), bar.getEndTime());
|
||||
assertNumEquals(3, bar.getVolume());
|
||||
assertNumEquals(100, bar.getOpenPrice());
|
||||
assertNumEquals(110, bar.getClosePrice());
|
||||
assertEquals(2, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeBuildsRealtimeTickBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(2, true)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(100), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(30), numFactory.numOf(2), numFactory.numOf(90), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = (RealtimeBar) series.getBar(0);
|
||||
assertTrue(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertNumEquals(1, bar.getBuyVolume());
|
||||
assertNumEquals(2, bar.getSellVolume());
|
||||
assertNumEquals(100, bar.getBuyAmount());
|
||||
assertNumEquals(180, bar.getSellAmount());
|
||||
assertEquals(1, bar.getBuyTrades());
|
||||
assertEquals(1, bar.getSellTrades());
|
||||
assertNumEquals(2, bar.getMakerVolume());
|
||||
assertNumEquals(1, bar.getTakerVolume());
|
||||
assertNumEquals(180, bar.getMakerAmount());
|
||||
assertNumEquals(100, bar.getTakerAmount());
|
||||
assertEquals(1, bar.getMakerTrades());
|
||||
assertEquals(1, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeRejectsSideDataWhenRealtimeDisabled() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(2)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(100), RealtimeBar.Side.BUY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeResetsSideAndLiquidityAcrossBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(2, true)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(100), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(10), numFactory.numOf(1), numFactory.numOf(110), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(120), null, null);
|
||||
series.barBuilder().addTrade(start.plusSeconds(30), numFactory.numOf(1), numFactory.numOf(130), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertTrue(first.hasSideData());
|
||||
assertTrue(first.hasLiquidityData());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertEquals(numFactory.zero(), second.getBuyVolume());
|
||||
assertEquals(numFactory.zero(), second.getSellVolume());
|
||||
assertEquals(numFactory.zero(), second.getMakerVolume());
|
||||
assertEquals(numFactory.zero(), second.getTakerVolume());
|
||||
assertEquals(0, second.getBuyTrades());
|
||||
assertEquals(0, second.getMakerTrades());
|
||||
assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeRejectsOutOfOrderTimestamp() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new TickBarBuilderFactory(2)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:10Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.numOf(1), numFactory.numOf(100));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addTrade(start.minusSeconds(1), numFactory.numOf(1), numFactory.numOf(90)));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
|
||||
public class VolumeBarBuilderTest {
|
||||
|
||||
@Test
|
||||
public void add() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(4)).build();
|
||||
final var now = Instant.now();
|
||||
final var oneDay = Duration.ofDays(1);
|
||||
|
||||
// add bar 1: aggregated volume = 1
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now).closePrice(1).volume(1).trades(3).add();
|
||||
|
||||
// add bar 2: aggregated volume = 1 + 1 = 2
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(1))).closePrice(2).volume(1).add();
|
||||
|
||||
// add bar 3: aggregated volume = 1 + 1 + 1 = 3
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(2)))
|
||||
.closePrice(5)
|
||||
.volume(1)
|
||||
.trades(7)
|
||||
.add();
|
||||
|
||||
// add bar 4: aggregated volume = 1 + 1 + 1 + 2= 5
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(3)))
|
||||
.closePrice(4)
|
||||
.volume(2) // sum is 5 and 1 moved to next bar (= remainder)
|
||||
.add();
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar1 = series.getBar(0);
|
||||
assertNumEquals(4, bar1.getVolume());
|
||||
assertNumEquals(1, bar1.getOpenPrice());
|
||||
assertNumEquals(4, bar1.getClosePrice());
|
||||
assertNumEquals(5, bar1.getHighPrice());
|
||||
assertNumEquals(1, bar1.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(4), bar1.getTimePeriod());
|
||||
final var beginTime0 = now.minus(oneDay);
|
||||
final var endTime4 = now.plus(Duration.ofDays(3));
|
||||
assertEquals(beginTime0, bar1.getBeginTime());
|
||||
assertEquals(endTime4, bar1.getEndTime());
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
assertEquals(numFactory.numOf(16), bar1.getAmount()); // 1 * 1 + 1 * 2 + 1 * 5 + 2 * 4 = 16
|
||||
assertEquals(10, bar1.getTrades());
|
||||
|
||||
// add bar 5: aggregated volume = 1 + 1= 2
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(4)))
|
||||
.closePrice(2)
|
||||
.volume(1)
|
||||
.amount(12)
|
||||
.add();
|
||||
|
||||
// add bar 6: aggregated volume = 1 + 1 + 1= 3
|
||||
series.barBuilder()
|
||||
.timePeriod(oneDay)
|
||||
.endTime(now.plus(Duration.ofDays(5)))
|
||||
.closePrice(3)
|
||||
.volume(1)
|
||||
.trades(5)
|
||||
.add();
|
||||
|
||||
// add bar 7: aggregated volume = 1 + 1 + 1+ 1 = 4
|
||||
series.barBuilder().timePeriod(oneDay).endTime(now.plus(Duration.ofDays(6))).closePrice(6).volume(1).add();
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var bar2 = series.getBar(1);
|
||||
assertNumEquals(4, bar2.getVolume());
|
||||
assertNumEquals(2, bar2.getOpenPrice());
|
||||
assertNumEquals(6, bar2.getClosePrice());
|
||||
assertNumEquals(6, bar2.getHighPrice());
|
||||
assertNumEquals(2, bar2.getLowPrice());
|
||||
assertEquals(oneDay.multipliedBy(3), bar2.getTimePeriod());
|
||||
final var beginTime5 = now.plus(Duration.ofDays(4)).minus(oneDay);
|
||||
final var endTime7 = now.plus(Duration.ofDays(6));
|
||||
assertEquals(beginTime5, bar2.getBeginTime());
|
||||
assertEquals(endTime7, bar2.getEndTime());
|
||||
assertEquals(numFactory.numOf(12), bar2.getAmount());
|
||||
assertEquals(5, bar2.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeBuildsVolumeBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder().addTrade(start, numFactory.numOf(1), numFactory.numOf(10));
|
||||
series.barBuilder().addTrade(start.plusSeconds(10), numFactory.numOf(1), numFactory.numOf(12));
|
||||
assertEquals(0, series.getBarCount());
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(11));
|
||||
assertEquals(1, series.getBarCount());
|
||||
|
||||
final var bar = series.getBar(0);
|
||||
assertEquals(start, bar.getBeginTime());
|
||||
assertEquals(start.plusSeconds(20), bar.getEndTime());
|
||||
assertNumEquals(3, bar.getVolume());
|
||||
assertNumEquals(10, bar.getOpenPrice());
|
||||
assertNumEquals(12, bar.getHighPrice());
|
||||
assertNumEquals(10, bar.getLowPrice());
|
||||
assertNumEquals(11, bar.getClosePrice());
|
||||
assertEquals(numFactory.numOf(33), bar.getAmount());
|
||||
assertEquals(3, bar.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeBuildsRealtimeVolumeBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(10), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(10), numFactory.numOf(1), numFactory.numOf(12), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(11), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
assertEquals(1, series.getBarCount());
|
||||
final var bar = (RealtimeBar) series.getBar(0);
|
||||
assertTrue(bar.hasSideData());
|
||||
assertTrue(bar.hasLiquidityData());
|
||||
assertNumEquals(2, bar.getBuyVolume());
|
||||
assertNumEquals(1, bar.getSellVolume());
|
||||
assertNumEquals(21, bar.getBuyAmount());
|
||||
assertNumEquals(12, bar.getSellAmount());
|
||||
assertEquals(2, bar.getBuyTrades());
|
||||
assertEquals(1, bar.getSellTrades());
|
||||
assertNumEquals(2, bar.getMakerVolume());
|
||||
assertNumEquals(1, bar.getTakerVolume());
|
||||
assertNumEquals(21, bar.getMakerAmount());
|
||||
assertNumEquals(12, bar.getTakerAmount());
|
||||
assertEquals(2, bar.getMakerTrades());
|
||||
assertEquals(1, bar.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeRejectsSideDataWhenRealtimeDisabled() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(10), RealtimeBar.Side.BUY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeResetsSideAndLiquidityAcrossBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
|
||||
series.barBuilder()
|
||||
.addTrade(start, numFactory.numOf(1), numFactory.numOf(10), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(10), numFactory.numOf(1), numFactory.numOf(12), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
series.barBuilder()
|
||||
.addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(11), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
|
||||
series.barBuilder().addTrade(start.plusSeconds(30), numFactory.numOf(1), numFactory.numOf(13), null, null);
|
||||
series.barBuilder().addTrade(start.plusSeconds(40), numFactory.numOf(1), numFactory.numOf(14), null, null);
|
||||
series.barBuilder().addTrade(start.plusSeconds(50), numFactory.numOf(1), numFactory.numOf(15), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertTrue(first.hasSideData());
|
||||
assertTrue(first.hasLiquidityData());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertEquals(numFactory.zero(), second.getBuyVolume());
|
||||
assertEquals(numFactory.zero(), second.getSellVolume());
|
||||
assertEquals(numFactory.zero(), second.getMakerVolume());
|
||||
assertEquals(numFactory.zero(), second.getTakerVolume());
|
||||
assertEquals(0, second.getBuyTrades());
|
||||
assertEquals(0, second.getMakerTrades());
|
||||
assertEquals(3, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeCarriesVolumeRemainderAcrossBars() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3, true))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertFalse(second.hasSideData());
|
||||
assertFalse(second.hasLiquidityData());
|
||||
assertNumEquals(3, second.getVolume());
|
||||
assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeCarriesSideAndLiquidityProportionally() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(new VolumeBarBuilderFactory(3, true, RemainderCarryOverPolicy.PROPORTIONAL))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertNumEquals(2, first.getBuyVolume());
|
||||
assertNumEquals(1, first.getSellVolume());
|
||||
assertNumEquals(20, first.getBuyAmount());
|
||||
assertNumEquals(10, first.getSellAmount());
|
||||
assertNumEquals(2, first.getMakerVolume());
|
||||
assertNumEquals(1, first.getTakerVolume());
|
||||
assertNumEquals(20, first.getMakerAmount());
|
||||
assertNumEquals(10, first.getTakerAmount());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertTrue(second.hasSideData());
|
||||
assertTrue(second.hasLiquidityData());
|
||||
assertNumEquals(0, second.getBuyVolume());
|
||||
assertNumEquals(1, second.getSellVolume());
|
||||
assertNumEquals(0, second.getBuyAmount());
|
||||
assertNumEquals(10, second.getSellAmount());
|
||||
assertNumEquals(0, second.getMakerVolume());
|
||||
assertNumEquals(1, second.getTakerVolume());
|
||||
assertNumEquals(0, second.getMakerAmount());
|
||||
assertNumEquals(10, second.getTakerAmount());
|
||||
assertNumEquals(3, second.getVolume());
|
||||
assertEquals(2, second.getTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeCarriesTradeCountsProportionally() {
|
||||
final var series = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(
|
||||
new VolumeBarBuilderFactory(3, true, RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT))
|
||||
.build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.BUY,
|
||||
RealtimeBar.Liquidity.MAKER);
|
||||
builder.addTrade(start.plusSeconds(10), numFactory.numOf(2), numFactory.numOf(10), RealtimeBar.Side.SELL,
|
||||
RealtimeBar.Liquidity.TAKER);
|
||||
|
||||
builder.addTrade(start.plusSeconds(20), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
builder.addTrade(start.plusSeconds(30), numFactory.numOf(1), numFactory.numOf(10), null, null);
|
||||
|
||||
assertEquals(2, series.getBarCount());
|
||||
final var first = (RealtimeBar) series.getBar(0);
|
||||
assertEquals(1, first.getTrades());
|
||||
assertEquals(1, first.getBuyTrades());
|
||||
assertEquals(0, first.getSellTrades());
|
||||
assertEquals(1, first.getMakerTrades());
|
||||
assertEquals(0, first.getTakerTrades());
|
||||
|
||||
final var second = (RealtimeBar) series.getBar(1);
|
||||
assertEquals(3, second.getTrades());
|
||||
assertEquals(0, second.getBuyTrades());
|
||||
assertEquals(1, second.getSellTrades());
|
||||
assertEquals(0, second.getMakerTrades());
|
||||
assertEquals(1, second.getTakerTrades());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTradeRejectsOutOfOrderTimestamp() {
|
||||
final var series = new BaseBarSeriesBuilder().withBarBuilderFactory(new VolumeBarBuilderFactory(3)).build();
|
||||
final var numFactory = DecimalNumFactory.getInstance();
|
||||
final var start = Instant.parse("2024-01-01T00:00:10Z");
|
||||
final var builder = series.barBuilder();
|
||||
|
||||
builder.addTrade(start, numFactory.numOf(1), numFactory.numOf(10));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addTrade(start.minusSeconds(1), numFactory.numOf(1), numFactory.numOf(10)));
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseStrategy;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.backtest.BarSeriesManager;
|
||||
import org.ta4j.core.backtest.TradeOnCurrentCloseModel;
|
||||
import org.ta4j.core.criteria.drawdown.ReturnOverMaxDrawdownCriterion;
|
||||
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.rules.BooleanRule;
|
||||
import org.ta4j.core.rules.FixedRule;
|
||||
|
||||
public class AbstractAnalysisCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
private Strategy alwaysStrategy;
|
||||
|
||||
private Strategy buyAndHoldStrategy;
|
||||
|
||||
private List<Strategy> strategies;
|
||||
|
||||
public AbstractAnalysisCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new GrossReturnCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
alwaysStrategy = new BaseStrategy(BooleanRule.TRUE, BooleanRule.TRUE);
|
||||
buyAndHoldStrategy = new BaseStrategy(new FixedRule(0), new FixedRule(4));
|
||||
strategies = new ArrayList<>();
|
||||
strategies.add(alwaysStrategy);
|
||||
strategies.add(buyAndHoldStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bestShouldBeAlwaysOperateOnProfit() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(6.0, 9.0, 6.0, 6.0).build();
|
||||
var manager = new BarSeriesManager(series);
|
||||
Strategy bestStrategy = getCriterion().chooseBest(manager, TradeType.BUY, strategies);
|
||||
assertEquals(alwaysStrategy, bestStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bestShouldBeBuyAndHoldOnLoss() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(6.0, 3.0, 6.0, 6.0).build();
|
||||
var manager = new BarSeriesManager(series, new TradeOnCurrentCloseModel());
|
||||
Strategy bestStrategy = getCriterion().chooseBest(manager, TradeType.BUY, strategies);
|
||||
assertEquals(buyAndHoldStrategy, bestStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringMethod() {
|
||||
AbstractAnalysisCriterion c1 = new AverageReturnPerBarCriterion();
|
||||
assertEquals("Average Return Per Bar", c1.toString());
|
||||
AbstractAnalysisCriterion c2 = new EnterAndHoldCriterion(new GrossReturnCriterion());
|
||||
assertEquals("EnterAndHoldCriterion of GrossReturnCriterion", c2.toString());
|
||||
AbstractAnalysisCriterion c3 = new ReturnOverMaxDrawdownCriterion();
|
||||
assertEquals("Return Over Max Drawdown", c3.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.ta4j.core.*;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public abstract class AbstractCriterionTest {
|
||||
|
||||
protected final NumFactory numFactory;
|
||||
protected final OpenedPositionUtils openedPositionUtils = new OpenedPositionUtils();
|
||||
private final CriterionFactory factory;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param factory CriterionFactory for building an AnalysisCriterion given
|
||||
* parameters
|
||||
*/
|
||||
public AbstractCriterionTest(CriterionFactory factory, NumFactory numFactory) {
|
||||
this.factory = factory;
|
||||
this.numFactory = numFactory;
|
||||
}
|
||||
|
||||
@Parameterized.Parameters(name = "Test Case: {index} (0=DoubleNum, 1=DecimalNum)")
|
||||
public static List<NumFactory> function() {
|
||||
return List.of(DoubleNumFactory.getInstance(), DecimalNumFactory.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an AnalysisCriterion given criterion parameters.
|
||||
*
|
||||
* @param params criterion parameters
|
||||
* @return AnalysisCriterion given parameters
|
||||
*/
|
||||
public AnalysisCriterion getCriterion(Object... params) {
|
||||
return factory.getCriterion(params);
|
||||
}
|
||||
|
||||
public Num numOf(Number n) {
|
||||
return numFactory.numOf(n);
|
||||
}
|
||||
|
||||
public BarSeries getBarSeries(String name) {
|
||||
return new BaseBarSeriesBuilder().withNumFactory(numFactory).withName(name).build();
|
||||
}
|
||||
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class AverageReturnPerBarCriterionTest extends AbstractCriterionTest {
|
||||
private BarSeries series;
|
||||
|
||||
public AverageReturnPerBarCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new AverageReturnPerBarCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithGainPositions() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 105d, 110d, 100d, 95d, 105d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
AnalysisCriterion averageProfit = getCriterion();
|
||||
assertNumEquals(1.0243, averageProfit.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithASimplePosition() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 105d, 110d, 100d, 95d, 105d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
AnalysisCriterion averageProfit = getCriterion();
|
||||
assertNumEquals(numOf(110d / 100).pow(numOf(1d / 3)), averageProfit.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithLossPositions() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
AnalysisCriterion averageProfit = getCriterion();
|
||||
assertNumEquals(numOf(95d / 100 * 70d / 100).pow(numOf(1d / 6)),
|
||||
averageProfit.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithLosingAShortPositions() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 105d, 110d, 100d, 95d, 105d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(2, series));
|
||||
AnalysisCriterion averageProfit = getCriterion();
|
||||
assertNumEquals(numOf(90d / 100).pow(numOf(1d / 3)), averageProfit.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoBarsShouldReturn1() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
AnalysisCriterion averageProfit = getCriterion();
|
||||
assertNumEquals(1, averageProfit.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoBarsShouldReturnZeroRateOfReturn() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
AnalysisCriterion averageProfit = new AverageReturnPerBarCriterion(ReturnRepresentation.DECIMAL);
|
||||
assertNumEquals(0, averageProfit.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
AnalysisCriterion averageMultiplicative = new AverageReturnPerBarCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
assertNumEquals(1, averageMultiplicative.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
AnalysisCriterion averagePercentage = new AverageReturnPerBarCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
assertNumEquals(0, averagePercentage.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOnePosition() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105).build();
|
||||
Position position = new Position(Trade.buyAt(0, series), Trade.sellAt(1, series));
|
||||
AnalysisCriterion average = getCriterion();
|
||||
assertNumEquals(numOf(105d / 100).pow(numOf(0.5)), average.calculate(series, position));
|
||||
|
||||
AnalysisCriterion averageDecimal = new AverageReturnPerBarCriterion(ReturnRepresentation.DECIMAL);
|
||||
Num expectedDecimal = numOf(105d / 100).pow(numOf(0.5)).minus(numFactory.one());
|
||||
assertNumEquals(expectedDecimal, averageDecimal.calculate(series, position));
|
||||
|
||||
AnalysisCriterion averagePercentage = new AverageReturnPerBarCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
Num expectedPercentage = expectedDecimal.multipliedBy(numFactory.numOf(100));
|
||||
assertNumEquals(expectedPercentage, averagePercentage.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateRateOfReturnRepresentation() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 110, 100, 95, 105).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
AnalysisCriterion averageProfit = new AverageReturnPerBarCriterion(ReturnRepresentation.DECIMAL);
|
||||
Num expected = numOf(110d / 100).pow(numOf(1d / 3)).minus(numFactory.one());
|
||||
assertNumEquals(expected, averageProfit.calculate(series, tradingRecord));
|
||||
|
||||
AnalysisCriterion averageMultiplicative = new AverageReturnPerBarCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
Num expectedMultiplicative = numOf(110d / 100).pow(numOf(1d / 3));
|
||||
assertNumEquals(expectedMultiplicative, averageMultiplicative.calculate(series, tradingRecord));
|
||||
|
||||
AnalysisCriterion averagePercentage = new AverageReturnPerBarCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
Num expectedPercentage = expected.multipliedBy(numFactory.numOf(100));
|
||||
assertNumEquals(expectedPercentage, averagePercentage.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(2.0), numOf(1.5)));
|
||||
assertFalse(criterion.betterThan(numOf(1.5), numOf(2.0)));
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.alwaysInvested;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.EquityCurveMode;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.utils.TimeConstants;
|
||||
|
||||
public class CalmarRatioCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public CalmarRatioCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new CalmarRatioCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForMixedReturnsTradingRecord() {
|
||||
double[] closes = new double[] { 100d, 80d, 120d };
|
||||
BarSeries series = buildYearlySeries("calmar_mixed", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsPercentageRepresentation() {
|
||||
double[] closes = new double[] { 100d, 80d, 120d };
|
||||
BarSeries series = buildYearlySeries("calmar_percentage", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
CalmarRatioCriterion criterion = new CalmarRatioCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected * 100d), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMultiplicativeRepresentation() {
|
||||
double[] closes = new double[] { 100d, 80d, 120d };
|
||||
BarSeries series = buildYearlySeries("calmar_multiplicative", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
CalmarRatioCriterion criterion = new CalmarRatioCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(1d + expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForPositiveReturnsTradingRecordWithNoDrawdown() {
|
||||
double[] closes = new double[] { 100d, 110d, 121d };
|
||||
BarSeries series = buildYearlySeries("calmar_positive", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForNegativeReturnsTradingRecord() {
|
||||
double[] closes = new double[] { 100d, 70d, 80d };
|
||||
BarSeries series = buildYearlySeries("calmar_negative", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenThereAreNoReturnObservations() {
|
||||
BarSeries series = buildYearlySeries("calmar_one_bar", new double[] { 100d });
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
|
||||
Num actual = criterion.calculate(series, new BaseTradingRecord());
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenTradingRecordIsNull() {
|
||||
BarSeries series = buildYearlySeries("calmar_null_record", new double[] { 100d, 120d });
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
|
||||
Num actual = criterion.calculate(series, (TradingRecord) null);
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForClosedPosition() {
|
||||
double[] closes = new double[] { 100d, 80d, 120d };
|
||||
BarSeries series = buildYearlySeries("calmar_closed_position", closes);
|
||||
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(),
|
||||
numFactory.one());
|
||||
tradingRecord.exit(series.getEndIndex(), series.getBar(series.getEndIndex()).getClosePrice(), numFactory.one());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
Num actual = criterion.calculate(series, position);
|
||||
double expected = referenceCalmar(series, closes);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionHandlingIgnoreReturnsZeroForOpenPosition() {
|
||||
BarSeries series = buildYearlySeries("calmar_open_position", new double[] { 100d, 120d, 80d });
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(),
|
||||
numFactory.one());
|
||||
|
||||
CalmarRatioCriterion markToMarket = new CalmarRatioCriterion(EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
CalmarRatioCriterion ignoreOpen = new CalmarRatioCriterion(EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
CalmarRatioCriterion realized = new CalmarRatioCriterion(EquityCurveMode.REALIZED,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
Num markToMarketValue = markToMarket.calculate(series, tradingRecord);
|
||||
Num ignoreOpenValue = ignoreOpen.calculate(series, tradingRecord);
|
||||
Num realizedValue = realized.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(markToMarketValue.isNegative());
|
||||
assertNumEquals(numFactory.zero(), ignoreOpenValue, 0d);
|
||||
assertNumEquals(numFactory.zero(), realizedValue, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNaNWhenBeginValueCannotBeUsed() {
|
||||
BarSeries series = buildYearlySeries("calmar_zero_begin", new double[] { 0d, 120d, 80d });
|
||||
CalmarRatioCriterion criterion = new CalmarRatioCriterion(ReturnRepresentation.DECIMAL);
|
||||
|
||||
Num actual = criterion.calculate(series, alwaysInvested(series));
|
||||
|
||||
assertTrue(actual.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesReturnRepresentation() {
|
||||
CalmarRatioCriterion criterion = new CalmarRatioCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
|
||||
assertEquals(Optional.of(ReturnRepresentation.PERCENTAGE), criterion.getReturnRepresentation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanUsesHigherValuesAsBetter() {
|
||||
CalmarRatioCriterion criterion = (CalmarRatioCriterion) getCriterion();
|
||||
|
||||
assertTrue(criterion.betterThan(numFactory.one(), numFactory.zero()));
|
||||
assertFalse(criterion.betterThan(numFactory.zero(), numFactory.one()));
|
||||
}
|
||||
|
||||
private BarSeries buildYearlySeries(String name, double[] closes) {
|
||||
BarSeries series = getBarSeries(name);
|
||||
Instant start = Instant.parse("2020-01-01T00:00:00Z");
|
||||
|
||||
for (int i = 0; i < closes.length; i++) {
|
||||
Instant endTime = start.plus(Duration.ofDays(365L * i));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(365))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
private double referenceCalmar(BarSeries series, double[] closes) {
|
||||
double annualizedReturn = referenceAnnualizedReturn(series, closes);
|
||||
double maximumDrawdown = referenceMaximumDrawdown(closes);
|
||||
if (maximumDrawdown == 0d) {
|
||||
return annualizedReturn;
|
||||
}
|
||||
return annualizedReturn / maximumDrawdown;
|
||||
}
|
||||
|
||||
private double referenceAnnualizedReturn(BarSeries series, double[] closes) {
|
||||
int beginIndex = series.getBeginIndex();
|
||||
int endIndex = series.getEndIndex();
|
||||
if (endIndex <= beginIndex) {
|
||||
return 0d;
|
||||
}
|
||||
|
||||
double elapsedSeconds = Duration
|
||||
.between(series.getBar(beginIndex).getEndTime(), series.getBar(endIndex).getEndTime())
|
||||
.getSeconds();
|
||||
if (elapsedSeconds <= 0d) {
|
||||
return 0d;
|
||||
}
|
||||
|
||||
double years = elapsedSeconds / TimeConstants.SECONDS_PER_YEAR;
|
||||
double totalReturn = closes[closes.length - 1] / closes[0];
|
||||
return Math.pow(totalReturn, 1d / years) - 1d;
|
||||
}
|
||||
|
||||
private double referenceMaximumDrawdown(double[] closes) {
|
||||
double peak = closes[0];
|
||||
double maximumDrawdown = 0d;
|
||||
for (double close : closes) {
|
||||
if (close > peak) {
|
||||
peak = close;
|
||||
}
|
||||
double drawdown = (peak - close) / peak;
|
||||
maximumDrawdown = Math.max(maximumDrawdown, drawdown);
|
||||
}
|
||||
return maximumDrawdown;
|
||||
}
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.criteria.pnl.GrossReturnCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
|
||||
import org.ta4j.core.criteria.pnl.NetProfitLossPercentageCriterion;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
public class EnterAndHoldCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public EnterAndHoldCriterionTest(NumFactory numFactory) {
|
||||
super(params -> params.length == 1 ? new EnterAndHoldCriterion((AnalysisCriterion) params[0])
|
||||
: new EnterAndHoldCriterion((TradeType) params[0], (AnalysisCriterion) params[1]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOnePosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105).build();
|
||||
var position = new Position(Trade.buyAt(0, series), Trade.sellAt(1, series));
|
||||
|
||||
// buy and hold of GrossReturnCriterion
|
||||
var buyAndHoldReturn = getCriterion(new GrossReturnCriterion());
|
||||
assertNumEquals(1.05, buyAndHoldReturn.calculate(series, position));
|
||||
|
||||
var buyAndHoldReturnPercentage = getCriterion(new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(5.0, buyAndHoldReturnPercentage.calculate(series, position));
|
||||
|
||||
var buyAndHoldReturnDecimal = getCriterion(new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.05, buyAndHoldReturnDecimal.calculate(series, position));
|
||||
|
||||
// sell and hold of GrossReturnCriterion
|
||||
var sellAndHoldReturn = getCriterion(TradeType.SELL, new GrossReturnCriterion());
|
||||
assertNumEquals(0.95, sellAndHoldReturn.calculate(series, position));
|
||||
|
||||
var sellAndHoldReturnPercentage = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-5.0, sellAndHoldReturnPercentage.calculate(series, position));
|
||||
|
||||
var sellAndHoldReturnDecimal = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.05, sellAndHoldReturnDecimal.calculate(series, position));
|
||||
|
||||
// buy and hold of PnlPercentageCriterion
|
||||
var buyAndHoldPnlPercentage = getCriterion(new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.05, buyAndHoldPnlPercentage.calculate(series, position));
|
||||
|
||||
var buyAndHoldPnlPercentagePercentage = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(5.0, buyAndHoldPnlPercentagePercentage.calculate(series, position));
|
||||
|
||||
var buyAndHoldPnlPercentageDecimal = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.05, buyAndHoldPnlPercentageDecimal.calculate(series, position));
|
||||
|
||||
// sell and hold of PnlPercentageCriterion
|
||||
var sellAndHoldPnlPercentageDecimal = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.05, sellAndHoldPnlPercentageDecimal.calculate(series, position));
|
||||
|
||||
var sellAndHoldPnlPercentagePercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-5.0, sellAndHoldPnlPercentagePercentage.calculate(series, position));
|
||||
|
||||
var sellAndHoldPnlPercentageMultiplicative = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(0.95, sellAndHoldPnlPercentageMultiplicative.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
|
||||
// buy and hold of GrossReturnCriterion
|
||||
var buyAndHoldReturn = getCriterion(new GrossReturnCriterion());
|
||||
assertNumEquals(0.7, buyAndHoldReturn.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var buyAndHoldReturnPercentage = getCriterion(new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-30.0, buyAndHoldReturnPercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var buyAndHoldReturnDecimal = getCriterion(new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.30, buyAndHoldReturnDecimal.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
// sell and hold of GrossReturnCriterion
|
||||
var sellAndHoldReturn = getCriterion(TradeType.SELL, new GrossReturnCriterion());
|
||||
assertNumEquals(1.3, sellAndHoldReturn.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var sellAndHoldReturnPercentage = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(30.0, sellAndHoldReturnPercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var sellAndHoldReturnDecimal = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.30, sellAndHoldReturnDecimal.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
// buy and hold of NetProfitLossPercentageCriterion
|
||||
var buyAndHoldPnlPercentage = getCriterion(new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.30, buyAndHoldPnlPercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var buyAndHoldPnlPercentagePercentage = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-30.0, buyAndHoldPnlPercentagePercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var buyAndHoldPnlPercentageMultiplicative = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(0.7, buyAndHoldPnlPercentageMultiplicative.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
// sell and hold of NetProfitLossPercentageCriterion
|
||||
var sellAndHoldPnlPercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.30, sellAndHoldPnlPercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var sellAndHoldPnlPercentagePercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(30.0, sellAndHoldPnlPercentagePercentage.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
var sellAndHoldPnlPercentageMultiplicative = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(1.3, sellAndHoldPnlPercentageMultiplicative.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithGainPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
// buy and hold of GrossReturnCriterion
|
||||
var buyAndHoldReturn = getCriterion(new GrossReturnCriterion());
|
||||
assertNumEquals(1.05, buyAndHoldReturn.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldReturnPercentage = getCriterion(new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(5.0, buyAndHoldReturnPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldReturnDecimal = getCriterion(new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.05, buyAndHoldReturnDecimal.calculate(series, tradingRecord));
|
||||
|
||||
// sell and hold of GrossReturnCriterion
|
||||
var sellAndHoldReturn = getCriterion(TradeType.SELL, new GrossReturnCriterion());
|
||||
assertNumEquals(0.95, sellAndHoldReturn.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldReturnPercentage = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-5.0, sellAndHoldReturnPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldReturnDecimal = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.05, sellAndHoldReturnDecimal.calculate(series, tradingRecord));
|
||||
|
||||
// buy and hold of NetProfitLossPercentageCriterion
|
||||
var buyAndHoldPnlPercentage = getCriterion(new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.05, buyAndHoldPnlPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldPnlPercentagePercentage = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(5.0, buyAndHoldPnlPercentagePercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldPnlPercentageMultiplicative = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(1.05, buyAndHoldPnlPercentageMultiplicative.calculate(series, tradingRecord));
|
||||
|
||||
// sell and hold of NetProfitLossPercentageCriterion
|
||||
var sellAndHoldPnlPercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.05, sellAndHoldPnlPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldPnlPercentagePercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-5.0, sellAndHoldPnlPercentagePercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldPnlPercentageMultiplicative = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(0.95, sellAndHoldPnlPercentageMultiplicative.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithLossPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
|
||||
// buy and hold of GrossReturnCriterion
|
||||
var buyAndHoldReturn = getCriterion(new GrossReturnCriterion());
|
||||
assertNumEquals(0.7, buyAndHoldReturn.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldReturnPercentage = getCriterion(new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-30.0, buyAndHoldReturnPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldReturnDecimal = getCriterion(new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.30, buyAndHoldReturnDecimal.calculate(series, tradingRecord));
|
||||
|
||||
// sell and hold of GrossReturnCriterion
|
||||
var sellAndHoldReturn = getCriterion(TradeType.SELL, new GrossReturnCriterion());
|
||||
assertNumEquals(1.3, sellAndHoldReturn.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldReturnPercentage = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(30.0, sellAndHoldReturnPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldReturnDecimal = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.30, sellAndHoldReturnDecimal.calculate(series, tradingRecord));
|
||||
|
||||
// buy and hold of NetProfitLossPercentageCriterion
|
||||
var buyAndHoldPnlPercentage = getCriterion(new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(-0.30, buyAndHoldPnlPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldPnlPercentagePercentage = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(-30.0, buyAndHoldPnlPercentagePercentage.calculate(series, tradingRecord));
|
||||
|
||||
var buyAndHoldPnlPercentageMultiplicative = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(0.7, buyAndHoldPnlPercentageMultiplicative.calculate(series, tradingRecord));
|
||||
|
||||
// sell and hold of NetProfitLossPercentageCriterion
|
||||
var sellAndHoldPnlPercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertNumEquals(0.30, sellAndHoldPnlPercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldPnlPercentagePercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertNumEquals(30.0, sellAndHoldPnlPercentagePercentage.calculate(series, tradingRecord));
|
||||
|
||||
var sellAndHoldPnlPercentageMultiplicative = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertNumEquals(1.3, sellAndHoldPnlPercentageMultiplicative.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithAmount() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
// 2 winning positions
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
// buy and hold with amount of 10 (which means the pnl is 10 times higher than
|
||||
// amount of 1)
|
||||
var buyAndHoldPnl = new EnterAndHoldCriterion(TradeType.BUY, new NetProfitLossCriterion(),
|
||||
BigDecimal.valueOf(10));
|
||||
var buyAndHoldPnlValue = buyAndHoldPnl.calculate(series, tradingRecord);
|
||||
assertNumEquals(50.0, buyAndHoldPnlValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
|
||||
// buy and hold of GrossReturnCriterion
|
||||
var buyAndHoldReturn = getCriterion(new GrossReturnCriterion());
|
||||
assertTrue(buyAndHoldReturn.betterThan(numOf(1.3), numOf(1.1)));
|
||||
assertFalse(buyAndHoldReturn.betterThan(numOf(0.6), numOf(0.9)));
|
||||
|
||||
var buyAndHoldReturnPercentage = getCriterion(new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertTrue(buyAndHoldReturnPercentage.betterThan(numOf(30.0), numOf(10.0)));
|
||||
assertFalse(buyAndHoldReturnPercentage.betterThan(numOf(-40.0), numOf(-10.0)));
|
||||
|
||||
var buyAndHoldReturnDecimal = getCriterion(new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertTrue(buyAndHoldReturnDecimal.betterThan(numOf(0.3), numOf(0.1)));
|
||||
assertFalse(buyAndHoldReturnDecimal.betterThan(numOf(-0.4), numOf(-0.1)));
|
||||
|
||||
// sell and hold of GrossReturnCriterion
|
||||
var sellAndHoldReturn = getCriterion(TradeType.SELL, new GrossReturnCriterion());
|
||||
assertTrue(sellAndHoldReturn.betterThan(numOf(1.3), numOf(1.1)));
|
||||
assertFalse(sellAndHoldReturn.betterThan(numOf(0.6), numOf(0.9)));
|
||||
|
||||
var sellAndHoldReturnPercentage = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertTrue(sellAndHoldReturnPercentage.betterThan(numOf(30.0), numOf(10.0)));
|
||||
assertFalse(sellAndHoldReturnPercentage.betterThan(numOf(-40.0), numOf(-10.0)));
|
||||
|
||||
var sellAndHoldReturnDecimal = getCriterion(TradeType.SELL,
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertTrue(sellAndHoldReturnDecimal.betterThan(numOf(0.3), numOf(0.1)));
|
||||
assertFalse(sellAndHoldReturnDecimal.betterThan(numOf(-0.4), numOf(-0.1)));
|
||||
|
||||
// buy and hold of PnlPercentageCriterion
|
||||
var buyAndHoldPnlPercentage = getCriterion(new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertTrue(buyAndHoldPnlPercentage.betterThan(numOf(0.3), numOf(0.1)));
|
||||
assertFalse(buyAndHoldPnlPercentage.betterThan(numOf(-0.4), numOf(-0.1)));
|
||||
|
||||
var buyAndHoldPnlPercentagePercentage = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertTrue(buyAndHoldPnlPercentagePercentage.betterThan(numOf(30.0), numOf(10.0)));
|
||||
assertFalse(buyAndHoldPnlPercentagePercentage.betterThan(numOf(-40.0), numOf(-10.0)));
|
||||
|
||||
var buyAndHoldPnlPercentageMultiplicative = getCriterion(
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertTrue(buyAndHoldPnlPercentageMultiplicative.betterThan(numOf(1.3), numOf(1.1)));
|
||||
assertFalse(buyAndHoldPnlPercentageMultiplicative.betterThan(numOf(0.6), numOf(0.9)));
|
||||
|
||||
// sell and hold of PnlPercentageCriterion
|
||||
var sellAndHoldPnlPercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertTrue(sellAndHoldPnlPercentage.betterThan(numOf(0.3), numOf(0.1)));
|
||||
assertFalse(sellAndHoldPnlPercentage.betterThan(numOf(-0.4), numOf(-0.1)));
|
||||
|
||||
var sellAndHoldPnlPercentagePercentage = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.PERCENTAGE));
|
||||
assertTrue(sellAndHoldPnlPercentagePercentage.betterThan(numOf(30.0), numOf(10.0)));
|
||||
assertFalse(sellAndHoldPnlPercentagePercentage.betterThan(numOf(-40.0), numOf(-10.0)));
|
||||
|
||||
var sellAndHoldPnlPercentageMultiplicative = getCriterion(TradeType.SELL,
|
||||
new NetProfitLossPercentageCriterion(ReturnRepresentation.MULTIPLICATIVE));
|
||||
assertTrue(sellAndHoldPnlPercentageMultiplicative.betterThan(numOf(1.3), numOf(1.1)));
|
||||
assertFalse(sellAndHoldPnlPercentageMultiplicative.betterThan(numOf(0.6), numOf(0.9)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTransactionCosts() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
|
||||
var txCostModel = new LinearTransactionCostModel(0.05);
|
||||
var holdingCostModel = new ZeroCostModel();
|
||||
|
||||
var record = new BaseTradingRecord(TradeType.BUY, txCostModel, holdingCostModel);
|
||||
var amount = series.numFactory().one();
|
||||
record.enter(0, series.getBar(0).getClosePrice(), amount);
|
||||
record.exit(1, series.getBar(1).getClosePrice(), amount);
|
||||
|
||||
var criterion = getCriterion(new NetProfitLossCriterion());
|
||||
// net = (110-100) − (100*0.05 + 110*0.05) = 10 - 10.5 = −0.5
|
||||
assertNumEquals(-0.5, criterion.calculate(series, record));
|
||||
|
||||
record = new BaseTradingRecord(TradeType.BUY);
|
||||
record.enter(0, series.getBar(0).getClosePrice(), amount);
|
||||
record.exit(1, series.getBar(1).getClosePrice(), amount);
|
||||
|
||||
// net = (110-100) = 10
|
||||
assertNumEquals(10, criterion.calculate(series, record));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getReturnRepresentationDelegatesToWrappedCriterion() {
|
||||
EnterAndHoldCriterion criterionWithRepresentation = new EnterAndHoldCriterion(
|
||||
new GrossReturnCriterion(ReturnRepresentation.DECIMAL));
|
||||
assertTrue(criterionWithRepresentation.getReturnRepresentation().isPresent());
|
||||
assertEquals(ReturnRepresentation.DECIMAL, criterionWithRepresentation.getReturnRepresentation().get());
|
||||
|
||||
EnterAndHoldCriterion criterionWithoutRepresentation = new EnterAndHoldCriterion(new NumberOfBarsCriterion());
|
||||
assertFalse(criterionWithoutRepresentation.getReturnRepresentation().isPresent());
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class ExpectancyCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public ExpectancyCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new ExpectancyCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithProfitPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 110, 120, 130, 150, 160)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion avgLoss = getCriterion();
|
||||
assertNumEquals(1.0, avgLoss.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithMixedPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 110, 80, 130, 150, 160)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion avgLoss = getCriterion();
|
||||
assertNumEquals(0.25, avgLoss.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithLossPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 80, 70, 60, 50).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion avgLoss = getCriterion();
|
||||
assertNumEquals(0, avgLoss.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateProfitWithShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(160, 140, 120, 100, 80, 60).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(2, series), Trade.buyAt(5, series));
|
||||
|
||||
AnalysisCriterion avgLoss = getCriterion();
|
||||
assertNumEquals(1.0, avgLoss.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateProfitWithMixedShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(160, 200, 120, 100, 80, 60).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(2, series), Trade.buyAt(5, series));
|
||||
|
||||
AnalysisCriterion avgLoss = getCriterion();
|
||||
assertNumEquals(0.25, avgLoss.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(2.0), numOf(1.5)));
|
||||
assertFalse(criterion.betterThan(numOf(1.5), numOf(2.0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.criteria.ReturnRepresentation;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class ExpectedShortfallCriterionTest {
|
||||
private BarSeries series;
|
||||
|
||||
private NumFactory numFactory = DoubleNumFactory.getInstance();
|
||||
|
||||
private ExpectedShortfallCriterion getCriterion() {
|
||||
return new ExpectedShortfallCriterion(0.95);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithGainPositions() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 105d, 106d, 107d, 108d, 115d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
AnalysisCriterion varCriterion = getCriterion();
|
||||
assertNumEquals(numFactory.one(), varCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithASimplePosition() {
|
||||
// if only one position in tail, VaR = ES
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 104d, 90d, 100d, 95d, 105d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
AnalysisCriterion esCriterion = getCriterion();
|
||||
assertNumEquals(numFactory.numOf(90d / 104), esCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateOnlyWithLossPosition() {
|
||||
// regularly decreasing prices
|
||||
List<Double> prices = IntStream.rangeClosed(1, 100)
|
||||
.asDoubleStream()
|
||||
.boxed()
|
||||
.sorted(Collections.reverseOrder())
|
||||
.collect(Collectors.toList());
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(prices).build();
|
||||
Position position = new Position(Trade.buyAt(series.getBeginIndex(), series),
|
||||
Trade.sellAt(series.getEndIndex(), series));
|
||||
AnalysisCriterion esCriterion = getCriterion();
|
||||
assertNumEquals(numFactory.numOf(0.6988271187715792), esCriterion.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoBarsShouldReturn0() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 95d, 100d, 80d, 85d, 70d).build();
|
||||
AnalysisCriterion varCriterion = getCriterion();
|
||||
assertNumEquals(numFactory.numOf(1), varCriterion.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoBarsShouldReturnZeroRateOfReturn() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 95d, 100d, 80d, 85d, 70d).build();
|
||||
AnalysisCriterion varCriterion = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.DECIMAL);
|
||||
assertNumEquals(numFactory.zero(), varCriterion.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
AnalysisCriterion esCriterionMultiplicative = new ExpectedShortfallCriterion(0.95,
|
||||
ReturnRepresentation.MULTIPLICATIVE);
|
||||
assertNumEquals(numFactory.one(), esCriterionMultiplicative.calculate(series, new BaseTradingRecord()));
|
||||
|
||||
AnalysisCriterion esCriterionPercentage = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.PERCENTAGE);
|
||||
assertNumEquals(numFactory.zero(), esCriterionPercentage.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithBuyAndHold() {
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100d, 99d).build();
|
||||
Position position = new Position(Trade.buyAt(0, series), Trade.sellAt(1, series));
|
||||
AnalysisCriterion varCriterion = getCriterion();
|
||||
assertNumEquals(numFactory.numOf(0.99), varCriterion.calculate(series, position));
|
||||
|
||||
AnalysisCriterion esCriterionDecimal = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.DECIMAL);
|
||||
assertNumEquals(numFactory.numOf(0.99 - 1), esCriterionDecimal.calculate(series, position));
|
||||
|
||||
AnalysisCriterion esCriterionPercentage = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.PERCENTAGE);
|
||||
assertNumEquals(numFactory.numOf((0.99 - 1) * 100), esCriterionPercentage.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateRateOfReturnRepresentation() {
|
||||
// if only one position in tail, VaR = ES
|
||||
series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 104d, 90d, 100d, 95d, 105d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
AnalysisCriterion esCriterion = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.DECIMAL);
|
||||
assertNumEquals(numFactory.numOf((90d / 104) - 1), esCriterion.calculate(series, tradingRecord));
|
||||
|
||||
AnalysisCriterion esCriterionMultiplicative = new ExpectedShortfallCriterion(0.95,
|
||||
ReturnRepresentation.MULTIPLICATIVE);
|
||||
assertNumEquals(numFactory.numOf(90d / 104), esCriterionMultiplicative.calculate(series, tradingRecord));
|
||||
|
||||
AnalysisCriterion esCriterionPercentage = new ExpectedShortfallCriterion(0.95, ReturnRepresentation.PERCENTAGE);
|
||||
assertNumEquals(numFactory.numOf(((90d / 104) - 1) * 100),
|
||||
esCriterionPercentage.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numFactory.numOf(-0.1), numFactory.numOf(-0.2)));
|
||||
assertFalse(criterion.betterThan(numFactory.numOf(-0.1), numFactory.numOf(0.0)));
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class InPositionPercentageCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public InPositionPercentageCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new InPositionPercentageCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateReturnsPercentageForClosedPosition() {
|
||||
var series = buildSeries(5, Duration.ofHours(1));
|
||||
var amount = numFactory.one();
|
||||
var entry = Trade.buyAt(1, series.getBar(1).getClosePrice(), amount);
|
||||
var exit = Trade.sellAt(3, series.getBar(3).getClosePrice(), amount);
|
||||
var position = new Position(entry, exit);
|
||||
|
||||
var criterion = getCriterion();
|
||||
var result = criterion.calculate(series, position);
|
||||
|
||||
var totalDuration = totalDuration(series);
|
||||
var positionDuration = positionDuration(series, entry.getIndex(), exit.getIndex());
|
||||
var expectedPercentage = getExpectedPercentage(totalDuration, positionDuration);
|
||||
var expected = numFactory.numOf(expectedPercentage);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateReturnsPercentageForOpenPosition() {
|
||||
var series = buildSeries(5, Duration.ofHours(1));
|
||||
var amount = numFactory.one();
|
||||
var record = new BaseTradingRecord();
|
||||
record.enter(3, series.getBar(3).getClosePrice(), amount);
|
||||
var openPosition = record.getCurrentPosition();
|
||||
|
||||
var criterion = getCriterion();
|
||||
var result = criterion.calculate(series, openPosition);
|
||||
|
||||
var totalDuration = totalDuration(series);
|
||||
var positionDuration = positionDuration(series, openPosition.getEntry().getIndex(), series.getEndIndex());
|
||||
var expectedPercentage = getExpectedPercentage(totalDuration, positionDuration);
|
||||
var expected = numFactory.numOf(expectedPercentage);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateAggregatesDurationsAcrossRecord() {
|
||||
var series = buildSeries(5, Duration.ofHours(1));
|
||||
var record = new BaseTradingRecord();
|
||||
var amount = numFactory.one();
|
||||
|
||||
record.enter(0, series.getBar(0).getClosePrice(), amount);
|
||||
record.exit(1, series.getBar(1).getClosePrice(), amount);
|
||||
|
||||
record.enter(3, series.getBar(3).getClosePrice(), amount);
|
||||
record.exit(4, series.getBar(4).getClosePrice(), amount);
|
||||
|
||||
var criterion = getCriterion();
|
||||
var result = criterion.calculate(series, record);
|
||||
|
||||
var totalDuration = totalDuration(series);
|
||||
var accumulatedDuration = record.getPositions()
|
||||
.stream()
|
||||
.mapToLong(p -> positionDuration(series, p.getEntry().getIndex(),
|
||||
p.isClosed() ? p.getExit().getIndex() : series.getEndIndex()))
|
||||
.sum();
|
||||
var expectedPercentage = getExpectedPercentage(totalDuration, accumulatedDuration);
|
||||
var expected = numFactory.numOf(expectedPercentage);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateReturnsZeroWhenRecordHasNoPositions() {
|
||||
var series = buildSeries(4, Duration.ofHours(1));
|
||||
var criterion = getCriterion();
|
||||
assertNumEquals(numFactory.zero(), criterion.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateReturnsZeroWhenSeriesIsEmpty() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
var criterion = getCriterion();
|
||||
|
||||
assertNumEquals(numFactory.zero(), criterion.calculate(series, new Position()));
|
||||
assertNumEquals(numFactory.zero(), criterion.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanPrefersSmallerPercentage() {
|
||||
var criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numFactory.numOf(20), numFactory.numOf(40)));
|
||||
assertFalse(criterion.betterThan(numFactory.numOf(60), numFactory.numOf(30)));
|
||||
}
|
||||
|
||||
private BarSeries buildSeries(int barCount, Duration barDuration) {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).build();
|
||||
for (var i = 0; i < barCount; i++) {
|
||||
series.barBuilder()
|
||||
.timePeriod(barDuration)
|
||||
.closePrice(numFactory.zero())
|
||||
.openPrice(numFactory.zero())
|
||||
.highPrice(numFactory.zero())
|
||||
.lowPrice(numFactory.zero())
|
||||
.volume(numFactory.zero())
|
||||
.trades(0)
|
||||
.add();
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
private static long totalDuration(BarSeries series) {
|
||||
return ChronoUnit.NANOS.between(series.getFirstBar().getBeginTime(), series.getLastBar().getEndTime());
|
||||
}
|
||||
|
||||
private static long positionDuration(BarSeries series, int entryIndex, int exitIndex) {
|
||||
var entryStart = series.getBar(entryIndex).getBeginTime();
|
||||
var exitEnd = series.getBar(exitIndex).getEndTime();
|
||||
return ChronoUnit.NANOS.between(entryStart, exitEnd);
|
||||
}
|
||||
|
||||
private static double getExpectedPercentage(long totalDuration, double positionDuration) {
|
||||
return totalDuration == 0 ? 0 : positionDuration / totalDuration;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.ExternalCriterionTest;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class LinearTransactionCostCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
private final ExternalCriterionTest xls;
|
||||
|
||||
public LinearTransactionCostCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new LinearTransactionCostCriterion((double) params[0], (double) params[1], (double) params[2]),
|
||||
numFactory);
|
||||
xls = new XLSCriterionTest(this.getClass(), "LTC.xls", 16, 6, numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalData() throws Exception {
|
||||
BarSeries xlsSeries = xls.getSeries();
|
||||
TradingRecord xlsTradingRecord = xls.getTradingRecord();
|
||||
Num value;
|
||||
|
||||
value = getCriterion(1000d, 0.005, 0.2).calculate(xlsSeries, xlsTradingRecord);
|
||||
assertNumEquals(xls.getFinalCriterionValue(1000d, 0.005, 0.2).doubleValue(), value);
|
||||
assertNumEquals(843.5492, value);
|
||||
|
||||
value = getCriterion(1000d, 0.1, 1.0).calculate(xlsSeries, xlsTradingRecord);
|
||||
assertNumEquals(xls.getFinalCriterionValue(1000d, 0.1, 1.0).doubleValue(), value);
|
||||
assertNumEquals(1122.4410, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dummyData() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 150, 200, 100, 50, 100)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
Num criterion;
|
||||
|
||||
tradingRecord.operate(0);
|
||||
tradingRecord.operate(1);
|
||||
criterion = getCriterion(1000d, 0.005, 0.2).calculate(series, tradingRecord);
|
||||
assertNumEquals(12.861, criterion);
|
||||
|
||||
tradingRecord.operate(2);
|
||||
tradingRecord.operate(3);
|
||||
criterion = getCriterion(1000d, 0.005, 0.2).calculate(series, tradingRecord);
|
||||
assertNumEquals(24.3759, criterion);
|
||||
|
||||
tradingRecord.operate(5);
|
||||
criterion = getCriterion(1000d, 0.005, 0.2).calculate(series, tradingRecord);
|
||||
assertNumEquals(28.2488, criterion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedCost() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
Num criterion;
|
||||
|
||||
tradingRecord.operate(0);
|
||||
tradingRecord.operate(1);
|
||||
criterion = getCriterion(1000d, 0d, 1.3d).calculate(series, tradingRecord);
|
||||
assertNumEquals(2.6d, criterion);
|
||||
|
||||
tradingRecord.operate(2);
|
||||
tradingRecord.operate(3);
|
||||
criterion = getCriterion(1000d, 0d, 1.3d).calculate(series, tradingRecord);
|
||||
assertNumEquals(5.2d, criterion);
|
||||
|
||||
tradingRecord.operate(0);
|
||||
criterion = getCriterion(1000d, 0d, 1.3d).calculate(series, tradingRecord);
|
||||
assertNumEquals(6.5d, criterion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedCostWithOnePosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
Position position = new Position();
|
||||
Num criterion;
|
||||
|
||||
criterion = getCriterion(1000d, 0d, 0.75d).calculate(series, position);
|
||||
assertNumEquals(0d, criterion);
|
||||
|
||||
position.operate(1);
|
||||
criterion = getCriterion(1000d, 0d, 0.75d).calculate(series, position);
|
||||
assertNumEquals(0.75d, criterion);
|
||||
|
||||
position.operate(3);
|
||||
criterion = getCriterion(1000d, 0d, 0.75d).calculate(series, position);
|
||||
assertNumEquals(1.5d, criterion);
|
||||
|
||||
position.operate(4);
|
||||
criterion = getCriterion(1000d, 0d, 0.75d).calculate(series, position);
|
||||
assertNumEquals(1.5d, criterion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
var criterion = new LinearTransactionCostCriterion(1000, 0.5);
|
||||
assertTrue(criterion.betterThan(numOf(3.1), numOf(4.2)));
|
||||
assertFalse(criterion.betterThan(numOf(2.1), numOf(1.9)));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfBarsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfBarsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new NumberOfBarsCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
AnalysisCriterion numberOfBars = getCriterion();
|
||||
assertNumEquals(0, numberOfBars.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion numberOfBars = getCriterion();
|
||||
assertNumEquals(6, numberOfBars.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOnePosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
Position t = new Position(Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
AnalysisCriterion numberOfBars = getCriterion();
|
||||
assertNumEquals(4, numberOfBars.calculate(series, t));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(3), numOf(6)));
|
||||
assertFalse(criterion.betterThan(numOf(6), numOf(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfBreakEvenPositionsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfBreakEvenPositionsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new NumberOfBreakEvenPositionsCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
assertNumEquals(0, getCriterion().calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(1, series), Trade.sellAt(5, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOneLongPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
Position position = new Position(Trade.buyAt(0, series), Trade.sellAt(3, series));
|
||||
|
||||
assertNumEquals(1, getCriterion().calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(3, series),
|
||||
Trade.sellAt(1, series), Trade.buyAt(5, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(3), numOf(6)));
|
||||
assertFalse(criterion.betterThan(numOf(7), numOf(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.AnalysisCriterion.PositionFilter;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfConsecutivePositionsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfConsecutivePositionsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new NumberOfConsecutivePositionsCriterion((PositionFilter) params[0]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
assertNumEquals(0, getCriterion(PositionFilter.LOSS).calculate(series, new BaseTradingRecord()));
|
||||
assertNumEquals(0, getCriterion(PositionFilter.PROFIT).calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoLongPositions() {
|
||||
var seriesLoss = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(110, 105, 100, 90, 80, 140)
|
||||
.build();
|
||||
TradingRecord tradingRecordLoss = new BaseTradingRecord(Trade.buyAt(0, seriesLoss), Trade.sellAt(2, seriesLoss),
|
||||
Trade.buyAt(3, seriesLoss), Trade.sellAt(4, seriesLoss));
|
||||
assertNumEquals(2, getCriterion(PositionFilter.LOSS).calculate(seriesLoss, tradingRecordLoss));
|
||||
|
||||
var seriesProfit = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 120, 130, 140)
|
||||
.build();
|
||||
TradingRecord tradingRecordProfit = new BaseTradingRecord(Trade.buyAt(1, seriesProfit),
|
||||
Trade.sellAt(3, seriesProfit), Trade.buyAt(3, seriesProfit), Trade.sellAt(4, seriesProfit));
|
||||
assertNumEquals(2, getCriterion(PositionFilter.PROFIT).calculate(seriesProfit, tradingRecordProfit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOneLongPosition() {
|
||||
var seriesLoss = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(110, 105, 100, 90, 95, 105)
|
||||
.build();
|
||||
Position positionLoss = new Position(Trade.buyAt(1, seriesLoss), Trade.sellAt(3, seriesLoss));
|
||||
assertNumEquals(1, getCriterion(PositionFilter.LOSS).calculate(seriesLoss, positionLoss));
|
||||
|
||||
var seriesProfit = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 120, 95, 105)
|
||||
.build();
|
||||
Position positionProfit = new Position(Trade.buyAt(1, seriesProfit), Trade.sellAt(3, seriesProfit));
|
||||
assertNumEquals(1, getCriterion(PositionFilter.PROFIT).calculate(seriesProfit, positionProfit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoShortPositions() {
|
||||
var seriesLoss = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 90, 110, 120, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecordLoss = new BaseTradingRecord(Trade.sellAt(0, seriesLoss), Trade.buyAt(1, seriesLoss),
|
||||
Trade.sellAt(3, seriesLoss), Trade.buyAt(5, seriesLoss));
|
||||
assertNumEquals(0, getCriterion(PositionFilter.LOSS).calculate(seriesLoss, tradingRecordLoss));
|
||||
|
||||
var seriesProfit = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecordProfit = new BaseTradingRecord(Trade.sellAt(0, seriesProfit),
|
||||
Trade.buyAt(1, seriesProfit), Trade.sellAt(3, seriesProfit), Trade.buyAt(5, seriesProfit));
|
||||
assertNumEquals(0, getCriterion(PositionFilter.PROFIT).calculate(seriesProfit, tradingRecordProfit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterionLoss = getCriterion(PositionFilter.LOSS);
|
||||
assertTrue(criterionLoss.betterThan(numOf(3), numOf(6)));
|
||||
assertFalse(criterionLoss.betterThan(numOf(7), numOf(4)));
|
||||
|
||||
AnalysisCriterion criterionProfit = getCriterion(PositionFilter.PROFIT);
|
||||
assertFalse(criterionProfit.betterThan(numOf(3), numOf(6)));
|
||||
assertTrue(criterionProfit.betterThan(numOf(7), numOf(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory,
|
||||
getCriterion(PositionFilter.LOSS), 0);
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory,
|
||||
getCriterion(PositionFilter.PROFIT), 0);
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfLosingPositionsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfLosingPositionsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new NumberOfLosingPositionsCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
assertNumEquals(0, getCriterion().calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(1, series), Trade.sellAt(3, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(4, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOneLongPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
Position position = new Position(Trade.buyAt(1, series), Trade.sellAt(3, series));
|
||||
|
||||
assertNumEquals(1, getCriterion().calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(3, series), Trade.buyAt(5, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(3), numOf(6)));
|
||||
assertFalse(criterion.betterThan(numOf(7), numOf(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfPositionsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfPositionsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> params.length == 0 ? new NumberOfPositionsCriterion()
|
||||
: new NumberOfPositionsCriterion((boolean) params[0]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
AnalysisCriterion buyAndHold = getCriterion();
|
||||
assertNumEquals(0, buyAndHold.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion buyAndHold = getCriterion();
|
||||
assertNumEquals(2, buyAndHold.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOnePosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
Position position = new Position();
|
||||
AnalysisCriterion positionsCriterion = getCriterion();
|
||||
|
||||
assertNumEquals(1, positionsCriterion.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanWithLessIsBetter() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(3), numOf(6)));
|
||||
assertFalse(criterion.betterThan(numOf(7), numOf(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanWithLessIsNotBetter() {
|
||||
AnalysisCriterion criterion = getCriterion(false);
|
||||
assertFalse(criterion.betterThan(numOf(3), numOf(6)));
|
||||
assertTrue(criterion.betterThan(numOf(7), numOf(4)));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class NumberOfWinningPositionsCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public NumberOfWinningPositionsCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new NumberOfWinningPositionsCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
assertNumEquals(0, getCriterion().calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOneLongPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
Position position = new Position(Trade.buyAt(0, series), Trade.sellAt(2, series));
|
||||
|
||||
assertNumEquals(1, getCriterion().calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithTwoShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(110, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(2, series), Trade.buyAt(4, series));
|
||||
|
||||
assertNumEquals(2, getCriterion().calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(6), numOf(3)));
|
||||
assertFalse(criterion.betterThan(numOf(4), numOf(7)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.alwaysInvested;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildDailySeries;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.EquityCurveMode;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class OmegaRatioCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public OmegaRatioCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new OmegaRatioCriterion((double) params[0]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForMixedReturnsTradingRecord() {
|
||||
double[] closes = new double[] { 100d, 120d, 90d, 99d };
|
||||
BarSeries series = buildSeries("omega_mixed", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceOmega(returnsFromCloses(closes), 0d);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForCustomThreshold() {
|
||||
double threshold = 0.05d;
|
||||
double[] closes = new double[] { 100d, 120d, 90d, 99d };
|
||||
BarSeries series = buildSeries("omega_threshold", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = new OmegaRatioCriterion(threshold);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceOmega(returnsFromCloses(closes), threshold);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsPercentageRepresentation() {
|
||||
double[] closes = new double[] { 100d, 120d, 90d, 99d };
|
||||
BarSeries series = buildSeries("omega_percentage", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = new OmegaRatioCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceOmega(returnsFromCloses(closes), 0d);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected * 100d), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMultiplicativeRepresentation() {
|
||||
double[] closes = new double[] { 100d, 120d, 90d, 99d };
|
||||
BarSeries series = buildSeries("omega_multiplicative", closes);
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = new OmegaRatioCriterion(ReturnRepresentation.MULTIPLICATIVE);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
double expected = referenceOmega(returnsFromCloses(closes), 0d);
|
||||
|
||||
assertNumEquals(numFactory.numOf(1d + expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateThresholdMustBeFinite() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new OmegaRatioCriterion(Double.POSITIVE_INFINITY));
|
||||
assertThrows(IllegalArgumentException.class, () -> new OmegaRatioCriterion(Double.NaN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNaNWhenTradingRecordHasOnlyUpsideReturns() {
|
||||
BarSeries series = buildSeries("omega_positive_only", new double[] { 100d, 110d, 121d });
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(actual.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenTradingRecordHasOnlyDownsideReturns() {
|
||||
BarSeries series = buildSeries("omega_negative_only", new double[] { 100d, 90d, 81d });
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenTradingRecordHasNoPositions() {
|
||||
BarSeries series = buildSeries("omega_no_positions", new double[] { 100d, 120d, 90d, 99d });
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
|
||||
Num actual = criterion.calculate(series, new BaseTradingRecord());
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenThereAreNoReturnObservations() {
|
||||
BarSeries series = buildSeries("omega_one_bar", new double[] { 100d });
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
|
||||
Num actual = criterion.calculate(series, new BaseTradingRecord());
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenTradingRecordIsNull() {
|
||||
BarSeries series = buildSeries("omega_null_record", new double[] { 100d, 110d });
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
|
||||
Num actual = criterion.calculate(series, (TradingRecord) null);
|
||||
|
||||
assertNumEquals(numFactory.zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatesExpectedValueForClosedPosition() {
|
||||
double[] closes = new double[] { 100d, 120d, 90d, 99d };
|
||||
BarSeries series = buildSeries("omega_closed_position", closes);
|
||||
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(),
|
||||
numFactory.one());
|
||||
tradingRecord.exit(series.getEndIndex(), series.getBar(series.getEndIndex()).getClosePrice(), numFactory.one());
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
Num actual = criterion.calculate(series, position);
|
||||
double expected = referenceOmega(returnsFromCloses(closes), 0d);
|
||||
|
||||
assertNumEquals(numFactory.numOf(expected), actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionHandlingIgnoreReturnsZeroForOpenPosition() {
|
||||
BarSeries series = buildSeries("omega_open_position", new double[] { 100d, 120d, 80d });
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(),
|
||||
numFactory.one());
|
||||
|
||||
OmegaRatioCriterion markToMarket = new OmegaRatioCriterion(0d, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
OmegaRatioCriterion ignoreOpen = new OmegaRatioCriterion(0d, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.IGNORE);
|
||||
OmegaRatioCriterion realized = new OmegaRatioCriterion(0d, EquityCurveMode.REALIZED,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
Num markToMarketValue = markToMarket.calculate(series, tradingRecord);
|
||||
Num ignoreOpenValue = ignoreOpen.calculate(series, tradingRecord);
|
||||
Num realizedValue = realized.calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(numFactory.numOf(0.6d), markToMarketValue, 1e-12);
|
||||
assertNumEquals(numFactory.zero(), ignoreOpenValue, 0d);
|
||||
assertNumEquals(numFactory.zero(), realizedValue, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanUsesHigherValuesAsBetter() {
|
||||
OmegaRatioCriterion criterion = (OmegaRatioCriterion) getCriterion(0d);
|
||||
|
||||
assertTrue(criterion.betterThan(numFactory.one(), numFactory.zero()));
|
||||
assertFalse(criterion.betterThan(numFactory.zero(), numFactory.one()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposesReturnRepresentation() {
|
||||
OmegaRatioCriterion criterion = new OmegaRatioCriterion(ReturnRepresentation.PERCENTAGE);
|
||||
|
||||
assertEquals(Optional.of(ReturnRepresentation.PERCENTAGE), criterion.getReturnRepresentation());
|
||||
}
|
||||
|
||||
private BarSeries buildSeries(String name, double[] closes) {
|
||||
return buildDailySeries(getBarSeries(name), closes, Instant.parse("2024-01-01T00:00:00Z"));
|
||||
}
|
||||
|
||||
private double[] returnsFromCloses(double[] closes) {
|
||||
double[] returns = new double[Math.max(closes.length - 1, 0)];
|
||||
for (int i = 1; i < closes.length; i++) {
|
||||
returns[i - 1] = (closes[i] / closes[i - 1]) - 1d;
|
||||
}
|
||||
return returns;
|
||||
}
|
||||
|
||||
private double referenceOmega(double[] returns, double threshold) {
|
||||
double upsideExcess = 0d;
|
||||
double downsideShortfall = 0d;
|
||||
|
||||
for (double value : returns) {
|
||||
double excess = value - threshold;
|
||||
if (excess > 0d) {
|
||||
upsideExcess += excess;
|
||||
} else if (excess < 0d) {
|
||||
downsideShortfall += -excess;
|
||||
}
|
||||
}
|
||||
|
||||
if (downsideShortfall == 0d) {
|
||||
return upsideExcess == 0d ? 0d : Double.NaN;
|
||||
}
|
||||
return upsideExcess / downsideShortfall;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.FixedTransactionCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class OpenPositionCostBasisCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public OpenPositionCostBasisCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new OpenPositionCostBasisCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateUsesBaseTradingRecord() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(new BaseTrade(0, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.one(), numFactory.numOf(0.1), ExecutionSide.BUY, null, null));
|
||||
record.recordFill(new BaseTrade(0, Instant.parse("2025-01-01T00:00:01Z"), numFactory.numOf(110),
|
||||
numFactory.one(), numFactory.numOf(0.2), ExecutionSide.BUY, null, null));
|
||||
|
||||
Num expected = numFactory.hundred()
|
||||
.plus(numFactory.numOf(110))
|
||||
.plus(numFactory.numOf(0.1))
|
||||
.plus(numFactory.numOf(0.2));
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateUsesCurrentPositionForStandardRecord() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
var costModel = new FixedTransactionCostModel(1.5);
|
||||
var record = new BaseTradingRecord(TradeType.BUY, costModel, new ZeroCostModel());
|
||||
|
||||
record.enter(0, series.getBar(0).getClosePrice(), numFactory.one());
|
||||
|
||||
Num expected = series.getBar(0)
|
||||
.getClosePrice()
|
||||
.multipliedBy(numFactory.one())
|
||||
.plus(costModel.calculate(series.getBar(0).getClosePrice(), numFactory.one()));
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenNoOpenPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(numFactory.zero(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanPrefersLowerCostBasis() {
|
||||
var criterion = getCriterion();
|
||||
|
||||
assertTrue(criterion.betterThan(numFactory.one(), numFactory.two()));
|
||||
assertFalse(criterion.betterThan(numFactory.two(), numFactory.one()));
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.BaseTrade;
|
||||
import org.ta4j.core.ExecutionMatchPolicy;
|
||||
import org.ta4j.core.ExecutionSide;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class OpenPositionUnrealizedProfitCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public OpenPositionUnrealizedProfitCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new OpenPositionUnrealizedProfitCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateForBaseTradingRecordLong() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 110).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(new BaseTrade(0, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.two(), numFactory.numOf(0.5), ExecutionSide.BUY, null, null));
|
||||
|
||||
Num expected = numFactory.numOf(110)
|
||||
.multipliedBy(numFactory.two())
|
||||
.minus(numFactory.hundred().multipliedBy(numFactory.two()))
|
||||
.minus(numFactory.numOf(0.5));
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateForBaseTradingRecordShort() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 90).build();
|
||||
var record = new BaseTradingRecord(TradeType.SELL, ExecutionMatchPolicy.FIFO, new ZeroCostModel(),
|
||||
new ZeroCostModel(), null, null);
|
||||
|
||||
record.recordFill(new BaseTrade(0, Instant.parse("2025-01-01T00:00:00Z"), numFactory.hundred(),
|
||||
numFactory.one(), numFactory.numOf(0.2), ExecutionSide.SELL, null, null));
|
||||
|
||||
Num expected = numFactory.hundred().minus(numFactory.numOf(90)).minus(numFactory.numOf(0.2));
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateForStandardRecordOpenPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 120).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
|
||||
record.enter(0, series.getBar(0).getClosePrice(), numFactory.one());
|
||||
|
||||
Num expected = numFactory.numOf(120).minus(numFactory.hundred());
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZeroWhenNoOpenPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 120).build();
|
||||
var record = new BaseTradingRecord(TradeType.BUY, new ZeroCostModel(), new ZeroCostModel());
|
||||
|
||||
var result = getCriterion().calculate(series, record);
|
||||
|
||||
assertNumEquals(numFactory.zero(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThanPrefersHigherProfit() {
|
||||
var criterion = getCriterion();
|
||||
|
||||
assertTrue(criterion.betterThan(numFactory.two(), numFactory.one()));
|
||||
assertFalse(criterion.betterThan(numFactory.one(), numFactory.two()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class OpenedPositionUtils {
|
||||
|
||||
public void testCalculateOneOpenPositionShouldReturnExpectedValue(NumFactory numFactory,
|
||||
AnalysisCriterion criterion, Num expectedValue) {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
|
||||
var trade = new Position(Trade.TradeType.BUY);
|
||||
trade.operate(0, series.numFactory().numOf(2.5), series.numFactory().one());
|
||||
|
||||
final Num value = criterion.calculate(series, trade);
|
||||
|
||||
assertNumEquals(expectedValue, value);
|
||||
}
|
||||
|
||||
public void testCalculateOneOpenPositionShouldReturnExpectedValue(NumFactory numFactory,
|
||||
AnalysisCriterion criterion, int expectedValue) {
|
||||
this.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, criterion,
|
||||
numFactory.numOf(expectedValue));
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class PositionDurationCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public PositionDurationCriterionTest(NumFactory numFactory) {
|
||||
super(params -> params.length == 0 ? new PositionDurationCriterion()
|
||||
: new PositionDurationCriterion((Statistics) params[0]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithPosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105, 110)
|
||||
.build();
|
||||
var position = new Position(Trade.buyAt(1, series), Trade.sellAt(4, series));
|
||||
|
||||
var criterion = getCriterion();
|
||||
long secondsPerBar = series.getBar(series.getBeginIndex()).getTimePeriod().toSeconds();
|
||||
|
||||
assertNumEquals(secondsPerBar * 3d, criterion.calculate(series, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithNoPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 105, 110).build();
|
||||
|
||||
var criterion = getCriterion();
|
||||
assertNumEquals(0, criterion.calculate(series, new BaseTradingRecord()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithMeanDurationByDefault() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105, 110, 120, 125, 130, 135, 140, 145, 150, 155, 160)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(4, series), Trade.buyAt(5, series), Trade.sellAt(15, series));
|
||||
|
||||
var criterion = getCriterion();
|
||||
var secondsPerBar = series.getBar(series.getBeginIndex()).getTimePeriod().toSeconds();
|
||||
var expectedMean = secondsPerBar * (13.0 / 3.0);
|
||||
|
||||
assertNumEquals(expectedMean, criterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithMedianDuration() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105, 110, 120, 125, 130, 135, 140, 145, 150, 155, 160)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(4, series), Trade.buyAt(5, series), Trade.sellAt(15, series));
|
||||
|
||||
var criterion = getCriterion(Statistics.MEDIAN);
|
||||
var secondsPerBar = series.getBar(series.getBeginIndex()).getTimePeriod().toSeconds();
|
||||
|
||||
assertNumEquals(secondsPerBar * 2d, criterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithPercentileDuration() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105, 110, 120, 125, 130, 135, 140, 145, 150, 155, 160)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(4, series), Trade.buyAt(5, series), Trade.sellAt(15, series));
|
||||
|
||||
var criterion = getCriterion(Statistics.P95);
|
||||
var secondsPerBar = series.getBar(series.getBeginIndex()).getTimePeriod().toSeconds();
|
||||
|
||||
assertNumEquals(secondsPerBar * 10d, criterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithMinimumDuration() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105, 110, 120, 125, 130, 135, 140, 145, 150, 155, 160)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(4, series), Trade.buyAt(5, series), Trade.sellAt(15, series));
|
||||
|
||||
var criterion = getCriterion(Statistics.MIN);
|
||||
var secondsPerBar = series.getBar(series.getBeginIndex()).getTimePeriod().toSeconds();
|
||||
|
||||
assertNumEquals(secondsPerBar * 1d, criterion.calculate(series, tradingRecord));
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.AnalysisCriterion.PositionFilter;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class PositionsRatioCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public PositionsRatioCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new PositionsRatioCriterion((PositionFilter) params[0]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculate() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 95d, 102d, 105d, 97d, 113d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(3, series), Trade.buyAt(4, series), Trade.sellAt(5, series));
|
||||
|
||||
// there are 3 positions with 2 winning positions
|
||||
AnalysisCriterion winningPositionsRatio = getCriterion(PositionFilter.PROFIT);
|
||||
assertNumEquals(2d / 3, winningPositionsRatio.calculate(series, tradingRecord));
|
||||
|
||||
// there are 3 positions with 1 losing positions
|
||||
AnalysisCriterion losingPositionsRatio = getCriterion(PositionFilter.LOSS);
|
||||
assertNumEquals(1d / 3, losingPositionsRatio.calculate(series, tradingRecord));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 95d, 102d, 105d, 97d, 113d)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(2, series),
|
||||
Trade.sellAt(3, series), Trade.buyAt(4, series));
|
||||
|
||||
// there are 3 positions with 1 winning positions
|
||||
AnalysisCriterion winningPositionsRatio = getCriterion(PositionFilter.PROFIT);
|
||||
assertNumEquals(0.5, winningPositionsRatio.calculate(series, tradingRecord));
|
||||
|
||||
// there are 3 positions with 1 losing positions
|
||||
AnalysisCriterion losingPositionsRatio = getCriterion(PositionFilter.LOSS);
|
||||
assertNumEquals(0.5, losingPositionsRatio.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOnePosition() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100d, 95d, 102d, 105d, 97d, 113d)
|
||||
.build();
|
||||
Position position = new Position(Trade.buyAt(0, series), Trade.sellAt(1, series));
|
||||
|
||||
// 0 winning position
|
||||
AnalysisCriterion winningPositionsRatio = getCriterion(PositionFilter.PROFIT);
|
||||
assertNumEquals(numOf(0), winningPositionsRatio.calculate(series, position));
|
||||
|
||||
// 1 winning position
|
||||
position = new Position(Trade.buyAt(1, series), Trade.sellAt(2, series));
|
||||
assertNumEquals(1, winningPositionsRatio.calculate(series, position));
|
||||
|
||||
// 1 losing position
|
||||
position = new Position(Trade.buyAt(0, series), Trade.sellAt(1, series));
|
||||
AnalysisCriterion losingPositionsRatio = getCriterion(PositionFilter.LOSS);
|
||||
assertNumEquals(numOf(1), losingPositionsRatio.calculate(series, position));
|
||||
|
||||
// 0 losing position
|
||||
position = new Position(Trade.buyAt(1, series), Trade.sellAt(2, series));
|
||||
assertNumEquals(0, losingPositionsRatio.calculate(series, position));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion winningPositionsRatio = getCriterion(PositionFilter.PROFIT);
|
||||
assertTrue(winningPositionsRatio.betterThan(numOf(12), numOf(8)));
|
||||
assertFalse(winningPositionsRatio.betterThan(numOf(8), numOf(12)));
|
||||
|
||||
AnalysisCriterion losingPositionsRatio = getCriterion(PositionFilter.LOSS);
|
||||
assertTrue(losingPositionsRatio.betterThan(numOf(8), numOf(12)));
|
||||
assertFalse(losingPositionsRatio.betterThan(numOf(12), numOf(8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory,
|
||||
getCriterion(PositionFilter.PROFIT), 0);
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory,
|
||||
getCriterion(PositionFilter.LOSS), 0);
|
||||
}
|
||||
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.stream.IntStream;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
final class RatioCriterionTestSupport {
|
||||
|
||||
private RatioCriterionTestSupport() {
|
||||
throw new AssertionError("No instances");
|
||||
}
|
||||
|
||||
static BarSeries buildDailySeries(BarSeries series, double[] closes, Instant start) {
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
Instant endTime = start.plus(Duration.ofDays(i + 1L));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
return series;
|
||||
}
|
||||
|
||||
static BarSeries buildHourlySeries(BarSeries series, double[] closes, Instant start) {
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
Instant endTime = start.plus(Duration.ofHours(i + 1L));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
return series;
|
||||
}
|
||||
|
||||
static BarSeries buildMinuteSeries(BarSeries series, double[] closes, Instant start) {
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
Instant endTime = start.plus(Duration.ofMinutes(i + 1L));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofMinutes(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
return series;
|
||||
}
|
||||
|
||||
static BarSeries buildSecondSeries(BarSeries series, double[] closes, Instant start) {
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
Instant endTime = start.plus(Duration.ofSeconds(i + 1L));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofSeconds(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
return series;
|
||||
}
|
||||
|
||||
static BarSeries buildIntradaySeriesWithDailyEnds(BarSeries series, double[] dailyEndCloses, Instant day0StartUtc) {
|
||||
Instant day0EndTime = day0StartUtc.plus(Duration.ofHours(23));
|
||||
double day0EndClose = dailyEndCloses[0];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(day0EndTime)
|
||||
.openPrice(day0EndClose)
|
||||
.highPrice(day0EndClose)
|
||||
.lowPrice(day0EndClose)
|
||||
.closePrice(day0EndClose)
|
||||
.volume(1)
|
||||
.build());
|
||||
|
||||
IntStream.range(1, dailyEndCloses.length).forEach(dayIndex -> {
|
||||
Instant dayBase = day0StartUtc.plus(Duration.ofDays(dayIndex));
|
||||
|
||||
Instant[] times = new Instant[] { dayBase.plus(Duration.ofHours(10)), dayBase.plus(Duration.ofHours(15)),
|
||||
dayBase.plus(Duration.ofHours(23)) };
|
||||
|
||||
double prevDayEnd = dailyEndCloses[dayIndex - 1];
|
||||
double dayEnd = dailyEndCloses[dayIndex];
|
||||
double mid = (prevDayEnd + dayEnd) / 2.0;
|
||||
|
||||
double[] closes = new double[] { prevDayEnd, mid, dayEnd };
|
||||
|
||||
IntStream.range(0, times.length).forEach(i -> {
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(times[i])
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
static TradingRecord alwaysInvested(BarSeries series) {
|
||||
Num amount = series.numFactory().one();
|
||||
int begin = series.getBeginIndex();
|
||||
int end = series.getEndIndex();
|
||||
int split = begin + (end - begin) / 2;
|
||||
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(begin, series.getBar(begin).getClosePrice(), amount);
|
||||
tradingRecord.exit(split, series.getBar(split).getClosePrice(), amount);
|
||||
|
||||
if (split < end) {
|
||||
tradingRecord.enter(split, series.getBar(split).getClosePrice(), amount);
|
||||
tradingRecord.exit(end, series.getBar(end).getClosePrice(), amount);
|
||||
}
|
||||
|
||||
return tradingRecord;
|
||||
}
|
||||
|
||||
static TradingRecord tradingRecordWithGap(BarSeries series) {
|
||||
Num amount = series.numFactory().one();
|
||||
BaseTradingRecord tradingRecord = new BaseTradingRecord();
|
||||
int begin = series.getBeginIndex();
|
||||
|
||||
tradingRecord.enter(begin, series.getBar(begin).getClosePrice(), amount);
|
||||
tradingRecord.exit(begin + 1, series.getBar(begin + 1).getClosePrice(), amount);
|
||||
|
||||
tradingRecord.enter(begin + 3, series.getBar(begin + 3).getClosePrice(), amount);
|
||||
tradingRecord.exit(begin + 4, series.getBar(begin + 4).getClosePrice(), amount);
|
||||
|
||||
return tradingRecord;
|
||||
}
|
||||
|
||||
static int[] weeklyEndIndicesUtc(BarSeries series) {
|
||||
int begin = series.getBeginIndex();
|
||||
int end = series.getEndIndex();
|
||||
|
||||
return IntStream.rangeClosed(begin, end).filter(i -> {
|
||||
if (i == begin || i == end) {
|
||||
return true;
|
||||
}
|
||||
ZonedDateTime now = series.getBar(i).getEndTime().atZone(ZoneOffset.UTC);
|
||||
ZonedDateTime next = series.getBar(i + 1).getEndTime().atZone(ZoneOffset.UTC);
|
||||
return !sameIsoWeek(now, next);
|
||||
}).toArray();
|
||||
}
|
||||
|
||||
private static boolean sameIsoWeek(ZonedDateTime a, ZonedDateTime b) {
|
||||
WeekFields weekFields = WeekFields.ISO;
|
||||
int weekA = a.get(weekFields.weekOfWeekBasedYear());
|
||||
int weekB = b.get(weekFields.weekOfWeekBasedYear());
|
||||
int yearA = a.get(weekFields.weekBasedYear());
|
||||
int yearB = b.get(weekFields.weekBasedYear());
|
||||
return weekA == weekB && yearA == yearB;
|
||||
}
|
||||
|
||||
static int[] monthlyEndIndicesUtc(BarSeries series) {
|
||||
int begin = series.getBeginIndex();
|
||||
int end = series.getEndIndex();
|
||||
|
||||
return IntStream.rangeClosed(begin, end).filter(i -> {
|
||||
if (i == begin || i == end) {
|
||||
return true;
|
||||
}
|
||||
ZonedDateTime now = series.getBar(i).getEndTime().atZone(ZoneOffset.UTC);
|
||||
ZonedDateTime next = series.getBar(i + 1).getEndTime().atZone(ZoneOffset.UTC);
|
||||
return !YearMonth.from(now).equals(YearMonth.from(next));
|
||||
}).toArray();
|
||||
}
|
||||
|
||||
static BarSeries compressSeries(BarSeries source, int[] indices, String name) {
|
||||
BarSeries series = new BaseBarSeriesBuilder().withName(name).withNumFactory(source.numFactory()).build();
|
||||
|
||||
IntStream.range(0, indices.length).forEach(i -> {
|
||||
Bar sourceBar = source.getBar(indices[i]);
|
||||
double close = sourceBar.getClosePrice().doubleValue();
|
||||
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(sourceBar.getEndTime())
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class ReturnRepresentationPolicyTest {
|
||||
|
||||
@Test
|
||||
public void useOverridesDefaultRepresentation() {
|
||||
var original = ReturnRepresentationPolicy.getDefaultRepresentation();
|
||||
try {
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(ReturnRepresentation.DECIMAL);
|
||||
assertSame(ReturnRepresentation.DECIMAL, ReturnRepresentationPolicy.getDefaultRepresentation());
|
||||
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(ReturnRepresentation.MULTIPLICATIVE);
|
||||
assertSame(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentationPolicy.getDefaultRepresentation());
|
||||
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(ReturnRepresentation.LOG);
|
||||
assertSame(ReturnRepresentation.LOG, ReturnRepresentationPolicy.getDefaultRepresentation());
|
||||
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(ReturnRepresentation.MULTIPLICATIVE);
|
||||
assertSame(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentationPolicy.getDefaultRepresentation());
|
||||
} finally {
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settingDefaultRepresentationToNullUsesDefault() {
|
||||
ReturnRepresentationPolicy.setDefaultRepresentation(null);
|
||||
assertSame(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentationPolicy.getDefaultRepresentation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMatchesEnumNamesCaseInsensitive() {
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("multiplicative"));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("Decimal"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse("percentage"));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("log"));
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("multiplicative"));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("decimal"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse("Percentage"));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("Log"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHandlesExactEnumNames() {
|
||||
// Fast path: exact matches
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("MULTIPLICATIVE"));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("DECIMAL"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse("PERCENTAGE"));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("LOG"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHandlesVariousFormats() {
|
||||
// Mixed separators and case
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("Multiplicative"));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("Decimal"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse("Percentage"));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("Log"));
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("multiplicative"));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("DECIMAL"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse("PERCENTAGE"));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("LOG"));
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse("multiplicative")); // multiple
|
||||
// underscores
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("decimal")); // multiple
|
||||
// spaces
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse("log"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHandlesWhitespace() {
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, ReturnRepresentation.parse(" MULTIPLICATIVE "));
|
||||
assertEquals(ReturnRepresentation.DECIMAL, ReturnRepresentation.parse("\tDecimal\n"));
|
||||
assertEquals(ReturnRepresentation.PERCENTAGE, ReturnRepresentation.parse(" percentage "));
|
||||
assertEquals(ReturnRepresentation.LOG, ReturnRepresentation.parse(" LOG "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseReturnsNullOnInvalidName() {
|
||||
ReturnRepresentation result = ReturnRepresentation.parse("invalid_name");
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseReturnsNullOnEmptyString() {
|
||||
ReturnRepresentation result = ReturnRepresentation.parse(" ");
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseReturnsNullOnNull() {
|
||||
ReturnRepresentation result = ReturnRepresentation.parse(null);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseErrorInStream() {
|
||||
ReturnRepresentation defaultRepresentation = Optional.ofNullable("GARBAGE IN")
|
||||
.map(ReturnRepresentation::parse)
|
||||
.orElse(ReturnRepresentation.MULTIPLICATIVE);
|
||||
|
||||
assertEquals(ReturnRepresentation.MULTIPLICATIVE, defaultRepresentation);
|
||||
}
|
||||
}
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class ReturnRepresentationTest {
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(0.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(0.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromLogReturn_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toRepresentationFromLogReturn(logReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromLogReturn_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toRepresentationFromLogReturn(logReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(1.12);
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toTotalReturn(representedReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(0.12);
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toTotalReturn(representedReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(1.12);
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toRateOfReturn(representedReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(0.12);
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toRateOfReturn(representedReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturnFromLogReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12));
|
||||
|
||||
Num result = ReturnRepresentation.MULTIPLICATIVE.toTotalReturnFromLogReturn(logReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Multiplicative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(1.12);
|
||||
|
||||
// Convert to decimal and back
|
||||
Num rateOfReturn = ReturnRepresentation.MULTIPLICATIVE.toRateOfReturn(original);
|
||||
Num backToMultiplicative = ReturnRepresentation.DECIMAL.toTotalReturn(rateOfReturn);
|
||||
|
||||
assertNumEquals(original, backToMultiplicative);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Decimal() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(0.12);
|
||||
|
||||
// Convert to multiplicative and back
|
||||
Num totalReturn = ReturnRepresentation.DECIMAL.toTotalReturn(original);
|
||||
Num backToDecimal = ReturnRepresentation.MULTIPLICATIVE.toRateOfReturn(totalReturn);
|
||||
|
||||
assertNumEquals(original, backToDecimal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worksWithDecimalNumFactory() {
|
||||
NumFactory factory = DecimalNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.12);
|
||||
|
||||
Num result = ReturnRepresentation.DECIMAL.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
|
||||
// Verify it uses the same factory
|
||||
assertEquals(factory.getClass(), result.getNumFactory().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeReturns() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(0.85); // -15% loss
|
||||
|
||||
Num decimalResult = ReturnRepresentation.DECIMAL.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(-0.15, decimalResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.DECIMAL.toTotalReturn(decimalResult);
|
||||
assertNumEquals(0.85, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.0); // 0% change
|
||||
|
||||
Num decimalResult = ReturnRepresentation.DECIMAL.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(0.0, decimalResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.DECIMAL.toTotalReturn(decimalResult);
|
||||
assertNumEquals(1.0, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.05); // +5% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(5.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Percentage_100PercentGain() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(2.0); // +100% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(100.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Percentage_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(0.85); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(-15.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(0.05); // +5% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(5.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Percentage_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(-0.15); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(-15.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromLogReturn_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromLogReturn(logReturn);
|
||||
assertNumEquals(12.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(5.0); // 5% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toTotalReturn(representedReturn);
|
||||
assertNumEquals(1.05, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Percentage_100PercentGain() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(100.0); // 100% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toTotalReturn(representedReturn);
|
||||
assertNumEquals(2.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Percentage_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(-15.0); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toTotalReturn(representedReturn);
|
||||
assertNumEquals(0.85, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(5.0); // 5% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRateOfReturn(representedReturn);
|
||||
assertNumEquals(0.05, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Percentage_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num representedReturn = factory.numOf(-15.0); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRateOfReturn(representedReturn);
|
||||
assertNumEquals(-0.15, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Percentage() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(5.0); // 5% gain
|
||||
|
||||
// Convert to total return and back
|
||||
Num totalReturn = ReturnRepresentation.PERCENTAGE.toTotalReturn(original);
|
||||
Num backToPercentage = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
|
||||
assertNumEquals(original, backToPercentage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Percentage_FromTotalReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(1.05); // +5% gain as total return
|
||||
|
||||
// Convert to percentage and back
|
||||
Num percentage = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(original);
|
||||
Num backToTotal = ReturnRepresentation.PERCENTAGE.toTotalReturn(percentage);
|
||||
|
||||
assertNumEquals(original, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Percentage_FromRateOfReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(0.05); // +5% gain as rate of return
|
||||
|
||||
// Convert to percentage and back
|
||||
Num percentage = ReturnRepresentation.PERCENTAGE.toRepresentationFromRateOfReturn(original);
|
||||
Num backToRate = ReturnRepresentation.PERCENTAGE.toRateOfReturn(percentage);
|
||||
|
||||
assertNumEquals(original, backToRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentageWorksWithDecimalNumFactory() {
|
||||
NumFactory factory = DecimalNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.05); // +5% gain
|
||||
|
||||
Num result = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(5.0, result);
|
||||
|
||||
// Verify it uses the same factory
|
||||
assertEquals(factory.getClass(), result.getNumFactory().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentageNegativeReturns() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(0.85); // -15% loss
|
||||
|
||||
Num percentageResult = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(-15.0, percentageResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.PERCENTAGE.toTotalReturn(percentageResult);
|
||||
assertNumEquals(0.85, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percentageZeroReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.0); // 0% change
|
||||
|
||||
Num percentageResult = ReturnRepresentation.PERCENTAGE.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(0.0, percentageResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.PERCENTAGE.toTotalReturn(percentageResult);
|
||||
assertNumEquals(1.0, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(Math.log(1.12), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Log_100PercentGain() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(2.0); // +100% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(Math.log(2.0), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromTotalReturn_Log_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(0.85); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(Math.log(0.85), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(0.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(Math.log(1.12), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromRateOfReturn_Log_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num rateOfReturn = factory.numOf(-0.15); // -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromRateOfReturn(rateOfReturn);
|
||||
assertNumEquals(Math.log(0.85), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRepresentationFromLogReturn_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromLogReturn(logReturn);
|
||||
assertNumEquals(Math.log(1.12), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toTotalReturn(logReturn);
|
||||
assertNumEquals(1.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Log_100PercentGain() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(2.0)); // log of +100% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toTotalReturn(logReturn);
|
||||
assertNumEquals(2.0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toTotalReturn_Log_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(0.85)); // log of -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toTotalReturn(logReturn);
|
||||
assertNumEquals(0.85, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRateOfReturn(logReturn);
|
||||
assertNumEquals(0.12, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRateOfReturn_Log_Negative() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num logReturn = factory.numOf(Math.log(0.85)); // log of -15% loss
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRateOfReturn(logReturn);
|
||||
assertNumEquals(-0.15, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Log() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(Math.log(1.12)); // log of +12% gain
|
||||
|
||||
// Convert to total return and back
|
||||
Num totalReturn = ReturnRepresentation.LOG.toTotalReturn(original);
|
||||
Num backToLog = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
|
||||
assertNumEquals(original, backToLog);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Log_FromTotalReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(1.12); // +12% gain as total return
|
||||
|
||||
// Convert to log and back
|
||||
Num logReturn = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(original);
|
||||
Num backToTotal = ReturnRepresentation.LOG.toTotalReturn(logReturn);
|
||||
|
||||
assertNumEquals(original, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripConversion_Log_FromRateOfReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num original = factory.numOf(0.12); // +12% gain as rate of return
|
||||
|
||||
// Convert to log and back
|
||||
Num logReturn = ReturnRepresentation.LOG.toRepresentationFromRateOfReturn(original);
|
||||
Num backToRate = ReturnRepresentation.LOG.toRateOfReturn(logReturn);
|
||||
|
||||
assertNumEquals(original, backToRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logWorksWithDecimalNumFactory() {
|
||||
NumFactory factory = DecimalNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.12); // +12% gain
|
||||
|
||||
Num result = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(Math.log(1.12), result);
|
||||
|
||||
// Verify it uses the same factory
|
||||
assertEquals(factory.getClass(), result.getNumFactory().getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logNegativeReturns() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(0.85); // -15% loss
|
||||
|
||||
Num logResult = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(Math.log(0.85), logResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.LOG.toTotalReturn(logResult);
|
||||
assertNumEquals(0.85, backToTotal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logZeroReturn() {
|
||||
NumFactory factory = DoubleNumFactory.getInstance();
|
||||
Num totalReturn = factory.numOf(1.0); // 0% change
|
||||
|
||||
Num logResult = ReturnRepresentation.LOG.toRepresentationFromTotalReturn(totalReturn);
|
||||
assertNumEquals(0.0, logResult);
|
||||
|
||||
Num backToTotal = ReturnRepresentation.LOG.toTotalReturn(logResult);
|
||||
assertNumEquals(1.0, backToTotal);
|
||||
}
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.alwaysInvested;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildDailySeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildHourlySeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildIntradaySeriesWithDailyEnds;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildMinuteSeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildSecondSeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.compressSeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.monthlyEndIndicesUtc;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.weeklyEndIndicesUtc;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.ExcessReturns.CashReturnPolicy;
|
||||
import org.ta4j.core.analysis.EquityCurveMode;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.analysis.frequency.SamplingFrequency;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class SharpeRatioCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public SharpeRatioCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new SharpeRatioCriterion((double) params[0], (SamplingFrequency) params[1],
|
||||
(Annualization) params[2], (ZoneId) params[3]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsKnownSharpePerPeriod_whenAlwaysInvested() {
|
||||
BarSeries series = getBarSeries("sr_test");
|
||||
|
||||
Instant start = Instant.parse("2024-01-01T00:00:00Z");
|
||||
double[] closes = new double[] { 100d, 150d, 150d, 225d, 225d };
|
||||
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
Instant endTime = start.plus(Duration.ofDays(i + 1L));
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofDays(1))
|
||||
.endTime(endTime)
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
Num expected = numFactory.numOf(Math.sqrt(3.0) / 2.0);
|
||||
|
||||
assertNumEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void annualizationEqualsPeriodTimesSqrtPeriodsPerYear_whenDailyBars() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 150d, 150d, 225d, 225d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num period = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num annualized = criterion(SamplingFrequency.BAR, Annualization.ANNUALIZED).calculate(series, tradingRecord);
|
||||
|
||||
Num expectedFactor = numFactory.numOf(Math.sqrt(365.2425d));
|
||||
Num expected = period.multipliedBy(expectedFactor);
|
||||
assertNumEquals(expected, annualized, 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingDailyEqualsPerBar_whenBarsAreDaily() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 150d, 150d, 225d, 225d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num perBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num daily = criterion(SamplingFrequency.DAY, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(perBar, daily, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingDailyOnIntradayMatchesPerBarOnCompressedDailySeries() {
|
||||
double[] dayEndCloses = new double[] { 100d, 150d, 150d, 225d, 225d };
|
||||
|
||||
BarSeries intradaySeries = buildIntradaySeriesWithDailyEnds(getBarSeries("intraday_series"), dayEndCloses,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord intradayTradingRecord = alwaysInvested(intradaySeries);
|
||||
|
||||
BarSeries dailySeries = buildDailySeries(getBarSeries("daily_series"), dayEndCloses,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord dailyTradingRecord = alwaysInvested(dailySeries);
|
||||
|
||||
Num intradayDailySampling = criterion(SamplingFrequency.DAY, Annualization.PERIOD).calculate(intradaySeries,
|
||||
intradayTradingRecord);
|
||||
|
||||
Num compressedDailyPerBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(dailySeries,
|
||||
dailyTradingRecord);
|
||||
|
||||
assertNumEquals(compressedDailyPerBar, intradayDailySampling, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupingZoneIdMakesSharpeZeroInOneZoneButNotTheOther() {
|
||||
BarSeries series = getBarSeries("zone_test_deterministic");
|
||||
|
||||
Instant[] endTimes = new Instant[] { Instant.parse("2024-01-01T23:30:00Z"), // NY: Jan 1
|
||||
Instant.parse("2024-01-02T00:30:00Z"), // NY: Jan 1 -> day boundary happens between this and next
|
||||
Instant.parse("2024-01-02T23:30:00Z"), // NY: Jan 2
|
||||
Instant.parse("2024-01-03T00:30:00Z"), // NY: Jan 2 -> day boundary happens between this and next
|
||||
Instant.parse("2024-01-03T23:30:00Z"), // NY: Jan 3
|
||||
Instant.parse("2024-01-04T00:30:00Z") // NY: Jan 3 (last)
|
||||
};
|
||||
|
||||
// Constructed so that NY daily sampling uses indices (1, 3, 5) with constant
|
||||
// returns:
|
||||
// 150/100 = 1.5, 225/150 = 1.5, 337.5/225 = 1.5 -> stdev=0 -> Sharpe=0
|
||||
// UTC daily sampling uses indices (2, 4, 5) -> non-constant returns -> Sharpe
|
||||
// != 0
|
||||
double[] closes = new double[] { 100d, 150d, 80d, 225d, 300d, 337.5d };
|
||||
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(endTimes[i])
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num sharpeNewYork = criterion(ZoneId.of("America/New_York")).calculate(series, tradingRecord);
|
||||
|
||||
Num sharpeUtc = criterion(ZoneOffset.UTC).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(series.numFactory().zero(), sharpeNewYork, 0d);
|
||||
assertTrue(sharpeUtc.abs().isGreaterThan(series.numFactory().numOf(1e-12)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riskFreeRateReducesSharpe_whenPositive() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 150d, 150d, 225d, 225d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num sharpeNoRf = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num sharpeWithRf = criterion().calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(sharpeWithRf.isLessThan(sharpeNoRf));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenNoClosedPositionsInTradingRecord() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(actual.isGreaterThan(series.numFactory().zero()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenOpenPositionIsEvaluatedDirectly() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
|
||||
Position openPosition = tradingRecord.getCurrentPosition();
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
|
||||
Num actual = criterion.calculate(series, openPosition);
|
||||
|
||||
assertTrue(actual.isGreaterThan(series.numFactory().zero()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realizedSharpeIgnoresOpenPositionEvenWhenMarkedToMarket() {
|
||||
var series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 90d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
var amount = series.numFactory().one();
|
||||
var tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
tradingRecord.exit(series.getBeginIndex() + 1, series.getBar(series.getBeginIndex() + 1).getClosePrice(),
|
||||
amount);
|
||||
tradingRecord.enter(series.getBeginIndex() + 2, series.getBar(series.getBeginIndex() + 2).getClosePrice(),
|
||||
amount);
|
||||
|
||||
var markToMarket = new SharpeRatioCriterion(0d, SamplingFrequency.BAR, Annualization.PERIOD, ZoneOffset.UTC,
|
||||
CashReturnPolicy.CASH_EARNS_RISK_FREE, EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
var realized = new SharpeRatioCriterion(0d, SamplingFrequency.BAR, Annualization.PERIOD, ZoneOffset.UTC,
|
||||
CashReturnPolicy.CASH_EARNS_RISK_FREE, EquityCurveMode.REALIZED, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
var sharpeMarkToMarket = markToMarket.calculate(series, tradingRecord);
|
||||
var sharpeRealized = realized.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(sharpeMarkToMarket.isGreaterThan(sharpeRealized));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZero_whenStdevIsZero() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 100d, 100d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(series.numFactory().zero(), actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZero_whenLessThanTwoReturnsAreAvailable() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(series.numFactory().zero(), actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingWeeklyOnDailyMatchesPerBarOnCompressedWeeklySeries() {
|
||||
double[] closes = IntStream.range(0, 15).mapToDouble(i -> 100d + i + ((i % 4) == 0 ? 7d : -3d)).toArray();
|
||||
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), closes,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num weekly = criterion(SamplingFrequency.WEEK, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
BarSeries weeklyCompressedSeries = compressSeries(series, weeklyEndIndicesUtc(series), "weekly_compressed");
|
||||
Num weeklyCompressed = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(weeklyCompressedSeries,
|
||||
alwaysInvested(weeklyCompressedSeries));
|
||||
|
||||
assertNumEquals(weeklyCompressed, weekly, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingMonthlyOnDailyMatchesPerBarOnCompressedMonthlySeries() {
|
||||
double[] closes = IntStream.range(0, 45).mapToDouble(i -> 120d + i + ((i % 7) == 0 ? 11d : -2d)).toArray();
|
||||
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), closes,
|
||||
Instant.parse("2024-01-24T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num monthly = criterion(SamplingFrequency.MONTH, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
BarSeries monthlyCompressedSeries = compressSeries(series, monthlyEndIndicesUtc(series), "monthly_compressed");
|
||||
Num monthlyCompressed = criterion(SamplingFrequency.BAR, Annualization.PERIOD)
|
||||
.calculate(monthlyCompressedSeries, alwaysInvested(monthlyCompressedSeries));
|
||||
|
||||
assertNumEquals(monthlyCompressed, monthly, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenClosedPositionIsEvaluatedDirectly() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 150d, 150d, 225d, 225d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
tradingRecord.exit(series.getEndIndex(), series.getBar(series.getEndIndex()).getClosePrice(), amount);
|
||||
|
||||
Position position = tradingRecord.getPositions().getFirst();
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
|
||||
Num actual = criterion.calculate(series, position);
|
||||
Num expected = numFactory.numOf(Math.sqrt(3.0) / 2.0);
|
||||
|
||||
assertNumEquals(expected, actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingHourlyEqualsPerBar_whenBarsAreHourly() {
|
||||
BarSeries series = buildHourlySeries(getBarSeries("hourly_series"), new double[] { 100d, 105d, 110d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num perBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num hourly = criterion(SamplingFrequency.HOUR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(perBar, hourly, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingMinutelyEqualsPerBar_whenBarsAreMinutely() {
|
||||
BarSeries series = buildMinuteSeries(getBarSeries("minute_series"), new double[] { 100d, 101d, 102d, 103d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num perBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num minutely = criterion(SamplingFrequency.MINUTE, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(perBar, minutely, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingPerSecondEqualsPerBar_whenBarsArePerSecond() {
|
||||
BarSeries series = buildSecondSeries(getBarSeries("second_series"), new double[] { 100d, 101d, 103d, 106d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num perBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num perSecond = criterion(SamplingFrequency.SECOND, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(perBar, perSecond, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenOnlyOneClosedPositionIsAvailable() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
tradingRecord.exit(series.getEndIndex(), series.getBar(series.getEndIndex()).getClosePrice(), amount);
|
||||
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
double return1 = 110d / 100d - 1d;
|
||||
double return2 = 120d / 110d - 1d;
|
||||
double mean = (return1 + return2) / 2d;
|
||||
double variance = (Math.pow(return1 - mean, 2) + Math.pow(return2 - mean, 2));
|
||||
double stdev = Math.sqrt(variance);
|
||||
Num expected = numFactory.numOf(mean / stdev);
|
||||
|
||||
assertNumEquals(expected, actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenOneClosedPositionAndOneOpenPositionAreAvailable() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 90d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
tradingRecord.exit(series.getBeginIndex() + 1, series.getBar(series.getBeginIndex() + 1).getClosePrice(),
|
||||
amount);
|
||||
tradingRecord.enter(series.getBeginIndex() + 2, series.getBar(series.getBeginIndex() + 2).getClosePrice(),
|
||||
amount);
|
||||
|
||||
SharpeRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(actual.isGreaterThan(series.numFactory().zero()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSharpe_whenOpenPositionIsExcludedFromReturnSeries() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 90d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
tradingRecord.exit(series.getBeginIndex() + 1, series.getBar(series.getBeginIndex() + 1).getClosePrice(),
|
||||
amount);
|
||||
tradingRecord.enter(series.getBeginIndex() + 2, series.getBar(series.getBeginIndex() + 2).getClosePrice(),
|
||||
amount);
|
||||
|
||||
SharpeRatioCriterion criterion = new SharpeRatioCriterion(0d, SamplingFrequency.BAR, Annualization.PERIOD,
|
||||
ZoneOffset.UTC, CashReturnPolicy.CASH_EARNS_RISK_FREE, OpenPositionHandling.IGNORE);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
double return1 = 110d / 100d - 1d;
|
||||
double mean = return1 / 3d;
|
||||
double variance = (Math.pow(return1 - mean, 2) + Math.pow(-mean, 2) + Math.pow(-mean, 2));
|
||||
double stdev = Math.sqrt(variance / 2d);
|
||||
Num expected = numFactory.numOf(mean / stdev);
|
||||
|
||||
assertNumEquals(expected, actual, 1e-12);
|
||||
}
|
||||
|
||||
private SharpeRatioCriterion criterion(ZoneId zoneId) {
|
||||
return (SharpeRatioCriterion) getCriterion(0d, SamplingFrequency.DAY, Annualization.PERIOD, zoneId);
|
||||
}
|
||||
|
||||
private SharpeRatioCriterion criterion(SamplingFrequency samplingFrequency, Annualization annualization) {
|
||||
return (SharpeRatioCriterion) getCriterion(0d, samplingFrequency, annualization, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private SharpeRatioCriterion criterion() {
|
||||
return (SharpeRatioCriterion) getCriterion(0.05d, SamplingFrequency.BAR, Annualization.PERIOD, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.alwaysInvested;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildDailySeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.buildIntradaySeriesWithDailyEnds;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.compressSeries;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.monthlyEndIndicesUtc;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.tradingRecordWithGap;
|
||||
import static org.ta4j.core.criteria.RatioCriterionTestSupport.weeklyEndIndicesUtc;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.ExcessReturns.CashReturnPolicy;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.analysis.frequency.SamplingFrequency;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
public class SortinoRatioCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public SortinoRatioCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new SortinoRatioCriterion((double) params[0], (SamplingFrequency) params[1],
|
||||
(Annualization) params[2], (ZoneId) params[3]), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsKnownSortinoPerPeriod_whenAlwaysInvested() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 50d, 100d, 50d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SortinoRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
Num expected = numFactory.numOf(Math.sqrt(0.5d));
|
||||
|
||||
assertNumEquals(expected, actual, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void annualizationEqualsPeriodTimesSqrtPeriodsPerYear_whenDailyBars() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 50d, 100d, 50d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num period = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num annualized = criterion(SamplingFrequency.BAR, Annualization.ANNUALIZED).calculate(series, tradingRecord);
|
||||
|
||||
Num expectedFactor = numFactory.numOf(Math.sqrt(365.2425d));
|
||||
Num expected = period.multipliedBy(expectedFactor);
|
||||
|
||||
assertNumEquals(expected, annualized, 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingDailyEqualsPerBar_whenBarsAreDaily() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 50d, 100d, 50d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num perBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num daily = criterion(SamplingFrequency.DAY, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(perBar, daily, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingDailyOnIntradayMatchesPerBarOnCompressedDailySeries() {
|
||||
double[] dayEndCloses = new double[] { 100d, 50d, 100d, 50d, 100d };
|
||||
|
||||
BarSeries intradaySeries = buildIntradaySeriesWithDailyEnds(getBarSeries("intraday_series"), dayEndCloses,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord intradayTradingRecord = alwaysInvested(intradaySeries);
|
||||
|
||||
BarSeries dailySeries = buildDailySeries(getBarSeries("daily_series"), dayEndCloses,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord dailyTradingRecord = alwaysInvested(dailySeries);
|
||||
|
||||
Num intradayDailySampling = criterion(SamplingFrequency.DAY, Annualization.PERIOD).calculate(intradaySeries,
|
||||
intradayTradingRecord);
|
||||
Num compressedDailyPerBar = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(dailySeries,
|
||||
dailyTradingRecord);
|
||||
|
||||
assertNumEquals(compressedDailyPerBar, intradayDailySampling, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingWeeklyOnDailyMatchesPerBarOnCompressedWeeklySeries() {
|
||||
double[] closes = IntStream.range(0, 15).mapToDouble(i -> 100d + i + ((i % 4) == 0 ? 7d : -3d)).toArray();
|
||||
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), closes,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num weekly = criterion(SamplingFrequency.WEEK, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
BarSeries weeklyCompressedSeries = compressSeries(series, weeklyEndIndicesUtc(series), "weekly_compressed");
|
||||
Num weeklyCompressed = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(weeklyCompressedSeries,
|
||||
alwaysInvested(weeklyCompressedSeries));
|
||||
|
||||
assertNumEquals(weeklyCompressed, weekly, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplingMonthlyOnDailyMatchesPerBarOnCompressedMonthlySeries() {
|
||||
double[] closes = IntStream.range(0, 45).mapToDouble(i -> 120d + i + ((i % 7) == 0 ? 11d : -2d)).toArray();
|
||||
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), closes,
|
||||
Instant.parse("2024-01-24T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num monthly = criterion(SamplingFrequency.MONTH, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
|
||||
BarSeries monthlyCompressedSeries = compressSeries(series, monthlyEndIndicesUtc(series), "monthly_compressed");
|
||||
Num monthlyCompressed = criterion(SamplingFrequency.BAR, Annualization.PERIOD)
|
||||
.calculate(monthlyCompressedSeries, alwaysInvested(monthlyCompressedSeries));
|
||||
|
||||
assertNumEquals(monthlyCompressed, monthly, 1e-12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riskFreeRateReducesSortino_whenPositive() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 50d, 100d, 50d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num sortinoNoRf = criterion(SamplingFrequency.BAR, Annualization.PERIOD).calculate(series, tradingRecord);
|
||||
Num sortinoWithRf = criterion().calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(sortinoWithRf.isLessThan(sortinoNoRf));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cashEarnsZeroLowersSortino_whenRiskFreeRatePositiveAndFlatIntervals() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 90d, 95d, 90d, 100d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = tradingRecordWithGap(series);
|
||||
|
||||
SortinoRatioCriterion cashRiskFree = new SortinoRatioCriterion(0.05d, SamplingFrequency.BAR,
|
||||
Annualization.PERIOD, ZoneOffset.UTC, CashReturnPolicy.CASH_EARNS_RISK_FREE,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
SortinoRatioCriterion cashZero = new SortinoRatioCriterion(0.05d, SamplingFrequency.BAR, Annualization.PERIOD,
|
||||
ZoneOffset.UTC, CashReturnPolicy.CASH_EARNS_ZERO, OpenPositionHandling.MARK_TO_MARKET);
|
||||
|
||||
Num sortinoRiskFree = cashRiskFree.calculate(series, tradingRecord);
|
||||
Num sortinoZero = cashZero.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(sortinoZero.isLessThan(sortinoRiskFree));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupingZoneIdMakesSortinoNaNInOneZoneButNotTheOther() {
|
||||
BarSeries series = getBarSeries("zone_test_sortino");
|
||||
|
||||
Instant[] endTimes = new Instant[] { Instant.parse("2024-01-01T23:30:00Z"), // NY: Jan 1
|
||||
Instant.parse("2024-01-02T00:30:00Z"), // NY: Jan 1 -> day boundary happens between this and next
|
||||
Instant.parse("2024-01-02T23:30:00Z"), // NY: Jan 2
|
||||
Instant.parse("2024-01-03T00:30:00Z"), // NY: Jan 2 -> day boundary happens between this and next
|
||||
Instant.parse("2024-01-03T23:30:00Z"), // NY: Jan 3
|
||||
Instant.parse("2024-01-04T00:30:00Z") // NY: Jan 3 (last)
|
||||
};
|
||||
|
||||
// NY daily sampling uses indices (1, 3, 5) -> constant positive returns ->
|
||||
// downside deviation = 0 -> NaN
|
||||
// UTC daily sampling uses indices (2, 4, 5) -> includes a negative return ->
|
||||
// finite Sortino
|
||||
double[] closes = new double[] { 100d, 150d, 80d, 225d, 300d, 337.5d };
|
||||
|
||||
IntStream.range(0, closes.length).forEach(i -> {
|
||||
double close = closes[i];
|
||||
series.addBar(series.barBuilder()
|
||||
.timePeriod(Duration.ofHours(1))
|
||||
.endTime(endTimes[i])
|
||||
.openPrice(close)
|
||||
.highPrice(close)
|
||||
.lowPrice(close)
|
||||
.closePrice(close)
|
||||
.volume(1)
|
||||
.build());
|
||||
});
|
||||
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
Num sortinoNewYork = new SortinoRatioCriterion(0d, SamplingFrequency.DAY, Annualization.PERIOD,
|
||||
ZoneId.of("America/New_York"), CashReturnPolicy.CASH_EARNS_RISK_FREE,
|
||||
OpenPositionHandling.MARK_TO_MARKET).calculate(series, tradingRecord);
|
||||
|
||||
Num sortinoUtc = new SortinoRatioCriterion(0d, SamplingFrequency.DAY, Annualization.PERIOD, ZoneOffset.UTC,
|
||||
CashReturnPolicy.CASH_EARNS_RISK_FREE, OpenPositionHandling.MARK_TO_MARKET)
|
||||
.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(sortinoNewYork.isNaN());
|
||||
assertFalse(sortinoUtc.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNaN_whenDownsideDeviationIsZero() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 110d, 121d, 133.1d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SortinoRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(actual.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNaN_whenDownsideDeviationIsZero_forWeeklySampling() {
|
||||
double[] closes = IntStream.range(0, 15).mapToDouble(i -> 100d * Math.pow(1.05d, i)).toArray();
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), closes,
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SortinoRatioCriterion criterion = criterion(SamplingFrequency.WEEK, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertTrue(actual.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsZero_whenLessThanTwoReturnsAreAvailable() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
TradingRecord tradingRecord = alwaysInvested(series);
|
||||
|
||||
SortinoRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
Num actual = criterion.calculate(series, tradingRecord);
|
||||
|
||||
assertNumEquals(series.numFactory().zero(), actual, 0d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openPositionHandlingIgnoreReturnsNaN_whenPositionIsOpen() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 80d, 120d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
|
||||
SortinoRatioCriterion markToMarket = new SortinoRatioCriterion(0d, SamplingFrequency.BAR, Annualization.PERIOD,
|
||||
ZoneOffset.UTC, CashReturnPolicy.CASH_EARNS_RISK_FREE, OpenPositionHandling.MARK_TO_MARKET);
|
||||
SortinoRatioCriterion ignore = new SortinoRatioCriterion(0d, SamplingFrequency.BAR, Annualization.PERIOD,
|
||||
ZoneOffset.UTC, CashReturnPolicy.CASH_EARNS_RISK_FREE, OpenPositionHandling.IGNORE);
|
||||
|
||||
Num markToMarketValue = markToMarket.calculate(series, tradingRecord);
|
||||
Num ignoreValue = ignore.calculate(series, tradingRecord);
|
||||
|
||||
assertFalse(markToMarketValue.isNaN());
|
||||
assertTrue(ignoreValue.isNaN());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSortino_whenOpenPositionIsEvaluatedDirectly() {
|
||||
BarSeries series = buildDailySeries(getBarSeries("daily_series"), new double[] { 100d, 90d, 110d },
|
||||
Instant.parse("2024-01-01T00:00:00Z"));
|
||||
|
||||
Num amount = series.numFactory().one();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord();
|
||||
tradingRecord.enter(series.getBeginIndex(), series.getBar(series.getBeginIndex()).getClosePrice(), amount);
|
||||
|
||||
Position openPosition = tradingRecord.getCurrentPosition();
|
||||
SortinoRatioCriterion criterion = criterion(SamplingFrequency.BAR, Annualization.PERIOD);
|
||||
|
||||
Num actual = criterion.calculate(series, openPosition);
|
||||
|
||||
assertTrue(actual.isGreaterThan(series.numFactory().zero()));
|
||||
}
|
||||
|
||||
private SortinoRatioCriterion criterion(SamplingFrequency samplingFrequency, Annualization annualization) {
|
||||
return (SortinoRatioCriterion) getCriterion(0d, samplingFrequency, annualization, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private SortinoRatioCriterion criterion() {
|
||||
return (SortinoRatioCriterion) getCriterion(0.05d, SamplingFrequency.BAR, Annualization.PERIOD, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.mocks.MockBarSeriesBuilder;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
public class SqnCriterionTest extends AbstractCriterionTest {
|
||||
|
||||
public SqnCriterionTest(NumFactory numFactory) {
|
||||
super(params -> new SqnCriterion(), numFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithWinningLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 105, 110, 100, 95, 105)
|
||||
.build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(2, series),
|
||||
Trade.buyAt(3, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion sqnCriterion = getCriterion();
|
||||
assertNumEquals(4.242640687119286, sqnCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithLosingLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 95, 100, 80, 85, 70).build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion sqnCriterion = getCriterion();
|
||||
assertNumEquals(-1.9798989873223332, sqnCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithOneWinningAndOneLosingLongPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 195, 100, 80, 85, 70).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.buyAt(0, series), Trade.sellAt(1, series),
|
||||
Trade.buyAt(2, series), Trade.sellAt(5, series));
|
||||
|
||||
AnalysisCriterion sqnCriterion = getCriterion();
|
||||
assertNumEquals(0.7353910524340095, sqnCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithWinningShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory).withData(100, 90, 100, 95, 95, 100).build();
|
||||
var tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(2, series), Trade.buyAt(3, series));
|
||||
|
||||
AnalysisCriterion sqnCriterion = getCriterion();
|
||||
assertNumEquals(4.242640687119286, sqnCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateWithLosingShortPositions() {
|
||||
var series = new MockBarSeriesBuilder().withNumFactory(numFactory)
|
||||
.withData(100, 110, 100, 105, 95, 105)
|
||||
.build();
|
||||
TradingRecord tradingRecord = new BaseTradingRecord(Trade.sellAt(0, series), Trade.buyAt(1, series),
|
||||
Trade.sellAt(2, series), Trade.buyAt(3, series));
|
||||
|
||||
AnalysisCriterion sqnCriterion = getCriterion();
|
||||
assertNumEquals(-4.242640687119286, sqnCriterion.calculate(series, tradingRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betterThan() {
|
||||
AnalysisCriterion criterion = getCriterion();
|
||||
assertTrue(criterion.betterThan(numOf(50), numOf(45)));
|
||||
assertFalse(criterion.betterThan(numOf(45), numOf(50)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateOneOpenPositionShouldReturnZero() {
|
||||
openedPositionUtils.testCalculateOneOpenPositionShouldReturnExpectedValue(numFactory, getCriterion(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.criteria;
|
||||
|
||||
import static org.ta4j.core.TestUtils.assertNumEquals;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class StatisticsTest {
|
||||
|
||||
private final NumFactory numFactory;
|
||||
|
||||
public StatisticsTest(NumFactory numFactory) {
|
||||
this.numFactory = numFactory;
|
||||
}
|
||||
|
||||
@Parameterized.Parameters(name = "NumFactory: {index} (0=DoubleNum, 1=DecimalNum)")
|
||||
public static List<NumFactory> function() {
|
||||
return List.of(DoubleNumFactory.getInstance(), DecimalNumFactory.getInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateReturnsZeroForEmptyValues() {
|
||||
var result = Statistics.MEAN.calculate(numFactory, new Num[0]);
|
||||
|
||||
assertNumEquals(numFactory.zero(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateMeanFromNumValues() {
|
||||
var values = new Num[] { numFactory.one(), numFactory.two(), numFactory.three() };
|
||||
|
||||
assertNumEquals(numFactory.two(), Statistics.MEAN.calculate(numFactory, values));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateMedianAndPercentilesFromNumValues() {
|
||||
var values = new Num[] { numFactory.one(), numFactory.two(), numFactory.three(), numFactory.numOf(4) };
|
||||
|
||||
assertNumEquals(numFactory.two(), Statistics.MEDIAN.calculate(numFactory, values));
|
||||
assertNumEquals(numFactory.numOf(4), Statistics.P95.calculate(numFactory, values));
|
||||
assertNumEquals(numFactory.numOf(4), Statistics.P99.calculate(numFactory, values));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateMinAndMaxFromNumValues() {
|
||||
var values = new Num[] { numFactory.three(), numFactory.one(), numFactory.two() };
|
||||
|
||||
assertNumEquals(numFactory.one(), Statistics.MIN.calculate(numFactory, values));
|
||||
assertNumEquals(numFactory.three(), Statistics.MAX.calculate(numFactory, values));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user