1
+ package net .swofty .stockmarkettester .data ;
2
+
3
+ import net .swofty .stockmarkettester .orders .HistoricalData ;
4
+ import net .swofty .stockmarkettester .orders .MarketDataPoint ;
5
+
6
+ import java .io .*;
7
+ import java .nio .file .Files ;
8
+ import java .nio .file .Path ;
9
+ import java .time .LocalDateTime ;
10
+ import java .time .format .DateTimeFormatter ;
11
+ import java .util .*;
12
+ import java .util .concurrent .ConcurrentHashMap ;
13
+ import java .util .stream .Collectors ;
14
+
15
+ public class SegmentedHistoricalCache {
16
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter .ofPattern ("yyyy-MM-dd" );
17
+ private final Path cacheDirectory ;
18
+ private final Map <String , TreeMap <LocalDateTime , CacheSegment >> segmentIndex ;
19
+
20
+ private static class CacheSegment implements Serializable {
21
+ @ Serial
22
+ private static final long serialVersionUID = 1L ;
23
+ private final LocalDateTime start ;
24
+ private final LocalDateTime end ;
25
+ private final String ticker ;
26
+ private final HistoricalData data ;
27
+
28
+ public CacheSegment (String ticker , LocalDateTime start , LocalDateTime end , HistoricalData data ) {
29
+ this .ticker = ticker ;
30
+ this .start = start ;
31
+ this .end = end ;
32
+ this .data = data ;
33
+ }
34
+
35
+ public boolean containsTimeRange (LocalDateTime queryStart , LocalDateTime queryEnd ) {
36
+ return !start .isAfter (queryStart ) && !end .isBefore (queryEnd );
37
+ }
38
+
39
+ public boolean overlaps (LocalDateTime queryStart , LocalDateTime queryEnd ) {
40
+ return !end .isBefore (queryStart ) && !start .isAfter (queryEnd );
41
+ }
42
+
43
+ public HistoricalData getData () {
44
+ return data ;
45
+ }
46
+
47
+ public LocalDateTime getStart () {
48
+ return start ;
49
+ }
50
+
51
+ public LocalDateTime getEnd () {
52
+ return end ;
53
+ }
54
+ }
55
+
56
+ public SegmentedHistoricalCache (Path cacheDirectory ) {
57
+ this .cacheDirectory = cacheDirectory ;
58
+ this .segmentIndex = new ConcurrentHashMap <>();
59
+ initializeFromDisk ();
60
+ }
61
+
62
+ private void initializeFromDisk () {
63
+ if (!Files .exists (cacheDirectory )) {
64
+ try {
65
+ Files .createDirectories (cacheDirectory );
66
+ } catch (IOException e ) {
67
+ throw new RuntimeException ("Failed to create cache directory" , e );
68
+ }
69
+ return ;
70
+ }
71
+
72
+ try {
73
+ Files .walk (cacheDirectory )
74
+ .filter (Files ::isRegularFile )
75
+ .filter (p -> p .toString ().endsWith (".cache" ))
76
+ .forEach (this ::loadSegment );
77
+ } catch (IOException e ) {
78
+ throw new RuntimeException ("Failed to initialize cache from disk" , e );
79
+ }
80
+ }
81
+
82
+ private void loadSegment (Path path ) {
83
+ try (ObjectInputStream ois = new ObjectInputStream (Files .newInputStream (path ))) {
84
+ CacheSegment segment = (CacheSegment ) ois .readObject ();
85
+ addToIndex (segment );
86
+ } catch (IOException | ClassNotFoundException e ) {
87
+ System .err .println ("Failed to load cache segment: " + path );
88
+ try {
89
+ Files .delete (path );
90
+ } catch (IOException ignored ) {}
91
+ }
92
+ }
93
+
94
+ private void addToIndex (CacheSegment segment ) {
95
+ segmentIndex .computeIfAbsent (segment .ticker , k -> new TreeMap <>())
96
+ .put (segment .start , segment );
97
+ }
98
+
99
+ public Optional <HistoricalData > get (String ticker , LocalDateTime start , LocalDateTime end ) {
100
+ TreeMap <LocalDateTime , CacheSegment > segments = segmentIndex .get (ticker );
101
+ if (segments == null ) return Optional .empty ();
102
+
103
+ // First try to find a single segment that contains the entire range
104
+ for (CacheSegment segment : segments .values ()) {
105
+ if (segment .containsTimeRange (start , end )) {
106
+ return Optional .of (segment .getData ());
107
+ }
108
+ }
109
+
110
+ // If no single segment contains the range, try to merge overlapping segments
111
+ List <CacheSegment > overlappingSegments = segments .values ().stream ()
112
+ .filter (s -> s .overlaps (start , end ))
113
+ .sorted (Comparator .comparing (CacheSegment ::getStart ))
114
+ .collect (Collectors .toList ());
115
+
116
+ if (overlappingSegments .isEmpty ()) return Optional .empty ();
117
+
118
+ // Check if segments form a continuous range
119
+ LocalDateTime currentEnd = overlappingSegments .get (0 ).getStart ();
120
+ for (CacheSegment segment : overlappingSegments ) {
121
+ if (segment .getStart ().isAfter (currentEnd )) {
122
+ return Optional .empty (); // Gap in the data
123
+ }
124
+ currentEnd = segment .getEnd ();
125
+ }
126
+
127
+ if (currentEnd .isBefore (end )) return Optional .empty ();
128
+
129
+ // Merge the segments
130
+ HistoricalData mergedData = new HistoricalData (ticker );
131
+ for (CacheSegment segment : overlappingSegments ) {
132
+ List <MarketDataPoint > points = segment .getData ().getDataPoints (start , end );
133
+ points .forEach (mergedData ::addDataPoint );
134
+ }
135
+
136
+ return Optional .of (mergedData );
137
+ }
138
+
139
+ public void put (String ticker , LocalDateTime start , LocalDateTime end , HistoricalData data ) {
140
+ CacheSegment segment = new CacheSegment (ticker , start , end , data );
141
+ addToIndex (segment );
142
+ saveSegment (segment );
143
+ }
144
+
145
+ private void saveSegment (CacheSegment segment ) {
146
+ Path path = getSegmentPath (segment );
147
+ try (ObjectOutputStream oos = new ObjectOutputStream (Files .newOutputStream (path ))) {
148
+ oos .writeObject (segment );
149
+ } catch (IOException e ) {
150
+ System .err .println ("Failed to save cache segment: " + e .getMessage ());
151
+ }
152
+ }
153
+
154
+ private Path getSegmentPath (CacheSegment segment ) {
155
+ String filename = String .format ("%s_%s_to_%s.cache" ,
156
+ segment .ticker ,
157
+ segment .start .format (DATE_FORMAT ),
158
+ segment .end .format (DATE_FORMAT ));
159
+ return cacheDirectory .resolve (filename );
160
+ }
161
+
162
+ public void clearCache () {
163
+ try {
164
+ Files .walk (cacheDirectory )
165
+ .filter (Files ::isRegularFile )
166
+ .forEach (file -> {
167
+ try {
168
+ Files .delete (file );
169
+ } catch (IOException e ) {
170
+ System .err .println ("Failed to delete cache file: " + file );
171
+ }
172
+ });
173
+ segmentIndex .clear ();
174
+ } catch (IOException e ) {
175
+ throw new RuntimeException ("Failed to clear cache" , e );
176
+ }
177
+ }
178
+ }
0 commit comments