Skip to content

Commit af8e460

Browse files
committed
sss
Signed-off-by: TsssIANHE <TIANHE@GMAIL.COM>
1 parent c0738c0 commit af8e460

File tree

17 files changed

+2864
-284
lines changed

17 files changed

+2864
-284
lines changed

backend_api_python/app/config/api_keys.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ def GROK_API_KEY(cls):
6868
from app.utils.config_loader import load_addon_config
6969
val = load_addon_config().get('grok', {}).get('api_key')
7070
return val if val else ''
71+
72+
@property
73+
def TAVILY_API_KEYS(cls):
74+
"""Tavily Search API keys (comma-separated for rotation)"""
75+
env_val = os.getenv('TAVILY_API_KEYS', '').strip()
76+
if env_val:
77+
return [k.strip() for k in env_val.split(',') if k.strip()]
78+
from app.utils.config_loader import load_addon_config
79+
val = load_addon_config().get('tavily', {}).get('api_keys', '')
80+
if val:
81+
return [k.strip() for k in val.split(',') if k.strip()]
82+
return []
83+
84+
@property
85+
def BOCHA_API_KEYS(cls):
86+
"""Bocha Search API keys (comma-separated for rotation)"""
87+
env_val = os.getenv('BOCHA_API_KEYS', '').strip()
88+
if env_val:
89+
return [k.strip() for k in env_val.split(',') if k.strip()]
90+
from app.utils.config_loader import load_addon_config
91+
val = load_addon_config().get('bocha', {}).get('api_keys', '')
92+
if val:
93+
return [k.strip() for k in val.split(',') if k.strip()]
94+
return []
95+
96+
@property
97+
def SERPAPI_KEYS(cls):
98+
"""SerpAPI keys (comma-separated for rotation)"""
99+
env_val = os.getenv('SERPAPI_KEYS', '').strip()
100+
if env_val:
101+
return [k.strip() for k in env_val.split(',') if k.strip()]
102+
from app.utils.config_loader import load_addon_config
103+
val = load_addon_config().get('serpapi', {}).get('api_keys', '')
104+
if val:
105+
return [k.strip() for k in val.split(',') if k.strip()]
106+
return []
71107

72108

73109
class APIKeys(metaclass=MetaAPIKeys):
Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,61 @@
11
"""
22
数据源模块
33
支持多种市场的K线数据获取
4+
5+
改进版本(参考 daily_stock_analysis 项目):
6+
- 熔断器保护 (circuit_breaker)
7+
- 数据缓存 (cache_manager)
8+
- 防封禁策略 (rate_limiter)
9+
- 多数据源自动切换 (data_manager)
410
"""
511
from app.data_sources.factory import DataSourceFactory
12+
from app.data_sources.circuit_breaker import (
13+
CircuitBreaker,
14+
get_ashare_circuit_breaker,
15+
get_realtime_circuit_breaker
16+
)
17+
from app.data_sources.cache_manager import (
18+
DataCache,
19+
get_realtime_cache,
20+
get_kline_cache,
21+
get_stock_info_cache
22+
)
23+
from app.data_sources.rate_limiter import (
24+
RateLimiter,
25+
get_eastmoney_limiter,
26+
get_tencent_limiter,
27+
get_akshare_limiter,
28+
get_random_user_agent,
29+
random_sleep,
30+
retry_with_backoff
31+
)
32+
from app.data_sources.data_manager import (
33+
AShareDataManager,
34+
get_ashare_data_manager
35+
)
636

7-
__all__ = ['DataSourceFactory']
37+
__all__ = [
38+
# 工厂
39+
'DataSourceFactory',
40+
# 熔断器
41+
'CircuitBreaker',
42+
'get_ashare_circuit_breaker',
43+
'get_realtime_circuit_breaker',
44+
# 缓存
45+
'DataCache',
46+
'get_realtime_cache',
47+
'get_kline_cache',
48+
'get_stock_info_cache',
49+
# 限流器
50+
'RateLimiter',
51+
'get_eastmoney_limiter',
52+
'get_tencent_limiter',
53+
'get_akshare_limiter',
54+
'get_random_user_agent',
55+
'random_sleep',
56+
'retry_with_backoff',
57+
# 数据管理器
58+
'AShareDataManager',
59+
'get_ashare_data_manager',
60+
]
861

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
===================================
4+
数据缓存管理模块
5+
===================================
6+
7+
参考 daily_stock_analysis 项目实现
8+
用于缓存实时行情和K线数据,减少重复请求
9+
10+
特性:
11+
1. TTL (Time To Live) 过期机制
12+
2. LRU (Least Recently Used) 淘汰策略
13+
3. 按数据类型分区管理
14+
"""
15+
16+
import time
17+
import logging
18+
from typing import Dict, Any, Optional, List
19+
from collections import OrderedDict
20+
from dataclasses import dataclass, field
21+
from datetime import datetime
22+
import threading
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
@dataclass
28+
class CacheEntry:
29+
"""缓存条目"""
30+
data: Any
31+
timestamp: float
32+
ttl: float
33+
hit_count: int = 0
34+
35+
def is_expired(self) -> bool:
36+
"""检查是否过期"""
37+
return time.time() - self.timestamp > self.ttl
38+
39+
def age(self) -> float:
40+
"""返回缓存年龄(秒)"""
41+
return time.time() - self.timestamp
42+
43+
44+
class DataCache:
45+
"""
46+
数据缓存管理器
47+
48+
特性:
49+
- TTL 过期机制
50+
- 最大容量限制
51+
- LRU 淘汰策略
52+
- 线程安全
53+
"""
54+
55+
def __init__(
56+
self,
57+
name: str = "default",
58+
default_ttl: float = 600.0, # 默认10分钟
59+
max_size: int = 1000 # 最大缓存条目数
60+
):
61+
self.name = name
62+
self.default_ttl = default_ttl
63+
self.max_size = max_size
64+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
65+
self._lock = threading.RLock()
66+
67+
# 统计信息
68+
self._hits = 0
69+
self._misses = 0
70+
71+
def get(self, key: str) -> Optional[Any]:
72+
"""
73+
获取缓存数据
74+
75+
Returns:
76+
缓存的数据,不存在或过期返回 None
77+
"""
78+
with self._lock:
79+
if key not in self._cache:
80+
self._misses += 1
81+
return None
82+
83+
entry = self._cache[key]
84+
85+
# 检查是否过期
86+
if entry.is_expired():
87+
del self._cache[key]
88+
self._misses += 1
89+
logger.debug(f"[缓存] {self.name}:{key} 已过期,删除")
90+
return None
91+
92+
# 更新访问顺序(LRU)
93+
self._cache.move_to_end(key)
94+
entry.hit_count += 1
95+
self._hits += 1
96+
97+
logger.debug(f"[缓存命中] {self.name}:{key} (年龄: {entry.age():.0f}s/{entry.ttl:.0f}s)")
98+
return entry.data
99+
100+
def set(
101+
self,
102+
key: str,
103+
data: Any,
104+
ttl: Optional[float] = None
105+
) -> None:
106+
"""
107+
设置缓存数据
108+
109+
Args:
110+
key: 缓存键
111+
data: 缓存数据
112+
ttl: 过期时间(秒),None 使用默认值
113+
"""
114+
with self._lock:
115+
# 检查容量,执行 LRU 淘汰
116+
while len(self._cache) >= self.max_size:
117+
oldest_key, _ = self._cache.popitem(last=False)
118+
logger.debug(f"[缓存] {self.name} 容量已满,淘汰: {oldest_key}")
119+
120+
actual_ttl = ttl if ttl is not None else self.default_ttl
121+
self._cache[key] = CacheEntry(
122+
data=data,
123+
timestamp=time.time(),
124+
ttl=actual_ttl
125+
)
126+
127+
logger.debug(f"[缓存更新] {self.name}:{key} TTL={actual_ttl}s")
128+
129+
def delete(self, key: str) -> bool:
130+
"""删除缓存条目"""
131+
with self._lock:
132+
if key in self._cache:
133+
del self._cache[key]
134+
logger.debug(f"[缓存] {self.name}:{key} 已删除")
135+
return True
136+
return False
137+
138+
def clear(self) -> int:
139+
"""清空缓存"""
140+
with self._lock:
141+
count = len(self._cache)
142+
self._cache.clear()
143+
logger.info(f"[缓存] {self.name} 已清空 {count} 条记录")
144+
return count
145+
146+
def cleanup_expired(self) -> int:
147+
"""清理过期条目"""
148+
with self._lock:
149+
expired_keys = [
150+
key for key, entry in self._cache.items()
151+
if entry.is_expired()
152+
]
153+
for key in expired_keys:
154+
del self._cache[key]
155+
156+
if expired_keys:
157+
logger.debug(f"[缓存] {self.name} 清理 {len(expired_keys)} 条过期记录")
158+
return len(expired_keys)
159+
160+
def stats(self) -> Dict[str, Any]:
161+
"""获取缓存统计信息"""
162+
with self._lock:
163+
total_requests = self._hits + self._misses
164+
hit_rate = self._hits / total_requests if total_requests > 0 else 0
165+
166+
return {
167+
'name': self.name,
168+
'size': len(self._cache),
169+
'max_size': self.max_size,
170+
'hits': self._hits,
171+
'misses': self._misses,
172+
'hit_rate': f"{hit_rate:.1%}",
173+
'default_ttl': self.default_ttl
174+
}
175+
176+
177+
# ============================================
178+
# 全局缓存实例
179+
# ============================================
180+
181+
# A股实时行情缓存(20分钟TTL,全市场数据量大)
182+
_ashare_realtime_cache = DataCache(
183+
name="ashare_realtime",
184+
default_ttl=1200.0, # 20分钟
185+
max_size=6000 # 约5000+股票
186+
)
187+
188+
# K线数据缓存(5分钟TTL,按需缓存)
189+
_kline_cache = DataCache(
190+
name="kline",
191+
default_ttl=300.0, # 5分钟
192+
max_size=500 # 最多500个交易对
193+
)
194+
195+
# 股票基本信息缓存(1天TTL)
196+
_stock_info_cache = DataCache(
197+
name="stock_info",
198+
default_ttl=86400.0, # 24小时
199+
max_size=6000
200+
)
201+
202+
203+
def get_realtime_cache() -> DataCache:
204+
"""获取实时行情缓存"""
205+
return _ashare_realtime_cache
206+
207+
208+
def get_kline_cache() -> DataCache:
209+
"""获取K线数据缓存"""
210+
return _kline_cache
211+
212+
213+
def get_stock_info_cache() -> DataCache:
214+
"""获取股票信息缓存"""
215+
return _stock_info_cache
216+
217+
218+
def generate_kline_cache_key(
219+
symbol: str,
220+
timeframe: str,
221+
limit: int,
222+
before_time: Optional[int] = None
223+
) -> str:
224+
"""
225+
生成K线缓存键
226+
227+
格式: symbol:timeframe:limit[:before_time]
228+
"""
229+
key = f"{symbol}:{timeframe}:{limit}"
230+
if before_time:
231+
key += f":{before_time}"
232+
return key

0 commit comments

Comments
 (0)