Skip to content

Commit b8116b3

Browse files
author
Mariusz Bernacki
committed
Add bar series timeline synchronization method
1 parent 5c478e4 commit b8116b3

File tree

6 files changed

+186
-8
lines changed

6 files changed

+186
-8
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ static Candle of(long time, double open, double high, double low, double close,
9898
return SimpleCandle.of(time, open, high, low, close, volume, count);
9999
}
100100

101+
static Candle of(LocalDateTime dateTime, double price) {
102+
return of(Chronological.toEpochMicros(dateTime), price);
103+
}
104+
101105
static Candle of(LocalDateTime dateTime, double open, double high, double low, double close, double volume) {
102106
return of(Chronological.toEpochMicros(dateTime), open, high, low, close, volume);
103107
}

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@
55
package one.chartsy.data;
66

77
import one.chartsy.Candle;
8+
import one.chartsy.SymbolResource;
9+
import one.chartsy.data.packed.PackedCandleSeries;
10+
import one.chartsy.data.packed.PackedDataset;
11+
import one.chartsy.time.AbstractTimeline;
812
import one.chartsy.time.Chronological;
13+
import one.chartsy.time.Timeline;
914

1015
import java.time.LocalDateTime;
16+
import java.util.ArrayList;
17+
import java.util.Collection;
18+
import java.util.Iterator;
19+
import java.util.List;
20+
import java.util.function.Consumer;
1121

1222
/**
1323
* This class provides utility methods for working with candle series data.
@@ -55,5 +65,99 @@ public static <E extends Candle> Dataset<E> getBarsBefore(Series<? extends E> ba
5565
return dataset;
5666
}
5767

68+
/**
69+
* Computes a unified timeline of the given series.
70+
*
71+
* @param seriesCollection the collection of series
72+
* @return a unified timeline
73+
*/
74+
public static Timeline getUnifiedTimeline(Collection<? extends Series<? extends Chronological>> seriesCollection) {
75+
var order = Chronological.Order.REVERSE_CHRONOLOGICAL;
76+
var times = seriesCollection.stream()
77+
.mapMulti((Series<? extends Chronological> series, Consumer<Chronological> result) -> series.forEach(result))
78+
.sorted(order.comparator())
79+
.distinct()
80+
.toList();
81+
82+
return new AbstractTimeline(order) {
83+
@Override public int length() { return times.size(); }
84+
@Override public long getTimeAt(int x) { return times.get(x).getTime(); }
85+
};
86+
}
87+
88+
/**
89+
* Synchronizes the timelines of multiple candle series to a common timeline. Missing
90+
* data points in the original series are filled with candles having the previous
91+
* candle's closing price and zero volume.
92+
*
93+
* @param seriesCollection a collection of candle series to synchronize
94+
* @return a new collection of candle series with synchronized timelines
95+
*/
96+
public static <E extends Candle> List<Series<Candle>> synchronizeTimelines(Collection<? extends Series<? extends E>> seriesCollection) {
97+
Timeline timeline = getUnifiedTimeline(seriesCollection);
98+
99+
return seriesCollection.stream()
100+
.map(series -> synchronizeTimeline(series, timeline))
101+
.toList();
102+
}
103+
104+
/**
105+
* Synchronizes the timeline of a given candle series with a provided desired timeline.
106+
* If no candle exists for a particular timestamp in the unified timeline, a new candle
107+
* is created with the previous candle's closing price and zero volume.
108+
*
109+
* @param series the candle series to synchronize
110+
* @param timeline the timeline to synchronize the series with
111+
* @return a new candle series with the desired timeline
112+
* @throws AssertionError if the series cannot be aligned, for example if it contains
113+
* timestamps which doesn't exist in the {@code timeline}
114+
*/
115+
public static Series<Candle> synchronizeTimeline(Series<? extends Candle> series, Timeline timeline) {
116+
Iterator<? extends Candle> iter = series.iterator();
117+
if (iter.hasNext()) {
118+
Candle curr = iter.next(), prev = null;
119+
int timeIndex = timeline.getTimeLocation(curr.getTime());
120+
if (timeIndex < 0) {
121+
timeIndex = -timeIndex - 1;
122+
}
123+
List<Candle> bars = new ArrayList<>(timeIndex + 1);
124+
125+
// Iterate through the timeline
126+
for (; timeIndex >= 0; timeIndex--) {
127+
long time = timeline.getTimeAt(timeIndex);
128+
129+
if (curr.getTime() > time && prev != null) {
130+
bars.add(Candle.of(time, prev.close()));
131+
}
132+
else if (curr.getTime() == time) {
133+
bars.add(curr);
134+
if (iter.hasNext()) {
135+
prev = curr;
136+
curr = iter.next();
137+
} else if (curr != MAX) {
138+
prev = curr;
139+
curr = MAX;
140+
}
141+
}
142+
else
143+
throw new AssertionError("Cannot synchronize with this Timeline");
144+
}
145+
return createSeries(series, bars, timeline);
146+
}
147+
return createSeries(series, List.of(), timeline);
148+
}
149+
150+
@SuppressWarnings("unchecked")
151+
private static PackedCandleSeries createSeries(Series<? extends Candle> series, List<Candle> bars, Timeline timeline) {
152+
return new PackedCandleSeries((SymbolResource<Candle>) series.getResource(), PackedDataset.of(bars, true)) {
153+
@Override
154+
public Timeline getTimeline() {
155+
return timeline;
156+
}
157+
};
158+
}
159+
160+
private static final Candle MAX = Candle.of(Long.MAX_VALUE, 0);
161+
58162
private CandleSeriesSupport() {} // cannot instantiate
59163
}

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,10 @@
88
public class ChronologicalDatasetTimeline extends AbstractTimeline {
99

1010
private final ChronologicalDataset data;
11-
private final Chronological.Order order;
1211

1312
public ChronologicalDatasetTimeline(ChronologicalDataset data, Chronological.Order order) {
13+
super(order);
1414
this.data = data;
15-
this.order = order;
16-
}
17-
18-
@Override
19-
public final Chronological.Order getOrder() {
20-
return order;
2115
}
2216

2317
@Override

chartsy-core/src/main/java/one/chartsy/data/packed/PackedSeries.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public E next() {
7777
}
7878

7979
@Override
80-
public final Timeline getTimeline() {
80+
public Timeline getTimeline() {
8181
return this;
8282
}
8383

chartsy-core/src/main/java/one/chartsy/time/AbstractTimeline.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
public abstract class AbstractTimeline implements Timeline {
88

9+
private final Chronological.Order order;
10+
11+
protected AbstractTimeline(Chronological.Order order) {
12+
this.order = order;
13+
}
14+
15+
16+
@Override
17+
public final Chronological.Order getOrder() {
18+
return order;
19+
}
20+
921
@Override
1022
public int getTimeLocation(long time) {
1123
if (getOrder().isReversed())
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package one.chartsy.data;
2+
3+
import one.chartsy.Candle;
4+
import one.chartsy.SymbolResource;
5+
import one.chartsy.TimeFrame;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.time.LocalDate;
9+
import java.time.LocalDateTime;
10+
import java.util.List;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
class CandleSeriesSupportTest {
15+
16+
static final SymbolResource<Candle> TEST_SYMBOL = SymbolResource.of("TEST_SYMBOL", TimeFrame.Period.DAILY);
17+
18+
@Test
19+
void synchronizeTimelines() {
20+
// Create sample candle series with different timelines
21+
Series<Candle> series1 = seriesOf(
22+
candle(LocalDate.of(2023, 11, 1)),
23+
candle(LocalDate.of(2023, 11, 2)),
24+
candle(LocalDate.of(2023, 11, 4))
25+
);
26+
Series<Candle> series2 = seriesOf(
27+
candle(LocalDate.of(2023, 11, 2)),
28+
candle(LocalDate.of(2023, 11, 3))
29+
);
30+
31+
// Synchronize the timelines
32+
List<Series<Candle>> resultSeries = CandleSeriesSupport.synchronizeTimelines(List.of(series1, series2));
33+
34+
// Verify the results
35+
assertEquals(2, resultSeries.size());
36+
37+
// Check series 1
38+
Series<Candle> syncSeries1 = resultSeries.getFirst();
39+
assertEquals(4, syncSeries1.length());
40+
assertEquals(candle(LocalDate.of(2023, 11, 1)), syncSeries1.get(3));
41+
assertEquals(candle(LocalDate.of(2023, 11, 2)), syncSeries1.get(2));
42+
assertEquals(Candle.of(LocalDateTime.of(2023, 11, 3, 0, 0), series1.get(1).close()), syncSeries1.get(1)); // Filled candle
43+
assertEquals(candle(LocalDate.of(2023, 11, 4)), syncSeries1.get(0));
44+
45+
// Check series 2
46+
Series<Candle> syncSeries2 = resultSeries.get(1);
47+
assertEquals(3, syncSeries2.length());
48+
assertEquals(candle(LocalDate.of(2023, 11, 2)), syncSeries2.get(2));
49+
assertEquals(candle(LocalDate.of(2023, 11, 3)), syncSeries2.get(1));
50+
assertEquals(Candle.of(LocalDateTime.of(2023, 11, 4, 0, 0), series2.get(0).close()), syncSeries2.get(0)); // Filled candle
51+
52+
// Verify that the series share the same Timeline
53+
assertSame(syncSeries1.getTimeline(), syncSeries2.getTimeline());
54+
}
55+
56+
static Series<Candle> seriesOf(Candle... cs) {
57+
return CandleSeries.of(TEST_SYMBOL, List.of(cs));
58+
}
59+
60+
static Candle candle(LocalDate dateTime) {
61+
var close = dateTime.getDayOfMonth();
62+
return Candle.of(dateTime.atStartOfDay(), 10, 31, 1, close, 100);
63+
}
64+
}

0 commit comments

Comments
 (0)