Skip to content

Commit 45a4bd6

Browse files
committed
New: VolatilitySqueeze, UltimateStrengthIndex, DoubleSeries.cross
1 parent 87193b7 commit 45a4bd6

File tree

30 files changed

+1554
-25
lines changed

30 files changed

+1554
-25
lines changed

chartsy-core/src/main/java/one/chartsy/base/DoubleDataset.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import one.chartsy.base.dataset.AbstractIntDataset;
1010
import one.chartsy.base.dataset.AbstractLongDataset;
1111
import one.chartsy.base.dataset.ImmutableDoubleDataset;
12+
import one.chartsy.base.function.DoubleBiPredicate;
1213

1314
import java.util.Objects;
1415
import java.util.Spliterator;
@@ -38,6 +39,34 @@ public interface DoubleDataset extends PrimitiveDataset<Double, DoubleDataset, S
3839
*/
3940
double get(int index);
4041

42+
/**
43+
* Returns a dataset indicating where the series crosses over the specified value.
44+
*
45+
* @param value the value to check crossings against
46+
* @return a {@code Dataset} of {@code Boolean} indicating crossings over the value
47+
*/
48+
default Dataset<Boolean> crossesOver(double value) {
49+
return crosses((prev, curr) -> prev <= value && curr > value);
50+
}
51+
52+
/**
53+
* Returns a dataset indicating where the series crosses under the specified value.
54+
*
55+
* @param value the value to check crossings against
56+
* @return a {@code Dataset} of {@code Boolean} indicating crossings over the value
57+
*/
58+
default Dataset<Boolean> crossesUnder(double value) {
59+
return crosses((prev, curr) -> prev >= value && curr < value);
60+
}
61+
62+
/**
63+
* Determines the dataset's crossings based on the provided crossing condition.
64+
*
65+
* @param crossingCondition a {@code DoubleBiPredicate} that defines the crossing condition
66+
* @return a {@code Dataset} of {@code Boolean} indicating crossings based on the condition
67+
*/
68+
Dataset<Boolean> crosses(DoubleBiPredicate crossingCondition);
69+
4170
default DoubleStream stream() {
4271
return StreamSupport.doubleStream(spliterator(), false);
4372
}

chartsy-core/src/main/java/one/chartsy/base/SequenceAlike.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ public boolean isDescending() {
4949
return this == INDEX_DESC;
5050
}
5151

52+
public int previousIndex(SequenceAlike seq, int index) {
53+
return switch (this) {
54+
case INDEX_ASC -> (index > 0) ? index - 1 : -1;
55+
case INDEX_DESC -> (index < seq.length() - 1) ? index + 1 : -1;
56+
default ->
57+
throw new UnsupportedOperationException("Operation `previousIndex` not implemented for order " + this);
58+
};
59+
}
60+
5261
public static void reverse(Object[] arr) {
5362
int start = 0;
5463
int end = arr.length - 1;

chartsy-core/src/main/java/one/chartsy/base/dataset/AbstractDoubleDataset.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
*/
55
package one.chartsy.base.dataset;
66

7+
import one.chartsy.base.Dataset;
78
import one.chartsy.base.DoubleDataset;
89
import one.chartsy.base.SequenceAlike;
10+
import one.chartsy.base.function.DoubleBiPredicate;
911
import one.chartsy.base.function.IndexedToDoubleFunction;
1012

13+
import java.util.Objects;
1114
import java.util.PrimitiveIterator;
1215
import java.util.Spliterator;
1316
import java.util.function.Function;
@@ -42,6 +45,20 @@ public Spliterator.OfDouble spliterator() {
4245
return stream().spliterator();
4346
}
4447

48+
@Override
49+
public Dataset<Boolean> crosses(DoubleBiPredicate crossingCondition) {
50+
Objects.requireNonNull(crossingCondition);
51+
return AbstractDataset.from(this, (dataset, index) -> {
52+
int prevIndex = dataset.getOrder().previousIndex(dataset, index);
53+
if (prevIndex == -1)
54+
return false;
55+
56+
double prevValue = dataset.get(prevIndex);
57+
double currValue = dataset.get(index);
58+
return crossingCondition.test(prevValue, currValue);
59+
});
60+
}
61+
4562
public static <T extends SequenceAlike>
4663
AbstractDoubleDataset from(T origin, IndexedToDoubleFunction<T> getter) {
4764
return new From<>(origin) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2024 Mariusz Bernacki <[email protected]>
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package one.chartsy.base.function;
6+
7+
import java.util.function.BiPredicate;
8+
9+
/**
10+
* Represents a predicate (boolean-valued function) of two {@code double}-valued arguments.
11+
* This is the {@code double}-consuming primitive type specialization of {@link BiPredicate}.
12+
*
13+
* <p>This is a functional interface whose functional method is {@link #test(double, double)}.
14+
*/
15+
@FunctionalInterface
16+
public interface DoubleBiPredicate {
17+
18+
/**
19+
* Evaluates this predicate on the given arguments.
20+
*
21+
* @param left the first input argument
22+
* @param right the second input argument
23+
* @return {@code true} if the input arguments match the predicate, otherwise {@code false}
24+
*/
25+
boolean test(double left, double right);
26+
}

chartsy-core/src/main/java/one/chartsy/core/NamedPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public String getName() {
1919
return name;
2020
}
2121

22+
public abstract String getLabel();
23+
2224
public <R> R query(NamedPluginQuery<R, T> query) {
2325
return query.queryFrom(this);
2426
}

chartsy-core/src/main/java/one/chartsy/data/Series.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ default Iterator<E> iterator() {
4040
return chronologicalIterator(iteratorContext());
4141
}
4242

43+
Dataset<E> getData();
44+
4345
private static ChronologicalIteratorContext iteratorContext() {
4446
class Holder {
4547
private static final ChronologicalIteratorContext INSTANCE = new ChronologicalIteratorContext(-1);

chartsy-core/src/main/java/one/chartsy/financial/ValueIndicatorSupport.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import one.chartsy.data.Series;
88
import one.chartsy.data.packed.PackedDoubleSeries;
99
import one.chartsy.time.Chronological;
10+
import one.chartsy.time.Timeline;
1011

1112
import java.util.Arrays;
1213
import java.util.List;
@@ -29,6 +30,12 @@ PackedDoubleSeries calculate(DoubleSeries data, VI indicator) {
2930
return calculate(data, indicator, ValueIndicator.OfDouble::getLast);
3031
}
3132

33+
private static PackedDoubleSeries createSeries(double[] values, Timeline timeline) {
34+
return (values == null)
35+
? DoubleSeries.empty(timeline)
36+
: DoubleSeries.of(values, timeline);
37+
}
38+
3239
public static <V extends ValueIndicator & DoubleConsumer>
3340
PackedDoubleSeries calculate(DoubleSeries data, V indicator, ToDoubleFunction<V> output) {
3441
double[] values = null;
@@ -41,7 +48,7 @@ PackedDoubleSeries calculate(DoubleSeries data, V indicator, ToDoubleFunction<V>
4148
if (values != null)
4249
values[index] = output.applyAsDouble(indicator);
4350
}
44-
return DoubleSeries.of(values, data.getTimeline());
51+
return createSeries(values, data.getTimeline());
4552
}
4653

4754
public static <E extends Chronological, V extends ValueIndicator.OfDouble & Consumer<E>>
@@ -61,7 +68,7 @@ PackedDoubleSeries calculate(Series<E> data, V indicator, ToDoubleFunction<V> ou
6168
if (values != null)
6269
values[index] = output.applyAsDouble(indicator);
6370
}
64-
return (values == null) ? DoubleSeries.empty(data.getTimeline()) : DoubleSeries.of(values, data.getTimeline());
71+
return createSeries(values, data.getTimeline());
6572
}
6673

6774
public static <E extends Chronological, V extends ValueIndicator & Consumer<E>>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2024 Mariusz Bernacki <[email protected]>
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package one.chartsy.financial.indicators;
6+
7+
import lombok.Getter;
8+
import one.chartsy.data.structures.RingBuffer;
9+
10+
public class UltimateSmoother {
11+
private final double a1, b1, c1, c2, c3;
12+
private final RingBuffer.OfDouble prices;
13+
private final RingBuffer.OfDouble values;
14+
@Getter
15+
private double last = Double.NaN;
16+
17+
public UltimateSmoother(int period) {
18+
this.prices = new RingBuffer.OfDouble(2);
19+
this.values = new RingBuffer.OfDouble(2);
20+
double sqrt2 = Math.sqrt(2);
21+
this.a1 = Math.exp(-sqrt2 * Math.PI / period);
22+
this.b1 = 2 * a1 * Math.cos(sqrt2 * Math.PI / period);
23+
this.c2 = b1;
24+
this.c3 = -a1 * a1;
25+
this.c1 = (1 + c2 - c3) / 4;
26+
}
27+
28+
public double smooth(double price) {
29+
if (!values.isFull()) {
30+
prices.add(price);
31+
values.add(price);
32+
last = price;
33+
return price;
34+
}
35+
36+
double value = (1 - c1) * price
37+
+ (2 * c1 - c2) * prices.get(0)
38+
- (c1 + c3) * prices.get(1)
39+
+ c2 * values.get(0)
40+
+ c3 * values.get(1);
41+
42+
prices.add(price);
43+
values.add(value);
44+
last = value;
45+
return value;
46+
}
47+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2024 Mariusz Bernacki <[email protected]>
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package one.chartsy.financial.indicators;
6+
7+
import one.chartsy.data.structures.DoubleWindowSummaryStatistics;
8+
import one.chartsy.financial.AbstractDoubleIndicator;
9+
10+
public class UltimateStrengthIndex extends AbstractDoubleIndicator {
11+
private final int length;
12+
private final DoubleWindowSummaryStatistics strengthUp;
13+
private final DoubleWindowSummaryStatistics strengthDown;
14+
private final UltimateSmoother usuSmoother;
15+
private final UltimateSmoother usdSmoother;
16+
private double lastClose = Double.NaN;
17+
private double last = Double.NaN;
18+
private int count;
19+
20+
public UltimateStrengthIndex(int length) {
21+
this.length = length;
22+
this.strengthUp = new DoubleWindowSummaryStatistics(4);
23+
this.strengthDown = new DoubleWindowSummaryStatistics(4);
24+
this.usuSmoother = new UltimateSmoother(length);
25+
this.usdSmoother = new UltimateSmoother(length);
26+
}
27+
28+
@Override
29+
public void accept(double close) {
30+
if (!Double.isNaN(lastClose)) {
31+
// Calculate strength up and down
32+
double diff = close - lastClose;
33+
strengthUp.add(diff > 0 ? diff : 0);
34+
strengthDown.add(diff < 0 ? -diff : 0);
35+
36+
if (count >= 3) {
37+
// Calculate 4-bar averages using DoubleWindowSummaryStatistics's capabilities
38+
double suAvg = strengthUp.getAverage();
39+
double sdAvg = strengthDown.getAverage();
40+
41+
// Apply Ultimate Smoother to the averages
42+
double usu = usuSmoother.smooth(suAvg);
43+
double usd = usdSmoother.smooth(sdAvg);
44+
45+
// Calculate USI
46+
if (usu + usd != 0.0 && usu > 0.0001 && usd > 0.0001)
47+
last = (usu - usd) / (usu + usd);
48+
}
49+
count++;
50+
}
51+
lastClose = close;
52+
}
53+
54+
@Override
55+
public double getLast() {
56+
return last;
57+
}
58+
59+
@Override
60+
public boolean isReady() {
61+
return count >= length;
62+
}
63+
64+
}

chartsy-samples/src/main/java/one/chartsy/data/provider/file/stooq/pl/VolatilityBasedSelectionFromStooqFlatFileDataProvider.java

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515

1616
import java.io.IOException;
1717
import java.nio.file.Path;
18-
import java.util.DoubleSummaryStatistics;
18+
import java.util.ArrayList;
19+
import java.util.Comparator;
1920
import java.util.List;
2021
import java.util.concurrent.atomic.AtomicReference;
2122
import java.util.stream.IntStream;
2223

2324
public class VolatilityBasedSelectionFromStooqFlatFileDataProvider {
2425

25-
private static final Path PATH_TO_STOOQ_FILE = Path.of("C:/Downloads/d_pl_txt(3).zip");
26-
// private static final Path PATH_TO_STOOQ_FILE = Path.of("C:/Downloads/d_us_txt(1).zip");
26+
private static final Path PATH_TO_STOOQ_FILE = Path.of("C:/Downloads/d_pl_txt(20).zip");
27+
// private static final Path PATH_TO_STOOQ_FILE = Path.of("C:/Downloads/d_us_txt(1).zip");
2728
private static final Logger log = LogManager.getLogger(VolatilityBasedSelectionFromStooqFlatFileDataProvider.class);
2829

2930
public static void main(String[] args) throws IOException {
@@ -34,26 +35,33 @@ public static void main(String[] args) throws IOException {
3435
// list all stocks contained in a file
3536
List<? extends SymbolIdentity> stocks = dataProvider.listSymbols(new SymbolGroup("/data/daily/pl/wse stocks"));
3637
int stockCount = stocks.size();
37-
log.info("Found {} stock(s)".replace("(s)", stockCount==1?"":"s"), stockCount);
38+
log.info("Found {} stock(s)".replace("(s)", stockCount == 1 ? "" : "s"), stockCount);
3839
log.info("");
3940

4041
// list summary of each stock data series
41-
int candleCount = 0;
42+
List<ExtremaResult> extremaResults = new ArrayList<>();
4243
for (SymbolIdentity stock : stocks) {
4344
var resource = SymbolResource.of(stock, TimeFrame.Period.DAILY);
4445
var candles = dataProvider.queryForCandles(
4546
DataQuery.of(resource))
4647
.collectSortedList()
4748
.as(CandleSeries.of(resource));
48-
detectDecreasingVolatility(candles);
49-
//log.info(candles);
49+
extremaResults.add(detectDecreasingVolatility(candles));
5050
}
51-
log.info("Total {} candle(s)".replace("(s)", candleCount==1?"":"s"), candleCount);
51+
52+
// Sort and print results by largest correlation coefficient
53+
extremaResults.stream()
54+
.sorted(Comparator.comparingDouble(ExtremaResult::coefficient).reversed())
55+
.forEach(result -> {
56+
System.out.println(result.candleSeries().getSymbol() + ":");
57+
System.out.println("\tHighest correlation coefficient: " + result.coefficient());
58+
System.out.println("\tOffset for highest correlation coefficient: " + result.offset());
59+
System.out.println("\tBarCount for highest correlation coefficient: " + result.barCount());
60+
});
5261
}
5362

54-
public static void detectDecreasingVolatility(CandleSeries candleSeries) {
55-
PearsonsCorrelation pearsonsCorrelation = new PearsonsCorrelation();
56-
AtomicReference<ExtremaParams> minParams = new AtomicReference<>(new ExtremaParams(0, -1, -1));
63+
public static ExtremaResult detectDecreasingVolatility(CandleSeries candleSeries) {
64+
AtomicReference<ExtremaParams> maxParams = new AtomicReference<>(new ExtremaParams(0, -1, -1));
5765

5866
// Iterate from 3 to 90 for barCount
5967
IntStream.rangeClosed(3, 90).forEach(barCount -> {
@@ -72,18 +80,19 @@ public static void detectDecreasingVolatility(CandleSeries candleSeries) {
7280
// Calculate correlation coefficient
7381
double correlationCoefficient = regression.getR();
7482
// Update stats if needed
75-
if (correlationCoefficient > minParams.get().coefficient()) {
76-
minParams.set(new ExtremaParams(correlationCoefficient, offset, barCount));
83+
if (correlationCoefficient > maxParams.get().coefficient()) {
84+
maxParams.set(new ExtremaParams(correlationCoefficient, offset, barCount));
7785
}
7886
}
7987
});
8088
});
8189

82-
System.out.println(candleSeries.getSymbol() + ":");
83-
System.out.println("\tLowest correlation coefficient: " + minParams.get().coefficient());
84-
System.out.println("\tOffset for lowest correlation coefficient: " + minParams.get().offset());
85-
System.out.println("\tBarCount for lowest correlation coefficient: " + minParams.get().barCount());
90+
return new ExtremaResult(candleSeries, maxParams.get().coefficient(), maxParams.get().offset(), maxParams.get().barCount());
91+
}
92+
93+
record ExtremaParams(double coefficient, int offset, int barCount) {
8694
}
8795

88-
record ExtremaParams(double coefficient, int offset, int barCount) { }
96+
record ExtremaResult(CandleSeries candleSeries, double coefficient, int offset, int barCount) {
97+
}
8998
}

0 commit comments

Comments
 (0)