Skip to content

Commit 9f32cbf

Browse files
committed
fix caching problems for A-stock hourly
1 parent c83c3d8 commit 9f32cbf

File tree

4 files changed

+96
-17
lines changed

4 files changed

+96
-17
lines changed

docs/CACHING.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ A Python script (`scripts/precompute_frontend_cache.py`) generates static JSON f
3333
The frontend (`assets/js/cache-manager.js`) implements smart caching with per-market storage:
3434

3535
1. **First Load**: Fetches pre-computed cache from server for the active market
36+
- Uses cache-busting headers to bypass browser HTTP cache
37+
- Ensures latest version is always checked from server
3638
2. **Saves to localStorage**: Stores each market's cache separately (us, cn, cn_hour)
3739
3. **Version Checking**: Auto-invalidates when data changes (version mismatch)
3840
4. **Graceful Degradation**: Falls back to live calculation if cache unavailable
@@ -331,13 +333,24 @@ if agents_data:
331333

332334
**Symptom**: Generated new cache file but browser still shows old data
333335

334-
**Cause**: Version hash based only on file timestamps didn't change
336+
**Root Cause**: Browser's HTTP cache serving stale `*_cache.json` files
335337

336-
**Solution**: Increment `CACHE_FORMAT_VERSION` in `precompute_frontend_cache.py`:
337-
```python
338-
CACHE_FORMAT_VERSION = 'v4' # Increment this!
338+
**Solution**: Added cache-busting to `cache-manager.js` (lines 126-136):
339+
```javascript
340+
// Add cache-busting to prevent browser HTTP cache from serving stale files
341+
const timestamp = Date.now();
342+
const response = await fetch(`./data/${market}_cache.json?v=${timestamp}`, {
343+
cache: 'no-store',
344+
headers: {
345+
'Cache-Control': 'no-cache, no-store, must-revalidate',
346+
'Pragma': 'no-cache',
347+
'Expires': '0'
348+
}
349+
});
339350
```
340351

352+
This ensures the browser always checks the server for the latest version instead of serving cached files.
353+
341354
#### 5. Incomplete Data for Latest Trading Day
342355

343356
**Symptom**: Cache missing today's data even though position files have it
@@ -798,6 +811,27 @@ To add hourly granularity to an existing daily market:
798811

799812
## Key Fixes and Design Decisions
800813

814+
### Browser HTTP Cache Issue (v4 Critical Fix)
815+
816+
**Problem**: After regenerating cache files with new data, browser continued showing old cached values (e.g., SSE-50 at ¥101,529 instead of ¥99,991)
817+
818+
**Root Cause**: The `cache-manager.js` fetch call didn't include cache-busting headers, so browser's HTTP cache served stale `*_cache.json` files without checking the server
819+
820+
**Solution**: Added cache-busting to `loadServerCache()` method:
821+
```javascript
822+
const timestamp = Date.now();
823+
const response = await fetch(`./data/${market}_cache.json?v=${timestamp}`, {
824+
cache: 'no-store',
825+
headers: {
826+
'Cache-Control': 'no-cache, no-store, must-revalidate',
827+
'Pragma': 'no-cache',
828+
'Expires': '0'
829+
}
830+
});
831+
```
832+
833+
**Impact**: This ensures version detection works correctly - the browser always fetches the latest cache file from server, compares versions, and updates localStorage when data changes.
834+
801835
### Nov 19, 2025 Data Inclusion Fix
802836

803837
**Problem**: Hourly A-shares cache stopped at Nov 18, missing Nov 19 data

docs/assets/js/asset-chart.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,23 +236,29 @@ function createChart() {
236236
sampleData: chartData.slice(0, 3)
237237
});
238238

239+
// Detect if we have hourly data (many data points with time component)
240+
const isHourlyData = sortedDates.length > 50 && sortedDates[0].includes(':');
241+
239242
const datasetObj = {
240243
label: dataLoader.getAgentDisplayName(agentName),
241244
data: chartData,
242245
borderColor: color,
243-
backgroundColor: isBenchmark ? 'transparent' : createGradient(ctx, color),
246+
backgroundColor: isBenchmark ? 'transparent' : createGradient(ctx, color), // Keep gradient for all
244247
borderWidth: borderWidth,
245248
borderDash: borderDash,
246-
tension: 0.42, // Smooth curves for financial charts
249+
tension: isHourlyData ? 0.45 : 0.4, // More smoothing for dense hourly data
247250
pointRadius: 0,
248251
pointHoverRadius: 7,
249252
pointHoverBackgroundColor: color,
250253
pointHoverBorderColor: '#fff',
251254
pointHoverBorderWidth: 3,
252-
fill: !isBenchmark, // No fill for benchmarks
255+
fill: !isBenchmark, // Fill for all non-benchmark agents
256+
spanGaps: true, // Draw continuous lines even with missing data points
257+
segment: {
258+
borderColor: color,
259+
},
253260
agentName: agentName,
254-
agentIcon: dataLoader.getAgentIcon(agentName),
255-
cubicInterpolationMode: 'monotone' // Smooth, monotonic interpolation
261+
agentIcon: dataLoader.getAgentIcon(agentName)
256262
};
257263

258264
console.log(`[DATASET OBJECT ${index}] borderColor: ${datasetObj.borderColor}, pointHoverBackgroundColor: ${datasetObj.pointHoverBackgroundColor}`);

docs/assets/js/cache-manager.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,18 @@ class CacheManager {
122122
async loadServerCache(market) {
123123
try {
124124
console.log(`[CacheManager] Loading server cache for ${market} market...`);
125-
const response = await fetch(`./data/${market}_cache.json`);
125+
126+
// Add cache-busting to prevent browser HTTP cache from serving stale files
127+
// This ensures we always check for the latest version from the server
128+
const timestamp = Date.now();
129+
const response = await fetch(`./data/${market}_cache.json?v=${timestamp}`, {
130+
cache: 'no-store',
131+
headers: {
132+
'Cache-Control': 'no-cache, no-store, must-revalidate',
133+
'Pragma': 'no-cache',
134+
'Expires': '0'
135+
}
136+
});
126137

127138
if (!response.ok) {
128139
console.warn(`[CacheManager] Server cache not found for ${market} (${response.status})`);

scripts/precompute_frontend_cache.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -541,20 +541,48 @@ def process_benchmark_cn(market_config, agents_data=None):
541541
print(f" Date filter: {start_date_filter} to {end_date_filter}")
542542
print(f" Using initial value from agents: {initial_value}")
543543

544+
# Detect if this is hourly market by checking agent timestamps
545+
is_hourly_market = False
546+
all_agent_timestamps = set()
547+
if agents_data:
548+
for agent_name, agent_data in agents_data.items():
549+
if agent_data.get('assetHistory'):
550+
first_date = agent_data['assetHistory'][0]['date']
551+
if ':' in first_date:
552+
is_hourly_market = True
553+
# Collect all agent timestamps for hourly expansion
554+
for h in agent_data['assetHistory']:
555+
all_agent_timestamps.add(h['date'])
556+
print(f" Market type: {'Hourly' if is_hourly_market else 'Daily'}")
557+
544558
# Convert to asset history format
545559
asset_history = []
546560
dates = sorted(time_series.keys())
547561

548562
benchmark_start_price = None
549563

550-
for date in dates:
564+
# For hourly markets, use agent timestamps; for daily markets, use benchmark dates
565+
timestamps_to_use = sorted(all_agent_timestamps) if is_hourly_market else dates
566+
567+
for timestamp in timestamps_to_use:
551568
# Apply date filtering to match agent date ranges
552-
if start_date_filter and date < start_date_filter:
569+
if start_date_filter and timestamp < start_date_filter:
553570
continue
554-
if end_date_filter and date > end_date_filter:
571+
if end_date_filter and timestamp > end_date_filter:
555572
continue
556573

557-
close_price = float(time_series[date].get('4. close') or time_series[date].get('4. sell price', 0))
574+
# Find the benchmark price
575+
if is_hourly_market:
576+
# For hourly timestamps, extract date part and look up daily price
577+
date_only = timestamp.split(' ')[0]
578+
if date_only not in time_series:
579+
continue
580+
close_price = float(time_series[date_only].get('4. close') or time_series[date_only].get('4. sell price', 0))
581+
else:
582+
# For daily timestamps, direct lookup
583+
if timestamp not in time_series:
584+
continue
585+
close_price = float(time_series[timestamp].get('4. close') or time_series[timestamp].get('4. sell price', 0))
558586

559587
if benchmark_start_price is None:
560588
benchmark_start_price = close_price
@@ -563,9 +591,9 @@ def process_benchmark_cn(market_config, agents_data=None):
563591
current_value = initial_value * (1 + benchmark_return)
564592

565593
asset_history.append({
566-
'date': date,
594+
'date': timestamp,
567595
'value': current_value,
568-
'id': f'sse50-{date}',
596+
'id': f'sse50-{timestamp}',
569597
'action': None
570598
})
571599

@@ -633,7 +661,7 @@ def generate_cache_for_market(market_id, market_config, config):
633661

634662
# Create cache object
635663
# Add a manual version prefix to force cache invalidation when data structure changes
636-
CACHE_FORMAT_VERSION = 'v3' # Increment this when changing data structure (v3: added date filtering to QQQ)
664+
CACHE_FORMAT_VERSION = 'v4' # Increment this when changing data structure (v4: fixed hourly SSE-50 benchmark)
637665
cache = {
638666
'version': f"{CACHE_FORMAT_VERSION}_{version}",
639667
'generatedAt': datetime.now().isoformat(),

0 commit comments

Comments
 (0)