fix(rotation): 溢价率缓存增加增量更新逻辑

- preload_premium: 检查缓存日期范围,不足时增量拉取
- 新增 _fetch_premium_api: 拉取并合并新溢价率数据
- 调用时传入 end_date 触发增量检查

修复前: premium CSV存在即返回旧数据,明天9点运行时拿不到最新
修复后: 检测 latest_cached < end_date 时自动拉取增量
This commit is contained in:
2026-06-01 23:56:18 +08:00
parent 19f1c63981
commit 5e11b6b690

View File

@@ -169,22 +169,40 @@ class DataCache:
except Exception: except Exception:
pass pass
def preload_premium(self, code: str, start_date: str = '2000-01-01', end_date: str = None) -> Optional[Dict[str, float]]: def preload_premium(self, code: str, end_date: str = None) -> Optional[Dict[str, float]]:
"""Load premium data for an ETF code from cache, or fetch from API if not available""" """Load premium data for an ETF code from cache, with incremental update.
If cache exists but doesn't cover end_date, fetches the gap."""
if code in self.premium_data: if code in self.premium_data:
return self.premium_data[code] # Already in memory - check if up-to-date
if end_date:
dates = sorted(self.premium_data[code].keys())
if dates and dates[-1] >= end_date:
return self.premium_data[code]
else:
return self.premium_data[code]
cache_path = self._premium_cache_path(code) cache_path = self._premium_cache_path(code)
if cache_path.exists(): if cache_path.exists():
try: try:
df = pd.read_csv(cache_path) df = pd.read_csv(cache_path)
if len(df) > 0 and 'date' in df.columns and 'premium' in df.columns: if len(df) > 0 and 'date' in df.columns and 'premium' in df.columns:
self.premium_data[code] = dict(zip(df['date'].astype(str), df['premium'])) self.premium_data[code] = dict(zip(df['date'].astype(str), df['premium']))
# Check if cache covers end_date
if end_date:
latest_cached = max(self.premium_data[code].keys())
if latest_cached >= end_date:
return self.premium_data[code]
# Cache is stale - fetch gap from latest_cached+1 to end_date
fetch_start = (pd.Timestamp(latest_cached) + timedelta(days=1)).strftime('%Y-%m-%d')
self._fetch_premium_api(code, fetch_start, end_date)
return self.premium_data[code] return self.premium_data[code]
except Exception: except Exception:
pass pass
# No cache: fetch premium_series directly from API (returns full history) # No cache: fetch full history from API
if end_date is None: self._fetch_premium_api(code, '2000-01-01', end_date or datetime.now().strftime('%Y-%m-%d'))
end_date = datetime.now().strftime('%Y-%m-%d') return self.premium_data.get(code)
def _fetch_premium_api(self, code: str, start_date: str, end_date: str):
"""Fetch premium_series from API and merge into cache"""
url = f"{self.base_url}{self.api_path}" url = f"{self.base_url}{self.api_path}"
params = {'code': code, 'start': start_date, 'end': end_date, 'adj': 'raw'} params = {'code': code, 'start': start_date, 'end': end_date, 'adj': 'raw'}
for attempt in range(3): for attempt in range(3):
@@ -194,25 +212,25 @@ class DataCache:
if attempt < 2: if attempt < 2:
time.sleep(1) time.sleep(1)
continue continue
return None return
data = resp.json() data = resp.json()
if 'error' in data: if 'error' in data:
return None return
premium_series = data.get('premium_series', []) premium_series = data.get('premium_series', [])
if premium_series: if premium_series:
premium_dict = {item['date']: item['premium'] for item in premium_series} new_data = {item['date']: item['premium'] for item in premium_series}
self.premium_data[code] = premium_dict if code not in self.premium_data:
self._save_premium_cache(code, premium_dict) self.premium_data[code] = {}
print(f" + premium {code}: {len(premium_dict)} days") self.premium_data[code].update(new_data)
return premium_dict self._save_premium_cache(code, self.premium_data[code])
return None print(f" + premium {code}: +{len(new_data)} days (total {len(self.premium_data[code])})")
return
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
if attempt < 2: if attempt < 2:
continue continue
return None return
except Exception: except Exception:
return None return
return None
def get_trading_calendar(self, market: str, start_date: str, end_date: str) -> Optional[pd.DatetimeIndex]: def get_trading_calendar(self, market: str, start_date: str, end_date: str) -> Optional[pd.DatetimeIndex]:
"""Fetch trading calendar from API""" """Fetch trading calendar from API"""
@@ -330,7 +348,7 @@ class SimpleRotationStrategy:
self.etf_data[code] = df self.etf_data[code] = df
# Load premium data cache for all ETF trade codes # Load premium data cache for all ETF trade codes
for code in trade_codes: for code in trade_codes:
self.data_cache.preload_premium(code) self.data_cache.preload_premium(code, end_date=end_date)
print(f"\n Trade: {len(self.etf_data)}/{len(trade_codes)} OK, premium: {len(self.data_cache.premium_data)} loaded") print(f"\n Trade: {len(self.etf_data)}/{len(trade_codes)} OK, premium: {len(self.data_cache.premium_data)} loaded")
def _compute_momentum(self, signal_code: str, date: pd.Timestamp) -> Optional[float]: def _compute_momentum(self, signal_code: str, date: pd.Timestamp) -> Optional[float]: