A Java library that provides a variety of technical indicators for time series data, such as OHLC bars.
For build.gradle.kts
:
implementation("trade.invision", "indicators", "1.2.0")
For build.gradle
:
implementation group: 'trade.invision', name: 'indicators', version: '1.2.0'
For pom.xml
:
<dependency>
<groupId>trade.invision</groupId>
<artifactId>indicators</artifactId>
<version>1.2.0</version>
</dependency>
There are two base classes in this library to be aware of:
Series
and
Indicator
.
Series
is a class that provides a simple interface for storing and retrieving values from a series. Values
can only be added to the Series
, except for the last value, which can be replaced with a new value. When the
number of values stored in the Series
exceed a configurable maximum upon adding a new value, then values at
the beginning of the Series
are removed. This emulates the behavior of a bar/candlestick chart that displays
historical bars along with a continuously-updating end bar that reflects live trade data. There are two types of
Series
this library provides: BarSeries
(for a series of Bars
) and
NumSeries
(for a series of
NumDatapoints
).
Indicator
is an abstract class for performing calculations on a Series
or another Indicator
, with optional result
caching. An Indicator
will implement some formula/algorithm and provide the result of the calculation at a given
index. Some Indicator
implementations and consumer access patterns may retrieve calculated results at
previously-calculated indices. So, in order to prevent redundantly recalculating the results at the same index
, the
Indicator
can cache its results in memory, with the cache size being the same as the Series
maximum length. Result
caching is disabled by default since the typical access pattern is to continually calculate the Indicator
value at the
end index of a Series
and perform some action based on that result. The cache should be enabled when consumers of an
Indicator
use the calculated values of non-ending indices, as opposed to only using the calculated value of the end
index. Indicator
implementations that utilize recursion should extend
RecursiveIndicator
, which forces caching
and prevents
StackOverflowError
exceptions. Typically, an Indicator
will be one of the following three types:
Num
(decimal number),
Boolean
(true/false), or
Instant
(timestamp). Each
Indicator
implementation provides a public static method for consumers to acquire an instance reference to the
Indicator
, as the constructor for Indicator
implementations is protected
and cannot be instantiated directly.
Check out the Javadoc for all classes and method signatures, but here's a simple example:
Imports
import trade.invision.indicators.indicators.Indicator;
import trade.invision.indicators.indicators.barprice.Hlc3;
import trade.invision.indicators.indicators.barprice.Ohlc4;
import trade.invision.indicators.indicators.bullishbearish.global.GlobalBullishPercentage;
import trade.invision.indicators.indicators.crossed.Crossed;
import trade.invision.indicators.indicators.ma.sma.SimpleMovingAverage;
import trade.invision.indicators.indicators.statistical.StandardDeviation;
import trade.invision.indicators.series.bar.Bar;
import trade.invision.indicators.series.bar.BarSeries;
import trade.invision.num.Num;
import java.io.InputStream;
import java.net.URI;
import java.time.Instant;
import static java.lang.Long.parseLong;
import static java.time.Instant.ofEpochMilli;
import static java.time.temporal.ChronoUnit.DAYS;
import static trade.invision.indicators.indicators.bar.Close.close;
import static trade.invision.indicators.indicators.barprice.Hlc3.typicalPrice;
import static trade.invision.indicators.indicators.barprice.Ohlc4.ohlc4;
import static trade.invision.indicators.indicators.bullishbearish.global.GlobalBullishPercentage.globalBullishPercentage;
import static trade.invision.indicators.indicators.crossed.Crossed.crossed;
import static trade.invision.indicators.indicators.ma.MovingAverageSupplier.wwmaSupplier;
import static trade.invision.indicators.indicators.ma.sma.SimpleMovingAverage.simpleMovingAverage;
import static trade.invision.indicators.indicators.rsi.RelativeStrengthIndex.rsi;
import static trade.invision.indicators.indicators.statistical.StandardDeviation.stddev;
final BarSeries barSeries = new BarSeries(20); // Store a maximum of 20 bars in memory
// Get AAPL 1D bar data from 2025-01-01 to 2025-02-01.
try (final InputStream inputStream = URI.create("https://pastebin.com/raw/dnLSgw44").toURL().openStream()) {
for (String barDataLine : new String(inputStream.readAllBytes()).split("\n")) {
final String[] barDataCsv = barDataLine.trim().split(",");
final Instant start = ofEpochMilli(parseLong(barDataCsv[5]));
final Instant end = start.plus(1, DAYS);
final Num open = barSeries.numOf(barDataCsv[1]);
final Num high = barSeries.numOf(barDataCsv[3]);
final Num low = barSeries.numOf(barDataCsv[4]);
final Num close = barSeries.numOf(barDataCsv[2]);
final Num volume = barSeries.numOf(barDataCsv[0]);
final Num tradeCount = barSeries.numOf(barDataCsv[6]);
final Bar bar = new Bar(start, end, open, high, low, close, volume, tradeCount);
barSeries.add(bar);
System.out.println("Added: " + bar);
}
}
// Calculate the Simple Moving Average (SMA) using the bar average price.
final Ohlc4 averagePrice = ohlc4(barSeries);
final SimpleMovingAverage sma = simpleMovingAverage(averagePrice, 20);
for (long index = barSeries.getStartIndex(); index <= barSeries.getEndIndex(); index++) {
System.out.printf("OHLC4 20 SMA at %d: %s\n", index, sma.getValue(index));
}
// Calculate the Standard Deviation (stddev) using the bar typical price.
final Hlc3 typicalPrice = typicalPrice(barSeries);
final StandardDeviation stddev = stddev(typicalPrice, 5, false);
for (long index = barSeries.getStartIndex(); index <= barSeries.getEndIndex(); index++) {
System.out.printf("HLC3 5 STDDEV at %d: %s\n", index, stddev.getValue(index));
}
// Calculate the Relative Strength Index (RSI) using the bar close price and Welles Wilder Moving Average (WWMA).
final Indicator<Num> close = close(barSeries);
final Indicator<Num> rsi = rsi(close, 10, wwmaSupplier());
for (long index = barSeries.getStartIndex(); index <= barSeries.getEndIndex(); index++) {
System.out.printf("C 10 RSI at %d: %s\n", index, rsi.getValue(index));
}
// Calculate the percentage of bullish bars.
final GlobalBullishPercentage globalBullishPercentage = globalBullishPercentage(barSeries);
for (long index = barSeries.getStartIndex(); index <= barSeries.getEndIndex(); index++) {
System.out.printf("Bullish %% at %d: %s%%\n", index,
globalBullishPercentage.getValue(index).multiply(100).round());
}
// Calculate SMA crossovers with bar close price.
final Crossed crossed = crossed(sma, close);
for (long index = barSeries.getStartIndex(); index <= barSeries.getEndIndex(); index++) {
System.out.printf("OHLC4 20 SMA crossed C at %d: %s\n", index, crossed.getValue(index));
}
This library was inspired by the excellent ta4j library. There are several improvements and additions that this library provides:
- Usage of improved
Num
interface via the Num library. - Configurable
Indicator
result caching. Indicator
instance caching.- Several optimizations to reduce memory consumption and improve execution time for common access patterns (e.g.
consecutive indices, identical indices, caching and reusing
Indicator
instances with identical configurations). - Configurable moving average types for
Indicator
implementations that utilize moving averages. - Configurable epsilon per
Series
. - Generic
Series
interface, allowing forNumSeries
and such. - Documentation improvements.
- Improved package structure.
- Several more helper/utility
Indicator
implementations.
Contributions are welcome. Use existing Indicator
class implementations as a reference for new implementations. A few
guidelines for creating Indicator
class implementations are as follows:
- There should only be one constructor and it should use the
protected
access modifier. - Create public static methods for consumers to use for instantiation or retrieval from a weak-valued instance cache. The method name should be the full indicator name. Add overloads for acronyms.
- All implementations should implement a weak-valued instance cache, except for implementations that are subclasses of
CachelessIndicator
orNum
/Boolean
/Instant
operations (e.g.UnaryOperation
,BinaryOperation
, andTernaryOperation
). - The class Javadoc should include an
@see
website reference for the indicator formula/algorithm. - Avoid declarative calculations via the
UnaryOperation
,BinaryOperation
, orTernaryOperation
indicators. Prefer to imperatively calculate results in thecalculate
method. - Avoid creating anonymous
Indicator
instances. - Prefer to use
Num
,Boolean
, orInstant
as the type for theIndicator
. UseNum
even in cases where anIndicator
only uses integer values. This greatly improves interoperability betweenIndicator
implementations and is more convenient.