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