javitechjkd/backtestingv2
0
1"""
2Data caching utilities for backtesting application.
3Uses diskcache for persistent caching with automatic expiration.
4"""
5import hashlib
6import json
7from typing import Any, Optional, Callable
8from functools import wraps
9from datetime import timedelta
10import diskcache
11from config import config
12from utils.logger import setup_logger
13
14logger = setup_logger(__name__)
15
16# Initialize disk cache
17cache = diskcache.Cache(str(config.CACHE_DIR))
18
19
20def generate_cache_key(*args, **kwargs) -> str:
21 """
22 Generate a unique cache key from function arguments.
23
24 Args:
25 *args: Positional arguments
26 **kwargs: Keyword arguments
27
28 Returns:
29 MD5 hash of the serialized arguments
30 """
31 key_data = {
32 'args': args,
33 'kwargs': kwargs
34 }
35 key_string = json.dumps(key_data, sort_keys=True, default=str)
36 return hashlib.md5(key_string.encode()).hexdigest()
37
38
39def cached(expire_hours: Optional[int] = None):
40 """
41 Decorator to cache function results on disk.
42
43 Args:
44 expire_hours: Hours until cache expires (uses config default if None)
45
46 Example:
47 @cached(expire_hours=24)
48 def fetch_data(symbol: str, period: str):
49 # Expensive operation
50 return data
51 """
52 def decorator(func: Callable) -> Callable:
53 @wraps(func)
54 def wrapper(*args, **kwargs) -> Any:
55 if not config.ENABLE_CACHE:
56 return func(*args, **kwargs)
57
58 # Generate cache key
59 func_name = f"{func.__module__}.{func.__name__}"
60 cache_key = f"{func_name}:{generate_cache_key(*args, **kwargs)}"
61
62 # Try to get from cache
63 cached_value = cache.get(cache_key)
64 if cached_value is not None:
65 logger.debug(f"Cache HIT for {func_name}")
66 return cached_value
67
68 # Cache miss - compute value
69 logger.debug(f"Cache MISS for {func_name}")
70 result = func(*args, **kwargs)
71
72 # Store in cache with expiration
73 expiry = expire_hours if expire_hours is not None else config.CACHE_EXPIRY_HOURS
74 expire_seconds = expiry * 3600
75 cache.set(cache_key, result, expire=expire_seconds)
76
77 return result
78
79 return wrapper
80 return decorator
81
82
83def clear_cache():
84 """Clear all cached data."""
85 cache.clear()
86 logger.info("Cache cleared")
87
88
89def get_cache_stats() -> dict:
90 """
91 Get cache statistics.
92
93 Returns:
94 Dictionary with cache stats (size, count, hits, misses)
95 """
96 return {
97 'size_mb': cache.volume() / (1024 * 1024),
98 'count': len(cache),
99 'hits': cache.stats(enable=True)[0],
100 'misses': cache.stats(enable=True)[1]
101 }
102
103
104# Example usage for yfinance data fetching
105def get_cached_market_data(symbol: str, period: str, interval: str):
106 """
107 Fetch market data with caching.
108 This is a helper function that can be used in data loaders.
109 """
110 from datetime import datetime
111 import yfinance as yf
112
113 @cached(expire_hours=config.CACHE_EXPIRY_HOURS)
114 def _fetch_data(symbol: str, period: str, interval: str, cache_bust: str):
115 logger.info(f"Fetching data for {symbol} ({period}, {interval})")
116 ticker = yf.Ticker(symbol)
117 data = ticker.history(period=period, interval=interval)
118 return data
119
120 # Cache bust daily to ensure fresh data
121 cache_bust = datetime.now().strftime('%Y-%m-%d')
122 return _fetch_data(symbol, period, interval, cache_bust)
123 