goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -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.
@@ -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
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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());
}
}
@@ -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 bars 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());
}
}
@@ -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";
}
}
}
@@ -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));
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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)));
}
}
@@ -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();
}
}
@@ -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)));
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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)));
}
}
@@ -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;
}
}
@@ -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)));
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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)));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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()));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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