Files

216 lines
5.9 KiB
Python
Raw Permalink Normal View History

2026-02-05 00:25:38 +08:00
# -*- coding: utf-8 -*-
"""
===================================
Data cache management module
2026-02-05 00:25:38 +08:00
===================================
Refer to daily_stock_analysis project implementation
Used to cache realtime market and K-line data to reduce repeated requests
2026-02-05 00:25:38 +08:00
characteristic:
1. TTL (Time To Live) expiration mechanism
2. LRU (Least Recently Used) elimination strategy
3. Partition management by data type
2026-02-05 00:25:38 +08:00
"""
import logging
import threading
import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, Dict, Optional
2026-02-05 00:25:38 +08:00
logger = logging.getLogger(__name__)
@dataclass
class CacheEntry:
"""cache entry"""
2026-02-05 00:25:38 +08:00
data: Any
timestamp: float
ttl: float
hit_count: int = 0
2026-02-05 00:25:38 +08:00
def is_expired(self) -> bool:
"""Check if expired"""
2026-02-05 00:25:38 +08:00
return time.time() - self.timestamp > self.ttl
2026-02-05 00:25:38 +08:00
def age(self) -> float:
"""Return cache age (seconds)"""
2026-02-05 00:25:38 +08:00
return time.time() - self.timestamp
class DataCache:
"""
Data Cache Manager
characteristic:
- TTL expiration mechanism
- Maximum capacity limit
- LRU elimination strategy
- Thread safety
2026-02-05 00:25:38 +08:00
"""
2026-02-05 00:25:38 +08:00
def __init__(
self,
name: str = "default",
default_ttl: float = 600.0, # Default 10 minutes
max_size: int = 1000, # Maximum number of cache entries
2026-02-05 00:25:38 +08:00
):
self.name = name
self.default_ttl = default_ttl
self.max_size = max_size
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
self._lock = threading.RLock()
# Statistics
2026-02-05 00:25:38 +08:00
self._hits = 0
self._misses = 0
2026-02-05 00:25:38 +08:00
def get(self, key: str) -> Optional[Any]:
"""
Get cached data
2026-02-05 00:25:38 +08:00
Returns:
Cached data, returns None if it does not exist or has expired.
2026-02-05 00:25:38 +08:00
"""
with self._lock:
if key not in self._cache:
self._misses += 1
return None
2026-02-05 00:25:38 +08:00
entry = self._cache[key]
# Check if expired
2026-02-05 00:25:38 +08:00
if entry.is_expired():
del self._cache[key]
self._misses += 1
logger.debug(f"[cache] {self.name}:{key} expired and was removed")
2026-02-05 00:25:38 +08:00
return None
# Update access order (LRU)
2026-02-05 00:25:38 +08:00
self._cache.move_to_end(key)
entry.hit_count += 1
self._hits += 1
logger.debug(f"[cache hit] {self.name}:{key} (age: {entry.age():.0f}s/{entry.ttl:.0f}s)")
2026-02-05 00:25:38 +08:00
return entry.data
def set(self, key: str, data: Any, ttl: Optional[float] = None) -> None:
2026-02-05 00:25:38 +08:00
"""
Set cache data
2026-02-05 00:25:38 +08:00
Args:
key: cache key
data: cache data
ttl: expiration time (seconds), None uses the default value
2026-02-05 00:25:38 +08:00
"""
with self._lock:
# Check capacity, perform LRU elimination
2026-02-05 00:25:38 +08:00
while len(self._cache) >= self.max_size:
oldest_key, _ = self._cache.popitem(last=False)
logger.debug(f"[cache] {self.name} reached capacity, evicted: {oldest_key}")
2026-02-05 00:25:38 +08:00
actual_ttl = ttl if ttl is not None else self.default_ttl
self._cache[key] = CacheEntry(data=data, timestamp=time.time(), ttl=actual_ttl)
logger.debug(f"[cache update] {self.name}:{key} TTL={actual_ttl}s")
2026-02-05 00:25:38 +08:00
def delete(self, key: str) -> bool:
"""Delete cache entry"""
2026-02-05 00:25:38 +08:00
with self._lock:
if key in self._cache:
del self._cache[key]
logger.debug(f"[cache] {self.name}:{key} deleted")
2026-02-05 00:25:38 +08:00
return True
return False
2026-02-05 00:25:38 +08:00
def clear(self) -> int:
"""Clear cache"""
2026-02-05 00:25:38 +08:00
with self._lock:
count = len(self._cache)
self._cache.clear()
logger.info(f"[cache] {self.name} cleared {count} records")
2026-02-05 00:25:38 +08:00
return count
2026-02-05 00:25:38 +08:00
def cleanup_expired(self) -> int:
"""Clean up expired entries"""
2026-02-05 00:25:38 +08:00
with self._lock:
expired_keys = [key for key, entry in self._cache.items() if entry.is_expired()]
2026-02-05 00:25:38 +08:00
for key in expired_keys:
del self._cache[key]
2026-02-05 00:25:38 +08:00
if expired_keys:
logger.debug(f"[cache] {self.name} cleaned {len(expired_keys)} expired records")
2026-02-05 00:25:38 +08:00
return len(expired_keys)
2026-02-05 00:25:38 +08:00
def stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
2026-02-05 00:25:38 +08:00
with self._lock:
total_requests = self._hits + self._misses
hit_rate = self._hits / total_requests if total_requests > 0 else 0
2026-02-05 00:25:38 +08:00
return {
"name": self.name,
"size": len(self._cache),
"max_size": self.max_size,
"hits": self._hits,
"misses": self._misses,
"hit_rate": f"{hit_rate:.1%}",
"default_ttl": self.default_ttl,
2026-02-05 00:25:38 +08:00
}
# ============================================
# Global cache instance
2026-02-05 00:25:38 +08:00
# ============================================
# Real-time quotation caching (20 minutes TTL)
_realtime_cache = DataCache(
name="realtime",
default_ttl=1200.0, # 20 minutes
max_size=6000,
2026-02-05 00:25:38 +08:00
)
# K-line data caching (5 minutes TTL, caching on demand)
2026-02-05 00:25:38 +08:00
_kline_cache = DataCache(
name="kline",
default_ttl=300.0, # 5 minutes
max_size=500, # Up to 500 trading pairs
2026-02-05 00:25:38 +08:00
)
# Stock basic information cache (1 day TTL)
2026-02-05 00:25:38 +08:00
_stock_info_cache = DataCache(
name="stock_info",
default_ttl=86400.0, # 24 hours
max_size=6000,
2026-02-05 00:25:38 +08:00
)
def get_realtime_cache() -> DataCache:
"""Get real-time quotation cache"""
return _realtime_cache
2026-02-05 00:25:38 +08:00
def get_kline_cache() -> DataCache:
"""Get K-line data cache"""
2026-02-05 00:25:38 +08:00
return _kline_cache
def get_stock_info_cache() -> DataCache:
"""Get stock information cache"""
2026-02-05 00:25:38 +08:00
return _stock_info_cache
def generate_kline_cache_key(symbol: str, timeframe: str, limit: int, before_time: Optional[int] = None) -> str:
2026-02-05 00:25:38 +08:00
"""
Generate K-line cache key
Format: symbol:timeframe:limit[:before_time]
2026-02-05 00:25:38 +08:00
"""
key = f"{symbol}:{timeframe}:{limit}"
if before_time:
key += f":{before_time}"
return key