Skip to content

Latest commit

 

History

History
479 lines (378 loc) · 28.2 KB

File metadata and controls

479 lines (378 loc) · 28.2 KB

tickbridge — 统一数据层

多数据源 + cache → DB → source fall-over 抽象层;目标是可独立开源。 顶层目录:tickbridge/不是 stocklens 的子包)。

产品愿景背景

stocklens 主仓的最终形态是「拼凑各个独立好仓 → 整合成一个 Web UI(看热力图等)」:

  ┌──────────────────────────────────────────────┐
  │   web UI (FastAPI + 模板) — 行情热力图 / 分析 │
  └────────────┬─────────────────────────────────┘
               │ HTTP / API
               ▼
  ┌──────────────────────────────────────────────┐
  │   stocklens (业务编排:pipeline / scoring /     │
  │              agent / pipeline / portfolio)     │
  └─┬──────────┬──────────┬──────────┬───────────┘
    │          │          │          │
    ▼          ▼          ▼          ▼
 tickbridge notify-    search-    llm-bridge ...
            bridge     bridge
 (本文)    (规划)      (规划)     (规划)

每个 bridge 都做成「单类对外 + 插件继承 + cache→DB→源 三级读路径」的统一形态,满足三件事:

  1. 在 stocklens 主仓里 in-tree 共存(开发期);
  2. 自带 pyproject.toml / LICENSE / README.md,未来可独立成单独 GitHub 仓 / PyPI 包;
  3. web UI 可以直接驱动它们 —— REST endpoint 简单转发到 bridge.X(),不再嵌套业务层。

tickbridge 是这条路线上的第一个,也是验证抽象套路是否可行的样板。下一个轮到 notify-bridge。

角色

  • 唯一对外门面tickbridge.DataProvider
  • 多源行情插件tickbridge.contracts.BaseFetcher 的子类(雪球/yfinance/...),按 priority 顺序 fall-over。
  • 单源领域插件tickbridge.contracts.DomainFetcher 的子类(finviz/reddit/...),单源直通,不 fall-over。
  • 可注入存储/缓存CacheProtocol / RepositoryProtocol —— 默认走 tickbridge 内置的 SQLite + 内存缓存,但 stocklens 这类宿主应用可注入自有实现。

stocklens 把 tickbridge 当作外部依赖使用:所有数据获取一律走 from tickbridge import DataProvider,禁止再 from data_provider.fetchers.X import Y 或自己拼 HTTP。

目录(Commit 1b 完成态)

tickbridge/
├── __init__.py                顶层暴露 DataProvider + __version__
├── pyproject.toml             独立包定义(pip install -e tickbridge/)
├── LICENSE                    MIT
├── README.md                  独立项目说明(不引用 stocklens)
│
├── core/                      门面 / 注册表 / 读路径 / 工具
│   ├── provider.py            DataProvider(唯一对外类,Commit 2 实装读路径)
│   ├── manager.py             DataFetcherManager(多源 fall-over)+ get_default_manager()
│   │                          / reset_default_manager()(进程级单例工厂,F1)
│   ├── realtime_mixin.py      实时报价能力 mixin
│   ├── chip_mixin.py          筹码分布 mixin
│   ├── fundamental_mixin.py   基本面聚合 mixin
│   ├── market_mixin.py        指数 / 板块 / 市场统计 mixin
│   ├── stock_info_mixin.py    股票名 / sector mixin
│   ├── circuit_breaker.py     CircuitBreaker — pybreaker 薄包装(per-source CLOSED/OPEN/HALF_OPEN)
│   ├── fail_open.py           fail_open(logger, op, exc) — 统一 fail-open 日志;
│   │                          tickbridge 不可 import stocklens,所以这是
│   │                          stocklens.utils.fail_open 的本地副本
│   ├── exceptions.py          DataFetchError / RateLimitError + 工具
│   └── utils.py               safe_float / safe_int
│
├── contracts/                 ABC / Protocol(接口契约)
│   ├── base_fetcher.py        BaseFetcher (ABC) — 多源行情接口
│   ├── domain_fetcher.py      DomainFetcher (ABC) — 单源领域接口
│   ├── cache_protocol.py      CacheProtocol — 缓存接口
│   └── repository_protocol.py RepositoryProtocol — 存储接口
│
├── models/                    公开数据契约(dataclass)
│   ├── quote.py               UnifiedRealtimeQuote / RealtimeSource (`trade_date` 字段 — 2026-05 新增,让下游做新鲜度验证)
│   ├── chip.py                ChipDistribution
│   ├── ohlcv.py / fundamental.py / market.py  (Commit 2 填充)
│
├── sources/                   10 个 BaseFetcher 子类
│   └── xueqiu / yfinance / akshare / tushare / efinance / openbb /
│       pytdx / baostock / twelvedata / tickflow
│
├── domain_sources/            单源 DomainFetcher 子类(搬自 stocklens/services/)
│   ├── finviz.py              FinvizFetcher + 5 个 fetch_* 函数
│   └── social_sentiment.py    SocialSentimentService(DomainFetcher 子类)
│
├── codes/                     股票代码工具
│   ├── normalize.py           normalize_stock_code / 市场识别(搬自 stock_code.py)
│   ├── us_index.py            US 指数符号映射(搬自 us_index_mapping.py)
│   └── stock_mapping.py       代码→中文名(搬自 common/stock_mapping.py)
│
├── adapters/                  上游 → 公开模型的字段适配
│   └── fundamental.py         A/港/美基本面字段统一(搬自 fundamental_adapter.py)
│
├── internal/                  私有补丁(不开放给调用方)
│   └── eastmoney_patch.py     东方财富 NID 授权
│
├── config/                    DI 钩子
│   └── settings.py            register_config_provider / get_dp_config / register_symbol_supplier / get_symbol_supplier(host 注入)
│
├── cache/                     默认 In-Memory TTL + SQLite 缓存(Commit 2 实现)
├── storage/                   默认 SQLite Repository(Commit 2 实现)
└── tests/
    └── test_smoke.py          32 个烟测:公开面、ABC、模块导入、模型 round-trip、CircuitBreaker

当前进度

Commit 状态 范围
1a 骨架、空 ABC、pyproject.toml + LICENSE + README、4 个 smoke test
1b git mv data_provider/* → tickbridge/* + 拆分 realtime_types.py(4 份)+ 合并 base_fetcher.py 接口契约 + 全工程 import 替换(30+ 文件,60+ 处)+ 32 个 smoke test
2 DataProvider 实装 cache → DB → manager 读路径;tickbridge/cache/ 出 InMemory + SQLite + Tiered 三层;tickbridge/storage/ 出默认 SqliteRepositorycore/read_through.py 高阶函数;67 个测试(cache / storage / read_through / provider integration / smoke)
3 git mv finviz / social_sentiment 到 tickbridge/domain_sources/FinvizFetcher + SocialSentimentService 实现 DomainFetcher 协议;DataProviderfinviz_* / social_* 方法 + source(name) 逃生口;stocklens/services/ 留薄壳 re-export;4 处直接 import + 2 处雪球野路子(sectors.py / etf_quote_service.py)全部改走 DataProvider;70+ tests
4 CLAUDE.md 强制约束("代码层禁止 from tickbridge.sources.*","requests.get 拉行情"等);tickbridge/README.md 加 ASCII 架构图 + DI 烹饪手册;主仓 README.md 加产品愿景段落;薄壳 re-export 仍保留(10 处 stocklens 调用方过渡期使用,不强制立即清理)
5 (已撤销 2026-05)原 5a-5d 引入了 market_hours.py / BackgroundPoller / dashboard 1s 轮询 / 7 个免费实时 fetcher。项目重新定位为盘后复盘工具,全部撤回,TTL 统一为 30 min,报价仅来自 stock_quote_snapshot DB 表。

读路径(Commit 2 实装)

DataProvider.get_realtime_quote("AAPL")
    │
    ▼
tickbridge.core.read_through.read_through(
    cache_key="quote:AAPL",
    cache=TieredCache(L1, L2),
    db_loader=lambda: repo.load_quote("AAPL"),
    db_saver=lambda v: repo.save_quote("AAPL", v),
    source_call=lambda: manager.get_realtime_quote("AAPL"),
    is_hit=_quote_is_hit,
)
    │
    ├─ L1 (InMemoryTTLCache) ─ HIT  ⇒ return
    │                          MISS ↓
    ├─ L2 (SqliteTTLCache)   ─ HIT  ⇒ populate L1, return
    │                          MISS ↓
    ├─ Repository.load_quote ─ HIT  ⇒ populate L1+L2, return
    │                          MISS ↓
    └─ DataFetcherManager     ─ HIT  ⇒ persist + cache, return
                               MISS ⇒ return None

每个 DataProvider.get_* 方法都走同一个 read_through() 高阶函数,避免 9 套相同形状的代码漂移。
TTL 政策(tickbridge/core/provider.py 顶部常量):

方法 TTL
get_realtime_quote / get_index_quote / get_etf_quote美股 盘中 30s,盘外 12h
get_realtime_quoteA股 / HK,待扩展) 60s(legacy default)
get_daily_data 6h
get_fundamental 12h
get_chip_distribution 1h
get_main_indices / get_market_stats / get_sector_rankings 60s

Quote TTL 统一 30 min(盘后复盘工具,无需盘中切换)。

命中判定 _quote_is_hit

读路径 cache / DB hit 是否"算数"由 _quote_is_hit(value) 决定(provider.py):

  1. value is None → miss
  2. value.has_basic_data() 必须为真(即 price > 0

DI 钩子

from tickbridge import DataProvider

# Default:内置 InMemoryTTLCache + SqliteTTLCache + SqliteRepository
provider = DataProvider()

# 自带 Redis 的宿主:注入自己的 Cache 实现
provider = DataProvider(cache=MyRedisCache())

# 完全无 cache 无 DB(测试 / 一次性脚本)
provider = DataProvider(cache=False, repository=False)

# 自带 SQLAlchemy 的宿主:注入 RepositoryProtocol 实现
provider = DataProvider(repository=MyHostRepoAdapter(host_db))

# 注入 Domain 插件(Commit 3)
from tickbridge.domain_sources import SocialSentimentService
provider = DataProvider(
    social_sentiment=SocialSentimentService(api_key="sk_live_..."),
)
# 显式禁用 finviz / social:
provider = DataProvider(finviz=False, social_sentiment=False)

逃生口:provider.source(name)

少数情形下(比如 XueqiuFetcher.fetch_barsYfinanceFetcher.get_earnings_calendar)需要直接调用某个 fetcher 的子类专属方法, 这些不在 BaseFetcher 标准契约里。provider.source(name) 提供逃生口:

xq = provider.source("xueqiu")     # 返回 XueqiuFetcher 实例
df = xq.fetch_bars(code, interval="1d", count=30)

注意:用 source() 拿到 fetcher 后绕过了 cache → DB → fall-over 读路径, 请优先用 provider.get_* 系列方法;只有在标准契约真的不够用时才回落到 source()

公开 API(target;分阶段实装)

from tickbridge import DataProvider

provider = DataProvider()                     # 默认配置 + 内置 SQLite

# Multi-source(cache → DB → fetcher)
provider.get_realtime_quote("AAPL")
provider.get_index_quote(".DJI")
provider.get_etf_quote("XLK")
provider.get_daily_data("AAPL", days=120)
provider.get_chip_distribution("000001")
provider.get_fundamental("AAPL")
provider.get_main_indices(region="us")
provider.get_market_stats()
provider.get_sector_rankings(n=5)

# Domain helpers(单源直通;Commit 3 实装)
provider.finviz_forward("AAPL")        # forward valuation snapshot
provider.finviz_news("AAPL", limit=10)
provider.finviz_peers("AAPL")
provider.finviz_insider("AAPL")
provider.finviz_sector_etf("Tech")
provider.social_reddit_report("AAPL")
provider.social_x_trending()
provider.social_get_context("AAPL")    # combined Reddit+X+Polymarket text block

# Escape hatch — backend-specific methods (rare)
xq = provider.source("xueqiu")
df = xq.fetch_bars("AAPL", interval="60m", count=200)

Commit 3 完成后所有方法都是真实实装;不再有 stub。
过渡期 stocklens/services/finviz_service.py / social_sentiment_service.py 保留薄壳 re-export shim(指向 tickbridge),现存调用点零改动;Commit 4 删除 shim 时再统一迁移。

跨层依赖规则(强约束)

  • stocklens/** 可以 from tickbridge import DataProviderfrom tickbridge.core.manager import get_default_manager(生产代码)或 DataFetcherManager(测试隔离)
  • 🔒 F1(2026-05):所有生产入口(pipeline / API / Bot / agent tools / services)必须用 get_default_manager() 而不是 DataFetcherManager()。原因:每个 fetcher 持有 pybreaker 熔断器,重建 manager 会让 OPEN/HALF_OPEN 状态归零,等于熔断失效。reset_default_manager() 仅在 SystemConfigService 改完 .env 时由内部调用,外部代码不要碰。
  • tickbridge/** 不可以 from stocklens.*from api.*from bot.*(保证 tickbridge 可独立开源)
  • stocklens/** 不可以 from data_provider.* —— data_provider/ 已删除(Commit 1b)
  • stocklens/services/** 不可以自己 requests.get(...) 拉行情(应走 provider.X(...))—— Commit 3 / 4 强制

完整规则见 CLAUDE.md 的"代码 → 文档同步映射"。

测试矩阵

pytest tickbridge/tests/(32 项):

类别 用例数 覆盖
公开面 3 tickbridge.DataProvider + __version__ 暴露;stub 方法必须 raise NotImplementedError
ABC / Protocol 1 BaseFetcher 拒绝实例化、DomainFetcher 可子类化、Cache/RepositoryProtocol 通过 isinstance
模块导入烟测 22 每个迁移过来的模块都能 import(含 10 个 fetcher 子类)
数据模型 2 UnifiedRealtimeQuote / ChipDistribution to_dict round-trip
CircuitBreaker 2 失败计数到阈值后 OPEN;reset 恢复
Manager 接口 1 DataFetcherManager 可导入
tests/unit/tickbridge/(搬自 tests/unit/data_provider/,stocklens 的 pytest 套件下)—— failover 行为、雪球字段适配、共 12 个用例

主仓 pytest tests/(341 项)—— 全部通过。

注意事项

  • 顶层目录命名 tickbridge/,PyPI 包名将来 publish 时再决定(可能因占用而改名,但本仓不受影响)
  • LICENSE 已选定 MIT;与 stocklens 主仓 LICENSE 独立
  • pyproject.toml 已声明独立 build:pip install -e tickbridge/ 可作为单独的 Python 包安装
  • 所有 ABC 默认实现返回 None 而非 raise NotImplementedError,让"不支持某接口"成为合法状态(避免每个子类大量 def get_X(...): return None 的占位代码)
  • get_dp_configdp_ 前缀临时保留以减少 fetcher 子类的 churn,等 Commit 4 一并 rename 为 get_config

性能优化(2026-05)

HTTP 连接复用

引入模块级 requests.Session / httpx.Client 单例,避免每次调用都做 TCP+TLS 握手 (每次冷连接 50-200ms):

文件 复用对象 用途
tickbridge/sources/akshare.py _get_session() Sina + Tencent 实时行情
tickbridge/sources/twelvedata.py _get_session() TwelveData API
tickbridge/sources/tushare.py _get_session() api.tushare.pro
tickbridge/domain_sources/social_sentiment.py _get_session() api.adanos.org

均采用 requests.adapters.HTTPAdapter(pool_connections=N, pool_maxsize=2N) 调优。 注意:xueqiu.py / eastmoney_patch.py 因深度耦合 requests 内部 API(cookie jar、Session monkey-patch)保持不变;新增 HTTP 调用一律走 httpx。

缓存集中化

tickbridge/domain_sources/finviz.py 中 5 个手写 Dict[str, tuple] + time.time() 缓存 全部迁移到 kvcache namespace(详见 docs/modules/kvcache.md):

旧 dict 新 namespace TTL
_page_cache finviz_page 30 min
_cache (forward) finviz_forward 1 h
_news_cache finviz_news 30 min
_peer_cache finviz_peer 1 h
_sector_etf_cache finviz_sector_etf 1 h

收益:统一 TTL 调优入口、共享 janitor 清理、纳入命中率统计。

tickbridge/core/fundamental_mixin.py_fundamental_cache(self-state,紧耦合 _prune_fundamental_cache / _fundamental_cache_lock)暂未迁移;后续如要迁移需改为 基于 kvcache namespace + 删除 mixin 内部 prune 逻辑。

Xueqiu 财务数据并行化(2026-05)

XueqiuFetcher.get_finance_data 之前 6 个独立 HTTP 请求串行执行,单只股票最差 60 秒 (10s timeout × 6)。重构后拆为 6 个 _fetch_*_block 静态方法, 通过 ThreadPoolExecutor(max_workers=6, thread_name_prefix='xq-finance') 并行 fan-out。共享 requests.Session_ensure_sessionpool_maxsize=30)天然 线程安全。预期单股财务取数从 ~30s 降至 ~5-10s。每个 block 独立 try/except, 失败追加 errors[],不影响其他 block。

Social sentiment 结构化数据(2026-05)

SocialSentimentService.get_social_context(ticker) 之前只返回格式化文本(供 LLM prompt 使用)。新增 get_social_raw_data(ticker) 返回 {"reddit": {...}, "x": {...}, "polymarket": {...}} 字典,供 stocklens/pipeline/stages/persistence_stage.py 写入 social_sentiment_snapshot 表。get_social_context 内部委托 get_social_raw_data 后再格式化,保证两条路径数据一致。

Akshare 死代码清理(2026-05)

_fetch_stock_data_sina / _fetch_stock_data_tx 之前各自包了一层 try/except Exception as e: raise e 的无意义 try/except,已删除。 异常现在自然向上传播到 manager 的 failover 层。

热点函数 lru_cache

  • tickbridge/codes/us_index.py:is_us_stock_code@lru_cache(maxsize=2048) —— 每只股票每次分析会被调用十几次,且 code 集合小,memoize 完美命中。

Refresh 链与新鲜度门(2026-05,FetcherChain)

背景:tickbridge-server 升级为独立服务后,refresh/tasks/{quote_snapshot,daily_bars,index_quote}.py 仍然硬编码 AkshareFetcher,没有利用 BaseFetcher 抽象的多源 fall-over 能力。一旦 akshare 的上游(东方财富 IPv6)抖动或被限流,整个 cohort cycle 阻塞,DB 一直 empty,前端全空。

tickbridge.refresh.fetcher_chain.FetcherChain

新增模块,把 _internal/sources/* 下的 fetcher 包装成有序链:

  • 构造FetcherChain.default_us_chain()TICKBRIDGE_SOURCE_PRIORITY 环境变量(默认 xueqiu,akshare,yfinance,openbb,twelvedata),按 fetcher 名实例化。
  • 懒加载:每个 _FetcherSlot 只在首次调用时实例化,openbb 没装也不阻塞守护进程启动。
  • per-call 超时TICKBRIDGE_FETCH_TIMEOUT(默认 15s),单源单次调用超时即跳到下一个源。
  • 冷却:单源连续失败 5 次自动 cooldown 60s,避免被限流的源在并发场景下持续浪费时间预算。
  • trade_date 戳印:xueqiu 等不带交易日的 quote 上,链层会戳上 cohort 期望的 trade_date,让 cohort validator 的漂移检查仍能工作。
  • 三个公开方法try_quote(code, expected_trade_date) / try_index_quote(code, expected_trade_date) / try_daily_bars(code, days, end_date)
  • 观测chain.stats() 返回 {source: {successes, failures, cooled_down}}

三个核心 task 全部接入

DailyBarsTask / QuoteSnapshotTask / IndexQuoteTask 现在共享 同一个 FetcherChain 实例(default_tasks() 里构造一次),让 cooldown / 统计跨任务累计;任意一个任务把 xueqiu 打到冷却,下一个任务自动跳过 xueqiu 走 akshare/yfinance。

老的硬编码 AkshareFetcher() / _get_us_realtime_quote_from_daily() / _get_us_index_realtime_quote_from_daily() / ak.stock_us_daily 全部替换成 chain.try_*()data_source 列从 "akshare" 改成 "chain"(具体走的源在日志里看 [chain] loaded fetcher: xxx + DEBUG 级 [chain] xxx.try_*(code) failed: ...)。

新鲜度门(should_skip_refresh

tickbridge/refresh/freshness.py 扩展三个新函数:

  • is_us_market_open(now=None):今天是 NYSE 交易日 + 当前 NY 时间在 09:30-16:00(含 30min 收盘缓冲)。
  • has_committed_cohort_for(trade_date):DB 是否已经有该日期的 committed cohort(直接 SQL SELECT 1 FROM tb_cohort WHERE status='committed' AND trade_date=?)。
  • should_skip_refresh(expected_date=None) 二元决策:
    • 市场开盘 → 永远刷新(盘中行情会变)
    • 市场闭盘 + DB 已有目标日 cohort → 跳过整个 cycle(不开新 tb_cohort 行),event bus 推 refresh_skipped
    • 市场闭盘 + DB 缺数据 → 正常刷新

实战效果:周末/节假日重启 tickbridge-server 时,看到的就是

[refresh] skipping cycle: market closed and DB already has committed cohort for trade_date=2026-05-22

而不是再触发一轮 6700-symbol 重抓。

跨股票 trade_date 一致性(cross-symbol date check)

orchestrator._verify_cross_symbol_dates(outcomes, expected)CohortValidator.validate 之后再扫一遍:每个 task 内按 code 取最大 row date(daily_bars 一只股 5 行历史 K 线,stock_quote_snapshot 一只股 1 行 snapshot),latest != expected 直接整轮 reject。

per-symbol "最大日期" 是关键 — 如果用「每行都校验」会把 daily_bars 的历史回填行(5/18/19/20/21/22 五行)误判为漂移。task 内部已经按"最后一行 date != cohort.trade_date 不入库"做了一道过滤;这里是 belt-and-braces 第二道,防止有 task 后续改逻辑时漏掉漂移分支。

配置开关

环境变量 默认值 含义
TICKBRIDGE_SOURCE_PRIORITY xueqiu,akshare,yfinance,openbb,twelvedata 链顺序(逗号分隔)
TICKBRIDGE_FETCH_TIMEOUT 15 单源单次调用超时(秒)
TICKBRIDGE_REFRESH_QUOTE_WORKERS 32 quote_snapshot 并发线程数
TICKBRIDGE_REFRESH_DAILY_BARS_WORKERS 32 daily_bars 并发线程数
TICKBRIDGE_MAX_SYMBOLS 10000 单 cycle 最多刷新的 symbol 数
TICKBRIDGE_SYMBOLS empty 显式 symbol 列表(开发/烟测用,覆盖 universe)
TICKBRIDGE_BOOT_REFRESH true 启动时立刻跑一轮(叠加新鲜度门)
TICKBRIDGE_REFRESH_PERIOD_SECONDS 3600 周期刷新间隔

_default_symbol_supplier DI 重构

_default_symbol_supplier 不再直接 import stocklens.*。改为通过 tickbridge.config.settings.register_symbol_supplier(fn) 注入 universe 构建回调。解析顺序:

  1. TICKBRIDGE_SYMBOLS env var(显式覆盖,开发/烟测用)
  2. 已注册的 symbol supplier 回调(host 应用启动时注入)
  3. 空列表(daemon 仍运行 index-only 任务)

TICKBRIDGE_MAX_SYMBOLS 对回调返回值做截断。Host 应用(如 StockLens)在启动时调用 register_symbol_supplier(build_universe) 注入 watchlist + ETF + catalog 逻辑。

Refresh Tasks 完整实现(2026-05)

所有 10 个 stub task 已替换为真实实现。每个 task 遵循两阶段模式: fetch_to_memory(cohort, symbols)persist(session, cohort, outcome)

Task 清单

Task 类 kind 目标表 数据源 并发
DailyBarsTask stock_daily tb_stock_daily FetcherChain (multi-source) 32 workers
QuoteSnapshotTask quote_snapshot tb_stock_quote_snapshot FetcherChain 32 workers
IndexQuoteTask index_quote tb_stock_quote_snapshot FetcherChain 4 indices
MacroTask macro tb_macro_snapshot akshare + yfinance sequential
ChipTask chip tb_chip_distribution_history akshare (stock_cyq_em) 8 workers
FundamentalTask fundamental tb_fundamental_snapshot yfinance (Ticker.info) 16 workers
IntradayBarsTask intraday_bars tb_stock_bars (interval='60m') yfinance (history(interval='1h')) 16 workers
FinvizTask finviz tb_finviz_forward_snapshot finvizfinance 4 workers
SentimentTask sentiment tb_social_sentiment_snapshot sentiment API / yfinance news proxy 8 workers
NewsTask news tb_market_news_archive akshare (Jin10) + yfinance (SPY news) sequential
IVTask iv tb_iv_snapshot yfinance options chain 8 workers
AnalystTargetTask analyst_target tb_analyst_target_snapshot yfinance (Ticker.info) 16 workers
SharesOutstandingTask shares_outstanding tb_shares_outstanding_snapshot yfinance (Ticker.info) 16 workers
InstitutionalTask institutional tb_institutional_snapshot yfinance (holders + insider + analyst) 8 workers

文件列表

tickbridge/refresh/tasks/
├── __init__.py              汇总导出 16 个 task 类
├── base.py                  RefreshTask + TaskOutcome 基类
├── _stubs.py                (已清空,保留向后兼容)
├── daily_bars.py            DailyBarsTask
├── quote_snapshot.py        QuoteSnapshotTask
├── index_quote.py           IndexQuoteTask
├── macro.py                 MacroTask
├── chip.py                  ChipTask (A-share only)
├── fundamental.py           FundamentalTask
├── intraday_bars.py         IntradayBarsTask
├── finviz.py                FinvizTask
├── sentiment.py             SentimentTask
├── news.py                  NewsTask
├── iv.py                    IVTask
├── analyst_target.py        AnalystTargetTask
├── shares_outstanding.py    SharesOutstandingTask
└── institutional.py         InstitutionalTask

Cohort 一致性分级

所有新 task 的 kind 均注册在 tickbridge/refresh/cohort.py:KIND_TIER 中:

  • CORE (≥90% 通过率):stock_daily, stock_weekly, quote_snapshot
  • AUX (≥70% 通过率):所有其他 task(macro, chip, fundamental, intraday_bars, finviz, sentiment, news, iv, analyst_target, shares_outstanding, institutional, index_quote, market_env, pe_history)

AUX task 允许稀疏覆盖(冷门股票无 Finviz 页面、无期权链、无社交提及等属正常情况)。

设计要点

  1. ChipTask 仅适用于 A 股(6 位代码 0/3/6 开头),通过 applicable_symbols() 过滤。
  2. FundamentalTask 存储完整 JSON payload(不拆列),下游按需提取字段,无需 schema migration。
  3. InstitutionalTask 每只股票产出 1-3 行(13f / insider / analyst 三个 snapshot_type),succeeded 按 symbol 计数而非按行。
  4. IVTask 包含 max pain 计算(遍历所有行权价,找总损失最小的 strike)。
  5. NewsTask / MacroTask 不依赖 symbol universe(applicable_symbols() 返回空列表)。
  6. FinvizTask 并发数故意压低(4 workers)—— Finviz 限流激进。
  7. SentimentTask 支持外部 API(TICKBRIDGE_SENTIMENT_API_URL env var),fallback 到 yfinance news count 作为粗略信号。

注意事项(变更日志)

  • 2026-05 Refresh Tasks 全量实现tickbridge/refresh/tasks/{chip,fundamental,intraday_bars,finviz,sentiment,news,iv,analyst_target,shares_outstanding,institutional}.py — 10 个 stub 全部替换为真实上游调用。_stubs.py 清空,__init__.py 改为直接导入各模块。cohort.py:KIND_TIER 新增 intraday_bars 条目。
  • 2026-05 FetcherChain + 新鲜度门tickbridge/refresh/{fetcher_chain.py,freshness.py,orchestrator.py,tasks/{quote_snapshot,daily_bars,index_quote}.py} 见上节。
  • 2026-05 清理死变量
    • tickbridge/sources/xueqiu.py:_fetch_raw_data — 原 session = self._ensure_session() 后从未使用,内部 _hit() 闭包另起一次 self._ensure_session() 调用;删除外层死变量。
    • tickbridge/core/chip_mixin.py — 删除未使用的 config = get_dp_config() 及对应 import。
    • tickbridge/core/fundamental_mixin.py:get_fundamental_context — 删除未使用的 start_ts = time.time()(函数内部所有 duration 统计都通过 per-stage _consume_budget(ms) 完成,外层 start_ts 是历史遗留)。