goldenChat base source add
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# ta4j-core
|
||||
|
||||
`ta4j-core` contains the production API surface for strategy modeling, backtesting, analysis, and live-style record management.
|
||||
|
||||
## Start here
|
||||
|
||||
- Series model: `BarSeries`, `BaseBarSeries`, `ConcurrentBarSeries`
|
||||
- Strategy model: `Indicator`, `Rule`, `Strategy`
|
||||
- Execution model: `BarSeriesManager`, `BacktestExecutor`, `TradeExecutionModel`
|
||||
- Trade/fill model: `TradingRecord`, `BaseTradingRecord`, `Trade`, `TradeFill`
|
||||
- Analysis model: `AnalysisCriterion` and criteria packages
|
||||
|
||||
## Choose the right execution path
|
||||
|
||||
- Single strategy over one series: use `BarSeriesManager`
|
||||
- Many candidates, tuning, weighted ranking: use `BacktestExecutor`
|
||||
- Broker-confirmed/partial-fill replay: use manual evaluation loop + `BaseTradingRecord.operate(fill)`
|
||||
|
||||
## Live evaluation semantics (important)
|
||||
|
||||
- ta4j evaluates the bar state you provide at the requested index; it does not force closed-candle-only evaluation.
|
||||
- If your feed uses `addBar(bar, true)` or equivalent replace-last-bar updates, you are evaluating a live (still-forming) candle.
|
||||
- If you evaluate only after adding a completed bar, you are evaluating closed candles.
|
||||
- For live execution, call `shouldEnter(index, tradingRecord)` / `shouldExit(index, tradingRecord)` and keep `tradingRecord` synchronized with broker-confirmed fills.
|
||||
- Add an integration guard (for example, one entry per bar index) to avoid duplicate orders when a live candle keeps the same rule state across multiple updates.
|
||||
|
||||
## Trace rule decisions
|
||||
|
||||
- To answer "why did this fire?" or "why did this not fire?", enable SLF4J `TRACE` on the relevant `Rule` or `Strategy` logger and run the normal `isSatisfied(...)`, `shouldEnter(...)`, or `shouldExit(...)` call.
|
||||
- TRACE logging is the off switch; there is no mutable trace mode to set on shared rule or strategy instances.
|
||||
- Default trace output is `Rule.TraceMode.VERBOSE`, which emits the evaluated rule plus child-rule path/depth fields where a composite rule evaluates children.
|
||||
- Use `Rule#isSatisfiedWithTraceMode(..., Rule.TraceMode.SUMMARY)` or `Strategy#shouldEnterWithTraceMode(...)` / `shouldExitWithTraceMode(...)` for a one-shot parent summary when child logs would be too noisy.
|
||||
- Price and numeric comparison rules include the values they compared, the operator or window, and a short `reason` so a single rule trace line explains the decision.
|
||||
- Stop rules include flat `key=value` decision fields such as `currentPrice`, `entryPrice`, `stopPrice`, `side`, trailing extremes, and configured amount or percentage fields.
|
||||
|
||||
## Choose the right series type
|
||||
|
||||
- Single-threaded backtests and deterministic local runs: `BaseBarSeries`
|
||||
- Concurrent ingestion/evaluation pipelines: `ConcurrentBarSeries`
|
||||
|
||||
## Choose the right numeric model
|
||||
|
||||
- Precision-first workflows: `DecimalNum`
|
||||
- Throughput-first workflows with accepted floating-point tradeoffs: `DoubleNum`
|
||||
|
||||
## Choose the right correlation metric
|
||||
|
||||
All rolling correlation indicators live under
|
||||
`org.ta4j.core.indicators.statistics` and return `NaN` when the requested
|
||||
window is not ready or the statistic is undefined.
|
||||
|
||||
| Question | Indicator | Notes |
|
||||
| --- | --- | --- |
|
||||
| Are two continuous signals linearly related in the same window? | `CorrelationCoefficientIndicator` | Pearson-style baseline for dense, simultaneous numeric series |
|
||||
| Is the relationship monotonic but not necessarily linear? | `SpearmanRankCorrelationIndicator` | Uses average ranks for ties before applying Pearson correlation |
|
||||
| Do ordered samples agree when ties matter? | `KendallTauIndicator` | Rolling Kendall tau-b with tie correction |
|
||||
| Does one signal lead or trail another by a fixed number of bars? | `LaggedCorrelationIndicator` | Positive lag means the first indicator leads the second |
|
||||
| Do two signals share non-linear structure? | `DistanceCorrelationIndicator` | Builds centered pairwise distance matrices; `O(window^2)` per calculated index |
|
||||
| Does knowing one discretized state reduce uncertainty about another? | `MutualInformationIndicator` | Equal-width bins for v1; reports natural-log mutual information in nats |
|
||||
| Does correlation only matter inside a trend, volatility, or custom state? | `RegimeSegmentedCorrelationIndicator` | Filters each rolling window with an `Indicator<Boolean>` regime selector |
|
||||
|
||||
## Companion user guides
|
||||
|
||||
- Backtesting: https://ta4j.github.io/ta4j-wiki/Backtesting.html
|
||||
- Live trading: https://ta4j.github.io/ta4j-wiki/Live-trading.html
|
||||
- Risk/criteria: https://ta4j.github.io/ta4j-wiki/Analysis-Criteria-and-Risk-Metrics.html
|
||||
@@ -0,0 +1,139 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.ta4j</groupId>
|
||||
<artifactId>ta4j-parent</artifactId>
|
||||
<version>0.22.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ta4j-core</artifactId>
|
||||
|
||||
<name>Ta4j Core</name>
|
||||
<description>ta4j is a Java library providing a simple API for technical analysis.</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Logging facade -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.opencsv</groupId>
|
||||
<artifactId>opencsv</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Override parent: header/config live in parent dir (avoids CWD dependency when building from workspace root) -->
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<licenseSets>
|
||||
<licenseSet>
|
||||
<header>${project.parent.basedir}/license-header.txt</header>
|
||||
<includes>
|
||||
<include>**/*.java</include>
|
||||
</includes>
|
||||
</licenseSet>
|
||||
</licenseSets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>net.revelc.code.formatter</groupId>
|
||||
<artifactId>formatter-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<configFile>${project.parent.basedir}/code-formatter.xml</configFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>test-jar</id>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>biz.aQute.bnd</groupId>
|
||||
<artifactId>bnd-maven-plugin</artifactId>
|
||||
<version>7.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-bnd-process</id>
|
||||
<goals>
|
||||
<goal>bnd-process</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<bnd>
|
||||
<![CDATA[
|
||||
-exportcontents: ${packages;NAMED;*org.ta4j.core*}
|
||||
]]>
|
||||
</bnd>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,37 @@
|
||||
# AGENTS instructions for `org.ta4j.core`
|
||||
|
||||
Applies to this package unless a deeper `AGENTS.md` overrides it.
|
||||
|
||||
## API and implementation conventions
|
||||
|
||||
- Mirror existing `BarSeries` builder patterns. New series types should include a dedicated builder in the same package.
|
||||
- Extend `BaseBarSeries` for new series implementations to reuse validation and index/removal behavior.
|
||||
- Keep concurrent access safe: guard writes with `ReentrantReadWriteLock`, guard read-mostly paths with read locks, and return immutable snapshots from `getBarData()`.
|
||||
- Add `@since <current-version-without-SNAPSHOT>` to each new public class or method in this scope.
|
||||
- Treat Javadoc as part of the API: new public APIs and behavior changes must include clear intent/usage documentation.
|
||||
|
||||
## Coding style and model rules
|
||||
|
||||
- Use loggers (`LogManager` / `Logger`) instead of `System.out` / `System.err`.
|
||||
- Prefer imports over fully qualified class names in implementation code.
|
||||
- Prefer explicit local variable types. Use `var` only when the inferred type is immediately obvious and meaningfully reduces noise.
|
||||
- When a valid reference `Num` or `BarSeries` already exists, derive the `NumFactory` from that reference instead of creating a parallel default factory.
|
||||
- For numerical domain code, keep calculations in `Num` and derive constants from the active `NumFactory`; convert to primitive `double` only when the required operation is unavailable in `Num`, and document why at the call site.
|
||||
- Do not keep generic null/NaN fallback helpers when the call site already guarantees a usable reference; encode the stronger invariant directly at the call site.
|
||||
- For DTO/model carrier types, prefer immutable shapes: `record` first, then public final fields, then mutable getter/setter models only when required.
|
||||
- For component metadata JSON, use helpers from `org.ta4j.core.serialization` (`ComponentDescriptor`, `ComponentSerialization`) instead of hand-rolled structures.
|
||||
|
||||
## Core surface-area gate (MUST)
|
||||
|
||||
- New top-level classes in `org.ta4j.core` are a last resort.
|
||||
- Prefer package-private nested helpers or existing extension points before adding new top-level core types.
|
||||
- Reuse existing shared APIs first (for example shared enums) rather than introducing parallel concepts.
|
||||
- If adding a new top-level core type is unavoidable, include a short rationale in the active PRD/checklist or PR notes.
|
||||
|
||||
## Scoped guides
|
||||
|
||||
- Read deeper package guides before editing these areas:
|
||||
- `indicators/AGENTS.md`
|
||||
- `serialization/AGENTS.md`
|
||||
- `strategy/named/AGENTS.md`
|
||||
- `bars/AGENTS.md`
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.AnalysisContext;
|
||||
import org.ta4j.core.analysis.AnalysisWindow;
|
||||
import org.ta4j.core.analysis.AnalysisContext.MissingHistoryPolicy;
|
||||
import org.ta4j.core.analysis.AnalysisContext.PositionInclusionPolicy;
|
||||
import org.ta4j.core.analysis.OpenPositionHandling;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.backtest.BarSeriesManager;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* An analysis criterion. It can be used to:
|
||||
*
|
||||
* <ul>
|
||||
* <li>analyze the performance of a {@link Strategy strategy}
|
||||
* <li>compare several {@link Strategy strategies} together
|
||||
* </ul>
|
||||
*/
|
||||
public interface AnalysisCriterion {
|
||||
|
||||
/** Filter to differentiate between winning or losing positions. */
|
||||
enum PositionFilter {
|
||||
/** Consider only winning positions. */
|
||||
PROFIT,
|
||||
/** Consider only losing positions. */
|
||||
LOSS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param series the bar series, not null
|
||||
* @param position the position, not null
|
||||
* @return the criterion value for the position
|
||||
*/
|
||||
Num calculate(BarSeries series, Position position);
|
||||
|
||||
/**
|
||||
* @param series the bar series, not null
|
||||
* @param tradingRecord the trading record, not null
|
||||
* @return the criterion value for the positions
|
||||
*/
|
||||
Num calculate(BarSeries series, TradingRecord tradingRecord);
|
||||
|
||||
/**
|
||||
* Calculates this criterion over a specific analysis window using
|
||||
* {@link AnalysisContext#defaults() default context options}.
|
||||
*
|
||||
* <p>
|
||||
* Examples:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Past 7 days:
|
||||
* {@code criterion.calculate(series, record, AnalysisWindow.lookbackDuration(Duration.ofDays(7)))}</li>
|
||||
* <li>Past 30 days:
|
||||
* {@code criterion.calculate(series, record, AnalysisWindow.lookbackDuration(Duration.ofDays(30)))}</li>
|
||||
* <li>Explicit date range:
|
||||
* {@code criterion.calculate(series, record, AnalysisWindow.timeRange(Instant.parse("2026-02-10T00:00:00Z"), Instant.parse("2026-02-14T00:00:00Z")))}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param series the bar series, not null
|
||||
* @param tradingRecord the trading record, not null
|
||||
* @param window the requested analysis window, not null
|
||||
* @return the criterion value for the window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default Num calculate(BarSeries series, TradingRecord tradingRecord, AnalysisWindow window) {
|
||||
return calculate(series, tradingRecord, window, AnalysisContext.defaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates this criterion over a specific analysis window.
|
||||
*
|
||||
* <p>
|
||||
* Window boundaries follow:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>bar indices: start inclusive, end inclusive</li>
|
||||
* <li>time windows: start inclusive, end exclusive (bar membership is based on
|
||||
* bar end time)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* On constrained or moving series (for example when
|
||||
* {@link BarSeries#setMaximumBarCount(int)} removed historical bars), missing
|
||||
* history is handled according to
|
||||
* {@link AnalysisContext#missingHistoryPolicy()}:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link AnalysisContext.MissingHistoryPolicy#STRICT}: fails when requested
|
||||
* history is unavailable</li>
|
||||
* <li>{@link AnalysisContext.MissingHistoryPolicy#CLAMP}: intersects requested
|
||||
* range with available logical indices</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param series the bar series, not null
|
||||
* @param tradingRecord the trading record, not null
|
||||
* @param window the requested analysis window, not null
|
||||
* @param context window resolution and projection options, not null
|
||||
* @return the criterion value for the window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default Num calculate(BarSeries series, TradingRecord tradingRecord, AnalysisWindow window,
|
||||
AnalysisContext context) {
|
||||
Objects.requireNonNull(series, "series");
|
||||
Objects.requireNonNull(tradingRecord, "tradingRecord");
|
||||
Objects.requireNonNull(window, "window");
|
||||
Objects.requireNonNull(context, "context");
|
||||
|
||||
if (series.isEmpty()) {
|
||||
return calculate(series, tradingRecord);
|
||||
}
|
||||
|
||||
int[] resolvedWindow = resolveWindow(series, window, context);
|
||||
int windowStartIndex = resolvedWindow[0];
|
||||
int windowEndIndex = resolvedWindow[1];
|
||||
boolean hasBars = resolvedWindow[2] == 1;
|
||||
TradingRecord projectedRecord = projectTradingRecord(series, tradingRecord, windowStartIndex, windowEndIndex,
|
||||
hasBars, context);
|
||||
return calculate(series, projectedRecord);
|
||||
}
|
||||
|
||||
private static int[] resolveWindow(BarSeries series, AnalysisWindow window, AnalysisContext context) {
|
||||
int availableStart = series.getBeginIndex();
|
||||
int availableEnd = series.getEndIndex();
|
||||
|
||||
int requestedStart;
|
||||
int requestedEnd;
|
||||
boolean requestedEmpty;
|
||||
boolean lowerBoundBeforeAvailable;
|
||||
boolean upperBoundAfterAvailable;
|
||||
|
||||
switch (window) {
|
||||
case AnalysisWindow.BarRange barRange:
|
||||
requestedStart = barRange.startIndexInclusive();
|
||||
requestedEnd = barRange.endIndexInclusive();
|
||||
requestedEmpty = false;
|
||||
lowerBoundBeforeAvailable = requestedStart < availableStart;
|
||||
upperBoundAfterAvailable = requestedEnd > availableEnd;
|
||||
break;
|
||||
case AnalysisWindow.LookbackBars lookbackBars:
|
||||
Instant lookbackBarsAsOf = context.asOf();
|
||||
Instant availableStartTimeForBars = series.getBar(availableStart).getEndTime();
|
||||
Instant availableEndExclusiveForBars = series.getBar(availableEnd).getEndTime().plusNanos(1);
|
||||
lowerBoundBeforeAvailable = lookbackBarsAsOf != null
|
||||
&& lookbackBarsAsOf.isBefore(availableStartTimeForBars);
|
||||
upperBoundAfterAvailable = lookbackBarsAsOf != null
|
||||
&& lookbackBarsAsOf.isAfter(availableEndExclusiveForBars);
|
||||
|
||||
int lookbackBarsAnchor = lookbackBarsAsOf == null ? availableEnd
|
||||
: findLastIndexAtOrBefore(series, lookbackBarsAsOf, availableStart, availableEnd);
|
||||
if (lookbackBarsAnchor < 0) {
|
||||
requestedStart = availableStart;
|
||||
requestedEnd = availableStart - 1;
|
||||
requestedEmpty = true;
|
||||
lowerBoundBeforeAvailable = true;
|
||||
break;
|
||||
}
|
||||
|
||||
requestedStart = lookbackBarsAnchor - lookbackBars.barCount() + 1;
|
||||
requestedEnd = lookbackBarsAnchor;
|
||||
requestedEmpty = false;
|
||||
lowerBoundBeforeAvailable = lowerBoundBeforeAvailable || requestedStart < availableStart;
|
||||
upperBoundAfterAvailable = upperBoundAfterAvailable || requestedEnd > availableEnd;
|
||||
break;
|
||||
case AnalysisWindow.TimeRange timeRange:
|
||||
requestedStart = findFirstIndexAtOrAfter(series, timeRange.startInclusive(), availableStart, availableEnd);
|
||||
requestedEnd = findLastIndexBefore(series, timeRange.endExclusive(), availableStart, availableEnd);
|
||||
requestedEmpty = requestedStart < 0 || requestedEnd < 0 || requestedStart > requestedEnd;
|
||||
lowerBoundBeforeAvailable = timeRange.startInclusive().isBefore(series.getBar(availableStart).getEndTime());
|
||||
upperBoundAfterAvailable = timeRange.endExclusive()
|
||||
.isAfter(series.getBar(availableEnd).getEndTime().plusNanos(1));
|
||||
if (requestedEmpty) {
|
||||
requestedStart = availableStart;
|
||||
requestedEnd = availableStart - 1;
|
||||
}
|
||||
break;
|
||||
case AnalysisWindow.LookbackDuration lookbackDuration:
|
||||
Instant endExclusive = context.asOf() != null ? context.asOf()
|
||||
: series.getBar(availableEnd).getEndTime().plusNanos(1);
|
||||
Instant startInclusive = endExclusive.minus(lookbackDuration.duration());
|
||||
requestedStart = findFirstIndexAtOrAfter(series, startInclusive, availableStart, availableEnd);
|
||||
requestedEnd = findLastIndexBefore(series, endExclusive, availableStart, availableEnd);
|
||||
requestedEmpty = requestedStart < 0 || requestedEnd < 0 || requestedStart > requestedEnd;
|
||||
lowerBoundBeforeAvailable = startInclusive.isBefore(series.getBar(availableStart).getEndTime());
|
||||
upperBoundAfterAvailable = endExclusive.isAfter(series.getBar(availableEnd).getEndTime().plusNanos(1));
|
||||
if (requestedEmpty) {
|
||||
requestedStart = availableStart;
|
||||
requestedEnd = availableStart - 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (context.missingHistoryPolicy() == MissingHistoryPolicy.STRICT
|
||||
&& (lowerBoundBeforeAvailable || upperBoundAfterAvailable)) {
|
||||
throw unavailableHistoryException(requestedStart, requestedEnd, availableStart, availableEnd);
|
||||
}
|
||||
|
||||
int resolvedStart = requestedStart;
|
||||
int resolvedEnd = requestedEnd;
|
||||
if (context.missingHistoryPolicy() == MissingHistoryPolicy.CLAMP) {
|
||||
resolvedStart = Math.max(resolvedStart, availableStart);
|
||||
resolvedEnd = Math.min(resolvedEnd, availableEnd);
|
||||
}
|
||||
|
||||
if (context.missingHistoryPolicy() == MissingHistoryPolicy.STRICT
|
||||
&& (resolvedStart < availableStart || resolvedEnd > availableEnd)) {
|
||||
throw unavailableHistoryException(requestedStart, requestedEnd, availableStart, availableEnd);
|
||||
}
|
||||
|
||||
if (requestedEmpty || resolvedStart > resolvedEnd) {
|
||||
int anchor = Math.min(Math.max(resolvedStart, availableStart), availableEnd);
|
||||
return new int[] { anchor, anchor, 0 };
|
||||
}
|
||||
return new int[] { resolvedStart, resolvedEnd, 1 };
|
||||
}
|
||||
|
||||
private static IllegalArgumentException unavailableHistoryException(int requestedStart, int requestedEnd,
|
||||
int availableStart, int availableEnd) {
|
||||
String message = String.format("Requested window [%d, %d] is outside available series range [%d, %d]",
|
||||
requestedStart, requestedEnd, availableStart, availableEnd);
|
||||
return new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
private static int findLastIndexAtOrBefore(BarSeries series, Instant asOf, int availableStart, int availableEnd) {
|
||||
for (int i = availableEnd; i >= availableStart; i--) {
|
||||
if (!series.getBar(i).getEndTime().isAfter(asOf)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findFirstIndexAtOrAfter(BarSeries series, Instant startInclusive, int availableStart,
|
||||
int availableEnd) {
|
||||
for (int i = availableStart; i <= availableEnd; i++) {
|
||||
if (!series.getBar(i).getEndTime().isBefore(startInclusive)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findLastIndexBefore(BarSeries series, Instant endExclusive, int availableStart,
|
||||
int availableEnd) {
|
||||
for (int i = availableEnd; i >= availableStart; i--) {
|
||||
if (series.getBar(i).getEndTime().isBefore(endExclusive)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static TradingRecord projectTradingRecord(BarSeries series, TradingRecord source, int start, int end,
|
||||
boolean hasBars, AnalysisContext context) {
|
||||
CostModel transactionCostModel = Objects.requireNonNullElseGet(source.getTransactionCostModel(),
|
||||
ZeroCostModel::new);
|
||||
CostModel holdingCostModel = Objects.requireNonNullElseGet(source.getHoldingCostModel(), ZeroCostModel::new);
|
||||
BaseTradingRecord projectedRecord = new BaseTradingRecord(source.getStartingType(), start, end,
|
||||
transactionCostModel, holdingCostModel);
|
||||
if (!hasBars) {
|
||||
return projectedRecord;
|
||||
}
|
||||
|
||||
PositionInclusionPolicy inclusionPolicy = context.positionInclusionPolicy();
|
||||
List<Position> includedPositions = new ArrayList<>();
|
||||
for (Position position : source.getPositions()) {
|
||||
if (includeClosedPosition(position, start, end, inclusionPolicy)) {
|
||||
includedPositions.add(position);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.openPositionHandling() == OpenPositionHandling.MARK_TO_MARKET) {
|
||||
List<Position> openPositions = openPositionsForMarkToMarket(source, end, transactionCostModel,
|
||||
holdingCostModel);
|
||||
for (Position openPosition : openPositions) {
|
||||
Position syntheticPosition = createMarkToMarketPosition(series, openPosition, end, holdingCostModel);
|
||||
if (syntheticPosition != null
|
||||
&& includeClosedPosition(syntheticPosition, start, end, inclusionPolicy)) {
|
||||
includedPositions.add(syntheticPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
includedPositions.sort(Comparator.comparingInt(position -> position.getExit().getIndex()));
|
||||
for (Position position : includedPositions) {
|
||||
Trade entry = position.getEntry();
|
||||
Trade exit = position.getExit();
|
||||
projectedRecord.operate(entry);
|
||||
projectedRecord.operate(exit);
|
||||
}
|
||||
return projectedRecord;
|
||||
}
|
||||
|
||||
private static Position createMarkToMarketPosition(BarSeries series, Position currentPosition, int windowEndIndex,
|
||||
CostModel holdingCostModel) {
|
||||
if (currentPosition == null || !currentPosition.isOpened()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Trade entryTrade = currentPosition.getEntry();
|
||||
if (entryTrade == null || entryTrade.getIndex() > windowEndIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Num amount = entryTrade.getAmount();
|
||||
Num closePrice = series.getBar(windowEndIndex).getClosePrice();
|
||||
CostModel transactionCostModel = entryTrade.getCostModel();
|
||||
Trade syntheticExit = entryTrade.isBuy()
|
||||
? Trade.sellAt(windowEndIndex, closePrice, amount, transactionCostModel)
|
||||
: Trade.buyAt(windowEndIndex, closePrice, amount, transactionCostModel);
|
||||
return new Position(entryTrade, syntheticExit, transactionCostModel, holdingCostModel);
|
||||
}
|
||||
|
||||
private static List<Position> openPositionsForMarkToMarket(TradingRecord source, int windowEndIndex,
|
||||
CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
List<Position> openPositions = source.getOpenPositions();
|
||||
if (!openPositions.isEmpty()) {
|
||||
return openPositionsWithinWindow(openPositions, windowEndIndex);
|
||||
}
|
||||
Position currentPosition = source.getCurrentPosition();
|
||||
if (currentPosition == null || !currentPosition.isOpened()) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(currentPosition);
|
||||
}
|
||||
|
||||
private static List<Position> openPositionsWithinWindow(List<Position> openPositions, int windowEndIndex) {
|
||||
List<Position> positions = new ArrayList<>();
|
||||
for (Position openPosition : openPositions) {
|
||||
if (openPosition == null || !openPosition.isOpened()) {
|
||||
continue;
|
||||
}
|
||||
if (openPosition.getEntry().getIndex() > windowEndIndex) {
|
||||
continue;
|
||||
}
|
||||
positions.add(openPosition);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private static boolean includeClosedPosition(Position position, int start, int end,
|
||||
PositionInclusionPolicy positionInclusionPolicy) {
|
||||
if (position == null || !position.isClosed()) {
|
||||
return false;
|
||||
}
|
||||
int entry = position.getEntry().getIndex();
|
||||
int exit = position.getExit().getIndex();
|
||||
return switch (positionInclusionPolicy) {
|
||||
case EXIT_IN_WINDOW -> exit >= start && exit <= end;
|
||||
case FULLY_CONTAINED -> entry >= start && exit <= end;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param manager the bar series manager with entry type of BUY
|
||||
* @param strategies a list of strategies
|
||||
* @return the best strategy (among the provided ones) according to the
|
||||
* criterion
|
||||
*/
|
||||
default Strategy chooseBest(BarSeriesManager manager, List<Strategy> strategies) {
|
||||
return chooseBest(manager, TradeType.BUY, strategies);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param manager the bar series manager
|
||||
* @param tradeType the entry type (BUY or SELL) of the first trade in the
|
||||
* trading session
|
||||
* @param strategies a list of strategies
|
||||
* @return the best strategy (among the provided ones) according to the
|
||||
* criterion
|
||||
*/
|
||||
default Strategy chooseBest(BarSeriesManager manager, TradeType tradeType, List<Strategy> strategies) {
|
||||
Strategy bestStrategy = strategies.getFirst();
|
||||
Num bestCriterionValue = calculate(manager.getBarSeries(), manager.run(bestStrategy));
|
||||
|
||||
for (int i = 1; i < strategies.size(); i++) {
|
||||
Strategy currentStrategy = strategies.get(i);
|
||||
Num currentCriterionValue = calculate(manager.getBarSeries(), manager.run(currentStrategy, tradeType));
|
||||
|
||||
if (betterThan(currentCriterionValue, bestCriterionValue)) {
|
||||
bestStrategy = currentStrategy;
|
||||
bestCriterionValue = currentCriterionValue;
|
||||
}
|
||||
}
|
||||
|
||||
return bestStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param criterionValue1 the first value
|
||||
* @param criterionValue2 the second value
|
||||
* @return true if the first value is better than (according to the criterion)
|
||||
* the second one, false otherwise
|
||||
*/
|
||||
boolean betterThan(Num criterionValue1, Num criterionValue2);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A {@code Bar} is aggregated open/high/low/close/volume/etc. data over a time
|
||||
* period. It represents the "end bar" of a time period.
|
||||
*/
|
||||
public interface Bar extends Serializable {
|
||||
|
||||
/**
|
||||
* @return the time period of the bar
|
||||
*/
|
||||
Duration getTimePeriod();
|
||||
|
||||
/**
|
||||
* @return the begin timestamp of the bar period (in UTC).
|
||||
*/
|
||||
Instant getBeginTime();
|
||||
|
||||
/**
|
||||
* @return the end timestamp of the bar period (in UTC).
|
||||
*/
|
||||
Instant getEndTime();
|
||||
|
||||
/**
|
||||
* @return the open price of the bar period
|
||||
*/
|
||||
Num getOpenPrice();
|
||||
|
||||
/**
|
||||
* @return the high price of the bar period
|
||||
*/
|
||||
Num getHighPrice();
|
||||
|
||||
/**
|
||||
* @return the low price of the bar period
|
||||
*/
|
||||
Num getLowPrice();
|
||||
|
||||
/**
|
||||
* @return the close price of the bar period
|
||||
*/
|
||||
Num getClosePrice();
|
||||
|
||||
/**
|
||||
* @return the total traded volume of the bar period
|
||||
*/
|
||||
Num getVolume();
|
||||
|
||||
/**
|
||||
* @return the total traded amount (tradePrice x tradeVolume) of the bar period
|
||||
*/
|
||||
Num getAmount();
|
||||
|
||||
/**
|
||||
* @return the number of trades of the bar period
|
||||
*/
|
||||
long getTrades();
|
||||
|
||||
/**
|
||||
* @param timestamp a timestamp
|
||||
* @return true if the provided timestamp is between the begin time and the end
|
||||
* time of the current period, false otherwise
|
||||
*/
|
||||
default boolean inPeriod(Instant timestamp) {
|
||||
return timestamp != null && !timestamp.isBefore(getBeginTime()) && timestamp.isBefore(getEndTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the bar's begin time in UTC as {@link ZonedDateTime}
|
||||
*/
|
||||
default ZonedDateTime getZonedBeginTime() {
|
||||
return getBeginTime().atZone(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the bar's end time in UTC as {@link ZonedDateTime}
|
||||
*/
|
||||
default ZonedDateTime getZonedEndTime() {
|
||||
return getEndTime().atZone(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the begin time of the bar to a time in the system's time zone.
|
||||
*
|
||||
* <p>
|
||||
* <b>Warning:</b> The use of {@link ZoneId#systemDefault()} may introduce
|
||||
* variability based on the system's default time zone settings. This can result
|
||||
* in inconsistencies in time calculations and comparisons, particularly due to
|
||||
* daylight saving time (DST). It is recommended to always utilize either
|
||||
* {@link #getBeginTime()} or {@link #getZonedBeginTime()} for accurate results.
|
||||
*
|
||||
* @return the bar's begin time converted to system time zone
|
||||
*/
|
||||
default ZonedDateTime getSystemZonedBeginTime() {
|
||||
return getBeginTime().atZone(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the end time of the bar to a time in the system's time zone.
|
||||
*
|
||||
* <p>
|
||||
* <b>Warning:</b> The use of {@link ZoneId#systemDefault()} may introduce
|
||||
* variability based on the system's default time zone settings. This can result
|
||||
* in inconsistencies in time calculations and comparisons, particularly due to
|
||||
* daylight saving time (DST). It is recommended to always utilize either
|
||||
* {@link #getEndTime()} or {@link #getZonedEndTime()} for accurate results.
|
||||
*
|
||||
* @return the bar's end time converted to system time zone
|
||||
*/
|
||||
default ZonedDateTime getSystemZonedEndTime() {
|
||||
return getEndTime().atZone(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a user-friendly representation of the end timestamp in the system's
|
||||
* time zone
|
||||
*/
|
||||
default String getDateName() {
|
||||
return getSystemZonedEndTime().format(DateTimeFormatter.ISO_DATE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an even more user-friendly representation of the end timestamp in the
|
||||
* system's time zone
|
||||
*/
|
||||
default String getSimpleDateName() {
|
||||
return getSystemZonedEndTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this is a bearish bar, false otherwise
|
||||
*/
|
||||
default boolean isBearish() {
|
||||
Num openPrice = getOpenPrice();
|
||||
Num closePrice = getClosePrice();
|
||||
return (openPrice != null) && (closePrice != null) && closePrice.isLessThan(openPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this is a bullish bar, false otherwise
|
||||
*/
|
||||
default boolean isBullish() {
|
||||
Num openPrice = getOpenPrice();
|
||||
Num closePrice = getClosePrice();
|
||||
return (openPrice != null) && (closePrice != null) && openPrice.isLessThan(closePrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a trade and updates the close price at the end of the bar period.
|
||||
*
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the actual price per asset
|
||||
*/
|
||||
void addTrade(Num tradeVolume, Num tradePrice);
|
||||
|
||||
/**
|
||||
* Updates the close price at the end of the bar period. The open, high and low
|
||||
* prices are also updated as needed.
|
||||
*
|
||||
* @param price the actual price per asset
|
||||
* @param numFunction the numbers precision
|
||||
*/
|
||||
default void addPrice(String price, Function<Number, Num> numFunction) {
|
||||
addPrice(numFunction.apply(new BigDecimal(price)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the close price at the end of the bar period. The open, high and low
|
||||
* prices are also updated as needed.
|
||||
*
|
||||
* @param price the actual price per asset
|
||||
* @param numFunction the numbers precision
|
||||
*/
|
||||
default void addPrice(Number price, Function<Number, Num> numFunction) {
|
||||
addPrice(numFunction.apply(price));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the close price at the end of the bar period. The open, high and low
|
||||
* prices are also updated as needed.
|
||||
*
|
||||
* @param price the actual price per asset
|
||||
*/
|
||||
void addPrice(Num price);
|
||||
|
||||
/**
|
||||
* Returns the {@link NumFactory} associated with the first available price
|
||||
* field on the bar.
|
||||
*
|
||||
* @return the {@link NumFactory} derived from the bar's numeric values
|
||||
* @throws IllegalArgumentException if no price fields are available to
|
||||
* determine a factory
|
||||
*
|
||||
* @since 0.22.1
|
||||
*/
|
||||
default NumFactory numFactory() {
|
||||
var open = getOpenPrice();
|
||||
if (open != null) {
|
||||
return open.getNumFactory();
|
||||
}
|
||||
var close = getClosePrice();
|
||||
if (close != null) {
|
||||
return close.getNumFactory();
|
||||
}
|
||||
var high = getHighPrice();
|
||||
if (high != null) {
|
||||
return high.getNumFactory();
|
||||
}
|
||||
var low = getLowPrice();
|
||||
if (low != null) {
|
||||
return low.getNumFactory();
|
||||
}
|
||||
var volume = getVolume();
|
||||
if (volume != null) {
|
||||
return volume.getNumFactory();
|
||||
}
|
||||
var amount = getAmount();
|
||||
if (amount != null) {
|
||||
return amount.getNumFactory();
|
||||
}
|
||||
throw new IllegalArgumentException("Cannot select a NumFactory: no price fields are available on the bar.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Builder for one OHLCV bar.
|
||||
*
|
||||
* <p>
|
||||
* Typical usage in backtests is to set period/time plus OHLCV fields and call
|
||||
* {@link #add()}. For real-time trade ingestion, prefer series-level ingestion
|
||||
* APIs such as
|
||||
* {@link ConcurrentBarSeries#ingestTrade(java.time.Instant, Number, Number)} so
|
||||
* rollover logic stays consistent.
|
||||
* </p>
|
||||
*/
|
||||
public interface BarBuilder {
|
||||
/**
|
||||
* @param timePeriod the time period (optional if {@link #beginTime(Instant)}
|
||||
* and {@link #endTime(Instant)} are given)
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder timePeriod(Duration timePeriod);
|
||||
|
||||
/**
|
||||
* @param beginTime the begin time of the bar period (optional if
|
||||
* {@link #endTime(Instant)} is given)
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder beginTime(Instant beginTime);
|
||||
|
||||
/**
|
||||
* @param endTime the end time of the bar period (optional if
|
||||
* {@link #beginTime(Instant)} is given)
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder endTime(Instant endTime);
|
||||
|
||||
/**
|
||||
* @param openPrice the open price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder openPrice(Num openPrice);
|
||||
|
||||
/**
|
||||
* @param openPrice the open price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder openPrice(Number openPrice);
|
||||
|
||||
/**
|
||||
* @param openPrice the open price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder openPrice(String openPrice);
|
||||
|
||||
/**
|
||||
* @param highPrice the highest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder highPrice(Number highPrice);
|
||||
|
||||
/**
|
||||
* @param highPrice the highest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder highPrice(String highPrice);
|
||||
|
||||
/**
|
||||
* @param highPrice the highest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder highPrice(Num highPrice);
|
||||
|
||||
/**
|
||||
* @param lowPrice the lowest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder lowPrice(Num lowPrice);
|
||||
|
||||
/**
|
||||
* @param lowPrice the lowest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder lowPrice(Number lowPrice);
|
||||
|
||||
/**
|
||||
* @param lowPrice the lowest price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder lowPrice(String lowPrice);
|
||||
|
||||
/**
|
||||
* @param closePrice the close price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder closePrice(Num closePrice);
|
||||
|
||||
/**
|
||||
* @param closePrice the close price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder closePrice(Number closePrice);
|
||||
|
||||
/**
|
||||
* @param closePrice the close price of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder closePrice(String closePrice);
|
||||
|
||||
/**
|
||||
* @param volume the total traded volume of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder volume(Num volume);
|
||||
|
||||
/**
|
||||
* @param volume the total traded volume of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder volume(Number volume);
|
||||
|
||||
/**
|
||||
* @param volume the total traded volume of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder volume(String volume);
|
||||
|
||||
/**
|
||||
* @param amount the total traded amount of the bar period (if {@code null},
|
||||
* then it is calculated by {@code closePrice * volume})
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder amount(Num amount);
|
||||
|
||||
/**
|
||||
* @param amount the total traded amount of the bar period (if {@code null},
|
||||
* then it is calculated by {@code closePrice * volume})
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder amount(Number amount);
|
||||
|
||||
/**
|
||||
* @param amount the total traded amount of the bar period (if {@code null},
|
||||
* then it is calculated by {@code closePrice * volume})
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder amount(String amount);
|
||||
|
||||
/**
|
||||
* @param trades the number of trades of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder trades(long trades);
|
||||
|
||||
/**
|
||||
* @param trades the number of trades of the bar period
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder trades(String trades);
|
||||
|
||||
/**
|
||||
* Updates the builder with a trade event and adds or updates bars as needed.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default void addTrade(Instant time, Num tradeVolume, Num tradePrice) {
|
||||
throw new UnsupportedOperationException("Trade ingestion not supported by " + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the builder with a trade event and adds or updates bars as needed.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default void addTrade(Instant time, Num tradeVolume, Num tradePrice, RealtimeBar.Side side,
|
||||
RealtimeBar.Liquidity liquidity) {
|
||||
addTrade(time, tradeVolume, tradePrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param barSeries the series used for bar addition
|
||||
* @return {@code this}
|
||||
*/
|
||||
BarBuilder bindTo(BarSeries barSeries);
|
||||
|
||||
/**
|
||||
* @return bar created from obtained data
|
||||
*/
|
||||
Bar build();
|
||||
|
||||
/**
|
||||
* Builds bar with {@link #build()} and adds it to series
|
||||
*/
|
||||
void add();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* A factory that provides a builder for a bar.
|
||||
*
|
||||
* <p>
|
||||
* Pair the factory with the bar semantics you need: time bars for clock-aligned
|
||||
* candles, or alternative factories for volume/amount/tick driven aggregation.
|
||||
* In most workflows, this is configured once at series construction time.
|
||||
* </p>
|
||||
*/
|
||||
public interface BarBuilderFactory extends Serializable {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series to which the created bar should be added
|
||||
* @return the bar builder
|
||||
*/
|
||||
BarBuilder createBarBuilder(BarSeries series);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A {@code BarSeries} is a sequence of {@link Bar bars} separated by a
|
||||
* predefined period (e.g. 15 minutes, 1 day, etc.).
|
||||
*
|
||||
* Notably, it can be:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the base of {@link Indicator indicator} calculations
|
||||
* <li>constrained between beginning and ending indices (e.g. for some
|
||||
* backtesting cases)
|
||||
* <li>limited to a fixed number of bars (e.g. for actual trading)
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The bar series is the core underlying dataset in ta4j. It represents a
|
||||
* timeline of financial data (OHLCV) and acts as the source of truth for
|
||||
* indicators, backtesting runs, and live trading operations.
|
||||
* </p>
|
||||
*/
|
||||
public interface BarSeries extends Serializable {
|
||||
|
||||
/**
|
||||
* @return factory that generates numbers usable in this BarSeries
|
||||
*/
|
||||
NumFactory numFactory();
|
||||
|
||||
/**
|
||||
* @return builder that generates compatible bars
|
||||
*/
|
||||
BarBuilder barBuilder();
|
||||
|
||||
/**
|
||||
* @return the name of the series
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Gets the bar from {@link #getBarData()} with index {@code i}.
|
||||
*
|
||||
* <p>
|
||||
* The given {@code i} can return the same bar within the first range of indices
|
||||
* due to {@link #setMaximumBarCount(int)}, for example: If you fill a BarSeries
|
||||
* with 30 bars and then apply a {@code maximumBarCount} of 10 bars, the first
|
||||
* 20 bars will be removed from the BarSeries. The indices going further from 0
|
||||
* to 29 remain but return the same bar from 0 to 20. The remaining 9 bars are
|
||||
* returned from index 21.
|
||||
*
|
||||
* @param i the index
|
||||
* @return the bar at the i-th position
|
||||
*/
|
||||
Bar getBar(int i);
|
||||
|
||||
/**
|
||||
* @return the first bar of the series
|
||||
*/
|
||||
default Bar getFirstBar() {
|
||||
return getBar(getBeginIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the last bar of the series
|
||||
*/
|
||||
default Bar getLastBar() {
|
||||
return getBar(getEndIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of bars in the series
|
||||
*/
|
||||
int getBarCount();
|
||||
|
||||
/**
|
||||
* @return true if the series is empty, false otherwise
|
||||
*/
|
||||
default boolean isEmpty() {
|
||||
return getBarCount() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw bar data, i.e. it returns the current list object, which is
|
||||
* used internally to store the {@link Bar bars}. It may be:
|
||||
*
|
||||
* <ul>
|
||||
* <li>a shortened bar list if a {@code maximumBarCount} has been set.
|
||||
* <li>an extended bar list if it is a constrained bar series.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>Warning:</b> This method should be used carefully!
|
||||
*
|
||||
* @return the raw bar data
|
||||
*/
|
||||
List<Bar> getBarData();
|
||||
|
||||
/**
|
||||
* @return the begin index of the series
|
||||
*/
|
||||
int getBeginIndex();
|
||||
|
||||
/**
|
||||
* @return the end index of the series
|
||||
*/
|
||||
int getEndIndex();
|
||||
|
||||
/**
|
||||
* @return the description of the series period (e.g. "from 2014-01-21T12:00:00Z
|
||||
* to 2014-01-21T12:15:00Z"); times are in UTC.
|
||||
*/
|
||||
default String getSeriesPeriodDescription() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!getBarData().isEmpty()) {
|
||||
var endTimeFirstBar = getFirstBar().getEndTime();
|
||||
var endTimeLastBar = getLastBar().getEndTime();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
|
||||
sb.append(formatter.format(endTimeFirstBar)).append(" - ").append(formatter.format(endTimeLastBar));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the description of the series period (e.g. "from 12:00 21/01/2014 to
|
||||
* 12:15 21/01/2014"); times are in system's default time zone.
|
||||
*/
|
||||
default String getSeriesPeriodDescriptionInSystemTimeZone() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!getBarData().isEmpty()) {
|
||||
var endTimeFirstBar = getFirstBar().getSystemZonedEndTime();
|
||||
var endTimeLastBar = getLastBar().getSystemZonedEndTime();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
|
||||
sb.append(formatter.format(endTimeFirstBar)).append(" - ").append(formatter.format(endTimeLastBar));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of bars
|
||||
*/
|
||||
int getMaximumBarCount();
|
||||
|
||||
/**
|
||||
* Sets the maximum number of bars that will be retained in the series.
|
||||
* <p>
|
||||
* If a new bar is added to the series such that the number of bars will exceed
|
||||
* the maximum bar count, then the FIRST bar in the series is automatically
|
||||
* removed, ensuring that the maximum bar count is not exceeded. The indices of
|
||||
* the bar series do not change.
|
||||
*
|
||||
* @param maximumBarCount the maximum bar count
|
||||
*/
|
||||
void setMaximumBarCount(int maximumBarCount);
|
||||
|
||||
/**
|
||||
* @return the number of removed bars
|
||||
*/
|
||||
int getRemovedBarsCount();
|
||||
|
||||
/**
|
||||
* Adds the {@code bar} at the end of the series.
|
||||
*
|
||||
* <p>
|
||||
* The {@code beginIndex} is set to {@code 0} if not already initialized.<br>
|
||||
* The {@code endIndex} is set to {@code 0} if not already initialized, or
|
||||
* incremented if it matches the end of the series.<br>
|
||||
* Exceeding bars are removed.
|
||||
*
|
||||
* @param bar the bar to be added
|
||||
* @see BarSeries#setMaximumBarCount(int)
|
||||
*/
|
||||
default void addBar(Bar bar) {
|
||||
addBar(bar, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@code bar} at the end of the series.
|
||||
*
|
||||
* <p>
|
||||
* The {@code beginIndex} is set to {@code 0} if not already initialized.<br>
|
||||
* The {@code endIndex} is set to {@code 0} if not already initialized, or
|
||||
* incremented if it matches the end of the series.<br>
|
||||
* Exceeding bars are removed.
|
||||
*
|
||||
* @param bar the bar to be added
|
||||
* @param replace true to replace the latest bar. Some exchanges continuously
|
||||
* provide new bar data in the respective period, e.g. 1 second
|
||||
* in 1 minute duration. Strategy checks run after a replace
|
||||
* therefore evaluate an in-progress bar ("live candle"), not a
|
||||
* closed candle.
|
||||
* @see BarSeries#setMaximumBarCount(int)
|
||||
*/
|
||||
void addBar(Bar bar, boolean replace);
|
||||
|
||||
/**
|
||||
* Adds a trade and updates the close price of the last bar.
|
||||
*
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the price
|
||||
* @see Bar#addTrade(Num, Num)
|
||||
*/
|
||||
default void addTrade(Number tradeVolume, Number tradePrice) {
|
||||
addTrade(numFactory().numOf(tradeVolume), numFactory().numOf(tradePrice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a trade and updates the close price of the last bar.
|
||||
*
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the price
|
||||
* @see Bar#addTrade(Num, Num)
|
||||
*/
|
||||
void addTrade(Num tradeVolume, Num tradePrice);
|
||||
|
||||
/**
|
||||
* Updates the close price of the last bar. The open, high and low prices are
|
||||
* also updated as needed.
|
||||
*
|
||||
* @param price the price for the bar
|
||||
* @see Bar#addPrice(Num)
|
||||
*/
|
||||
void addPrice(Num price);
|
||||
|
||||
/**
|
||||
* Updates the close price of the last bar. The open, high and low prices are
|
||||
* also updated as needed.
|
||||
*
|
||||
* @param price the price for the bar
|
||||
* @see Bar#addPrice(Num)
|
||||
*/
|
||||
default void addPrice(Number price) {
|
||||
addPrice(numFactory().numOf(price));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link BarSeries} instance (= "subseries") that is a subset of
|
||||
* {@code this} BarSeries instance. It contains a copy of all {@link Bar bars}
|
||||
* between {@code startIndex} (inclusive) and {@code endIndex} (exclusive) of
|
||||
* {@code this} instance. The indices of {@code this} and its subseries can be
|
||||
* different, i. e. index 0 of the subseries will be the {@code startIndex} of
|
||||
* {@code this}. If {@code startIndex} {@literal <} this.seriesBeginIndex, then
|
||||
* the subseries will start with the first available bar of {@code this}. If
|
||||
* {@code endIndex} {@literal >} this.seriesEndIndex, then the subseries will
|
||||
* end at the last available bar of {@code this}.
|
||||
*
|
||||
* @param startIndex the startIndex (inclusive)
|
||||
* @param endIndex the endIndex (exclusive)
|
||||
* @return a new BarSeries with Bars from startIndex to endIndex-1
|
||||
* @throws IllegalArgumentException if endIndex {@literal <=} startIndex or
|
||||
* startIndex {@literal <} 0
|
||||
*/
|
||||
BarSeries getSubSeries(int startIndex, int endIndex);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
/**
|
||||
* Interface to build a {@link BarSeries}.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link org.ta4j.core.BaseBarSeriesBuilder} for deterministic
|
||||
* single-threaded workflows, and
|
||||
* {@link org.ta4j.core.ConcurrentBarSeriesBuilder} when ingestion and
|
||||
* evaluation may happen concurrently.
|
||||
* </p>
|
||||
*/
|
||||
public interface BarSeriesBuilder {
|
||||
|
||||
/**
|
||||
* Builds the bar series with corresponding parameters.
|
||||
*
|
||||
* @return bar series
|
||||
*/
|
||||
BarSeries build();
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link Bar}.
|
||||
*/
|
||||
public class BaseBar implements Bar {
|
||||
|
||||
private static final long serialVersionUID = 8038383777467488147L;
|
||||
|
||||
/** The time period (e.g. 1 day, 15 min, etc.) of the bar. */
|
||||
private final Duration timePeriod;
|
||||
|
||||
/** The begin time of the bar period (in UTC). */
|
||||
private final Instant beginTime;
|
||||
|
||||
/** The end time of the bar period (in UTC). */
|
||||
private final Instant endTime;
|
||||
|
||||
/** The open price of the bar period. */
|
||||
private Num openPrice;
|
||||
|
||||
/** The high price of the bar period. */
|
||||
private Num highPrice;
|
||||
|
||||
/** The low price of the bar period. */
|
||||
private Num lowPrice;
|
||||
|
||||
/** The close price of the bar period. */
|
||||
private Num closePrice;
|
||||
|
||||
/** The total traded volume of the bar period. */
|
||||
private Num volume;
|
||||
|
||||
/** The total traded amount of the bar period. */
|
||||
private Num amount;
|
||||
|
||||
/** The number of trades of the bar period. */
|
||||
private long trades;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* <ul>
|
||||
* <li>If {@link #timePeriod} is not provided, it will be calculated as
|
||||
* {@link #endTime} - {@link #beginTime}.
|
||||
* <li>If {@link #beginTime} is not provided, it will be calculated as
|
||||
* {@link #endTime} - {@link #timePeriod}.
|
||||
* <li>If {@link #endTime} is not provided, it will be calculated as
|
||||
* {@link #beginTime} + {@link #timePeriod}.
|
||||
* </ul>
|
||||
*
|
||||
* @param timePeriod the time period (optional if beginTime and endTime is
|
||||
* given)
|
||||
* @param beginTime the begin time of the bar period (in UTC) (optional if
|
||||
* endTime is given)
|
||||
* @param endTime the end time of the bar period (in UTC) (optional if
|
||||
* beginTime is given)
|
||||
* @param openPrice the open price of the bar period
|
||||
* @param highPrice the highest price of the bar period
|
||||
* @param lowPrice the lowest price of the bar period
|
||||
* @param closePrice the close price of the bar period
|
||||
* @param volume the total traded volume of the bar period
|
||||
* @param amount the total traded amount of the bar period
|
||||
* @param trades the number of trades of the bar period
|
||||
* @throws NullPointerException if given or calculated {@link #timePeriod},
|
||||
* {@link #beginTime} or {@link #endTime}
|
||||
* values are {@code null}
|
||||
* @throws IllegalArgumentException If the calculated timePeriod between the
|
||||
* provided beginTime and endTime does not
|
||||
* match the provided timePeriod
|
||||
*/
|
||||
public BaseBar(Duration timePeriod, Instant beginTime, Instant endTime, Num openPrice, Num highPrice, Num lowPrice,
|
||||
Num closePrice, Num volume, Num amount, long trades) {
|
||||
|
||||
// set timePeriod
|
||||
if (timePeriod != null) {
|
||||
if (beginTime != null && endTime != null
|
||||
&& timePeriod.compareTo(Duration.between(beginTime, endTime)) != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"The calculated timePeriod between beginTime and endTime does not match the given timePeriod.");
|
||||
}
|
||||
this.timePeriod = timePeriod;
|
||||
} else {
|
||||
this.timePeriod = beginTime != null && endTime != null ? Duration.between(beginTime, endTime)
|
||||
: Objects.requireNonNull(timePeriod, "Time period cannot be null");
|
||||
}
|
||||
|
||||
// set beginTime
|
||||
if (beginTime == null && endTime != null) {
|
||||
this.beginTime = endTime.minus(timePeriod);
|
||||
} else {
|
||||
this.beginTime = Objects.requireNonNull(beginTime, "Begin time cannot be null");
|
||||
}
|
||||
|
||||
// set endTime
|
||||
if (beginTime != null && endTime == null) {
|
||||
this.endTime = beginTime.plus(timePeriod);
|
||||
} else {
|
||||
this.endTime = Objects.requireNonNull(endTime, "End time cannot be null");
|
||||
}
|
||||
|
||||
this.openPrice = openPrice;
|
||||
this.highPrice = highPrice;
|
||||
this.lowPrice = lowPrice;
|
||||
this.closePrice = closePrice;
|
||||
this.volume = volume;
|
||||
this.amount = amount;
|
||||
this.trades = trades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getTimePeriod() {
|
||||
return timePeriod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getBeginTime() {
|
||||
return beginTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getOpenPrice() {
|
||||
return openPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getHighPrice() {
|
||||
return highPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getLowPrice() {
|
||||
return lowPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getClosePrice() {
|
||||
return closePrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTrades() {
|
||||
return trades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(Num tradeVolume, Num tradePrice) {
|
||||
addPrice(tradePrice);
|
||||
|
||||
volume = volume.plus(tradeVolume);
|
||||
amount = amount.plus(tradeVolume.multipliedBy(tradePrice));
|
||||
trades++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrice(Num price) {
|
||||
if (openPrice == null) {
|
||||
openPrice = price;
|
||||
}
|
||||
closePrice = price;
|
||||
if (highPrice == null || highPrice.isLessThan(price)) {
|
||||
highPrice = price;
|
||||
}
|
||||
if (lowPrice == null || lowPrice.isGreaterThan(price)) {
|
||||
lowPrice = price;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {end time, close price, open price, low price, high price, volume}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"{end time: %1s, close price: %2s, open price: %3s, low price: %4s high price: %5s, volume: %6s}",
|
||||
endTime, closePrice, openPrice, lowPrice, highPrice, volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(beginTime, endTime, timePeriod, openPrice, highPrice, lowPrice, closePrice, volume, amount,
|
||||
trades);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof BaseBar))
|
||||
return false;
|
||||
final BaseBar other = (BaseBar) obj;
|
||||
return Objects.equals(beginTime, other.beginTime) && Objects.equals(endTime, other.endTime)
|
||||
&& Objects.equals(timePeriod, other.timePeriod) && Objects.equals(openPrice, other.openPrice)
|
||||
&& Objects.equals(highPrice, other.highPrice) && Objects.equals(lowPrice, other.lowPrice)
|
||||
&& Objects.equals(closePrice, other.closePrice) && Objects.equals(volume, other.volume)
|
||||
&& Objects.equals(amount, other.amount) && trades == other.trades;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link BarSeries}.
|
||||
*
|
||||
* <p>
|
||||
* This is the default choice for single-threaded backtests and deterministic
|
||||
* replay workflows. If your pipeline ingests bars/trades concurrently with
|
||||
* strategy evaluation, prefer {@link ConcurrentBarSeries}.
|
||||
* </p>
|
||||
*/
|
||||
public class BaseBarSeries implements BarSeries {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1878027009398790126L;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
private final transient Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* The name of the bar series.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The list of bars of the bar series.
|
||||
*/
|
||||
private final List<Bar> bars;
|
||||
private final BarBuilderFactory barBuilderFactory;
|
||||
|
||||
private final NumFactory numFactory;
|
||||
/**
|
||||
* True if the current bar series is constrained (i.e. its indexes cannot
|
||||
* change), false otherwise.
|
||||
*/
|
||||
private final boolean constrained;
|
||||
/**
|
||||
* The begin index of the bar series
|
||||
*/
|
||||
private int seriesBeginIndex = -1;
|
||||
/**
|
||||
* The end index of the bar series.
|
||||
*/
|
||||
private int seriesEndIndex = -1;
|
||||
/**
|
||||
* The maximum number of bars for the bar series.
|
||||
*/
|
||||
private int maximumBarCount = Integer.MAX_VALUE;
|
||||
/**
|
||||
* The number of removed bars.
|
||||
*/
|
||||
private int removedBarsCount = 0;
|
||||
|
||||
/**
|
||||
* Convenience constructor for BaseBarSeries minimizing upfront parameter
|
||||
* setting. Defaults to Time-based bars, and DecimalNum values
|
||||
*
|
||||
* @param name the name of the bar series
|
||||
* @param bars the list of bars of the bar series
|
||||
*/
|
||||
public BaseBarSeries(final String name, final List<Bar> bars) {
|
||||
this(name, bars, 0, bars.size() - 1, false, DecimalNumFactory.getInstance(), new TimeBarBuilderFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the bar series
|
||||
* @param bars the list of bars of the bar series
|
||||
* @param seriesBeginIndex the begin index (inclusive) of the bar series
|
||||
* @param seriesEndIndex the end index (inclusive) of the bar series
|
||||
* @param constrained true to constrain the bar series (i.e. indexes
|
||||
* cannot change), false otherwise
|
||||
* @param numFactory the factory of numbers used in series {@link Num Num
|
||||
* implementation}
|
||||
* @param barBuilderFactory factory for creating bars of this series
|
||||
*/
|
||||
BaseBarSeries(final String name, final List<Bar> bars, final int seriesBeginIndex, final int seriesEndIndex,
|
||||
final boolean constrained, final NumFactory numFactory, final BarBuilderFactory barBuilderFactory) {
|
||||
this.name = name;
|
||||
this.numFactory = numFactory;
|
||||
|
||||
this.bars = new ArrayList<>(bars);
|
||||
this.barBuilderFactory = Objects.requireNonNull(barBuilderFactory);
|
||||
if (bars.isEmpty()) {
|
||||
// Bar list empty
|
||||
this.constrained = false;
|
||||
return;
|
||||
}
|
||||
// Bar list not empty: checking indexes
|
||||
if (seriesEndIndex < seriesBeginIndex - 1) {
|
||||
throw new IllegalArgumentException("End index must be >= to begin index - 1");
|
||||
}
|
||||
if (seriesEndIndex >= bars.size()) {
|
||||
throw new IllegalArgumentException("End index must be < to the bar list size");
|
||||
}
|
||||
this.seriesBeginIndex = seriesBeginIndex;
|
||||
this.seriesEndIndex = seriesEndIndex;
|
||||
this.constrained = constrained;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts a list of bars into a new list of bars that is a subset of it.
|
||||
*
|
||||
* @param bars the list of {@link Bar bars}
|
||||
* @param startIndex start index of the subset
|
||||
* @param endIndex end index of the subset
|
||||
* @return a new list of bars with tick from startIndex (inclusive) to endIndex
|
||||
* (exclusive)
|
||||
*/
|
||||
private static List<Bar> cut(final List<Bar> bars, final int startIndex, final int endIndex) {
|
||||
return new ArrayList<>(bars.subList(startIndex, endIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param series a bar series
|
||||
* @param index an out-of-bounds bar index
|
||||
* @return a message for an OutOfBoundsException
|
||||
*/
|
||||
private static String buildOutOfBoundsMessage(final BaseBarSeries series, final int index) {
|
||||
return String.format("Size of series: %s bars, %s bars removed, index = %s", series.bars.size(),
|
||||
series.removedBarsCount, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBarSeries getSubSeries(final int startIndex, final int endIndex) {
|
||||
if (startIndex < 0) {
|
||||
throw new IllegalArgumentException(String.format("the startIndex: %s must not be negative", startIndex));
|
||||
}
|
||||
if (startIndex >= endIndex) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("the endIndex: %s must be greater than startIndex: %s", endIndex, startIndex));
|
||||
}
|
||||
var builder = new BaseBarSeriesBuilder().withName(getName())
|
||||
.withNumFactory(this.numFactory)
|
||||
.withMaxBarCount(this.maximumBarCount);
|
||||
if (!this.bars.isEmpty()) {
|
||||
var removedBarsCount = getRemovedBarsCount();
|
||||
var start = startIndex - removedBarsCount;
|
||||
var end = Math.min(endIndex - removedBarsCount, this.getEndIndex() + 1);
|
||||
return builder.withBars(cut(this.bars, start, end)).build();
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumFactory numFactory() {
|
||||
return this.numFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder barBuilder() {
|
||||
return barBuilderFactory.createBarBuilder(this);
|
||||
}
|
||||
|
||||
protected BarBuilderFactory barBuilderFactory() {
|
||||
return barBuilderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar getBar(final int i) {
|
||||
int innerIndex = i - this.removedBarsCount;
|
||||
if (innerIndex < 0) {
|
||||
if (i < 0) {
|
||||
// Cannot return the i-th bar if i < 0
|
||||
throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, i));
|
||||
}
|
||||
if (this.log.isTraceEnabled()) {
|
||||
this.log.trace("Bar series `{}` ({} bars): bar {} already removed, use {}-th instead", this.name,
|
||||
this.bars.size(), i, this.removedBarsCount);
|
||||
}
|
||||
if (this.bars.isEmpty()) {
|
||||
throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, this.removedBarsCount));
|
||||
}
|
||||
innerIndex = 0;
|
||||
} else if (innerIndex >= this.bars.size()) {
|
||||
// Cannot return the n-th bar if n >= bars.size()
|
||||
throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, i));
|
||||
}
|
||||
return this.bars.get(innerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBarCount() {
|
||||
if (this.seriesEndIndex < 0) {
|
||||
return 0;
|
||||
}
|
||||
final int startIndex = Math.max(this.removedBarsCount, this.seriesBeginIndex);
|
||||
return this.seriesEndIndex - startIndex + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Bar> getBarData() {
|
||||
return this.bars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBeginIndex() {
|
||||
return this.seriesBeginIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndIndex() {
|
||||
return this.seriesEndIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximumBarCount() {
|
||||
return this.maximumBarCount;
|
||||
}
|
||||
|
||||
boolean isConstrained() {
|
||||
return this.constrained;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaximumBarCount(final int maximumBarCount) {
|
||||
if (this.constrained) {
|
||||
throw new IllegalStateException("Cannot set a maximum bar count on a constrained bar series");
|
||||
}
|
||||
if (maximumBarCount <= 0) {
|
||||
throw new IllegalArgumentException("Maximum bar count must be strictly positive");
|
||||
}
|
||||
this.maximumBarCount = maximumBarCount;
|
||||
removeExceedingBars();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemovedBarsCount() {
|
||||
return this.removedBarsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException if {@code bar} is {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void addBar(final Bar bar, final boolean replace) {
|
||||
Objects.requireNonNull(bar, "bar must not be null");
|
||||
if (!numFactory.produces(bar.getClosePrice())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot add Bar with data type: %s to series with datatype: %s",
|
||||
bar.getClosePrice().getClass(), this.numFactory.one().getClass()));
|
||||
}
|
||||
|
||||
if (!this.bars.isEmpty()) {
|
||||
if (replace) {
|
||||
this.bars.set(this.bars.size() - 1, bar);
|
||||
return;
|
||||
}
|
||||
final int lastBarIndex = this.bars.size() - 1;
|
||||
final Instant seriesEndTime = this.bars.get(lastBarIndex).getEndTime();
|
||||
if (!bar.getEndTime().isAfter(seriesEndTime)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot add a bar with end time:%s that is <= to series end time: %s",
|
||||
bar.getEndTime(), seriesEndTime));
|
||||
}
|
||||
}
|
||||
|
||||
this.bars.add(bar);
|
||||
if (this.seriesBeginIndex == -1) {
|
||||
// The begin index is set to 0 if not already initialized:
|
||||
this.seriesBeginIndex = 0;
|
||||
}
|
||||
this.seriesEndIndex++;
|
||||
removeExceedingBars();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a bar at the provided series index without changing bar count or
|
||||
* indices.
|
||||
*
|
||||
* @param index the series index to replace
|
||||
* @param bar the replacement bar
|
||||
*
|
||||
* @throws NullPointerException if {@code bar} is {@code null}
|
||||
* @throws IllegalArgumentException if the bar does not match the series
|
||||
* numFactory
|
||||
* @throws IndexOutOfBoundsException if the index is outside the current series
|
||||
* window
|
||||
*/
|
||||
protected void replaceBar(final int index, final Bar bar) {
|
||||
Objects.requireNonNull(bar, "bar must not be null");
|
||||
if (!numFactory.produces(bar.getClosePrice())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot add Bar with data type: %s to series with datatype: %s",
|
||||
bar.getClosePrice().getClass(), this.numFactory.one().getClass()));
|
||||
}
|
||||
if (index < this.seriesBeginIndex || index > this.seriesEndIndex || this.bars.isEmpty()) {
|
||||
throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, index));
|
||||
}
|
||||
final int innerIndex = index - this.removedBarsCount;
|
||||
if (innerIndex < 0 || innerIndex >= this.bars.size()) {
|
||||
throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, index));
|
||||
}
|
||||
this.bars.set(innerIndex, bar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(final Number tradeVolume, final Number tradePrice) {
|
||||
addTrade(numFactory().numOf(tradeVolume), numFactory().numOf(tradePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(final Num tradeVolume, final Num tradePrice) {
|
||||
getLastBar().addTrade(tradeVolume, tradePrice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrice(final Num price) {
|
||||
getLastBar().addPrice(price);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first N bars that exceed the {@link #maximumBarCount}.
|
||||
*/
|
||||
protected void removeExceedingBars() {
|
||||
final int barCount = this.bars.size();
|
||||
if (barCount > this.maximumBarCount) {
|
||||
// Removing old bars
|
||||
final int nbBarsToRemove = barCount - this.maximumBarCount;
|
||||
if (nbBarsToRemove == 1) {
|
||||
this.bars.removeFirst();
|
||||
} else {
|
||||
this.bars.subList(0, nbBarsToRemove).clear();
|
||||
}
|
||||
// Updating removed bars count
|
||||
this.removedBarsCount += nbBarsToRemove;
|
||||
this.seriesBeginIndex = Math.max(this.seriesBeginIndex, this.removedBarsCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBarSeries}.
|
||||
*/
|
||||
public class BaseBarSeriesBuilder implements BarSeriesBuilder {
|
||||
|
||||
/** The {@link #name} for an unnamed bar series. */
|
||||
private static final String UNNAMED_SERIES_NAME = "unnamed_series";
|
||||
|
||||
private List<Bar> bars;
|
||||
private String name;
|
||||
private boolean constrained;
|
||||
private int maxBarCount;
|
||||
private boolean isNumFactoryAssigned = false;
|
||||
private NumFactory numFactory = DecimalNumFactory.getInstance();
|
||||
private BarBuilderFactory barBuilderFactory = new TimeBarBuilderFactory();
|
||||
|
||||
/** Constructor to build a {@code BaseBarSeries}. */
|
||||
public BaseBarSeriesBuilder() {
|
||||
initValues();
|
||||
}
|
||||
|
||||
private void initValues() {
|
||||
this.bars = new ArrayList<>();
|
||||
this.name = "unnamed_series";
|
||||
this.constrained = false;
|
||||
this.maxBarCount = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBarSeries build() {
|
||||
int beginIndex = -1;
|
||||
int endIndex = -1;
|
||||
if (!bars.isEmpty()) {
|
||||
beginIndex = 0;
|
||||
endIndex = bars.size() - 1;
|
||||
|
||||
if (!isNumFactoryAssigned) {
|
||||
// use numFactory derived from bars instead of default numFactory
|
||||
numFactory = bars.getFirst().numFactory();
|
||||
}
|
||||
|
||||
// check if each bar has the same numFactory as the series numFactory
|
||||
for (var bar : bars) {
|
||||
if (bar.getClosePrice() != null) {
|
||||
if (!numFactory.produces(bar.getClosePrice())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot add Bar with data type: %s to series with datatype: %s",
|
||||
bar.getClosePrice().getClass(), this.numFactory.one().getClass()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var series = new BaseBarSeries(name == null ? UNNAMED_SERIES_NAME : name, bars, beginIndex, endIndex,
|
||||
constrained, numFactory, barBuilderFactory);
|
||||
series.setMaximumBarCount(maxBarCount);
|
||||
initValues(); // reinitialize values for next series
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param constrained to set
|
||||
* @return {@code this}
|
||||
*
|
||||
* @deprecated Constrained mode is being derived from max-bar-count
|
||||
* configuration instead of being set directly. Prefer configuring
|
||||
* retention via {@link #withMaxBarCount(int)} (or omit it for the
|
||||
* default constrained behavior).
|
||||
*/
|
||||
@Deprecated(since = "0.22.2")
|
||||
public BaseBarSeriesBuilder setConstrained(boolean constrained) {
|
||||
this.constrained = constrained;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numFactory to set {@link BaseBarSeries#numFactory()} (by default, uses
|
||||
* either {@link DecimalNumFactory} or {@code numFactory}
|
||||
* derived from {@link #bars})
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BaseBarSeriesBuilder withNumFactory(NumFactory numFactory) {
|
||||
if (numFactory != null) {
|
||||
// user has explicitly assigned a numFactory
|
||||
isNumFactoryAssigned = true;
|
||||
}
|
||||
this.numFactory = numFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name to set {@link BaseBarSeries#getName()}
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BaseBarSeriesBuilder withName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bars to set {@link BaseBarSeries#getBarData()}; If {@link #numFactory}
|
||||
* is not assigned by {@link #withNumFactory(NumFactory)},
|
||||
* {@link #numFactory} defaults to the {@code numFactory} of the
|
||||
* {@code bars}.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BaseBarSeriesBuilder withBars(List<Bar> bars) {
|
||||
this.bars = bars;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxBarCount to set {@link BaseBarSeries#getMaximumBarCount()}
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BaseBarSeriesBuilder withMaxBarCount(int maxBarCount) {
|
||||
this.maxBarCount = maxBarCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param barBuilderFactory to build bars with the same datatype as series (by
|
||||
* default, uses {@link TimeBarBuilderFactory})
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BaseBarSeriesBuilder withBarBuilderFactory(final BarBuilderFactory barBuilderFactory) {
|
||||
this.barBuilderFactory = barBuilderFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* {@link Bar} implementation that tracks realtime side and liquidity
|
||||
* breakdowns.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public class BaseRealtimeBar extends BaseBar implements RealtimeBar {
|
||||
|
||||
private static final long serialVersionUID = -5572746191534014724L;
|
||||
|
||||
private final NumFactory numFactory;
|
||||
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private boolean hasSideData;
|
||||
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasLiquidityData;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param timePeriod the time period (optional if beginTime and endTime is
|
||||
* given)
|
||||
* @param beginTime the begin time of the bar period (in UTC) (optional if
|
||||
* endTime is given)
|
||||
* @param endTime the end time of the bar period (in UTC) (optional if
|
||||
* beginTime is given)
|
||||
* @param openPrice the open price of the bar period
|
||||
* @param highPrice the highest price of the bar period
|
||||
* @param lowPrice the lowest price of the bar period
|
||||
* @param closePrice the close price of the bar period
|
||||
* @param volume the total traded volume of the bar period
|
||||
* @param amount the total traded amount of the bar period
|
||||
* @param trades the number of trades of the bar period
|
||||
* @param buyVolume buy-side volume
|
||||
* @param sellVolume sell-side volume
|
||||
* @param buyAmount buy-side amount
|
||||
* @param sellAmount sell-side amount
|
||||
* @param buyTrades buy-side trades
|
||||
* @param sellTrades sell-side trades
|
||||
* @param makerVolume maker-side volume
|
||||
* @param takerVolume taker-side volume
|
||||
* @param makerAmount maker-side amount
|
||||
* @param takerAmount taker-side amount
|
||||
* @param makerTrades maker-side trades
|
||||
* @param takerTrades taker-side trades
|
||||
* @param hasSideData {@code true} if side data was provided
|
||||
* @param hasLiquidity {@code true} if liquidity data was provided
|
||||
* @param numFactory the number factory backing this bar
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BaseRealtimeBar(final Duration timePeriod, final Instant beginTime, final Instant endTime,
|
||||
final Num openPrice, final Num highPrice, final Num lowPrice, final Num closePrice, final Num volume,
|
||||
final Num amount, final long trades, final Num buyVolume, final Num sellVolume, final Num buyAmount,
|
||||
final Num sellAmount, final long buyTrades, final long sellTrades, final Num makerVolume,
|
||||
final Num takerVolume, final Num makerAmount, final Num takerAmount, final long makerTrades,
|
||||
final long takerTrades, final boolean hasSideData, final boolean hasLiquidity,
|
||||
final NumFactory numFactory) {
|
||||
super(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice, volume, amount, trades);
|
||||
this.numFactory = Objects.requireNonNull(numFactory, "numFactory cannot be null");
|
||||
this.buyVolume = buyVolume;
|
||||
this.sellVolume = sellVolume;
|
||||
this.buyAmount = buyAmount;
|
||||
this.sellAmount = sellAmount;
|
||||
this.buyTrades = buyTrades;
|
||||
this.sellTrades = sellTrades;
|
||||
this.hasSideData = hasSideData;
|
||||
this.makerVolume = makerVolume;
|
||||
this.takerVolume = takerVolume;
|
||||
this.makerAmount = makerAmount;
|
||||
this.takerAmount = takerAmount;
|
||||
this.makerTrades = makerTrades;
|
||||
this.takerTrades = takerTrades;
|
||||
this.hasLiquidityData = hasLiquidity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSideData() {
|
||||
return hasSideData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLiquidityData() {
|
||||
return hasLiquidityData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getBuyVolume() {
|
||||
return buyVolume == null ? numFactory.zero() : buyVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getSellVolume() {
|
||||
return sellVolume == null ? numFactory.zero() : sellVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getBuyAmount() {
|
||||
return buyAmount == null ? numFactory.zero() : buyAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getSellAmount() {
|
||||
return sellAmount == null ? numFactory.zero() : sellAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBuyTrades() {
|
||||
return buyTrades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSellTrades() {
|
||||
return sellTrades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getMakerVolume() {
|
||||
return makerVolume == null ? numFactory.zero() : makerVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getTakerVolume() {
|
||||
return takerVolume == null ? numFactory.zero() : takerVolume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getMakerAmount() {
|
||||
return makerAmount == null ? numFactory.zero() : makerAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getTakerAmount() {
|
||||
return takerAmount == null ? numFactory.zero() : takerAmount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMakerTrades() {
|
||||
return makerTrades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTakerTrades() {
|
||||
return takerTrades;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(final Num tradeVolume, final Num tradePrice, final Side side, final Liquidity liquidity) {
|
||||
super.addTrade(tradeVolume, tradePrice);
|
||||
addSideData(tradeVolume, tradePrice, side);
|
||||
addLiquidityData(tradeVolume, tradePrice, liquidity);
|
||||
}
|
||||
|
||||
private void addSideData(final Num tradeVolume, final Num tradePrice, final Side side) {
|
||||
if (side == null) {
|
||||
return;
|
||||
}
|
||||
hasSideData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (side == Side.BUY) {
|
||||
buyVolume = buyVolume == null ? tradeVolume : buyVolume.plus(tradeVolume);
|
||||
buyAmount = buyAmount == null ? tradeAmount : buyAmount.plus(tradeAmount);
|
||||
buyTrades++;
|
||||
} else {
|
||||
sellVolume = sellVolume == null ? tradeVolume : sellVolume.plus(tradeVolume);
|
||||
sellAmount = sellAmount == null ? tradeAmount : sellAmount.plus(tradeAmount);
|
||||
sellTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
private void addLiquidityData(final Num tradeVolume, final Num tradePrice, final Liquidity liquidity) {
|
||||
if (liquidity == null) {
|
||||
return;
|
||||
}
|
||||
hasLiquidityData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (liquidity == Liquidity.MAKER) {
|
||||
makerVolume = makerVolume == null ? tradeVolume : makerVolume.plus(tradeVolume);
|
||||
makerAmount = makerAmount == null ? tradeAmount : makerAmount.plus(tradeAmount);
|
||||
makerTrades++;
|
||||
} else {
|
||||
takerVolume = takerVolume == null ? tradeVolume : takerVolume.plus(tradeVolume);
|
||||
takerAmount = takerAmount == null ? tradeAmount : takerAmount.plus(tradeAmount);
|
||||
takerTrades++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link Strategy}.
|
||||
*/
|
||||
public class BaseStrategy implements Strategy {
|
||||
|
||||
/** The logger. */
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/** The class name. */
|
||||
private final String className = getClass().getSimpleName();
|
||||
|
||||
/** The name of the strategy. */
|
||||
private final String name;
|
||||
|
||||
/** The entry rule. */
|
||||
private final Rule entryRule;
|
||||
|
||||
/** The exit rule. */
|
||||
private final Rule exitRule;
|
||||
|
||||
/** The entry trade type for this strategy. */
|
||||
private final TradeType startingType;
|
||||
|
||||
/**
|
||||
* The number of first bars in a bar series that this strategy ignores. During
|
||||
* the unstable bars of the strategy, any trade placement will be canceled i.e.
|
||||
* no entry/exit signal will be triggered before {@code index == unstableBars}.
|
||||
*/
|
||||
private int unstableBars;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
*/
|
||||
public BaseStrategy(Rule entryRule, Rule exitRule) {
|
||||
this(null, entryRule, exitRule, 0, TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param unstableBars strategy will ignore possible signals at
|
||||
* {@code index < unstableBars}
|
||||
*/
|
||||
public BaseStrategy(Rule entryRule, Rule exitRule, int unstableBars) {
|
||||
this(null, entryRule, exitRule, unstableBars, TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param startingType the entry trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BaseStrategy(Rule entryRule, Rule exitRule, TradeType startingType) {
|
||||
this(null, entryRule, exitRule, 0, startingType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param unstableBars strategy will ignore possible signals at
|
||||
* {@code index < unstableBars}
|
||||
* @param startingType the entry trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BaseStrategy(Rule entryRule, Rule exitRule, int unstableBars, TradeType startingType) {
|
||||
this(null, entryRule, exitRule, unstableBars, startingType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the strategy
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
*/
|
||||
public BaseStrategy(String name, Rule entryRule, Rule exitRule) {
|
||||
this(name, entryRule, exitRule, 0, TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the strategy
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param startingType the entry trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BaseStrategy(String name, Rule entryRule, Rule exitRule, TradeType startingType) {
|
||||
this(name, entryRule, exitRule, 0, startingType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the strategy
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param unstableBars strategy will ignore possible signals at
|
||||
* {@code index < unstableBars}
|
||||
* @throws IllegalArgumentException if entryRule or exitRule is null
|
||||
*/
|
||||
public BaseStrategy(String name, Rule entryRule, Rule exitRule, int unstableBars) {
|
||||
this(name, entryRule, exitRule, unstableBars, TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the strategy
|
||||
* @param entryRule the entry rule
|
||||
* @param exitRule the exit rule
|
||||
* @param unstableBars strategy will ignore possible signals at
|
||||
* {@code index < unstableBars}
|
||||
* @param startingType the entry trade type
|
||||
* @throws IllegalArgumentException if entryRule or exitRule is null
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BaseStrategy(String name, Rule entryRule, Rule exitRule, int unstableBars, TradeType startingType) {
|
||||
if (entryRule == null || exitRule == null) {
|
||||
throw new IllegalArgumentException("Rules cannot be null");
|
||||
}
|
||||
if (unstableBars < 0) {
|
||||
throw new IllegalArgumentException("Unstable bars must be >= 0");
|
||||
}
|
||||
if (startingType == null) {
|
||||
throw new IllegalArgumentException("Starting type cannot be null");
|
||||
}
|
||||
this.name = name;
|
||||
this.entryRule = entryRule;
|
||||
this.exitRule = exitRule;
|
||||
this.unstableBars = unstableBars;
|
||||
this.startingType = startingType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rule getEntryRule() {
|
||||
return entryRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rule getExitRule() {
|
||||
return exitRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradeType getStartingType() {
|
||||
return startingType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnstableBars() {
|
||||
return unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUnstableBars(int unstableBars) {
|
||||
this.unstableBars = unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnstableAt(int index) {
|
||||
return index < unstableBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnter(int index, TradingRecord tradingRecord) {
|
||||
return evaluateShouldEnter(index, tradingRecord, Rule.TraceMode.VERBOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.22.7
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldEnterWithTraceMode(int index, TradingRecord tradingRecord, Rule.TraceMode traceMode) {
|
||||
return evaluateShouldEnter(index, tradingRecord, traceMode);
|
||||
}
|
||||
|
||||
private boolean evaluateShouldEnter(int index, TradingRecord tradingRecord, Rule.TraceMode requestedTraceMode) {
|
||||
Rule.TraceMode activeTraceMode = requestedTraceMode == null ? Rule.TraceMode.VERBOSE : requestedTraceMode;
|
||||
boolean traceLoggingEnabled = log.isTraceEnabled();
|
||||
if (isUnstableAt(index)) {
|
||||
traceShouldEnter(index, false, traceLoggingEnabled, activeTraceMode, "unstable");
|
||||
return false;
|
||||
}
|
||||
boolean enter = traceLoggingEnabled
|
||||
? getEntryRule().isSatisfiedWithTraceMode(index, tradingRecord, activeTraceMode)
|
||||
: getEntryRule().isSatisfied(index, tradingRecord);
|
||||
traceShouldEnter(index, enter, traceLoggingEnabled, activeTraceMode, enter ? null : "entryRule");
|
||||
return enter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldExit(int index, TradingRecord tradingRecord) {
|
||||
return evaluateShouldExit(index, tradingRecord, Rule.TraceMode.VERBOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.22.7
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldExitWithTraceMode(int index, TradingRecord tradingRecord, Rule.TraceMode traceMode) {
|
||||
return evaluateShouldExit(index, tradingRecord, traceMode);
|
||||
}
|
||||
|
||||
private boolean evaluateShouldExit(int index, TradingRecord tradingRecord, Rule.TraceMode requestedTraceMode) {
|
||||
Rule.TraceMode activeTraceMode = requestedTraceMode == null ? Rule.TraceMode.VERBOSE : requestedTraceMode;
|
||||
boolean traceLoggingEnabled = log.isTraceEnabled();
|
||||
if (isUnstableAt(index)) {
|
||||
traceShouldExit(index, false, traceLoggingEnabled, activeTraceMode, "unstable");
|
||||
return false;
|
||||
}
|
||||
boolean exit = traceLoggingEnabled
|
||||
? getExitRule().isSatisfiedWithTraceMode(index, tradingRecord, activeTraceMode)
|
||||
: getExitRule().isSatisfied(index, tradingRecord);
|
||||
traceShouldExit(index, exit, traceLoggingEnabled, activeTraceMode, exit ? null : "exitRule");
|
||||
return exit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Strategy and(Strategy strategy) {
|
||||
String andName = "and(" + name + "," + strategy.getName() + ")";
|
||||
int unstable = Math.max(unstableBars, strategy.getUnstableBars());
|
||||
return and(andName, strategy, unstable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Strategy or(Strategy strategy) {
|
||||
String orName = "or(" + name + "," + strategy.getName() + ")";
|
||||
int unstable = Math.max(unstableBars, strategy.getUnstableBars());
|
||||
return or(orName, strategy, unstable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Strategy opposite() {
|
||||
return new BaseStrategy("opposite(" + name + ")", exitRule, entryRule, unstableBars, startingType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Strategy and(String name, Strategy strategy, int unstableBars) {
|
||||
return new BaseStrategy(name, entryRule.and(strategy.getEntryRule()), exitRule.and(strategy.getExitRule()),
|
||||
unstableBars, getStartingType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Strategy or(String name, Strategy strategy, int unstableBars) {
|
||||
return new BaseStrategy(name, entryRule.or(strategy.getEntryRule()), exitRule.or(strategy.getExitRule()),
|
||||
unstableBars, getStartingType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name to use in trace logs. Uses the configured name if
|
||||
* set, otherwise falls back to the class name.
|
||||
*
|
||||
* @return display name for tracing
|
||||
*/
|
||||
protected String getTraceDisplayName() {
|
||||
return name != null ? name : className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traces the {@code shouldEnter()} method calls.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param enter true if the strategy should enter, false otherwise
|
||||
*/
|
||||
protected void traceShouldEnter(int index, boolean enter) {
|
||||
traceShouldEnter(index, enter, log.isTraceEnabled(), Rule.TraceMode.VERBOSE, enter ? null : "entryRule");
|
||||
}
|
||||
|
||||
private void traceShouldEnter(int index, boolean enter, boolean traceLoggingEnabled, Rule.TraceMode activeTraceMode,
|
||||
String reason) {
|
||||
if (traceLoggingEnabled) {
|
||||
log.trace(">>> {}#shouldEnter({}): {} mode={}{}", getTraceDisplayName(), index, enter, activeTraceMode,
|
||||
strategyTraceContext(reason));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traces the {@code shouldExit()} method calls.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param exit true if the strategy should exit, false otherwise
|
||||
*/
|
||||
protected void traceShouldExit(int index, boolean exit) {
|
||||
traceShouldExit(index, exit, log.isTraceEnabled(), Rule.TraceMode.VERBOSE, exit ? null : "exitRule");
|
||||
}
|
||||
|
||||
private void traceShouldExit(int index, boolean exit, boolean traceLoggingEnabled, Rule.TraceMode activeTraceMode,
|
||||
String reason) {
|
||||
if (traceLoggingEnabled) {
|
||||
log.trace(">>> {}#shouldExit({}): {} mode={}{}", getTraceDisplayName(), index, exit, activeTraceMode,
|
||||
strategyTraceContext(reason));
|
||||
}
|
||||
}
|
||||
|
||||
private String strategyTraceContext(String reason) {
|
||||
if (reason == null) {
|
||||
return "";
|
||||
}
|
||||
if ("unstable".equals(reason)) {
|
||||
return " reason=unstable unstableBars=" + unstableBars;
|
||||
}
|
||||
return " reason=" + reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.io.Serial;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Unified {@link Trade} implementation for backtest and live flows.
|
||||
*
|
||||
* <ul>
|
||||
* <li>the index (in the {@link BarSeries bar series}) on which the trade is
|
||||
* executed
|
||||
* <li>a {@link Trade.TradeType type} (BUY or SELL)
|
||||
* <li>a pricePerAsset (optional)
|
||||
* <li>a trade amount (optional)
|
||||
* </ul>
|
||||
*
|
||||
* A {@link Position position} is a pair of complementary trades.
|
||||
*
|
||||
* <p>
|
||||
* Trades are backed by one or more {@link TradeFill} entries. Scalar
|
||||
* constructors create a single fill; aggregated constructors preserve full fill
|
||||
* progression.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class BaseTrade implements Trade {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -905474949010114150L;
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final CostModel DEFAULT_COST_MODEL = new ZeroCostModel();
|
||||
|
||||
/** The type of the trade. */
|
||||
private final Trade.TradeType type;
|
||||
|
||||
/** The index the trade was executed. */
|
||||
private final int index;
|
||||
|
||||
/** The trade price per asset. */
|
||||
private Num pricePerAsset;
|
||||
|
||||
/**
|
||||
* The net price per asset for the trade (i.e. {@link #pricePerAsset} with
|
||||
* {@link #cost}).
|
||||
*/
|
||||
private Num netPrice;
|
||||
|
||||
/** The trade amount. */
|
||||
private final Num amount;
|
||||
|
||||
/** Execution fills for this trade (single fill for scalar trades). */
|
||||
private final List<TradeFill> fills;
|
||||
|
||||
/** Execution timestamp. */
|
||||
private final Instant time;
|
||||
|
||||
/** Execution side. */
|
||||
private final ExecutionSide side;
|
||||
|
||||
/** Optional order id. */
|
||||
private final String orderId;
|
||||
|
||||
/** Optional correlation id. */
|
||||
private final String correlationId;
|
||||
|
||||
/**
|
||||
* The simulated execution cost for this trade, derived from the configured
|
||||
* {@link CostModel}.
|
||||
*/
|
||||
private Num cost;
|
||||
|
||||
/** The cost model for trade execution. */
|
||||
private transient CostModel costModel;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
*/
|
||||
protected BaseTrade(int index, BarSeries series, Trade.TradeType type) {
|
||||
this(index, series, type, series.numFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
* @param amount the trade amount
|
||||
*/
|
||||
protected BaseTrade(int index, BarSeries series, Trade.TradeType type, Num amount) {
|
||||
this(index, series, type, amount, new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution cost
|
||||
*/
|
||||
protected BaseTrade(int index, BarSeries series, Trade.TradeType type, Num amount, CostModel transactionCostModel) {
|
||||
Num executionPrice = series.getBar(index).getClosePrice();
|
||||
Instant executionTime = series.getBar(index).getEndTime();
|
||||
this.type = type;
|
||||
this.index = index;
|
||||
this.amount = amount;
|
||||
this.time = executionTime;
|
||||
this.side = executionSide(type);
|
||||
this.orderId = null;
|
||||
this.correlationId = null;
|
||||
this.fills = List.of(new TradeFill(index, executionTime, executionPrice, amount, side));
|
||||
setPricesAndCost(executionPrice, amount, transactionCostModel, this.fills);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
*/
|
||||
protected BaseTrade(int index, Trade.TradeType type, Num pricePerAsset) {
|
||||
this(index, type, pricePerAsset, pricePerAsset.getNumFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
* @param amount the trade amount
|
||||
*/
|
||||
protected BaseTrade(int index, Trade.TradeType type, Num pricePerAsset, Num amount) {
|
||||
this(index, type, pricePerAsset, amount, new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
*/
|
||||
protected BaseTrade(int index, Trade.TradeType type, Num pricePerAsset, Num amount,
|
||||
CostModel transactionCostModel) {
|
||||
this.type = type;
|
||||
this.index = index;
|
||||
this.amount = amount;
|
||||
this.time = null;
|
||||
this.side = executionSide(type);
|
||||
this.orderId = null;
|
||||
this.correlationId = null;
|
||||
this.fills = List.of(new TradeFill(index, null, pricePerAsset, amount, side));
|
||||
setPricesAndCost(pricePerAsset, amount, transactionCostModel, this.fills);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for multi-fill trades.
|
||||
*
|
||||
* @param type trade type
|
||||
* @param fills execution fills (must not be empty)
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @since 0.22.4
|
||||
*/
|
||||
protected BaseTrade(Trade.TradeType type, List<TradeFill> fills, CostModel transactionCostModel) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(transactionCostModel, "transactionCostModel");
|
||||
FillSummary fillSummary = summarizeFills(type, fills);
|
||||
FillMetadata metadata = summarizeMetadata(type, fillSummary.firstFill());
|
||||
this.type = type;
|
||||
this.index = fillSummary.firstFill().index();
|
||||
this.amount = fillSummary.totalAmount();
|
||||
this.time = metadata.time();
|
||||
this.side = metadata.side();
|
||||
this.orderId = metadata.orderId();
|
||||
this.correlationId = metadata.correlationId();
|
||||
this.fills = fillSummary.fills();
|
||||
setPricesAndCost(fillSummary.weightedAveragePrice(), fillSummary.totalAmount(), transactionCostModel,
|
||||
this.fills);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for live execution trades.
|
||||
*
|
||||
* @param index trade index
|
||||
* @param time execution timestamp
|
||||
* @param pricePerAsset execution price per asset
|
||||
* @param amount execution amount
|
||||
* @param fee recorded execution fee (nullable, defaults to zero)
|
||||
* @param side execution side
|
||||
* @param orderId optional order id
|
||||
* @param correlationId optional correlation id
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BaseTrade(int index, Instant time, Num pricePerAsset, Num amount, Num fee, ExecutionSide side,
|
||||
String orderId, String correlationId) {
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("index must be >= 0");
|
||||
}
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(pricePerAsset, "pricePerAsset");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
Objects.requireNonNull(side, "side");
|
||||
Num normalizedFee = fee == null ? pricePerAsset.getNumFactory().zero() : fee;
|
||||
this.type = side.toTradeType();
|
||||
this.index = index;
|
||||
this.amount = amount;
|
||||
this.time = time;
|
||||
this.side = side;
|
||||
this.orderId = orderId;
|
||||
this.correlationId = correlationId;
|
||||
this.fills = List
|
||||
.of(new TradeFill(index, time, pricePerAsset, amount, normalizedFee, side, orderId, correlationId));
|
||||
setPricesAndCost(pricePerAsset, amount, RecordedTradeCostModel.INSTANCE, this.fills);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trade.TradeType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getPricePerAsset() {
|
||||
return pricePerAsset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getPricePerAsset(BarSeries barSeries) {
|
||||
if (pricePerAsset.isNaN()) {
|
||||
return barSeries.getBar(index).getClosePrice();
|
||||
}
|
||||
return pricePerAsset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getNetPrice() {
|
||||
return netPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCorrelationId() {
|
||||
return correlationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TradeFill> getFills() {
|
||||
return exportedFills();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return execution timestamp
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Instant time() {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return execution price per asset
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num price() {
|
||||
return pricePerAsset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return execution amount
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return recorded fee/cost
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num fee() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return execution side
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public ExecutionSide side() {
|
||||
return side;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return optional order id
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public String orderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return optional correlation id
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public String correlationId() {
|
||||
return correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured cost model, or a zero-cost model after deserialization
|
||||
* when the transient model is unset
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Override
|
||||
public CostModel getCostModel() {
|
||||
return costModel == null ? DEFAULT_COST_MODEL : costModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the raw and net prices of the trade.
|
||||
*
|
||||
* @param pricePerAsset the raw price of the asset
|
||||
* @param amount the amount of assets ordered
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
*/
|
||||
private void setPricesAndCost(Num pricePerAsset, Num amount, CostModel transactionCostModel,
|
||||
List<TradeFill> fills) {
|
||||
Objects.requireNonNull(transactionCostModel, "transactionCostModel");
|
||||
this.costModel = transactionCostModel;
|
||||
this.pricePerAsset = pricePerAsset;
|
||||
this.cost = resolveCost(transactionCostModel, this.pricePerAsset, amount, fills);
|
||||
|
||||
if (amount.isZero()) {
|
||||
this.netPrice = this.pricePerAsset;
|
||||
return;
|
||||
}
|
||||
Num costPerAsset = cost.dividedBy(amount);
|
||||
// add transaction costs to the pricePerAsset at the trade
|
||||
if (type.equals(Trade.TradeType.BUY)) {
|
||||
this.netPrice = this.pricePerAsset.plus(costPerAsset);
|
||||
} else {
|
||||
this.netPrice = this.pricePerAsset.minus(costPerAsset);
|
||||
}
|
||||
}
|
||||
|
||||
private static Num resolveCost(CostModel transactionCostModel, Num pricePerAsset, Num amount,
|
||||
List<TradeFill> fills) {
|
||||
if (transactionCostModel instanceof RecordedTradeCostModel) {
|
||||
return sumFillFees(pricePerAsset.getNumFactory().zero(), fills);
|
||||
}
|
||||
return transactionCostModel.calculate(pricePerAsset, amount);
|
||||
}
|
||||
|
||||
private static Num sumFillFees(Num zero, List<TradeFill> fills) {
|
||||
Num totalFee = zero;
|
||||
for (TradeFill fill : fills) {
|
||||
totalFee = totalFee.plus(fill.fee());
|
||||
}
|
||||
return totalFee;
|
||||
}
|
||||
|
||||
private static FillSummary summarizeFills(Trade.TradeType tradeType, List<TradeFill> fills) {
|
||||
Objects.requireNonNull(fills, "fills");
|
||||
if (fills.isEmpty()) {
|
||||
throw new IllegalArgumentException("fills must not be empty");
|
||||
}
|
||||
Num totalAmount = fills.getFirst().amount().getNumFactory().zero();
|
||||
Num weightedPrice = fills.getFirst().price().getNumFactory().zero();
|
||||
TradeFill earliestFill = fills.getFirst();
|
||||
ExecutionSide expectedSide = executionSide(tradeType);
|
||||
for (TradeFill fill : fills) {
|
||||
if (fill.side() != null && fill.side() != expectedSide) {
|
||||
throw new IllegalArgumentException("fill side must match trade type at index " + fill.index());
|
||||
}
|
||||
if (fill.price().isNaN()) {
|
||||
throw new IllegalArgumentException("fill price must be set");
|
||||
}
|
||||
if (fill.amount().isNaN() || fill.amount().isZero() || fill.amount().isNegative()) {
|
||||
throw new IllegalArgumentException("fill amount must be positive");
|
||||
}
|
||||
if (fill.index() < earliestFill.index()) {
|
||||
earliestFill = fill;
|
||||
}
|
||||
totalAmount = totalAmount.plus(fill.amount());
|
||||
weightedPrice = weightedPrice.plus(fill.price().multipliedBy(fill.amount()));
|
||||
}
|
||||
return new FillSummary(List.copyOf(fills), earliestFill, totalAmount, weightedPrice.dividedBy(totalAmount));
|
||||
}
|
||||
|
||||
private static FillMetadata summarizeMetadata(Trade.TradeType tradeType, TradeFill firstFill) {
|
||||
Instant firstTime = firstFill.time();
|
||||
String firstOrderId = firstFill.orderId();
|
||||
String firstCorrelationId = firstFill.correlationId();
|
||||
ExecutionSide resolvedSide = firstFill.side() == null ? executionSide(tradeType) : firstFill.side();
|
||||
return new FillMetadata(firstTime, resolvedSide, firstOrderId, firstCorrelationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports fills with trade-level modeled costs apportioned back onto the fills
|
||||
* when no explicit per-fill fees were recorded.
|
||||
*/
|
||||
private List<TradeFill> exportedFills() {
|
||||
if (fills.isEmpty() || cost == null || cost.isNaN()) {
|
||||
return fills;
|
||||
}
|
||||
|
||||
CostModel effectiveCostModel = getCostModel();
|
||||
if (effectiveCostModel instanceof RecordedTradeCostModel) {
|
||||
return fills;
|
||||
}
|
||||
|
||||
Num zero = fills.getFirst().price().getNumFactory().zero();
|
||||
Num recordedFeeTotal = sumFillFees(zero, fills);
|
||||
Num residualFee = cost.minus(recordedFeeTotal);
|
||||
if (!residualFee.isPositive()) {
|
||||
return fills;
|
||||
}
|
||||
|
||||
Num totalWeight = totalFillWeight(zero);
|
||||
if (totalWeight.isZero()) {
|
||||
return fills;
|
||||
}
|
||||
|
||||
Num remainingFee = residualFee;
|
||||
List<TradeFill> adjustedFills = new ArrayList<>(fills.size());
|
||||
for (int i = 0; i < fills.size(); i++) {
|
||||
TradeFill fill = fills.get(i);
|
||||
Num feeShare = i == fills.size() - 1 ? remainingFee
|
||||
: residualFee.multipliedBy(fillWeight(fill)).dividedBy(totalWeight);
|
||||
remainingFee = remainingFee.minus(feeShare);
|
||||
adjustedFills.add(copyWithFee(fill, fill.fee().plus(feeShare)));
|
||||
}
|
||||
return List.copyOf(adjustedFills);
|
||||
}
|
||||
|
||||
private Num totalFillWeight(Num zero) {
|
||||
Num totalWeight = zero;
|
||||
for (TradeFill fill : fills) {
|
||||
totalWeight = totalWeight.plus(fillWeight(fill));
|
||||
}
|
||||
return totalWeight;
|
||||
}
|
||||
|
||||
private Num fillWeight(TradeFill fill) {
|
||||
return fill.price().multipliedBy(fill.amount());
|
||||
}
|
||||
|
||||
private TradeFill copyWithFee(TradeFill fill, Num fee) {
|
||||
return new TradeFill(fill.index(), fill.time(), fill.price(), fill.amount(), fee, fill.side(), fill.orderId(),
|
||||
fill.correlationId());
|
||||
}
|
||||
|
||||
private static ExecutionSide executionSide(Trade.TradeType tradeType) {
|
||||
if (tradeType == Trade.TradeType.BUY) {
|
||||
return ExecutionSide.BUY;
|
||||
}
|
||||
return ExecutionSide.SELL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBuy() {
|
||||
return type == Trade.TradeType.BUY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSell() {
|
||||
return type == Trade.TradeType.SELL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, index, time, pricePerAsset, amount, cost, side, orderId, correlationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof BaseTrade other)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(type, other.type) && Objects.equals(index, other.index)
|
||||
&& Objects.equals(time, other.time) && Objects.equals(pricePerAsset, other.pricePerAsset)
|
||||
&& Objects.equals(amount, other.amount) && Objects.equals(cost, other.cost)
|
||||
&& Objects.equals(side, other.side) && Objects.equals(orderId, other.orderId)
|
||||
&& Objects.equals(correlationId, other.correlationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("type", type == null ? null : type.name());
|
||||
json.addProperty("index", index);
|
||||
json.addProperty("time", time == null ? null : time.toString());
|
||||
json.addProperty("pricePerAsset", pricePerAsset == null ? null : pricePerAsset.toString());
|
||||
json.addProperty("netPrice", netPrice == null ? null : netPrice.toString());
|
||||
json.addProperty("amount", amount == null ? null : amount.toString());
|
||||
json.addProperty("cost", cost == null ? null : cost.toString());
|
||||
json.addProperty("side", side == null ? null : side.name());
|
||||
json.addProperty("orderId", orderId);
|
||||
json.addProperty("correlationId", correlationId);
|
||||
return GSON.toJson(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this trade with a new index.
|
||||
*
|
||||
* @param index trade index
|
||||
* @return trade with the provided index
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BaseTrade withIndex(int index) {
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("index must be >= 0");
|
||||
}
|
||||
int delta = index - this.index;
|
||||
List<TradeFill> indexedFills = fills.stream()
|
||||
.map(fill -> new TradeFill(fill.index() + delta, fill.time(), fill.price(), fill.amount(), fill.fee(),
|
||||
fill.side(), fill.orderId(), fill.correlationId()))
|
||||
.toList();
|
||||
return new BaseTrade(type, indexedFills, resolveCopyCostModel(indexedFills));
|
||||
}
|
||||
|
||||
private CostModel resolveCopyCostModel(List<TradeFill> indexedFills) {
|
||||
if (costModel != null) {
|
||||
return costModel;
|
||||
}
|
||||
Num fillFeeTotal = sumFillFees(cost.getNumFactory().zero(), indexedFills);
|
||||
if (cost.equals(fillFeeTotal)) {
|
||||
return RecordedTradeCostModel.INSTANCE;
|
||||
}
|
||||
return new PreservedTradeCostModel(cost);
|
||||
}
|
||||
|
||||
private record FillSummary(List<TradeFill> fills, TradeFill firstFill, Num totalAmount, Num weightedAveragePrice) {
|
||||
}
|
||||
|
||||
private record FillMetadata(Instant time, ExecutionSide side, String orderId, String correlationId) {
|
||||
}
|
||||
|
||||
private static final class PreservedTradeCostModel implements CostModel {
|
||||
|
||||
private final Num preservedCost;
|
||||
|
||||
private PreservedTradeCostModel(Num preservedCost) {
|
||||
this.preservedCost = Objects.requireNonNull(preservedCost, "preservedCost");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position, int finalIndex) {
|
||||
return preservedCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
return preservedCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return preservedCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
if (!(otherModel instanceof PreservedTradeCostModel other)) {
|
||||
return false;
|
||||
}
|
||||
return preservedCost.equals(other.preservedCost);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,635 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Thread-safe {@link BarSeries} implementation for concurrent read/write use
|
||||
* cases.
|
||||
*
|
||||
* <p>
|
||||
* Choose this type only when ingestion and evaluation can overlap on different
|
||||
* threads. For single-threaded backtests and deterministic replay pipelines,
|
||||
* {@link BaseBarSeries} is usually simpler.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For real-time data feeds, prefer {@link #ingestTrade(Instant, Num, Num)} and
|
||||
* {@link #ingestTrade(Instant, Number, Number)} to let the configured
|
||||
* {@link BarBuilder} handle bar rollovers. Direct bar mutations remain
|
||||
* available for reconciliation and data correction workflows.
|
||||
*
|
||||
* <p>
|
||||
* Java serialization preserves bar data, the {@link NumFactory}, and the
|
||||
* {@link BarBuilderFactory} configuration. Transient locks are reinitialized on
|
||||
* deserialization, and the trade bar builder is recreated lazily on the next
|
||||
* ingestion call.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public class ConcurrentBarSeries extends BaseBarSeries {
|
||||
|
||||
private static final long serialVersionUID = -1868546230609071876L;
|
||||
|
||||
private transient Lock readLock;
|
||||
private transient Lock writeLock;
|
||||
|
||||
private transient BarBuilder tradeBarBuilder;
|
||||
|
||||
/**
|
||||
* Indicates how a streaming bar was applied to the series.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum StreamingBarIngestAction {
|
||||
APPENDED, REPLACED_LAST, REPLACED_HISTORICAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the outcome of ingesting a streaming bar.
|
||||
*
|
||||
* @param action indicates how the bar was applied
|
||||
* @param index the affected series index
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public record StreamingBarIngestResult(StreamingBarIngestAction action, int index) {
|
||||
public StreamingBarIngestResult {
|
||||
Objects.requireNonNull(action, "action cannot be null");
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("index cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConcurrentBarSeries(final String name, final List<Bar> bars) {
|
||||
this(name, bars, 0, bars.size() - 1, false, DecimalNumFactory.getInstance(), new TimeBarBuilderFactory(true),
|
||||
new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
ConcurrentBarSeries(final String name, final List<Bar> bars, final int seriesBeginIndex, final int seriesEndIndex,
|
||||
final boolean constrained, final NumFactory numFactory, final BarBuilderFactory barBuilderFactory) {
|
||||
this(name, bars, seriesBeginIndex, seriesEndIndex, constrained, numFactory, barBuilderFactory,
|
||||
new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
ConcurrentBarSeries(final String name, final List<Bar> bars, final int seriesBeginIndex, final int seriesEndIndex,
|
||||
final boolean constrained, final NumFactory numFactory, final BarBuilderFactory barBuilderFactory,
|
||||
final ReadWriteLock readWriteLock) {
|
||||
super(name, bars, seriesBeginIndex, seriesEndIndex, constrained, numFactory, barBuilderFactory);
|
||||
initLocks(readWriteLock);
|
||||
this.tradeBarBuilder = Objects.requireNonNull(super.barBuilder(), "barBuilder cannot be null");
|
||||
}
|
||||
|
||||
private void initLocks(final ReadWriteLock readWriteLock) {
|
||||
ReadWriteLock rwLock = Objects.requireNonNull(readWriteLock, "readWriteLock cannot be null");
|
||||
this.readLock = rwLock.readLock();
|
||||
this.writeLock = rwLock.writeLock();
|
||||
}
|
||||
|
||||
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
initLocks(new ReentrantReadWriteLock());
|
||||
tradeBarBuilder = null;
|
||||
}
|
||||
|
||||
private static List<Bar> cut(final List<Bar> bars, final int startIndex, final int endIndex) {
|
||||
return new ArrayList<>(bars.subList(startIndex, endIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConcurrentBarSeries getSubSeries(final int startIndex, final int endIndex) {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
if (startIndex < 0) {
|
||||
throw new IllegalArgumentException(String.format("startIndex: %s cannot be negative", startIndex));
|
||||
}
|
||||
if (startIndex >= endIndex) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("endIndex: %s must be greater than startIndex: %s", endIndex, startIndex));
|
||||
}
|
||||
final List<Bar> bars = super.getBarData();
|
||||
if (!bars.isEmpty()) {
|
||||
final int start = startIndex - super.getRemovedBarsCount();
|
||||
final int end = Math.min(endIndex - super.getRemovedBarsCount(), super.getEndIndex() + 1);
|
||||
final var builder = new ConcurrentBarSeriesBuilder().withName(getName())
|
||||
.withBars(cut(bars, start, end))
|
||||
.withNumFactory(super.numFactory())
|
||||
.withBarBuilderFactory(super.barBuilderFactory());
|
||||
if (!isConstrained()) {
|
||||
builder.withMaxBarCount(super.getMaximumBarCount());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
final var builder = new ConcurrentBarSeriesBuilder().withNumFactory(super.numFactory())
|
||||
.withBarBuilderFactory(super.barBuilderFactory())
|
||||
.withName(getName());
|
||||
if (!isConstrained()) {
|
||||
builder.withMaxBarCount(super.getMaximumBarCount());
|
||||
}
|
||||
return builder.build();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder barBuilder() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.barBuilder();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getName();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumFactory numFactory() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.numFactory();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar getBar(final int i) {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getBar(i);
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar getFirstBar() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getBar(super.getBeginIndex());
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar getLastBar() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getBar(super.getEndIndex());
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBarCount() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getBarCount();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Bar> getBarData() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return List.copyOf(super.getBarData());
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBeginIndex() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getBeginIndex();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndIndex() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getEndIndex();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximumBarCount() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getMaximumBarCount();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaximumBarCount(final int maximumBarCount) {
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
super.setMaximumBarCount(maximumBarCount);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemovedBarsCount() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getRemovedBarsCount();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the builder used for streaming trade ingestion. Configure it (for
|
||||
* example, set the time period) before calling
|
||||
* {@link #ingestTrade(Instant, Num, Num)}.
|
||||
*
|
||||
* @return the trade bar builder
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public BarBuilder tradeBarBuilder() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
if (tradeBarBuilder != null) {
|
||||
return tradeBarBuilder;
|
||||
}
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
if (tradeBarBuilder == null) {
|
||||
tradeBarBuilder = Objects.requireNonNull(super.barBuilder(), "barBuilder cannot be null");
|
||||
}
|
||||
return tradeBarBuilder;
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the supplied action while holding the read lock.
|
||||
*
|
||||
* @param action read-only action to execute
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void withReadLock(final Runnable action) {
|
||||
Objects.requireNonNull(action, "action cannot be null");
|
||||
this.readLock.lock();
|
||||
try {
|
||||
action.run();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the supplied action while holding the read lock.
|
||||
*
|
||||
* @param action read-only action to execute
|
||||
* @param <T> return type
|
||||
* @return the action result
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public <T> T withReadLock(final Supplier<T> action) {
|
||||
Objects.requireNonNull(action, "action cannot be null");
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return action.get();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the supplied action while holding the write lock.
|
||||
*
|
||||
* @param action mutating action to execute
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void withWriteLock(final Runnable action) {
|
||||
Objects.requireNonNull(action, "action cannot be null");
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
action.run();
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the supplied action while holding the write lock.
|
||||
*
|
||||
* @param action mutating action to execute
|
||||
* @param <T> return type
|
||||
* @return the action result
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public <T> T withWriteLock(final Supplier<T> action) {
|
||||
Objects.requireNonNull(action, "action cannot be null");
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
return action.get();
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBar(final Bar bar, final boolean replace) {
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
super.addBar(bar, replace);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(final Number tradeVolume, final Number tradePrice) {
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
super.addTrade(tradeVolume, tradePrice);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTrade(final Num tradeVolume, final Num tradePrice) {
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
super.addTrade(tradeVolume, tradePrice);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrice(final Num price) {
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
super.addPrice(price);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade event into the series using the configured bar builder.
|
||||
*
|
||||
* @param tradeTime the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void ingestTrade(final Instant tradeTime, final Number tradeVolume, final Number tradePrice) {
|
||||
ingestTrade(tradeTime, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade event into the series using the configured bar builder.
|
||||
*
|
||||
* @param tradeTime the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void ingestTrade(final Instant tradeTime, final Num tradeVolume, final Num tradePrice) {
|
||||
ingestTrade(tradeTime, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade event into the series using the configured bar builder.
|
||||
*
|
||||
* @param tradeTime the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void ingestTrade(final Instant tradeTime, final Number tradeVolume, final Number tradePrice,
|
||||
final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(tradeTime, "tradeTime cannot be null");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume cannot be null");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice cannot be null");
|
||||
final NumFactory factory = super.numFactory();
|
||||
ingestTrade(tradeTime, factory.numOf(tradeVolume), factory.numOf(tradePrice), side, liquidity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade event into the series using the configured bar builder.
|
||||
*
|
||||
* @param tradeTime the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void ingestTrade(final Instant tradeTime, final Num tradeVolume, final Num tradePrice,
|
||||
final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(tradeTime, "tradeTime cannot be null");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume cannot be null");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice cannot be null");
|
||||
if (!super.numFactory().produces(tradeVolume) || !super.numFactory().produces(tradePrice)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot ingest trade with data types: %s/%s into series with datatype: %s",
|
||||
tradeVolume.getClass(), tradePrice.getClass(), super.numFactory().one().getClass()));
|
||||
}
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
if (tradeBarBuilder == null) {
|
||||
tradeBarBuilder = Objects.requireNonNull(super.barBuilder(), "barBuilder");
|
||||
}
|
||||
tradeBarBuilder.addTrade(tradeTime, tradeVolume, tradePrice, side, liquidity);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a single streaming bar (e.g., one emitted from an exchange WebSocket
|
||||
* candles) and appends or replaces the matching interval.
|
||||
*
|
||||
* <p>
|
||||
* Unlike {@link #addBar(Bar, boolean)}, this method can replace historical bars
|
||||
* when exchanges replay snapshots that include prior intervals.
|
||||
*
|
||||
* @param bar streaming bar payload
|
||||
* @return the applied action and affected series index
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public StreamingBarIngestResult ingestStreamingBar(final Bar bar) {
|
||||
Objects.requireNonNull(bar, "bar cannot be null");
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
return addStreamingBarUnsafe(bar);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-ingests streaming bars. Incoming payloads are sorted by their end time
|
||||
* to gracefully handle candle snapshots that are emitted with the most recent
|
||||
* intervals first.
|
||||
*
|
||||
* @param bars streaming bars to ingest
|
||||
* @return applied actions in ascending end-time order
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public List<StreamingBarIngestResult> ingestStreamingBars(final Collection<Bar> bars) {
|
||||
if (bars == null || bars.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final List<Bar> ordered = new ArrayList<>(bars);
|
||||
ordered.removeIf(Objects::isNull);
|
||||
if (ordered.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
ordered.sort(Comparator.comparing(Bar::getEndTime));
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
final List<StreamingBarIngestResult> results = new ArrayList<>(ordered.size());
|
||||
for (Bar bar : ordered) {
|
||||
results.add(addStreamingBarUnsafe(bar));
|
||||
}
|
||||
return List.copyOf(results);
|
||||
} finally {
|
||||
this.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private StreamingBarIngestResult addStreamingBarUnsafe(final Bar newBar) {
|
||||
validateBarMatchesSeries(newBar);
|
||||
final List<Bar> internal = super.getBarData();
|
||||
if (internal.isEmpty()) {
|
||||
super.addBar(newBar, false);
|
||||
return new StreamingBarIngestResult(StreamingBarIngestAction.APPENDED, super.getEndIndex());
|
||||
}
|
||||
final Instant newEndTime = Objects.requireNonNull(newBar.getEndTime(), "Bar endTime cannot be null");
|
||||
final Bar lastBar = internal.get(internal.size() - 1);
|
||||
final Instant lastEndTime = Objects.requireNonNull(lastBar.getEndTime(), "Last bar endTime cannot be null");
|
||||
int endTimeComparison = newEndTime.compareTo(lastEndTime);
|
||||
if (endTimeComparison == 0) {
|
||||
super.addBar(newBar, true);
|
||||
return new StreamingBarIngestResult(StreamingBarIngestAction.REPLACED_LAST, super.getEndIndex());
|
||||
}
|
||||
if (endTimeComparison > 0) {
|
||||
super.addBar(newBar, false);
|
||||
return new StreamingBarIngestResult(StreamingBarIngestAction.APPENDED, super.getEndIndex());
|
||||
}
|
||||
final int internalIndex = findBarIndexByEndTime(internal, newEndTime);
|
||||
if (internalIndex >= 0) {
|
||||
final int seriesIndex = internalIndex + super.getRemovedBarsCount();
|
||||
// Replacing a historical bar doesn't change bar count or indices, so we bypass
|
||||
// addBar().
|
||||
super.replaceBar(seriesIndex, newBar);
|
||||
return new StreamingBarIngestResult(StreamingBarIngestAction.REPLACED_HISTORICAL, seriesIndex);
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot insert streaming bar ending at %s because series end time is %s",
|
||||
newBar.getEndTime(), lastBar.getEndTime()));
|
||||
}
|
||||
|
||||
private void validateBarMatchesSeries(final Bar bar) {
|
||||
if (!super.numFactory().produces(bar.getClosePrice())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot add Bar with data type: %s to series with datatype: %s",
|
||||
bar.getClosePrice().getClass(), super.numFactory().one().getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
private static int findBarIndexByEndTime(final List<Bar> bars, final Instant endTime) {
|
||||
int low = 0;
|
||||
int high = bars.size() - 1;
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
final Instant midTime = bars.get(mid).getEndTime();
|
||||
int comparison = midTime.compareTo(endTime);
|
||||
if (comparison < 0) {
|
||||
low = mid + 1;
|
||||
} else if (comparison > 0) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSeriesPeriodDescription() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getSeriesPeriodDescription();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSeriesPeriodDescriptionInSystemTimeZone() {
|
||||
this.readLock.lock();
|
||||
try {
|
||||
return super.getSeriesPeriodDescriptionInSystemTimeZone();
|
||||
} finally {
|
||||
this.readLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DecimalNumFactory;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Builder for {@link ConcurrentBarSeries} instances.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public class ConcurrentBarSeriesBuilder implements BarSeriesBuilder {
|
||||
|
||||
private static final String UNNAMED_SERIES_NAME = "unnamed_series";
|
||||
|
||||
private List<Bar> bars;
|
||||
private String name;
|
||||
private boolean maxBarCountConfigured;
|
||||
private int maxBarCount;
|
||||
private NumFactory numFactory = DecimalNumFactory.getInstance();
|
||||
private BarBuilderFactory barBuilderFactory = new TimeBarBuilderFactory(true);
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link ConcurrentBarSeries}.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder() {
|
||||
initValues();
|
||||
}
|
||||
|
||||
private void initValues() {
|
||||
this.bars = new ArrayList<>();
|
||||
this.name = UNNAMED_SERIES_NAME;
|
||||
this.maxBarCountConfigured = false;
|
||||
this.maxBarCount = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public ConcurrentBarSeries build() {
|
||||
int beginIndex = -1;
|
||||
int endIndex = -1;
|
||||
if (!bars.isEmpty()) {
|
||||
beginIndex = 0;
|
||||
endIndex = bars.size() - 1;
|
||||
}
|
||||
// If maxBarCount is configured, the series must be unconstrained to allow
|
||||
// removals.
|
||||
boolean effectiveConstrained = !maxBarCountConfigured;
|
||||
var series = new ConcurrentBarSeries(name == null ? UNNAMED_SERIES_NAME : name, bars, beginIndex, endIndex,
|
||||
effectiveConstrained, numFactory, barBuilderFactory);
|
||||
if (maxBarCountConfigured) {
|
||||
series.setMaximumBarCount(maxBarCount);
|
||||
}
|
||||
initValues();
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numFactory {@link NumFactory} to back the series
|
||||
* @return {@code this}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder withNumFactory(NumFactory numFactory) {
|
||||
this.numFactory = numFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name name of the series
|
||||
* @return {@code this}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder withName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bars initial bars for the series
|
||||
* @return {@code this}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder withBars(List<Bar> bars) {
|
||||
this.bars = new ArrayList<>(bars);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxBarCount maximum retained bars (also opts the series into
|
||||
* pruning/unconstrained mode)
|
||||
* @return {@code this}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder withMaxBarCount(int maxBarCount) {
|
||||
this.maxBarCount = maxBarCount;
|
||||
this.maxBarCountConfigured = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param barBuilderFactory builder factory for bars
|
||||
* @return {@code this}
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ConcurrentBarSeriesBuilder withBarBuilderFactory(BarBuilderFactory barBuilderFactory) {
|
||||
this.barBuilderFactory = barBuilderFactory;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Deprecated live-fill compatibility contract.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link TradeFill} for new code. This interface remains available in the
|
||||
* 0.22.x line so existing live adapters can migrate without a hard compile
|
||||
* break.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
public interface ExecutionFill extends Serializable {
|
||||
|
||||
/**
|
||||
* @return the execution timestamp (UTC)
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Instant time();
|
||||
|
||||
/**
|
||||
* @return the execution price per asset
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num price();
|
||||
|
||||
/**
|
||||
* @return the execution amount
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num amount();
|
||||
|
||||
/**
|
||||
* @return the execution fee (nullable, zero when unknown)
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num fee();
|
||||
|
||||
/**
|
||||
* @return the execution side
|
||||
* @since 0.22.2
|
||||
*/
|
||||
ExecutionSide side();
|
||||
|
||||
/**
|
||||
* @return the exchange order id if available
|
||||
* @since 0.22.2
|
||||
*/
|
||||
String orderId();
|
||||
|
||||
/**
|
||||
* @return the correlation id if available
|
||||
* @since 0.22.2
|
||||
*/
|
||||
String correlationId();
|
||||
|
||||
/**
|
||||
* @return the associated intent id, defaulting to {@link #correlationId()}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default String intentId() {
|
||||
return correlationId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the bar index for the fill, or {@code -1} when not specified
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default int index() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when the fill has a non-zero fee
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default boolean hasFee() {
|
||||
Num fee = fee();
|
||||
return fee != null && !fee.isZero();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Generic execution intent metadata for live trading systems.
|
||||
*
|
||||
* <p>
|
||||
* This is intentionally minimal so adapters can map their decision objects
|
||||
* without leaking platform-specific details into ta4j core.
|
||||
* </p>
|
||||
*
|
||||
* @param intentId unique intent identifier
|
||||
* @param side execution side
|
||||
* @param createdAt intent creation timestamp (UTC)
|
||||
* @param correlationId optional correlation id for external systems
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public record ExecutionIntent(String intentId, ExecutionSide side, Instant createdAt,
|
||||
String correlationId) implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 8030047863134194008L;
|
||||
|
||||
public ExecutionIntent {
|
||||
if (intentId == null || intentId.isBlank()) {
|
||||
throw new IllegalArgumentException("intentId must be non-blank");
|
||||
}
|
||||
Objects.requireNonNull(side, "side");
|
||||
Objects.requireNonNull(createdAt, "createdAt");
|
||||
if (correlationId != null && correlationId.isBlank()) {
|
||||
throw new IllegalArgumentException("correlationId must be non-blank when provided");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
/**
|
||||
* Matching policy for pairing exits against open position lots.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum ExecutionMatchPolicy {
|
||||
|
||||
/**
|
||||
* First-in, first-out matching; exits close the earliest open lots.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
FIFO,
|
||||
|
||||
/**
|
||||
* Last-in, first-out matching; exits close the most recent open lots.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
LIFO,
|
||||
|
||||
/**
|
||||
* Average-cost matching; entries are merged into a single lot and exits use the
|
||||
* weighted average cost basis.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
AVG_COST,
|
||||
|
||||
/**
|
||||
* Match exits to a specific lot using correlationId or orderId; exit amounts
|
||||
* must not exceed the matched lot.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
SPECIFIC_ID
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
/**
|
||||
* Execution side for live fills.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum ExecutionSide {
|
||||
|
||||
/**
|
||||
* Buy-side execution.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
BUY,
|
||||
|
||||
/**
|
||||
* Sell-side execution.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
SELL;
|
||||
|
||||
/**
|
||||
* @return the corresponding {@link Trade.TradeType}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Trade.TradeType toTradeType() {
|
||||
return this == BUY ? Trade.TradeType.BUY : Trade.TradeType.SELL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.serialization.ComponentDescriptor;
|
||||
import org.ta4j.core.serialization.IndicatorSerialization;
|
||||
import org.ta4j.core.serialization.IndicatorSerializationException;
|
||||
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Indicator over a {@link BarSeries bar series}.
|
||||
*
|
||||
* <p>
|
||||
* Returns a value of type <b>T</b> for each index of the bar series.
|
||||
*
|
||||
* <p>
|
||||
* Indicators transform price/volume data (or other indicators) into new data
|
||||
* series (e.g., Moving Averages, RSI). They calculate values lazily or eagerly
|
||||
* and are the fundamental building blocks for {@link Rule rules}. Values are
|
||||
* retrieved per-index via {@link #getValue(int)}.
|
||||
*
|
||||
* @param <T> the type of the returned value (Double, Boolean, etc.)
|
||||
*/
|
||||
public interface Indicator<T> {
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @return the value of the indicator
|
||||
*/
|
||||
T getValue(int index);
|
||||
|
||||
/**
|
||||
* Returns {@code true} once {@code this} indicator has enough bars to
|
||||
* accurately calculate its value. Otherwise, {@code false} will be returned,
|
||||
* which means the indicator will give incorrect values due to insufficient
|
||||
* data. This method determines stability using the formula:
|
||||
*
|
||||
* <pre>
|
||||
* isStable = {@link BarSeries#getBarCount()} >= {@link #getCountOfUnstableBars()}
|
||||
* </pre>
|
||||
*
|
||||
* @return true if the calculated indicator value is correct
|
||||
*/
|
||||
default boolean isStable() {
|
||||
return getBarSeries().getBarCount() >= getCountOfUnstableBars();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bars up to which {@code this} Indicator calculates
|
||||
* wrong values.
|
||||
*
|
||||
* @return unstable bars
|
||||
*/
|
||||
int getCountOfUnstableBars();
|
||||
|
||||
/**
|
||||
* @return the related bar series
|
||||
*/
|
||||
BarSeries getBarSeries();
|
||||
|
||||
/**
|
||||
* @return all values from {@code this} Indicator over {@link #getBarSeries()}
|
||||
* as a Stream
|
||||
*/
|
||||
default Stream<T> stream() {
|
||||
return IntStream.range(getBarSeries().getBeginIndex(), getBarSeries().getEndIndex() + 1)
|
||||
.mapToObj(this::getValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values of an {@link Indicator} within the given {@code index} and
|
||||
* {@code barCount} as an array of Doubles. The returned doubles could have a
|
||||
* minor loss of precision, if {@link Indicator} was based on {@link Num Num}.
|
||||
*
|
||||
* @param ref the indicator
|
||||
* @param index the index
|
||||
* @param barCount the barCount
|
||||
* @return array of Doubles within {@code index} and {@code barCount}
|
||||
*/
|
||||
static Double[] toDouble(Indicator<Num> ref, int index, int barCount) {
|
||||
int startIndex = Math.max(0, index - barCount + 1);
|
||||
return IntStream.range(startIndex, startIndex + barCount)
|
||||
.mapToObj(ref::getValue)
|
||||
.map(Num::doubleValue)
|
||||
.toArray(Double[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes {@code this} indicator into a JSON payload that captures its type,
|
||||
* numeric parameters, and child indicators.
|
||||
*
|
||||
* <p>
|
||||
* The serialization process uses reflection to introspect the indicator's
|
||||
* structure, extracting numeric constructor parameters and recursively
|
||||
* serializing child indicators. The resulting JSON can be used to reconstruct
|
||||
* an equivalent indicator instance using {@link #fromJson(BarSeries, String)}.
|
||||
*
|
||||
* <p>
|
||||
* The JSON format includes:
|
||||
* <ul>
|
||||
* <li>The indicator's simple class name (type)</li>
|
||||
* <li>Numeric parameters extracted from constructor arguments</li>
|
||||
* <li>Child indicators as nested component descriptors</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return JSON description of the indicator
|
||||
* @throws IndicatorSerializationException if serialization fails due to
|
||||
* reflection errors, class loading
|
||||
* issues, or JSON generation problems.
|
||||
* This exception wraps all underlying
|
||||
* serialization failures, providing a
|
||||
* consistent exception type for error
|
||||
* handling.
|
||||
* @since 0.19
|
||||
*/
|
||||
default String toJson() {
|
||||
return IndicatorSerialization.toJson(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@code this} indicator into a structured descriptor that can be
|
||||
* embedded inside other component metadata.
|
||||
*
|
||||
* <p>
|
||||
* This method creates a {@link ComponentDescriptor} that represents the
|
||||
* indicator's structure, including its type, numeric parameters, and child
|
||||
* indicators. The descriptor can be used for serialization, comparison, or
|
||||
* embedding within larger component hierarchies (such as rules or strategies).
|
||||
*
|
||||
* <p>
|
||||
* The descriptor extraction process:
|
||||
* <ul>
|
||||
* <li>Uses reflection to introspect the indicator's fields</li>
|
||||
* <li>Extracts numeric constructor parameters</li>
|
||||
* <li>Recursively processes child indicators, handling circular references</li>
|
||||
* <li>Builds a tree structure representing the indicator's composition</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return component descriptor for the indicator
|
||||
* @throws IndicatorSerializationException if descriptor creation fails due to
|
||||
* reflection errors, class loading
|
||||
* issues, or problems processing
|
||||
* circular references. This exception
|
||||
* wraps all underlying failures,
|
||||
* providing a consistent exception type
|
||||
* for error handling.
|
||||
* @since 0.19
|
||||
*/
|
||||
default ComponentDescriptor toDescriptor() {
|
||||
return IndicatorSerialization.describe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs an indicator instance from its serialized representation.
|
||||
*
|
||||
* <p>
|
||||
* This method parses a JSON payload (typically generated by {@link #toJson()})
|
||||
* and reconstructs an equivalent indicator instance. The deserialization
|
||||
* process:
|
||||
* <ul>
|
||||
* <li>Parses the JSON into a component descriptor structure</li>
|
||||
* <li>Resolves indicator types by simple name from the classpath</li>
|
||||
* <li>Matches constructor parameters to descriptor values</li>
|
||||
* <li>Recursively instantiates child indicators</li>
|
||||
* <li>Constructs the indicator using reflection-based constructor matching</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Important:</strong> The indicator type must be resolvable from the
|
||||
* classpath. Custom indicator classes must be in the
|
||||
* {@code org.ta4j.core.indicators} package or registered appropriately for
|
||||
* successful deserialization.
|
||||
*
|
||||
* @param series backing series to attach to the reconstructed indicator
|
||||
* @param json serialized indicator payload generated by {@link #toJson()}
|
||||
* @return indicator instance
|
||||
* @throws IndicatorSerializationException if deserialization fails due to:
|
||||
* <ul>
|
||||
* <li>Invalid or malformed JSON
|
||||
* syntax</li>
|
||||
* <li>Unknown indicator type (class not
|
||||
* found or not in expected
|
||||
* package)</li>
|
||||
* <li>Missing or incompatible
|
||||
* constructor parameters</li>
|
||||
* <li>Constructor instantiation
|
||||
* failures</li>
|
||||
* <li>Class loading or reflection
|
||||
* errors</li>
|
||||
* </ul>
|
||||
* This exception wraps all underlying
|
||||
* deserialization failures, providing a
|
||||
* consistent exception type for error
|
||||
* handling. The original cause is
|
||||
* preserved and can be accessed via
|
||||
* {@link Throwable#getCause()}.
|
||||
* @since 0.19
|
||||
*/
|
||||
static Indicator<?> fromJson(BarSeries series, String json) {
|
||||
return IndicatorSerialization.fromJson(series, json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.io.Serial;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
/**
|
||||
* Deprecated live-trade compatibility record.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link BaseTrade} and {@link TradeFill} for new code.
|
||||
* </p>
|
||||
*
|
||||
* @param index trade index
|
||||
* @param time execution timestamp (UTC)
|
||||
* @param price execution price per asset
|
||||
* @param amount execution amount
|
||||
* @param fee execution fee (nullable, defaults to zero)
|
||||
* @param side execution side (BUY/SELL)
|
||||
* @param orderId optional order identifier
|
||||
* @param correlationId optional correlation identifier
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
public record LiveTrade(int index, Instant time, Num price, Num amount, Num fee, ExecutionSide side, String orderId,
|
||||
String correlationId) implements Trade, ExecutionFill {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 3196554864123769210L;
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final CostModel RECORDED_COST_MODEL = RecordedTradeCostModel.INSTANCE;
|
||||
|
||||
public LiveTrade {
|
||||
DeprecationNotifier.warnOnce(LiveTrade.class, "org.ta4j.core.BaseTrade");
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("index must be >= 0");
|
||||
}
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(price, "price");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
Objects.requireNonNull(side, "side");
|
||||
if (fee == null) {
|
||||
fee = price.getNumFactory().zero();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trade.TradeType getType() {
|
||||
return side.toTradeType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getPricePerAsset() {
|
||||
return price;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getNetPrice() {
|
||||
if (amount.isZero()) {
|
||||
return price;
|
||||
}
|
||||
Num costPerAsset = fee.dividedBy(amount);
|
||||
if (side == ExecutionSide.BUY) {
|
||||
return price.plus(costPerAsset);
|
||||
}
|
||||
return price.minus(costPerAsset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num getCost() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CostModel getCostModel() {
|
||||
return RECORDED_COST_MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCorrelationId() {
|
||||
return correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this trade with a new index.
|
||||
*
|
||||
* @param index the trade index
|
||||
* @return a trade with the provided index
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public LiveTrade withIndex(int index) {
|
||||
return new LiveTrade(index, time, price, amount, fee, side, orderId, correlationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("index", index);
|
||||
json.addProperty("time", time == null ? null : time.toString());
|
||||
json.addProperty("price", price == null ? null : price.toString());
|
||||
json.addProperty("amount", amount == null ? null : amount.toString());
|
||||
json.addProperty("fee", fee == null ? null : fee.toString());
|
||||
json.addProperty("side", side == null ? null : side.name());
|
||||
json.addProperty("orderId", orderId);
|
||||
json.addProperty("correlationId", correlationId);
|
||||
return GSON.toJson(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
/**
|
||||
* Deprecated live-trading compatibility facade.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link BaseTradingRecord} for new code. This type remains available in
|
||||
* the 0.22.x line so existing adapters can migrate without a hard patch-line
|
||||
* break.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
public class LiveTradingRecord extends BaseTradingRecord implements PositionLedger {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 7960596064337713648L;
|
||||
|
||||
/**
|
||||
* Creates a live trading record with BUY entries and FIFO matching.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public LiveTradingRecord() {
|
||||
this(TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a live trading record.
|
||||
*
|
||||
* @param startingType entry trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public LiveTradingRecord(TradeType startingType) {
|
||||
this(startingType, ExecutionMatchPolicy.FIFO, RecordedTradeCostModel.INSTANCE, new ZeroCostModel(), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a live trading record.
|
||||
*
|
||||
* @param startingType entry trade type
|
||||
* @param matchPolicy matching policy
|
||||
* @param transactionCostModel ignored in favor of recorded fees
|
||||
* @param holdingCostModel holding cost model
|
||||
* @param startIndex optional start index
|
||||
* @param endIndex optional end index
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public LiveTradingRecord(TradeType startingType, ExecutionMatchPolicy matchPolicy, CostModel transactionCostModel,
|
||||
CostModel holdingCostModel, Integer startIndex, Integer endIndex) {
|
||||
super(startingType, matchPolicy, RecordedTradeCostModel.INSTANCE,
|
||||
holdingCostModel == null ? new ZeroCostModel() : holdingCostModel, startIndex, endIndex);
|
||||
warnDeprecated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a live trade using an auto-incremented trade index.
|
||||
*
|
||||
* @param trade live trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void recordFill(LiveTrade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
TradeFill fill = new TradeFill(-1, trade.time(), trade.price(), trade.amount(), trade.fee(), trade.side(),
|
||||
trade.orderId(), trade.correlationId());
|
||||
super.operate(Trade.fromFill(fill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a live trade using the provided trade index.
|
||||
*
|
||||
* @param index trade index
|
||||
* @param trade live trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void recordFill(int index, LiveTrade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
TradeFill fill = new TradeFill(index, trade.time(), trade.price(), trade.amount(), trade.fee(), trade.side(),
|
||||
trade.orderId(), trade.correlationId());
|
||||
super.operate(Trade.fromFill(fill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a live fill using the deprecated {@link ExecutionFill} contract.
|
||||
*
|
||||
* @param fill execution fill
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public void recordExecutionFill(ExecutionFill fill) {
|
||||
Objects.requireNonNull(fill, "fill");
|
||||
ExecutionSide side = resolveExecutionSide(fill.side());
|
||||
Instant time = fill.time() == null ? Instant.EPOCH : fill.time();
|
||||
TradeFill tradeFill = new TradeFill(fill.index(), time, fill.price(), fill.amount(), fill.fee(), side,
|
||||
fill.orderId(), fill.correlationId());
|
||||
super.operate(Trade.fromFill(tradeFill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehydrates transient cost models after deserialization.
|
||||
*
|
||||
* <p>
|
||||
* Live trading records always use {@link RecordedTradeCostModel} for
|
||||
* transaction costs.
|
||||
* </p>
|
||||
*
|
||||
* @param holdingCostModel holding cost model, null defaults to
|
||||
* {@link ZeroCostModel}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void rehydrate(CostModel holdingCostModel) {
|
||||
rehydrate(RecordedTradeCostModel.INSTANCE, holdingCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehydrates transient cost models after deserialization.
|
||||
*
|
||||
* <p>
|
||||
* Live trading records always use {@link RecordedTradeCostModel} for
|
||||
* transaction costs. The supplied transaction cost model is ignored.
|
||||
* </p>
|
||||
*
|
||||
* @param transactionCostModel ignored in favor of recorded fees
|
||||
* @param holdingCostModel holding cost model, null defaults to
|
||||
* {@link ZeroCostModel}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void rehydrate(CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
super.rehydrate(RecordedTradeCostModel.INSTANCE, holdingCostModel);
|
||||
}
|
||||
|
||||
private ExecutionSide resolveExecutionSide(ExecutionSide side) {
|
||||
if (side != null) {
|
||||
return side;
|
||||
}
|
||||
Position currentPosition = getCurrentPosition();
|
||||
if (currentPosition == null || !currentPosition.isOpened() || currentPosition.getEntry() == null) {
|
||||
return getStartingType() == TradeType.BUY ? ExecutionSide.BUY : ExecutionSide.SELL;
|
||||
}
|
||||
return currentPosition.getEntry().isBuy() ? ExecutionSide.SELL : ExecutionSide.BUY;
|
||||
}
|
||||
|
||||
private static void warnDeprecated() {
|
||||
DeprecationNotifier.warnOnce(LiveTradingRecord.class, "org.ta4j.core.BaseTradingRecord");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.ta4j.core.num.NaN.NaN;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* A {@code Position} models either a closed entry/exit pair or an open position
|
||||
* snapshot with only an entry trade.
|
||||
*
|
||||
* <p>
|
||||
* The exit trade has the complement type of the entry trade, i.e.:
|
||||
* <ul>
|
||||
* <li>entry == BUY --> exit == SELL
|
||||
* <li>entry == SELL --> exit == BUY
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Open-position inspection APIs on {@link TradingRecord} also use this type, so
|
||||
* callers can query per-lot and net exposure through one consistent contract.
|
||||
* </p>
|
||||
*/
|
||||
public class Position implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -5484709075767220358L;
|
||||
|
||||
/** The entry trade */
|
||||
private Trade entry;
|
||||
|
||||
/** The exit trade */
|
||||
private Trade exit;
|
||||
|
||||
/** The type of the entry trade */
|
||||
private final TradeType startingType;
|
||||
|
||||
/** The cost model for transactions of the asset */
|
||||
private final transient CostModel transactionCostModel;
|
||||
|
||||
/** The cost model for holding the asset */
|
||||
private final transient CostModel holdingCostModel;
|
||||
|
||||
/** Constructor with {@link #startingType} = BUY. */
|
||||
public Position() {
|
||||
this(TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param startingType the starting {@link TradeType trade type} of the position
|
||||
* (i.e. type of the entry trade)
|
||||
*/
|
||||
public Position(TradeType startingType) {
|
||||
this(startingType, new ZeroCostModel(), new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param startingType the starting {@link TradeType trade type} of the
|
||||
* position (i.e. type of the entry trade)
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding asset (e.g. borrowing)
|
||||
*/
|
||||
public Position(TradeType startingType, CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
if (startingType == null) {
|
||||
throw new IllegalArgumentException("Starting type must not be null");
|
||||
}
|
||||
this.startingType = startingType;
|
||||
this.transactionCostModel = transactionCostModel;
|
||||
this.holdingCostModel = holdingCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entry the entry {@link Trade trade}
|
||||
* @param exit the exit {@link Trade trade}
|
||||
*/
|
||||
public Position(Trade entry, Trade exit) {
|
||||
this(entry, exit, entry.getCostModel(), new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entry the entry {@link Trade trade}
|
||||
* @param exit the exit {@link Trade trade}
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding asset (e.g. borrowing)
|
||||
*/
|
||||
public Position(Trade entry, Trade exit, CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
|
||||
if (entry.getType().equals(exit.getType())) {
|
||||
throw new IllegalArgumentException("Both trades must have different types");
|
||||
}
|
||||
|
||||
if (!(entry.getCostModel().equals(transactionCostModel))
|
||||
|| !(exit.getCostModel().equals(transactionCostModel))) {
|
||||
throw new IllegalArgumentException("Trades and the position must incorporate the same trading cost model");
|
||||
}
|
||||
|
||||
this.startingType = entry.getType();
|
||||
this.entry = entry;
|
||||
this.exit = exit;
|
||||
this.transactionCostModel = transactionCostModel;
|
||||
this.holdingCostModel = holdingCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for an open position.
|
||||
*
|
||||
* @param entry the entry {@link Trade trade}
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding asset (e.g. borrowing)
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Position(Trade entry, CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
Objects.requireNonNull(entry, "entry");
|
||||
if (!(entry.getCostModel().equals(transactionCostModel))) {
|
||||
throw new IllegalArgumentException("Trades and the position must incorporate the same trading cost model");
|
||||
}
|
||||
this.startingType = entry.getType();
|
||||
this.entry = entry;
|
||||
this.exit = null;
|
||||
this.transactionCostModel = transactionCostModel;
|
||||
this.holdingCostModel = holdingCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the entry {@link Trade trade} of the position
|
||||
*/
|
||||
public Trade getEntry() {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the exit {@link Trade trade} of the position
|
||||
*/
|
||||
public Trade getExit() {
|
||||
return exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry-side direction of this position.
|
||||
*
|
||||
* @return the entry side, or {@code null} when the position has no entry yet
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public ExecutionSide side() {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.isBuy() ? ExecutionSide.BUY : ExecutionSide.SELL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry amount of this position.
|
||||
*
|
||||
* <p>
|
||||
* For aggregated open positions this is the net open amount.
|
||||
* </p>
|
||||
*
|
||||
* @return the entry amount, or {@code null} when the position has no entry yet
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num amount() {
|
||||
return entry == null ? null : entry.getAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average entry price of this position.
|
||||
*
|
||||
* <p>
|
||||
* For standard positions this is the entry trade price. For aggregated open
|
||||
* positions this is the weighted average entry price of the net exposure.
|
||||
* </p>
|
||||
*
|
||||
* @return the average entry price, or {@code null} when the position has no
|
||||
* entry yet
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num averageEntryPrice() {
|
||||
return entry == null ? null : entry.getPricePerAsset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total entry cost of this position.
|
||||
*
|
||||
* @return the total entry cost, or {@code null} when the position has no entry
|
||||
* yet
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num totalEntryCost() {
|
||||
return entry == null ? null : entry.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry fees currently carried by this position.
|
||||
*
|
||||
* <p>
|
||||
* For aggregated open positions this reflects the summed remaining entry fees.
|
||||
* </p>
|
||||
*
|
||||
* @return the entry fees, or {@code null} when the position has no entry yet
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num totalFees() {
|
||||
return entry == null ? null : entry.getCost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Position p) {
|
||||
return (entry == null ? p.getEntry() == null : entry.equals(p.getEntry()))
|
||||
&& (exit == null ? p.getExit() == null : exit.equals(p.getExit()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(entry, exit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operates the position at the index-th position.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @return the trade
|
||||
* @see #operate(int, Num, Num)
|
||||
*/
|
||||
public Trade operate(int index) {
|
||||
return operate(index, NaN, NaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operates the position at the index-th position.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param price the price
|
||||
* @param amount the amount
|
||||
* @return the trade
|
||||
* @throws IllegalStateException if {@link #isOpened()} and index {@literal <}
|
||||
* entry.index
|
||||
*/
|
||||
public Trade operate(int index, Num price, Num amount) {
|
||||
CostModel effectiveTransactionCostModel = getTransactionCostModel();
|
||||
Trade trade = null;
|
||||
if (isNew()) {
|
||||
trade = operate(new BaseTrade(index, startingType, price, amount, effectiveTransactionCostModel));
|
||||
} else if (isOpened()) {
|
||||
if (index < entry.getIndex()) {
|
||||
throw new IllegalStateException("The index i is less than the entryTrade index");
|
||||
}
|
||||
trade = operate(
|
||||
new BaseTrade(index, startingType.complementType(), price, amount, effectiveTransactionCostModel));
|
||||
}
|
||||
return trade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Operates the position with a pre-built trade.
|
||||
*
|
||||
* @param trade the trade to apply
|
||||
* @return the trade
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Trade operate(Trade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
CostModel effectiveTransactionCostModel = getTransactionCostModel();
|
||||
if (!trade.getCostModel().equals(effectiveTransactionCostModel)) {
|
||||
throw new IllegalArgumentException("Trades and the position must incorporate the same trading cost model");
|
||||
}
|
||||
if (isNew()) {
|
||||
if (trade.getType() != startingType) {
|
||||
throw new IllegalArgumentException("The first trade type must match the starting type");
|
||||
}
|
||||
entry = trade;
|
||||
return trade;
|
||||
}
|
||||
if (isOpened()) {
|
||||
if (trade.getType() != startingType.complementType()) {
|
||||
throw new IllegalArgumentException("The exit trade type must complement the entry trade type");
|
||||
}
|
||||
if (trade.getIndex() < entry.getIndex()) {
|
||||
throw new IllegalStateException("The index i is less than the entryTrade index");
|
||||
}
|
||||
exit = trade;
|
||||
return trade;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the position is closed, false otherwise
|
||||
*/
|
||||
public boolean isClosed() {
|
||||
return (entry != null) && (exit != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the position is opened, false otherwise
|
||||
*/
|
||||
public boolean isOpened() {
|
||||
return (entry != null) && (exit == null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the position is new, false otherwise
|
||||
*/
|
||||
public boolean isNew() {
|
||||
return (entry == null) && (exit == null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if position is closed and {@link #getProfit()} > 0
|
||||
*/
|
||||
public boolean hasProfit() {
|
||||
return getProfit().isPositive();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if position is closed and {@link #getProfit()} {@literal <} 0
|
||||
*/
|
||||
public boolean hasLoss() {
|
||||
return getProfit().isNegative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the net profit of the position if it is closed. The net profit
|
||||
* includes any trading costs.
|
||||
*
|
||||
* @return the profit or loss of the position
|
||||
*/
|
||||
public Num getProfit() {
|
||||
if (isOpened()) {
|
||||
return zero();
|
||||
} else {
|
||||
return getGrossProfit(exit.getPricePerAsset()).minus(getPositionCost());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the net profit of the position. If it is open, calculates the
|
||||
* profit until the final bar. The net profit includes any trading costs.
|
||||
*
|
||||
* @param finalIndex the index of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @param finalPrice the price of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @return the profit or loss of the position
|
||||
*/
|
||||
public Num getProfit(int finalIndex, Num finalPrice) {
|
||||
Num grossProfit = getGrossProfit(finalPrice);
|
||||
Num tradingCost = getPositionCost(finalIndex);
|
||||
return grossProfit.minus(tradingCost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross profit of the position if it is closed. The gross profit
|
||||
* excludes any trading costs.
|
||||
*
|
||||
* @return the gross profit of the position
|
||||
*/
|
||||
public Num getGrossProfit() {
|
||||
if (isOpened()) {
|
||||
return zero();
|
||||
} else {
|
||||
return getGrossProfit(exit.getPricePerAsset());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross profit of the position. The gross profit excludes any
|
||||
* trading costs.
|
||||
*
|
||||
* @param finalPrice the price of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @return the profit or loss of the position
|
||||
*/
|
||||
public Num getGrossProfit(Num finalPrice) {
|
||||
Num grossProfit;
|
||||
if (isOpened()) {
|
||||
grossProfit = entry.getAmount().multipliedBy(finalPrice).minus(entry.getValue());
|
||||
} else {
|
||||
grossProfit = exit.getValue().minus(entry.getValue());
|
||||
}
|
||||
|
||||
// Profits of long position are losses of short
|
||||
if (entry.isSell()) {
|
||||
grossProfit = grossProfit.negate();
|
||||
}
|
||||
return grossProfit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross return of the position if it is closed. The gross return
|
||||
* excludes any trading costs (and includes the base).
|
||||
*
|
||||
* @return the gross return of the position in percent
|
||||
* @see #getGrossReturn(Num)
|
||||
*/
|
||||
public Num getGrossReturn() {
|
||||
if (isOpened()) {
|
||||
return zero();
|
||||
} else {
|
||||
return getGrossReturn(exit.getPricePerAsset());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross return of the position, if it exited at the provided
|
||||
* price. The gross return excludes any trading costs (and includes the base).
|
||||
*
|
||||
* @param finalPrice the price of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @return the gross return of the position in percent
|
||||
* @see #getGrossReturn(Num, Num)
|
||||
*/
|
||||
public Num getGrossReturn(Num finalPrice) {
|
||||
return getGrossReturn(getEntry().getPricePerAsset(), finalPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross return of the position. If either the entry or exit
|
||||
* price is {@code NaN}, the close price from given {@code barSeries} is used.
|
||||
* The gross return excludes any trading costs (and includes the base).
|
||||
*
|
||||
* @param barSeries
|
||||
* @return the gross return in percent with entry and exit prices from the
|
||||
* barSeries
|
||||
* @see #getGrossReturn(Num, Num)
|
||||
*/
|
||||
public Num getGrossReturn(BarSeries barSeries) {
|
||||
Num entryPrice = getEntry().getPricePerAsset(barSeries);
|
||||
Num exitPrice = getExit().getPricePerAsset(barSeries);
|
||||
return getGrossReturn(entryPrice, exitPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the gross return between entry and exit price in percent. Includes
|
||||
* the base.
|
||||
*
|
||||
* <p>
|
||||
* For example:
|
||||
* <ul>
|
||||
* <li>For buy position with a profit of 4%, it returns 1.04 (includes the base)
|
||||
* <li>For sell position with a loss of 4%, it returns 0.96 (includes the base)
|
||||
* </ul>
|
||||
*
|
||||
* @param entryPrice the entry price
|
||||
* @param exitPrice the exit price
|
||||
* @return the gross return in percent between entryPrice and exitPrice
|
||||
* (includes the base)
|
||||
*/
|
||||
public Num getGrossReturn(Num entryPrice, Num exitPrice) {
|
||||
if (getEntry().isBuy()) {
|
||||
return exitPrice.dividedBy(entryPrice);
|
||||
} else {
|
||||
Num one = entryPrice.getNumFactory().one();
|
||||
return ((exitPrice.dividedBy(entryPrice).minus(one)).negate()).plus(one);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total cost of the position.
|
||||
*
|
||||
* @param finalIndex the index of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @return the cost of the position
|
||||
*/
|
||||
public Num getPositionCost(int finalIndex) {
|
||||
Num transactionCost = transactionCostModel.calculate(this, finalIndex);
|
||||
Num borrowingCost = getHoldingCost(finalIndex);
|
||||
return transactionCost.plus(borrowingCost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total cost of the closed position.
|
||||
*
|
||||
* @return the cost of the position
|
||||
*/
|
||||
public Num getPositionCost() {
|
||||
Num transactionCost = transactionCostModel.calculate(this);
|
||||
Num borrowingCost = getHoldingCost();
|
||||
return transactionCost.plus(borrowingCost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the holding cost of the closed position.
|
||||
*
|
||||
* @return the cost of the position
|
||||
*/
|
||||
public Num getHoldingCost() {
|
||||
return holdingCostModel.calculate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the holding cost of the position.
|
||||
*
|
||||
* @param finalIndex the index of the final bar to be considered (if position is
|
||||
* open)
|
||||
* @return the cost of the position
|
||||
*/
|
||||
public Num getHoldingCost(int finalIndex) {
|
||||
return holdingCostModel.calculate(this, finalIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the transaction cost model, or a zero-cost model after
|
||||
* deserialization when the model is unset
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CostModel getTransactionCostModel() {
|
||||
return transactionCostModel == null ? new ZeroCostModel() : transactionCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the holding cost model, or a zero-cost model after deserialization
|
||||
* when the model is unset
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CostModel getHoldingCostModel() {
|
||||
return holdingCostModel == null ? new ZeroCostModel() : holdingCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link #startingType}
|
||||
*/
|
||||
public TradeType getStartingType() {
|
||||
return startingType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Num of 0
|
||||
*/
|
||||
private Num zero() {
|
||||
return entry.getNetPrice().getNumFactory().zero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Entry: " + entry + " exit: " + exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Deprecated compatibility view of closed and open positions.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link TradingRecord#getPositions()},
|
||||
* {@link TradingRecord#getOpenPositions()}, and
|
||||
* {@link TradingRecord#getCurrentPosition()} directly in new code.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
public interface PositionLedger {
|
||||
|
||||
/**
|
||||
* @return the recorded closed positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
List<Position> getPositions();
|
||||
|
||||
/**
|
||||
* @return open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
List<Position> getOpenPositions();
|
||||
|
||||
/**
|
||||
* @return the aggregated net open position
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Position getNetOpenPosition();
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Extension of {@link Bar} for realtime analytics that track trade side and
|
||||
* liquidity breakdowns.
|
||||
*
|
||||
* <p>
|
||||
* Side- and liquidity-aware values are optional because exchanges may not
|
||||
* provide them for every trade. When {@link #hasSideData()} or
|
||||
* {@link #hasLiquidityData()} is {@code false}, the corresponding getters
|
||||
* return zero values.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public interface RealtimeBar extends Bar {
|
||||
|
||||
/**
|
||||
* The aggressor side of a trade.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
enum Side {
|
||||
BUY, SELL
|
||||
}
|
||||
|
||||
/**
|
||||
* The liquidity classification of a trade.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
enum Liquidity {
|
||||
MAKER, TAKER
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if at least one trade with side information was
|
||||
* aggregated into this bar
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
boolean hasSideData();
|
||||
|
||||
/**
|
||||
* @return {@code true} if at least one trade with liquidity information was
|
||||
* aggregated into this bar
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
boolean hasLiquidityData();
|
||||
|
||||
/**
|
||||
* @return buy-side traded volume
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getBuyVolume();
|
||||
|
||||
/**
|
||||
* @return sell-side traded volume
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getSellVolume();
|
||||
|
||||
/**
|
||||
* @return buy-side traded amount
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getBuyAmount();
|
||||
|
||||
/**
|
||||
* @return sell-side traded amount
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getSellAmount();
|
||||
|
||||
/**
|
||||
* @return number of buy-side trades
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
long getBuyTrades();
|
||||
|
||||
/**
|
||||
* @return number of sell-side trades
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
long getSellTrades();
|
||||
|
||||
/**
|
||||
* @return maker-side traded volume
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getMakerVolume();
|
||||
|
||||
/**
|
||||
* @return taker-side traded volume
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getTakerVolume();
|
||||
|
||||
/**
|
||||
* @return maker-side traded amount
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getMakerAmount();
|
||||
|
||||
/**
|
||||
* @return taker-side traded amount
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
Num getTakerAmount();
|
||||
|
||||
/**
|
||||
* @return number of maker-side trades
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
long getMakerTrades();
|
||||
|
||||
/**
|
||||
* @return number of taker-side trades
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
long getTakerTrades();
|
||||
|
||||
/**
|
||||
* Adds a trade with optional side and liquidity classification.
|
||||
*
|
||||
* @param tradeVolume traded volume
|
||||
* @param tradePrice traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
void addTrade(Num tradeVolume, Num tradePrice, Side side, Liquidity liquidity);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.rules.AndRule;
|
||||
import org.ta4j.core.rules.NotRule;
|
||||
import org.ta4j.core.rules.OrRule;
|
||||
import org.ta4j.core.rules.XorRule;
|
||||
import org.ta4j.core.serialization.ComponentDescriptor;
|
||||
import org.ta4j.core.serialization.ComponentSerialization;
|
||||
import org.ta4j.core.serialization.RuleSerialization;
|
||||
|
||||
/**
|
||||
* A rule (also called "trading rule") used to build a {@link Strategy trading
|
||||
* strategy}. A trading rule can consist of a combination of other rules.
|
||||
*
|
||||
* <p>
|
||||
* Rules encapsulate the logic for trading decisions (e.g., crossing indicators,
|
||||
* stop-loss thresholds, or boolean conditions). They are evaluated per bar
|
||||
* index using {@link #isSatisfied(int, TradingRecord)}. Complex logic is built
|
||||
* by composing primitive rules with logical operators like {@link #and(Rule)},
|
||||
* {@link #or(Rule)}, or {@link #negation()}.
|
||||
* </p>
|
||||
*/
|
||||
public interface Rule {
|
||||
|
||||
/**
|
||||
* Controls trace logging behavior for rule evaluation.
|
||||
*
|
||||
* <p>
|
||||
* SLF4J TRACE logging is the off switch. This selector only changes the amount
|
||||
* of detail emitted during an evaluation where the relevant logger is already
|
||||
* TRACE-enabled.
|
||||
*
|
||||
* @since 0.22.7
|
||||
*/
|
||||
enum TraceMode {
|
||||
/**
|
||||
* Emit one trace event for the evaluated rule while suppressing child-rule
|
||||
* trace events inside the same scoped evaluation.
|
||||
*/
|
||||
SUMMARY,
|
||||
/** Emit trace logs for this rule and all children in an evaluation scope. */
|
||||
VERBOSE
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this rule to JSON.
|
||||
*
|
||||
* @return JSON representation
|
||||
* @since 0.22
|
||||
*/
|
||||
default String toJson() {
|
||||
ComponentDescriptor descriptor = RuleSerialization.describe(this);
|
||||
return ComponentSerialization.toJson(descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a rule from JSON.
|
||||
*
|
||||
* @param series bar series context
|
||||
* @param json payload
|
||||
* @return reconstructed rule
|
||||
* @since 0.22
|
||||
*/
|
||||
static Rule fromJson(BarSeries series, String json) {
|
||||
ComponentDescriptor descriptor = ComponentSerialization.parse(json);
|
||||
return RuleSerialization.fromDescriptor(series, descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rule another trading rule
|
||||
* @return a rule which is the AND combination of this rule with the provided
|
||||
* one
|
||||
*/
|
||||
default Rule and(Rule rule) {
|
||||
return new AndRule(this, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rule another trading rule
|
||||
* @return a rule which is the OR combination of this rule with the provided one
|
||||
*/
|
||||
default Rule or(Rule rule) {
|
||||
return new OrRule(this, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rule another trading rule
|
||||
* @return a rule which is the XOR combination of this rule with the provided
|
||||
* one
|
||||
*/
|
||||
default Rule xor(Rule rule) {
|
||||
return new XorRule(this, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a rule which is the logical negation of this rule
|
||||
*/
|
||||
default Rule negation() {
|
||||
return new NotRule(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @return true if this rule is satisfied for the provided index, false
|
||||
* otherwise
|
||||
*/
|
||||
default boolean isSatisfied(int index) {
|
||||
return isSatisfied(index, (TradingRecord) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @return true if this rule is satisfied for the provided index, false
|
||||
* otherwise
|
||||
*/
|
||||
boolean isSatisfied(int index, TradingRecord tradingRecord);
|
||||
|
||||
/**
|
||||
* Evaluates this rule once with the supplied trace detail. Implementations that
|
||||
* do not support scoped tracing may ignore {@code traceMode} and delegate to
|
||||
* {@link #isSatisfied(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link TraceMode#VERBOSE}
|
||||
* @return true if this rule is satisfied for the provided index, false
|
||||
* otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean isSatisfiedWithTraceMode(int index, TraceMode traceMode) {
|
||||
return isSatisfiedWithTraceMode(index, null, traceMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates this rule once with the supplied trace detail. Implementations that
|
||||
* do not support scoped tracing may ignore {@code traceMode} and delegate to
|
||||
* {@link #isSatisfied(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link TraceMode#VERBOSE}
|
||||
* @return true if this rule is satisfied for the provided index, false
|
||||
* otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean isSatisfiedWithTraceMode(int index, TradingRecord tradingRecord, TraceMode traceMode) {
|
||||
return isSatisfied(index, tradingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a human friendly name for this rule. Implementations that support naming
|
||||
* should override this method.
|
||||
*
|
||||
* <p>
|
||||
* The default implementation is a no-op and does not store the name.
|
||||
*
|
||||
* @param name desired name; {@code null} or blank should reset the rule name
|
||||
* back to the implementation specific default
|
||||
* @since 0.19
|
||||
*/
|
||||
default void setName(String name) {
|
||||
// no-op by default to preserve backwards compatibility for custom Rule
|
||||
// implementations that do not support naming yet
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured name for this rule.
|
||||
*
|
||||
* @return a descriptive name or a lightweight default
|
||||
* @since 0.19
|
||||
*/
|
||||
default String getName() {
|
||||
return toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serial;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.utils.DeprecationNotifier;
|
||||
|
||||
/**
|
||||
* Deprecated simulated-trade compatibility facade.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link BaseTrade} or the static factory methods on {@link Trade} for new
|
||||
* code. This type remains available in the 0.22.x line so existing backtest
|
||||
* integrations can migrate without a hard patch-line break.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
public class SimulatedTrade extends BaseTrade {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -905474949010114150L;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, BarSeries series, Trade.TradeType type) {
|
||||
this(index, series, type, series.numFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
* @param amount the trade amount
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, BarSeries series, Trade.TradeType type, Num amount) {
|
||||
this(index, series, type, amount, new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param type the trade type
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution cost
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, BarSeries series, Trade.TradeType type, Num amount,
|
||||
CostModel transactionCostModel) {
|
||||
super(index, series, type, amount, transactionCostModel);
|
||||
warnDeprecated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, Trade.TradeType type, Num pricePerAsset) {
|
||||
this(index, type, pricePerAsset, pricePerAsset.getNumFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, Trade.TradeType type, Num pricePerAsset, Num amount) {
|
||||
this(index, type, pricePerAsset, amount, new ZeroCostModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param index the index the trade is executed
|
||||
* @param type the trade type
|
||||
* @param pricePerAsset the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @since 0.22.2
|
||||
*/
|
||||
protected SimulatedTrade(int index, Trade.TradeType type, Num pricePerAsset, Num amount,
|
||||
CostModel transactionCostModel) {
|
||||
super(index, type, pricePerAsset, amount, transactionCostModel);
|
||||
warnDeprecated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @return a BUY trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade buyAt(int index, BarSeries series) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a BUY trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade buyAt(int index, Num price, Num amount, CostModel transactionCostModel) {
|
||||
return new SimulatedTrade(index, Trade.TradeType.BUY, price, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return a BUY trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade buyAt(int index, Num price, Num amount) {
|
||||
return new SimulatedTrade(index, Trade.TradeType.BUY, price, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @return a BUY trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade buyAt(int index, BarSeries series, Num amount) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.BUY, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a BUY trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade buyAt(int index, BarSeries series, Num amount, CostModel transactionCostModel) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.BUY, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @return a SELL trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade sellAt(int index, BarSeries series) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.SELL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return a SELL trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade sellAt(int index, Num price, Num amount) {
|
||||
return new SimulatedTrade(index, Trade.TradeType.SELL, price, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a SELL trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade sellAt(int index, Num price, Num amount, CostModel transactionCostModel) {
|
||||
return new SimulatedTrade(index, Trade.TradeType.SELL, price, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @return a SELL trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade sellAt(int index, BarSeries series, Num amount) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.SELL, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a SELL trade
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public static SimulatedTrade sellAt(int index, BarSeries series, Num amount, CostModel transactionCostModel) {
|
||||
return new SimulatedTrade(index, series, Trade.TradeType.SELL, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
private static void warnDeprecated() {
|
||||
DeprecationNotifier.warnOnce(SimulatedTrade.class, "org.ta4j.core.BaseTrade");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.serialization.ComponentDescriptor;
|
||||
import org.ta4j.core.serialization.StrategySerialization;
|
||||
|
||||
/**
|
||||
* A {@code Strategy} (also called "trading strategy") is a pair of
|
||||
* complementary (entry and exit) {@link Rule rules}. It may recommend to enter
|
||||
* or to exit. Recommendations are based respectively on the entry rule or on
|
||||
* the exit rule.
|
||||
*
|
||||
* <p>
|
||||
* In ta4j, a strategy is evaluated bar by bar during a backtest (via
|
||||
* {@code BarSeriesManager} or {@code BacktestExecutor}) or in real-time. It
|
||||
* signals {@code shouldEnter} or {@code shouldExit} based on the current index
|
||||
* and trading record. Strategies can be composed using logical operators like
|
||||
* {@link #and(Strategy)} or {@link #or(Strategy)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Live-evaluation reminder: ta4j evaluates whatever bar state you provide at
|
||||
* {@code index}. If your integration replaces the last bar as new ticks arrive,
|
||||
* strategy decisions are evaluated against that still-forming bar (live
|
||||
* candle). If your integration evaluates only after appending a finished bar,
|
||||
* decisions are based on closed candles.
|
||||
* </p>
|
||||
*/
|
||||
public interface Strategy {
|
||||
|
||||
/**
|
||||
* @return the name of the strategy
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* @return the entry rule
|
||||
*/
|
||||
Rule getEntryRule();
|
||||
|
||||
/**
|
||||
* Returns the starting trade type for this strategy.
|
||||
*
|
||||
* <p>
|
||||
* Defaults to {@link TradeType#BUY} (long-only). Override when the strategy is
|
||||
* meant to run in short-only mode.
|
||||
* </p>
|
||||
*
|
||||
* @return starting trade type
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default TradeType getStartingType() {
|
||||
return TradeType.BUY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the exit rule
|
||||
*/
|
||||
Rule getExitRule();
|
||||
|
||||
/**
|
||||
* @param strategy the other strategy
|
||||
* @return the AND combination of two {@link Strategy strategies}
|
||||
*/
|
||||
Strategy and(Strategy strategy);
|
||||
|
||||
/**
|
||||
* @param strategy the other strategy
|
||||
* @return the OR combination of two {@link Strategy strategies}
|
||||
*/
|
||||
Strategy or(Strategy strategy);
|
||||
|
||||
/**
|
||||
* @param name the name of the strategy
|
||||
* @param strategy the other strategy
|
||||
* @param unstableBars the number of first bars in a bar series that this
|
||||
* strategy ignores
|
||||
* @return the AND combination of two {@link Strategy strategies}
|
||||
*/
|
||||
Strategy and(String name, Strategy strategy, int unstableBars);
|
||||
|
||||
/**
|
||||
* @param name the name of the strategy
|
||||
* @param strategy the other strategy
|
||||
* @param unstableBars the number of first bars in a bar series that this
|
||||
* strategy ignores
|
||||
* @return the OR combination of two {@link Strategy strategies}
|
||||
*/
|
||||
Strategy or(String name, Strategy strategy, int unstableBars);
|
||||
|
||||
/**
|
||||
* @return the opposite of the {@link Strategy strategy}
|
||||
*/
|
||||
Strategy opposite();
|
||||
|
||||
/**
|
||||
* @param unstableBars the number of first bars in a bar series that this
|
||||
* strategy ignores
|
||||
*/
|
||||
void setUnstableBars(int unstableBars);
|
||||
|
||||
/**
|
||||
* @return unstableBars the number of first bars in a bar series that this
|
||||
* strategy ignores
|
||||
*/
|
||||
int getUnstableBars();
|
||||
|
||||
/**
|
||||
* @param index a bar index
|
||||
* @return true if this strategy is unstable at the provided index, false
|
||||
* otherwise (stable)
|
||||
*/
|
||||
boolean isUnstableAt(int index);
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @return true to recommend a trade, false otherwise (no recommendation)
|
||||
*/
|
||||
default boolean shouldOperate(int index, TradingRecord tradingRecord) {
|
||||
Position position = tradingRecord.getCurrentPosition();
|
||||
if (position.isNew()) {
|
||||
return shouldEnter(index, tradingRecord);
|
||||
} else if (position.isOpened()) {
|
||||
return shouldExit(index, tradingRecord);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* <b>Implementation note:</b> This overload ignores trading state. In live
|
||||
* execution, prefer {@link #shouldEnter(int, TradingRecord)} so rules can see
|
||||
* open positions and avoid repeated entry signals while a position is already
|
||||
* open.
|
||||
* </p>
|
||||
*
|
||||
* @param index the bar index
|
||||
* @return true to recommend to enter, false otherwise
|
||||
*/
|
||||
default boolean shouldEnter(int index) {
|
||||
return shouldEnter(index, (TradingRecord) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* <b>Implementation note:</b> Use this overload for live systems so entry
|
||||
* decisions include the current position state. After broker-confirmed fills,
|
||||
* keep {@code tradingRecord} synchronized with executed fills.
|
||||
* </p>
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @return true to recommend to enter, false otherwise
|
||||
*/
|
||||
default boolean shouldEnter(int index, TradingRecord tradingRecord) {
|
||||
return !isUnstableAt(index) && getEntryRule().isSatisfied(index, tradingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the entry rule once with the supplied trace detail. Implementations
|
||||
* that do not support scoped tracing may ignore {@code traceMode} and delegate
|
||||
* to {@link #shouldEnter(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link Rule.TraceMode#VERBOSE}
|
||||
* @return true to recommend to enter, false otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean shouldEnterWithTraceMode(int index, Rule.TraceMode traceMode) {
|
||||
return shouldEnterWithTraceMode(index, null, traceMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the entry rule once with the supplied trace detail. Implementations
|
||||
* that do not support scoped tracing may ignore {@code traceMode} and delegate
|
||||
* to {@link #shouldEnter(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link Rule.TraceMode#VERBOSE}
|
||||
* @return true to recommend to enter, false otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean shouldEnterWithTraceMode(int index, TradingRecord tradingRecord, Rule.TraceMode traceMode) {
|
||||
return shouldEnter(index, tradingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @return true to recommend to exit, false otherwise
|
||||
*/
|
||||
default boolean shouldExit(int index) {
|
||||
return shouldExit(index, (TradingRecord) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @return true to recommend to exit, false otherwise
|
||||
*/
|
||||
default boolean shouldExit(int index, TradingRecord tradingRecord) {
|
||||
return !isUnstableAt(index) && getExitRule().isSatisfied(index, tradingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the exit rule once with the supplied trace detail. Implementations
|
||||
* that do not support scoped tracing may ignore {@code traceMode} and delegate
|
||||
* to {@link #shouldExit(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link Rule.TraceMode#VERBOSE}
|
||||
* @return true to recommend to exit, false otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean shouldExitWithTraceMode(int index, Rule.TraceMode traceMode) {
|
||||
return shouldExitWithTraceMode(index, null, traceMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the exit rule once with the supplied trace detail. Implementations
|
||||
* that do not support scoped tracing may ignore {@code traceMode} and delegate
|
||||
* to {@link #shouldExit(int, TradingRecord)}.
|
||||
*
|
||||
* @param index the bar index
|
||||
* @param tradingRecord the potentially needed trading history
|
||||
* @param traceMode trace detail for this evaluation only; {@code null} uses
|
||||
* {@link Rule.TraceMode#VERBOSE}
|
||||
* @return true to recommend to exit, false otherwise
|
||||
* @since 0.22.7
|
||||
*/
|
||||
default boolean shouldExitWithTraceMode(int index, TradingRecord tradingRecord, Rule.TraceMode traceMode) {
|
||||
return shouldExit(index, tradingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes {@code this} strategy into a JSON payload that captures its
|
||||
* metadata and rule descriptors.
|
||||
*
|
||||
* @return JSON description of the strategy
|
||||
* @throws NullPointerException if the strategy or any of its components are
|
||||
* {@code null}
|
||||
* @throws IllegalStateException if serialization fails due to an internal error
|
||||
* during JSON generation
|
||||
* @throws RuntimeException if serialization fails due to an I/O error or
|
||||
* other runtime exception during JSON processing
|
||||
* @since 0.19
|
||||
*/
|
||||
default String toJson() {
|
||||
return StrategySerialization.toJson(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@code this} strategy into a structured descriptor that can be
|
||||
* embedded inside other component metadata.
|
||||
*
|
||||
* @return component descriptor for the strategy
|
||||
* @throws NullPointerException if the strategy or any of its rules are
|
||||
* {@code null}
|
||||
* @throws IllegalArgumentException if rule serialization fails due to
|
||||
* unsupported constructor signatures or
|
||||
* invalid rule configurations
|
||||
* @since 0.19
|
||||
*/
|
||||
default ComponentDescriptor toDescriptor() {
|
||||
return StrategySerialization.describe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a strategy instance from its serialized representation.
|
||||
*
|
||||
* @param series backing series to attach to the reconstructed strategy; must
|
||||
* not be {@code null}
|
||||
* @param json serialized strategy payload generated by {@link #toJson()};
|
||||
* must not be {@code null} and must be a valid JSON
|
||||
* representation of a strategy descriptor
|
||||
* @return reconstructed strategy instance with entry and exit rules restored
|
||||
* from the descriptor
|
||||
* @throws NullPointerException if {@code series} or {@code json} is
|
||||
* {@code null}
|
||||
* @throws IllegalArgumentException if the JSON payload is malformed, missing
|
||||
* required component descriptors (entry/exit
|
||||
* rules), contains incompatible component
|
||||
* types, or fails validation during
|
||||
* deserialization
|
||||
* @throws IllegalStateException if strategy construction fails due to
|
||||
* missing constructors, instantiation errors,
|
||||
* or unresolved component dependencies
|
||||
* @since 0.19
|
||||
*/
|
||||
static Strategy fromJson(BarSeries series, String json) {
|
||||
return StrategySerialization.fromJson(series, json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.RecordedTradeCostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Read-only trade contract shared by simulated and live executions.
|
||||
*
|
||||
* <ul>
|
||||
* <li>the index (in the {@link BarSeries bar series}) on which the trade is
|
||||
* executed</li>
|
||||
* <li>a {@link TradeType type} (BUY or SELL)</li>
|
||||
* <li>a price per asset (optional)</li>
|
||||
* <li>a trade amount (optional)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Metadata fields (timestamp, instrument, ids) are optional and may return
|
||||
* {@code null}. They loosely mirror the attributes in XChange's trade DTO so
|
||||
* adapters can preserve exchange-provided identifiers when available.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Use the static factory methods on {@link Trade} for new code. The concrete
|
||||
* implementation type is an internal detail.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public interface Trade extends Serializable {
|
||||
|
||||
/** The type of a trade. */
|
||||
enum TradeType {
|
||||
|
||||
/** A BUY corresponds to a <i>BID</i> trade. */
|
||||
BUY {
|
||||
@Override
|
||||
public TradeType complementType() {
|
||||
return SELL;
|
||||
}
|
||||
},
|
||||
|
||||
/** A SELL corresponds to an <i>ASK</i> trade. */
|
||||
SELL {
|
||||
@Override
|
||||
public TradeType complementType() {
|
||||
return BUY;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return the complementary trade type
|
||||
*/
|
||||
public abstract TradeType complementType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the trade type (BUY or SELL)
|
||||
*/
|
||||
TradeType getType();
|
||||
|
||||
/**
|
||||
* @return the index the trade is executed
|
||||
*/
|
||||
int getIndex();
|
||||
|
||||
/**
|
||||
* @return the trade price per asset
|
||||
*/
|
||||
Num getPricePerAsset();
|
||||
|
||||
/**
|
||||
* @param barSeries the bar series
|
||||
* @return the trade price per asset, or, if {@code NaN}, the close price from
|
||||
* the supplied {@link BarSeries}
|
||||
*/
|
||||
default Num getPricePerAsset(BarSeries barSeries) {
|
||||
Num price = getPricePerAsset();
|
||||
if (price.isNaN()) {
|
||||
return barSeries.getBar(getIndex()).getClosePrice();
|
||||
}
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the net price per asset for the trade (i.e.
|
||||
* {@link #getPricePerAsset()} with trading costs)
|
||||
*/
|
||||
Num getNetPrice();
|
||||
|
||||
/**
|
||||
* @return the trade amount
|
||||
*/
|
||||
Num getAmount();
|
||||
|
||||
/**
|
||||
* @return the simulated costs of the trade as calculated by the configured
|
||||
* {@link CostModel}
|
||||
*/
|
||||
Num getCost();
|
||||
|
||||
/**
|
||||
* @return the cost model for trade execution
|
||||
*/
|
||||
CostModel getCostModel();
|
||||
|
||||
/**
|
||||
* @return execution timestamp if available, otherwise {@code null}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default Instant getTime() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return exchange-provided trade id if available, otherwise {@code null}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default String getId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return instrument identifier (symbol/pair) if available, otherwise
|
||||
* {@code null}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default String getInstrument() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return originating order id if available, otherwise {@code null}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default String getOrderId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return correlation id if available, otherwise {@code null}
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default String getCorrelationId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this is a BUY trade, false otherwise
|
||||
*/
|
||||
default boolean isBuy() {
|
||||
return getType() == TradeType.BUY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this is a SELL trade, false otherwise
|
||||
*/
|
||||
default boolean isSell() {
|
||||
return getType() == TradeType.SELL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the value of a trade (without transaction cost)
|
||||
*/
|
||||
default Num getValue() {
|
||||
return getPricePerAsset().multipliedBy(getAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns execution fills for this trade.
|
||||
*
|
||||
* <p>
|
||||
* Default simulated trades expose a single fill. Aggregated/partial trades may
|
||||
* return multiple fills. The default single fill mirrors trade-level metadata
|
||||
* (time, fee, order/correlation ids) when available.
|
||||
* </p>
|
||||
*
|
||||
* @return execution fills of this trade
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<TradeFill> getFills() {
|
||||
ExecutionSide side = getType() == TradeType.BUY ? ExecutionSide.BUY : ExecutionSide.SELL;
|
||||
return List.of(new TradeFill(getIndex(), getTime(), getPricePerAsset(), getAmount(), getCost(), side,
|
||||
getOrderId(), getCorrelationId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves execution fills for the provided trade.
|
||||
*
|
||||
* <p>
|
||||
* Trades should expose fills via {@link #getFills()}. When an implementation
|
||||
* returns an empty list, this method falls back to index/price/amount to
|
||||
* preserve compatibility with legacy scalar trade semantics.
|
||||
* </p>
|
||||
*
|
||||
* @param trade trade to inspect
|
||||
* @return immutable execution fills for the trade
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static List<TradeFill> executionFillsOf(Trade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
List<TradeFill> fills = List.copyOf(trade.getFills());
|
||||
if (!fills.isEmpty()) {
|
||||
return fills;
|
||||
}
|
||||
ExecutionSide side = trade.getType() == TradeType.BUY ? ExecutionSide.BUY : ExecutionSide.SELL;
|
||||
return List.of(new TradeFill(trade.getIndex(), trade.getTime(), trade.getPricePerAsset(), trade.getAmount(),
|
||||
trade.getCost(), side, trade.getOrderId(), trade.getCorrelationId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one execution fill using recorded-fee semantics.
|
||||
*
|
||||
* <p>
|
||||
* The fill must expose {@link TradeFill#side()} so the trade direction is
|
||||
* explicit at construction time.
|
||||
* </p>
|
||||
*
|
||||
* @param fill execution fill
|
||||
* @return a trade representing the provided fill
|
||||
* @throws IllegalArgumentException when {@code fill.side()} is missing
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFill(TradeFill fill) {
|
||||
return fromFill(fill, RecordedTradeCostModel.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one execution fill using an explicit cost model.
|
||||
*
|
||||
* <p>
|
||||
* The fill must expose {@link TradeFill#side()} so the trade direction is
|
||||
* explicit at construction time.
|
||||
* </p>
|
||||
*
|
||||
* @param fill execution fill
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @return a trade representing the provided fill
|
||||
* @throws IllegalArgumentException when {@code fill.side()} is missing
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFill(TradeFill fill, CostModel transactionCostModel) {
|
||||
Objects.requireNonNull(fill, "fill");
|
||||
if (fill.side() == null) {
|
||||
throw new IllegalArgumentException("fill.side must be set when trade type is not provided");
|
||||
}
|
||||
return fromFill(fill.side().toTradeType(), fill, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one execution fill using recorded-fee semantics.
|
||||
*
|
||||
* @param type trade type
|
||||
* @param fill execution fill
|
||||
* @return a trade representing the provided fill
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFill(TradeType type, TradeFill fill) {
|
||||
return fromFill(type, fill, RecordedTradeCostModel.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one execution fill.
|
||||
*
|
||||
* @param type trade type
|
||||
* @param fill execution fill
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @return a trade representing the provided fill
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFill(TradeType type, TradeFill fill, CostModel transactionCostModel) {
|
||||
Objects.requireNonNull(fill, "fill");
|
||||
return fromFills(type, List.of(fill), transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one or more execution fills using recorded-fee
|
||||
* semantics.
|
||||
*
|
||||
* @param type trade type
|
||||
* @param fills execution fills (must not be empty)
|
||||
* @return a trade representing the provided fills
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFills(TradeType type, List<TradeFill> fills) {
|
||||
return fromFills(type, fills, RecordedTradeCostModel.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade from one or more execution fills.
|
||||
*
|
||||
* <p>
|
||||
* The returned trade is a {@link BaseTrade}. Single-fill inputs keep scalar
|
||||
* semantics; multi-fill inputs preserve full fill progression while exposing
|
||||
* aggregated price/amount views.
|
||||
* </p>
|
||||
*
|
||||
* @param type trade type
|
||||
* @param fills execution fills (must not be empty)
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @return a trade representing the provided fills
|
||||
* @throws IllegalArgumentException when fills are empty or invalid
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Trade fromFills(TradeType type, List<TradeFill> fills, CostModel transactionCostModel) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(fills, "fills");
|
||||
Objects.requireNonNull(transactionCostModel, "transactionCostModel");
|
||||
if (fills.isEmpty()) {
|
||||
throw new IllegalArgumentException("fills must not be empty");
|
||||
}
|
||||
return new BaseTrade(type, fills, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @return a BUY trade
|
||||
*/
|
||||
static Trade buyAt(int index, BarSeries series) {
|
||||
return new BaseTrade(index, series, TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a BUY trade
|
||||
*/
|
||||
static Trade buyAt(int index, Num price, Num amount, CostModel transactionCostModel) {
|
||||
return new BaseTrade(index, TradeType.BUY, price, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return a BUY trade
|
||||
*/
|
||||
static Trade buyAt(int index, Num price, Num amount) {
|
||||
return new BaseTrade(index, TradeType.BUY, price, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @return a BUY trade
|
||||
*/
|
||||
static Trade buyAt(int index, BarSeries series, Num amount) {
|
||||
return new BaseTrade(index, series, TradeType.BUY, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a BUY trade
|
||||
*/
|
||||
static Trade buyAt(int index, BarSeries series, Num amount, CostModel transactionCostModel) {
|
||||
return new BaseTrade(index, series, TradeType.BUY, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @return a SELL trade
|
||||
*/
|
||||
static Trade sellAt(int index, BarSeries series) {
|
||||
return new BaseTrade(index, series, TradeType.SELL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return a SELL trade
|
||||
*/
|
||||
static Trade sellAt(int index, Num price, Num amount) {
|
||||
return new BaseTrade(index, TradeType.SELL, price, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a SELL trade
|
||||
*/
|
||||
static Trade sellAt(int index, Num price, Num amount, CostModel transactionCostModel) {
|
||||
return new BaseTrade(index, TradeType.SELL, price, amount, transactionCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @return a SELL trade
|
||||
*/
|
||||
static Trade sellAt(int index, BarSeries series, Num amount) {
|
||||
return new BaseTrade(index, series, TradeType.SELL, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the index the trade is executed
|
||||
* @param series the bar series
|
||||
* @param amount the trade amount
|
||||
* @param transactionCostModel the cost model for trade execution
|
||||
* @return a SELL trade
|
||||
*/
|
||||
static Trade sellAt(int index, BarSeries series, Num amount, CostModel transactionCostModel) {
|
||||
return new BaseTrade(index, series, TradeType.SELL, amount, transactionCostModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Immutable execution fill used to represent partial trade executions.
|
||||
*
|
||||
* <p>
|
||||
* This is the preferred fill primitive for new integrations that need
|
||||
* broker-confirmed execution recording through
|
||||
* {@link TradingRecord#operate(TradeFill)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Metadata fields ({@code time}, {@code side}, IDs) are optional and may be
|
||||
* {@code null}. {@code fee} defaults to zero when omitted.
|
||||
* </p>
|
||||
*
|
||||
* @param index bar index where the fill happened; {@code -1} is
|
||||
* reserved for fills awaiting recorder-assigned indices
|
||||
* @param time optional execution timestamp (UTC)
|
||||
* @param price execution price per asset
|
||||
* @param amount executed amount
|
||||
* @param fee optional execution fee (defaults to zero when null)
|
||||
* @param side optional execution side
|
||||
* @param orderId optional order id
|
||||
* @param correlationId optional correlation id
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record TradeFill(int index, Instant time, Num price, Num amount, Num fee, ExecutionSide side, String orderId,
|
||||
String correlationId) implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -258216480640174496L;
|
||||
|
||||
/**
|
||||
* Creates a trade fill with scalar fields only.
|
||||
*
|
||||
* @param index bar index where the fill happened
|
||||
* @param price execution price per asset
|
||||
* @param amount executed amount
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradeFill(int index, Num price, Num amount) {
|
||||
this(index, null, price, amount, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade fill with execution side/time metadata.
|
||||
*
|
||||
* @param index bar index where the fill happened
|
||||
* @param time execution timestamp (UTC), nullable
|
||||
* @param price execution price per asset
|
||||
* @param amount executed amount
|
||||
* @param side execution side, nullable
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradeFill(int index, Instant time, Num price, Num amount, ExecutionSide side) {
|
||||
this(index, time, price, amount, null, side, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade fill.
|
||||
*
|
||||
* @throws NullPointerException if price or amount is null
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradeFill {
|
||||
if (index < -1) {
|
||||
throw new IllegalArgumentException("index must be >= -1");
|
||||
}
|
||||
Objects.requireNonNull(price, "price");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
if (fee == null) {
|
||||
fee = price.getNumFactory().zero();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core;
|
||||
|
||||
import static org.ta4j.core.num.NaN.NaN;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* A {@code TradingRecord} holds the full history/record of a trading session
|
||||
* when running a {@link Strategy strategy}. It can be used to:
|
||||
*
|
||||
* <ul>
|
||||
* <li>analyze the performance of a {@link Strategy strategy}
|
||||
* <li>check whether some {@link Rule rules} are satisfied (while running a
|
||||
* strategy)
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* {@link Trade} is the public trade contract. Concrete trade implementations
|
||||
* are internal details and should not be required by strategy or backtest code.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Execution metadata on trades/fills ({@code time}, {@code side},
|
||||
* {@code orderId}, {@code correlationId}) may be missing in simulated
|
||||
* environments. Implementations should preserve this metadata when provided and
|
||||
* apply deterministic fallbacks when it is absent.
|
||||
* </p>
|
||||
*/
|
||||
public interface TradingRecord extends Serializable {
|
||||
|
||||
/**
|
||||
* @return the entry type (BUY or SELL) of the first trade in the trading
|
||||
* session
|
||||
*/
|
||||
TradeType getStartingType();
|
||||
|
||||
/**
|
||||
* @return the name of the TradingRecord
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Places a trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the trade
|
||||
*/
|
||||
default void operate(int index) {
|
||||
operate(index, NaN, NaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the trade
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
*/
|
||||
void operate(int index, Num price, Num amount);
|
||||
|
||||
/**
|
||||
* Places one execution fill in the trading record.
|
||||
*
|
||||
* <p>
|
||||
* This is a convenience overload for streaming partial fills directly into a
|
||||
* fill-aware record. It is equivalent to {@code operate(Trade.fromFill(fill))}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The fill must expose {@link TradeFill#side()} so the trade direction is
|
||||
* explicit at ingestion time.
|
||||
* </p>
|
||||
*
|
||||
* @param fill the execution fill to place
|
||||
* @throws IllegalArgumentException when {@code fill.side()} is missing
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default void operate(TradeFill fill) {
|
||||
operate(Trade.fromFill(fill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a pre-built trade in the trading record.
|
||||
*
|
||||
* <p>
|
||||
* This is useful for execution models that aggregate partial fills into a
|
||||
* single entry or exit trade.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The default implementation delegates to {@link #operate(int, Num, Num)} and
|
||||
* therefore supports only index/price/amount semantics. Implementations that
|
||||
* store additional execution metadata should override this method.
|
||||
* </p>
|
||||
*
|
||||
* @param trade the trade to place
|
||||
* @throws UnsupportedOperationException if {@code trade} contains multiple
|
||||
* fills and this implementation has not
|
||||
* overridden this method
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default void operate(Trade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
List<TradeFill> fills = Trade.executionFillsOf(trade);
|
||||
if (fills.size() > 1) {
|
||||
throw new UnsupportedOperationException(
|
||||
"This TradingRecord implementation must override operate(Trade) to preserve multi-fill trades");
|
||||
}
|
||||
TradeFill fill = fills.getFirst();
|
||||
operate(fill.index(), fill.price(), fill.amount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an entry trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the entry
|
||||
* @return true if the entry has been placed, false otherwise
|
||||
*/
|
||||
default boolean enter(int index) {
|
||||
return enter(index, NaN, NaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an entry trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the entry
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return true if the entry has been placed, false otherwise
|
||||
*/
|
||||
boolean enter(int index, Num price, Num amount);
|
||||
|
||||
/**
|
||||
* Places an entry trade in the trading record.
|
||||
*
|
||||
* @param trade the entry trade to place
|
||||
* @return true if the entry has been placed, false otherwise
|
||||
* @throws IllegalArgumentException when trade type is not the configured entry
|
||||
* type
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default boolean enter(Trade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
TradeType expectedEntryType = getStartingType();
|
||||
if (trade.getType() != expectedEntryType) {
|
||||
throw new IllegalArgumentException("Entry trade type must be " + expectedEntryType);
|
||||
}
|
||||
if (isClosed()) {
|
||||
operate(trade);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an exit trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the exit
|
||||
* @return true if the exit has been placed, false otherwise
|
||||
*/
|
||||
default boolean exit(int index) {
|
||||
return exit(index, NaN, NaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an exit trade in the trading record.
|
||||
*
|
||||
* @param index the index to place the exit
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount
|
||||
* @return true if the exit has been placed, false otherwise
|
||||
*/
|
||||
boolean exit(int index, Num price, Num amount);
|
||||
|
||||
/**
|
||||
* Places an exit trade in the trading record.
|
||||
*
|
||||
* @param trade the exit trade to place
|
||||
* @return true if the exit has been placed, false otherwise
|
||||
* @throws IllegalArgumentException when trade type is not the configured exit
|
||||
* type
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default boolean exit(Trade trade) {
|
||||
Objects.requireNonNull(trade, "trade");
|
||||
TradeType expectedExitType = getStartingType().complementType();
|
||||
if (trade.getType() != expectedExitType) {
|
||||
throw new IllegalArgumentException("Exit trade type must be " + expectedExitType);
|
||||
}
|
||||
if (!isClosed()) {
|
||||
operate(trade);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if no position is open, false otherwise
|
||||
*/
|
||||
default boolean isClosed() {
|
||||
return !getCurrentPosition().isOpened();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the transaction cost model
|
||||
*/
|
||||
CostModel getTransactionCostModel();
|
||||
|
||||
/**
|
||||
* @return holding cost model
|
||||
*/
|
||||
CostModel getHoldingCostModel();
|
||||
|
||||
/**
|
||||
* @return the recorded closed positions
|
||||
*/
|
||||
List<Position> getPositions();
|
||||
|
||||
/**
|
||||
* @return the number of recorded closed positions
|
||||
*/
|
||||
default int getPositionCount() {
|
||||
return getPositions().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonical current-position view for this record.
|
||||
*
|
||||
* <p>
|
||||
* When the record has open exposure, this is the aggregated net-open
|
||||
* {@link Position}. When the record is flat, this returns a new/empty
|
||||
* {@link Position} snapshot instead of {@code null}.
|
||||
* </p>
|
||||
*
|
||||
* @return the canonical current-position view
|
||||
*/
|
||||
Position getCurrentPosition();
|
||||
|
||||
/**
|
||||
* Returns recorded execution fees when the implementation maintains a fee
|
||||
* ledger separate from modeled transaction costs.
|
||||
*
|
||||
* <p>
|
||||
* Legacy trading-record implementations can rely on this default and return
|
||||
* {@code null} when they only support model-derived transaction costs.
|
||||
* </p>
|
||||
*
|
||||
* @return recorded execution fees, or {@code null} when unavailable
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default Num getRecordedTotalFees() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns open positions when supported by the implementation.
|
||||
*
|
||||
* <p>
|
||||
* Lot-aware implementations should return one open {@link Position} snapshot
|
||||
* per remaining open lot. Legacy trading-record implementations that only model
|
||||
* a single synthetic current position can rely on this default and return an
|
||||
* empty list.
|
||||
* </p>
|
||||
*
|
||||
* @return open per-lot position snapshots
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<Position> getOpenPositions() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the aggregated net open position when supported by the
|
||||
* implementation.
|
||||
*
|
||||
* <p>
|
||||
* New code should prefer {@link #getCurrentPosition()}. This method remains as
|
||||
* a compatibility alias for callers that previously requested a dedicated open
|
||||
* position view. Legacy implementations that do not expose a distinct net-open
|
||||
* snapshot can rely on the default and return {@code null}.
|
||||
* </p>
|
||||
*
|
||||
* @return aggregated net open position, or {@code null} when no position is
|
||||
* open
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Deprecated(since = "0.22.4")
|
||||
default Position getNetOpenPosition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the last closed position recorded
|
||||
*/
|
||||
default Position getLastPosition() {
|
||||
List<Position> positions = getPositions();
|
||||
if (!positions.isEmpty()) {
|
||||
return positions.getLast();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the trades recorded
|
||||
*/
|
||||
List<Trade> getTrades();
|
||||
|
||||
/**
|
||||
* @return the last trade recorded
|
||||
*/
|
||||
default Trade getLastTrade() {
|
||||
List<Trade> trades = getTrades();
|
||||
if (!trades.isEmpty()) {
|
||||
return trades.getLast();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tradeType the type of the trade to get the last of
|
||||
* @return the last trade (of the provided type) recorded
|
||||
*/
|
||||
default Trade getLastTrade(TradeType tradeType) {
|
||||
List<Trade> trades = getTrades();
|
||||
for (int i = trades.size() - 1; i >= 0; i--) {
|
||||
Trade trade = trades.get(i);
|
||||
if (trade.getType() == tradeType) {
|
||||
return trade;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the last entry trade recorded
|
||||
*/
|
||||
default Trade getLastEntry() {
|
||||
return getLastTrade(getStartingType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the last exit trade recorded
|
||||
*/
|
||||
default Trade getLastExit() {
|
||||
return getLastTrade(getStartingType().complementType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the start of the recording (included)
|
||||
*/
|
||||
Integer getStartIndex();
|
||||
|
||||
/**
|
||||
* @return the end of the recording (included)
|
||||
*/
|
||||
Integer getEndIndex();
|
||||
|
||||
/**
|
||||
* @param series the bar series, not null
|
||||
* @return the {@link #getStartIndex()} if not null and greater than
|
||||
* {@link BarSeries#getBeginIndex()}, otherwise
|
||||
* {@link BarSeries#getBeginIndex()}
|
||||
*/
|
||||
default int getStartIndex(BarSeries series) {
|
||||
return getStartIndex() == null ? series.getBeginIndex() : Math.max(getStartIndex(), series.getBeginIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param series the bar series, not null
|
||||
* @return the {@link #getEndIndex()} if not null and less than
|
||||
* {@link BarSeries#getEndIndex()}, otherwise
|
||||
* {@link BarSeries#getEndIndex()}
|
||||
*/
|
||||
default int getEndIndex(BarSeries series) {
|
||||
return getEndIndex() == null ? series.getEndIndex() : Math.min(getEndIndex(), series.getEndIndex());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
|
||||
/**
|
||||
* Aggregates a list of {@link Bar bars} into another one.
|
||||
*/
|
||||
public interface BarAggregator {
|
||||
|
||||
/**
|
||||
* Aggregates the {@code bars} into another one.
|
||||
*
|
||||
* @param bars the bars to be aggregated
|
||||
* @return aggregated bars
|
||||
*/
|
||||
List<Bar> aggregate(List<Bar> bars);
|
||||
|
||||
/**
|
||||
* Validates that source bars are contiguous and have a consistent positive
|
||||
* interval.
|
||||
*
|
||||
* @param bars source bars in chronological order
|
||||
* @return the common source interval
|
||||
* @throws NullPointerException if {@code bars} is {@code null}
|
||||
* @throws IllegalArgumentException if any bar is null, has an invalid period, a
|
||||
* measured-period mismatch, an uneven
|
||||
* interval, or a gap between intervals
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default Duration requireEvenIntervals(List<Bar> bars) {
|
||||
Objects.requireNonNull(bars, "bars");
|
||||
if (bars.isEmpty()) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
String aggregatorName = getClass().getSimpleName();
|
||||
Bar firstBar = bars.getFirst();
|
||||
Duration expectedTimePeriod = requireConsistentPeriod(firstBar, aggregatorName, 0);
|
||||
Bar previousBar = firstBar;
|
||||
|
||||
for (int i = 1; i < bars.size(); i++) {
|
||||
Bar currentBar = bars.get(i);
|
||||
Duration currentTimePeriod = requireConsistentPeriod(currentBar, aggregatorName, i);
|
||||
if (!expectedTimePeriod.equals(currentTimePeriod)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s requires even source intervals: bar %d has period %s but expected %s.",
|
||||
aggregatorName, i, currentTimePeriod, expectedTimePeriod));
|
||||
}
|
||||
if (!currentBar.getBeginTime().equals(previousBar.getEndTime())) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"%s requires contiguous source intervals: bar %d begins at %s but previous bar ended at %s.",
|
||||
aggregatorName, i, currentBar.getBeginTime(), previousBar.getEndTime()));
|
||||
}
|
||||
previousBar = currentBar;
|
||||
}
|
||||
|
||||
return expectedTimePeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a numeric threshold/configuration value is finite and strictly
|
||||
* positive.
|
||||
*
|
||||
* @param value the candidate numeric value
|
||||
* @param parameterName the constructor/argument name
|
||||
* @return {@code value} when validation succeeds
|
||||
* @throws NullPointerException if {@code value} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code value} is non-finite, not
|
||||
* decimal-representable, or not greater than
|
||||
* zero
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static Number requirePositiveFiniteNumber(Number value, String parameterName) {
|
||||
Objects.requireNonNull(value, parameterName);
|
||||
ensureFiniteNumber(value, parameterName);
|
||||
BigDecimal decimal = parseDecimal(value, parameterName);
|
||||
if (decimal.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException(parameterName + " must be greater than zero.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Duration requireConsistentPeriod(Bar bar, String aggregatorName, int index) {
|
||||
if (bar == null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s requires non-null source bars: bar %d is null.", aggregatorName, index));
|
||||
}
|
||||
Duration timePeriod = bar.getTimePeriod();
|
||||
if (timePeriod == null || timePeriod.isNegative() || timePeriod.isZero()) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s requires positive source intervals: bar %d has invalid period %s.",
|
||||
aggregatorName, index, timePeriod));
|
||||
}
|
||||
Instant beginTime = bar.getBeginTime();
|
||||
Instant endTime = bar.getEndTime();
|
||||
if (beginTime == null || endTime == null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s requires non-null source timestamps: bar %d has begin=%s, end=%s.",
|
||||
aggregatorName, index, beginTime, endTime));
|
||||
}
|
||||
Duration measuredPeriod = Duration.between(beginTime, endTime);
|
||||
if (!measuredPeriod.equals(timePeriod)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s requires consistent source intervals: bar %d spans %s but period is %s.",
|
||||
aggregatorName, index, measuredPeriod, timePeriod));
|
||||
}
|
||||
return timePeriod;
|
||||
}
|
||||
|
||||
private static BigDecimal parseDecimal(Number value, String parameterName) {
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
parameterName + " must be a finite numeric value representable as decimal.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureFiniteNumber(Number value, String parameterName) {
|
||||
if (value instanceof Double d && !Double.isFinite(d)) {
|
||||
throw new IllegalArgumentException(parameterName + " must be finite.");
|
||||
}
|
||||
if (value instanceof Float f && !Float.isFinite(f)) {
|
||||
throw new IllegalArgumentException(parameterName + " must be finite.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
/**
|
||||
* Aggregates a {@link BarSeries} into another one.
|
||||
*/
|
||||
public interface BarSeriesAggregator {
|
||||
|
||||
/**
|
||||
* Aggregates the {@code series} into another one.
|
||||
*
|
||||
* @param series the series to be aggregated
|
||||
* @return aggregated series
|
||||
*/
|
||||
default BarSeries aggregate(BarSeries series) {
|
||||
return aggregate(series, series.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates the {@code series} into another one.
|
||||
*
|
||||
* @param series the series to be aggregated
|
||||
* @param aggregatedSeriesName the name for the aggregated series
|
||||
* @return aggregated series with specified name
|
||||
*/
|
||||
BarSeries aggregate(BarSeries series, String aggregatedSeriesName);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
|
||||
/**
|
||||
* Aggregates a {@link BaseBarSeries} into another one using a
|
||||
* {@link BarAggregator}.
|
||||
*/
|
||||
public class BaseBarSeriesAggregator implements BarSeriesAggregator {
|
||||
|
||||
private final BarAggregator barAggregator;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barAggregator the {@link BarAggregator}
|
||||
*/
|
||||
public BaseBarSeriesAggregator(BarAggregator barAggregator) {
|
||||
this.barAggregator = barAggregator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries aggregate(BarSeries series, String aggregatedSeriesName) {
|
||||
final List<Bar> aggregatedBars = barAggregator.aggregate(series.getBarData());
|
||||
return new BaseBarSeriesBuilder().withName(aggregatedSeriesName).withBars(aggregatedBars).build();
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.BaseBar;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Aggregates a list of {@link BaseBar bars} into another one by
|
||||
* {@link BaseBar#timePeriod duration}.
|
||||
*/
|
||||
public class DurationBarAggregator implements BarAggregator {
|
||||
|
||||
/** The target time period that aggregated bars should have. */
|
||||
private final Duration timePeriod;
|
||||
|
||||
private final boolean onlyFinalBars;
|
||||
|
||||
/**
|
||||
* Duration based bar aggregator. Only bars with elapsed time (final bars) will
|
||||
* be created.
|
||||
*
|
||||
* @param timePeriod the target time period that aggregated bars should have
|
||||
*/
|
||||
public DurationBarAggregator(Duration timePeriod) {
|
||||
this(timePeriod, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration based bar aggregator.
|
||||
*
|
||||
* @param timePeriod the target time period that aggregated bars should have
|
||||
* @param onlyFinalBars if true, only bars with elapsed time (final bars) will
|
||||
* be created, otherwise also pending bars
|
||||
*/
|
||||
public DurationBarAggregator(Duration timePeriod, boolean onlyFinalBars) {
|
||||
this.timePeriod = timePeriod;
|
||||
this.onlyFinalBars = onlyFinalBars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates the {@code bars} into another one by {@link #timePeriod}.
|
||||
*
|
||||
* @param bars the actual bars with actual {@code timePeriod}
|
||||
* @return the aggregated bars with new {@link #timePeriod}
|
||||
* @throws IllegalArgumentException if {@link #timePeriod} is not a
|
||||
* multiplication of actual {@code timePeriod}
|
||||
*/
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> bars) {
|
||||
final List<Bar> aggregated = new ArrayList<>();
|
||||
if (bars.isEmpty()) {
|
||||
return aggregated;
|
||||
}
|
||||
final Bar firstBar = bars.getFirst();
|
||||
// get the actual time period
|
||||
final Duration actualDur = firstBar.getTimePeriod();
|
||||
// check if new timePeriod is a multiplication of actual time period
|
||||
final boolean isMultiplication = timePeriod.getSeconds() % actualDur.getSeconds() == 0;
|
||||
if (!isMultiplication) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot aggregate bars: the new timePeriod must be a multiplication of the actual timePeriod.");
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
final Num zero = firstBar.numFactory().zero();
|
||||
while (i < bars.size()) {
|
||||
Bar bar = bars.get(i);
|
||||
final Instant beginTime = bar.getBeginTime();
|
||||
final Num open = bar.getOpenPrice();
|
||||
Num high = bar.getHighPrice();
|
||||
Num low = bar.getLowPrice();
|
||||
|
||||
Num close = null;
|
||||
Num volume = zero;
|
||||
Num amount = zero;
|
||||
long trades = 0;
|
||||
Duration sumDur = Duration.ZERO;
|
||||
|
||||
while (isInDuration(sumDur)) {
|
||||
if (i < bars.size()) {
|
||||
if (!beginTimesInDuration(beginTime, bars.get(i).getBeginTime())) {
|
||||
break;
|
||||
}
|
||||
bar = bars.get(i);
|
||||
if (high == null || bar.getHighPrice().isGreaterThan(high)) {
|
||||
high = bar.getHighPrice();
|
||||
}
|
||||
if (low == null || bar.getLowPrice().isLessThan(low)) {
|
||||
low = bar.getLowPrice();
|
||||
}
|
||||
close = bar.getClosePrice();
|
||||
|
||||
if (bar.getVolume() != null) {
|
||||
volume = volume.plus(bar.getVolume());
|
||||
}
|
||||
if (bar.getAmount() != null) {
|
||||
amount = amount.plus(bar.getAmount());
|
||||
}
|
||||
if (bar.getTrades() != 0) {
|
||||
trades = trades + bar.getTrades();
|
||||
}
|
||||
}
|
||||
|
||||
sumDur = sumDur.plus(actualDur);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!onlyFinalBars || i <= bars.size()) {
|
||||
final Bar aggregatedBar = new TimeBarBuilder().timePeriod(timePeriod)
|
||||
.endTime(beginTime.plus(timePeriod))
|
||||
.openPrice(open)
|
||||
.highPrice(high)
|
||||
.lowPrice(low)
|
||||
.closePrice(close)
|
||||
.volume(volume)
|
||||
.amount(amount)
|
||||
.trades(trades)
|
||||
.build();
|
||||
aggregated.add(aggregatedBar);
|
||||
}
|
||||
}
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
private boolean beginTimesInDuration(Instant startTime, Instant endTime) {
|
||||
return Duration.between(startTime, endTime).compareTo(timePeriod) < 0;
|
||||
}
|
||||
|
||||
private boolean isInDuration(Duration duration) {
|
||||
return duration.compareTo(timePeriod) < 0;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.bars.HeikinAshiBarBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Aggregates a list of {@link BaseBar bars} into another one by following
|
||||
* Heikin-Ashi logic
|
||||
*/
|
||||
public class HeikinAshiBarAggregator implements BarAggregator {
|
||||
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> ohlcBars) {
|
||||
var heikinAshiBars = new ArrayList<Bar>();
|
||||
var haBuilder = new HeikinAshiBarBuilder();
|
||||
Num previousOpen = null;
|
||||
Num previousClose = null;
|
||||
|
||||
for (Bar ohlcBar : ohlcBars) {
|
||||
haBuilder.timePeriod(ohlcBar.getTimePeriod())
|
||||
.endTime(ohlcBar.getEndTime())
|
||||
.openPrice(ohlcBar.getOpenPrice())
|
||||
.highPrice(ohlcBar.getHighPrice())
|
||||
.lowPrice(ohlcBar.getLowPrice())
|
||||
.closePrice(ohlcBar.getClosePrice())
|
||||
.volume(ohlcBar.getVolume())
|
||||
.amount(ohlcBar.getAmount())
|
||||
.trades(ohlcBar.getTrades());
|
||||
|
||||
if (previousOpen != null && previousClose != null) {
|
||||
haBuilder.previousHeikinAshiOpenPrice(previousOpen).previousHeikinAshiClosePrice(previousClose);
|
||||
} else {
|
||||
haBuilder.previousHeikinAshiOpenPrice(null).previousHeikinAshiClosePrice(null);
|
||||
}
|
||||
|
||||
var haBar = haBuilder.build();
|
||||
heikinAshiBars.add(haBar);
|
||||
|
||||
previousOpen = haBar.getOpenPrice();
|
||||
previousClose = haBar.getClosePrice();
|
||||
}
|
||||
return heikinAshiBars;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Aggregates source bars into range bars.
|
||||
*
|
||||
* <p>
|
||||
* A range bar is closed once the aggregated high-low range reaches the
|
||||
* configured range size. Source bars must be contiguous and evenly spaced.
|
||||
*
|
||||
* <p>
|
||||
* Usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* BarAggregator rangeAggregator = new RangeBarAggregator(2.5);
|
||||
* List<Bar> rangeBars = rangeAggregator.aggregate(sourceBars);
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Source bars that do not complete the configured range are emitted only when
|
||||
* {@code onlyFinalBars} is {@code false}.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class RangeBarAggregator implements BarAggregator {
|
||||
|
||||
private final Number rangeSize;
|
||||
private final boolean onlyFinalBars;
|
||||
|
||||
/**
|
||||
* Creates a range-bar aggregator that emits only completed range bars.
|
||||
*
|
||||
* @param rangeSize the minimum high-low range required to close a bar
|
||||
* @throws NullPointerException if {@code rangeSize} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code rangeSize} is not a finite,
|
||||
* positive value
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RangeBarAggregator(Number rangeSize) {
|
||||
this(rangeSize, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a range-bar aggregator.
|
||||
*
|
||||
* @param rangeSize the minimum high-low range required to close a bar
|
||||
* @param onlyFinalBars if {@code true}, incomplete trailing bars are omitted
|
||||
* @throws NullPointerException if {@code rangeSize} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code rangeSize} is not a finite,
|
||||
* positive value
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RangeBarAggregator(Number rangeSize, boolean onlyFinalBars) {
|
||||
this.rangeSize = BarAggregator.requirePositiveFiniteNumber(rangeSize, "rangeSize");
|
||||
this.onlyFinalBars = onlyFinalBars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates bars into range bars.
|
||||
*
|
||||
* @param bars source bars in chronological order
|
||||
* @return range bars
|
||||
* @throws NullPointerException if {@code bars} is {@code null}
|
||||
* @throws IllegalArgumentException if source intervals are uneven or
|
||||
* non-contiguous
|
||||
*/
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> bars) {
|
||||
Objects.requireNonNull(bars, "bars");
|
||||
if (bars.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
requireEvenIntervals(bars);
|
||||
|
||||
NumFactory numFactory = bars.getFirst().numFactory();
|
||||
Num resolvedRangeSize = numFactory.numOf(rangeSize);
|
||||
Num zero = numFactory.zero();
|
||||
|
||||
return ThresholdBarAggregationSupport.aggregate(bars, numFactory, onlyFinalBars, snapshot -> {
|
||||
Num currentRange = zero;
|
||||
if (snapshot.highPrice() != null && snapshot.lowPrice() != null) {
|
||||
currentRange = snapshot.highPrice().minus(snapshot.lowPrice());
|
||||
}
|
||||
return currentRange.isGreaterThanOrEqual(resolvedRangeSize);
|
||||
}, snapshot -> ThresholdBarAggregationSupport.buildTimeBar(numFactory, snapshot));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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 java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Aggregates source bars into Renko bricks.
|
||||
*
|
||||
* <p>
|
||||
* Bricks are generated from close-price movement using a configurable
|
||||
* {@code boxSize}. Reversals require a move of
|
||||
* {@code reversalAmount * boxSize}. Source bars must be contiguous and evenly
|
||||
* spaced. When multiple bricks are emitted from a single source bar, aggregated
|
||||
* volume/amount/trades are attached to the first emitted brick and set to zero
|
||||
* for additional bricks from the same source bar.
|
||||
*
|
||||
* <p>
|
||||
* Usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* BarAggregator renkoAggregator = new RenkoBarAggregator(2.0, 2);
|
||||
* List<Bar> renkoBars = renkoAggregator.aggregate(sourceBars);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class RenkoBarAggregator implements BarAggregator {
|
||||
|
||||
private enum Direction {
|
||||
NONE, UP, DOWN
|
||||
}
|
||||
|
||||
private final Number boxSize;
|
||||
private final int reversalAmount;
|
||||
|
||||
/**
|
||||
* Creates a Renko aggregator with a two-brick reversal.
|
||||
*
|
||||
* @param boxSize the price movement represented by one brick
|
||||
* @throws NullPointerException if {@code boxSize} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code boxSize} is not a finite, positive
|
||||
* value
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RenkoBarAggregator(Number boxSize) {
|
||||
this(boxSize, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Renko aggregator.
|
||||
*
|
||||
* @param boxSize the price movement represented by one brick
|
||||
* @param reversalAmount the number of boxes required for reversal
|
||||
* @throws NullPointerException if {@code boxSize} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code boxSize} is not a finite, positive
|
||||
* value, or if {@code reversalAmount <= 0}
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RenkoBarAggregator(Number boxSize, int reversalAmount) {
|
||||
this.boxSize = BarAggregator.requirePositiveFiniteNumber(boxSize, "boxSize");
|
||||
if (reversalAmount <= 0) {
|
||||
throw new IllegalArgumentException("reversalAmount must be greater than zero.");
|
||||
}
|
||||
this.reversalAmount = reversalAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates bars into Renko bricks.
|
||||
*
|
||||
* @param bars source bars in chronological order
|
||||
* @return Renko bricks
|
||||
* @throws NullPointerException if {@code bars} is {@code null}
|
||||
* @throws IllegalArgumentException if source intervals are uneven or
|
||||
* non-contiguous, or if any source bar has a
|
||||
* null close price
|
||||
*/
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> bars) {
|
||||
Objects.requireNonNull(bars, "bars");
|
||||
List<Bar> renkoBars = new ArrayList<>();
|
||||
if (bars.isEmpty()) {
|
||||
return renkoBars;
|
||||
}
|
||||
|
||||
Duration sourcePeriod = requireEvenIntervals(bars);
|
||||
NumFactory numFactory = bars.getFirst().numFactory();
|
||||
Num resolvedBoxSize = numFactory.numOf(boxSize);
|
||||
Num reversalDistance = resolvedBoxSize.multipliedBy(numFactory.numOf(reversalAmount));
|
||||
Num zero = numFactory.zero();
|
||||
|
||||
Num lastBrickClose = requireClosePrice(bars.getFirst(), 0);
|
||||
Direction direction = Direction.NONE;
|
||||
Num pendingVolume = zero;
|
||||
Num pendingAmount = zero;
|
||||
long pendingTrades = 0L;
|
||||
Instant nextBrickEndTime = bars.getFirst().getEndTime();
|
||||
|
||||
for (int i = 0; i < bars.size(); i++) {
|
||||
Bar sourceBar = bars.get(i);
|
||||
Num closePrice = requireClosePrice(sourceBar, i);
|
||||
if (sourceBar.getVolume() != null) {
|
||||
pendingVolume = pendingVolume.plus(sourceBar.getVolume());
|
||||
}
|
||||
if (sourceBar.getAmount() != null) {
|
||||
pendingAmount = pendingAmount.plus(sourceBar.getAmount());
|
||||
}
|
||||
pendingTrades += sourceBar.getTrades();
|
||||
|
||||
boolean emittedFromCurrentSourceBar = false;
|
||||
|
||||
if (direction == Direction.UP || direction == Direction.NONE) {
|
||||
while (closePrice.isGreaterThanOrEqual(lastBrickClose.plus(resolvedBoxSize))) {
|
||||
Num openPrice = lastBrickClose;
|
||||
Num close = lastBrickClose.plus(resolvedBoxSize);
|
||||
Instant brickEndTime = resolveBrickEndTime(sourceBar.getEndTime(), nextBrickEndTime);
|
||||
Num brickVolume = emittedFromCurrentSourceBar ? zero : pendingVolume;
|
||||
Num brickAmount = emittedFromCurrentSourceBar ? zero : pendingAmount;
|
||||
long brickTrades = emittedFromCurrentSourceBar ? 0L : pendingTrades;
|
||||
renkoBars.add(buildBrick(numFactory, sourcePeriod, brickEndTime, openPrice, close, brickVolume,
|
||||
brickAmount, brickTrades));
|
||||
lastBrickClose = close;
|
||||
direction = Direction.UP;
|
||||
nextBrickEndTime = brickEndTime.plus(sourcePeriod);
|
||||
if (!emittedFromCurrentSourceBar) {
|
||||
pendingVolume = zero;
|
||||
pendingAmount = zero;
|
||||
pendingTrades = 0L;
|
||||
emittedFromCurrentSourceBar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == Direction.NONE) {
|
||||
while (closePrice.isLessThanOrEqual(lastBrickClose.minus(resolvedBoxSize))) {
|
||||
Num openPrice = lastBrickClose;
|
||||
Num close = lastBrickClose.minus(resolvedBoxSize);
|
||||
Instant brickEndTime = resolveBrickEndTime(sourceBar.getEndTime(), nextBrickEndTime);
|
||||
Num brickVolume = emittedFromCurrentSourceBar ? zero : pendingVolume;
|
||||
Num brickAmount = emittedFromCurrentSourceBar ? zero : pendingAmount;
|
||||
long brickTrades = emittedFromCurrentSourceBar ? 0L : pendingTrades;
|
||||
renkoBars.add(buildBrick(numFactory, sourcePeriod, brickEndTime, openPrice, close, brickVolume,
|
||||
brickAmount, brickTrades));
|
||||
lastBrickClose = close;
|
||||
direction = Direction.DOWN;
|
||||
nextBrickEndTime = brickEndTime.plus(sourcePeriod);
|
||||
if (!emittedFromCurrentSourceBar) {
|
||||
pendingVolume = zero;
|
||||
pendingAmount = zero;
|
||||
pendingTrades = 0L;
|
||||
emittedFromCurrentSourceBar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == Direction.UP && closePrice.isLessThanOrEqual(lastBrickClose.minus(reversalDistance))) {
|
||||
while (closePrice.isLessThanOrEqual(lastBrickClose.minus(resolvedBoxSize))) {
|
||||
Num openPrice = lastBrickClose;
|
||||
Num close = lastBrickClose.minus(resolvedBoxSize);
|
||||
Instant brickEndTime = resolveBrickEndTime(sourceBar.getEndTime(), nextBrickEndTime);
|
||||
Num brickVolume = emittedFromCurrentSourceBar ? zero : pendingVolume;
|
||||
Num brickAmount = emittedFromCurrentSourceBar ? zero : pendingAmount;
|
||||
long brickTrades = emittedFromCurrentSourceBar ? 0L : pendingTrades;
|
||||
renkoBars.add(buildBrick(numFactory, sourcePeriod, brickEndTime, openPrice, close, brickVolume,
|
||||
brickAmount, brickTrades));
|
||||
lastBrickClose = close;
|
||||
direction = Direction.DOWN;
|
||||
nextBrickEndTime = brickEndTime.plus(sourcePeriod);
|
||||
if (!emittedFromCurrentSourceBar) {
|
||||
pendingVolume = zero;
|
||||
pendingAmount = zero;
|
||||
pendingTrades = 0L;
|
||||
emittedFromCurrentSourceBar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == Direction.DOWN) {
|
||||
while (closePrice.isLessThanOrEqual(lastBrickClose.minus(resolvedBoxSize))) {
|
||||
Num openPrice = lastBrickClose;
|
||||
Num close = lastBrickClose.minus(resolvedBoxSize);
|
||||
Instant brickEndTime = resolveBrickEndTime(sourceBar.getEndTime(), nextBrickEndTime);
|
||||
Num brickVolume = emittedFromCurrentSourceBar ? zero : pendingVolume;
|
||||
Num brickAmount = emittedFromCurrentSourceBar ? zero : pendingAmount;
|
||||
long brickTrades = emittedFromCurrentSourceBar ? 0L : pendingTrades;
|
||||
renkoBars.add(buildBrick(numFactory, sourcePeriod, brickEndTime, openPrice, close, brickVolume,
|
||||
brickAmount, brickTrades));
|
||||
lastBrickClose = close;
|
||||
nextBrickEndTime = brickEndTime.plus(sourcePeriod);
|
||||
if (!emittedFromCurrentSourceBar) {
|
||||
pendingVolume = zero;
|
||||
pendingAmount = zero;
|
||||
pendingTrades = 0L;
|
||||
emittedFromCurrentSourceBar = true;
|
||||
}
|
||||
}
|
||||
if (closePrice.isGreaterThanOrEqual(lastBrickClose.plus(reversalDistance))) {
|
||||
while (closePrice.isGreaterThanOrEqual(lastBrickClose.plus(resolvedBoxSize))) {
|
||||
Num openPrice = lastBrickClose;
|
||||
Num close = lastBrickClose.plus(resolvedBoxSize);
|
||||
Instant brickEndTime = resolveBrickEndTime(sourceBar.getEndTime(), nextBrickEndTime);
|
||||
Num brickVolume = emittedFromCurrentSourceBar ? zero : pendingVolume;
|
||||
Num brickAmount = emittedFromCurrentSourceBar ? zero : pendingAmount;
|
||||
long brickTrades = emittedFromCurrentSourceBar ? 0L : pendingTrades;
|
||||
renkoBars.add(buildBrick(numFactory, sourcePeriod, brickEndTime, openPrice, close, brickVolume,
|
||||
brickAmount, brickTrades));
|
||||
lastBrickClose = close;
|
||||
direction = Direction.UP;
|
||||
nextBrickEndTime = brickEndTime.plus(sourcePeriod);
|
||||
if (!emittedFromCurrentSourceBar) {
|
||||
pendingVolume = zero;
|
||||
pendingAmount = zero;
|
||||
pendingTrades = 0L;
|
||||
emittedFromCurrentSourceBar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renkoBars;
|
||||
}
|
||||
|
||||
private static Num requireClosePrice(Bar bar, int index) {
|
||||
if (bar.getClosePrice() == null) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"RenkoBarAggregator requires close prices on all source bars. Missing at index %d.", index));
|
||||
}
|
||||
return bar.getClosePrice();
|
||||
}
|
||||
|
||||
private static Bar buildBrick(NumFactory numFactory, Duration sourcePeriod, Instant endTime, Num openPrice,
|
||||
Num closePrice, Num volume, Num amount, long trades) {
|
||||
Num highPrice = openPrice.max(closePrice);
|
||||
Num lowPrice = openPrice.min(closePrice);
|
||||
return new TimeBarBuilder(numFactory).timePeriod(sourcePeriod)
|
||||
.endTime(endTime)
|
||||
.openPrice(openPrice)
|
||||
.highPrice(highPrice)
|
||||
.lowPrice(lowPrice)
|
||||
.closePrice(closePrice)
|
||||
.volume(volume)
|
||||
.amount(amount)
|
||||
.trades(trades)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Instant resolveBrickEndTime(Instant sourceBarEndTime, Instant nextBrickEndTime) {
|
||||
return sourceBarEndTime.isAfter(nextBrickEndTime) ? sourceBarEndTime : nextBrickEndTime;
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.bars.TimeBarBuilder;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Shared accumulation workflow for threshold-based bar aggregators.
|
||||
*/
|
||||
final class ThresholdBarAggregationSupport {
|
||||
|
||||
private ThresholdBarAggregationSupport() {
|
||||
}
|
||||
|
||||
static List<Bar> aggregate(List<Bar> bars, NumFactory numFactory, boolean onlyFinalBars,
|
||||
Predicate<MutableWindow> completionPredicate, Function<MutableWindow, Bar> barBuilder) {
|
||||
Objects.requireNonNull(bars, "bars");
|
||||
Objects.requireNonNull(numFactory, "numFactory");
|
||||
Objects.requireNonNull(completionPredicate, "completionPredicate");
|
||||
Objects.requireNonNull(barBuilder, "barBuilder");
|
||||
|
||||
List<Bar> aggregated = new ArrayList<>();
|
||||
if (bars.isEmpty()) {
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
MutableWindow mutableWindow = new MutableWindow(numFactory);
|
||||
for (Bar bar : bars) {
|
||||
mutableWindow.add(bar);
|
||||
if (completionPredicate.test(mutableWindow)) {
|
||||
aggregated.add(barBuilder.apply(mutableWindow));
|
||||
mutableWindow.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if (!onlyFinalBars && !mutableWindow.isEmpty()) {
|
||||
aggregated.add(barBuilder.apply(mutableWindow));
|
||||
}
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
static Bar buildTimeBar(NumFactory numFactory, MutableWindow window) {
|
||||
Duration aggregatedPeriod = Duration.between(window.beginTime(), window.endTime());
|
||||
return new TimeBarBuilder(numFactory).timePeriod(aggregatedPeriod)
|
||||
.endTime(window.endTime())
|
||||
.openPrice(window.openPrice())
|
||||
.highPrice(window.highPrice())
|
||||
.lowPrice(window.lowPrice())
|
||||
.closePrice(window.closePrice())
|
||||
.volume(window.volume())
|
||||
.amount(window.amount())
|
||||
.trades(window.trades())
|
||||
.build();
|
||||
}
|
||||
|
||||
static final class MutableWindow {
|
||||
|
||||
private final Num zero;
|
||||
private Instant beginTime;
|
||||
private Instant endTime;
|
||||
private Num openPrice;
|
||||
private Num highPrice;
|
||||
private Num lowPrice;
|
||||
private Num closePrice;
|
||||
private Num volume;
|
||||
private Num amount;
|
||||
private long trades;
|
||||
|
||||
private MutableWindow(NumFactory numFactory) {
|
||||
this.zero = numFactory.zero();
|
||||
reset();
|
||||
}
|
||||
|
||||
private void add(Bar bar) {
|
||||
if (beginTime == null) {
|
||||
beginTime = bar.getBeginTime();
|
||||
openPrice = bar.getOpenPrice();
|
||||
highPrice = bar.getHighPrice();
|
||||
lowPrice = bar.getLowPrice();
|
||||
} else {
|
||||
if (highPrice == null || (bar.getHighPrice() != null && bar.getHighPrice().isGreaterThan(highPrice))) {
|
||||
highPrice = bar.getHighPrice();
|
||||
}
|
||||
if (lowPrice == null || (bar.getLowPrice() != null && bar.getLowPrice().isLessThan(lowPrice))) {
|
||||
lowPrice = bar.getLowPrice();
|
||||
}
|
||||
}
|
||||
|
||||
endTime = bar.getEndTime();
|
||||
closePrice = bar.getClosePrice();
|
||||
if (bar.getVolume() != null) {
|
||||
volume = volume.plus(bar.getVolume());
|
||||
}
|
||||
if (bar.getAmount() != null) {
|
||||
amount = amount.plus(bar.getAmount());
|
||||
}
|
||||
trades += bar.getTrades();
|
||||
}
|
||||
|
||||
private boolean isEmpty() {
|
||||
return beginTime == null;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
beginTime = null;
|
||||
endTime = null;
|
||||
openPrice = null;
|
||||
highPrice = null;
|
||||
lowPrice = null;
|
||||
closePrice = null;
|
||||
volume = zero;
|
||||
amount = zero;
|
||||
trades = 0L;
|
||||
}
|
||||
|
||||
public Instant beginTime() {
|
||||
return beginTime;
|
||||
}
|
||||
|
||||
public Instant endTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public Num openPrice() {
|
||||
return openPrice;
|
||||
}
|
||||
|
||||
public Num highPrice() {
|
||||
return highPrice;
|
||||
}
|
||||
|
||||
public Num lowPrice() {
|
||||
return lowPrice;
|
||||
}
|
||||
|
||||
public Num closePrice() {
|
||||
return closePrice;
|
||||
}
|
||||
|
||||
public Num volume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
public Num amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public long trades() {
|
||||
return trades;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Aggregates source bars into volume-threshold bars.
|
||||
*
|
||||
* <p>
|
||||
* A volume bar is closed once the aggregated volume reaches the configured
|
||||
* threshold. Source bars must be contiguous and evenly spaced.
|
||||
*
|
||||
* <p>
|
||||
* Usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* BarAggregator volumeAggregator = new VolumeBarAggregator(10_000);
|
||||
* List<Bar> volumeBars = volumeAggregator.aggregate(sourceBars);
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Source bars that do not complete the configured volume threshold are emitted
|
||||
* only when {@code onlyFinalBars} is {@code false}.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class VolumeBarAggregator implements BarAggregator {
|
||||
|
||||
private final Number volumeThreshold;
|
||||
private final boolean onlyFinalBars;
|
||||
|
||||
/**
|
||||
* Creates a volume-bar aggregator that emits only completed volume bars.
|
||||
*
|
||||
* @param volumeThreshold the minimum aggregated volume required to close a bar
|
||||
* @throws NullPointerException if {@code volumeThreshold} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code volumeThreshold} is not a finite,
|
||||
* positive value
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public VolumeBarAggregator(Number volumeThreshold) {
|
||||
this(volumeThreshold, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a volume-bar aggregator.
|
||||
*
|
||||
* @param volumeThreshold the minimum aggregated volume required to close a bar
|
||||
* @param onlyFinalBars if {@code true}, incomplete trailing bars are omitted
|
||||
* @throws NullPointerException if {@code volumeThreshold} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code volumeThreshold} is not a finite,
|
||||
* positive value
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public VolumeBarAggregator(Number volumeThreshold, boolean onlyFinalBars) {
|
||||
this.volumeThreshold = BarAggregator.requirePositiveFiniteNumber(volumeThreshold, "volumeThreshold");
|
||||
this.onlyFinalBars = onlyFinalBars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates bars into volume bars.
|
||||
*
|
||||
* @param bars source bars in chronological order
|
||||
* @return volume bars
|
||||
* @throws NullPointerException if {@code bars} is {@code null}
|
||||
* @throws IllegalArgumentException if source intervals are uneven or
|
||||
* non-contiguous
|
||||
*/
|
||||
@Override
|
||||
public List<Bar> aggregate(List<Bar> bars) {
|
||||
Objects.requireNonNull(bars, "bars");
|
||||
if (bars.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
requireEvenIntervals(bars);
|
||||
|
||||
NumFactory numFactory = bars.getFirst().numFactory();
|
||||
Num resolvedVolumeThreshold = numFactory.numOf(volumeThreshold);
|
||||
return ThresholdBarAggregationSupport.aggregate(bars, numFactory, onlyFinalBars,
|
||||
snapshot -> snapshot.volume().isGreaterThanOrEqual(resolvedVolumeThreshold),
|
||||
snapshot -> ThresholdBarAggregationSupport.buildTimeBar(numFactory, snapshot));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
/**
|
||||
* Aggregator.
|
||||
*
|
||||
* <p>
|
||||
* This package can be used to aggregate {@link org.ta4j.core.Bar bars} by
|
||||
* various conditions, for example:
|
||||
* {@link org.ta4j.core.aggregator.DurationBarAggregator duration},
|
||||
* {@link org.ta4j.core.aggregator.RangeBarAggregator range},
|
||||
* {@link org.ta4j.core.aggregator.VolumeBarAggregator volume}, and
|
||||
* {@link org.ta4j.core.aggregator.RenkoBarAggregator Renko bricks}.
|
||||
*/
|
||||
package org.ta4j.core.aggregator;
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Options controlling how windowed criterion analysis is resolved.
|
||||
*
|
||||
* <p>
|
||||
* The default context is conservative and deterministic:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link MissingHistoryPolicy#STRICT}</li>
|
||||
* <li>{@link PositionInclusionPolicy#EXIT_IN_WINDOW}</li>
|
||||
* <li>{@link OpenPositionHandling#IGNORE}</li>
|
||||
* <li>no explicit anchor/as-of instant</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param missingHistoryPolicy behavior when requested history is unavailable
|
||||
* @param positionInclusionPolicy policy used to include closed positions in the
|
||||
* window
|
||||
* @param openPositionHandling handling strategy for open position at window
|
||||
* end; reuses the existing shared enum
|
||||
* {@link OpenPositionHandling} where
|
||||
* {@link OpenPositionHandling#IGNORE} excludes
|
||||
* open positions and
|
||||
* {@link OpenPositionHandling#MARK_TO_MARKET}
|
||||
* synthesizes window-end valuation
|
||||
* @param asOf optional anchor/as-of instant; when null, the
|
||||
* series end is used for lookback windows
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record AnalysisContext(MissingHistoryPolicy missingHistoryPolicy,
|
||||
PositionInclusionPolicy positionInclusionPolicy, OpenPositionHandling openPositionHandling, Instant asOf) {
|
||||
|
||||
/**
|
||||
* Policy for unavailable historical bars.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public enum MissingHistoryPolicy {
|
||||
/**
|
||||
* Fail fast when requested window references unavailable series history.
|
||||
*/
|
||||
STRICT,
|
||||
/**
|
||||
* Intersect the requested window with available series history.
|
||||
*/
|
||||
CLAMP
|
||||
}
|
||||
|
||||
/**
|
||||
* Policy controlling which closed positions are included in a window.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public enum PositionInclusionPolicy {
|
||||
/**
|
||||
* Include a closed position when its exit index is inside the window.
|
||||
*/
|
||||
EXIT_IN_WINDOW,
|
||||
/**
|
||||
* Include a closed position only when both entry and exit are in the window.
|
||||
*/
|
||||
FULLY_CONTAINED
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default context.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public AnalysisContext() {
|
||||
this(MissingHistoryPolicy.STRICT, PositionInclusionPolicy.EXIT_IN_WINDOW, OpenPositionHandling.IGNORE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the context.
|
||||
*/
|
||||
public AnalysisContext {
|
||||
Objects.requireNonNull(missingHistoryPolicy, "missingHistoryPolicy");
|
||||
Objects.requireNonNull(positionInclusionPolicy, "positionInclusionPolicy");
|
||||
Objects.requireNonNull(openPositionHandling, "openPositionHandling");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default context.
|
||||
*
|
||||
* @return default window analysis context
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static AnalysisContext defaults() {
|
||||
return new AnalysisContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with a different missing-history policy.
|
||||
*
|
||||
* @param policy the policy to use
|
||||
* @return updated context
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public AnalysisContext withMissingHistoryPolicy(MissingHistoryPolicy policy) {
|
||||
return new AnalysisContext(Objects.requireNonNull(policy, "policy"), positionInclusionPolicy,
|
||||
openPositionHandling, asOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with a different position-inclusion policy.
|
||||
*
|
||||
* @param policy the policy to use
|
||||
* @return updated context
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public AnalysisContext withPositionInclusionPolicy(PositionInclusionPolicy policy) {
|
||||
return new AnalysisContext(missingHistoryPolicy, Objects.requireNonNull(policy, "policy"), openPositionHandling,
|
||||
asOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with a different open-position handling.
|
||||
*
|
||||
* @param handling the handling to use
|
||||
* @return updated context
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public AnalysisContext withOpenPositionHandling(OpenPositionHandling handling) {
|
||||
return new AnalysisContext(missingHistoryPolicy, positionInclusionPolicy,
|
||||
Objects.requireNonNull(handling, "handling"), asOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with a different anchor/as-of instant.
|
||||
*
|
||||
* @param asOf the anchor instant used by lookback windows, or null to use
|
||||
* series end
|
||||
* @return updated context
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public AnalysisContext withAsOf(Instant asOf) {
|
||||
return new AnalysisContext(missingHistoryPolicy, positionInclusionPolicy, openPositionHandling, asOf);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.*;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
|
||||
final class AnalysisPositionSupport {
|
||||
|
||||
private AnalysisPositionSupport() {
|
||||
}
|
||||
|
||||
static List<Position> positionsForAnalysis(TradingRecord record, int finalIndex,
|
||||
OpenPositionHandling openPositionHandling, EquityCurveMode equityCurveMode) {
|
||||
Objects.requireNonNull(record, "record");
|
||||
Objects.requireNonNull(openPositionHandling, "openPositionHandling");
|
||||
Objects.requireNonNull(equityCurveMode, "equityCurveMode");
|
||||
List<Position> positions = new ArrayList<>();
|
||||
for (Position position : record.getPositions()) {
|
||||
if (shouldIncludePosition(position, finalIndex, openPositionHandling, equityCurveMode)) {
|
||||
positions.add(position);
|
||||
}
|
||||
}
|
||||
if (shouldIncludeOpenPositions(openPositionHandling, equityCurveMode)) {
|
||||
positions.addAll(openPositions(record, finalIndex));
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
static boolean shouldIncludePosition(Position position, int finalIndex, OpenPositionHandling openPositionHandling,
|
||||
EquityCurveMode equityCurveMode) {
|
||||
if (position == null || position.getEntry() == null) {
|
||||
return false;
|
||||
}
|
||||
int entryIndex = position.getEntry().getIndex();
|
||||
if (entryIndex > finalIndex) {
|
||||
return false;
|
||||
}
|
||||
if (!shouldIncludeOpenPositions(openPositionHandling, equityCurveMode)) {
|
||||
Trade exit = position.getExit();
|
||||
boolean isOpenAtFinalIndex = exit == null || exit.getIndex() > finalIndex;
|
||||
return !isOpenAtFinalIndex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean shouldIncludeOpenPositions(OpenPositionHandling openPositionHandling,
|
||||
EquityCurveMode equityCurveMode) {
|
||||
return equityCurveMode != EquityCurveMode.REALIZED
|
||||
&& openPositionHandling == OpenPositionHandling.MARK_TO_MARKET;
|
||||
}
|
||||
|
||||
static List<Position> openPositions(TradingRecord record, int finalIndex) {
|
||||
List<Position> openPositions = record.getOpenPositions();
|
||||
if (!openPositions.isEmpty()) {
|
||||
return openPositionsWithinRange(openPositions, finalIndex);
|
||||
}
|
||||
List<Position> positions = new ArrayList<>();
|
||||
Position current = record.getCurrentPosition();
|
||||
if (current != null && current.isOpened() && current.getEntry() != null
|
||||
&& current.getEntry().getIndex() <= finalIndex) {
|
||||
positions.add(current);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private static List<Position> openPositionsWithinRange(List<Position> openPositions, int finalIndex) {
|
||||
List<Position> positions = new ArrayList<>();
|
||||
for (Position openPosition : openPositions) {
|
||||
if (openPosition == null || !openPosition.isOpened()) {
|
||||
continue;
|
||||
}
|
||||
if (openPosition.getEntry().getIndex() > finalIndex) {
|
||||
continue;
|
||||
}
|
||||
positions.add(openPosition);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
/**
|
||||
* Executes a one-shot analysis for a series and caller-provided context.
|
||||
*
|
||||
* @param <C> analysis context type (for example degree, configuration, or
|
||||
* request object)
|
||||
* @param <R> analysis result type
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AnalysisRunner<C, R> {
|
||||
|
||||
/**
|
||||
* Runs analysis for the supplied series and context.
|
||||
*
|
||||
* @param series series to analyze
|
||||
* @param context caller-provided analysis context
|
||||
* @return analysis result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
R analyze(BarSeries series, C context);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Describes a requested analysis window.
|
||||
*
|
||||
* <p>
|
||||
* A window can be expressed by:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>explicit bar index range (inclusive/inclusive)</li>
|
||||
* <li>lookback bars (count of bars ending at an anchor)</li>
|
||||
* <li>explicit time range (inclusive/exclusive)</li>
|
||||
* <li>lookback duration (duration ending at an anchor)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Use one of the static factory methods to create an immutable window.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public sealed interface AnalysisWindow permits AnalysisWindow.BarRange, AnalysisWindow.LookbackBars,
|
||||
AnalysisWindow.TimeRange, AnalysisWindow.LookbackDuration {
|
||||
|
||||
/**
|
||||
* Creates a window from explicit bar indices.
|
||||
*
|
||||
* @param startIndexInclusive the first bar index to include
|
||||
* @param endIndexInclusive the last bar index to include
|
||||
* @return the requested bar-range window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static AnalysisWindow barRange(int startIndexInclusive, int endIndexInclusive) {
|
||||
return new BarRange(startIndexInclusive, endIndexInclusive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window from a lookback bar count.
|
||||
*
|
||||
* <p>
|
||||
* The anchor is resolved at calculation time from {@link AnalysisContext} (or
|
||||
* defaults).
|
||||
* </p>
|
||||
*
|
||||
* @param barCount the number of bars to include
|
||||
* @return the requested lookback-bars window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static AnalysisWindow lookbackBars(int barCount) {
|
||||
return new LookbackBars(barCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window from an explicit time range.
|
||||
*
|
||||
* <p>
|
||||
* Time windows use start-inclusive/end-exclusive semantics.
|
||||
* </p>
|
||||
*
|
||||
* @param startInclusive the start instant (inclusive)
|
||||
* @param endExclusive the end instant (exclusive)
|
||||
* @return the requested time-range window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static AnalysisWindow timeRange(Instant startInclusive, Instant endExclusive) {
|
||||
return new TimeRange(startInclusive, endExclusive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a window from a lookback duration.
|
||||
*
|
||||
* <p>
|
||||
* The anchor is resolved at calculation time from {@link AnalysisContext} (or
|
||||
* defaults).
|
||||
* </p>
|
||||
*
|
||||
* @param duration the lookback duration
|
||||
* @return the requested lookback-duration window
|
||||
* @since 0.22.4
|
||||
*/
|
||||
static AnalysisWindow lookbackDuration(Duration duration) {
|
||||
return new LookbackDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit bar-index range window with inclusive boundaries.
|
||||
*
|
||||
* @param startIndexInclusive the start index (inclusive)
|
||||
* @param endIndexInclusive the end index (inclusive)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record BarRange(int startIndexInclusive, int endIndexInclusive) implements AnalysisWindow {
|
||||
|
||||
/**
|
||||
* Creates a bar-index range.
|
||||
*
|
||||
* @param startIndexInclusive the start index (inclusive)
|
||||
* @param endIndexInclusive the end index (inclusive)
|
||||
*/
|
||||
public BarRange {
|
||||
if (startIndexInclusive < 0) {
|
||||
throw new IllegalArgumentException("startIndexInclusive must be >= 0");
|
||||
}
|
||||
if (endIndexInclusive < startIndexInclusive) {
|
||||
throw new IllegalArgumentException("endIndexInclusive must be >= startIndexInclusive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookback-bar-count window.
|
||||
*
|
||||
* @param barCount number of bars to include
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record LookbackBars(int barCount) implements AnalysisWindow {
|
||||
|
||||
/**
|
||||
* Creates a lookback-bar-count window.
|
||||
*
|
||||
* @param barCount number of bars to include
|
||||
*/
|
||||
public LookbackBars {
|
||||
if (barCount <= 0) {
|
||||
throw new IllegalArgumentException("barCount must be > 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit time-range window with start-inclusive/end-exclusive boundaries.
|
||||
*
|
||||
* @param startInclusive the start instant (inclusive)
|
||||
* @param endExclusive the end instant (exclusive)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record TimeRange(Instant startInclusive, Instant endExclusive) implements AnalysisWindow {
|
||||
|
||||
/**
|
||||
* Creates a time-range window.
|
||||
*
|
||||
* @param startInclusive the start instant (inclusive)
|
||||
* @param endExclusive the end instant (exclusive)
|
||||
*/
|
||||
public TimeRange {
|
||||
Objects.requireNonNull(startInclusive, "startInclusive");
|
||||
Objects.requireNonNull(endExclusive, "endExclusive");
|
||||
if (!startInclusive.isBefore(endExclusive)) {
|
||||
throw new IllegalArgumentException("startInclusive must be before endExclusive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookback-duration window.
|
||||
*
|
||||
* @param duration the lookback duration
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record LookbackDuration(Duration duration) implements AnalysisWindow {
|
||||
|
||||
/**
|
||||
* Creates a lookback-duration window.
|
||||
*
|
||||
* @param duration the lookback duration
|
||||
*/
|
||||
public LookbackDuration {
|
||||
Objects.requireNonNull(duration, "duration");
|
||||
if (duration.isZero() || duration.isNegative()) {
|
||||
throw new IllegalArgumentException("duration must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Allows to follow the money cash flow involved by a list of positions over a
|
||||
* bar series, either marked to market or using realized values only.
|
||||
*/
|
||||
public class CashFlow implements PerformanceIndicator {
|
||||
|
||||
/**
|
||||
* The bar series.
|
||||
*/
|
||||
private final BarSeries barSeries;
|
||||
|
||||
/**
|
||||
* The (accrued) cash flow sequence (without trading costs).
|
||||
*/
|
||||
private final List<Num> values;
|
||||
|
||||
/**
|
||||
* The first logical bar index materialized in {@link #values}.
|
||||
*/
|
||||
private final int valueStartIndex;
|
||||
|
||||
/**
|
||||
* The last logical bar index materialized in {@link #values}.
|
||||
*/
|
||||
private final int valueEndIndex;
|
||||
|
||||
/**
|
||||
* The equity curve calculation mode.
|
||||
*/
|
||||
private final EquityCurveMode equityCurveMode;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex index up until cash flows of open positions are
|
||||
* considered
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex, EquityCurveMode equityCurveMode,
|
||||
OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, 0, barSeries.getEndIndex(), finalIndex, equityCurveMode, openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor materializing only a bounded logical window on the original
|
||||
* series.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param startIndex first logical bar index to materialize
|
||||
* @param finalIndex last logical bar index to materialize and to
|
||||
* consider for open positions
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.5
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, int startIndex, int finalIndex,
|
||||
EquityCurveMode equityCurveMode, OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, startIndex, finalIndex, finalIndex, equityCurveMode, openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for cash flows of a closed position.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, Position position, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, new BaseTradingRecord(position), barSeries.getEndIndex(), equityCurveMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex index up until cash flows of open positions are
|
||||
* considered
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, finalIndex, equityCurveMode, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for cash flows of a closed position.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, Position position) {
|
||||
this(barSeries, position, EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for cash flows of closed positions of a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), equityCurveMode,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, EquityCurveMode equityCurveMode,
|
||||
OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), equityCurveMode, openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex index up until cash flows of open positions are
|
||||
* considered
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex) {
|
||||
this(barSeries, tradingRecord, finalIndex, EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CashFlow(BarSeries barSeries, TradingRecord tradingRecord, OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), EquityCurveMode.MARK_TO_MARKET,
|
||||
openPositionHandling);
|
||||
}
|
||||
|
||||
private CashFlow(BarSeries barSeries, TradingRecord tradingRecord, int startIndex, int endIndex, int finalIndex,
|
||||
EquityCurveMode equityCurveMode, OpenPositionHandling openPositionHandling) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
this.equityCurveMode = Objects.requireNonNull(equityCurveMode);
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
this.valueStartIndex = Math.max(0, startIndex);
|
||||
this.valueEndIndex = seriesEnd < 0 ? -1 : Math.min(Math.max(endIndex, this.valueStartIndex), seriesEnd);
|
||||
int size = this.valueEndIndex < this.valueStartIndex ? 0 : this.valueEndIndex - this.valueStartIndex + 1;
|
||||
this.values = new ArrayList<>(Collections.nCopies(size, barSeries.numFactory().one()));
|
||||
calculate(Objects.requireNonNull(tradingRecord), finalIndex, Objects.requireNonNull(openPositionHandling));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the cash flow for a single position (including accrued cashflow
|
||||
* for open positions).
|
||||
*
|
||||
* @param position a single position
|
||||
* @param finalIndex index up until cash flow of open positions is considered
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void calculatePosition(Position position, int finalIndex) {
|
||||
Trade entry = position.getEntry();
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
int entryIndex = entry.getIndex();
|
||||
if (entryIndex > finalIndex || entryIndex > seriesEnd) {
|
||||
return;
|
||||
}
|
||||
int endIndex = determineEndIndex(position, finalIndex, seriesEnd);
|
||||
int seriesBegin = barSeries.getBeginIndex();
|
||||
if (endIndex < seriesBegin) {
|
||||
return;
|
||||
}
|
||||
int windowStartIndex = Math.max(valueStartIndex, seriesBegin);
|
||||
int windowEndIndex = Math.min(valueEndIndex, seriesEnd);
|
||||
if (windowStartIndex > windowEndIndex || endIndex < windowStartIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
NumFactory numFactory = barSeries.numFactory();
|
||||
boolean isLongTrade = entry.isBuy();
|
||||
Num netEntryPrice = entry.getNetPrice();
|
||||
Num entryEquity = getStoredValue(Math.max(entryIndex, windowStartIndex));
|
||||
if (!entryEquity.isGreaterThan(numFactory.zero())) {
|
||||
return;
|
||||
}
|
||||
int ratioIndex = endIndex;
|
||||
if (ratioIndex == entryIndex && entryIndex < seriesEnd) {
|
||||
ratioIndex = entryIndex + 1;
|
||||
}
|
||||
|
||||
if (equityCurveMode == EquityCurveMode.MARK_TO_MARKET) {
|
||||
Num averageHoldingCostPerPeriod = averageHoldingCostPerPeriod(position, endIndex, numFactory);
|
||||
boolean windowStartSeeded = false;
|
||||
if (entryIndex < windowStartIndex) {
|
||||
Num windowStartPrice = windowStartIndex == endIndex ? resolveExitPrice(position, endIndex, barSeries)
|
||||
: barSeries.getBar(windowStartIndex).getClosePrice();
|
||||
Num windowStartNetPrice = addCost(windowStartPrice, averageHoldingCostPerPeriod, isLongTrade);
|
||||
Num windowStartRatio = getIntermediateRatio(isLongTrade, netEntryPrice, windowStartNetPrice);
|
||||
multiplyValue(windowStartIndex, windowStartRatio);
|
||||
windowStartSeeded = true;
|
||||
}
|
||||
int start = Math.max(Math.max(entryIndex + 1, seriesBegin + 1), windowStartIndex + 1);
|
||||
for (int barIndex = start; barIndex < endIndex && barIndex <= windowEndIndex; barIndex++) {
|
||||
Num closePrice = barSeries.getBar(barIndex).getClosePrice();
|
||||
Num intermediateNetPrice = addCost(closePrice, averageHoldingCostPerPeriod, isLongTrade);
|
||||
Num ratio = getIntermediateRatio(isLongTrade, netEntryPrice, intermediateNetPrice);
|
||||
multiplyValue(barIndex, ratio);
|
||||
}
|
||||
Num exitPrice = resolveExitPrice(position, endIndex, barSeries);
|
||||
Num netExitPrice = addCost(exitPrice, averageHoldingCostPerPeriod, isLongTrade);
|
||||
Num ratio = getIntermediateRatio(isLongTrade, netEntryPrice, netExitPrice);
|
||||
if (ratioIndex <= windowEndIndex && !(windowStartSeeded && ratioIndex == windowStartIndex)) {
|
||||
multiplyValue(ratioIndex, ratio);
|
||||
}
|
||||
multiplyRange(ratioIndex + 1, windowEndIndex, ratio);
|
||||
return;
|
||||
}
|
||||
|
||||
Trade exit = position.getExit();
|
||||
if (exit != null && endIndex >= exit.getIndex()) {
|
||||
Num holdingCost = position.getHoldingCost(endIndex);
|
||||
Num netExitPrice = addCost(exit.getNetPrice(), holdingCost, isLongTrade);
|
||||
Num ratio = getIntermediateRatio(isLongTrade, netEntryPrice, netExitPrice);
|
||||
multiplyRange(Math.max(ratioIndex, windowStartIndex), windowEndIndex, ratio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @return the cash flow value at the index-th position
|
||||
*/
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return getStoredValue(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return barSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the bar series
|
||||
*/
|
||||
public int getSize() {
|
||||
return barSeries.getBarCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the equity curve mode used for this cash flow
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public EquityCurveMode getEquityCurveMode() {
|
||||
return equityCurveMode;
|
||||
}
|
||||
|
||||
private void multiplyValue(int index, Num ratio) {
|
||||
if (!containsIndex(index)) {
|
||||
return;
|
||||
}
|
||||
int valueIndex = toValueIndex(index);
|
||||
values.set(valueIndex, values.get(valueIndex).multipliedBy(ratio));
|
||||
}
|
||||
|
||||
private void multiplyRange(int startIndex, int endIndex, Num ratio) {
|
||||
if (values.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int start = Math.max(valueStartIndex, startIndex);
|
||||
int end = Math.min(endIndex, valueEndIndex);
|
||||
if (start > end) {
|
||||
return;
|
||||
}
|
||||
for (int i = start; i <= end; i++) {
|
||||
int valueIndex = toValueIndex(i);
|
||||
values.set(valueIndex, values.get(valueIndex).multipliedBy(ratio));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsIndex(int index) {
|
||||
return index >= valueStartIndex && index <= valueEndIndex;
|
||||
}
|
||||
|
||||
private Num getStoredValue(int index) {
|
||||
return values.get(toValueIndex(index));
|
||||
}
|
||||
|
||||
private int toValueIndex(int index) {
|
||||
return index - valueStartIndex;
|
||||
}
|
||||
|
||||
private static Num getIntermediateRatio(boolean isLongTrade, Num entryPrice, Num exitPrice) {
|
||||
if (isLongTrade) {
|
||||
return exitPrice.dividedBy(entryPrice);
|
||||
}
|
||||
return entryPrice.getNumFactory().numOf(2).minus(exitPrice.dividedBy(entryPrice));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.*;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A {@link PerformanceIndicator} implementation that computes the cumulative
|
||||
* profit and loss (PnL) series of one or more trading positions over a given
|
||||
* {@link BarSeries}.
|
||||
* <p>
|
||||
* The cumulative PnL is calculated incrementally from the start of the
|
||||
* {@code BarSeries}, taking into account realized and unrealized gains/losses,
|
||||
* trading costs, and position direction (long or short). Each index in the
|
||||
* series represents the total PnL up to that bar. The calculation mode can be
|
||||
* configured to mark open positions to market or to only realize PnL at exits.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public final class CumulativePnL implements PerformanceIndicator {
|
||||
|
||||
private final BarSeries barSeries;
|
||||
private final List<Num> values;
|
||||
private final EquityCurveMode equityCurveMode;
|
||||
|
||||
/**
|
||||
* Constructor for a trading record with a specified final index.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex the final index to calculate up to
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex,
|
||||
EquityCurveMode equityCurveMode, OpenPositionHandling openPositionHandling) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
this.equityCurveMode = Objects.requireNonNull(equityCurveMode);
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
int size = Math.max(seriesEnd + 1, 0);
|
||||
this.values = new ArrayList<>(Collections.nCopies(size, barSeries.numFactory().zero()));
|
||||
calculate(Objects.requireNonNull(tradingRecord), finalIndex, Objects.requireNonNull(openPositionHandling));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a single closed position.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position the closed position
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, Position position, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, new BaseTradingRecord(position), barSeries.getEndIndex(), equityCurveMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record with a specified final index.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex the final index to calculate up to
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex,
|
||||
EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, finalIndex, equityCurveMode, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a single closed position.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position the closed position
|
||||
* @since 0.19
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, Position position) {
|
||||
this(barSeries, position, EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @since 0.19
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), EquityCurveMode.MARK_TO_MARKET,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), equityCurveMode,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, EquityCurveMode equityCurveMode,
|
||||
OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), equityCurveMode, openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex the final index to calculate up to
|
||||
* @since 0.19
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex) {
|
||||
this(barSeries, tradingRecord, finalIndex, EquityCurveMode.MARK_TO_MARKET, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a trading record.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public CumulativePnL(BarSeries barSeries, TradingRecord tradingRecord, OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), EquityCurveMode.MARK_TO_MARKET,
|
||||
openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the cumulative PnL for a single position.
|
||||
*
|
||||
* @param position the position
|
||||
* @param finalIndex the final index to calculate up to
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void calculatePosition(Position position, int finalIndex) {
|
||||
Trade entry = position.getEntry();
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
int entryIndex = entry.getIndex();
|
||||
if (entryIndex > finalIndex || entryIndex > seriesEnd) {
|
||||
return;
|
||||
}
|
||||
int endIndex = determineEndIndex(position, finalIndex, seriesEnd);
|
||||
int seriesBegin = barSeries.getBeginIndex();
|
||||
if (endIndex < seriesBegin) {
|
||||
return;
|
||||
}
|
||||
|
||||
NumFactory numFactory = barSeries.numFactory();
|
||||
boolean isLong = entry.isBuy();
|
||||
Num netEntryPrice = entry.getNetPrice();
|
||||
|
||||
if (equityCurveMode == EquityCurveMode.MARK_TO_MARKET) {
|
||||
Num averageCostPerPeriod = averageHoldingCostPerPeriod(position, endIndex, numFactory);
|
||||
int start = Math.max(entryIndex + 1, seriesBegin + 1);
|
||||
for (int i = start; i < endIndex; i++) {
|
||||
Num close = barSeries.getBar(i).getClosePrice();
|
||||
Num netIntermediate = addCost(close, averageCostPerPeriod, isLong);
|
||||
Num delta = isLong ? netIntermediate.minus(netEntryPrice) : netEntryPrice.minus(netIntermediate);
|
||||
addValue(i, delta);
|
||||
}
|
||||
Num exitRaw = resolveExitPrice(position, endIndex, barSeries);
|
||||
Num netExit = addCost(exitRaw, averageCostPerPeriod, isLong);
|
||||
Num deltaExit = isLong ? netExit.minus(netEntryPrice) : netEntryPrice.minus(netExit);
|
||||
addToRange(endIndex, seriesEnd, deltaExit);
|
||||
return;
|
||||
}
|
||||
|
||||
Trade exit = position.getExit();
|
||||
if (exit != null && endIndex >= exit.getIndex()) {
|
||||
Num holdingCost = position.getHoldingCost(endIndex);
|
||||
Num netExit = addCost(exit.getNetPrice(), holdingCost, isLong);
|
||||
Num deltaExit = isLong ? netExit.minus(netEntryPrice) : netEntryPrice.minus(netExit);
|
||||
addToRange(exit.getIndex(), seriesEnd, deltaExit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return values.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return barSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bars in the underlying series.
|
||||
*
|
||||
* @return the bar count
|
||||
* @since 0.19
|
||||
*/
|
||||
public int getSize() {
|
||||
return barSeries.getBarCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the equity curve mode used for this cumulative PnL
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public EquityCurveMode getEquityCurveMode() {
|
||||
return equityCurveMode;
|
||||
}
|
||||
|
||||
private void addValue(int index, Num delta) {
|
||||
if (index < 0 || index >= values.size()) {
|
||||
return;
|
||||
}
|
||||
values.set(index, values.get(index).plus(delta));
|
||||
}
|
||||
|
||||
private void addToRange(int startIndex, int endIndex, Num delta) {
|
||||
if (values.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int start = Math.max(0, startIndex);
|
||||
int end = Math.min(endIndex, values.size() - 1);
|
||||
if (start > end) {
|
||||
return;
|
||||
}
|
||||
for (int i = start; i <= end; i++) {
|
||||
values.set(i, values.get(i).plus(delta));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
/**
|
||||
* Defines how equity curves are computed.
|
||||
* <p>
|
||||
* {@link #MARK_TO_MARKET} reflects unrealized profit and loss on each bar using
|
||||
* intermediate prices, while {@link #REALIZED} updates the curve only when a
|
||||
* position is closed, keeping interim bars flat.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum EquityCurveMode {
|
||||
|
||||
/**
|
||||
* Updates the equity curve on every bar, including unrealized gains/losses.
|
||||
*/
|
||||
MARK_TO_MARKET,
|
||||
|
||||
/**
|
||||
* Updates the equity curve only when positions close, reflecting realized
|
||||
* gains/losses.
|
||||
*/
|
||||
REALIZED
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.utils.BarSeriesUtils;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Computes compounded excess returns between sampled index pairs.
|
||||
*
|
||||
* <p>
|
||||
* For each sampled pair, the excess return is formed by compounding the per-bar
|
||||
* excess growth factors between the indices. This ensures mixed
|
||||
* in/out-of-market segments within the sampling window contribute
|
||||
* proportionally.
|
||||
*
|
||||
* <p>
|
||||
* The {@link CashReturnPolicy} defines how flat equity intervals are treated
|
||||
* relative to the risk-free benchmark, allowing flat segments to be neutral or
|
||||
* to incur underperformance against cash.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public final class ExcessReturns {
|
||||
|
||||
/**
|
||||
* Describes how flat equity intervals are treated when computing excess
|
||||
* returns.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum CashReturnPolicy {
|
||||
/**
|
||||
* Treats flat equity while out of the market as earning the risk-free rate, so
|
||||
* those intervals do not contribute to excess return underperformance.
|
||||
*/
|
||||
CASH_EARNS_RISK_FREE,
|
||||
/**
|
||||
* Treats flat equity while out of the market as earning zero return, so those
|
||||
* intervals underperform the risk-free benchmark.
|
||||
*/
|
||||
CASH_EARNS_ZERO
|
||||
}
|
||||
|
||||
private final Num annualRiskFreeRate;
|
||||
private final CashReturnPolicy cashReturnPolicy;
|
||||
private final BarSeries series;
|
||||
private final InvestedInterval investedInterval;
|
||||
private final CashFlow cashFlow;
|
||||
|
||||
/**
|
||||
* Creates an excess return calculator with invested interval detection from a
|
||||
* trading record.
|
||||
*
|
||||
* @param series the bar series providing time deltas and num
|
||||
* factory
|
||||
* @param annualRiskFreeRate the annual risk-free rate (e.g. 0.05 for 5%)
|
||||
* @param cashReturnPolicy the policy for flat equity intervals
|
||||
* @param tradingRecord the trading record used to detect invested
|
||||
* intervals
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ExcessReturns(BarSeries series, Num annualRiskFreeRate, CashReturnPolicy cashReturnPolicy,
|
||||
TradingRecord tradingRecord) {
|
||||
this(series, annualRiskFreeRate, cashReturnPolicy, tradingRecord, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an excess return calculator with invested interval detection from a
|
||||
* trading record.
|
||||
*
|
||||
* @param series the bar series providing time deltas and num
|
||||
* factory
|
||||
* @param annualRiskFreeRate the annual risk-free rate (e.g. 0.05 for 5%)
|
||||
* @param cashReturnPolicy the policy for flat equity intervals
|
||||
* @param tradingRecord the trading record used to detect invested
|
||||
* intervals
|
||||
* @param openPositionHandling how open positions should be handled
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ExcessReturns(BarSeries series, Num annualRiskFreeRate, CashReturnPolicy cashReturnPolicy,
|
||||
TradingRecord tradingRecord, OpenPositionHandling openPositionHandling) {
|
||||
this(series, annualRiskFreeRate, cashReturnPolicy, tradingRecord, EquityCurveMode.MARK_TO_MARKET,
|
||||
openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an excess return calculator with invested interval detection from a
|
||||
* trading record.
|
||||
*
|
||||
* @param series the bar series providing time deltas and num
|
||||
* factory
|
||||
* @param annualRiskFreeRate the annual risk-free rate (e.g. 0.05 for 5%)
|
||||
* @param cashReturnPolicy the policy for flat equity intervals
|
||||
* @param tradingRecord the trading record used to detect invested
|
||||
* intervals
|
||||
* @param equityCurveMode the cash flow calculation mode
|
||||
* @param openPositionHandling how open positions should be handled
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public ExcessReturns(BarSeries series, Num annualRiskFreeRate, CashReturnPolicy cashReturnPolicy,
|
||||
TradingRecord tradingRecord, EquityCurveMode equityCurveMode, OpenPositionHandling openPositionHandling) {
|
||||
this.series = Objects.requireNonNull(series, "series cannot be null");
|
||||
this.annualRiskFreeRate = Objects.requireNonNull(annualRiskFreeRate, "annualRiskFreeRate cannot be null");
|
||||
this.cashReturnPolicy = Objects.requireNonNull(cashReturnPolicy, "cashReturnPolicy cannot be null");
|
||||
|
||||
Objects.requireNonNull(tradingRecord, "tradingRecord cannot be null");
|
||||
Objects.requireNonNull(equityCurveMode, "equityCurveMode cannot be null");
|
||||
Objects.requireNonNull(openPositionHandling, "openPositionHandling cannot be null");
|
||||
|
||||
OpenPositionHandling effectiveOpenPositionHandling = equityCurveMode == EquityCurveMode.REALIZED
|
||||
? OpenPositionHandling.IGNORE
|
||||
: openPositionHandling;
|
||||
this.investedInterval = new InvestedInterval(series, tradingRecord, effectiveOpenPositionHandling);
|
||||
this.cashFlow = new CashFlow(series, tradingRecord, equityCurveMode, effectiveOpenPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the compounded excess return using the configured cash flow.
|
||||
*
|
||||
* @param previousIndex the start index
|
||||
* @param currentIndex the end index
|
||||
* @return the compounded excess return
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Num excessReturn(int previousIndex, int currentIndex) {
|
||||
NumFactory numFactory = series.numFactory();
|
||||
Num zero = numFactory.zero();
|
||||
Num one = numFactory.one();
|
||||
if (currentIndex <= previousIndex) {
|
||||
return zero;
|
||||
}
|
||||
|
||||
Num excessGrowth = one;
|
||||
for (int i = previousIndex + 1; i <= currentIndex; i++) {
|
||||
Num previousEquity = cashFlow.getValue(i - 1);
|
||||
Num currentEquity = cashFlow.getValue(i);
|
||||
Num riskFreeGrowth = riskFreeGrowth(i - 1, i, one);
|
||||
boolean isFlat = currentEquity.isEqual(previousEquity);
|
||||
boolean isInvested = isInvested(i);
|
||||
if (cashReturnPolicy == CashReturnPolicy.CASH_EARNS_RISK_FREE && isFlat && !isInvested) {
|
||||
continue;
|
||||
}
|
||||
if (previousEquity.isZero()) {
|
||||
if (!currentEquity.isZero()) {
|
||||
excessGrowth = zero;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (riskFreeGrowth.isZero()) {
|
||||
excessGrowth = excessGrowth.multipliedBy(currentEquity.dividedBy(previousEquity));
|
||||
continue;
|
||||
}
|
||||
|
||||
Num growth = currentEquity.dividedBy(previousEquity).dividedBy(riskFreeGrowth);
|
||||
excessGrowth = excessGrowth.multipliedBy(growth);
|
||||
}
|
||||
|
||||
return excessGrowth.minus(one);
|
||||
}
|
||||
|
||||
private Num riskFreeGrowth(int previousIndex, int currentIndex, Num one) {
|
||||
NumFactory numFactory = series.numFactory();
|
||||
Num zero = numFactory.zero();
|
||||
Num deltaYears = BarSeriesUtils.deltaYears(series, previousIndex, currentIndex);
|
||||
if (deltaYears.isLessThanOrEqual(zero)) {
|
||||
return one;
|
||||
}
|
||||
return one.plus(annualRiskFreeRate).pow(deltaYears);
|
||||
}
|
||||
|
||||
private boolean isInvested(int index) {
|
||||
return investedInterval.getValue(index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.indicators.CachedIndicator;
|
||||
|
||||
/**
|
||||
* Indicates whether each bar interval is part of an invested position.
|
||||
*
|
||||
* <p>
|
||||
* The indicator marks index {@code i} as invested when the interval between
|
||||
* {@code i - 1} and {@code i} belongs to a position in the provided trading
|
||||
* record.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public class InvestedInterval extends CachedIndicator<Boolean> {
|
||||
|
||||
private final boolean[] investedIntervals;
|
||||
|
||||
/**
|
||||
* Creates an indicator that reports invested intervals for the trading record.
|
||||
*
|
||||
* @param series the bar series backing the indicator
|
||||
* @param tradingRecord the trading record used to detect invested intervals
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public InvestedInterval(BarSeries series, TradingRecord tradingRecord) {
|
||||
this(series, tradingRecord, OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indicator that reports invested intervals for the trading record.
|
||||
*
|
||||
* @param series the bar series backing the indicator
|
||||
* @param tradingRecord the trading record used to detect invested
|
||||
* intervals
|
||||
* @param openPositionHandling how open positions should be handled
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public InvestedInterval(BarSeries series, TradingRecord tradingRecord, OpenPositionHandling openPositionHandling) {
|
||||
super(series);
|
||||
Objects.requireNonNull(series, "series cannot be null");
|
||||
Objects.requireNonNull(tradingRecord, "tradingRecord cannot be null");
|
||||
Objects.requireNonNull(openPositionHandling, "openPositionHandling cannot be null");
|
||||
investedIntervals = buildInvestedIntervals(tradingRecord, openPositionHandling);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean calculate(int index) {
|
||||
if (index < 0 || index >= investedIntervals.length) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return investedIntervals[index];
|
||||
}
|
||||
|
||||
private boolean[] buildInvestedIntervals(TradingRecord tradingRecord, OpenPositionHandling openPositionHandling) {
|
||||
BarSeries series = getBarSeries();
|
||||
int size = Math.max(series.getEndIndex() + 1, 0);
|
||||
boolean[] invested = new boolean[size];
|
||||
tradingRecord.getPositions().forEach(position -> markInvestedIntervals(position, invested));
|
||||
if (openPositionHandling == OpenPositionHandling.MARK_TO_MARKET) {
|
||||
List<Position> openPositions = AnalysisPositionSupport.openPositions(tradingRecord, series.getEndIndex());
|
||||
openPositions.forEach(position -> markInvestedIntervals(position, invested));
|
||||
}
|
||||
return invested;
|
||||
}
|
||||
|
||||
private void markInvestedIntervals(Position position, boolean[] invested) {
|
||||
BarSeries series = getBarSeries();
|
||||
if (position == null || position.getEntry() == null) {
|
||||
return;
|
||||
}
|
||||
int entryIndex = position.getEntry().getIndex();
|
||||
int exitIndex = position.isClosed() ? position.getExit().getIndex() : series.getEndIndex();
|
||||
int start = Math.max(entryIndex + 1, series.getBeginIndex() + 1);
|
||||
int end = Math.min(exitIndex, invested.length - 1);
|
||||
for (int i = start; i <= end; i++) {
|
||||
invested[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Shared contract for named scoring functions.
|
||||
*
|
||||
* <p>
|
||||
* Implementations expose a stable display name and compute a score-like output
|
||||
* for an input payload.
|
||||
* </p>
|
||||
*
|
||||
* @param <I> input payload type
|
||||
* @param <S> score/output type
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public interface NamedScoreFunction<I, S> extends Function<I, S> {
|
||||
|
||||
/**
|
||||
* @return human-readable function name
|
||||
* @since 0.22.4
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Computes output for the provided input payload.
|
||||
*
|
||||
* @param input input payload
|
||||
* @return computed output
|
||||
* @since 0.22.4
|
||||
*/
|
||||
S score(I input);
|
||||
|
||||
/**
|
||||
* Function-style alias for {@link #score(Object)}.
|
||||
*
|
||||
* @param input input payload
|
||||
* @return computed output
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Override
|
||||
default S apply(I input) {
|
||||
return score(input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
/**
|
||||
* Controls how open positions are treated in analysis calculations.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum OpenPositionHandling {
|
||||
|
||||
/**
|
||||
* Include open positions and mark them to market at the final index.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
MARK_TO_MARKET,
|
||||
|
||||
/**
|
||||
* Ignore open positions.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
IGNORE
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Shared contract for performance indicators derived from trading records.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public interface PerformanceIndicator extends Indicator<Num> {
|
||||
|
||||
/**
|
||||
* Returns the equity curve mode that influences open position handling.
|
||||
*
|
||||
* @return the equity curve mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
EquityCurveMode getEquityCurveMode();
|
||||
|
||||
/**
|
||||
* Calculates indicator values for a single position.
|
||||
*
|
||||
* @param position the position
|
||||
* @param finalIndex index up until values of open positions are considered
|
||||
* @since 0.22.2
|
||||
*/
|
||||
void calculatePosition(Position position, int finalIndex);
|
||||
|
||||
/**
|
||||
* Calculates indicator values based on the provided trading record.
|
||||
*
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex index up until values of open positions are
|
||||
* considered
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default void calculate(TradingRecord tradingRecord, int finalIndex, OpenPositionHandling openPositionHandling) {
|
||||
Objects.requireNonNull(tradingRecord);
|
||||
Objects.requireNonNull(openPositionHandling);
|
||||
OpenPositionHandling effectiveOpenPositionHandling = getEffectiveOpenPositionHandling(openPositionHandling);
|
||||
List<Position> positions = AnalysisPositionSupport.positionsForAnalysis(tradingRecord, finalIndex,
|
||||
effectiveOpenPositionHandling, getEquityCurveMode());
|
||||
positions.forEach(position -> calculatePosition(position, finalIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the open-position handling from the equity curve mode to keep
|
||||
* realized-only curves from leaking unrealized P&L into the calculation.
|
||||
*
|
||||
* <p>
|
||||
* When the equity curve is realized-only, we force
|
||||
* {@link OpenPositionHandling#IGNORE} regardless of the caller preference. For
|
||||
* all other modes we defer to the requested handling, so callers can opt into
|
||||
* mark-to-market behavior.
|
||||
* </p>
|
||||
*
|
||||
* @param openPositionHandling the requested handling for open positions
|
||||
* @return the effective handling aligned with the equity curve mode
|
||||
*/
|
||||
private OpenPositionHandling getEffectiveOpenPositionHandling(OpenPositionHandling openPositionHandling) {
|
||||
return getEquityCurveMode() == EquityCurveMode.REALIZED ? OpenPositionHandling.IGNORE : openPositionHandling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the valid final index to be considered.
|
||||
*
|
||||
* @param position the position
|
||||
* @param finalIndex index up until cash flows of open positions are considered
|
||||
* @param maxIndex maximal valid index
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default int determineEndIndex(Position position, int finalIndex, int maxIndex) {
|
||||
int idx = finalIndex;
|
||||
// After closing of the position, no further accrual necessary
|
||||
if (position.getExit() != null) {
|
||||
idx = Math.min(position.getExit().getIndex(), finalIndex);
|
||||
}
|
||||
// Accrual at most until maximal index of asset data
|
||||
if (idx > maxIndex) {
|
||||
idx = maxIndex;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts (intermediate) price to incorporate trading costs.
|
||||
*
|
||||
* @param rawPrice the gross asset price
|
||||
* @param holdingCost share of the holding cost per period
|
||||
* @param isLongTrade true, if the entry trade type is BUY
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default Num addCost(Num rawPrice, Num holdingCost, boolean isLongTrade) {
|
||||
if (isLongTrade) {
|
||||
return rawPrice.minus(holdingCost);
|
||||
} else {
|
||||
return rawPrice.plus(holdingCost);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the average holding cost per period for the given position.
|
||||
*
|
||||
* @param position the position
|
||||
* @param endIndex index up until cash flows of open positions are considered
|
||||
* @param numFactory the {@link Num} factory
|
||||
* @return the average holding cost per period, or zero when no periods elapsed
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default Num averageHoldingCostPerPeriod(Position position, int endIndex, NumFactory numFactory) {
|
||||
int periods = Math.max(0, endIndex - position.getEntry().getIndex());
|
||||
if (periods == 0) {
|
||||
return numFactory.zero();
|
||||
}
|
||||
Num holdingCost = position.getHoldingCost(endIndex);
|
||||
return holdingCost.dividedBy(numFactory.numOf(periods));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the exit price for a position at the given end index.
|
||||
*
|
||||
* @param position the position
|
||||
* @param endIndex index up until values of open positions are considered
|
||||
* @param series the bar series
|
||||
* @return the exit price if an exit exists within the end index, otherwise the
|
||||
* bar close
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default Num resolveExitPrice(Position position, int endIndex, BarSeries series) {
|
||||
Trade exit = position.getExit();
|
||||
if (exit != null && exit.getIndex() <= endIndex) {
|
||||
return exit.getNetPrice();
|
||||
}
|
||||
return series.getBar(endIndex).getClosePrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads a list up to and including the specified end index using the provided
|
||||
* pad value.
|
||||
*
|
||||
* @param values the list to pad
|
||||
* @param endIndex the last required index
|
||||
* @param padValue the value to append while padding
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default void padToEndIndex(List<Num> values, int endIndex, Num padValue) {
|
||||
if (endIndex >= values.size()) {
|
||||
padToSize(values, endIndex + 1, padValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads a list up to the required size using the provided pad value.
|
||||
*
|
||||
* @param values the list to pad
|
||||
* @param requiredSize the required list size
|
||||
* @param padValue the value to append while padding
|
||||
* @since 0.22.2
|
||||
*/
|
||||
default void padToSize(List<Num> values, int requiredSize, Num padValue) {
|
||||
if (requiredSize > values.size()) {
|
||||
values.addAll(Collections.nCopies(requiredSize - values.size(), padValue));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.Bar;
|
||||
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.criteria.ReturnRepresentationPolicy;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Allows to compute the return rate of a price time-series.
|
||||
* <p>
|
||||
* Returns are calculated and formatted according to the specified
|
||||
* {@link ReturnRepresentation}. Use {@link ReturnRepresentation#LOG} for log
|
||||
* returns, or {@link ReturnRepresentation#DECIMAL},
|
||||
* {@link ReturnRepresentation#MULTIPLICATIVE}, or
|
||||
* {@link ReturnRepresentation#PERCENTAGE} for arithmetic returns in different
|
||||
* formats.
|
||||
* <p>
|
||||
* The default representation (when not explicitly specified) is obtained from
|
||||
* {@link ReturnRepresentationPolicy#getDefaultRepresentation()}.
|
||||
*
|
||||
* @see ReturnRepresentation
|
||||
* @see ReturnRepresentationPolicy
|
||||
*/
|
||||
public class Returns implements PerformanceIndicator {
|
||||
|
||||
private final ReturnRepresentation representation;
|
||||
private final EquityCurveMode equityCurveMode;
|
||||
|
||||
/** The bar series. */
|
||||
private final BarSeries barSeries;
|
||||
|
||||
/**
|
||||
* The raw return rates (before formatting).
|
||||
* <p>
|
||||
* Stores log returns if {@code representation == LOG}, otherwise stores
|
||||
* arithmetic returns in DECIMAL format (0-based, e.g., 0.12 for +12%). Used by
|
||||
* {@link #getRawValues()} for statistical calculations.
|
||||
*/
|
||||
private final List<Num> rawValues;
|
||||
|
||||
/**
|
||||
* The formatted return rates (according to the configured representation).
|
||||
* <p>
|
||||
* Values are formatted during calculation using
|
||||
* {@link ReturnRepresentation#toRepresentationFromRateOfReturn(Num)} for
|
||||
* arithmetic returns, or returned as-is for log returns.
|
||||
*/
|
||||
private final List<Num> values;
|
||||
|
||||
private final List<Num> returnFactors;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param finalIndex the index up to which the returns of open
|
||||
* positions are considered
|
||||
* @param representation the return representation (determines both
|
||||
* calculation method and output format)
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, int finalIndex,
|
||||
ReturnRepresentation representation, EquityCurveMode equityCurveMode,
|
||||
OpenPositionHandling openPositionHandling) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
this.representation = Objects.requireNonNull(representation);
|
||||
this.equityCurveMode = Objects.requireNonNull(equityCurveMode);
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
int size = Math.max(seriesEnd + 1, 0);
|
||||
Num one = barSeries.numFactory().one();
|
||||
Num zero = barSeries.numFactory().zero();
|
||||
Num initial = representation == ReturnRepresentation.LOG ? zero : one;
|
||||
returnFactors = new ArrayList<>(Collections.nCopies(size, initial));
|
||||
rawValues = new ArrayList<>(Collections.nCopies(size, zero));
|
||||
values = new ArrayList<>(Collections.nCopies(size, zero));
|
||||
calculate(Objects.requireNonNull(tradingRecord), finalIndex, Objects.requireNonNull(openPositionHandling));
|
||||
buildReturns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default representation from
|
||||
* {@link ReturnRepresentationPolicy#getDefaultRepresentation()}.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
*/
|
||||
public Returns(BarSeries barSeries, Position position) {
|
||||
this(barSeries, position, ReturnRepresentationPolicy.getDefaultRepresentation(),
|
||||
EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default representation from
|
||||
* {@link ReturnRepresentationPolicy#getDefaultRepresentation()}.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, Position position, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, position, ReturnRepresentationPolicy.getDefaultRepresentation(), equityCurveMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
* @param representation the return representation (determines both calculation
|
||||
* method and output format)
|
||||
*/
|
||||
public Returns(BarSeries barSeries, Position position, ReturnRepresentation representation) {
|
||||
this(barSeries, position, representation, EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param position a single position
|
||||
* @param representation the return representation (determines both calculation
|
||||
* method and output format)
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, Position position, ReturnRepresentation representation,
|
||||
EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, new BaseTradingRecord(position), representation, equityCurveMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param representation the return representation (determines both calculation
|
||||
* method and output format)
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, ReturnRepresentation representation,
|
||||
EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), representation, equityCurveMode,
|
||||
OpenPositionHandling.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default representation from
|
||||
* {@link ReturnRepresentationPolicy#getDefaultRepresentation()}.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord) {
|
||||
this(barSeries, tradingRecord, ReturnRepresentationPolicy.getDefaultRepresentation(),
|
||||
EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default representation from
|
||||
* {@link ReturnRepresentationPolicy#getDefaultRepresentation()}.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, EquityCurveMode equityCurveMode) {
|
||||
this(barSeries, tradingRecord, ReturnRepresentationPolicy.getDefaultRepresentation(), equityCurveMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param representation the return representation (determines both calculation
|
||||
* method and output format)
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, ReturnRepresentation representation) {
|
||||
this(barSeries, tradingRecord, representation, EquityCurveMode.MARK_TO_MARKET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param representation the return representation (determines both
|
||||
* calculation method and output format)
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, ReturnRepresentation representation,
|
||||
OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), representation,
|
||||
EquityCurveMode.MARK_TO_MARKET, openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series
|
||||
* @param tradingRecord the trading record
|
||||
* @param representation the return representation (determines both
|
||||
* calculation method and output format)
|
||||
* @param equityCurveMode the calculation mode
|
||||
* @param openPositionHandling how to handle open positions
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Returns(BarSeries barSeries, TradingRecord tradingRecord, ReturnRepresentation representation,
|
||||
EquityCurveMode equityCurveMode, OpenPositionHandling openPositionHandling) {
|
||||
this(barSeries, tradingRecord, tradingRecord.getEndIndex(barSeries), representation, equityCurveMode,
|
||||
openPositionHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the return rates (formatted according to the configured
|
||||
* representation)
|
||||
*/
|
||||
public List<Num> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index the bar index
|
||||
* @return the return rate value at the index-th position (formatted according
|
||||
* to the configured representation)
|
||||
*/
|
||||
@Override
|
||||
public Num getValue(int index) {
|
||||
return values.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the raw return rates (before formatting)
|
||||
*/
|
||||
public List<Num> getRawValues() {
|
||||
return rawValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarSeries getBarSeries() {
|
||||
return barSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the return series.
|
||||
*/
|
||||
public int getSize() {
|
||||
return barSeries.getBarCount() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the returns for a single position.
|
||||
*
|
||||
* @param position a single position
|
||||
* @param finalIndex the index up to which the returns of open positions are
|
||||
* considered
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void calculatePosition(Position position, int finalIndex) {
|
||||
Trade entry = position.getEntry();
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
int entryIndex = entry.getIndex();
|
||||
int seriesEnd = barSeries.getEndIndex();
|
||||
if (entryIndex > finalIndex || entryIndex > seriesEnd) {
|
||||
return;
|
||||
}
|
||||
int endIndex = determineEndIndex(position, finalIndex, seriesEnd);
|
||||
int seriesBegin = barSeries.getBeginIndex();
|
||||
if (endIndex < seriesBegin) {
|
||||
return;
|
||||
}
|
||||
|
||||
NumFactory numFactory = barSeries.numFactory();
|
||||
Num minusOne = numFactory.minusOne();
|
||||
boolean isLongTrade = entry.isBuy();
|
||||
int start = Math.max(entryIndex + 1, seriesBegin + 1);
|
||||
|
||||
if (equityCurveMode == EquityCurveMode.MARK_TO_MARKET) {
|
||||
Num avgCost = averageHoldingCostPerPeriod(position, endIndex, numFactory);
|
||||
Num lastPrice = entry.getNetPrice();
|
||||
for (int i = start; i < endIndex; i++) {
|
||||
Bar bar = barSeries.getBar(i);
|
||||
Num intermediateNetPrice = addCost(bar.getClosePrice(), avgCost, isLongTrade);
|
||||
Num rawReturn = calculateReturn(intermediateNetPrice, lastPrice);
|
||||
Num strategyReturn = isLongTrade ? rawReturn : rawReturn.multipliedBy(minusOne);
|
||||
combineReturnAtIndex(i, strategyReturn);
|
||||
lastPrice = intermediateNetPrice;
|
||||
}
|
||||
Num exitPrice = resolveExitPrice(position, endIndex, barSeries);
|
||||
Num rawReturn = calculateReturn(addCost(exitPrice, avgCost, isLongTrade), lastPrice);
|
||||
Num strategyReturn = isLongTrade ? rawReturn : rawReturn.multipliedBy(minusOne);
|
||||
combineReturnAtIndex(endIndex, strategyReturn);
|
||||
return;
|
||||
}
|
||||
|
||||
Trade exit = position.getExit();
|
||||
if (exit != null && endIndex >= exit.getIndex()) {
|
||||
Num holdingCost = position.getHoldingCost(endIndex);
|
||||
Num netExit = addCost(exit.getNetPrice(), holdingCost, isLongTrade);
|
||||
Num rawReturn = calculateReturn(netExit, entry.getNetPrice());
|
||||
Num strategyReturn = isLongTrade ? rawReturn : rawReturn.multipliedBy(minusOne);
|
||||
combineReturnAtIndex(exit.getIndex(), strategyReturn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the equity curve mode used for this return series
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public EquityCurveMode getEquityCurveMode() {
|
||||
return equityCurveMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the raw return between two prices.
|
||||
*
|
||||
* @param xNew the new price
|
||||
* @param xOld the old price
|
||||
* @return the raw return (log return if representation is LOG, arithmetic
|
||||
* return otherwise)
|
||||
*/
|
||||
private Num calculateReturn(Num xNew, Num xOld) {
|
||||
if (representation == ReturnRepresentation.LOG) {
|
||||
// r_i = ln(P_i/P_(i-1))
|
||||
return (xNew.dividedBy(xOld)).log();
|
||||
}
|
||||
// r_i = P_i/P_(i-1) - 1 (arithmetic return, which is DECIMAL format)
|
||||
Num one = barSeries.numFactory().one();
|
||||
return xNew.dividedBy(xOld).minus(one);
|
||||
}
|
||||
|
||||
private Num toFactor(Num strategyReturn) {
|
||||
Num one = barSeries.numFactory().one();
|
||||
return strategyReturn.plus(one);
|
||||
}
|
||||
|
||||
private void combineReturnAtIndex(int index, Num strategyReturn) {
|
||||
if (index < 0 || index >= returnFactors.size()) {
|
||||
return;
|
||||
}
|
||||
if (representation == ReturnRepresentation.LOG) {
|
||||
returnFactors.set(index, returnFactors.get(index).plus(strategyReturn));
|
||||
} else {
|
||||
returnFactors.set(index, returnFactors.get(index).multipliedBy(toFactor(strategyReturn)));
|
||||
}
|
||||
}
|
||||
|
||||
private void buildReturns() {
|
||||
if (rawValues.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
rawValues.set(0, NaN.NaN);
|
||||
values.set(0, NaN.NaN);
|
||||
Num one = barSeries.numFactory().one();
|
||||
for (int i = 1; i < rawValues.size(); i++) {
|
||||
if (representation == ReturnRepresentation.LOG) {
|
||||
Num logReturn = returnFactors.get(i);
|
||||
rawValues.set(i, logReturn);
|
||||
values.set(i, logReturn);
|
||||
} else {
|
||||
Num factor = returnFactors.get(i);
|
||||
Num rawReturn = factor.minus(one);
|
||||
rawValues.set(i, rawReturn);
|
||||
values.set(i, representation.toRepresentationFromRateOfReturn(rawReturn));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
/**
|
||||
* Selects a series window or transformed series for one-shot analysis.
|
||||
*
|
||||
* @param <C> selector context type (for example degree, timeframe, or offset)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SeriesSelector<C> {
|
||||
|
||||
/**
|
||||
* Selects the series to analyze for the supplied context.
|
||||
*
|
||||
* @param series root input series
|
||||
* @param context caller-provided selector context
|
||||
* @return selected series (may be a subseries or transformed series)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
BarSeries select(BarSeries series, C context);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* Value associated with a finite numeric weight.
|
||||
*
|
||||
* <p>
|
||||
* This reusable primitive centralizes common weighting operations used across
|
||||
* ranking, objective scoring, and confidence aggregation.
|
||||
* </p>
|
||||
*
|
||||
* @param <T> value type
|
||||
* @param value weighted value
|
||||
* @param weight finite weight
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record WeightedValue<T>(T value, Num weight) {
|
||||
|
||||
/**
|
||||
* Creates a validated weighted value.
|
||||
*
|
||||
* @param value weighted value
|
||||
* @param weight finite weight
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public WeightedValue {
|
||||
Objects.requireNonNull(value, "value");
|
||||
validateWeight(weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes weights so their sum is exactly {@code 1}.
|
||||
*
|
||||
* @param weightedValues weighted values
|
||||
* @param numFactory target numeric factory
|
||||
* @param <T> value type
|
||||
* @return normalized weighted values preserving order
|
||||
* @throws IllegalArgumentException if list is empty or total weight is zero
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static <T> List<WeightedValue<T>> normalizeWeights(List<WeightedValue<T>> weightedValues,
|
||||
NumFactory numFactory) {
|
||||
Objects.requireNonNull(weightedValues, "weightedValues");
|
||||
Objects.requireNonNull(numFactory, "numFactory");
|
||||
if (weightedValues.isEmpty()) {
|
||||
throw new IllegalArgumentException("weightedValues must not be empty");
|
||||
}
|
||||
|
||||
List<WeightedValue<T>> normalizedInput = new ArrayList<>(weightedValues.size());
|
||||
Num totalWeight = numFactory.zero();
|
||||
for (WeightedValue<T> weightedValue : weightedValues) {
|
||||
Objects.requireNonNull(weightedValue, "weightedValues must not contain null entries");
|
||||
Num normalizedWeight = normalize(weightedValue.weight(), numFactory);
|
||||
validateWeight(normalizedWeight);
|
||||
normalizedInput.add(new WeightedValue<>(weightedValue.value(), normalizedWeight));
|
||||
totalWeight = totalWeight.plus(normalizedWeight);
|
||||
}
|
||||
if (totalWeight.isZero()) {
|
||||
throw new IllegalArgumentException("sum of weights must be > 0");
|
||||
}
|
||||
|
||||
List<WeightedValue<T>> normalizedValues = new ArrayList<>(normalizedInput.size());
|
||||
for (WeightedValue<T> weightedValue : normalizedInput) {
|
||||
Num normalizedWeight = weightedValue.weight().dividedBy(totalWeight);
|
||||
normalizedValues.add(new WeightedValue<>(weightedValue.value(), normalizedWeight));
|
||||
}
|
||||
return List.copyOf(normalizedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes weighted sum for resolved values.
|
||||
*
|
||||
* <p>
|
||||
* Entries with missing or NaN resolved values are skipped.
|
||||
* </p>
|
||||
*
|
||||
* @param weightedValues weighted values
|
||||
* @param valueResolver resolves value to aggregate for each weighted entry
|
||||
* @param numFactory target numeric factory
|
||||
* @param <T> value type
|
||||
* @return weighted sum
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static <T> Num weightedSum(List<WeightedValue<T>> weightedValues, Function<T, Num> valueResolver,
|
||||
NumFactory numFactory) {
|
||||
Objects.requireNonNull(weightedValues, "weightedValues");
|
||||
Objects.requireNonNull(valueResolver, "valueResolver");
|
||||
Objects.requireNonNull(numFactory, "numFactory");
|
||||
|
||||
Num sum = numFactory.zero();
|
||||
for (WeightedValue<T> weightedValue : weightedValues) {
|
||||
Objects.requireNonNull(weightedValue, "weightedValues must not contain null entries");
|
||||
Num normalizedWeight = normalize(weightedValue.weight(), numFactory);
|
||||
validateWeight(normalizedWeight);
|
||||
Num resolvedValue = normalize(valueResolver.apply(weightedValue.value()), numFactory);
|
||||
if (Num.isNaNOrNull(resolvedValue)) {
|
||||
continue;
|
||||
}
|
||||
sum = sum.plus(normalizedWeight.multipliedBy(resolvedValue));
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private static void validateWeight(Num weight) {
|
||||
Objects.requireNonNull(weight, "weight");
|
||||
if (Num.isNaNOrNull(weight) || Double.isNaN(weight.doubleValue()) || Double.isInfinite(weight.doubleValue())) {
|
||||
throw new IllegalArgumentException("weight must be finite");
|
||||
}
|
||||
}
|
||||
|
||||
private static Num normalize(Num value, NumFactory numFactory) {
|
||||
if (Num.isNaNOrNull(value)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
if (numFactory.produces(value)) {
|
||||
return value;
|
||||
}
|
||||
return numFactory.numOf(value.doubleValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* With the {@code CostModel}, we can include trading costs that may be incurred
|
||||
* when opening or closing a position.
|
||||
*/
|
||||
public interface CostModel {
|
||||
|
||||
/**
|
||||
* @param position the position
|
||||
* @param finalIndex the index up to which open positions are considered
|
||||
* @return the trading cost of the single {@code position}
|
||||
*/
|
||||
Num calculate(Position position, int finalIndex);
|
||||
|
||||
/**
|
||||
* @param position the position
|
||||
* @return the trading cost of the single {@code position}
|
||||
*/
|
||||
Num calculate(Position position);
|
||||
|
||||
/**
|
||||
* @param price the trade price per asset
|
||||
* @param amount the trade amount (i.e. the number of traded assets)
|
||||
* @return the trading cost for the traded {@code amount}
|
||||
*/
|
||||
Num calculate(Num price, Num amount);
|
||||
|
||||
/**
|
||||
* Evaluates if two models are equal.
|
||||
*
|
||||
* @param otherModel
|
||||
* @return true if {@code this} and {@code otherModel} are equal
|
||||
*/
|
||||
boolean equals(CostModel otherModel);
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* With this cost model, the trading costs for opening or closing a position are
|
||||
* accrued through a constant fee per trade (i.e. a fixed fee per transaction).
|
||||
*/
|
||||
public class FixedTransactionCostModel implements CostModel {
|
||||
|
||||
/** The fixed fee per {@link Trade trade}. */
|
||||
private final double feePerTrade;
|
||||
|
||||
/**
|
||||
* Constructor for a fixed fee trading cost model.
|
||||
*
|
||||
* <pre>
|
||||
* Cost of opened {@link Position position}: (fixedFeePerTrade * 1)
|
||||
* Cost of closed {@link Position position}: (fixedFeePerTrade * 2)
|
||||
* </pre>
|
||||
*
|
||||
* @param feePerTrade the fixed fee per {@link Trade trade}
|
||||
*/
|
||||
public FixedTransactionCostModel(double feePerTrade) {
|
||||
this.feePerTrade = feePerTrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position the position
|
||||
* @param currentIndex the current bar index (irrelevant for
|
||||
* {@code FixedTransactionCostModel})
|
||||
* @return the transaction cost of the single {@code position}
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Position position, int currentIndex) {
|
||||
final var numFactory = position.getEntry().getPricePerAsset().getNumFactory();
|
||||
Num multiplier = numFactory.one();
|
||||
if (position.isClosed()) {
|
||||
multiplier = numFactory.numOf(2);
|
||||
}
|
||||
return numFactory.numOf(feePerTrade).multipliedBy(multiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the transaction cost of the single {@code position}
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
return this.calculate(position, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Note:</b> Both {@code price} and {@code amount} are irrelevant as the fee
|
||||
* in {@code FixedTransactionCostModel} is always the same.
|
||||
*
|
||||
* @return {@link #feePerTrade}
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return price.getNumFactory().numOf(feePerTrade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
boolean equality = false;
|
||||
if (this.getClass().equals(otherModel.getClass())) {
|
||||
equality = ((FixedTransactionCostModel) otherModel).feePerTrade == this.feePerTrade;
|
||||
}
|
||||
return equality;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* With this cost model, the trading costs for borrowing a position (i.e.
|
||||
* selling a position short) accrue linearly.
|
||||
*/
|
||||
public class LinearBorrowingCostModel implements CostModel {
|
||||
|
||||
/** The slope of the linear model (fee per period). */
|
||||
private final double feePerPeriod;
|
||||
|
||||
/**
|
||||
* Constructor with {@code feePerPeriod * nPeriod}.
|
||||
*
|
||||
* @param feePerPeriod the coefficient (e.g. 0.0001 for 1bp per period)
|
||||
*/
|
||||
public LinearBorrowingCostModel(double feePerPeriod) {
|
||||
this.feePerPeriod = feePerPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return always {@code 0}, as borrowing costs depend on borrowed period
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return price.getNumFactory().zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the borrowing cost of the closed {@code position}
|
||||
* @throws IllegalArgumentException if {@code position} is still open
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
if (position.isOpened()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Position is not closed. Final index of observation needs to be provided.");
|
||||
}
|
||||
return calculate(position, position.getExit().getIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the borrowing cost of the {@code position}
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Position position, int currentIndex) {
|
||||
Trade entryTrade = position.getEntry();
|
||||
Trade exitTrade = position.getExit();
|
||||
Num borrowingCost = position.getEntry().getNetPrice().getNumFactory().zero();
|
||||
|
||||
// Borrowing costs only apply to short positions.
|
||||
if (entryTrade != null && entryTrade.getType().equals(TradeType.SELL) && entryTrade.getAmount() != null) {
|
||||
int tradingPeriods = 0;
|
||||
if (position.isClosed()) {
|
||||
tradingPeriods = exitTrade.getIndex() - entryTrade.getIndex();
|
||||
} else if (position.isOpened()) {
|
||||
tradingPeriods = currentIndex - entryTrade.getIndex();
|
||||
}
|
||||
borrowingCost = getHoldingCostForPeriods(tradingPeriods, position.getEntry().getValue());
|
||||
}
|
||||
return borrowingCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tradingPeriods the number of periods
|
||||
* @param tradedValue the value of the initial trading position of the trade
|
||||
* @return the absolute borrowing cost
|
||||
*/
|
||||
private Num getHoldingCostForPeriods(int tradingPeriods, Num tradedValue) {
|
||||
return tradedValue.multipliedBy(tradedValue.getNumFactory()
|
||||
.numOf(tradingPeriods)
|
||||
.multipliedBy(tradedValue.getNumFactory().numOf(feePerPeriod)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
boolean equality = false;
|
||||
if (this.getClass().equals(otherModel.getClass())) {
|
||||
equality = ((LinearBorrowingCostModel) otherModel).feePerPeriod == this.feePerPeriod;
|
||||
}
|
||||
return equality;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* With this cost model, the trading costs for opening or closing a position
|
||||
* accrue linearly.
|
||||
*/
|
||||
public class LinearTransactionCostModel implements CostModel {
|
||||
|
||||
/** The slope of the linear model (fee per position). */
|
||||
private final double feePerPosition;
|
||||
|
||||
/**
|
||||
* Constructor with {@code feePerPosition * x}.
|
||||
*
|
||||
* @param feePerPosition the feePerPosition coefficient (e.g. 0.005 for 0.5% per
|
||||
* {@link Trade trade})
|
||||
*/
|
||||
public LinearTransactionCostModel(double feePerPosition) {
|
||||
this.feePerPosition = feePerPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position the position
|
||||
* @param currentIndex current bar index (irrelevant for the
|
||||
* LinearTransactionCostModel)
|
||||
* @return the trading cost of the single {@code position}
|
||||
*/
|
||||
@Override
|
||||
public Num calculate(Position position, int currentIndex) {
|
||||
return this.calculate(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
Num totalPositionCost = null;
|
||||
Trade entryTrade = position.getEntry();
|
||||
if (entryTrade != null) {
|
||||
// transaction costs of the entry trade
|
||||
totalPositionCost = entryTrade.getCost();
|
||||
if (position.getExit() != null) {
|
||||
totalPositionCost = totalPositionCost.plus(position.getExit().getCost());
|
||||
}
|
||||
}
|
||||
return totalPositionCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return amount.getNumFactory().numOf(feePerPosition).multipliedBy(price).multipliedBy(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
boolean equality = false;
|
||||
if (this.getClass().equals(otherModel.getClass())) {
|
||||
equality = ((LinearTransactionCostModel) otherModel).feePerPosition == this.feePerPosition;
|
||||
}
|
||||
return equality;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Cost model that uses recorded trade costs (fees) instead of recomputing them.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public final class RecordedTradeCostModel implements CostModel {
|
||||
|
||||
/** Shared instance. */
|
||||
public static final RecordedTradeCostModel INSTANCE = new RecordedTradeCostModel();
|
||||
|
||||
private RecordedTradeCostModel() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position, int finalIndex) {
|
||||
Trade entry = position == null ? null : position.getEntry();
|
||||
Trade exit = position == null ? null : position.getExit();
|
||||
Num zero = zeroFor(entry, exit);
|
||||
if (entry == null) {
|
||||
return zero;
|
||||
}
|
||||
Num total = entry.getIndex() <= finalIndex ? entry.getCost() : zero;
|
||||
if (exit != null && exit.getIndex() <= finalIndex) {
|
||||
total = total.plus(exit.getCost());
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Position position) {
|
||||
Trade entry = position == null ? null : position.getEntry();
|
||||
Trade exit = position == null ? null : position.getExit();
|
||||
Num zero = zeroFor(entry, exit);
|
||||
if (entry == null) {
|
||||
return zero;
|
||||
}
|
||||
if (exit == null) {
|
||||
return entry.getCost();
|
||||
}
|
||||
return entry.getCost().plus(exit.getCost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num calculate(Num price, Num amount) {
|
||||
return price == null ? DoubleNumFactory.getInstance().zero() : price.getNumFactory().zero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(CostModel otherModel) {
|
||||
return otherModel instanceof RecordedTradeCostModel;
|
||||
}
|
||||
|
||||
private Num zeroFor(Trade entry, Trade exit) {
|
||||
if (entry != null) {
|
||||
return entry.getCost().getNumFactory().zero();
|
||||
}
|
||||
if (exit != null) {
|
||||
return exit.getCost().getNumFactory().zero();
|
||||
}
|
||||
return DoubleNumFactory.getInstance().zero();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
|
||||
/**
|
||||
* With this cost model there are no trading costs.
|
||||
*/
|
||||
public class ZeroCostModel extends FixedTransactionCostModel {
|
||||
|
||||
private static final double ZERO_FEE_PER_TRADE = 0.0;
|
||||
|
||||
/**
|
||||
* Constructor with {@code feePerTrade = 0}.
|
||||
*
|
||||
* @see FixedTransactionCostModel
|
||||
*/
|
||||
public ZeroCostModel() {
|
||||
super(ZERO_FEE_PER_TRADE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
/**
|
||||
* Cost models for transaction and holding expense simulation.
|
||||
*
|
||||
* <p>
|
||||
* Use these models to align backtest assumptions with venue fees, borrow costs,
|
||||
* and recorded live execution fees.
|
||||
* </p>
|
||||
*/
|
||||
package org.ta4j.core.analysis.cost;
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
|
||||
/**
|
||||
* Pair of indices describing a sampled interval.
|
||||
*
|
||||
* @param previousIndex the interval start index
|
||||
* @param currentIndex the interval end index
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public record IndexPair(int previousIndex, int currentIndex) {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Single observation for frequency-aware statistics.
|
||||
*
|
||||
* <p>
|
||||
* Each sample includes the observed {@code value} alongside the elapsed time in
|
||||
* years ({@code deltaYears}) since the previous observation. The time delta is
|
||||
* used to derive annualization factors when summarizing unevenly spaced series.
|
||||
* </p>
|
||||
*
|
||||
* @param value the observed numeric value
|
||||
* @param deltaYears the elapsed time in years since the previous observation
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public record Sample(Num value, Num deltaYears) {
|
||||
/**
|
||||
* Creates a sample with the provided value and elapsed time in years.
|
||||
*
|
||||
* @param value the observed numeric value
|
||||
* @param deltaYears the elapsed time in years since the previous observation
|
||||
* @throws NullPointerException if {@code value} or {@code deltaYears} is
|
||||
* {@code null}
|
||||
*/
|
||||
public Sample {
|
||||
Objects.requireNonNull(value, "value must not be null");
|
||||
Objects.requireNonNull(deltaYears, "deltaYears must not be null");
|
||||
}
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Summary statistics for a numeric sample series with optional annualization
|
||||
* metadata.
|
||||
*
|
||||
* <p>
|
||||
* The summary accumulates central moments for mean, variance, skewness, and
|
||||
* kurtosis while also tracking the elapsed time between samples (in years). If
|
||||
* time deltas are provided, callers can derive an annualization factor for
|
||||
* volatility scaling.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public final class SampleSummary {
|
||||
|
||||
private final Moments moments;
|
||||
private final Num deltaYearsSum;
|
||||
private final Num deltaCount;
|
||||
|
||||
private SampleSummary(Moments moments, Num deltaYearsSum, Num deltaCount) {
|
||||
this.moments = moments;
|
||||
this.deltaYearsSum = deltaYearsSum;
|
||||
this.deltaCount = deltaCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a summary from frequency-aware samples.
|
||||
*
|
||||
* @param samples the samples to summarize
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return a summary of the provided samples
|
||||
*/
|
||||
public static SampleSummary fromSamples(Stream<Sample> samples, NumFactory numFactory) {
|
||||
var zero = numFactory.zero();
|
||||
var acc = samples.reduce(Acc.empty(zero),
|
||||
(current, sample) -> current.add(sample.value(), sample.deltaYears(), numFactory),
|
||||
(left, right) -> left.merge(right, numFactory));
|
||||
return acc.toSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a summary from raw values with no annualization metadata.
|
||||
*
|
||||
* <p>
|
||||
* The resulting summary treats all time deltas as zero, so the
|
||||
* {@link #annualizationFactor(NumFactory)} will be empty.
|
||||
* </p>
|
||||
*
|
||||
* @param values the values to summarize
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return a summary of the provided values
|
||||
*/
|
||||
public static SampleSummary fromValues(Stream<Num> values, NumFactory numFactory) {
|
||||
return fromSamples(values.map(value -> new Sample(value, numFactory.zero())), numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of samples observed.
|
||||
*
|
||||
* @return the sample count
|
||||
*/
|
||||
public int count() {
|
||||
return moments.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the arithmetic mean of the samples.
|
||||
*
|
||||
* @return the sample mean
|
||||
*/
|
||||
public Num mean() {
|
||||
return moments.mean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the second central moment (sum of squared deviations).
|
||||
*
|
||||
* @return the second central moment
|
||||
*/
|
||||
public Num m2() {
|
||||
return moments.m2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the third central moment.
|
||||
*
|
||||
* @return the third central moment
|
||||
*/
|
||||
public Num m3() {
|
||||
return moments.m3();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fourth central moment.
|
||||
*
|
||||
* @return the fourth central moment
|
||||
*/
|
||||
public Num m4() {
|
||||
return moments.m4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbiased sample variance.
|
||||
*
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return the sample variance (zero when fewer than two samples are available)
|
||||
*/
|
||||
public Num sampleVariance(NumFactory numFactory) {
|
||||
return moments.sampleVariance(numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample skewness.
|
||||
*
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return the sample skewness (zero when fewer than three samples or variance
|
||||
* is zero)
|
||||
*/
|
||||
public Num sampleSkewness(NumFactory numFactory) {
|
||||
return moments.sampleSkewness(numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample excess kurtosis.
|
||||
*
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return the sample kurtosis (zero when fewer than four samples or variance is
|
||||
* zero)
|
||||
*/
|
||||
public Num sampleKurtosis(NumFactory numFactory) {
|
||||
return moments.sampleKurtosis(numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the annualization factor derived from positive time deltas.
|
||||
*
|
||||
* <p>
|
||||
* The factor is {@code sqrt(count / deltaYearsSum)} and is typically used to
|
||||
* annualize volatility-like measures. If there are no positive deltas, the
|
||||
* result is empty.
|
||||
* </p>
|
||||
*
|
||||
* @param numFactory the numeric factory to use for calculations
|
||||
* @return the annualization factor, if time deltas are available
|
||||
*/
|
||||
public Optional<Num> annualizationFactor(NumFactory numFactory) {
|
||||
var zero = numFactory.zero();
|
||||
if (deltaCount.isLessThanOrEqual(zero) || deltaYearsSum.isLessThanOrEqual(zero)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(deltaCount.dividedBy(deltaYearsSum).sqrt());
|
||||
}
|
||||
|
||||
private record Acc(Moments moments, Num deltaYearsSum, Num deltaCount) {
|
||||
|
||||
static Acc empty(Num zero) {
|
||||
return new Acc(Moments.empty(zero), zero, zero);
|
||||
}
|
||||
|
||||
Acc add(Num value, Num deltaYears, NumFactory numFactory) {
|
||||
var nextMoments = moments.add(value, numFactory);
|
||||
if (deltaYears.isLessThanOrEqual(numFactory.zero())) {
|
||||
return new Acc(nextMoments, deltaYearsSum, deltaCount);
|
||||
}
|
||||
return new Acc(nextMoments, deltaYearsSum.plus(deltaYears), deltaCount.plus(numFactory.one()));
|
||||
}
|
||||
|
||||
Acc merge(Acc other, NumFactory numFactory) {
|
||||
var mergedMoments = moments.merge(other.moments, numFactory);
|
||||
return new Acc(mergedMoments, deltaYearsSum.plus(other.deltaYearsSum), deltaCount.plus(other.deltaCount));
|
||||
}
|
||||
|
||||
SampleSummary toSummary() {
|
||||
return new SampleSummary(moments, deltaYearsSum, deltaCount);
|
||||
}
|
||||
}
|
||||
|
||||
private record Moments(Num mean, Num m2, Num m3, Num m4, int count) {
|
||||
|
||||
static Moments empty(Num zero) {
|
||||
return new Moments(zero, zero, zero, zero, 0);
|
||||
}
|
||||
|
||||
Moments add(Num x, NumFactory f) {
|
||||
if (count == 0) {
|
||||
return new Moments(x, f.zero(), f.zero(), f.zero(), 1);
|
||||
}
|
||||
var n1 = count;
|
||||
var n = count + 1;
|
||||
var nNum = f.numOf(n);
|
||||
var n1Num = f.numOf(n1);
|
||||
var delta = x.minus(mean);
|
||||
var deltaN = delta.dividedBy(nNum);
|
||||
var deltaN2 = deltaN.multipliedBy(deltaN);
|
||||
var term1 = delta.multipliedBy(deltaN).multipliedBy(n1Num);
|
||||
var meanNext = mean.plus(deltaN);
|
||||
var m4Next = m4.plus(term1.multipliedBy(deltaN2).multipliedBy(f.numOf(n * n - 3 * n + 3)))
|
||||
.plus(deltaN2.multipliedBy(m2).multipliedBy(f.numOf(6)))
|
||||
.minus(deltaN.multipliedBy(m3).multipliedBy(f.numOf(4)));
|
||||
var m3Next = m3.plus(term1.multipliedBy(deltaN).multipliedBy(f.numOf(n - 2)))
|
||||
.minus(deltaN.multipliedBy(m2).multipliedBy(f.numOf(3)));
|
||||
var m2Next = m2.plus(term1);
|
||||
return new Moments(meanNext, m2Next, m3Next, m4Next, n);
|
||||
}
|
||||
|
||||
Moments merge(Moments other, NumFactory f) {
|
||||
if (other.count == 0) {
|
||||
return this;
|
||||
}
|
||||
if (count == 0) {
|
||||
return other;
|
||||
}
|
||||
var n1 = count;
|
||||
var n2 = other.count;
|
||||
var n = n1 + n2;
|
||||
var n1Num = f.numOf(n1);
|
||||
var n2Num = f.numOf(n2);
|
||||
var nNum = f.numOf(n);
|
||||
var delta = other.mean.minus(mean);
|
||||
var delta2 = delta.multipliedBy(delta);
|
||||
var delta3 = delta2.multipliedBy(delta);
|
||||
var delta4 = delta2.multipliedBy(delta2);
|
||||
var meanNext = mean.plus(delta.multipliedBy(n2Num).dividedBy(nNum));
|
||||
var m2Next = m2.plus(other.m2).plus(delta2.multipliedBy(n1Num).multipliedBy(n2Num).dividedBy(nNum));
|
||||
var m3Next = m3.plus(other.m3)
|
||||
.plus(delta3.multipliedBy(n1Num)
|
||||
.multipliedBy(n2Num)
|
||||
.multipliedBy(f.numOf(n1 - n2))
|
||||
.dividedBy(nNum.multipliedBy(nNum)))
|
||||
.plus(delta.multipliedBy(f.numOf(3))
|
||||
.multipliedBy(n1Num.multipliedBy(other.m2).minus(n2Num.multipliedBy(m2)))
|
||||
.dividedBy(nNum));
|
||||
var m4Next = m4.plus(other.m4)
|
||||
.plus(delta4.multipliedBy(n1Num)
|
||||
.multipliedBy(n2Num)
|
||||
.multipliedBy(f.numOf(n1 * n1 - n1 * n2 + n2 * n2))
|
||||
.dividedBy(nNum.multipliedBy(nNum).multipliedBy(nNum)))
|
||||
.plus(delta2.multipliedBy(f.numOf(6))
|
||||
.multipliedBy(n1Num.multipliedBy(n1Num)
|
||||
.multipliedBy(other.m2)
|
||||
.plus(n2Num.multipliedBy(n2Num).multipliedBy(m2)))
|
||||
.dividedBy(nNum.multipliedBy(nNum)))
|
||||
.plus(delta.multipliedBy(f.numOf(4))
|
||||
.multipliedBy(n1Num.multipliedBy(other.m3).minus(n2Num.multipliedBy(m3)))
|
||||
.dividedBy(nNum));
|
||||
return new Moments(meanNext, m2Next, m3Next, m4Next, n);
|
||||
}
|
||||
|
||||
Num sampleVariance(NumFactory f) {
|
||||
if (count < 2) {
|
||||
return f.zero();
|
||||
}
|
||||
return m2.dividedBy(f.numOf(count - 1));
|
||||
}
|
||||
|
||||
Num sampleSkewness(NumFactory f) {
|
||||
if (count < 3 || m2.isZero()) {
|
||||
return f.zero();
|
||||
}
|
||||
var n = f.numOf(count);
|
||||
var nMinus1 = f.numOf(count - 1);
|
||||
var nMinus2 = f.numOf(count - 2);
|
||||
var denom = m2.sqrt().multipliedBy(m2);
|
||||
var factor = n.multipliedBy(nMinus1).sqrt().dividedBy(nMinus2);
|
||||
return m3.dividedBy(denom).multipliedBy(factor);
|
||||
}
|
||||
|
||||
Num sampleKurtosis(NumFactory f) {
|
||||
if (count < 4 || m2.isZero()) {
|
||||
return f.zero();
|
||||
}
|
||||
var n = f.numOf(count);
|
||||
var nMinus1 = f.numOf(count - 1);
|
||||
var nMinus2 = f.numOf(count - 2);
|
||||
var nMinus3 = f.numOf(count - 3);
|
||||
var m2Squared = m2.multipliedBy(m2);
|
||||
var term = n.plus(f.numOf(1)).multipliedBy(m4).dividedBy(m2Squared).minus(nMinus1.multipliedBy(f.numOf(3)));
|
||||
return nMinus1.dividedBy(nMinus2.multipliedBy(nMinus3)).multipliedBy(term);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
|
||||
/**
|
||||
* Supported sampling granularities.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum SamplingFrequency {
|
||||
BAR, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
/**
|
||||
* Groups bar indices into sampling periods so return calculations can be
|
||||
* aggregated.
|
||||
*
|
||||
* <p>
|
||||
* The grouping logic detects period boundaries from bar end times in the
|
||||
* provided {@link ZoneId}. Weekly grouping follows ISO week rules. Each
|
||||
* sampling interval is represented by a pair of indices
|
||||
* {@code (previousIndex, currentIndex)} that spans the period. The first pair
|
||||
* always uses the supplied anchor index as its starting point.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public final class SamplingFrequencyIndexes {
|
||||
|
||||
private static final WeekFields ISO_WEEK_FIELDS = WeekFields.ISO;
|
||||
|
||||
private final SamplingFrequency samplingFrequency;
|
||||
private final ZoneId groupingZoneId;
|
||||
|
||||
/**
|
||||
* Creates a grouping helper for the chosen aggregation mode and time zone.
|
||||
*
|
||||
* @param samplingFrequency the sampling granularity
|
||||
* @param groupingZoneId the time zone used to interpret bar end times
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public SamplingFrequencyIndexes(SamplingFrequency samplingFrequency, ZoneId groupingZoneId) {
|
||||
this.samplingFrequency = Objects.requireNonNull(samplingFrequency, "samplingFrequency must not be null");
|
||||
this.groupingZoneId = Objects.requireNonNull(groupingZoneId, "groupingZoneId must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index pairs spanning each sampling period from the provided range.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param anchorIndex the starting anchor index for the first sampled pair
|
||||
* @param start the first index eligible for sampling
|
||||
* @param end the last index eligible for sampling
|
||||
* @return a stream of index pairs describing each sampling interval
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public Stream<IndexPair> sample(BarSeries series, int anchorIndex, int start, int end) {
|
||||
if (start > end || end - start < 1) {
|
||||
return Stream.empty();
|
||||
}
|
||||
if (samplingFrequency == SamplingFrequency.BAR) {
|
||||
return IntStream.rangeClosed(start, end).mapToObj(i -> new IndexPair(i - 1, i));
|
||||
}
|
||||
|
||||
var periodEndIndices = periodEndIndices(series, start, end).toArray();
|
||||
if (periodEndIndices.length == 0) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var firstPair = Stream.of(new IndexPair(anchorIndex, periodEndIndices[0]));
|
||||
var consecutivePairs = IntStream.range(1, periodEndIndices.length)
|
||||
.mapToObj(k -> new IndexPair(periodEndIndices[k - 1], periodEndIndices[k]));
|
||||
|
||||
return Stream.concat(firstPair, consecutivePairs);
|
||||
}
|
||||
|
||||
private IntStream periodEndIndices(BarSeries series, int start, int end) {
|
||||
return IntStream.rangeClosed(start, end).filter(i -> isPeriodEnd(series, i, end));
|
||||
}
|
||||
|
||||
private boolean isPeriodEnd(BarSeries series, int index, int endIndex) {
|
||||
if (index == endIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var now = endTimeZoned(series, index);
|
||||
var next = endTimeZoned(series, index + 1);
|
||||
|
||||
return switch (samplingFrequency) {
|
||||
case SECOND -> crossesChronoUnitBoundary(now, next, ChronoUnit.SECONDS);
|
||||
case MINUTE -> crossesChronoUnitBoundary(now, next, ChronoUnit.MINUTES);
|
||||
case HOUR -> crossesChronoUnitBoundary(now, next, ChronoUnit.HOURS);
|
||||
case DAY -> !now.toLocalDate().equals(next.toLocalDate());
|
||||
case WEEK -> !sameIsoWeek(now, next);
|
||||
case MONTH -> !YearMonth.from(now).equals(YearMonth.from(next));
|
||||
case BAR -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean crossesChronoUnitBoundary(ZonedDateTime a, ZonedDateTime b, ChronoUnit chronoUnit) {
|
||||
return !a.truncatedTo(chronoUnit).equals(b.truncatedTo(chronoUnit));
|
||||
}
|
||||
|
||||
private boolean sameIsoWeek(ZonedDateTime a, ZonedDateTime b) {
|
||||
var weekA = a.get(ISO_WEEK_FIELDS.weekOfWeekBasedYear());
|
||||
var weekB = b.get(ISO_WEEK_FIELDS.weekOfWeekBasedYear());
|
||||
var yearA = a.get(ISO_WEEK_FIELDS.weekBasedYear());
|
||||
var yearB = b.get(ISO_WEEK_FIELDS.weekBasedYear());
|
||||
return weekA == weekB && yearA == yearB;
|
||||
}
|
||||
|
||||
private ZonedDateTime endTimeZoned(BarSeries series, int index) {
|
||||
return endTimeInstant(series, index).atZone(groupingZoneId);
|
||||
}
|
||||
|
||||
private Instant endTimeInstant(BarSeries series, int index) {
|
||||
return series.getBar(index).getEndTime();
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
/**
|
||||
* Sampling frequency utilities for analysis windows and aggregation.
|
||||
*
|
||||
* <p>
|
||||
* Use this package to normalize return and risk calculations across intraday
|
||||
* and multi-day horizons with explicit sampling semantics.
|
||||
* </p>
|
||||
*/
|
||||
package org.ta4j.core.analysis.frequency;
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
/**
|
||||
* Analysis.
|
||||
*
|
||||
* <p>
|
||||
* This package contains instruments to inspect backtesting results like the
|
||||
* {@link org.ta4j.core.analysis.CashFlow CashFlow}, window definitions and
|
||||
* context policies for window-aware criterion evaluation
|
||||
* ({@link org.ta4j.core.analysis.AnalysisWindow AnalysisWindow},
|
||||
* {@link org.ta4j.core.analysis.AnalysisContext AnalysisContext}), and to
|
||||
* calculate {@link org.ta4j.core.analysis.cost.CostModel trading costs}.
|
||||
*/
|
||||
package org.ta4j.core.analysis;
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.reports.BaseTradingStatement;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.serialization.DurationTypeAdapter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Wraps the outcome of a {@link BacktestExecutor} run including runtime
|
||||
* metrics.
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public record BacktestExecutionResult(BarSeries barSeries, List<TradingStatement> tradingStatements,
|
||||
BacktestRuntimeReport runtimeReport) implements TradingStatementExecutionResult<BacktestRuntimeReport> {
|
||||
|
||||
/**
|
||||
* Ensures properties are non-null.
|
||||
*
|
||||
* @param barSeries the bar series used for backtesting
|
||||
* @param tradingStatements produced trading statements in the order of the
|
||||
* supplied strategies
|
||||
* @param runtimeReport runtime statistics for the execution
|
||||
*/
|
||||
public BacktestExecutionResult {
|
||||
barSeries = Objects.requireNonNull(barSeries, "barSeries must not be null");
|
||||
tradingStatements = Objects.requireNonNull(tradingStatements, "tradingStatements must not be null");
|
||||
runtimeReport = Objects.requireNonNull(runtimeReport, "runtimeReport must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top strategies sorted by the provided analysis criteria in order
|
||||
* of importance.
|
||||
* <p>
|
||||
* This method preserves the legacy lexicographic behavior where the first
|
||||
* criterion is primary and later criteria are tie-breakers. For weighted and
|
||||
* normalized ranking, use
|
||||
* {@link #getTopStrategiesWeighted(int, RankingProfile)} or
|
||||
* {@link #getTopStrategiesWeighted(int, TradingStatementExecutionResult.WeightedCriterion...)}.
|
||||
* </p>
|
||||
*
|
||||
* @param limit the maximum number of strategies to return
|
||||
* @param criteria the analysis criteria to sort by, in order of importance
|
||||
* (first criterion is primary, second breaks ties, etc.)
|
||||
* @return a list of the top trading statements sorted by the criteria
|
||||
* @throws NullPointerException if criteria is null
|
||||
* @throws IllegalArgumentException if criteria is empty or limit is negative
|
||||
* @since 0.19
|
||||
*/
|
||||
public List<TradingStatement> getTopStrategies(int limit, AnalysisCriterion... criteria) {
|
||||
Objects.requireNonNull(criteria, "criteria must not be null");
|
||||
if (criteria.length == 0) {
|
||||
throw new IllegalArgumentException("At least one criterion must be provided");
|
||||
}
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException("limit must not be negative");
|
||||
}
|
||||
|
||||
return getTopStrategies(limit, Arrays.asList(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top strategies sorted by the provided analysis criteria in order
|
||||
* of importance.
|
||||
* <p>
|
||||
* This method preserves the legacy lexicographic behavior where the first
|
||||
* criterion is primary and later criteria are tie-breakers. For weighted and
|
||||
* normalized ranking, use
|
||||
* {@link #getTopStrategiesWeighted(int, RankingProfile)} or
|
||||
* {@link #getTopStrategiesWeighted(int, TradingStatementExecutionResult.WeightedCriterion...)}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Performance: Uses a hybrid approach that selects the optimal algorithm based
|
||||
* on the limit size relative to the total number of strategies. For small
|
||||
* limits ({@literal <} 25% of total), uses a heap-based partial sort O(n log
|
||||
* k). For larger limits, uses a full sort O(n log n) which is more
|
||||
* cache-friendly.
|
||||
*
|
||||
* @param limit the maximum number of strategies to return
|
||||
* @param criteria the analysis criteria to sort by, in order of importance
|
||||
* (first criterion is primary, second breaks ties, etc.)
|
||||
* @return a list of the top trading statements sorted by the criteria
|
||||
* @throws NullPointerException if criteria is null
|
||||
* @throws IllegalArgumentException if criteria is empty or limit is negative
|
||||
* @since 0.19
|
||||
*/
|
||||
public List<TradingStatement> getTopStrategies(int limit, List<AnalysisCriterion> criteria) {
|
||||
Objects.requireNonNull(criteria, "criteria must not be null");
|
||||
if (criteria.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one criterion must be provided");
|
||||
}
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException("limit must not be negative");
|
||||
}
|
||||
|
||||
// Early returns for edge cases
|
||||
if (limit == 0 || tradingStatements.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int effectiveLimit = Math.min(limit, tradingStatements.size());
|
||||
|
||||
// Pre-calculate criterion values for all statements using IdentityHashMap
|
||||
// (faster than HashMap for object identity)
|
||||
Map<TradingStatement, List<Num>> criterionValuesMap = new IdentityHashMap<>(tradingStatements.size());
|
||||
Map<TradingStatement, Map<AnalysisCriterion, Num>> criterionScoresMap = new IdentityHashMap<>(
|
||||
tradingStatements.size());
|
||||
for (TradingStatement statement : tradingStatements) {
|
||||
List<Num> values = new ArrayList<>(criteria.size());
|
||||
Map<AnalysisCriterion, Num> scores = new HashMap<>(criteria.size());
|
||||
for (AnalysisCriterion criterion : criteria) {
|
||||
Num value = criterion.calculate(barSeries, statement.getTradingRecord());
|
||||
values.add(value);
|
||||
scores.put(criterion, value);
|
||||
}
|
||||
criterionValuesMap.put(statement, values);
|
||||
criterionScoresMap.put(statement, scores);
|
||||
}
|
||||
|
||||
Comparator<TradingStatement> comparator = createComparator(criteria, criterionValuesMap);
|
||||
|
||||
// Use heap-based partial sort for small limits (more efficient O(n log k))
|
||||
// Use full sort for large limits (more cache-friendly)
|
||||
List<TradingStatement> topStatements;
|
||||
if (effectiveLimit < tradingStatements.size() / 4) {
|
||||
topStatements = selectTopKWithHeap(tradingStatements, effectiveLimit, comparator);
|
||||
} else {
|
||||
topStatements = selectTopKWithSort(tradingStatements, effectiveLimit, comparator);
|
||||
}
|
||||
|
||||
// Attach criterion scores to the returned statements
|
||||
return attachCriterionScores(topStatements, criterionScoresMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top strategies using weighted, normalized criterion ranking.
|
||||
* <p>
|
||||
* Multipliers are normalized internally so any positive scale is accepted (for
|
||||
* example {@code 1.5/1.1/0.8} and {@code 15/11/8} produce equivalent weight
|
||||
* proportions).
|
||||
* </p>
|
||||
*
|
||||
* @param limit the maximum number of strategies to return
|
||||
* @param profile weighted ranking profile
|
||||
* @return the top trading statements ordered by composite weighted score
|
||||
* @throws NullPointerException if profile is null
|
||||
* @throws IllegalArgumentException if limit is negative
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<TradingStatement> getTopStrategiesWeighted(int limit, RankingProfile profile) {
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException("limit must not be negative");
|
||||
}
|
||||
if (limit == 0 || tradingStatements.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<RankedTradingStatement> rankedStatements = rankTradingStatements(profile);
|
||||
return attachRankedCriterionScores(rankedStatements, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top strategies using weighted, normalized criterion ranking with
|
||||
* the default normalizer and missing-value policy.
|
||||
*
|
||||
* <p>
|
||||
* This overload is the shortest path for weighted ranking when callers already
|
||||
* know their criteria and relative weights.
|
||||
* </p>
|
||||
*
|
||||
* @param limit the maximum number of strategies to return
|
||||
* @param criteria weighted criteria to normalize and combine
|
||||
* @return the top trading statements ordered by composite weighted score
|
||||
* @throws NullPointerException if criteria is null
|
||||
* @throws IllegalArgumentException if limit is negative
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<TradingStatement> getTopStrategiesWeighted(int limit,
|
||||
TradingStatementExecutionResult.WeightedCriterion... criteria) {
|
||||
return getTopStrategiesWeighted(limit, RankingProfile.weighted(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches criterion scores to trading statements by creating new
|
||||
* BaseTradingStatement instances with the scores included.
|
||||
*
|
||||
* @param statements the trading statements to attach scores to
|
||||
* @param criterionScoresMap map of statement to criterion scores
|
||||
* @return list of trading statements with criterion scores attached
|
||||
*/
|
||||
private List<TradingStatement> attachCriterionScores(List<TradingStatement> statements,
|
||||
Map<TradingStatement, Map<AnalysisCriterion, Num>> criterionScoresMap) {
|
||||
List<TradingStatement> result = new ArrayList<>(statements.size());
|
||||
for (TradingStatement statement : statements) {
|
||||
Map<AnalysisCriterion, Num> scores = criterionScoresMap.get(statement);
|
||||
result.add(attachCriterionScores(statement, scores));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<TradingStatement> attachRankedCriterionScores(List<RankedTradingStatement> rankedStatements,
|
||||
int limit) {
|
||||
int effectiveLimit = Math.min(limit, rankedStatements.size());
|
||||
List<TradingStatement> statementsWithScores = new ArrayList<>(effectiveLimit);
|
||||
for (int i = 0; i < effectiveLimit; i++) {
|
||||
RankedTradingStatement rankedStatement = rankedStatements.get(i);
|
||||
statementsWithScores.add(attachCriterionScores(rankedStatement.statement(), rankedStatement.rawScores()));
|
||||
}
|
||||
return statementsWithScores;
|
||||
}
|
||||
|
||||
private TradingStatement attachCriterionScores(TradingStatement statement, Map<AnalysisCriterion, Num> scores) {
|
||||
if (statement instanceof BaseTradingStatement && scores != null && !scores.isEmpty()) {
|
||||
BaseTradingStatement baseStatement = (BaseTradingStatement) statement;
|
||||
return new BaseTradingStatement(baseStatement.strategy, baseStatement.tradingRecord,
|
||||
baseStatement.positionStatsReport, baseStatement.performanceReport, scores);
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a comparator that sorts trading statements by multiple criteria in
|
||||
* order of importance.
|
||||
*
|
||||
* @param criteria the analysis criteria to sort by
|
||||
* @param criterionValuesMap pre-calculated criterion values for each statement
|
||||
* @return a comparator for trading statements
|
||||
*/
|
||||
private Comparator<TradingStatement> createComparator(List<AnalysisCriterion> criteria,
|
||||
Map<TradingStatement, List<Num>> criterionValuesMap) {
|
||||
return (statement1, statement2) -> {
|
||||
List<Num> values1 = criterionValuesMap.get(statement1);
|
||||
List<Num> values2 = criterionValuesMap.get(statement2);
|
||||
|
||||
for (int i = 0; i < criteria.size(); i++) {
|
||||
AnalysisCriterion criterion = criteria.get(i);
|
||||
Num value1 = values1.get(i);
|
||||
Num value2 = values2.get(i);
|
||||
|
||||
// Use criterion's betterThan method to determine order
|
||||
if (criterion.betterThan(value1, value2)) {
|
||||
return -1; // statement1 is better, should come first
|
||||
} else if (criterion.betterThan(value2, value1)) {
|
||||
return 1; // statement2 is better, should come first
|
||||
}
|
||||
// If equal, continue to next criterion
|
||||
}
|
||||
return 0; // All criteria equal
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects top k strategies using a min-heap (priority queue). Efficient for
|
||||
* small k relative to n: O(n log k).
|
||||
*
|
||||
* @param statements the trading statements to select from
|
||||
* @param k the number of top strategies to return
|
||||
* @param comparator the comparator to determine ranking
|
||||
* @return a list of the top k trading statements
|
||||
*/
|
||||
private List<TradingStatement> selectTopKWithHeap(List<TradingStatement> statements, int k,
|
||||
Comparator<TradingStatement> comparator) {
|
||||
// Use a min-heap with reversed comparator (so worst of the top-k is at root)
|
||||
PriorityQueue<TradingStatement> heap = new PriorityQueue<>(k + 1, comparator.reversed());
|
||||
|
||||
for (TradingStatement statement : statements) {
|
||||
heap.offer(statement);
|
||||
if (heap.size() > k) {
|
||||
heap.poll(); // Remove the worst element from top-k
|
||||
}
|
||||
}
|
||||
|
||||
// Extract results and sort them in correct order (best-first)
|
||||
List<TradingStatement> result = new ArrayList<>(heap);
|
||||
result.sort(comparator);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects top k strategies using full sort. Efficient for large k relative to
|
||||
* n: O(n log n).
|
||||
*
|
||||
* @param statements the trading statements to select from
|
||||
* @param k the number of top strategies to return
|
||||
* @param comparator the comparator to determine ranking
|
||||
* @return a list of the top k trading statements
|
||||
*/
|
||||
private List<TradingStatement> selectTopKWithSort(List<TradingStatement> statements, int k,
|
||||
Comparator<TradingStatement> comparator) {
|
||||
List<TradingStatement> sorted = new ArrayList<>(statements);
|
||||
sorted.sort(comparator);
|
||||
return sorted.subList(0, k);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Gson gson = new GsonBuilder().registerTypeAdapter(Duration.class, new DurationTypeAdapter()).create();
|
||||
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("barSeriesName", barSeries.getName());
|
||||
json.addProperty("tradingStatementsCount", tradingStatements.size());
|
||||
json.add("runtimeReport", JsonParser.parseString(runtimeReport.toString()).getAsJsonObject());
|
||||
return gson.toJson(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
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.num.Num;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.reports.TradingStatementGenerator;
|
||||
import org.ta4j.core.walkforward.AnchoredExpandingWalkForwardSplitter;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Allows backtesting multiple strategies and comparing them to find out which
|
||||
* is the best.
|
||||
*
|
||||
* <p>
|
||||
* Prefer this class when running many candidate strategies, parameter sweeps,
|
||||
* or weighted ranking workflows. For one strategy over one series, use
|
||||
* {@link BarSeriesManager} for lower setup overhead.
|
||||
* </p>
|
||||
*/
|
||||
public class BacktestExecutor {
|
||||
|
||||
private final BarSeriesManager seriesManager;
|
||||
private final TradingStatementGenerator tradingStatementGenerator;
|
||||
|
||||
/**
|
||||
* Default batch size for processing strategies. When the number of strategies
|
||||
* exceeds this threshold, they will be processed in batches to prevent memory
|
||||
* exhaustion. Default is 500.
|
||||
*/
|
||||
private static final int DEFAULT_BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Smaller batch size for very large strategy counts (>5000) to reduce memory
|
||||
* pressure. Default is 250.
|
||||
*/
|
||||
private static final int SMALL_BATCH_SIZE = 250;
|
||||
|
||||
/**
|
||||
* Threshold for switching from parallel to sequential execution. When the
|
||||
* number of strategies exceeds this value, batched sequential processing is
|
||||
* used instead of unbounded parallel execution. Default is 1000.
|
||||
*/
|
||||
private static final int PARALLEL_THRESHOLD = 1000;
|
||||
|
||||
/**
|
||||
* Threshold for using smaller batch size. When strategy count exceeds this, use
|
||||
* SMALL_BATCH_SIZE instead of DEFAULT_BATCH_SIZE. Default is 5000.
|
||||
*/
|
||||
private static final int LARGE_COUNT_THRESHOLD = 5000;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series) {
|
||||
this(new BarSeriesManager(series), new TradingStatementGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param tradeExecutionModel the trade execution model
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series, TradeExecutionModel tradeExecutionModel) {
|
||||
this(new BarSeriesManager(series, tradeExecutionModel), new TradingStatementGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding the asset (e.g.
|
||||
* borrowing)
|
||||
* @param tradeExecutionModel the trade execution model
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series, CostModel transactionCostModel, CostModel holdingCostModel,
|
||||
TradeExecutionModel tradeExecutionModel) {
|
||||
this(new BarSeriesManager(series, transactionCostModel, holdingCostModel, tradeExecutionModel),
|
||||
new TradingStatementGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param tradingStatementGenerator the TradingStatementGenerator
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator) {
|
||||
this(new BarSeriesManager(series), tradingStatementGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param tradingStatementGenerator the TradingStatementGenerator
|
||||
* @param tradeExecutionModel the trade execution model
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator,
|
||||
TradeExecutionModel tradeExecutionModel) {
|
||||
this(new BarSeriesManager(series, tradeExecutionModel), tradingStatementGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param series the bar series
|
||||
* @param tradingStatementGenerator the TradingStatementGenerator
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding the asset (e.g.
|
||||
* borrowing)
|
||||
*/
|
||||
public BacktestExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator,
|
||||
CostModel transactionCostModel, CostModel holdingCostModel, TradeExecutionModel tradeExecutionModel) {
|
||||
this(new BarSeriesManager(series, transactionCostModel, holdingCostModel, tradeExecutionModel),
|
||||
tradingStatementGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param seriesManager the preconfigured series manager including its cost
|
||||
* models, trade execution model, and default
|
||||
* trading-record creation policy
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestExecutor(BarSeriesManager seriesManager) {
|
||||
this(seriesManager, new TradingStatementGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param seriesManager the preconfigured series manager including
|
||||
* its cost models, trade execution model, and
|
||||
* default trading-record creation policy
|
||||
* @param tradingStatementGenerator the TradingStatementGenerator
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestExecutor(BarSeriesManager seriesManager, TradingStatementGenerator tradingStatementGenerator) {
|
||||
this.seriesManager = Objects.requireNonNull(seriesManager, "seriesManager");
|
||||
this.tradingStatementGenerator = Objects.requireNonNull(tradingStatementGenerator, "tradingStatementGenerator");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes given strategies and returns trading statements with
|
||||
* {@code tradeType} (to open the position) = BUY.
|
||||
*
|
||||
* @param strategies the strategies
|
||||
* @param amount the amount used to open/close the position
|
||||
* @return a list of TradingStatements
|
||||
*/
|
||||
public List<TradingStatement> execute(List<Strategy> strategies, Num amount) {
|
||||
return execute(strategies, amount, Trade.TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes given strategies with specified trade type to open the position and
|
||||
* return the trading statements.
|
||||
*
|
||||
* @param strategies the strategies
|
||||
* @param amount the amount used to open/close the position
|
||||
* @param tradeType the {@link Trade.TradeType} used to open the position
|
||||
* @return a list of TradingStatements
|
||||
*/
|
||||
public List<TradingStatement> execute(List<Strategy> strategies, Num amount, Trade.TradeType tradeType) {
|
||||
return executeWithRuntimeReport(strategies, amount, tradeType).tradingStatements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies while collecting runtime measurements and trading
|
||||
* statements.
|
||||
*
|
||||
* @param strategies the strategies to execute (read-only)
|
||||
* @param amount the amount used to open/close the position
|
||||
* @return execution result containing immutable trading statements and a
|
||||
* runtime report
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public BacktestExecutionResult executeWithRuntimeReport(List<Strategy> strategies, Num amount) {
|
||||
return executeWithRuntimeReport(strategies, amount, Trade.TradeType.BUY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies while collecting runtime measurements and trading
|
||||
* statements.
|
||||
*
|
||||
* @param strategies the list of strategies to execute (read-only)
|
||||
* @param amount the amount used to open/close the position
|
||||
* @param tradeType the {@link Trade.TradeType} used to open the position
|
||||
* @return execution result containing immutable trading statements and a
|
||||
* runtime report
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public BacktestExecutionResult executeWithRuntimeReport(List<Strategy> strategies, Num amount,
|
||||
Trade.TradeType tradeType) {
|
||||
return executeWithRuntimeReport(strategies, amount, tradeType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies while collecting runtime measurements and trading
|
||||
* statements, with optional progress reporting.
|
||||
* <p>
|
||||
* For large strategy counts (> {@value #PARALLEL_THRESHOLD}), automatically
|
||||
* uses batched sequential processing with a batch size of
|
||||
* {@value #DEFAULT_BATCH_SIZE} to prevent memory exhaustion. For smaller
|
||||
* counts, uses standard parallel execution.
|
||||
* </p>
|
||||
* <p>
|
||||
* If {@code progressCallback} is null, uses {@link ProgressCompletion#noOp()}
|
||||
* as the default (no progress reporting). To use default logging progress, pass
|
||||
* {@link ProgressCompletion#logging(Class)} or
|
||||
* {@link ProgressCompletion#logging(String)} with your class or logger name.
|
||||
* </p>
|
||||
*
|
||||
* @param strategies the strategies
|
||||
* @param amount the amount used to open/close the position
|
||||
* @param tradeType the {@link Trade.TradeType} used to open the position
|
||||
* @param progressCallback optional callback for progress updates (receives
|
||||
* completed count). May be null, in which case
|
||||
* {@link ProgressCompletion#noOp()} is used.
|
||||
* @return execution result with trading statements and runtime report
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public BacktestExecutionResult executeWithRuntimeReport(List<Strategy> strategies, Num amount,
|
||||
Trade.TradeType tradeType, Consumer<Integer> progressCallback) {
|
||||
return executeWithRuntimeReport(strategies, amount, tradeType, progressCallback, DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies while collecting runtime measurements and trading
|
||||
* statements, with configurable batch size and optional progress reporting.
|
||||
* <p>
|
||||
* When the strategy count exceeds {@value #PARALLEL_THRESHOLD}, uses batched
|
||||
* sequential processing to prevent memory exhaustion. Each batch is processed
|
||||
* in parallel, but batches are executed sequentially with explicit GC hints
|
||||
* between batches to manage memory pressure.
|
||||
* </p>
|
||||
*
|
||||
* @param strategies the strategies
|
||||
* @param amount the amount used to open/close the position
|
||||
* @param tradeType the {@link Trade.TradeType} used to open the position
|
||||
* @param progressCallback optional callback for progress updates (receives
|
||||
* completed count). May be null.
|
||||
* @param batchSize the maximum number of strategies to process in each
|
||||
* batch. Ignored if strategy count {@literal <=}
|
||||
* {@value #PARALLEL_THRESHOLD}.
|
||||
* @return execution result with trading statements and runtime report
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public BacktestExecutionResult executeWithRuntimeReport(List<Strategy> strategies, Num amount,
|
||||
Trade.TradeType tradeType, Consumer<Integer> progressCallback, int batchSize) {
|
||||
Objects.requireNonNull(strategies, "strategies must not be null");
|
||||
Objects.requireNonNull(amount, "amount must not be null");
|
||||
Objects.requireNonNull(tradeType, "tradeType must not be null");
|
||||
|
||||
if (batchSize <= 0) {
|
||||
throw new IllegalArgumentException("batchSize must be positive");
|
||||
}
|
||||
|
||||
if (strategies.isEmpty()) {
|
||||
return new BacktestExecutionResult(seriesManager.getBarSeries(), new ArrayList<>(),
|
||||
BacktestRuntimeReport.empty());
|
||||
}
|
||||
|
||||
Strategy[] strategyArray = strategies.toArray(Strategy[]::new);
|
||||
int strategyCount = strategyArray.length;
|
||||
TradingStatement[] statements = new TradingStatement[strategyCount];
|
||||
long[] durations = new long[strategyCount];
|
||||
|
||||
// Use default no-op callback if none provided, and set total strategies for
|
||||
// logging callbacks
|
||||
Consumer<Integer> effectiveCallback = ProgressCompletion.withTotalStrategies(
|
||||
progressCallback != null ? progressCallback : ProgressCompletion.noOp(), strategyCount);
|
||||
|
||||
long overallStart = System.nanoTime();
|
||||
|
||||
// For large strategy counts, use batched processing to prevent memory
|
||||
// exhaustion. Use smaller batches for very large counts.
|
||||
if (strategyCount > PARALLEL_THRESHOLD) {
|
||||
int effectiveBatchSize = strategyCount > LARGE_COUNT_THRESHOLD ? Math.min(batchSize, SMALL_BATCH_SIZE)
|
||||
: batchSize;
|
||||
executeBatched(strategyArray, statements, durations, amount, tradeType, effectiveCallback,
|
||||
effectiveBatchSize);
|
||||
} else {
|
||||
executeUnbounded(strategyArray, statements, durations, amount, tradeType, effectiveCallback);
|
||||
}
|
||||
|
||||
Duration overallRuntime = Duration.ofNanos(System.nanoTime() - overallStart);
|
||||
|
||||
List<TradingStatement> tradingStatements = new ArrayList<>(strategyCount);
|
||||
for (TradingStatement statement : statements) {
|
||||
tradingStatements.add(statement);
|
||||
}
|
||||
|
||||
List<BacktestRuntimeReport.StrategyRuntime> strategyRuntimes = new ArrayList<>(strategyCount);
|
||||
for (int i = 0; i < strategyCount; i++) {
|
||||
strategyRuntimes
|
||||
.add(new BacktestRuntimeReport.StrategyRuntime(strategyArray[i], Duration.ofNanos(durations[i])));
|
||||
}
|
||||
|
||||
BacktestRuntimeReport runtimeReport = buildRuntimeReport(durations, overallRuntime, strategyRuntimes);
|
||||
return new BacktestExecutionResult(seriesManager.getBarSeries(), tradingStatements, runtimeReport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy using strategy starting type
|
||||
* and unit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult executeWalkForward(Strategy strategy, WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Num unitAmount = seriesManager.getBarSeries().numFactory().one();
|
||||
return executeWalkForward(strategy, unitAmount, strategy.getStartingType(), config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy with explicit amount and
|
||||
* strategy starting trade type.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param amount amount used to open/close trades
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult executeWalkForward(Strategy strategy, Num amount,
|
||||
WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
return executeWalkForward(strategy, amount, strategy.getStartingType(), config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy with explicit amount and trade
|
||||
* type.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param amount amount used to open/close trades
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult executeWalkForward(Strategy strategy, Num amount,
|
||||
Trade.TradeType tradeType, WalkForwardConfig config) {
|
||||
return executeWalkForward(strategy, amount, tradeType, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy with optional per-fold
|
||||
* progress callback.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param amount amount used to open/close trades
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param config walk-forward configuration
|
||||
* @param progressCallback optional callback receiving completed fold count
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult executeWalkForward(Strategy strategy, Num amount,
|
||||
Trade.TradeType tradeType, WalkForwardConfig config, Consumer<Integer> progressCallback) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
Objects.requireNonNull(tradeType, "tradeType");
|
||||
Objects.requireNonNull(config, "config");
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(seriesManager, tradingStatementGenerator,
|
||||
new AnchoredExpandingWalkForwardSplitter());
|
||||
return executor.execute(strategy, tradeType, amount, config, progressCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs both standard backtest and walk-forward evaluation for one strategy
|
||||
* using strategy starting type and unit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param config walk-forward configuration
|
||||
* @return combined backtest and walk-forward result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestAndWalkForwardResult executeWithWalkForward(Strategy strategy, WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Num unitAmount = seriesManager.getBarSeries().numFactory().one();
|
||||
return executeWithWalkForward(strategy, unitAmount, strategy.getStartingType(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs both standard backtest and walk-forward evaluation for one strategy with
|
||||
* explicit amount and strategy starting trade type.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param amount amount used to open/close trades
|
||||
* @param config walk-forward configuration
|
||||
* @return combined backtest and walk-forward result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestAndWalkForwardResult executeWithWalkForward(Strategy strategy, Num amount,
|
||||
WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
return executeWithWalkForward(strategy, amount, strategy.getStartingType(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs both standard backtest and walk-forward evaluation for one strategy.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param amount amount used to open/close trades
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param config walk-forward configuration
|
||||
* @return combined backtest and walk-forward result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestAndWalkForwardResult executeWithWalkForward(Strategy strategy, Num amount, Trade.TradeType tradeType,
|
||||
WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
Objects.requireNonNull(tradeType, "tradeType");
|
||||
Objects.requireNonNull(config, "config");
|
||||
|
||||
BacktestExecutionResult backtestResult = executeWithRuntimeReport(List.of(strategy), amount, tradeType);
|
||||
StrategyWalkForwardExecutionResult walkForwardResult = executeWalkForward(strategy, amount, tradeType, config);
|
||||
return new BacktestAndWalkForwardResult(backtestResult, walkForwardResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies and returns only the top K results based on a criterion,
|
||||
* using a streaming approach that minimizes memory usage.
|
||||
* <p>
|
||||
* This method is ideal for very large strategy counts (10,000+) where you only
|
||||
* need the best performers. It uses a min-heap to track only the top K
|
||||
* strategies, discarding worse performers immediately to minimize memory
|
||||
* pressure.
|
||||
* </p>
|
||||
* <p>
|
||||
* Memory usage is O(K + batchSize) instead of O(strategyCount), making it
|
||||
* suitable for massive parameter sweeps.
|
||||
* </p>
|
||||
*
|
||||
* @param strategies the strategies to evaluate
|
||||
* @param amount the amount used to open/close the position
|
||||
* @param tradeType the {@link Trade.TradeType} used to open the position
|
||||
* @param criterion the criterion used to rank strategies (higher is
|
||||
* better)
|
||||
* @param topK the maximum number of top strategies to return
|
||||
* @param progressCallback optional callback for progress updates (receives
|
||||
* completed count). May be null.
|
||||
* @return execution result containing only the top K strategies and runtime
|
||||
* report
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public BacktestExecutionResult executeAndKeepTopK(List<Strategy> strategies, Num amount, Trade.TradeType tradeType,
|
||||
AnalysisCriterion criterion, int topK, Consumer<Integer> progressCallback) {
|
||||
Objects.requireNonNull(strategies, "strategies must not be null");
|
||||
Objects.requireNonNull(amount, "amount must not be null");
|
||||
Objects.requireNonNull(tradeType, "tradeType must not be null");
|
||||
Objects.requireNonNull(criterion, "criterion must not be null");
|
||||
|
||||
if (topK <= 0) {
|
||||
throw new IllegalArgumentException("topK must be positive");
|
||||
}
|
||||
|
||||
if (strategies.isEmpty()) {
|
||||
return new BacktestExecutionResult(seriesManager.getBarSeries(), new ArrayList<>(),
|
||||
BacktestRuntimeReport.empty());
|
||||
}
|
||||
|
||||
int strategyCount = strategies.size();
|
||||
int effectiveTopK = Math.min(topK, strategyCount);
|
||||
|
||||
Comparator<StrategyEvaluation> bestFirstComparator = createBestFirstComparator(criterion);
|
||||
PriorityQueue<StrategyEvaluation> topStrategies = new PriorityQueue<>(effectiveTopK + 1,
|
||||
bestFirstComparator.reversed());
|
||||
|
||||
ConcurrentLinkedQueue<StrategyEvaluation> batchResults = new ConcurrentLinkedQueue<>();
|
||||
// Use default no-op callback if none provided, and set total strategies for
|
||||
// logging callbacks
|
||||
Consumer<Integer> effectiveCallback = ProgressCompletion.withTotalStrategies(
|
||||
progressCallback != null ? progressCallback : ProgressCompletion.noOp(), strategyCount);
|
||||
ProgressTracker progressTracker = ProgressTracker.create(effectiveCallback);
|
||||
|
||||
long overallStart = System.nanoTime();
|
||||
|
||||
// Determine batch size based on strategy count
|
||||
int batchSize = strategyCount > LARGE_COUNT_THRESHOLD ? SMALL_BATCH_SIZE : DEFAULT_BATCH_SIZE;
|
||||
|
||||
Strategy[] strategyArray = strategies.toArray(Strategy[]::new);
|
||||
long[] durationNanos = new long[strategyCount];
|
||||
|
||||
// Process in batches
|
||||
for (int batchStart = 0; batchStart < strategyCount; batchStart += batchSize) {
|
||||
int batchEnd = Math.min(batchStart + batchSize, strategyCount);
|
||||
final int batchStartFinal = batchStart;
|
||||
|
||||
batchResults.clear();
|
||||
|
||||
// Evaluate batch in parallel
|
||||
IntStream.range(0, batchEnd - batchStart).parallel().forEach(localIndex -> {
|
||||
int globalIndex = batchStartFinal + localIndex;
|
||||
Strategy strategy = strategyArray[globalIndex];
|
||||
|
||||
long strategyStart = System.nanoTime();
|
||||
TradingRecord tradingRecord = seriesManager.run(strategy, tradeType, amount);
|
||||
TradingStatement statement = tradingStatementGenerator.generate(strategy, tradingRecord,
|
||||
seriesManager.getBarSeries());
|
||||
long duration = System.nanoTime() - strategyStart;
|
||||
durationNanos[globalIndex] = duration;
|
||||
Num criterionValue = criterion.calculate(seriesManager.getBarSeries(), statement.getTradingRecord());
|
||||
batchResults.add(new StrategyEvaluation(statement, criterionValue, globalIndex));
|
||||
|
||||
if (progressTracker != null) {
|
||||
progressTracker.reportCompletion();
|
||||
}
|
||||
});
|
||||
|
||||
// Merge batch results into top-K heap
|
||||
for (StrategyEvaluation evaluation : batchResults) {
|
||||
if (topStrategies.size() < effectiveTopK) {
|
||||
topStrategies.offer(evaluation);
|
||||
} else {
|
||||
// Heap is full - compare with worst strategy
|
||||
StrategyEvaluation worst = topStrategies.peek();
|
||||
if (worst != null) {
|
||||
if (bestFirstComparator.compare(evaluation, worst) < 0) {
|
||||
topStrategies.poll(); // Remove worst
|
||||
topStrategies.offer(evaluation); // Add new
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear batch results and suggest GC
|
||||
batchResults.clear();
|
||||
if (batchEnd < strategyCount) {
|
||||
System.gc();
|
||||
Thread.yield(); // Give GC a chance to run
|
||||
}
|
||||
}
|
||||
|
||||
Duration overallRuntime = Duration.ofNanos(System.nanoTime() - overallStart);
|
||||
|
||||
// Extract top strategies and sort them in correct order (best first)
|
||||
List<StrategyEvaluation> sortedEvaluations = new ArrayList<>(topStrategies);
|
||||
sortedEvaluations.sort(bestFirstComparator); // Sort using non-reversed comparator (best first)
|
||||
List<TradingStatement> resultStatements = new ArrayList<>(sortedEvaluations.size());
|
||||
for (StrategyEvaluation evaluation : sortedEvaluations) {
|
||||
resultStatements.add(evaluation.statement());
|
||||
}
|
||||
|
||||
// Build runtime report (approximate, since we don't track all individual times)
|
||||
List<BacktestRuntimeReport.StrategyRuntime> strategyRuntimes = new ArrayList<>(sortedEvaluations.size());
|
||||
for (StrategyEvaluation evaluation : sortedEvaluations) {
|
||||
Duration runtime = Duration.ofNanos(durationNanos[evaluation.index()]);
|
||||
strategyRuntimes
|
||||
.add(new BacktestRuntimeReport.StrategyRuntime(evaluation.statement().getStrategy(), runtime));
|
||||
}
|
||||
|
||||
// Calculate summary statistics from saved durations
|
||||
BacktestRuntimeReport runtimeReport = buildRuntimeReport(durationNanos, overallRuntime, strategyRuntimes);
|
||||
|
||||
return new BacktestExecutionResult(seriesManager.getBarSeries(), resultStatements, runtimeReport);
|
||||
}
|
||||
|
||||
private Comparator<StrategyEvaluation> createBestFirstComparator(AnalysisCriterion criterion) {
|
||||
return (left, right) -> {
|
||||
Num leftValue = left.criterionValue();
|
||||
Num rightValue = right.criterionValue();
|
||||
|
||||
boolean leftNaN = leftValue.isNaN();
|
||||
boolean rightNaN = rightValue.isNaN();
|
||||
if (leftNaN && rightNaN) {
|
||||
return Integer.compare(left.index(), right.index());
|
||||
}
|
||||
if (leftNaN) {
|
||||
return 1;
|
||||
}
|
||||
if (rightNaN) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (criterion.betterThan(leftValue, rightValue)) {
|
||||
return -1;
|
||||
}
|
||||
if (criterion.betterThan(rightValue, leftValue)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int naturalComparison = leftValue.compareTo(rightValue);
|
||||
if (naturalComparison != 0) {
|
||||
return naturalComparison;
|
||||
}
|
||||
|
||||
return Integer.compare(left.index(), right.index());
|
||||
};
|
||||
}
|
||||
|
||||
private static final class StrategyEvaluation {
|
||||
|
||||
private final TradingStatement statement;
|
||||
private final Num criterionValue;
|
||||
private final int index;
|
||||
|
||||
private StrategyEvaluation(TradingStatement statement, Num criterionValue, int index) {
|
||||
this.statement = statement;
|
||||
this.criterionValue = criterionValue;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
private TradingStatement statement() {
|
||||
return statement;
|
||||
}
|
||||
|
||||
private Num criterionValue() {
|
||||
return criterionValue;
|
||||
}
|
||||
|
||||
private int index() {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined result for one-strategy backtest and walk-forward execution.
|
||||
*
|
||||
* @param backtest standard backtest output
|
||||
* @param walkForward walk-forward output
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record BacktestAndWalkForwardResult(BacktestExecutionResult backtest,
|
||||
StrategyWalkForwardExecutionResult walkForward) {
|
||||
|
||||
/**
|
||||
* Creates a validated combined result.
|
||||
*
|
||||
* @param backtest standard backtest output
|
||||
* @param walkForward walk-forward output
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BacktestAndWalkForwardResult {
|
||||
backtest = Objects.requireNonNull(backtest, "backtest");
|
||||
walkForward = Objects.requireNonNull(walkForward, "walkForward");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies using unbounded parallel execution (standard behavior).
|
||||
*/
|
||||
private void executeUnbounded(Strategy[] strategyArray, TradingStatement[] statements, long[] durations, Num amount,
|
||||
Trade.TradeType tradeType, Consumer<Integer> progressCallback) {
|
||||
int strategyCount = strategyArray.length;
|
||||
ProgressTracker progressTracker = ProgressTracker.create(progressCallback);
|
||||
|
||||
IntStream indexStream = IntStream.range(0, strategyCount);
|
||||
if (strategyCount > 1) {
|
||||
indexStream = indexStream.parallel();
|
||||
}
|
||||
|
||||
indexStream.forEach(index -> {
|
||||
Strategy strategy = strategyArray[index];
|
||||
long strategyStart = System.nanoTime();
|
||||
TradingRecord tradingRecord = seriesManager.run(strategy, tradeType, amount);
|
||||
TradingStatement statement = tradingStatementGenerator.generate(strategy, tradingRecord,
|
||||
seriesManager.getBarSeries());
|
||||
statements[index] = statement;
|
||||
durations[index] = System.nanoTime() - strategyStart;
|
||||
|
||||
if (progressTracker != null) {
|
||||
progressTracker.reportCompletion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes strategies in batches to prevent memory exhaustion. Each batch is
|
||||
* processed in parallel, but batches are executed sequentially with explicit GC
|
||||
* hints between batches.
|
||||
*/
|
||||
private void executeBatched(Strategy[] strategyArray, TradingStatement[] statements, long[] durations, Num amount,
|
||||
Trade.TradeType tradeType, Consumer<Integer> progressCallback, int batchSize) {
|
||||
int strategyCount = strategyArray.length;
|
||||
ProgressTracker progressTracker = ProgressTracker.create(progressCallback);
|
||||
|
||||
for (int batchStart = 0; batchStart < strategyCount; batchStart += batchSize) {
|
||||
int batchEnd = Math.min(batchStart + batchSize, strategyCount);
|
||||
final int batchStartFinal = batchStart;
|
||||
|
||||
IntStream.range(0, batchEnd - batchStart).parallel().forEach(localIndex -> {
|
||||
int globalIndex = batchStartFinal + localIndex;
|
||||
Strategy strategy = strategyArray[globalIndex];
|
||||
long strategyStart = System.nanoTime();
|
||||
TradingRecord tradingRecord = seriesManager.run(strategy, tradeType, amount);
|
||||
TradingStatement statement = tradingStatementGenerator.generate(strategy, tradingRecord,
|
||||
seriesManager.getBarSeries());
|
||||
statements[globalIndex] = statement;
|
||||
durations[globalIndex] = System.nanoTime() - strategyStart;
|
||||
|
||||
if (progressTracker != null) {
|
||||
progressTracker.reportCompletion();
|
||||
}
|
||||
});
|
||||
|
||||
// Aggressively suggest GC between batches to manage memory pressure
|
||||
// For very large counts, be more aggressive
|
||||
if (batchEnd < strategyCount) {
|
||||
System.gc();
|
||||
Thread.yield(); // Give GC a chance to run
|
||||
if (strategyCount > LARGE_COUNT_THRESHOLD) {
|
||||
// For very large strategy counts, try even harder
|
||||
try {
|
||||
Thread.sleep(10); // Brief pause to let GC complete
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProgressTracker {
|
||||
|
||||
private final Consumer<Integer> callback;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition ready = lock.newCondition();
|
||||
private int completedCount = 0;
|
||||
private int nextToReport = 1;
|
||||
|
||||
private ProgressTracker(Consumer<Integer> callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
static ProgressTracker create(Consumer<Integer> callback) {
|
||||
return callback == null ? null : new ProgressTracker(callback);
|
||||
}
|
||||
|
||||
void reportCompletion() {
|
||||
int completionOrder;
|
||||
lock.lock();
|
||||
try {
|
||||
completionOrder = ++completedCount;
|
||||
while (completionOrder != nextToReport) {
|
||||
ready.await();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while reporting progress", e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
try {
|
||||
callback.accept(completionOrder);
|
||||
} finally {
|
||||
lock.lock();
|
||||
try {
|
||||
nextToReport++;
|
||||
ready.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BacktestRuntimeReport buildRuntimeReport(long[] durations, Duration overallRuntime,
|
||||
List<BacktestRuntimeReport.StrategyRuntime> strategyRuntimes) {
|
||||
LongSummaryStatistics summaryStatistics = Arrays.stream(durations).summaryStatistics();
|
||||
if (summaryStatistics.getCount() == 0) {
|
||||
return new BacktestRuntimeReport(overallRuntime, Duration.ZERO, Duration.ZERO, Duration.ZERO, Duration.ZERO,
|
||||
strategyRuntimes);
|
||||
}
|
||||
|
||||
long[] sortedDurations = durations.clone();
|
||||
Arrays.sort(sortedDurations);
|
||||
int midPoint = sortedDurations.length / 2;
|
||||
long medianNanos;
|
||||
if (sortedDurations.length % 2 == 0) {
|
||||
medianNanos = (sortedDurations[midPoint - 1] + sortedDurations[midPoint]) / 2;
|
||||
} else {
|
||||
medianNanos = sortedDurations[midPoint];
|
||||
}
|
||||
|
||||
Duration min = Duration.ofNanos(summaryStatistics.getMin());
|
||||
Duration max = Duration.ofNanos(summaryStatistics.getMax());
|
||||
Duration average = Duration.ofNanos(Math.round(summaryStatistics.getAverage()));
|
||||
Duration median = Duration.ofNanos(medianNanos);
|
||||
|
||||
return new BacktestRuntimeReport(overallRuntime, min, max, average, median, strategyRuntimes);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.serialization.DurationTypeAdapter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Captures runtime statistics collected while executing strategies with the
|
||||
* {@link BacktestExecutor}.
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public record BacktestRuntimeReport(Duration overallRuntime, Duration minStrategyRuntime, Duration maxStrategyRuntime,
|
||||
Duration averageStrategyRuntime, Duration medianStrategyRuntime, List<StrategyRuntime> strategyRuntimes) {
|
||||
|
||||
/**
|
||||
* Exclusion strategy for JSON serialization that excludes the strategyRuntimes
|
||||
* field.
|
||||
*/
|
||||
private static final ExclusionStrategy STRATEGY_RUNTIMES_EXCLUSION = new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes f) {
|
||||
return f.getName().equals("strategyRuntimes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor that ensures {@link #strategyRuntimes} is non-null.
|
||||
*
|
||||
* @param overallRuntime total wall clock time to execute all strategies
|
||||
* @param minStrategyRuntime minimum runtime measured for a single strategy
|
||||
* @param maxStrategyRuntime maximum runtime measured for a single strategy
|
||||
* @param averageStrategyRuntime average runtime measured for the executed
|
||||
* strategies
|
||||
* @param medianStrategyRuntime median runtime measured for the executed
|
||||
* strategies
|
||||
* @param strategyRuntimes individual runtime per evaluated strategy
|
||||
*/
|
||||
public BacktestRuntimeReport {
|
||||
strategyRuntimes = List.copyOf(Objects.requireNonNull(strategyRuntimes, "strategyRuntimes must not be null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty runtime report.
|
||||
*
|
||||
* @return runtime report with zeroed statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static BacktestRuntimeReport empty() {
|
||||
return new BacktestRuntimeReport(Duration.ZERO, Duration.ZERO, Duration.ZERO, Duration.ZERO, Duration.ZERO,
|
||||
List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the number of strategies that were evaluated.
|
||||
*
|
||||
* @return strategy runtime count
|
||||
* @since 0.19
|
||||
*/
|
||||
public int strategyCount() {
|
||||
return strategyRuntimes.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON string representation of this {@code BacktestRuntimeReport}
|
||||
* instance. The JSON format includes runtime statistics but excludes the
|
||||
* detailed strategy runtimes list.
|
||||
*
|
||||
* @return a JSON string representing the current state of this report
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
Gson gson = new GsonBuilder().registerTypeAdapter(Duration.class, new DurationTypeAdapter())
|
||||
.setExclusionStrategies(STRATEGY_RUNTIMES_EXCLUSION)
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the runtime for an individual strategy evaluation.
|
||||
*
|
||||
* @param strategy strategy instance that was evaluated
|
||||
* @param runtime elapsed time for the evaluation
|
||||
* @since 0.19
|
||||
*/
|
||||
public record StrategyRuntime(Strategy strategy, Duration runtime) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseTradingRecord;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.cost.CostModel;
|
||||
import org.ta4j.core.analysis.cost.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.reports.TradingStatementGenerator;
|
||||
import org.ta4j.core.walkforward.AnchoredExpandingWalkForwardSplitter;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
|
||||
/**
|
||||
* A manager for {@link BarSeries} objects used for backtesting. Allows to run a
|
||||
* {@link Strategy trading strategy} over the managed bar series.
|
||||
*
|
||||
* <p>
|
||||
* Default {@code run(...)} overloads create a fresh trading record through this
|
||||
* manager's configured {@link TradingRecordFactory}. Existing behavior remains
|
||||
* unchanged by default ({@link BaseTradingRecord}), while callers can inject a
|
||||
* custom record implementation for unified backtest/live execution paths.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Use this class as the default backtest entrypoint when you are evaluating one
|
||||
* strategy over one series. For large strategy batches with ranking and runtime
|
||||
* telemetry, switch to {@link BacktestExecutor}.
|
||||
* </p>
|
||||
*/
|
||||
public class BarSeriesManager {
|
||||
|
||||
/** The logger */
|
||||
private static final Logger log = LoggerFactory.getLogger(BarSeriesManager.class);
|
||||
|
||||
/** Default trading record factory. */
|
||||
private static final TradingRecordFactory DEFAULT_TRADING_RECORD_FACTORY = (tradeType, startIndex, endIndex,
|
||||
transactionCostModel, holdingCostModel) -> new BaseTradingRecord(tradeType, startIndex, endIndex,
|
||||
transactionCostModel, holdingCostModel);
|
||||
|
||||
/** The managed bar series */
|
||||
private final BarSeries barSeries;
|
||||
|
||||
/** The trading cost models */
|
||||
private final CostModel transactionCostModel;
|
||||
private final CostModel holdingCostModel;
|
||||
|
||||
/** The trade execution model to use */
|
||||
private final TradeExecutionModel tradeExecutionModel;
|
||||
|
||||
/** The trading record factory used by default run overloads. */
|
||||
private final TradingRecordFactory tradingRecordFactory;
|
||||
|
||||
/**
|
||||
* Factory for creating trading records for backtest runs.
|
||||
*
|
||||
* <p>
|
||||
* Implementations must return a fresh mutable {@link TradingRecord} for each
|
||||
* invocation. Reusing the same instance across runs causes state leakage
|
||||
* between executions.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TradingRecordFactory {
|
||||
/**
|
||||
* Creates a trading record.
|
||||
*
|
||||
* @param tradeType strategy entry type
|
||||
* @param startIndex run start index (already clamped)
|
||||
* @param endIndex run end index (already clamped)
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @param holdingCostModel holding cost model
|
||||
* @return a new trading record instance for the run
|
||||
* @since 0.22.4
|
||||
*/
|
||||
TradingRecord create(TradeType tradeType, int startIndex, int endIndex, CostModel transactionCostModel,
|
||||
CostModel holdingCostModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with {@link #tradeExecutionModel} = {@link TradeOnNextOpenModel}.
|
||||
*
|
||||
* @param barSeries the bar series to be managed
|
||||
*/
|
||||
public BarSeriesManager(BarSeries barSeries) {
|
||||
this(barSeries, new ZeroCostModel(), new ZeroCostModel(), new TradeOnNextOpenModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series to be managed
|
||||
* @param tradeExecutionModel the trade execution model to use
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BarSeriesManager(BarSeries barSeries, TradeExecutionModel tradeExecutionModel) {
|
||||
this(barSeries, new ZeroCostModel(), new ZeroCostModel(), tradeExecutionModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with {@link #tradeExecutionModel} = {@link TradeOnNextOpenModel}.
|
||||
*
|
||||
* @param barSeries the bar series to be managed
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding the asset (e.g.
|
||||
* borrowing)
|
||||
*/
|
||||
public BarSeriesManager(BarSeries barSeries, CostModel transactionCostModel, CostModel holdingCostModel) {
|
||||
this(barSeries, transactionCostModel, holdingCostModel, new TradeOnNextOpenModel(),
|
||||
DEFAULT_TRADING_RECORD_FACTORY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series to be managed
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding asset (e.g. borrowing)
|
||||
* @param tradeExecutionModel the trade execution model to use
|
||||
*/
|
||||
public BarSeriesManager(BarSeries barSeries, CostModel transactionCostModel, CostModel holdingCostModel,
|
||||
TradeExecutionModel tradeExecutionModel) {
|
||||
this(barSeries, transactionCostModel, holdingCostModel, tradeExecutionModel, DEFAULT_TRADING_RECORD_FACTORY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param barSeries the bar series to be managed
|
||||
* @param transactionCostModel the cost model for transactions of the asset
|
||||
* @param holdingCostModel the cost model for holding asset (e.g. borrowing)
|
||||
* @param tradeExecutionModel the trade execution model to use
|
||||
* @param tradingRecordFactory factory for default run overloads
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public BarSeriesManager(BarSeries barSeries, CostModel transactionCostModel, CostModel holdingCostModel,
|
||||
TradeExecutionModel tradeExecutionModel, TradingRecordFactory tradingRecordFactory) {
|
||||
Objects.requireNonNull(barSeries, "barSeries");
|
||||
Objects.requireNonNull(transactionCostModel, "transactionCostModel");
|
||||
Objects.requireNonNull(holdingCostModel, "holdingCostModel");
|
||||
Objects.requireNonNull(tradeExecutionModel, "tradeExecutionModel");
|
||||
Objects.requireNonNull(tradingRecordFactory, "tradingRecordFactory");
|
||||
this.barSeries = barSeries;
|
||||
this.transactionCostModel = transactionCostModel;
|
||||
this.holdingCostModel = holdingCostModel;
|
||||
this.tradeExecutionModel = tradeExecutionModel;
|
||||
this.tradingRecordFactory = tradingRecordFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the managed bar series
|
||||
*/
|
||||
public BarSeries getBarSeries() {
|
||||
return barSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the transaction cost model
|
||||
*/
|
||||
public CostModel getTransactionCostModel() {
|
||||
return transactionCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the holding cost model
|
||||
*/
|
||||
public CostModel getHoldingCostModel() {
|
||||
return holdingCostModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series.
|
||||
*
|
||||
* Opens the position with the strategy {@link TradeType starting type}.
|
||||
*
|
||||
* @return the trading record coming from the run
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy) {
|
||||
return run(strategy, strategy.getStartingType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series (from startIndex to
|
||||
* finishIndex).
|
||||
*
|
||||
* Opens the position with the strategy {@link TradeType starting type}.
|
||||
*
|
||||
* @param strategy the trading strategy to execute (read-only)
|
||||
* @param startIndex the start index for the run (included)
|
||||
* @param finishIndex the finish index for the run (included)
|
||||
* @return a new trading record populated with the run's trades
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, int startIndex, int finishIndex) {
|
||||
return run(strategy, strategy.getStartingType(), barSeries.numFactory().one(), startIndex, finishIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series.
|
||||
*
|
||||
* Opens the position with a trade of {@link TradeType tradeType}.
|
||||
*
|
||||
* @param strategy the trading strategy to execute (read-only)
|
||||
* @param tradeType the {@link TradeType} used to open the position
|
||||
* @return a new trading record populated with the run's trades
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradeType tradeType) {
|
||||
return run(strategy, tradeType, barSeries.numFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series (from startIndex to
|
||||
* finishIndex).
|
||||
*
|
||||
* Opens the position with a trade of {@link TradeType tradeType}.
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradeType the {@link TradeType} used to open the position
|
||||
* @param startIndex the start index for the run (included)
|
||||
* @param finishIndex the finish index for the run (included)
|
||||
* @return the trading record coming from the run
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradeType tradeType, int startIndex, int finishIndex) {
|
||||
return run(strategy, tradeType, barSeries.numFactory().one(), startIndex, finishIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series.
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradeType the {@link TradeType} used to open the position
|
||||
* @param amount the amount used to open/close the trades
|
||||
* @return the trading record coming from the run
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradeType tradeType, Num amount) {
|
||||
return run(strategy, tradeType, amount, barSeries.getBeginIndex(), barSeries.getEndIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series (from startIndex to
|
||||
* finishIndex).
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradeType the {@link TradeType} used to open the trades
|
||||
* @param amount the amount used to open/close the trades
|
||||
* @param startIndex the start index for the run (included)
|
||||
* @param finishIndex the finish index for the run (included)
|
||||
* @return the trading record coming from the run
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradeType tradeType, Num amount, int startIndex, int finishIndex) {
|
||||
TradingRecord tradingRecord = createDefaultTradingRecord(tradeType, startIndex, finishIndex);
|
||||
return run(strategy, tradingRecord, amount, startIndex, finishIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series using the supplied trading
|
||||
* record.
|
||||
*
|
||||
* <p>
|
||||
* This allows callers to backtest with alternate {@link TradingRecord}
|
||||
* implementations (for example a lot-aware {@code BaseTradingRecord}) while
|
||||
* reusing {@link BarSeriesManager}'s execution loop.
|
||||
* </p>
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradingRecord the trading record instance to mutate
|
||||
* @return the supplied trading record after execution
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradingRecord tradingRecord) {
|
||||
return run(strategy, tradingRecord, barSeries.numFactory().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series using the supplied trading
|
||||
* record.
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradingRecord the trading record instance to mutate
|
||||
* @param amount the amount used to open/close the trades
|
||||
* @return the supplied trading record after execution
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradingRecord tradingRecord, Num amount) {
|
||||
return run(strategy, tradingRecord, amount, barSeries.getBeginIndex(), barSeries.getEndIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided strategy over the managed series using the supplied trading
|
||||
* record (from startIndex to finishIndex).
|
||||
*
|
||||
* <p>
|
||||
* <strong>Thread safety:</strong> This {@code BarSeriesManager.run(...)}
|
||||
* overload mutates the supplied {@link TradingRecord}. Callers must ensure
|
||||
* exclusive access to that record, or synchronize externally, while the run is
|
||||
* executing. Concurrent reads or writes against the same {@link TradingRecord}
|
||||
* during execution lead to undefined behavior.
|
||||
* </p>
|
||||
*
|
||||
* @param strategy the trading strategy
|
||||
* @param tradingRecord the trading record instance to mutate
|
||||
* @param amount the amount used to open/close the trades
|
||||
* @param startIndex the start index for the run (included)
|
||||
* @param finishIndex the finish index for the run (included)
|
||||
* @return the supplied trading record after execution
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public TradingRecord run(Strategy strategy, TradingRecord tradingRecord, Num amount, int startIndex,
|
||||
int finishIndex) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Objects.requireNonNull(tradingRecord, "tradingRecord");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
int runBeginIndex = Math.max(startIndex, barSeries.getBeginIndex());
|
||||
int runEndIndex = Math.min(finishIndex, barSeries.getEndIndex());
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Running strategy (indexes: {} -> {}): {} (starting with {})", runBeginIndex, runEndIndex,
|
||||
strategy, tradingRecord.getStartingType());
|
||||
}
|
||||
|
||||
int lastProcessedIndex = runEndIndex;
|
||||
for (int i = runBeginIndex; i <= runEndIndex; i++) {
|
||||
lastProcessedIndex = i;
|
||||
tradeExecutionModel.onBar(i, tradingRecord, barSeries);
|
||||
// For each bar between both indexes...
|
||||
if (strategy.shouldOperate(i, tradingRecord)) {
|
||||
tradeExecutionModel.execute(i, tradingRecord, barSeries, amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tradingRecord.isClosed() && runEndIndex == barSeries.getEndIndex()) {
|
||||
// If the last position is still open and there are still bars after the
|
||||
// endIndex of the barSeries, then we execute the strategy on these bars
|
||||
// to give an opportunity to close this position.
|
||||
int seriesMaxSize = Math.max(barSeries.getEndIndex() + 1, barSeries.getBarData().size());
|
||||
for (int i = runEndIndex + 1; i < seriesMaxSize; i++) {
|
||||
lastProcessedIndex = i;
|
||||
tradeExecutionModel.onBar(i, tradingRecord, barSeries);
|
||||
// For each bar after the end index of this run...
|
||||
// --> Trying to close the last position
|
||||
if (strategy.shouldOperate(i, tradingRecord)) {
|
||||
tradeExecutionModel.execute(i, tradingRecord, barSeries, amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tradeExecutionModel.onRunEnd(lastProcessedIndex, tradingRecord);
|
||||
return tradingRecord;
|
||||
}
|
||||
|
||||
private TradingRecord createDefaultTradingRecord(TradeType tradeType, int startIndex, int finishIndex) {
|
||||
int clampedStartIndex = Math.max(startIndex, barSeries.getBeginIndex());
|
||||
int clampedEndIndex = Math.min(finishIndex, barSeries.getEndIndex());
|
||||
TradingRecord tradingRecord = tradingRecordFactory.create(tradeType, clampedStartIndex, clampedEndIndex,
|
||||
transactionCostModel, holdingCostModel);
|
||||
if (tradingRecord == null) {
|
||||
throw new IllegalStateException("tradingRecordFactory returned null");
|
||||
}
|
||||
return tradingRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy using the strategy starting
|
||||
* trade type and unit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult runWalkForward(Strategy strategy, WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Num unitAmount = barSeries.numFactory().one();
|
||||
return runWalkForward(strategy, strategy.getStartingType(), unitAmount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy using the provided entry trade
|
||||
* type and unit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult runWalkForward(Strategy strategy, TradeType tradeType,
|
||||
WalkForwardConfig config) {
|
||||
Num unitAmount = barSeries.numFactory().one();
|
||||
return runWalkForward(strategy, tradeType, unitAmount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy with explicit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param amount amount used to open/close trades
|
||||
* @param config walk-forward configuration
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult runWalkForward(Strategy strategy, TradeType tradeType, Num amount,
|
||||
WalkForwardConfig config) {
|
||||
return runWalkForward(strategy, tradeType, amount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing for one strategy with optional per-fold
|
||||
* progress updates.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param amount amount used to open/close trades
|
||||
* @param config walk-forward configuration
|
||||
* @param progressCallback optional callback receiving completed fold count
|
||||
* @return walk-forward execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult runWalkForward(Strategy strategy, TradeType tradeType, Num amount,
|
||||
WalkForwardConfig config, Consumer<Integer> progressCallback) {
|
||||
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(this, new TradingStatementGenerator(),
|
||||
new AnchoredExpandingWalkForwardSplitter());
|
||||
return executor.execute(strategy, tradeType, amount, config, progressCallback);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Position;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Shared helper functions for backtest execution models.
|
||||
*
|
||||
* <p>
|
||||
* Centralizes common execution concerns such as trade-side resolution and
|
||||
* mapping strategy signal bars to concrete execution index/price pairs.
|
||||
* </p>
|
||||
*/
|
||||
final class ExecutionModelSupport {
|
||||
|
||||
private ExecutionModelSupport() {
|
||||
}
|
||||
|
||||
static ExecutionTarget resolveExecutionTarget(int signalIndex, BarSeries barSeries,
|
||||
TradeExecutionModel.PriceSource priceSource) {
|
||||
if (signalIndex < barSeries.getBeginIndex()) {
|
||||
return null;
|
||||
}
|
||||
if (priceSource == TradeExecutionModel.PriceSource.CURRENT_CLOSE) {
|
||||
if (!hasAccessibleBar(signalIndex, barSeries)) {
|
||||
return null;
|
||||
}
|
||||
return new ExecutionTarget(signalIndex, barSeries.getBar(signalIndex).getClosePrice());
|
||||
}
|
||||
if (signalIndex > barSeries.getEndIndex()) {
|
||||
return null;
|
||||
}
|
||||
int executionIndex = signalIndex + 1;
|
||||
if (executionIndex > barSeries.getEndIndex()) {
|
||||
return null;
|
||||
}
|
||||
return new ExecutionTarget(executionIndex, barSeries.getBar(executionIndex).getOpenPrice());
|
||||
}
|
||||
|
||||
private static boolean hasAccessibleBar(int signalIndex, BarSeries barSeries) {
|
||||
int rawIndex = signalIndex - barSeries.getRemovedBarsCount();
|
||||
return rawIndex >= 0 && rawIndex < barSeries.getBarData().size();
|
||||
}
|
||||
|
||||
static TradeType nextTradeType(TradingRecord tradingRecord) {
|
||||
if (tradingRecord.isClosed()) {
|
||||
return tradingRecord.getStartingType();
|
||||
}
|
||||
Position currentPosition = tradingRecord.getCurrentPosition();
|
||||
if (currentPosition == null || currentPosition.getEntry() == null) {
|
||||
return tradingRecord.getStartingType();
|
||||
}
|
||||
return currentPosition.getEntry().getType().complementType();
|
||||
}
|
||||
|
||||
record ExecutionTarget(int index, Num price) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Utility class providing default progress completion callbacks for backtest
|
||||
* execution.
|
||||
* <p>
|
||||
* Provides factory methods for common progress reporting patterns:
|
||||
* <ul>
|
||||
* <li>{@link #noOp()} - No-op callback that does nothing
|
||||
* <li>{@link #logging()} - Logs progress using a logger for the calling class
|
||||
* (convenience method with automatic caller detection)
|
||||
* <li>{@link #logging(int)} - Logs progress using a logger for the calling
|
||||
* class at the specified interval (convenience method with automatic caller
|
||||
* detection)
|
||||
* <li>{@link #logging(String)} - Logs progress to a logger identified by the
|
||||
* given name
|
||||
* <li>{@link #logging(Class)} - Logs progress to a logger for the given class
|
||||
* <li>{@link #logging(Logger)} - Logs progress to the provided logger
|
||||
* <li>{@link #logging(String, int)} - Logs progress at specified intervals
|
||||
* <li>{@link #logging(Class, int)} - Logs progress at specified intervals
|
||||
* <li>{@link #logging(Logger, int)} - Logs progress at specified intervals
|
||||
* <li>{@link #loggingWithMemory()} - Logs progress and memory statistics using
|
||||
* a logger for the calling class (convenience method with automatic caller
|
||||
* detection)
|
||||
* <li>{@link #loggingWithMemory(int)} - Logs progress and memory statistics
|
||||
* using a logger for the calling class at the specified interval (convenience
|
||||
* method with automatic caller detection)
|
||||
* <li>{@link #loggingWithMemory(String)} - Logs progress and memory statistics
|
||||
* to a logger identified by the given name
|
||||
* <li>{@link #loggingWithMemory(Class)} - Logs progress and memory statistics
|
||||
* to a logger for the given class
|
||||
* <li>{@link #loggingWithMemory(Logger)} - Logs progress and memory statistics
|
||||
* to the provided logger
|
||||
* <li>{@link #loggingWithMemory(String, int)} - Logs progress and memory
|
||||
* statistics at specified intervals
|
||||
* <li>{@link #loggingWithMemory(Class, int)} - Logs progress and memory
|
||||
* statistics at specified intervals
|
||||
* <li>{@link #loggingWithMemory(Logger, int)} - Logs progress and memory
|
||||
* statistics at specified intervals
|
||||
* </ul>
|
||||
* <p>
|
||||
* When using logging callbacks, progress is reported at milestones (every 100
|
||||
* completions, at 25%, 50%, 75%, and 100% completion) to avoid log spam while
|
||||
* providing useful feedback. Progress is logged at TRACE level for minimal
|
||||
* performance impact when trace logging is disabled.
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public final class ProgressCompletion {
|
||||
|
||||
/**
|
||||
* Default interval for logging progress updates. Progress is logged every
|
||||
* {@value #DEFAULT_LOG_INTERVAL} completions.
|
||||
*
|
||||
* @since 0.19
|
||||
*/
|
||||
public static final int DEFAULT_LOG_INTERVAL = 100;
|
||||
|
||||
private ProgressCompletion() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a no-op progress completion callback that does nothing.
|
||||
*
|
||||
* @return a Consumer that ignores all progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> noOp() {
|
||||
return completed -> {
|
||||
// No-op
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress using a logger for
|
||||
* the calling class. This is a convenience method that automatically detects
|
||||
* the caller class.
|
||||
* <p>
|
||||
* Progress is logged at milestones: every {@value #DEFAULT_LOG_INTERVAL}
|
||||
* completions, and at completion milestones (25%, 50%, 75%, 100%).
|
||||
* <p>
|
||||
* <b>Note:</b> Caller detection uses stack trace analysis and may not work
|
||||
* correctly in all environments (e.g., with proxies, AOP frameworks, or certain
|
||||
* JVM optimizations). For maximum reliability, use {@link #logging(Class)} or
|
||||
* {@link #logging(String)} to explicitly specify the logger.
|
||||
*
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging() {
|
||||
return logging(detectCallerClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress using a logger for
|
||||
* the calling class, at the specified interval. This is a convenience method
|
||||
* that automatically detects the caller class.
|
||||
* <p>
|
||||
* Progress is logged every {@code interval} completions, and at completion
|
||||
* milestones (25%, 50%, 75%, 100%).
|
||||
* <p>
|
||||
* <b>Note:</b> Caller detection uses stack trace analysis and may not work
|
||||
* correctly in all environments (e.g., with proxies, AOP frameworks, or certain
|
||||
* JVM optimizations). For maximum reliability, use {@link #logging(Class, int)}
|
||||
* or {@link #logging(String, int)} to explicitly specify the logger.
|
||||
*
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(int interval) {
|
||||
return logging(detectCallerClass(), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to a logger
|
||||
* identified by the given name.
|
||||
* <p>
|
||||
* Progress is logged at milestones: every {@value #DEFAULT_LOG_INTERVAL}
|
||||
* completions, and at completion milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param loggerName the name of the logger to use (e.g., class name)
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(String loggerName) {
|
||||
return logging(LoggerFactory.getLogger(loggerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to a logger for the
|
||||
* given class.
|
||||
* <p>
|
||||
* Progress is logged at milestones: every {@value #DEFAULT_LOG_INTERVAL}
|
||||
* completions, and at completion milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param clazz the class whose logger to use
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(Class<?> clazz) {
|
||||
return logging(LoggerFactory.getLogger(clazz));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to the provided
|
||||
* logger.
|
||||
* <p>
|
||||
* Progress is logged at milestones: every {@value #DEFAULT_LOG_INTERVAL}
|
||||
* completions, and at completion milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param logger the logger to use for progress updates
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(Logger logger) {
|
||||
return logging(logger, DEFAULT_LOG_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to a logger
|
||||
* identified by the given name, at the specified interval.
|
||||
* <p>
|
||||
* Progress is logged every {@code interval} completions, and at completion
|
||||
* milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param loggerName the name of the logger to use (e.g., class name)
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(String loggerName, int interval) {
|
||||
return logging(LoggerFactory.getLogger(loggerName), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to a logger for the
|
||||
* given class, at the specified interval.
|
||||
* <p>
|
||||
* Progress is logged every {@code interval} completions, and at completion
|
||||
* milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param clazz the class whose logger to use
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(Class<?> clazz, int interval) {
|
||||
return logging(LoggerFactory.getLogger(clazz), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress to the provided
|
||||
* logger, at the specified interval.
|
||||
* <p>
|
||||
* Progress is logged every {@code interval} completions, and at completion
|
||||
* milestones (25%, 50%, 75%, 100%).
|
||||
*
|
||||
* @param logger the logger to use for progress updates
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> logging(Logger logger, int interval) {
|
||||
if (logger == null) {
|
||||
throw new IllegalArgumentException("logger must not be null");
|
||||
}
|
||||
if (interval <= 0) {
|
||||
throw new IllegalArgumentException("interval must be positive");
|
||||
}
|
||||
|
||||
return new LoggingProgressCallback(logger, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics using a logger for the calling class. This is a convenience method
|
||||
* that automatically detects the caller class.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged at milestones: every
|
||||
* {@value #DEFAULT_LOG_INTERVAL} completions, and at completion milestones
|
||||
* (25%, 50%, 75%, 100%). Memory statistics include heap used, heap free, and
|
||||
* heap max.
|
||||
* <p>
|
||||
* <b>Note:</b> Caller detection uses stack trace analysis and may not work
|
||||
* correctly in all environments (e.g., with proxies, AOP frameworks, or certain
|
||||
* JVM optimizations). For maximum reliability, use
|
||||
* {@link #loggingWithMemory(Class)} or {@link #loggingWithMemory(String)} to
|
||||
* explicitly specify the logger.
|
||||
*
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory() {
|
||||
return loggingWithMemory(detectCallerClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics using a logger for the calling class, at the specified interval.
|
||||
* This is a convenience method that automatically detects the caller class.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged every {@code interval} completions,
|
||||
* and at completion milestones (25%, 50%, 75%, 100%). Memory statistics include
|
||||
* heap used, heap free, and heap max.
|
||||
* <p>
|
||||
* <b>Note:</b> Caller detection uses stack trace analysis and may not work
|
||||
* correctly in all environments (e.g., with proxies, AOP frameworks, or certain
|
||||
* JVM optimizations). For maximum reliability, use
|
||||
* {@link #loggingWithMemory(Class, int)} or
|
||||
* {@link #loggingWithMemory(String, int)} to explicitly specify the logger.
|
||||
*
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(int interval) {
|
||||
return loggingWithMemory(detectCallerClass(), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to a logger identified by the given name.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged at milestones: every
|
||||
* {@value #DEFAULT_LOG_INTERVAL} completions, and at completion milestones
|
||||
* (25%, 50%, 75%, 100%). Memory statistics include heap used, heap free, and
|
||||
* heap max.
|
||||
*
|
||||
* @param loggerName the name of the logger to use (e.g., class name)
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(String loggerName) {
|
||||
return loggingWithMemory(LoggerFactory.getLogger(loggerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to a logger for the given class.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged at milestones: every
|
||||
* {@value #DEFAULT_LOG_INTERVAL} completions, and at completion milestones
|
||||
* (25%, 50%, 75%, 100%). Memory statistics include heap used, heap free, and
|
||||
* heap max.
|
||||
*
|
||||
* @param clazz the class whose logger to use
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(Class<?> clazz) {
|
||||
return loggingWithMemory(LoggerFactory.getLogger(clazz));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to the provided logger.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged at milestones: every
|
||||
* {@value #DEFAULT_LOG_INTERVAL} completions, and at completion milestones
|
||||
* (25%, 50%, 75%, 100%). Memory statistics include heap used, heap free, and
|
||||
* heap max.
|
||||
*
|
||||
* @param logger the logger to use for progress updates
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(Logger logger) {
|
||||
return loggingWithMemory(logger, DEFAULT_LOG_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to a logger identified by the given name, at the specified
|
||||
* interval.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged every {@code interval} completions,
|
||||
* and at completion milestones (25%, 50%, 75%, 100%). Memory statistics include
|
||||
* heap used, heap free, and heap max.
|
||||
*
|
||||
* @param loggerName the name of the logger to use (e.g., class name)
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(String loggerName, int interval) {
|
||||
return loggingWithMemory(LoggerFactory.getLogger(loggerName), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to a logger for the given class, at the specified interval.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged every {@code interval} completions,
|
||||
* and at completion milestones (25%, 50%, 75%, 100%). Memory statistics include
|
||||
* heap used, heap free, and heap max.
|
||||
*
|
||||
* @param clazz the class whose logger to use
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(Class<?> clazz, int interval) {
|
||||
return loggingWithMemory(LoggerFactory.getLogger(clazz), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a progress completion callback that logs progress and memory
|
||||
* statistics to the provided logger, at the specified interval.
|
||||
* <p>
|
||||
* Progress and memory statistics are logged every {@code interval} completions,
|
||||
* and at completion milestones (25%, 50%, 75%, 100%). Memory statistics include
|
||||
* heap used, heap free, and heap max.
|
||||
*
|
||||
* @param logger the logger to use for progress updates
|
||||
* @param interval the number of completions between log messages
|
||||
* @return a Consumer that logs progress updates with memory statistics
|
||||
* @since 0.19
|
||||
*/
|
||||
public static Consumer<Integer> loggingWithMemory(Logger logger, int interval) {
|
||||
if (logger == null) {
|
||||
throw new IllegalArgumentException("logger must not be null");
|
||||
}
|
||||
if (interval <= 0) {
|
||||
throw new IllegalArgumentException("interval must be positive");
|
||||
}
|
||||
|
||||
return new LoggingWithMemoryProgressCallback(logger, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the calling class by analyzing the stack trace. This method skips
|
||||
* internal frames (ProgressCompletion, Thread, etc.) to find the actual caller.
|
||||
*
|
||||
* @return the calling class, or ProgressCompletion.class as a fallback
|
||||
*/
|
||||
private static Class<?> detectCallerClass() {
|
||||
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
|
||||
String thisClassName = ProgressCompletion.class.getName();
|
||||
|
||||
// Start from index 3 to skip:
|
||||
// 0: getStackTrace
|
||||
// 1: detectCallerClass
|
||||
// 2: logging()/logging(int)/loggingWithMemory()/loggingWithMemory(int)
|
||||
for (int i = 3; i < stack.length; i++) {
|
||||
String className = stack[i].getClassName();
|
||||
|
||||
// Skip internal frames
|
||||
if (className.equals(thisClassName) || className.equals(Thread.class.getName())
|
||||
|| className.startsWith("java.lang.reflect.") || className.startsWith("sun.reflect.")
|
||||
|| className.startsWith("jdk.internal.reflect.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Continue searching if class can't be loaded
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to ProgressCompletion if we can't detect the caller
|
||||
return ProgressCompletion.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats memory size in bytes to a human-readable string (e.g., "512 MB").
|
||||
*
|
||||
* @param bytes the size in bytes
|
||||
* @return formatted string
|
||||
*/
|
||||
private static String formatMemory(long bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.1f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of logging progress callback.
|
||||
*/
|
||||
private static final class LoggingProgressCallback implements Consumer<Integer> {
|
||||
|
||||
private final Logger logger;
|
||||
private final int interval;
|
||||
private volatile int totalStrategies = -1;
|
||||
|
||||
private LoggingProgressCallback(Logger logger, int interval) {
|
||||
this.logger = logger;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Integer completed) {
|
||||
// Early exit if trace logging is not enabled to avoid unnecessary calculations
|
||||
if (!logger.isTraceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int total = totalStrategies;
|
||||
|
||||
// If we don't know the total yet, just log at interval milestones
|
||||
if (total < 0) {
|
||||
if (completed % interval == 0) {
|
||||
logger.trace("Progress: {} strategies completed", completed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log at interval milestones
|
||||
if (completed % interval == 0) {
|
||||
int percentComplete = (int) (completed * 100.0) / total;
|
||||
logger.trace("Progress: {}/{} strategies completed ({}%)", completed, total, percentComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log at completion milestones (25%, 50%, 75%, 100%)
|
||||
int percentComplete = (int) (completed * 100.0) / total;
|
||||
int prevPercent = completed > 1 ? (int) ((completed - 1) * 100.0) / total : 0;
|
||||
|
||||
if (percentComplete >= 25 && prevPercent < 25) {
|
||||
logger.trace("Progress: {}/{} strategies completed (25%)", completed, total);
|
||||
} else if (percentComplete >= 50 && prevPercent < 50) {
|
||||
logger.trace("Progress: {}/{} strategies completed (50%)", completed, total);
|
||||
} else if (percentComplete >= 75 && prevPercent < 75) {
|
||||
logger.trace("Progress: {}/{} strategies completed (75%)", completed, total);
|
||||
} else if (completed == total) {
|
||||
logger.trace("Progress: {}/{} strategies completed (100%)", completed, total);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of strategies for percentage calculation. This is
|
||||
* called internally by BacktestExecutor.
|
||||
*
|
||||
* @param total the total number of strategies
|
||||
*/
|
||||
void setTotalStrategies(int total) {
|
||||
this.totalStrategies = total;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of logging progress callback with memory statistics.
|
||||
*/
|
||||
private static final class LoggingWithMemoryProgressCallback implements Consumer<Integer> {
|
||||
|
||||
private final Logger logger;
|
||||
private final int interval;
|
||||
private volatile int totalStrategies = -1;
|
||||
|
||||
private LoggingWithMemoryProgressCallback(Logger logger, int interval) {
|
||||
this.logger = logger;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Integer completed) {
|
||||
// Early exit if trace logging is not enabled to avoid unnecessary calculations
|
||||
if (!logger.isTraceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int total = totalStrategies;
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long totalMemory = runtime.totalMemory();
|
||||
long freeMemory = runtime.freeMemory();
|
||||
long usedMemory = totalMemory - freeMemory;
|
||||
long maxMemory = runtime.maxMemory();
|
||||
|
||||
String memoryInfo = String.format("Heap: %s/%s used (%.1f%%)", formatMemory(usedMemory),
|
||||
formatMemory(maxMemory), (usedMemory * 100.0) / maxMemory);
|
||||
|
||||
// If we don't know the total yet, just log at interval milestones
|
||||
if (total < 0) {
|
||||
if (completed % interval == 0) {
|
||||
logger.trace("Progress: {} strategies completed | {}", completed, memoryInfo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log at interval milestones
|
||||
if (completed % interval == 0) {
|
||||
int percentComplete = (int) (completed * 100.0) / total;
|
||||
logger.trace("Progress: {}/{} strategies completed ({}%) | {}", completed, total, percentComplete,
|
||||
memoryInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log at completion milestones (25%, 50%, 75%, 100%)
|
||||
int percentComplete = (int) (completed * 100.0) / total;
|
||||
int prevPercent = completed > 1 ? (int) ((completed - 1) * 100.0) / total : 0;
|
||||
|
||||
if (percentComplete >= 25 && prevPercent < 25) {
|
||||
logger.trace("Progress: {}/{} strategies completed (25%) | {}", completed, total, memoryInfo);
|
||||
} else if (percentComplete >= 50 && prevPercent < 50) {
|
||||
logger.trace("Progress: {}/{} strategies completed (50%) | {}", completed, total, memoryInfo);
|
||||
} else if (percentComplete >= 75 && prevPercent < 75) {
|
||||
logger.trace("Progress: {}/{} strategies completed (75%) | {}", completed, total, memoryInfo);
|
||||
} else if (completed == total) {
|
||||
logger.trace("Progress: {}/{} strategies completed (100%) | {}", completed, total, memoryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of strategies for percentage calculation. This is
|
||||
* called internally by BacktestExecutor.
|
||||
*
|
||||
* @param total the total number of strategies
|
||||
*/
|
||||
void setTotalStrategies(int total) {
|
||||
this.totalStrategies = total;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a progress callback with the total strategies count. This allows
|
||||
* percentage-based logging to work correctly for logging callbacks.
|
||||
* <p>
|
||||
* This method only affects logging callbacks ({@link LoggingProgressCallback}
|
||||
* and {@link LoggingWithMemoryProgressCallback}). For other callback types, the
|
||||
* callback is returned unchanged and the {@code totalStrategies} parameter is
|
||||
* ignored.
|
||||
*
|
||||
* @param callback the progress callback to configure (must not be null)
|
||||
* @param totalStrategies the total number of strategies
|
||||
* @return the same callback instance (configured if it's a logging callback)
|
||||
* @throws IllegalArgumentException if callback is null
|
||||
*/
|
||||
static Consumer<Integer> withTotalStrategies(Consumer<Integer> callback, int totalStrategies) {
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("callback must not be null");
|
||||
}
|
||||
if (callback instanceof LoggingProgressCallback) {
|
||||
((LoggingProgressCallback) callback).setTotalStrategies(totalStrategies);
|
||||
} else if (callback instanceof LoggingWithMemoryProgressCallback) {
|
||||
((LoggingWithMemoryProgressCallback) callback).setTotalStrategies(totalStrategies);
|
||||
}
|
||||
// For other callback types, return unchanged (totalStrategies is ignored)
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Trade.TradeType;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.backtest.ExecutionModelSupport.ExecutionTarget;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Execution model that applies configurable slippage to each trade.
|
||||
*
|
||||
* <p>
|
||||
* Buy orders are filled at a worse price ({@code +slippage}), while sell orders
|
||||
* are filled at a worse price ({@code -slippage}).
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class SlippageExecutionModel implements TradeExecutionModel {
|
||||
|
||||
private final Num slippageRatio;
|
||||
private final PriceSource priceSource;
|
||||
|
||||
/**
|
||||
* Creates a slippage execution model based on next-bar open prices.
|
||||
*
|
||||
* @param slippageRatio slippage ratio (for example 0.001 for 10 bps)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public SlippageExecutionModel(Num slippageRatio) {
|
||||
this(slippageRatio, PriceSource.NEXT_OPEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a slippage execution model.
|
||||
*
|
||||
* @param slippageRatio slippage ratio in [0,1)
|
||||
* @param priceSource base price source
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public SlippageExecutionModel(Num slippageRatio, PriceSource priceSource) {
|
||||
Objects.requireNonNull(slippageRatio, "slippageRatio");
|
||||
Objects.requireNonNull(priceSource, "priceSource");
|
||||
if (slippageRatio.isNaN() || slippageRatio.isNegative()) {
|
||||
throw new IllegalArgumentException("slippageRatio must be positive or zero");
|
||||
}
|
||||
Num one = slippageRatio.getNumFactory().one();
|
||||
if (slippageRatio.isGreaterThanOrEqual(one)) {
|
||||
throw new IllegalArgumentException("slippageRatio must be less than 1");
|
||||
}
|
||||
this.slippageRatio = slippageRatio;
|
||||
this.priceSource = priceSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return configured slippage ratio
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Num getSlippageRatio() {
|
||||
return slippageRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return configured base execution price source
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public PriceSource getPriceSource() {
|
||||
return priceSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
ExecutionTarget executionTarget = ExecutionModelSupport.resolveExecutionTarget(index, barSeries, priceSource);
|
||||
if (executionTarget == null) {
|
||||
return;
|
||||
}
|
||||
TradeType tradeType = ExecutionModelSupport.nextTradeType(tradingRecord);
|
||||
Num slippedPrice = applySlippage(executionTarget.price(), tradeType, slippageRatio);
|
||||
tradingRecord.operate(executionTarget.index(), slippedPrice, amount);
|
||||
}
|
||||
|
||||
private static Num applySlippage(Num price, TradeType tradeType, Num slippageRatio) {
|
||||
Num one = price.getNumFactory().one();
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return price.multipliedBy(one.plus(slippageRatio));
|
||||
}
|
||||
return price.multipliedBy(one.minus(slippageRatio));
|
||||
}
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
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.TradeFill;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.backtest.ExecutionModelSupport.ExecutionTarget;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* Stop-limit execution model with partial fill progression.
|
||||
*
|
||||
* <p>
|
||||
* Strategy signals place stop-limit orders. Pending orders are evaluated on
|
||||
* each bar through {@link #onBar(int, TradingRecord, BarSeries)} and can be
|
||||
* filled progressively based on available bar volume participation. Partially
|
||||
* filled orders are committed on expiry when the target trading record supports
|
||||
* partial-exit accounting. Generated fills include execution side and bar end
|
||||
* timestamps so backtest fills match live-fill metadata shape.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class StopLimitExecutionModel implements TradeExecutionModel {
|
||||
|
||||
private final Num stopTriggerRatio;
|
||||
private final Num limitOffsetRatio;
|
||||
private final Num maxBarParticipationRate;
|
||||
private final int maxBarsToFill;
|
||||
private final PriceSource priceSource;
|
||||
|
||||
private final Map<TradingRecord, PendingOrder> pendingOrders = new WeakHashMap<>();
|
||||
private final Map<TradingRecord, List<RejectedOrder>> rejectedOrders = new WeakHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a stop-limit execution model using next-bar open as the reference
|
||||
* price.
|
||||
*
|
||||
* @param stopTriggerRatio stop trigger ratio in [0,1)
|
||||
* @param limitOffsetRatio limit offset ratio in [0,1)
|
||||
* @param maxBarParticipation max per-bar fill participation in (0,1]
|
||||
* @param maxBarsToFill order time-to-live in bars (>= 1)
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StopLimitExecutionModel(Num stopTriggerRatio, Num limitOffsetRatio, Num maxBarParticipation,
|
||||
int maxBarsToFill) {
|
||||
this(stopTriggerRatio, limitOffsetRatio, maxBarParticipation, maxBarsToFill, PriceSource.NEXT_OPEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stop-limit execution model.
|
||||
*
|
||||
* @param stopTriggerRatio stop trigger ratio in [0,1)
|
||||
* @param limitOffsetRatio limit offset ratio in [0,1) (must be >= stop
|
||||
* ratio)
|
||||
* @param maxBarParticipation max per-bar fill participation in (0,1]
|
||||
* @param maxBarsToFill order time-to-live in bars (>= 1)
|
||||
* @param priceSource base signal price source
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StopLimitExecutionModel(Num stopTriggerRatio, Num limitOffsetRatio, Num maxBarParticipation,
|
||||
int maxBarsToFill, PriceSource priceSource) {
|
||||
validateRatio(stopTriggerRatio, "stopTriggerRatio");
|
||||
validateRatio(limitOffsetRatio, "limitOffsetRatio");
|
||||
validateRatio(maxBarParticipation, "maxBarParticipation");
|
||||
Objects.requireNonNull(priceSource, "priceSource");
|
||||
Num one = stopTriggerRatio.getNumFactory().one();
|
||||
if (stopTriggerRatio.isGreaterThanOrEqual(one)) {
|
||||
throw new IllegalArgumentException("stopTriggerRatio must be < 1");
|
||||
}
|
||||
if (limitOffsetRatio.isGreaterThanOrEqual(one)) {
|
||||
throw new IllegalArgumentException("limitOffsetRatio must be < 1");
|
||||
}
|
||||
if (maxBarParticipation.isZero()) {
|
||||
throw new IllegalArgumentException("maxBarParticipation must be > 0");
|
||||
}
|
||||
if (maxBarParticipation.isGreaterThan(one)) {
|
||||
throw new IllegalArgumentException("maxBarParticipation must be <= 1");
|
||||
}
|
||||
if (limitOffsetRatio.isLessThan(stopTriggerRatio)) {
|
||||
throw new IllegalArgumentException("limitOffsetRatio must be >= stopTriggerRatio");
|
||||
}
|
||||
if (maxBarsToFill < 1) {
|
||||
throw new IllegalArgumentException("maxBarsToFill must be >= 1");
|
||||
}
|
||||
this.stopTriggerRatio = stopTriggerRatio;
|
||||
this.limitOffsetRatio = limitOffsetRatio;
|
||||
this.maxBarParticipationRate = maxBarParticipation;
|
||||
this.maxBarsToFill = maxBarsToFill;
|
||||
this.priceSource = priceSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rejected orders for a trading record.
|
||||
*
|
||||
* @param tradingRecord trading record
|
||||
* @return rejected orders
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<RejectedOrder> getRejectedOrders(TradingRecord tradingRecord) {
|
||||
List<RejectedOrder> rejected = rejectedOrders.get(tradingRecord);
|
||||
return rejected == null ? List.of() : List.copyOf(rejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current pending order snapshot for the trading record.
|
||||
*
|
||||
* @param tradingRecord trading record
|
||||
* @return pending order snapshot
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Optional<PendingOrderSnapshot> getPendingOrder(TradingRecord tradingRecord) {
|
||||
PendingOrder order = pendingOrders.get(tradingRecord);
|
||||
if (order == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(order.snapshot());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
Objects.requireNonNull(tradingRecord, "tradingRecord");
|
||||
Objects.requireNonNull(barSeries, "barSeries");
|
||||
expireIfStale(index, tradingRecord);
|
||||
if (amount == null || amount.isNaN() || amount.isZero() || amount.isNegative()) {
|
||||
Num requestedAmount = amountOrZero(amount, barSeries);
|
||||
addRejectedOrder(tradingRecord,
|
||||
new RejectedOrder(index, index, ExecutionModelSupport.nextTradeType(tradingRecord), requestedAmount,
|
||||
requestedAmount.getNumFactory().zero(), "Invalid requested amount"));
|
||||
return;
|
||||
}
|
||||
Num requestedAmount = resolveRequestedAmount(tradingRecord, amount);
|
||||
PendingOrder pendingOrder = pendingOrders.get(tradingRecord);
|
||||
if (pendingOrder != null) {
|
||||
addRejectedOrder(tradingRecord,
|
||||
new RejectedOrder(index, index, pendingOrder.tradeType, requestedAmount,
|
||||
requestedAmount.getNumFactory().zero(),
|
||||
"Signal ignored while another stop-limit order is pending"));
|
||||
return;
|
||||
}
|
||||
ExecutionTarget referenceTarget = ExecutionModelSupport.resolveExecutionTarget(index, barSeries, priceSource);
|
||||
if (referenceTarget == null) {
|
||||
addRejectedOrder(tradingRecord,
|
||||
new RejectedOrder(index, index, ExecutionModelSupport.nextTradeType(tradingRecord), requestedAmount,
|
||||
requestedAmount.getNumFactory().zero(),
|
||||
"Unable to resolve reference bar for stop-limit order"));
|
||||
return;
|
||||
}
|
||||
|
||||
TradeType tradeType = ExecutionModelSupport.nextTradeType(tradingRecord);
|
||||
Num stopPrice = toStopPrice(referenceTarget.price(), tradeType);
|
||||
Num limitPrice = toLimitPrice(referenceTarget.price(), tradeType);
|
||||
int activationIndex = resolveActivationIndex(referenceTarget.index());
|
||||
if (activationIndex > barSeries.getEndIndex()) {
|
||||
addRejectedOrder(tradingRecord, new RejectedOrder(index, index, tradeType, requestedAmount,
|
||||
requestedAmount.getNumFactory().zero(), "Unable to resolve activation bar for stop-limit order"));
|
||||
return;
|
||||
}
|
||||
pendingOrders.put(tradingRecord, new PendingOrder(index, activationIndex, tradeType, requestedAmount, stopPrice,
|
||||
limitPrice, activationIndex + maxBarsToFill - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBar(int index, TradingRecord tradingRecord, BarSeries barSeries) {
|
||||
PendingOrder order = pendingOrders.get(tradingRecord);
|
||||
if (order == null || index < order.activationIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bar bar = barSeries.getBar(index);
|
||||
if (!order.triggered) {
|
||||
order.triggered = triggerReached(order.tradeType, bar, order.stopPrice);
|
||||
}
|
||||
|
||||
if (order.triggered && limitReachable(order.tradeType, bar, order.limitPrice)) {
|
||||
Num fillAmount = fillAmount(order.remainingAmount(), bar.getVolume());
|
||||
if (fillAmount.isPositive()) {
|
||||
order.recordFill(index, bar, order.limitPrice, fillAmount);
|
||||
}
|
||||
}
|
||||
|
||||
if (order.isCompletelyFilled()) {
|
||||
tradingRecord.operate(order.toTrade(tradingRecord));
|
||||
pendingOrders.remove(tradingRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= order.expiryIndex) {
|
||||
expireOrder(index, tradingRecord, order);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRunEnd(int lastProcessedIndex, TradingRecord tradingRecord) {
|
||||
PendingOrder order = pendingOrders.get(tradingRecord);
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
expireOrder(lastProcessedIndex, tradingRecord, order);
|
||||
}
|
||||
|
||||
private void expireIfStale(int index, TradingRecord tradingRecord) {
|
||||
PendingOrder order = pendingOrders.get(tradingRecord);
|
||||
if (order == null || index <= order.expiryIndex) {
|
||||
return;
|
||||
}
|
||||
expireOrder(index, tradingRecord, order);
|
||||
}
|
||||
|
||||
private void expireOrder(int index, TradingRecord tradingRecord, PendingOrder order) {
|
||||
if (shouldCommitPartial(order, tradingRecord)) {
|
||||
tradingRecord.operate(order.toTrade(tradingRecord));
|
||||
}
|
||||
addRejectedOrder(tradingRecord, order.toExpiryRejection(index));
|
||||
pendingOrders.remove(tradingRecord);
|
||||
}
|
||||
|
||||
private static boolean shouldCommitPartial(PendingOrder order, TradingRecord tradingRecord) {
|
||||
if (!order.hasAnyFill()) {
|
||||
return false;
|
||||
}
|
||||
if (order.tradeType == tradingRecord.getStartingType()) {
|
||||
return true;
|
||||
}
|
||||
return !tradingRecord.getOpenPositions().isEmpty();
|
||||
}
|
||||
|
||||
private static void validateRatio(Num ratio, String name) {
|
||||
Objects.requireNonNull(ratio, name);
|
||||
if (ratio.isNaN() || ratio.isNegative()) {
|
||||
throw new IllegalArgumentException(name + " must be positive or zero");
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveActivationIndex(int referenceIndex) {
|
||||
if (priceSource == PriceSource.CURRENT_CLOSE) {
|
||||
return referenceIndex + 1;
|
||||
}
|
||||
return referenceIndex;
|
||||
}
|
||||
|
||||
private Num toStopPrice(Num reference, TradeType tradeType) {
|
||||
Num one = reference.getNumFactory().one();
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return reference.multipliedBy(one.plus(stopTriggerRatio));
|
||||
}
|
||||
return reference.multipliedBy(one.minus(stopTriggerRatio));
|
||||
}
|
||||
|
||||
private Num toLimitPrice(Num reference, TradeType tradeType) {
|
||||
Num one = reference.getNumFactory().one();
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return reference.multipliedBy(one.plus(limitOffsetRatio));
|
||||
}
|
||||
return reference.multipliedBy(one.minus(limitOffsetRatio));
|
||||
}
|
||||
|
||||
private Num fillAmount(Num remainingAmount, Num barVolume) {
|
||||
Num availableAmount = remainingAmount;
|
||||
if (!Num.isNaNOrNull(barVolume)) {
|
||||
if (!barVolume.isPositive()) {
|
||||
return remainingAmount.getNumFactory().zero();
|
||||
}
|
||||
availableAmount = barVolume.multipliedBy(maxBarParticipationRate);
|
||||
}
|
||||
if (availableAmount.isNaN() || availableAmount.isNegativeOrZero()) {
|
||||
return remainingAmount.getNumFactory().zero();
|
||||
}
|
||||
if (availableAmount.isGreaterThan(remainingAmount)) {
|
||||
return remainingAmount;
|
||||
}
|
||||
return availableAmount;
|
||||
}
|
||||
|
||||
private static boolean triggerReached(TradeType tradeType, Bar bar, Num stopPrice) {
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return bar.getHighPrice().isGreaterThanOrEqual(stopPrice);
|
||||
}
|
||||
return bar.getLowPrice().isLessThanOrEqual(stopPrice);
|
||||
}
|
||||
|
||||
private static boolean limitReachable(TradeType tradeType, Bar bar, Num limitPrice) {
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return bar.getLowPrice().isLessThanOrEqual(limitPrice);
|
||||
}
|
||||
return bar.getHighPrice().isGreaterThanOrEqual(limitPrice);
|
||||
}
|
||||
|
||||
private static Num amountOrZero(Num amount, BarSeries barSeries) {
|
||||
if (amount == null || amount.isNaN()) {
|
||||
return barSeries.numFactory().zero();
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
private static Num resolveRequestedAmount(TradingRecord tradingRecord, Num defaultAmount) {
|
||||
if (tradingRecord.isClosed()) {
|
||||
return defaultAmount;
|
||||
}
|
||||
Position currentPosition = tradingRecord.getCurrentPosition();
|
||||
if (currentPosition.isOpened() && currentPosition.getEntry() != null
|
||||
&& currentPosition.getEntry().getAmount() != null && !currentPosition.getEntry().getAmount().isNaN()) {
|
||||
return currentPosition.getEntry().getAmount();
|
||||
}
|
||||
return defaultAmount;
|
||||
}
|
||||
|
||||
private void addRejectedOrder(TradingRecord tradingRecord, RejectedOrder rejection) {
|
||||
rejectedOrders.computeIfAbsent(tradingRecord, ignored -> new ArrayList<>()).add(rejection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejected stop-limit order metadata.
|
||||
*
|
||||
* @param signalIndex strategy signal index
|
||||
* @param rejectionIndex bar index where rejection happened
|
||||
* @param tradeType trade side
|
||||
* @param requestedAmount requested amount
|
||||
* @param filledAmount amount filled before rejection
|
||||
* @param reason rejection reason
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record RejectedOrder(int signalIndex, int rejectionIndex, TradeType tradeType, Num requestedAmount,
|
||||
Num filledAmount, String reason) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of a pending stop-limit order.
|
||||
*
|
||||
* @param signalIndex strategy signal index
|
||||
* @param activationIndex first index where order can execute
|
||||
* @param tradeType order side
|
||||
* @param requestedAmount requested amount
|
||||
* @param filledAmount filled amount
|
||||
* @param stopPrice stop trigger price
|
||||
* @param limitPrice limit price
|
||||
* @param expiryIndex last fillable bar index
|
||||
* @param triggered true if stop trigger was reached
|
||||
* @param fills current fills
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record PendingOrderSnapshot(int signalIndex, int activationIndex, TradeType tradeType, Num requestedAmount,
|
||||
Num filledAmount, Num stopPrice, Num limitPrice, int expiryIndex, boolean triggered,
|
||||
List<TradeFill> fills) {
|
||||
}
|
||||
|
||||
private static final class PendingOrder {
|
||||
private final int signalIndex;
|
||||
private final int activationIndex;
|
||||
private final TradeType tradeType;
|
||||
private final Num requestedAmount;
|
||||
private final Num stopPrice;
|
||||
private final Num limitPrice;
|
||||
private final int expiryIndex;
|
||||
private boolean triggered;
|
||||
private Num filledAmount;
|
||||
private final List<TradeFill> fills;
|
||||
|
||||
private PendingOrder(int signalIndex, int activationIndex, TradeType tradeType, Num requestedAmount,
|
||||
Num stopPrice, Num limitPrice, int expiryIndex) {
|
||||
this.signalIndex = signalIndex;
|
||||
this.activationIndex = activationIndex;
|
||||
this.tradeType = tradeType;
|
||||
this.requestedAmount = requestedAmount;
|
||||
this.stopPrice = stopPrice;
|
||||
this.limitPrice = limitPrice;
|
||||
this.expiryIndex = expiryIndex;
|
||||
this.triggered = false;
|
||||
this.filledAmount = requestedAmount.getNumFactory().zero();
|
||||
this.fills = new ArrayList<>();
|
||||
}
|
||||
|
||||
private Num remainingAmount() {
|
||||
return requestedAmount.minus(filledAmount);
|
||||
}
|
||||
|
||||
private void recordFill(int index, Bar bar, Num price, Num amount) {
|
||||
fills.add(new TradeFill(index, bar.getEndTime(), price, amount, sideOf(tradeType)));
|
||||
filledAmount = filledAmount.plus(amount);
|
||||
}
|
||||
|
||||
private boolean isCompletelyFilled() {
|
||||
return !requestedAmount.minus(filledAmount).isPositive();
|
||||
}
|
||||
|
||||
private boolean hasAnyFill() {
|
||||
return filledAmount.isPositive();
|
||||
}
|
||||
|
||||
private Trade toTrade(TradingRecord tradingRecord) {
|
||||
return Trade.fromFills(tradeType, fills, tradingRecord.getTransactionCostModel());
|
||||
}
|
||||
|
||||
private RejectedOrder toExpiryRejection(int rejectionIndex) {
|
||||
return new RejectedOrder(signalIndex, rejectionIndex, tradeType, requestedAmount, filledAmount,
|
||||
"Stop-limit order expired before filling requested amount");
|
||||
}
|
||||
|
||||
private PendingOrderSnapshot snapshot() {
|
||||
return new PendingOrderSnapshot(signalIndex, activationIndex, tradeType, requestedAmount, filledAmount,
|
||||
stopPrice, limitPrice, expiryIndex, triggered, List.copyOf(fills));
|
||||
}
|
||||
|
||||
private static ExecutionSide sideOf(TradeType tradeType) {
|
||||
if (tradeType == TradeType.BUY) {
|
||||
return ExecutionSide.BUY;
|
||||
}
|
||||
return ExecutionSide.SELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Strategy;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
import org.ta4j.core.walkforward.WalkForwardRuntimeReport;
|
||||
import org.ta4j.core.walkforward.WalkForwardSplit;
|
||||
|
||||
/**
|
||||
* Wraps walk-forward execution output for one strategy.
|
||||
*
|
||||
* @param barSeries series used for execution
|
||||
* @param strategy evaluated strategy
|
||||
* @param config walk-forward configuration
|
||||
* @param folds fold-level execution results
|
||||
* @param runtimeReport aggregate runtime report across folds
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record StrategyWalkForwardExecutionResult(BarSeries barSeries, Strategy strategy, WalkForwardConfig config,
|
||||
List<FoldResult> folds,
|
||||
WalkForwardRuntimeReport runtimeReport) implements TradingStatementExecutionResult<WalkForwardRuntimeReport> {
|
||||
|
||||
/**
|
||||
* Creates a validated result.
|
||||
*
|
||||
* @param barSeries series used for execution
|
||||
* @param strategy evaluated strategy
|
||||
* @param config walk-forward configuration
|
||||
* @param folds fold-level execution results
|
||||
* @param runtimeReport aggregate runtime report across folds
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult {
|
||||
barSeries = Objects.requireNonNull(barSeries, "barSeries");
|
||||
strategy = Objects.requireNonNull(strategy, "strategy");
|
||||
config = Objects.requireNonNull(config, "config");
|
||||
folds = List.copyOf(Objects.requireNonNull(folds, "folds"));
|
||||
runtimeReport = Objects.requireNonNull(runtimeReport, "runtimeReport");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the optional holdout fold execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Optional<FoldResult> holdoutFold() {
|
||||
return folds.stream().filter(fold -> fold.split().holdout()).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all non-holdout folds
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<FoldResult> inSampleFolds() {
|
||||
return folds.stream().filter(fold -> !fold.split().holdout()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all holdout folds
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<FoldResult> outOfSampleFolds() {
|
||||
return folds.stream().filter(fold -> fold.split().holdout()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return fold trading statements in execution order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Override
|
||||
public List<TradingStatement> tradingStatements() {
|
||||
return folds.stream().map(FoldResult::tradingStatement).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return fold trading records in execution order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Override
|
||||
public List<TradingRecord> tradingRecords() {
|
||||
return TradingStatementExecutionResult.super.tradingRecords();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion for every fold.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return criterion values in fold execution order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@Override
|
||||
public List<Num> criterionValues(AnalysisCriterion criterion) {
|
||||
return TradingStatementExecutionResult.super.criterionValues(criterion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion for every in-sample fold.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return criterion values in fold execution order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<Num> inSampleCriterionValues(AnalysisCriterion criterion) {
|
||||
return criterionValuesFor(criterion, inSampleFolds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion for every out-of-sample fold.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return criterion values in fold execution order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public List<Num> outOfSampleCriterionValues(AnalysisCriterion criterion) {
|
||||
return criterionValuesFor(criterion, outOfSampleFolds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion for the holdout fold when present.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return optional criterion value for the holdout fold
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Optional<Num> holdoutCriterionValue(AnalysisCriterion criterion) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
return holdoutFold().map(fold -> criterion.calculate(barSeries, fold.tradingRecord()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion and returns values keyed by fold id.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return ordered fold-id to criterion value map
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public Map<String, Num> criterionValuesByFold(AnalysisCriterion criterion) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
Map<String, Num> values = new LinkedHashMap<>();
|
||||
for (FoldResult fold : folds) {
|
||||
values.put(fold.split().foldId(), criterion.calculate(barSeries, fold.tradingRecord()));
|
||||
}
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
private List<Num> criterionValuesFor(AnalysisCriterion criterion, List<FoldResult> selectedFolds) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
return selectedFolds.stream().map(fold -> criterion.calculate(barSeries, fold.tradingRecord())).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold-level walk-forward execution output.
|
||||
*
|
||||
* @param split fold boundary metadata
|
||||
* @param tradingRecord generated trading record for the fold's test window
|
||||
* @param tradingStatement generated trading statement for the fold's test
|
||||
* window
|
||||
* @param runtime fold runtime duration
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public record FoldResult(WalkForwardSplit split, TradingRecord tradingRecord, TradingStatement tradingStatement,
|
||||
Duration runtime) {
|
||||
|
||||
/**
|
||||
* Creates a validated fold result.
|
||||
*
|
||||
* @param split fold boundary metadata
|
||||
* @param tradingRecord generated trading record for the fold
|
||||
* @param tradingStatement generated trading statement for the fold
|
||||
* @param runtime fold runtime duration
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public FoldResult {
|
||||
split = Objects.requireNonNull(split, "split");
|
||||
tradingRecord = Objects.requireNonNull(tradingRecord, "tradingRecord");
|
||||
tradingStatement = Objects.requireNonNull(tradingStatement, "tradingStatement");
|
||||
runtime = Objects.requireNonNull(runtime, "runtime");
|
||||
if (runtime.isNegative()) {
|
||||
throw new IllegalArgumentException("runtime must be >= 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
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.ZeroCostModel;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
import org.ta4j.core.reports.TradingStatementGenerator;
|
||||
import org.ta4j.core.walkforward.AnchoredExpandingWalkForwardSplitter;
|
||||
import org.ta4j.core.walkforward.WalkForwardConfig;
|
||||
import org.ta4j.core.walkforward.WalkForwardRuntimeReport;
|
||||
import org.ta4j.core.walkforward.WalkForwardSplit;
|
||||
import org.ta4j.core.walkforward.WalkForwardSplitter;
|
||||
|
||||
/**
|
||||
* Executes one strategy in walk-forward mode with a backtest-symmetric API.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public class StrategyWalkForwardExecutor {
|
||||
|
||||
private final BarSeriesManager seriesManager;
|
||||
private final TradingStatementGenerator tradingStatementGenerator;
|
||||
private final WalkForwardSplitter splitter;
|
||||
|
||||
/**
|
||||
* Creates an executor with default cost and trade execution models.
|
||||
*
|
||||
* @param series input bar series
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutor(BarSeries series) {
|
||||
this(series, new TradingStatementGenerator(), new ZeroCostModel(), new ZeroCostModel(),
|
||||
new TradeOnNextOpenModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executor with explicit cost and trade execution models.
|
||||
*
|
||||
* @param series input bar series
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @param holdingCostModel holding cost model
|
||||
* @param tradeExecutionModel trade execution model
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutor(BarSeries series, CostModel transactionCostModel, CostModel holdingCostModel,
|
||||
TradeExecutionModel tradeExecutionModel) {
|
||||
this(series, new TradingStatementGenerator(), transactionCostModel, holdingCostModel, tradeExecutionModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executor with explicit statement generator.
|
||||
*
|
||||
* @param series input bar series
|
||||
* @param tradingStatementGenerator trading statement generator
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator) {
|
||||
this(series, tradingStatementGenerator, new ZeroCostModel(), new ZeroCostModel(), new TradeOnNextOpenModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executor with explicit statement generator, cost models, and trade
|
||||
* execution model.
|
||||
*
|
||||
* @param series input bar series
|
||||
* @param tradingStatementGenerator trading statement generator
|
||||
* @param transactionCostModel transaction cost model
|
||||
* @param holdingCostModel holding cost model
|
||||
* @param tradeExecutionModel trade execution model
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator,
|
||||
CostModel transactionCostModel, CostModel holdingCostModel, TradeExecutionModel tradeExecutionModel) {
|
||||
this(new BarSeriesManager(series, transactionCostModel, holdingCostModel, tradeExecutionModel),
|
||||
tradingStatementGenerator, new AnchoredExpandingWalkForwardSplitter());
|
||||
}
|
||||
|
||||
StrategyWalkForwardExecutor(BarSeriesManager seriesManager, TradingStatementGenerator tradingStatementGenerator,
|
||||
WalkForwardSplitter splitter) {
|
||||
this.seriesManager = Objects.requireNonNull(seriesManager, "seriesManager");
|
||||
this.tradingStatementGenerator = Objects.requireNonNull(tradingStatementGenerator, "tradingStatementGenerator");
|
||||
this.splitter = Objects.requireNonNull(splitter, "splitter");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing using strategy starting trade type and unit
|
||||
* amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param config walk-forward configuration
|
||||
* @return execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult execute(Strategy strategy, WalkForwardConfig config) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Num amount = seriesManager.getBarSeries().numFactory().one();
|
||||
return execute(strategy, strategy.getStartingType(), amount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing with explicit entry trade type and unit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param config walk-forward configuration
|
||||
* @return execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult execute(Strategy strategy, Trade.TradeType tradeType,
|
||||
WalkForwardConfig config) {
|
||||
Num amount = seriesManager.getBarSeries().numFactory().one();
|
||||
return execute(strategy, tradeType, amount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing with explicit amount.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param amount amount used for entries/exits
|
||||
* @param config walk-forward configuration
|
||||
* @return execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult execute(Strategy strategy, Trade.TradeType tradeType, Num amount,
|
||||
WalkForwardConfig config) {
|
||||
return execute(strategy, tradeType, amount, config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes walk-forward testing with optional per-fold progress callback.
|
||||
*
|
||||
* @param strategy strategy to execute
|
||||
* @param tradeType trade type used to open positions
|
||||
* @param amount amount used for entries/exits
|
||||
* @param config walk-forward configuration
|
||||
* @param progressCallback optional callback receiving completed fold count
|
||||
* @return execution result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public StrategyWalkForwardExecutionResult execute(Strategy strategy, Trade.TradeType tradeType, Num amount,
|
||||
WalkForwardConfig config, Consumer<Integer> progressCallback) {
|
||||
Objects.requireNonNull(strategy, "strategy");
|
||||
Objects.requireNonNull(tradeType, "tradeType");
|
||||
Objects.requireNonNull(amount, "amount");
|
||||
Objects.requireNonNull(config, "config");
|
||||
|
||||
BarSeries series = seriesManager.getBarSeries();
|
||||
List<WalkForwardSplit> splits = splitter.split(series, config);
|
||||
if (splits.isEmpty()) {
|
||||
return new StrategyWalkForwardExecutionResult(series, strategy, config, List.of(),
|
||||
WalkForwardRuntimeReport.empty());
|
||||
}
|
||||
|
||||
Consumer<Integer> effectiveCallback = progressCallback == null ? ProgressCompletion.noOp() : progressCallback;
|
||||
List<StrategyWalkForwardExecutionResult.FoldResult> foldResults = new ArrayList<>(splits.size());
|
||||
List<WalkForwardRuntimeReport.FoldRuntime> foldRuntimes = new ArrayList<>(splits.size());
|
||||
|
||||
long overallStart = System.nanoTime();
|
||||
int completed = 0;
|
||||
for (WalkForwardSplit split : splits) {
|
||||
long foldStart = System.nanoTime();
|
||||
TradingRecord foldRecord = seriesManager.run(strategy, tradeType, amount, split.testStart(),
|
||||
split.testEnd());
|
||||
TradingStatement statement = tradingStatementGenerator.generate(strategy, foldRecord, series);
|
||||
Duration foldRuntime = Duration.ofNanos(System.nanoTime() - foldStart);
|
||||
|
||||
foldResults
|
||||
.add(new StrategyWalkForwardExecutionResult.FoldResult(split, foldRecord, statement, foldRuntime));
|
||||
foldRuntimes
|
||||
.add(new WalkForwardRuntimeReport.FoldRuntime(split.foldId(), foldRuntime, split.testBarCount()));
|
||||
|
||||
completed++;
|
||||
effectiveCallback.accept(completed);
|
||||
}
|
||||
|
||||
Duration overallRuntime = Duration.ofNanos(System.nanoTime() - overallStart);
|
||||
WalkForwardRuntimeReport runtimeReport = buildRuntimeReport(foldRuntimes, overallRuntime);
|
||||
return new StrategyWalkForwardExecutionResult(series, strategy, config, foldResults, runtimeReport);
|
||||
}
|
||||
|
||||
private WalkForwardRuntimeReport buildRuntimeReport(List<WalkForwardRuntimeReport.FoldRuntime> foldRuntimes,
|
||||
Duration overallRuntime) {
|
||||
if (foldRuntimes.isEmpty()) {
|
||||
return WalkForwardRuntimeReport.empty();
|
||||
}
|
||||
|
||||
List<Duration> durations = new ArrayList<>(foldRuntimes.size());
|
||||
for (WalkForwardRuntimeReport.FoldRuntime foldRuntime : foldRuntimes) {
|
||||
durations.add(foldRuntime.runtime());
|
||||
}
|
||||
|
||||
Duration min = Collections.min(durations);
|
||||
Duration max = Collections.max(durations);
|
||||
long totalNanos = 0L;
|
||||
for (Duration duration : durations) {
|
||||
totalNanos += duration.toNanos();
|
||||
}
|
||||
Duration average = Duration.ofNanos(totalNanos / durations.size());
|
||||
|
||||
List<Duration> sorted = new ArrayList<>(durations);
|
||||
sorted.sort(Comparator.naturalOrder());
|
||||
int middle = sorted.size() / 2;
|
||||
Duration median;
|
||||
if (sorted.size() % 2 == 0) {
|
||||
long medianNanos = (sorted.get(middle - 1).toNanos() + sorted.get(middle).toNanos()) / 2;
|
||||
median = Duration.ofNanos(medianNanos);
|
||||
} else {
|
||||
median = sorted.get(middle);
|
||||
}
|
||||
|
||||
return new WalkForwardRuntimeReport(overallRuntime, min, max, average, median, foldRuntimes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* An execution model for {@link BarSeriesManager} objects.
|
||||
*
|
||||
* Used for backtesting. Instructs {@link BarSeriesManager} on how to execute
|
||||
* trades.
|
||||
*
|
||||
* <p>
|
||||
* Selection guidance:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Use {@link TradeOnNextOpenModel} for conservative signal-at-close,
|
||||
* fill-next-open simulation.</li>
|
||||
* <li>Use {@link TradeOnCurrentCloseModel} when your strategy intentionally
|
||||
* fills on bar close.</li>
|
||||
* <li>Use {@link SlippageExecutionModel} when you need directional price-impact
|
||||
* assumptions.</li>
|
||||
* <li>Use {@link StopLimitExecutionModel} when pending-order lifecycle and
|
||||
* partial fills matter.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface TradeExecutionModel {
|
||||
|
||||
/**
|
||||
* Common price-source contract for execution models.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
enum PriceSource {
|
||||
/** Use the current bar close price. */
|
||||
CURRENT_CLOSE,
|
||||
/** Use the next bar open price. */
|
||||
NEXT_OPEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes per-bar execution state before strategy signals are evaluated.
|
||||
*
|
||||
* <p>
|
||||
* Implementations can use this hook to progress pending orders (for example,
|
||||
* stop/limit orders with partial fills) even when no new strategy signal is
|
||||
* emitted on the current bar.
|
||||
* </p>
|
||||
*
|
||||
* @param index current bar index
|
||||
* @param tradingRecord trading record to mutate
|
||||
* @param barSeries bar series
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default void onBar(int index, TradingRecord tradingRecord, BarSeries barSeries) {
|
||||
// Default no-op for immediate execution models.
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes model state when a {@link BarSeriesManager} run ends.
|
||||
*
|
||||
* <p>
|
||||
* Implementations can use this hook to expire or flush pending orders that
|
||||
* would otherwise be stranded when no more bars will be processed.
|
||||
* </p>
|
||||
*
|
||||
* @param lastProcessedIndex last bar index examined during the run
|
||||
* @param tradingRecord trading record to mutate
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default void onRunEnd(int lastProcessedIndex, TradingRecord tradingRecord) {
|
||||
// Default no-op for immediate execution models.
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a trade in the given {@code tradingRecord}.
|
||||
*
|
||||
* @param index the trade index from {@code barSeries}
|
||||
* @param tradingRecord the trading record to place the trade
|
||||
* @param barSeries the bar series
|
||||
* @param amount the trade amount
|
||||
*/
|
||||
void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount);
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.backtest.ExecutionModelSupport.ExecutionTarget;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* An execution model for {@link BarSeriesManager} objects.
|
||||
*
|
||||
* Executes trades on the current bar being considered using the closing price.
|
||||
*
|
||||
* This is used for strategies that explicitly trade just before the bar closes
|
||||
* at index `t`, in order to execute new or close existing trades as close as
|
||||
* possible to the closing price.
|
||||
*/
|
||||
public class TradeOnCurrentCloseModel implements TradeExecutionModel {
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
ExecutionTarget executionTarget = ExecutionModelSupport.resolveExecutionTarget(index, barSeries,
|
||||
TradeExecutionModel.PriceSource.CURRENT_CLOSE);
|
||||
if (executionTarget != null) {
|
||||
tradingRecord.operate(executionTarget.index(), executionTarget.price(), amount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.backtest.ExecutionModelSupport.ExecutionTarget;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* An execution model for {@link BarSeriesManager} objects.
|
||||
*
|
||||
* Executes trades on the next bar at the open price.
|
||||
*
|
||||
* This is used for strategies that explicitly trade just after a new bar opens
|
||||
* at bar index `t + 1`, in order to execute new or close existing trades as
|
||||
* close as possible to the opening price.
|
||||
*/
|
||||
public class TradeOnNextOpenModel implements TradeExecutionModel {
|
||||
|
||||
@Override
|
||||
public void execute(int index, TradingRecord tradingRecord, BarSeries barSeries, Num amount) {
|
||||
ExecutionTarget executionTarget = ExecutionModelSupport.resolveExecutionTarget(index, barSeries,
|
||||
TradeExecutionModel.PriceSource.NEXT_OPEN);
|
||||
if (executionTarget != null) {
|
||||
tradingRecord.operate(executionTarget.index(), executionTarget.price(), amount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+653
@@ -0,0 +1,653 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.ta4j.core.AnalysisCriterion;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.analysis.WeightedValue;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.NaN;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
import org.ta4j.core.reports.TradingStatement;
|
||||
|
||||
/**
|
||||
* Shared contract for execution results that expose statement-level outputs.
|
||||
*
|
||||
* <p>
|
||||
* This contract provides common statement/criterion utilities for backtest and
|
||||
* strategy walk-forward result models.
|
||||
* </p>
|
||||
*
|
||||
* @param <R> runtime report type
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public interface TradingStatementExecutionResult<R> {
|
||||
|
||||
/**
|
||||
* @return bar series used to produce this result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
BarSeries barSeries();
|
||||
|
||||
/**
|
||||
* @return ordered trading statements produced by this result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
List<TradingStatement> tradingStatements();
|
||||
|
||||
/**
|
||||
* @return runtime report for this result
|
||||
* @since 0.22.4
|
||||
*/
|
||||
R runtimeReport();
|
||||
|
||||
/**
|
||||
* Returns trading records in statement order.
|
||||
*
|
||||
* @return ordered trading records
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<TradingRecord> tradingRecords() {
|
||||
List<TradingRecord> records = new ArrayList<>(tradingStatements().size());
|
||||
for (TradingStatement statement : tradingStatements()) {
|
||||
records.add(statement.getTradingRecord());
|
||||
}
|
||||
return Collections.unmodifiableList(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion over all statement trading records in statement
|
||||
* order.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return criterion values in statement order
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<Num> criterionValues(AnalysisCriterion criterion) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
List<Num> values = new ArrayList<>(tradingStatements().size());
|
||||
for (TradingStatement statement : tradingStatements()) {
|
||||
TradingRecord tradingRecord = statement.getTradingRecord();
|
||||
Num value = tradingRecord == null ? NaN.NaN : criterion.calculate(barSeries(), tradingRecord);
|
||||
values.add(value);
|
||||
}
|
||||
return Collections.unmodifiableList(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates one criterion and returns values keyed by statement index.
|
||||
*
|
||||
* @param criterion analysis criterion
|
||||
* @return ordered statement-index to criterion value map
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default Map<Integer, Num> criterionValuesByIndex(AnalysisCriterion criterion) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
Map<Integer, Num> values = new LinkedHashMap<>();
|
||||
List<TradingStatement> statements = tradingStatements();
|
||||
for (int index = 0; index < statements.size(); index++) {
|
||||
TradingRecord tradingRecord = statements.get(index).getTradingRecord();
|
||||
Num value = tradingRecord == null ? NaN.NaN : criterion.calculate(barSeries(), tradingRecord);
|
||||
values.put(index, value);
|
||||
}
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranks all statements using a weighted, normalized criterion profile.
|
||||
*
|
||||
* @param profile ranking profile
|
||||
* @return ranked statement rows sorted by composite score descending
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<RankedTradingStatement> rankTradingStatements(RankingProfile profile) {
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
|
||||
List<TradingStatement> statements = tradingStatements();
|
||||
if (statements.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
NumFactory numFactory = barSeries().numFactory();
|
||||
List<WeightedCriterion> weightedCriteria = profile.criteria();
|
||||
int criterionCount = weightedCriteria.size();
|
||||
List<WeightedValue<AnalysisCriterion>> normalizedWeightedCriteria = normalizeCriteria(weightedCriteria,
|
||||
numFactory);
|
||||
AnalysisCriterion[] criteria = new AnalysisCriterion[criterionCount];
|
||||
for (int i = 0; i < criterionCount; i++) {
|
||||
criteria[i] = normalizedWeightedCriteria.get(i).value();
|
||||
}
|
||||
|
||||
int statementCount = statements.size();
|
||||
Num[][] rawValuesByCriterion = new Num[criterionCount][statementCount];
|
||||
for (int statementIndex = 0; statementIndex < statementCount; statementIndex++) {
|
||||
TradingRecord tradingRecord = statements.get(statementIndex).getTradingRecord();
|
||||
for (int criterionIndex = 0; criterionIndex < criterionCount; criterionIndex++) {
|
||||
AnalysisCriterion criterion = criteria[criterionIndex];
|
||||
rawValuesByCriterion[criterionIndex][statementIndex] = tradingRecord == null ? NaN.NaN
|
||||
: criterion.calculate(barSeries(), tradingRecord);
|
||||
}
|
||||
}
|
||||
|
||||
Num[] bestValuesByCriterion = new Num[criterionCount];
|
||||
Num[] worstValuesByCriterion = new Num[criterionCount];
|
||||
for (int criterionIndex = 0; criterionIndex < criterionCount; criterionIndex++) {
|
||||
Num[] pair = findBestWorst(rawValuesByCriterion[criterionIndex], criteria[criterionIndex]);
|
||||
bestValuesByCriterion[criterionIndex] = pair[0];
|
||||
worstValuesByCriterion[criterionIndex] = pair[1];
|
||||
}
|
||||
|
||||
List<RankedTradingStatement> rankedStatements = new ArrayList<>(statementCount);
|
||||
for (int statementIndex = 0; statementIndex < statementCount; statementIndex++) {
|
||||
TradingStatement statement = statements.get(statementIndex);
|
||||
Map<AnalysisCriterion, Num> rawScores = new LinkedHashMap<>();
|
||||
Map<AnalysisCriterion, Num> normalizedScores = new LinkedHashMap<>();
|
||||
|
||||
Num weightedScore = numFactory.zero();
|
||||
Num activeWeight = numFactory.zero();
|
||||
boolean excluded = false;
|
||||
|
||||
for (int criterionIndex = 0; criterionIndex < criterionCount; criterionIndex++) {
|
||||
AnalysisCriterion criterion = criteria[criterionIndex];
|
||||
Num weight = normalizedWeightedCriteria.get(criterionIndex).weight();
|
||||
Num rawValue = rawValuesByCriterion[criterionIndex][statementIndex];
|
||||
rawScores.put(criterion, rawValue);
|
||||
|
||||
Num normalizedValue = resolveNormalizedScore(rawValue, criterion, bestValuesByCriterion[criterionIndex],
|
||||
worstValuesByCriterion[criterionIndex], profile.normalizer(), numFactory);
|
||||
Num effectiveScore = normalizedValue;
|
||||
if (Num.isNaNOrNull(normalizedValue)) {
|
||||
if (profile.missingValuePolicy() == MissingValuePolicy.EXCLUDE_STATEMENT) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
if (profile.missingValuePolicy() == MissingValuePolicy.RENORMALIZE_WEIGHTS) {
|
||||
normalizedScores.put(criterion, NaN.NaN);
|
||||
continue;
|
||||
}
|
||||
effectiveScore = numFactory.zero();
|
||||
}
|
||||
normalizedScores.put(criterion, effectiveScore);
|
||||
weightedScore = weightedScore.plus(weight.multipliedBy(effectiveScore));
|
||||
activeWeight = activeWeight.plus(weight);
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Num compositeScore = weightedScore;
|
||||
if (profile.missingValuePolicy() == MissingValuePolicy.RENORMALIZE_WEIGHTS) {
|
||||
compositeScore = activeWeight.isZero() ? numFactory.zero() : weightedScore.dividedBy(activeWeight);
|
||||
}
|
||||
|
||||
rankedStatements
|
||||
.add(new RankedTradingStatement(statement, compositeScore, Collections.unmodifiableMap(rawScores),
|
||||
Collections.unmodifiableMap(normalizedScores), statementIndex));
|
||||
}
|
||||
|
||||
rankedStatements.sort(Comparator
|
||||
.comparing(RankedTradingStatement::compositeScore, TradingStatementExecutionResult::compareScores)
|
||||
.reversed()
|
||||
.thenComparingInt(RankedTradingStatement::originalIndex));
|
||||
return Collections.unmodifiableList(rankedStatements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranks all statements using weighted criteria with the default normalizer and
|
||||
* missing-value policy.
|
||||
*
|
||||
* <p>
|
||||
* This is the shortest path when callers already know the criteria and weights
|
||||
* they want to combine and do not need a reusable {@link RankingProfile}
|
||||
* instance.
|
||||
* </p>
|
||||
*
|
||||
* @param criteria weighted criteria to normalize and combine
|
||||
* @return ranked statement rows sorted by composite score descending
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<RankedTradingStatement> rankTradingStatements(WeightedCriterion... criteria) {
|
||||
return rankTradingStatements(RankingProfile.weighted(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns top-ranked statements for the supplied weighted profile.
|
||||
*
|
||||
* @param limit maximum number of statements to return
|
||||
* @param profile weighted ranking profile
|
||||
* @return top-ranked statements
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<TradingStatement> topTradingStatements(int limit, RankingProfile profile) {
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException("limit must not be negative");
|
||||
}
|
||||
if (limit == 0) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<RankedTradingStatement> ranked = rankTradingStatements(profile);
|
||||
int effectiveLimit = Math.min(limit, ranked.size());
|
||||
List<TradingStatement> topStatements = new ArrayList<>(effectiveLimit);
|
||||
for (int i = 0; i < effectiveLimit; i++) {
|
||||
topStatements.add(ranked.get(i).statement());
|
||||
}
|
||||
return Collections.unmodifiableList(topStatements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns top-ranked statements for the supplied weighted criteria using the
|
||||
* default normalizer and missing-value policy.
|
||||
*
|
||||
* @param limit maximum number of statements to return
|
||||
* @param criteria weighted criteria to normalize and combine
|
||||
* @return top-ranked statements
|
||||
* @since 0.22.4
|
||||
*/
|
||||
default List<TradingStatement> topTradingStatements(int limit, WeightedCriterion... criteria) {
|
||||
return topTradingStatements(limit, RankingProfile.weighted(criteria));
|
||||
}
|
||||
|
||||
/**
|
||||
* Weighted criterion entry for composite ranking.
|
||||
*
|
||||
* @param criterion criterion to evaluate
|
||||
* @param multiplier arbitrary non-negative multiplier
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record WeightedCriterion(AnalysisCriterion criterion, Num multiplier) {
|
||||
|
||||
/**
|
||||
* Creates a validated weighted criterion.
|
||||
*
|
||||
* @param criterion criterion to evaluate
|
||||
* @param multiplier arbitrary non-negative multiplier
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public WeightedCriterion {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
Objects.requireNonNull(multiplier, "multiplier");
|
||||
if (multiplier.isNaN()) {
|
||||
throw new IllegalArgumentException("multiplier must be finite");
|
||||
}
|
||||
if (Double.isInfinite(multiplier.doubleValue())) {
|
||||
throw new IllegalArgumentException("multiplier must be finite");
|
||||
}
|
||||
if (multiplier.isNegative()) {
|
||||
throw new IllegalArgumentException("multiplier must be >= 0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an equally weighted criterion.
|
||||
*
|
||||
* @param criterion criterion to evaluate
|
||||
* @return weighted criterion with multiplier {@code 1}
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static WeightedCriterion of(AnalysisCriterion criterion) {
|
||||
return new WeightedCriterion(criterion, DoubleNumFactory.getInstance().one());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a weighted criterion from an explicit {@link Num} multiplier.
|
||||
*
|
||||
* @param criterion criterion to evaluate
|
||||
* @param multiplier arbitrary non-negative multiplier
|
||||
* @return weighted criterion
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static WeightedCriterion of(AnalysisCriterion criterion, Num multiplier) {
|
||||
return new WeightedCriterion(criterion, multiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a weighted criterion from a primitive multiplier.
|
||||
*
|
||||
* <p>
|
||||
* Primitive multipliers are stored as {@link org.ta4j.core.num.DoubleNum}
|
||||
* values and normalized into the result series factory during ranking.
|
||||
* </p>
|
||||
*
|
||||
* @param criterion criterion to evaluate
|
||||
* @param multiplier arbitrary non-negative multiplier
|
||||
* @return weighted criterion
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static WeightedCriterion of(AnalysisCriterion criterion, double multiplier) {
|
||||
return new WeightedCriterion(criterion, DoubleNumFactory.getInstance().numOf(multiplier));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Criterion normalization strategy used by weighted ranking.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface CriterionNormalizer {
|
||||
|
||||
/**
|
||||
* Normalizes a raw criterion value using criterion-specific best/worst values.
|
||||
*
|
||||
* @param criterion criterion being normalized
|
||||
* @param rawValue raw criterion value for one statement
|
||||
* @param bestValue best observed finite value across statements for this
|
||||
* criterion
|
||||
* @param worstValue worst observed finite value across statements for this
|
||||
* criterion
|
||||
* @param numFactory target num factory
|
||||
* @return normalized value (expected in {@code [0,1]}); NaN to indicate
|
||||
* missing/unavailable normalization
|
||||
* @since 0.22.4
|
||||
*/
|
||||
Num normalize(AnalysisCriterion criterion, Num rawValue, Num bestValue, Num worstValue, NumFactory numFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direction-aware min-max normalizer.
|
||||
*
|
||||
* <p>
|
||||
* Best observed value maps to {@code 1}, worst observed value maps to
|
||||
* {@code 0}, and intermediate values are scaled linearly.
|
||||
* </p>
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
final class DirectionAwareMinMaxNormalizer implements CriterionNormalizer {
|
||||
|
||||
/**
|
||||
* Shared singleton instance.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static final DirectionAwareMinMaxNormalizer INSTANCE = new DirectionAwareMinMaxNormalizer();
|
||||
|
||||
private DirectionAwareMinMaxNormalizer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Num normalize(AnalysisCriterion criterion, Num rawValue, Num bestValue, Num worstValue,
|
||||
NumFactory numFactory) {
|
||||
Objects.requireNonNull(criterion, "criterion");
|
||||
Objects.requireNonNull(rawValue, "rawValue");
|
||||
Objects.requireNonNull(bestValue, "bestValue");
|
||||
Objects.requireNonNull(worstValue, "worstValue");
|
||||
Objects.requireNonNull(numFactory, "numFactory");
|
||||
|
||||
Num normalizedRaw = normalizeToFactory(rawValue, numFactory);
|
||||
Num normalizedBest = normalizeToFactory(bestValue, numFactory);
|
||||
Num normalizedWorst = normalizeToFactory(worstValue, numFactory);
|
||||
|
||||
if (Num.isNaNOrNull(normalizedRaw) || Num.isNaNOrNull(normalizedBest) || Num.isNaNOrNull(normalizedWorst)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
if (normalizedBest.isEqual(normalizedWorst)) {
|
||||
return numFactory.one();
|
||||
}
|
||||
|
||||
if (normalizedBest.isGreaterThan(normalizedWorst)) {
|
||||
Num denominator = normalizedBest.minus(normalizedWorst);
|
||||
if (denominator.isZero()) {
|
||||
return numFactory.one();
|
||||
}
|
||||
return normalizedRaw.minus(normalizedWorst).dividedBy(denominator);
|
||||
}
|
||||
|
||||
Num denominator = normalizedWorst.minus(normalizedBest);
|
||||
if (denominator.isZero()) {
|
||||
return numFactory.one();
|
||||
}
|
||||
return normalizedWorst.minus(normalizedRaw).dividedBy(denominator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing-value behavior for weighted ranking.
|
||||
*
|
||||
* @since 0.22.4
|
||||
*/
|
||||
enum MissingValuePolicy {
|
||||
/** Treat missing criterion values as normalized score 0. */
|
||||
WORST_SCORE,
|
||||
/** Exclude missing criteria from statement-level weight sum. */
|
||||
RENORMALIZE_WEIGHTS,
|
||||
/** Exclude statements with any missing criterion value. */
|
||||
EXCLUDE_STATEMENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Weighted ranking configuration.
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @param normalizer criterion normalizer (defaults to
|
||||
* {@link DirectionAwareMinMaxNormalizer#INSTANCE})
|
||||
* @param missingValuePolicy missing-value handling policy (defaults to
|
||||
* {@link MissingValuePolicy#WORST_SCORE})
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record RankingProfile(List<WeightedCriterion> criteria, CriterionNormalizer normalizer,
|
||||
MissingValuePolicy missingValuePolicy) {
|
||||
|
||||
/**
|
||||
* Creates a validated ranking profile.
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @param normalizer criterion normalizer
|
||||
* @param missingValuePolicy missing-value handling policy
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RankingProfile {
|
||||
criteria = List.copyOf(Objects.requireNonNull(criteria, "criteria"));
|
||||
if (criteria.isEmpty()) {
|
||||
throw new IllegalArgumentException("criteria must not be empty");
|
||||
}
|
||||
normalizer = normalizer == null ? DirectionAwareMinMaxNormalizer.INSTANCE : normalizer;
|
||||
missingValuePolicy = missingValuePolicy == null ? MissingValuePolicy.WORST_SCORE : missingValuePolicy;
|
||||
|
||||
Set<AnalysisCriterion> uniqueCriteria = new LinkedHashSet<>();
|
||||
for (WeightedCriterion weightedCriterion : criteria) {
|
||||
Objects.requireNonNull(weightedCriterion, "criteria must not contain null entries");
|
||||
if (!uniqueCriteria.add(weightedCriterion.criterion())) {
|
||||
throw new IllegalArgumentException("criteria must not contain duplicates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a profile with default normalizer and missing-value policy.
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @return ranking profile
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static RankingProfile of(List<WeightedCriterion> criteria) {
|
||||
return weighted(criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a profile with default normalizer and missing-value policy.
|
||||
*
|
||||
* <p>
|
||||
* This named factory is the main entry point for callers who want weighted,
|
||||
* normalized ranking without choosing custom normalization behavior.
|
||||
* </p>
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @return ranking profile
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static RankingProfile weighted(List<WeightedCriterion> criteria) {
|
||||
return new RankingProfile(criteria, DirectionAwareMinMaxNormalizer.INSTANCE,
|
||||
MissingValuePolicy.WORST_SCORE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a profile with default normalizer and missing-value policy.
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @return ranking profile
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static RankingProfile of(WeightedCriterion... criteria) {
|
||||
Objects.requireNonNull(criteria, "criteria");
|
||||
return weighted(criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a profile with default normalizer and missing-value policy.
|
||||
*
|
||||
* @param criteria criterion multipliers
|
||||
* @return ranking profile
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public static RankingProfile weighted(WeightedCriterion... criteria) {
|
||||
Objects.requireNonNull(criteria, "criteria");
|
||||
return weighted(List.of(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranked statement row with composite and per-criterion details.
|
||||
*
|
||||
* @param statement trading statement
|
||||
* @param compositeScore weighted normalized composite score
|
||||
* @param rawScores raw criterion scores by criterion
|
||||
* @param normalizedScores normalized criterion scores by criterion
|
||||
* @param originalIndex original statement index before ranking
|
||||
* @since 0.22.4
|
||||
*/
|
||||
record RankedTradingStatement(TradingStatement statement, Num compositeScore, Map<AnalysisCriterion, Num> rawScores,
|
||||
Map<AnalysisCriterion, Num> normalizedScores, int originalIndex) {
|
||||
|
||||
/**
|
||||
* Creates a validated ranked statement row.
|
||||
*
|
||||
* @param statement trading statement
|
||||
* @param compositeScore weighted normalized composite score
|
||||
* @param rawScores raw criterion scores by criterion
|
||||
* @param normalizedScores normalized criterion scores by criterion
|
||||
* @param originalIndex original statement index before ranking
|
||||
* @since 0.22.4
|
||||
*/
|
||||
public RankedTradingStatement {
|
||||
Objects.requireNonNull(statement, "statement");
|
||||
Objects.requireNonNull(compositeScore, "compositeScore");
|
||||
rawScores = Collections
|
||||
.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(rawScores, "rawScores")));
|
||||
normalizedScores = Collections
|
||||
.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(normalizedScores, "normalizedScores")));
|
||||
if (originalIndex < 0) {
|
||||
throw new IllegalArgumentException("originalIndex must be >= 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Num compareSafe(Num value, NumFactory numFactory) {
|
||||
if (Num.isNaNOrNull(value)) {
|
||||
return numFactory.minusOne();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int compareScores(Num left, Num right) {
|
||||
NumFactory numFactory = left != null && !left.isNaN() ? left.getNumFactory()
|
||||
: right != null && !right.isNaN() ? right.getNumFactory() : null;
|
||||
if (numFactory == null) {
|
||||
return 0;
|
||||
}
|
||||
Num leftComparable = compareSafe(left, numFactory);
|
||||
Num rightComparable = compareSafe(right, numFactory);
|
||||
return leftComparable.compareTo(rightComparable);
|
||||
}
|
||||
|
||||
private static Num normalizeToFactory(Num value, NumFactory numFactory) {
|
||||
if (Num.isNaNOrNull(value)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
if (numFactory.produces(value)) {
|
||||
return value;
|
||||
}
|
||||
return numFactory.numOf(value.doubleValue());
|
||||
}
|
||||
|
||||
private static Num clamp01(Num value, NumFactory numFactory) {
|
||||
if (Num.isNaNOrNull(value)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
Num normalizedValue = normalizeToFactory(value, numFactory);
|
||||
Num zero = numFactory.zero();
|
||||
Num one = numFactory.one();
|
||||
if (normalizedValue.isLessThan(zero)) {
|
||||
return zero;
|
||||
}
|
||||
if (normalizedValue.isGreaterThan(one)) {
|
||||
return one;
|
||||
}
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
private static Num resolveNormalizedScore(Num rawValue, AnalysisCriterion criterion, Num bestValue, Num worstValue,
|
||||
CriterionNormalizer normalizer, NumFactory numFactory) {
|
||||
if (Num.isNaNOrNull(rawValue)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
Num normalized = normalizer.normalize(criterion, rawValue, bestValue, worstValue, numFactory);
|
||||
if (Num.isNaNOrNull(normalized)) {
|
||||
return NaN.NaN;
|
||||
}
|
||||
return clamp01(normalized, numFactory);
|
||||
}
|
||||
|
||||
private static List<WeightedValue<AnalysisCriterion>> normalizeCriteria(List<WeightedCriterion> weightedCriteria,
|
||||
NumFactory numFactory) {
|
||||
List<WeightedValue<AnalysisCriterion>> weightedValues = new ArrayList<>(weightedCriteria.size());
|
||||
for (WeightedCriterion weightedCriterion : weightedCriteria) {
|
||||
Num multiplier = normalizeToFactory(weightedCriterion.multiplier(), numFactory);
|
||||
if (Num.isNaNOrNull(multiplier) || multiplier.isNegative()) {
|
||||
throw new IllegalArgumentException("criterion multiplier must be finite and >= 0");
|
||||
}
|
||||
weightedValues.add(new WeightedValue<>(weightedCriterion.criterion(), multiplier));
|
||||
}
|
||||
return WeightedValue.normalizeWeights(weightedValues, numFactory);
|
||||
}
|
||||
|
||||
private static Num[] findBestWorst(Num[] values, AnalysisCriterion criterion) {
|
||||
Num bestValue = null;
|
||||
Num worstValue = null;
|
||||
for (Num value : values) {
|
||||
if (Num.isNaNOrNull(value)) {
|
||||
continue;
|
||||
}
|
||||
if (bestValue == null) {
|
||||
bestValue = value;
|
||||
worstValue = value;
|
||||
continue;
|
||||
}
|
||||
if (criterion.betterThan(value, bestValue)) {
|
||||
bestValue = value;
|
||||
}
|
||||
if (criterion.betterThan(worstValue, value)) {
|
||||
worstValue = value;
|
||||
}
|
||||
}
|
||||
if (bestValue == null) {
|
||||
return new Num[] { NaN.NaN, NaN.NaN };
|
||||
}
|
||||
return new Num[] { bestValue, worstValue };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
/**
|
||||
* Backtesting execution infrastructure.
|
||||
*
|
||||
* <p>
|
||||
* This package defines how strategies are executed over historical series and
|
||||
* how runs are ranked or compared.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Choose by workload:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link org.ta4j.core.backtest.BarSeriesManager} for one strategy over one
|
||||
* series</li>
|
||||
* <li>{@link org.ta4j.core.backtest.BacktestExecutor} for many strategies,
|
||||
* telemetry, and ranking</li>
|
||||
* <li>{@link org.ta4j.core.backtest.TradeExecutionModel} implementations to
|
||||
* align fill semantics with your simulation assumptions</li>
|
||||
* </ul>
|
||||
*/
|
||||
package org.ta4j.core.backtest;
|
||||
@@ -0,0 +1,8 @@
|
||||
# AGENTS instructions for `org.ta4j.core.bars`
|
||||
|
||||
## TimeBarBuilder gap semantics
|
||||
|
||||
- `TimeBarBuilder` must align bars to the supplied time periods.
|
||||
- Do not auto-reconcile or backfill missing periods.
|
||||
- Missing periods should remain missing while preserving correct chronological placement of subsequent bars.
|
||||
- When changing bar-construction logic, add or update tests that cover both contiguous data and explicit time gaps.
|
||||
@@ -0,0 +1,574 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* An amount bar is sampled after a fixed number of amount (= price * volume)
|
||||
* have been traded.
|
||||
*/
|
||||
public class AmountBarBuilder implements BarBuilder {
|
||||
|
||||
private Num distinctVolume;
|
||||
private final Num amountThreshold;
|
||||
private final boolean setAmountByVolume;
|
||||
|
||||
private final NumFactory numFactory;
|
||||
private final RemainderCarryOverPolicy carryOverPolicy;
|
||||
private final boolean realtimeBars;
|
||||
private BarSeries barSeries;
|
||||
private Duration timePeriod;
|
||||
private Instant beginTime;
|
||||
private Instant endTime;
|
||||
private Num openPrice;
|
||||
private Num highPrice;
|
||||
private Num lowPrice;
|
||||
private Num closePrice;
|
||||
private Num volume;
|
||||
private Num amount;
|
||||
private long trades;
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private boolean hasSideData;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasLiquidityData;
|
||||
private Num lastTradeVolume;
|
||||
private Num lastTradePrice;
|
||||
private RealtimeBar.Side lastTradeSide;
|
||||
private RealtimeBar.Liquidity lastTradeLiquidity;
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} with {@link DoubleNumFactory}
|
||||
*
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@link #amount} is set by
|
||||
* {@link #volume} * {@link #closePrice}, otherwise
|
||||
* {@link #amount} must be explicitly set
|
||||
*/
|
||||
public AmountBarBuilder(final int amountThreshold, final boolean setAmountByVolume) {
|
||||
this(DoubleNumFactory.getInstance(), amountThreshold, setAmountByVolume, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@link #amount} is set by
|
||||
* {@link #volume} * {@link #closePrice}, otherwise
|
||||
* {@link #amount} must be explicitly set
|
||||
*/
|
||||
public AmountBarBuilder(final NumFactory numFactory, final int amountThreshold, final boolean setAmountByVolume) {
|
||||
this(numFactory, amountThreshold, setAmountByVolume, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@link #amount} is set by
|
||||
* {@link #volume} * {@link #closePrice}, otherwise
|
||||
* {@link #amount} must be explicitly set
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public AmountBarBuilder(final NumFactory numFactory, final int amountThreshold, final boolean setAmountByVolume,
|
||||
final boolean realtimeBars) {
|
||||
this(numFactory, amountThreshold, setAmountByVolume, realtimeBars, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory the backing number factory
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@link #amount} is set by
|
||||
* {@link #volume} * {@link #closePrice}, otherwise
|
||||
* {@link #amount} must be explicitly set
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public AmountBarBuilder(final NumFactory numFactory, final int amountThreshold, final boolean setAmountByVolume,
|
||||
final boolean realtimeBars, final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this.numFactory = numFactory;
|
||||
this.amountThreshold = numFactory.numOf(amountThreshold);
|
||||
this.setAmountByVolume = setAmountByVolume;
|
||||
this.carryOverPolicy = carryOverPolicy == null ? RemainderCarryOverPolicy.NONE : carryOverPolicy;
|
||||
this.realtimeBars = realtimeBars;
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder timePeriod(final Duration timePeriod) {
|
||||
this.timePeriod = this.timePeriod == null ? timePeriod : this.timePeriod.plus(timePeriod);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder beginTime(final Instant beginTime) {
|
||||
this.beginTime = beginTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder endTime(final Instant endTime) {
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Num openPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Number openPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final String openPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Number highPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final String highPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Num highPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Num lowPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Number lowPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final String lowPrice) {
|
||||
throw new IllegalArgumentException("AmountBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Num tickPrice) {
|
||||
closePrice = tickPrice;
|
||||
if (openPrice == null) {
|
||||
openPrice = tickPrice;
|
||||
}
|
||||
|
||||
highPrice = highPrice.max(tickPrice);
|
||||
lowPrice = lowPrice.min(tickPrice);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Number closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final String closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Num volume) {
|
||||
this.distinctVolume = volume;
|
||||
this.volume = this.volume == null ? volume : this.volume.plus(volume);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Number volume) {
|
||||
volume(numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final String volume) {
|
||||
volume(numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Num amount) {
|
||||
if (setAmountByVolume) {
|
||||
throw new IllegalArgumentException("AmountBar.amount can only be built from closePrice*volume");
|
||||
}
|
||||
this.amount = this.amount == null ? amount : this.amount.plus(amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Number amount) {
|
||||
amount(numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final String amount) {
|
||||
amount(numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final long trades) {
|
||||
this.trades += trades;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final String trades) {
|
||||
trades(Long.parseLong(trades));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AmountBarBuilder bindTo(final BarSeries barSeries) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current amount bar and appends the bar once the
|
||||
* amount threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice) {
|
||||
addTrade(time, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current amount bar and appends the bar once the
|
||||
* amount threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice");
|
||||
ensureRealtimeTracking(side, liquidity);
|
||||
if (endTime != null && time.isBefore(endTime)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Trade time %s is before current bar end time %s", time, endTime));
|
||||
}
|
||||
if (beginTime == null) {
|
||||
beginTime = time;
|
||||
}
|
||||
endTime = time;
|
||||
closePrice(tradePrice);
|
||||
volume(tradeVolume);
|
||||
if (!setAmountByVolume) {
|
||||
amount(tradePrice.multipliedBy(tradeVolume));
|
||||
}
|
||||
trades(1);
|
||||
lastTradeVolume = tradeVolume;
|
||||
lastTradePrice = tradePrice;
|
||||
lastTradeSide = side;
|
||||
lastTradeLiquidity = liquidity;
|
||||
recordRealtimeTrade(tradeVolume, tradePrice, side, liquidity);
|
||||
add();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds bar from current state that is modified for each tick.
|
||||
*
|
||||
* @return snapshot of current state
|
||||
*/
|
||||
@Override
|
||||
public Bar build() {
|
||||
if (realtimeBars) {
|
||||
return new BaseRealtimeBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice,
|
||||
volume, amount, trades, buyVolume, sellVolume, buyAmount, sellAmount, buyTrades, sellTrades,
|
||||
makerVolume, takerVolume, makerAmount, takerAmount, makerTrades, takerTrades, hasSideData,
|
||||
hasLiquidityData, numFactory);
|
||||
}
|
||||
return new BaseBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice, volume, amount,
|
||||
trades);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
|
||||
if (setAmountByVolume) {
|
||||
final var calculatedAmount = closePrice.multipliedBy(distinctVolume);
|
||||
amount = amount == null ? calculatedAmount : amount.plus(calculatedAmount);
|
||||
}
|
||||
|
||||
if (amount.isGreaterThanOrEqual(amountThreshold)) {
|
||||
// move amount remainder to next bar
|
||||
var amountRemainder = numFactory.zero();
|
||||
// move volume remainder to next bar
|
||||
var volumeRemainder = numFactory.zero();
|
||||
CarryOverSnapshot carryOverSnapshot = null;
|
||||
if (amount.isGreaterThan(amountThreshold)) {
|
||||
amountRemainder = amount.minus(amountThreshold);
|
||||
// Use closePrice for division, but fall back to lastTradePrice if closePrice is
|
||||
// zero
|
||||
// This prevents division by zero when the current trade has a zero price
|
||||
final Num priceForDivision = (closePrice == null || closePrice.isZero()) && lastTradePrice != null
|
||||
&& !lastTradePrice.isZero() ? lastTradePrice : closePrice;
|
||||
if (priceForDivision == null || priceForDivision.isZero()) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot calculate volume remainder: both closePrice and lastTradePrice are zero or null, but amount remainder exists");
|
||||
}
|
||||
volumeRemainder = amountRemainder.dividedBy(priceForDivision);
|
||||
|
||||
// cap currently built bar, amount is then restored to amountRemainder
|
||||
amount = amountThreshold;
|
||||
// cap currently built bar, volume is then restored to volumeRemainder
|
||||
volume = volume.minus(volumeRemainder);
|
||||
if (carryOverPolicy == RemainderCarryOverPolicy.PROPORTIONAL
|
||||
|| carryOverPolicy == RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT) {
|
||||
carryOverSnapshot = applyProportionalCarryOver(volumeRemainder, amountRemainder);
|
||||
}
|
||||
}
|
||||
|
||||
barSeries.addBar(build());
|
||||
amount = amountRemainder;
|
||||
volume = volumeRemainder;
|
||||
|
||||
reset();
|
||||
if (carryOverSnapshot != null) {
|
||||
carryOverSnapshot.applyTo(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
distinctVolume = null;
|
||||
|
||||
timePeriod = null;
|
||||
beginTime = null;
|
||||
endTime = null;
|
||||
openPrice = null;
|
||||
highPrice = numFactory.zero();
|
||||
lowPrice = numFactory.numOf(Integer.MAX_VALUE);
|
||||
closePrice = null;
|
||||
trades = 0;
|
||||
buyVolume = null;
|
||||
sellVolume = null;
|
||||
buyAmount = null;
|
||||
sellAmount = null;
|
||||
buyTrades = 0;
|
||||
sellTrades = 0;
|
||||
hasSideData = false;
|
||||
makerVolume = null;
|
||||
takerVolume = null;
|
||||
makerAmount = null;
|
||||
takerAmount = null;
|
||||
makerTrades = 0;
|
||||
takerTrades = 0;
|
||||
hasLiquidityData = false;
|
||||
lastTradeVolume = null;
|
||||
lastTradePrice = null;
|
||||
lastTradeSide = null;
|
||||
lastTradeLiquidity = null;
|
||||
}
|
||||
|
||||
private CarryOverSnapshot applyProportionalCarryOver(final Num volumeRemainder, final Num amountRemainder) {
|
||||
if (volumeRemainder == null || volumeRemainder.isZero() || amountRemainder == null || lastTradeVolume == null
|
||||
|| lastTradePrice == null) {
|
||||
return null;
|
||||
}
|
||||
final CarryOverSnapshot snapshot = new CarryOverSnapshot();
|
||||
final boolean carryTradeCount = shouldCarryTradeCount(volumeRemainder);
|
||||
if (lastTradeSide != null) {
|
||||
if (lastTradeSide == RealtimeBar.Side.BUY) {
|
||||
buyVolume = subtractOrNull(buyVolume, volumeRemainder);
|
||||
buyAmount = subtractOrNull(buyAmount, amountRemainder);
|
||||
snapshot.buyVolume = volumeRemainder;
|
||||
snapshot.buyAmount = amountRemainder;
|
||||
if (carryTradeCount) {
|
||||
buyTrades = Math.max(0, buyTrades - 1);
|
||||
snapshot.buyTrades = 1;
|
||||
}
|
||||
} else {
|
||||
sellVolume = subtractOrNull(sellVolume, volumeRemainder);
|
||||
sellAmount = subtractOrNull(sellAmount, amountRemainder);
|
||||
snapshot.sellVolume = volumeRemainder;
|
||||
snapshot.sellAmount = amountRemainder;
|
||||
if (carryTradeCount) {
|
||||
sellTrades = Math.max(0, sellTrades - 1);
|
||||
snapshot.sellTrades = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastTradeLiquidity != null) {
|
||||
if (lastTradeLiquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = subtractOrNull(makerVolume, volumeRemainder);
|
||||
makerAmount = subtractOrNull(makerAmount, amountRemainder);
|
||||
snapshot.makerVolume = volumeRemainder;
|
||||
snapshot.makerAmount = amountRemainder;
|
||||
if (carryTradeCount) {
|
||||
makerTrades = Math.max(0, makerTrades - 1);
|
||||
snapshot.makerTrades = 1;
|
||||
}
|
||||
} else {
|
||||
takerVolume = subtractOrNull(takerVolume, volumeRemainder);
|
||||
takerAmount = subtractOrNull(takerAmount, amountRemainder);
|
||||
snapshot.takerVolume = volumeRemainder;
|
||||
snapshot.takerAmount = amountRemainder;
|
||||
if (carryTradeCount) {
|
||||
takerTrades = Math.max(0, takerTrades - 1);
|
||||
snapshot.takerTrades = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (carryTradeCount) {
|
||||
trades = Math.max(0, trades - 1);
|
||||
snapshot.trades = 1;
|
||||
}
|
||||
snapshot.hasSideData = snapshot.buyVolume != null || snapshot.sellVolume != null;
|
||||
snapshot.hasLiquidityData = snapshot.makerVolume != null || snapshot.takerVolume != null;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private boolean shouldCarryTradeCount(final Num volumeRemainder) {
|
||||
if (carryOverPolicy != RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
if (lastTradeVolume == null || lastTradeVolume.isZero()) {
|
||||
return false;
|
||||
}
|
||||
return volumeRemainder.multipliedBy(numFactory.numOf(2)).isGreaterThanOrEqual(lastTradeVolume);
|
||||
}
|
||||
|
||||
private Num subtractOrNull(final Num current, final Num remainder) {
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
final Num updated = current.minus(remainder);
|
||||
return updated.isZero() ? null : updated;
|
||||
}
|
||||
|
||||
private static final class CarryOverSnapshot {
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long trades;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasSideData;
|
||||
private boolean hasLiquidityData;
|
||||
|
||||
private void applyTo(final AmountBarBuilder builder) {
|
||||
builder.buyVolume = buyVolume;
|
||||
builder.sellVolume = sellVolume;
|
||||
builder.buyAmount = buyAmount;
|
||||
builder.sellAmount = sellAmount;
|
||||
builder.makerVolume = makerVolume;
|
||||
builder.takerVolume = takerVolume;
|
||||
builder.makerAmount = makerAmount;
|
||||
builder.takerAmount = takerAmount;
|
||||
builder.trades = trades;
|
||||
builder.buyTrades = buyTrades;
|
||||
builder.sellTrades = sellTrades;
|
||||
builder.makerTrades = makerTrades;
|
||||
builder.takerTrades = takerTrades;
|
||||
builder.hasSideData = hasSideData;
|
||||
builder.hasLiquidityData = hasLiquidityData;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordRealtimeTrade(final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
if (side != null) {
|
||||
hasSideData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (side == RealtimeBar.Side.BUY) {
|
||||
buyVolume = buyVolume == null ? tradeVolume : buyVolume.plus(tradeVolume);
|
||||
buyAmount = buyAmount == null ? tradeAmount : buyAmount.plus(tradeAmount);
|
||||
buyTrades++;
|
||||
} else {
|
||||
sellVolume = sellVolume == null ? tradeVolume : sellVolume.plus(tradeVolume);
|
||||
sellAmount = sellAmount == null ? tradeAmount : sellAmount.plus(tradeAmount);
|
||||
sellTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (liquidity != null) {
|
||||
hasLiquidityData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (liquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = makerVolume == null ? tradeVolume : makerVolume.plus(tradeVolume);
|
||||
makerAmount = makerAmount == null ? tradeAmount : makerAmount.plus(tradeAmount);
|
||||
makerTrades++;
|
||||
} else {
|
||||
takerVolume = takerVolume == null ? tradeVolume : takerVolume.plus(tradeVolume);
|
||||
takerAmount = takerAmount == null ? tradeAmount : takerAmount.plus(tradeAmount);
|
||||
takerTrades++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRealtimeTracking(final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
if (!realtimeBars && (side != null || liquidity != null)) {
|
||||
throw new IllegalStateException("Realtime trade data requires a realtime bar builder");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.ta4j.core.BarBuilderFactory;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
public class AmountBarBuilderFactory implements BarBuilderFactory {
|
||||
|
||||
private final int amountThreshold;
|
||||
private final boolean setAmountByVolume;
|
||||
private final RemainderCarryOverPolicy carryOverPolicy;
|
||||
private final boolean realtimeBars;
|
||||
private transient AmountBarBuilder barBuilder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@code amount} is set by
|
||||
* {@code volume * closePrice}, otherwise
|
||||
* {@code amount} must be explicitly set
|
||||
*/
|
||||
public AmountBarBuilderFactory(final int amountThreshold, final boolean setAmountByVolume) {
|
||||
this(amountThreshold, setAmountByVolume, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@code amount} is set by
|
||||
* {@code volume * closePrice}, otherwise
|
||||
* {@code amount} must be explicitly set
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public AmountBarBuilderFactory(final int amountThreshold, final boolean setAmountByVolume,
|
||||
final boolean realtimeBars) {
|
||||
this(amountThreshold, setAmountByVolume, realtimeBars, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@code amount} is set by
|
||||
* {@code volume * closePrice}, otherwise
|
||||
* {@code amount} must be explicitly set
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public AmountBarBuilderFactory(final int amountThreshold, final boolean setAmountByVolume,
|
||||
final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this(amountThreshold, setAmountByVolume, false, carryOverPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param amountThreshold the threshold at which a new bar should be created
|
||||
* @param setAmountByVolume if {@code true} the {@code amount} is set by
|
||||
* {@code volume * closePrice}, otherwise
|
||||
* {@code amount} must be explicitly set
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public AmountBarBuilderFactory(final int amountThreshold, final boolean setAmountByVolume,
|
||||
final boolean realtimeBars, final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this.amountThreshold = amountThreshold;
|
||||
this.setAmountByVolume = setAmountByVolume;
|
||||
this.realtimeBars = realtimeBars;
|
||||
this.carryOverPolicy = carryOverPolicy == null ? RemainderCarryOverPolicy.NONE : carryOverPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder createBarBuilder(final BarSeries series) {
|
||||
if (this.barBuilder == null) {
|
||||
this.barBuilder = new AmountBarBuilder(series.numFactory(), this.amountThreshold, this.setAmountByVolume,
|
||||
this.realtimeBars, this.carryOverPolicy).bindTo(series);
|
||||
}
|
||||
|
||||
return this.barBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/*
|
||||
* Heikin-Ashi bar builder
|
||||
* @see <a href="https://www.investopedia.com/trading/heikin-ashi-better-candlestick/">Heikin-Ashi</a>
|
||||
*/
|
||||
public class HeikinAshiBarBuilder extends TimeBarBuilder {
|
||||
private Num previousHeikinAshiOpenPrice;
|
||||
private Num previousHeikinAshiClosePrice;
|
||||
|
||||
public HeikinAshiBarBuilder() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HeikinAshiBarBuilder(NumFactory numFactory) {
|
||||
super(numFactory);
|
||||
}
|
||||
|
||||
public HeikinAshiBarBuilder previousHeikinAshiOpenPrice(Num previousOpen) {
|
||||
previousHeikinAshiOpenPrice = previousOpen;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HeikinAshiBarBuilder previousHeikinAshiClosePrice(Num previousClose) {
|
||||
previousHeikinAshiClosePrice = previousClose;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar build() {
|
||||
if (previousHeikinAshiOpenPrice == null || previousHeikinAshiClosePrice == null) {
|
||||
return super.build();
|
||||
} else {
|
||||
var numFactory = openPrice.getNumFactory();
|
||||
var heikinAshiClose = openPrice.plus(highPrice)
|
||||
.plus(lowPrice)
|
||||
.plus(closePrice)
|
||||
.dividedBy(numFactory.numOf(4));
|
||||
var heikinAshiOpen = previousHeikinAshiOpenPrice.plus(previousHeikinAshiClosePrice)
|
||||
.dividedBy(numFactory.numOf(2));
|
||||
var heikinAshiHigh = highPrice.max(heikinAshiOpen).max(heikinAshiClose);
|
||||
var heikinAshiLow = lowPrice.min(heikinAshiOpen).min(heikinAshiClose);
|
||||
return new BaseBar(timePeriod, beginTime, endTime, heikinAshiOpen, heikinAshiHigh, heikinAshiLow,
|
||||
heikinAshiClose, volume, amount, trades);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarBuilderFactory;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
public class HeikinAshiBarBuilderFactory implements BarBuilderFactory {
|
||||
|
||||
@Override
|
||||
public BarBuilder createBarBuilder(BarSeries series) {
|
||||
return new HeikinAshiBarBuilder(series.numFactory()).bindTo(series);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
/**
|
||||
* Policy for handling side/liquidity data when a volume or amount bar carries a
|
||||
* remainder into the next bar.
|
||||
*
|
||||
* <p>
|
||||
* Trade remainders occur when a single trade pushes the bar past its volume or
|
||||
* amount threshold. The bar is capped and the remainder is rolled into the next
|
||||
* bar. This policy controls whether side/liquidity breakdowns follow that
|
||||
* remainder.
|
||||
*
|
||||
* <p>
|
||||
* In practice:
|
||||
* <ul>
|
||||
* <li>{@link #NONE} keeps side/liquidity data attached to the trade that caused
|
||||
* the rollover. This preserves trade fidelity (no synthetic splits) but can
|
||||
* make side/liquidity totals diverge from the capped volume/amount.</li>
|
||||
* <li>{@link #PROPORTIONAL} splits the trade's side/liquidity volumes and
|
||||
* amounts proportionally between the capped bar and the remainder. This keeps
|
||||
* side/liquidity totals aligned with volume/amount but injects an assumption
|
||||
* about how to attribute partial trades.</li>
|
||||
* <li>{@link #PROPORTIONAL_WITH_TRADE_COUNT} applies the proportional split and
|
||||
* assigns the trade count to the bar that receives the larger portion of the
|
||||
* final trade (rounded: the remainder receives the count if its share is at
|
||||
* least 50%). This preserves integer trade counts while keeping them aligned
|
||||
* with the majority of the trade volume.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Trade counts are not split by this policy unless
|
||||
* {@link #PROPORTIONAL_WITH_TRADE_COUNT} is selected; they remain whole-trade
|
||||
* counts.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public enum RemainderCarryOverPolicy {
|
||||
|
||||
/**
|
||||
* Do not carry side/liquidity data with the remainder.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Split side/liquidity volumes and amounts proportionally with the remainder.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
PROPORTIONAL,
|
||||
/**
|
||||
* Split side/liquidity volumes and amounts proportionally and allocate the
|
||||
* trade count to the remainder if its share is at least 50%.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
PROPORTIONAL_WITH_TRADE_COUNT
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A tick bar is sampled after a fixed number of ticks.
|
||||
*/
|
||||
public class TickBarBuilder implements BarBuilder {
|
||||
|
||||
private final NumFactory numFactory;
|
||||
private final boolean realtimeBars;
|
||||
private final int tickCount;
|
||||
private int passedTicksCount;
|
||||
private BarSeries barSeries;
|
||||
private Duration timePeriod;
|
||||
private Instant beginTime;
|
||||
private Instant endTime;
|
||||
private Num volume;
|
||||
private Num openPrice;
|
||||
private Num highPrice;
|
||||
private Num closePrice;
|
||||
private Num lowPrice;
|
||||
private Num amount;
|
||||
private long trades;
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private boolean hasSideData;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasLiquidityData;
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} with {@link DoubleNumFactory}
|
||||
*
|
||||
* @param tickCount the number of ticks at which a new bar should be created
|
||||
*/
|
||||
public TickBarBuilder(final int tickCount) {
|
||||
this(DoubleNumFactory.getInstance(), tickCount, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param tickCount the number of ticks at which a new bar should be created
|
||||
*/
|
||||
public TickBarBuilder(final NumFactory numFactory, final int tickCount) {
|
||||
this(numFactory, tickCount, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param tickCount the number of ticks at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar} instances
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public TickBarBuilder(final NumFactory numFactory, final int tickCount, final boolean realtimeBars) {
|
||||
this.numFactory = numFactory;
|
||||
this.realtimeBars = realtimeBars;
|
||||
this.tickCount = tickCount;
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder timePeriod(final Duration timePeriod) {
|
||||
this.timePeriod = this.timePeriod == null ? timePeriod : this.timePeriod.plus(timePeriod);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder beginTime(final Instant beginTime) {
|
||||
this.beginTime = beginTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder endTime(final Instant endTime) {
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Num openPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Number openPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final String openPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Number highPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final String highPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Num highPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Num lowPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Number lowPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final String lowPrice) {
|
||||
throw new IllegalArgumentException("TickBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Num tickPrice) {
|
||||
closePrice = tickPrice;
|
||||
if (openPrice == null) {
|
||||
openPrice = tickPrice;
|
||||
}
|
||||
|
||||
highPrice = highPrice.max(tickPrice);
|
||||
lowPrice = lowPrice.min(tickPrice);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Number closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final String closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Num volume) {
|
||||
this.volume = this.volume.plus(volume);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Number volume) {
|
||||
volume(this.numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final String volume) {
|
||||
volume(this.numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Num amount) {
|
||||
this.amount = this.amount == null ? amount : this.amount.plus(amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Number amount) {
|
||||
amount(this.numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final String amount) {
|
||||
amount(this.numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final long trades) {
|
||||
this.trades += trades;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final String trades) {
|
||||
trades(Long.parseLong(trades));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TickBarBuilder bindTo(final BarSeries barSeries) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current tick bar and appends the bar once the tick
|
||||
* threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice) {
|
||||
addTrade(time, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current tick bar and appends the bar once the tick
|
||||
* threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice");
|
||||
ensureRealtimeTracking(side, liquidity);
|
||||
if (endTime != null && time.isBefore(endTime)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Trade time %s is before current bar end time %s", time, endTime));
|
||||
}
|
||||
if (beginTime == null) {
|
||||
beginTime = time;
|
||||
}
|
||||
endTime = time;
|
||||
closePrice(tradePrice);
|
||||
volume(tradeVolume);
|
||||
trades(1);
|
||||
recordRealtimeTrade(tradeVolume, tradePrice, side, liquidity);
|
||||
add();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds bar from current state that is modified for each tick.
|
||||
*
|
||||
* @return snapshot of current state
|
||||
*/
|
||||
@Override
|
||||
public Bar build() {
|
||||
if (realtimeBars) {
|
||||
return new BaseRealtimeBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice,
|
||||
volume, amount, trades, buyVolume, sellVolume, buyAmount, sellAmount, buyTrades, sellTrades,
|
||||
makerVolume, takerVolume, makerAmount, takerAmount, makerTrades, takerTrades, hasSideData,
|
||||
hasLiquidityData, numFactory);
|
||||
}
|
||||
return new BaseBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice, volume, amount,
|
||||
trades);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
if (++passedTicksCount % tickCount == 0) {
|
||||
if (amount == null && volume != null) {
|
||||
amount = closePrice.multipliedBy(volume);
|
||||
}
|
||||
|
||||
barSeries.addBar(build());
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
final var zero = numFactory.zero();
|
||||
timePeriod = null;
|
||||
beginTime = null;
|
||||
endTime = null;
|
||||
openPrice = null;
|
||||
highPrice = zero;
|
||||
lowPrice = numFactory.numOf(Integer.MAX_VALUE);
|
||||
closePrice = null;
|
||||
amount = null;
|
||||
trades = 0;
|
||||
volume = zero;
|
||||
buyVolume = null;
|
||||
sellVolume = null;
|
||||
buyAmount = null;
|
||||
sellAmount = null;
|
||||
buyTrades = 0;
|
||||
sellTrades = 0;
|
||||
hasSideData = false;
|
||||
makerVolume = null;
|
||||
takerVolume = null;
|
||||
makerAmount = null;
|
||||
takerAmount = null;
|
||||
makerTrades = 0;
|
||||
takerTrades = 0;
|
||||
hasLiquidityData = false;
|
||||
}
|
||||
|
||||
private void recordRealtimeTrade(final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
if (side != null) {
|
||||
hasSideData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (side == RealtimeBar.Side.BUY) {
|
||||
buyVolume = buyVolume == null ? tradeVolume : buyVolume.plus(tradeVolume);
|
||||
buyAmount = buyAmount == null ? tradeAmount : buyAmount.plus(tradeAmount);
|
||||
buyTrades++;
|
||||
} else {
|
||||
sellVolume = sellVolume == null ? tradeVolume : sellVolume.plus(tradeVolume);
|
||||
sellAmount = sellAmount == null ? tradeAmount : sellAmount.plus(tradeAmount);
|
||||
sellTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (liquidity != null) {
|
||||
hasLiquidityData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (liquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = makerVolume == null ? tradeVolume : makerVolume.plus(tradeVolume);
|
||||
makerAmount = makerAmount == null ? tradeAmount : makerAmount.plus(tradeAmount);
|
||||
makerTrades++;
|
||||
} else {
|
||||
takerVolume = takerVolume == null ? tradeVolume : takerVolume.plus(tradeVolume);
|
||||
takerAmount = takerAmount == null ? tradeAmount : takerAmount.plus(tradeAmount);
|
||||
takerTrades++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRealtimeTracking(final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
if (!realtimeBars && (side != null || liquidity != null)) {
|
||||
throw new IllegalStateException("Realtime trade data requires a realtime bar builder");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.ta4j.core.BarBuilderFactory;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
public class TickBarBuilderFactory implements BarBuilderFactory {
|
||||
|
||||
private final boolean realtimeBars;
|
||||
private final int tickCount;
|
||||
private transient TickBarBuilder barBuilder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tickCount the number of ticks at which a new bar should be created
|
||||
*/
|
||||
public TickBarBuilderFactory(final int tickCount) {
|
||||
this(tickCount, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tickCount the number of ticks at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar} instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public TickBarBuilderFactory(final int tickCount, final boolean realtimeBars) {
|
||||
this.tickCount = tickCount;
|
||||
this.realtimeBars = realtimeBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder createBarBuilder(final BarSeries series) {
|
||||
if (this.barBuilder == null) {
|
||||
this.barBuilder = new TickBarBuilder(series.numFactory(), this.tickCount, this.realtimeBars).bindTo(series);
|
||||
}
|
||||
|
||||
return this.barBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A time bar is sampled after a fixed time period.
|
||||
*
|
||||
* <p>
|
||||
* When ingesting trades, missing intervals are omitted. Bars are created only
|
||||
* when a trade arrives within a time period. If you need continuity, reconcile
|
||||
* and backfill OHLCV data upstream (often by fetching a window with overlap and
|
||||
* upserting by bar end time).
|
||||
*/
|
||||
public class TimeBarBuilder implements BarBuilder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TimeBarBuilder.class);
|
||||
|
||||
private final NumFactory numFactory;
|
||||
private final boolean realtimeBars;
|
||||
Duration timePeriod;
|
||||
Instant beginTime;
|
||||
Instant endTime;
|
||||
Num openPrice;
|
||||
Num highPrice;
|
||||
Num lowPrice;
|
||||
Num closePrice;
|
||||
Num volume;
|
||||
Num amount;
|
||||
long trades;
|
||||
private BarSeries baseBarSeries;
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private boolean hasSideData;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasLiquidityData;
|
||||
|
||||
/** A builder to build a new {@link BaseBar} with {@link DoubleNumFactory} */
|
||||
public TimeBarBuilder() {
|
||||
this(DoubleNumFactory.getInstance(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar}
|
||||
*
|
||||
* @param numFactory
|
||||
*/
|
||||
public TimeBarBuilder(final NumFactory numFactory) {
|
||||
this(numFactory, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar} instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public TimeBarBuilder(final NumFactory numFactory, final boolean realtimeBars) {
|
||||
this.numFactory = numFactory;
|
||||
this.realtimeBars = realtimeBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder timePeriod(final Duration timePeriod) {
|
||||
this.timePeriod = timePeriod;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder beginTime(final Instant beginTime) {
|
||||
this.beginTime = beginTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder endTime(final Instant endTime) {
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Num openPrice) {
|
||||
this.openPrice = openPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Number openPrice) {
|
||||
openPrice(this.numFactory.numOf(openPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final String openPrice) {
|
||||
openPrice(this.numFactory.numOf(openPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Number highPrice) {
|
||||
highPrice(this.numFactory.numOf(highPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final String highPrice) {
|
||||
highPrice(this.numFactory.numOf(highPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Num highPrice) {
|
||||
this.highPrice = highPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Num lowPrice) {
|
||||
this.lowPrice = lowPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Number lowPrice) {
|
||||
lowPrice(this.numFactory.numOf(lowPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final String lowPrice) {
|
||||
lowPrice(this.numFactory.numOf(lowPrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Num closePrice) {
|
||||
this.closePrice = closePrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Number closePrice) {
|
||||
closePrice(this.numFactory.numOf(closePrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final String closePrice) {
|
||||
closePrice(this.numFactory.numOf(closePrice));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Num volume) {
|
||||
this.volume = volume;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Number volume) {
|
||||
volume(this.numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final String volume) {
|
||||
volume(this.numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Num amount) {
|
||||
this.amount = amount;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Number amount) {
|
||||
amount(this.numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final String amount) {
|
||||
amount(this.numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final long trades) {
|
||||
this.trades = trades;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final String trades) {
|
||||
trades(Long.parseLong(trades));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder bindTo(final BarSeries barSeries) {
|
||||
this.baseBarSeries = Objects.requireNonNull(barSeries);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current time bar and adds/replaces the bar in the
|
||||
* bound series. Bars are aligned to UTC epoch boundaries based on the current
|
||||
* {@link #timePeriod}. When the trade time skips one or more full periods,
|
||||
* those intervals are omitted; no bars are inserted for the gap.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice) {
|
||||
addTrade(time, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current time bar and adds/replaces the bar in the
|
||||
* bound series. Bars are aligned to UTC epoch boundaries based on the current
|
||||
* {@link #timePeriod}. When the trade time skips one or more full periods,
|
||||
* those intervals are omitted; no bars are inserted for the gap.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice");
|
||||
ensureRealtimeTracking(side, liquidity);
|
||||
if (timePeriod == null) {
|
||||
throw new IllegalStateException("Time period must be set before ingesting trades");
|
||||
}
|
||||
Objects.requireNonNull(baseBarSeries, "barSeries");
|
||||
ensureTimeRange(time);
|
||||
if (time.isBefore(beginTime)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Trade time %s is before current bar begin time %s", time, beginTime));
|
||||
}
|
||||
Instant previousEndTime = endTime;
|
||||
long skippedPeriods = 0;
|
||||
while (!time.isBefore(endTime)) {
|
||||
persistCurrentBarIfPresent();
|
||||
resetTradeState();
|
||||
beginTime = endTime;
|
||||
endTime = beginTime.plus(timePeriod);
|
||||
skippedPeriods++;
|
||||
}
|
||||
if (skippedPeriods > 1) {
|
||||
long missingPeriods = skippedPeriods - 1;
|
||||
LOG.warn("Detected {} missing bar period(s) between {} and {} for series {}", missingPeriods,
|
||||
previousEndTime, beginTime, baseBarSeries.getName());
|
||||
}
|
||||
recordTrade(tradeVolume, tradePrice, side, liquidity);
|
||||
baseBarSeries.addBar(build(), shouldReplaceCurrentBar());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bar build() {
|
||||
if (realtimeBars) {
|
||||
return new BaseRealtimeBar(this.timePeriod, this.beginTime, this.endTime, this.openPrice, this.highPrice,
|
||||
this.lowPrice, this.closePrice, this.volume, this.amount, this.trades, buyVolume, sellVolume,
|
||||
buyAmount, sellAmount, buyTrades, sellTrades, makerVolume, takerVolume, makerAmount, takerAmount,
|
||||
makerTrades, takerTrades, hasSideData, hasLiquidityData, numFactory);
|
||||
}
|
||||
return new BaseBar(this.timePeriod, this.beginTime, this.endTime, this.openPrice, this.highPrice, this.lowPrice,
|
||||
this.closePrice, this.volume, this.amount, this.trades);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
if (amount == null && closePrice != null && volume != null) {
|
||||
amount = closePrice.multipliedBy(volume);
|
||||
}
|
||||
|
||||
this.baseBarSeries.addBar(build());
|
||||
}
|
||||
|
||||
private void ensureTimeRange(final Instant time) {
|
||||
if (beginTime == null && endTime == null) {
|
||||
beginTime = alignToTimePeriodStart(time);
|
||||
endTime = beginTime.plus(timePeriod);
|
||||
return;
|
||||
}
|
||||
if (beginTime == null) {
|
||||
beginTime = endTime.minus(timePeriod);
|
||||
} else if (endTime == null) {
|
||||
endTime = beginTime.plus(timePeriod);
|
||||
}
|
||||
}
|
||||
|
||||
private Instant alignToTimePeriodStart(final Instant time) {
|
||||
try {
|
||||
final long periodNanos = timePeriod.toNanos();
|
||||
if (periodNanos <= 0) {
|
||||
throw new IllegalStateException("Time period must be positive");
|
||||
}
|
||||
final long timeNanos = Math.addExact(Math.multiplyExact(time.getEpochSecond(), 1_000_000_000L),
|
||||
time.getNano());
|
||||
final long alignedNanos = timeNanos - Math.floorMod(timeNanos, periodNanos);
|
||||
final long alignedSeconds = Math.floorDiv(alignedNanos, 1_000_000_000L);
|
||||
final int alignedNanoPart = (int) Math.floorMod(alignedNanos, 1_000_000_000L);
|
||||
return Instant.ofEpochSecond(alignedSeconds, alignedNanoPart);
|
||||
} catch (ArithmeticException ex) {
|
||||
throw new IllegalStateException("Time period too large to align trade time", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordTrade(final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
if (openPrice == null) {
|
||||
openPrice = tradePrice;
|
||||
highPrice = tradePrice;
|
||||
lowPrice = tradePrice;
|
||||
} else {
|
||||
highPrice = highPrice == null ? tradePrice : highPrice.max(tradePrice);
|
||||
lowPrice = lowPrice == null ? tradePrice : lowPrice.min(tradePrice);
|
||||
}
|
||||
closePrice = tradePrice;
|
||||
volume = volume == null ? tradeVolume : volume.plus(tradeVolume);
|
||||
Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
amount = amount == null ? tradeAmount : amount.plus(tradeAmount);
|
||||
trades++;
|
||||
|
||||
if (side != null) {
|
||||
hasSideData = true;
|
||||
if (side == RealtimeBar.Side.BUY) {
|
||||
buyVolume = buyVolume == null ? tradeVolume : buyVolume.plus(tradeVolume);
|
||||
buyAmount = buyAmount == null ? tradeAmount : buyAmount.plus(tradeAmount);
|
||||
buyTrades++;
|
||||
} else {
|
||||
sellVolume = sellVolume == null ? tradeVolume : sellVolume.plus(tradeVolume);
|
||||
sellAmount = sellAmount == null ? tradeAmount : sellAmount.plus(tradeAmount);
|
||||
sellTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (liquidity != null) {
|
||||
hasLiquidityData = true;
|
||||
if (liquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = makerVolume == null ? tradeVolume : makerVolume.plus(tradeVolume);
|
||||
makerAmount = makerAmount == null ? tradeAmount : makerAmount.plus(tradeAmount);
|
||||
makerTrades++;
|
||||
} else {
|
||||
takerVolume = takerVolume == null ? tradeVolume : takerVolume.plus(tradeVolume);
|
||||
takerAmount = takerAmount == null ? tradeAmount : takerAmount.plus(tradeAmount);
|
||||
takerTrades++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistCurrentBarIfPresent() {
|
||||
if (!hasBarData()) {
|
||||
return;
|
||||
}
|
||||
if (amount == null && closePrice != null && volume != null) {
|
||||
amount = closePrice.multipliedBy(volume);
|
||||
}
|
||||
baseBarSeries.addBar(build(), shouldReplaceCurrentBar());
|
||||
}
|
||||
|
||||
private boolean shouldReplaceCurrentBar() {
|
||||
if (baseBarSeries == null || baseBarSeries.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return baseBarSeries.getLastBar().getEndTime().equals(endTime);
|
||||
}
|
||||
|
||||
private boolean hasBarData() {
|
||||
return openPrice != null || highPrice != null || lowPrice != null || closePrice != null || volume != null
|
||||
|| amount != null || trades > 0 || hasSideData || hasLiquidityData || buyVolume != null
|
||||
|| sellVolume != null || buyAmount != null || sellAmount != null || buyTrades > 0 || sellTrades > 0
|
||||
|| makerVolume != null || takerVolume != null || makerAmount != null || takerAmount != null
|
||||
|| makerTrades > 0 || takerTrades > 0;
|
||||
}
|
||||
|
||||
private void resetTradeState() {
|
||||
openPrice = null;
|
||||
highPrice = null;
|
||||
lowPrice = null;
|
||||
closePrice = null;
|
||||
volume = null;
|
||||
amount = null;
|
||||
trades = 0;
|
||||
buyVolume = null;
|
||||
sellVolume = null;
|
||||
buyAmount = null;
|
||||
sellAmount = null;
|
||||
buyTrades = 0;
|
||||
sellTrades = 0;
|
||||
hasSideData = false;
|
||||
makerVolume = null;
|
||||
takerVolume = null;
|
||||
makerAmount = null;
|
||||
takerAmount = null;
|
||||
makerTrades = 0;
|
||||
takerTrades = 0;
|
||||
hasLiquidityData = false;
|
||||
}
|
||||
|
||||
private void ensureRealtimeTracking(final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
if (!realtimeBars && (side != null || liquidity != null)) {
|
||||
throw new IllegalStateException("Realtime trade data requires a realtime bar builder");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.ta4j.core.BarBuilderFactory;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
public class TimeBarBuilderFactory implements BarBuilderFactory {
|
||||
|
||||
private final boolean realtimeBars;
|
||||
private final Duration timePeriod;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public TimeBarBuilderFactory() {
|
||||
this(null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar} instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public TimeBarBuilderFactory(final boolean realtimeBars) {
|
||||
this(null, realtimeBars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param timePeriod the default time period for time bars
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public TimeBarBuilderFactory(final Duration timePeriod) {
|
||||
this(timePeriod, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param timePeriod the default time period for time bars
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar} instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public TimeBarBuilderFactory(final Duration timePeriod, final boolean realtimeBars) {
|
||||
this.timePeriod = timePeriod;
|
||||
this.realtimeBars = realtimeBars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder createBarBuilder(BarSeries series) {
|
||||
BarBuilder builder = new TimeBarBuilder(series.numFactory(), realtimeBars).bindTo(series);
|
||||
if (timePeriod != null) {
|
||||
builder.timePeriod(timePeriod);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBar;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.RealtimeBar;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
import org.ta4j.core.num.Num;
|
||||
import org.ta4j.core.num.NumFactory;
|
||||
|
||||
/**
|
||||
* A volume bar is sampled after a fixed number of contracts (volume) have been
|
||||
* traded.
|
||||
*/
|
||||
public class VolumeBarBuilder implements BarBuilder {
|
||||
|
||||
private final NumFactory numFactory;
|
||||
private final RemainderCarryOverPolicy carryOverPolicy;
|
||||
private final boolean realtimeBars;
|
||||
private final Num volumeThreshold;
|
||||
private BarSeries barSeries;
|
||||
private Duration timePeriod;
|
||||
private Instant beginTime;
|
||||
private Instant endTime;
|
||||
private Num volume;
|
||||
private Num openPrice;
|
||||
private Num highPrice;
|
||||
private Num closePrice;
|
||||
private Num lowPrice;
|
||||
private Num amount;
|
||||
private long trades;
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private boolean hasSideData;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasLiquidityData;
|
||||
private Num lastTradeVolume;
|
||||
private Num lastTradePrice;
|
||||
private RealtimeBar.Side lastTradeSide;
|
||||
private RealtimeBar.Liquidity lastTradeLiquidity;
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} with {@link DoubleNumFactory}
|
||||
*
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
*/
|
||||
public VolumeBarBuilder(final int volumeThreshold) {
|
||||
this(DoubleNumFactory.getInstance(), volumeThreshold, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
*/
|
||||
public VolumeBarBuilder(final NumFactory numFactory, final int volumeThreshold) {
|
||||
this(numFactory, volumeThreshold, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public VolumeBarBuilder(final NumFactory numFactory, final int volumeThreshold, final boolean realtimeBars) {
|
||||
this(numFactory, volumeThreshold, realtimeBars, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder to build a new {@link BaseBar} or {@link BaseRealtimeBar}
|
||||
*
|
||||
* @param numFactory the backing number factory
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public VolumeBarBuilder(final NumFactory numFactory, final int volumeThreshold, final boolean realtimeBars,
|
||||
final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this.numFactory = numFactory;
|
||||
this.carryOverPolicy = carryOverPolicy == null ? RemainderCarryOverPolicy.NONE : carryOverPolicy;
|
||||
this.realtimeBars = realtimeBars;
|
||||
this.volumeThreshold = numFactory.numOf(volumeThreshold);
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder timePeriod(final Duration timePeriod) {
|
||||
this.timePeriod = this.timePeriod == null ? timePeriod : this.timePeriod.plus(timePeriod);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder beginTime(final Instant beginTime) {
|
||||
this.beginTime = beginTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder endTime(final Instant endTime) {
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Num openPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final Number openPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder openPrice(final String openPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Number highPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final String highPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder highPrice(final Num highPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Num lowPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final Number lowPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder lowPrice(final String lowPrice) {
|
||||
throw new IllegalArgumentException("VolumeBar can only be built from closePrice");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Num tickPrice) {
|
||||
closePrice = tickPrice;
|
||||
if (openPrice == null) {
|
||||
openPrice = tickPrice;
|
||||
}
|
||||
|
||||
highPrice = highPrice.max(tickPrice);
|
||||
lowPrice = lowPrice.min(tickPrice);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final Number closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder closePrice(final String closePrice) {
|
||||
return closePrice(numFactory.numOf(closePrice));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Num volume) {
|
||||
this.volume = this.volume == null ? volume : this.volume.plus(volume);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final Number volume) {
|
||||
volume(numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder volume(final String volume) {
|
||||
volume(numFactory.numOf(volume));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Num amount) {
|
||||
this.amount = this.amount == null ? amount : this.amount.plus(amount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final Number amount) {
|
||||
amount(numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder amount(final String amount) {
|
||||
amount(numFactory.numOf(amount));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final long trades) {
|
||||
this.trades += trades;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder trades(final String trades) {
|
||||
trades(Long.parseLong(trades));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeBarBuilder bindTo(final BarSeries barSeries) {
|
||||
this.barSeries = Objects.requireNonNull(barSeries);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current volume bar and appends the bar once the
|
||||
* volume threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice) {
|
||||
addTrade(time, tradeVolume, tradePrice, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingests a trade into the current volume bar and appends the bar once the
|
||||
* volume threshold is met.
|
||||
*
|
||||
* @param time the trade timestamp (UTC)
|
||||
* @param tradeVolume the traded volume
|
||||
* @param tradePrice the traded price
|
||||
* @param side aggressor side (optional)
|
||||
* @param liquidity liquidity classification (optional)
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
@Override
|
||||
public void addTrade(final Instant time, final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
Objects.requireNonNull(time, "time");
|
||||
Objects.requireNonNull(tradeVolume, "tradeVolume");
|
||||
Objects.requireNonNull(tradePrice, "tradePrice");
|
||||
ensureRealtimeTracking(side, liquidity);
|
||||
if (endTime != null && time.isBefore(endTime)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Trade time %s is before current bar end time %s", time, endTime));
|
||||
}
|
||||
if (beginTime == null) {
|
||||
beginTime = time;
|
||||
}
|
||||
endTime = time;
|
||||
closePrice(tradePrice);
|
||||
volume(tradeVolume);
|
||||
trades(1);
|
||||
lastTradeVolume = tradeVolume;
|
||||
lastTradePrice = tradePrice;
|
||||
lastTradeSide = side;
|
||||
lastTradeLiquidity = liquidity;
|
||||
recordRealtimeTrade(tradeVolume, tradePrice, side, liquidity);
|
||||
add();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds bar from current state that is modified for each tick.
|
||||
*
|
||||
* @return snapshot of current state
|
||||
*/
|
||||
@Override
|
||||
public Bar build() {
|
||||
if (realtimeBars) {
|
||||
return new BaseRealtimeBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice,
|
||||
volume, amount, trades, buyVolume, sellVolume, buyAmount, sellAmount, buyTrades, sellTrades,
|
||||
makerVolume, takerVolume, makerAmount, takerAmount, makerTrades, takerTrades, hasSideData,
|
||||
hasLiquidityData, numFactory);
|
||||
}
|
||||
return new BaseBar(timePeriod, beginTime, endTime, openPrice, highPrice, lowPrice, closePrice, volume, amount,
|
||||
trades);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
if (volume.isGreaterThanOrEqual(volumeThreshold)) {
|
||||
// move volume remainder to next bar
|
||||
var volumeRemainder = numFactory.zero();
|
||||
CarryOverSnapshot carryOverSnapshot = null;
|
||||
if (volume.isGreaterThan(volumeThreshold)) {
|
||||
volumeRemainder = volume.minus(volumeThreshold);
|
||||
// cap currently built bar, volume is then restored to volumeRemainder
|
||||
volume = volumeThreshold;
|
||||
if (carryOverPolicy == RemainderCarryOverPolicy.PROPORTIONAL
|
||||
|| carryOverPolicy == RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT) {
|
||||
carryOverSnapshot = applyProportionalCarryOver(volumeRemainder);
|
||||
}
|
||||
}
|
||||
|
||||
if (amount == null) {
|
||||
amount = closePrice.multipliedBy(volume);
|
||||
}
|
||||
|
||||
barSeries.addBar(build());
|
||||
volume = volumeRemainder;
|
||||
|
||||
reset();
|
||||
if (carryOverSnapshot != null) {
|
||||
carryOverSnapshot.applyTo(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
timePeriod = null;
|
||||
beginTime = null;
|
||||
endTime = null;
|
||||
openPrice = null;
|
||||
highPrice = numFactory.zero();
|
||||
lowPrice = numFactory.numOf(Integer.MAX_VALUE);
|
||||
amount = null;
|
||||
trades = 0;
|
||||
closePrice = null;
|
||||
buyVolume = null;
|
||||
sellVolume = null;
|
||||
buyAmount = null;
|
||||
sellAmount = null;
|
||||
buyTrades = 0;
|
||||
sellTrades = 0;
|
||||
hasSideData = false;
|
||||
makerVolume = null;
|
||||
takerVolume = null;
|
||||
makerAmount = null;
|
||||
takerAmount = null;
|
||||
makerTrades = 0;
|
||||
takerTrades = 0;
|
||||
hasLiquidityData = false;
|
||||
lastTradeVolume = null;
|
||||
lastTradePrice = null;
|
||||
lastTradeSide = null;
|
||||
lastTradeLiquidity = null;
|
||||
}
|
||||
|
||||
private CarryOverSnapshot applyProportionalCarryOver(final Num volumeRemainder) {
|
||||
if (volumeRemainder == null || volumeRemainder.isZero() || lastTradeVolume == null || lastTradePrice == null) {
|
||||
return null;
|
||||
}
|
||||
final CarryOverSnapshot snapshot = new CarryOverSnapshot();
|
||||
final Num remainderAmount = lastTradePrice.multipliedBy(volumeRemainder);
|
||||
final boolean carryTradeCount = shouldCarryTradeCount(volumeRemainder);
|
||||
if (lastTradeSide != null) {
|
||||
if (lastTradeSide == RealtimeBar.Side.BUY) {
|
||||
buyVolume = subtractOrNull(buyVolume, volumeRemainder);
|
||||
buyAmount = subtractOrNull(buyAmount, remainderAmount);
|
||||
snapshot.buyVolume = volumeRemainder;
|
||||
snapshot.buyAmount = remainderAmount;
|
||||
if (carryTradeCount) {
|
||||
buyTrades = Math.max(0, buyTrades - 1);
|
||||
snapshot.buyTrades = 1;
|
||||
}
|
||||
} else {
|
||||
sellVolume = subtractOrNull(sellVolume, volumeRemainder);
|
||||
sellAmount = subtractOrNull(sellAmount, remainderAmount);
|
||||
snapshot.sellVolume = volumeRemainder;
|
||||
snapshot.sellAmount = remainderAmount;
|
||||
if (carryTradeCount) {
|
||||
sellTrades = Math.max(0, sellTrades - 1);
|
||||
snapshot.sellTrades = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastTradeLiquidity != null) {
|
||||
if (lastTradeLiquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = subtractOrNull(makerVolume, volumeRemainder);
|
||||
makerAmount = subtractOrNull(makerAmount, remainderAmount);
|
||||
snapshot.makerVolume = volumeRemainder;
|
||||
snapshot.makerAmount = remainderAmount;
|
||||
if (carryTradeCount) {
|
||||
makerTrades = Math.max(0, makerTrades - 1);
|
||||
snapshot.makerTrades = 1;
|
||||
}
|
||||
} else {
|
||||
takerVolume = subtractOrNull(takerVolume, volumeRemainder);
|
||||
takerAmount = subtractOrNull(takerAmount, remainderAmount);
|
||||
snapshot.takerVolume = volumeRemainder;
|
||||
snapshot.takerAmount = remainderAmount;
|
||||
if (carryTradeCount) {
|
||||
takerTrades = Math.max(0, takerTrades - 1);
|
||||
snapshot.takerTrades = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (carryTradeCount) {
|
||||
trades = Math.max(0, trades - 1);
|
||||
snapshot.trades = 1;
|
||||
}
|
||||
snapshot.hasSideData = snapshot.buyVolume != null || snapshot.sellVolume != null;
|
||||
snapshot.hasLiquidityData = snapshot.makerVolume != null || snapshot.takerVolume != null;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private boolean shouldCarryTradeCount(final Num volumeRemainder) {
|
||||
if (carryOverPolicy != RemainderCarryOverPolicy.PROPORTIONAL_WITH_TRADE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
if (lastTradeVolume == null || lastTradeVolume.isZero()) {
|
||||
return false;
|
||||
}
|
||||
return volumeRemainder.multipliedBy(numFactory.numOf(2)).isGreaterThanOrEqual(lastTradeVolume);
|
||||
}
|
||||
|
||||
private Num subtractOrNull(final Num current, final Num remainder) {
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
final Num updated = current.minus(remainder);
|
||||
return updated.isZero() ? null : updated;
|
||||
}
|
||||
|
||||
private static final class CarryOverSnapshot {
|
||||
private Num buyVolume;
|
||||
private Num sellVolume;
|
||||
private Num buyAmount;
|
||||
private Num sellAmount;
|
||||
private Num makerVolume;
|
||||
private Num takerVolume;
|
||||
private Num makerAmount;
|
||||
private Num takerAmount;
|
||||
private long trades;
|
||||
private long buyTrades;
|
||||
private long sellTrades;
|
||||
private long makerTrades;
|
||||
private long takerTrades;
|
||||
private boolean hasSideData;
|
||||
private boolean hasLiquidityData;
|
||||
|
||||
private void applyTo(final VolumeBarBuilder builder) {
|
||||
builder.buyVolume = buyVolume;
|
||||
builder.sellVolume = sellVolume;
|
||||
builder.buyAmount = buyAmount;
|
||||
builder.sellAmount = sellAmount;
|
||||
builder.makerVolume = makerVolume;
|
||||
builder.takerVolume = takerVolume;
|
||||
builder.makerAmount = makerAmount;
|
||||
builder.takerAmount = takerAmount;
|
||||
builder.trades = trades;
|
||||
builder.buyTrades = buyTrades;
|
||||
builder.sellTrades = sellTrades;
|
||||
builder.makerTrades = makerTrades;
|
||||
builder.takerTrades = takerTrades;
|
||||
builder.hasSideData = hasSideData;
|
||||
builder.hasLiquidityData = hasLiquidityData;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordRealtimeTrade(final Num tradeVolume, final Num tradePrice, final RealtimeBar.Side side,
|
||||
final RealtimeBar.Liquidity liquidity) {
|
||||
if (side != null) {
|
||||
hasSideData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (side == RealtimeBar.Side.BUY) {
|
||||
buyVolume = buyVolume == null ? tradeVolume : buyVolume.plus(tradeVolume);
|
||||
buyAmount = buyAmount == null ? tradeAmount : buyAmount.plus(tradeAmount);
|
||||
buyTrades++;
|
||||
} else {
|
||||
sellVolume = sellVolume == null ? tradeVolume : sellVolume.plus(tradeVolume);
|
||||
sellAmount = sellAmount == null ? tradeAmount : sellAmount.plus(tradeAmount);
|
||||
sellTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (liquidity != null) {
|
||||
hasLiquidityData = true;
|
||||
final Num tradeAmount = tradePrice.multipliedBy(tradeVolume);
|
||||
if (liquidity == RealtimeBar.Liquidity.MAKER) {
|
||||
makerVolume = makerVolume == null ? tradeVolume : makerVolume.plus(tradeVolume);
|
||||
makerAmount = makerAmount == null ? tradeAmount : makerAmount.plus(tradeAmount);
|
||||
makerTrades++;
|
||||
} else {
|
||||
takerVolume = takerVolume == null ? tradeVolume : takerVolume.plus(tradeVolume);
|
||||
takerAmount = takerAmount == null ? tradeAmount : takerAmount.plus(tradeAmount);
|
||||
takerTrades++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRealtimeTracking(final RealtimeBar.Side side, final RealtimeBar.Liquidity liquidity) {
|
||||
if (!realtimeBars && (side != null || liquidity != null)) {
|
||||
throw new IllegalStateException("Realtime trade data requires a realtime bar builder");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
package org.ta4j.core.bars;
|
||||
|
||||
import org.ta4j.core.BarBuilderFactory;
|
||||
import org.ta4j.core.BaseRealtimeBar;
|
||||
import org.ta4j.core.BarBuilder;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
public class VolumeBarBuilderFactory implements BarBuilderFactory {
|
||||
|
||||
private final int volumeThreshold;
|
||||
private final RemainderCarryOverPolicy carryOverPolicy;
|
||||
private final boolean realtimeBars;
|
||||
private transient VolumeBarBuilder barBuilder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
*/
|
||||
public VolumeBarBuilderFactory(final int volumeThreshold) {
|
||||
this(volumeThreshold, false, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public VolumeBarBuilderFactory(final int volumeThreshold, final boolean realtimeBars) {
|
||||
this(volumeThreshold, realtimeBars, RemainderCarryOverPolicy.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.0
|
||||
*/
|
||||
public VolumeBarBuilderFactory(final int volumeThreshold, final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this(volumeThreshold, false, carryOverPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param volumeThreshold the threshold at which a new bar should be created
|
||||
* @param realtimeBars {@code true} to build {@link BaseRealtimeBar}
|
||||
* instances
|
||||
* @param carryOverPolicy policy for handling side/liquidity remainder splits
|
||||
*
|
||||
* @since 0.22.2
|
||||
*/
|
||||
public VolumeBarBuilderFactory(final int volumeThreshold, final boolean realtimeBars,
|
||||
final RemainderCarryOverPolicy carryOverPolicy) {
|
||||
this.volumeThreshold = volumeThreshold;
|
||||
this.realtimeBars = realtimeBars;
|
||||
this.carryOverPolicy = carryOverPolicy == null ? RemainderCarryOverPolicy.NONE : carryOverPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarBuilder createBarBuilder(final BarSeries series) {
|
||||
if (this.barBuilder == null) {
|
||||
this.barBuilder = new VolumeBarBuilder(series.numFactory(), this.volumeThreshold, this.realtimeBars,
|
||||
this.carryOverPolicy).bindTo(series);
|
||||
}
|
||||
|
||||
return this.barBuilder;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user