Skip to content

Commit 905b366

Browse files
massif-01ZhuLinsen
andauthored
feat: integrate TickFlow market review enhancement (ZhuLinsen#632) (ZhuLinsen#745)
Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
1 parent f57f21f commit 905b366

14 files changed

Lines changed: 988 additions & 4 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ STOCK_LIST=600519,300750,002594
1111
# 数据源配置
1212
# Tushare Pro Token(可选,从 https://tushare.pro/weborder/#/login?reg=834638 获取)
1313
TUSHARE_TOKEN=
14+
# TickFlow API Key(可选,用于 A 股大盘复盘指数增强;若套餐支持标的池查询,也可增强市场统计)
15+
# TICKFLOW_API_KEY=
1416

1517
# ===================================
1618
# 定时任务配置(本地/Docker运行)

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
| `SOCIAL_SENTIMENT_API_KEY` | [Stock Sentiment API](https://api.adanos.org/docs)(Reddit/X/Polymarket 社交舆情,仅美股) | 可选 |
173173
| `SOCIAL_SENTIMENT_API_URL` | 自定义社交舆情 API 地址(默认 `https://api.adanos.org`| 可选 |
174174
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 |
175+
| `TICKFLOW_API_KEY` | [TickFlow](https://tickflow.org) API Key(增强 A 股大盘复盘指数;若套餐支持标的池查询,也可增强市场统计) | 可选 |
175176
| `PREFETCH_REALTIME_QUOTES` | 实时行情预取开关:设为 `false` 可禁用全市场预取(默认 `true`| 可选 |
176177
| `WECHAT_MSG_TYPE` | 企微消息类型,默认 markdown,支持配置 text 类型,发送纯 markdown 文本 | 可选 |
177178
| `NEWS_STRATEGY_PROFILE` | 新闻策略窗口档位:`ultra_short`(1天) / `short`(3天) / `medium`(7天) / `long`(30天),默认 `short` | 可选 |
@@ -202,6 +203,10 @@
202203
> - `get_stock_info.belong_boards` = 个股所属板块列表;
203204
> - `get_stock_info.boards` 为兼容别名,值与 `belong_boards` 相同(未来仅在大版本考虑移除);
204205
> - `get_stock_info.sector_rankings``fundamental_context.boards.data` 保持一致。
206+
> - 配置 `TICKFLOW_API_KEY` 后,A 股大盘复盘的主要指数行情会优先尝试 TickFlow;若当前套餐支持标的池查询,市场涨跌统计也会优先尝试 TickFlow。失败或权限不足时会回退到现有数据源。
207+
> - 该行为按能力分层而非仅按“是否有 Key”判断:有限权限套餐仍可获得 TickFlow 指数增强;支持 `CN_Equity_A` 标的池查询的套餐会额外获得 TickFlow 市场统计增强。
208+
> - TickFlow 官方 quickstart 展示了 `quotes.get(universes=["CN_Equity_A"])` 的正式用法,但真实线上验证确认:不同套餐权限不同,且 `quotes.get(symbols=[...])` 单次有标的数量限制。
209+
> - TickFlow 返回的 `change_pct` / `amplitude` 在实际接口中为比例值,本项目已统一转换为内部使用的百分比口径,保证与 AkShare / Tushare / efinance 一致。
205210
> - 板块涨跌榜采用固定回退顺序:`AkShare(EM->Sina) -> Tushare -> efinance`
206211
207212
#### 3. 启用 Actions

data_provider/base.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,11 +492,83 @@ def __init__(self, fetchers: Optional[List[BaseFetcher]] = None):
492492
# 默认数据源将在首次使用时延迟加载
493493
self._init_default_fetchers()
494494
self._fundamental_adapter = AkshareFundamentalAdapter()
495+
self._tickflow_fetcher = None
496+
self._tickflow_api_key: Optional[str] = None
497+
self._tickflow_lock = RLock()
495498
self._fundamental_cache: Dict[str, Dict[str, Any]] = {}
496499
self._fundamental_cache_lock = RLock()
497500
self._fundamental_timeout_worker_limit = 8
498501
self._fundamental_timeout_slots = BoundedSemaphore(self._fundamental_timeout_worker_limit)
499502

503+
def _get_tickflow_fetcher(self):
504+
"""Lazily create a TickFlow fetcher for market-review-only calls."""
505+
from src.config import get_config
506+
507+
config = get_config()
508+
api_key = (getattr(config, "tickflow_api_key", None) or "").strip()
509+
510+
if not hasattr(self, "_tickflow_lock") or self._tickflow_lock is None:
511+
self._tickflow_lock = RLock()
512+
513+
with self._tickflow_lock:
514+
current_fetcher = getattr(self, "_tickflow_fetcher", None)
515+
current_key = getattr(self, "_tickflow_api_key", None)
516+
517+
if not api_key:
518+
if current_fetcher is not None and hasattr(current_fetcher, "close"):
519+
try:
520+
current_fetcher.close()
521+
except Exception as exc:
522+
logger.debug("[TickFlowFetcher] 关闭旧实例失败: %s", exc)
523+
self._tickflow_fetcher = None
524+
self._tickflow_api_key = None
525+
return None
526+
527+
if current_fetcher is not None and current_key == api_key:
528+
return current_fetcher
529+
530+
if current_fetcher is not None and hasattr(current_fetcher, "close"):
531+
try:
532+
current_fetcher.close()
533+
except Exception as exc:
534+
logger.debug("[TickFlowFetcher] 切换实例时关闭失败: %s", exc)
535+
536+
try:
537+
from .tickflow_fetcher import TickFlowFetcher
538+
539+
fetcher = TickFlowFetcher(api_key=api_key)
540+
self._tickflow_fetcher = fetcher
541+
self._tickflow_api_key = api_key
542+
return fetcher
543+
except Exception as exc:
544+
logger.warning("[TickFlowFetcher] 初始化失败: %s", exc)
545+
self._tickflow_fetcher = None
546+
self._tickflow_api_key = None
547+
return None
548+
549+
def close(self) -> None:
550+
"""Best-effort release of manager-owned resources."""
551+
if not hasattr(self, "_tickflow_lock") or self._tickflow_lock is None:
552+
self._tickflow_lock = RLock()
553+
554+
with self._tickflow_lock:
555+
current_fetcher = getattr(self, "_tickflow_fetcher", None)
556+
self._tickflow_fetcher = None
557+
self._tickflow_api_key = None
558+
559+
if current_fetcher is not None and hasattr(current_fetcher, "close"):
560+
try:
561+
current_fetcher.close()
562+
except Exception as exc:
563+
logger.debug("[TickFlowFetcher] 关闭管理器资源失败: %s", exc)
564+
565+
def __del__(self) -> None:
566+
try:
567+
self.close()
568+
except Exception:
569+
# Best-effort cleanup during interpreter shutdown.
570+
pass
571+
500572
def _get_fundamental_cache_key(self, stock_code: str, budget_seconds: Optional[float] = None) -> str:
501573
"""生成基本面缓存 key(包含预算分桶以避免低预算结果污染高预算请求)。"""
502574
normalized_code = normalize_stock_code(stock_code)
@@ -1348,6 +1420,17 @@ def batch_get_stock_names(self, stock_codes: List[str]) -> Dict[str, str]:
13481420

13491421
def get_main_indices(self, region: str = "cn") -> List[Dict[str, Any]]:
13501422
"""获取主要指数实时行情(自动切换数据源)"""
1423+
if region == "cn":
1424+
tickflow_fetcher = self._get_tickflow_fetcher()
1425+
if tickflow_fetcher is not None:
1426+
try:
1427+
data = tickflow_fetcher.get_main_indices(region=region)
1428+
if data:
1429+
logger.info("[TickFlowFetcher] 获取指数行情成功")
1430+
return data
1431+
except Exception as e:
1432+
logger.warning(f"[TickFlowFetcher] 获取指数行情失败: {e}")
1433+
13511434
for fetcher in self._fetchers:
13521435
try:
13531436
data = fetcher.get_main_indices(region=region)
@@ -1361,6 +1444,16 @@ def get_main_indices(self, region: str = "cn") -> List[Dict[str, Any]]:
13611444

13621445
def get_market_stats(self) -> Dict[str, Any]:
13631446
"""获取市场涨跌统计(自动切换数据源)"""
1447+
tickflow_fetcher = self._get_tickflow_fetcher()
1448+
if tickflow_fetcher is not None:
1449+
try:
1450+
data = tickflow_fetcher.get_market_stats()
1451+
if data:
1452+
logger.info("[TickFlowFetcher] 获取市场统计成功")
1453+
return data
1454+
except Exception as e:
1455+
logger.warning(f"[TickFlowFetcher] 获取市场统计失败: {e}")
1456+
13641457
for fetcher in self._fetchers:
13651458
try:
13661459
data = fetcher.get_market_stats()

0 commit comments

Comments
 (0)