Since release 0.12 Ta4j supports using different types for basic calculations proceeded in Indicator
or BarSeries
. That means you have the possibility to write your own implementation of the Num
interface or you can choose between existing implementations. At the moment there are two existing implementations available: PrecisionNum
(default) and DoubleNum
. As the names suggest, the available implementations use different types (delegates) for arithmetic calculations. DoubleNum
uses the double
primitive and PrecisionNum
uses the BigDecimal
class for calculations. The following code snippets illustrate the difference:
Plus operation of DoubleNum
:
@Override
public Num plus(Num augend) {
return augend.isNaN() ? NaN : new DoubleNum(delegate + ((DoubleNum)augend).delegate);
}
Plus operation of PrecisionNum
:
public Num plus(Num augend) {
return augend.isNaN() ? NaN : new BigDecimalNum(delegate.add(((BigDecimalNum)augend).delegate, MATH_CONTEXT));
}
Take a look at the corresponding section in the usage examples to find out how to use Num
and BaseBarSeries
.
The main purpose of supporting different data types for arithmetic operations are opposing goals in technical analysis. On the one hand performance is a critical factor in high frequency trading or big data analysis, on the other hand crypto currencies and the general handling of monetary values require arithmetic that is not based on binary floating-point types like double
or float
. For example the following simple code fragment using double
will print an unexpected result:
System.out.println(1.0 - 9*0.1)
The output will be 0.0999999999999999998
witch is not equal to 0.1. This is no bug, but the result of trying to represent decimal values in a binary number system. It is not possible to represent 0.1 (or any other negative power of ten) exactly in double or float. With those data types you can only approximate such kind of decimal values. In many cases this representation may be sufficient and you would say "rounding to the last cent will give me the correct value", but please note that .9999 trillion dollars is approximately equal to 1 trillion dollars. Could you please deposit the difference in my bank account?
The right way to solve this problem is to use BigDecimal
, int
or long
for monetary calculations. It doesn't mean though that doubles can never be used for that purpose. Based of the fact that indicator just use monetary values as input but further calculations and results do not have a monetary dimension, Double's 53 significant bits (~16 decimal digits) are usually good enough for things that merely require accuracy.
You have to know your application and you should study your goals and inform yourself about which kind of data type implementation works best for you.
The PrecisionNum
implementation of Num
uses BigDecimal as delgate and can represent any decimal value exact up to 32 decimal places. It can be used to do highly accurate calculations and to work with crypto currencies which representation needs a lot of decimal places. It is the default Num
implementation if you create a BaseBarSeries
. For some purposes that requier very fast or a lot calculations you could notice a performance bottleneck due to this implementation of Num
.
BarSeries series_1 = new BaseBarSeriesBuilder().build() // implicit initialize BarSeries with PrecisionNum
BarSeries series_2 = new BaseBarSeriesBuilder().withNumType(PrecisionNum::valueOf).build() // explicit initialize BarSeries with PrecisionNum
After having found out the disadvanteges aboutDoubleNum
, please note that it can give your Ta4j application a heavy performance boost. You can create a BaseBarSeries
using DoubleNum
as follows:
BarSeries series_3 = new BaseBarSeriesBuilder().withNumType(DoubleNum::valueOf).build() // explicit initialize BarSeries with DoubleNum
If you want to write your own implementation of Num
you just have to let your class implement the Num
interface:
public class MyNum implements Num {
// Override interface functions...
}
For instance you could use integer
or long
as delegate to solve the problem of performance vs. accuracy. An existing alternative to BigDecimal
could be Decimal4j.
Special attention requiers the following prototype of the Num
interface:
public Function<Number, Num> function(); // required from every class that implements Num..
This function must returns a java.util.Function object, that allows users and other classes of the library to convert any Number extending class (like Double, Integer, BigDecimal, ...) into your Num implementation.
The existing implementations DoubleNum
and BigDecimalNum
provide static functions to convert a Number
into the corresponding Num
implementing class:
/**
* Returns a {@code Num} version of the given {@code Number}.
* Warning: This method turns the number into a string first
* @param val the number
* @return the {@code Num}
*/
public static BigDecimalNum valueOf(Number val) {
return new BigDecimalNum(val.toString());
}
The function()
should return a lamba expression of this static valueOf()
function. The following code snipped shows how the BigDecimalNum
class overrides the function()
function with help of a method reference:
@Override
public Function<Number, Num> function() {
return BigDecimalNum::valueOf;
}
BarSeries
and Bar
need a reference to this Function
object that enables to transform any Number
to the wanted Num
implementation. Because of that you have to pass this function when creating a Bar
manually:
// The bar object has to transform the intput into Num with help of the given function
Bar bar = new BaseBar(ZonedDateTime.now(),1,3,1,1,1,BigDecimalNum::valueOf);
Also the BarSeries needs this Function
. The easiest way to handle this is to use the SeriesBuilder and to add bar data directly to the BarSeries:
BarSeries series = new BaseBarSeriesBuilder().withName("mySeries").build(); // the builder uses BigDecimalNum as default
ZonedDateTime endTime = ZonedDateTime.now();
// add bar data directly. It will be transformed automatically to Num implementation of BarSeries
series.addBar(endTime, 105.42, 112.99, 104.01, 111.42, 1337);
series.addBar(endTime.plusDays(1), 111.43, 112.83, 107.77, 107.99, 1234);
series.addBar(endTime.plusDays(2), 107.90, 117.50, 107.90, 115.42, 4242);
You can determine the Num
transforming Function
with the builder by using the withNumTypeOf(function)
function:
BarSeries series = new BaseBarSeriesBuilder().withName("mySeries").withNumTypeOf(DoubleNum::valueOf).build();
**Please note that once instantiating a `BarSeries` with a specific `Num` implementation you cannot add data in another `Num` implementation to the `BarSeries`.**
BarSeries series = new BaseBarSeriesBuilder().build().build(); // implicit initialize with PrecisionNum
series.addTrade(DoubleNum.valueOf(volume), DoubleNum.valueOf(bid)); // try to add DoubleNum values
// throws ClassCastException: org.ta4j.core.num.DoubleNum
// cannot be cast to org.ta4j.core.num.PrecisionNum