Imported from IS-ME-YELLOW/intern-takeaway (
analyst-skill-versions/AGENTS.md). Install upstream withnpx skills add IS-ME-YELLOW/intern-takeaway --skill analyst-skill-versions. Copyright stays with the author.
Analyst Board — 多分析师 AI 协作选股
概述
从 analyst-skill-versions/ 下的 16 个蒸馏分析师 skill 中,各自独立选股 → 纯计票共识 → 沪深 300 Top 50。
架构
用户(CLI 或 UI 触发)→ 选时点 T
│
▼
Phase 0a: fetch_csi300.mjs → csi300_constituents.py --date T
→ board/data/csi300_<actual_trading_day>.json (300 只)
(非交易日自动回退到 ≤ T 的最近交易日,文件名和 as_of 用实际交易日)
│
▼
Phase 0b: fetch_macro.mjs → datapro (HTTP) + data_fetcher.py (DB SQL)
→ board/out/macro_fetched/<skill>_<T_tag>.json (16 个)
(双源实取截至 T 的宏观数据,查不到的字段填 0 降级,coverage_report 产出)
│
▼
Phase 1-4: run-board.ts
├─ ensureMacroData(): 检查 macro 文件存在 + _meta.T 匹配 → 缺则自动跑 fetch_macro
├─ findCSI300(): 扫 board/data 找 ≤ T 的最新 CSI300 文件
├─ 16 分析师 × 有界并发(4)
│ ├─ Turn1 (plain, 无 format): 路径指针 prompt → agent read SKILL.md + read views.md + bash screen.py --data <预取JSON>
│ └─ Turn2 (format=json_schema): 内联 300 候选清单 → agent 选 10 只 → structured_output
├─ 每 analyst 4 次重试(所有 error 全重试:content-filter/StructuredOutputError/transport error)
├─ checkpoint: 每完成一个分析师写 runs/<runId>.json
└─ 降级: screen.py 没跑仍保留(screenRan=false)、全失败跳过列 failures
│
▼
Phase 5: countVotes (纯计票) → Top 50 + categories 分布 + matrix → runs/<runId>.json + index.json + <runId>_matrix.json
│
▼
Phase 6: web/ (Next.js) → 看板: 历史列表 / 详情(分析师卡+热度图+Top50) / live 轮询
版本锁定机制(核心)
- skill 工具禁用(opencode.json
permission.skill:{"*":"deny"})→ agent 不能按 name 加载任何 skill。 - board 按路径直读
analyst-skill-versions/analyst-*/SKILL.md→ Turn1 prompt 放绝对路径 → agent 自己read该文件。 - 按路径读与 name 无关 → 别处同名 skill 无法影响。不靠 name 锁版本。
两段式(核心)
format: json_schema会抑制工具调用(agent 直奔 StructuredOutput、跳过 screen.py)。- 解法:Turn1 无 format(agent 跑满工作流:read SKILL.md + views + screen.py)→ Turn2 有 format(agent 从上下文选 10 只出结构化 JSON)。
防数据泄露(核心)
- 单次运行内(硬约束):
- datapro 查询关键词含时间范围"2023年1月至T" → 只返回 ≤ T 的数据点。
- DB SQL
WHERE date <= 'T'→ 只返回 ≤ T 的行。 - 代码过滤
month <= T_MONTH→ 丢弃 > T 的数据。 - screen.py 输入 JSON 的
as_of≤ T。 - assets/ 已清空 → 不引用任何预烘焙快照。
- 跨运行(ABC 隔离):
- A. T-tagged 文件名:macro 数据存为
<skill>_<T_tag>.json→ 不同 T 不覆盖。 - B. _meta.T 校验:run-board.ts 启动时校验 macro 文件的
_meta.T === CSI300_DATE→ 不匹配则报错。 - C. 自动前置:run-board.ts 的
ensureMacroData()检测缺文件 → 自动跑fetch_macro.mjs <T>。
- A. T-tagged 文件名:macro 数据存为
- 仍为软约束:views.md 含未来观点 → prompt 指令"只参考 ≤ T"(非确定性)。
0-fill 降级
- 查不到的字段填 0(不是 None)→ screen.py 不崩。
_meta.fields_zero_list标注哪些字段是填 0 的。- Turn1 prompt 告诉 agent 数据降级情况 → agent 在分析中标注信号可信度。
- 覆盖率约 25%(datapro ~17% + DB ~8%),金工 skill 基本全降级,固收/宏观/策略 skill 有部分真值。
运行
CLI 跑
cd /home/yongyi.xie/skills项目/analyst-skill-versions
# run-board.ts 会自动处理一切:
# 1. findCSI300() → 找 ≤ T 的最新 CSI300 文件(没有则需先跑 fetch_csi300.mjs)
# 2. ensureMacroData() → 检查 + 自动跑 fetch_macro.mjs <T>
# 3. 16 路分析师两段式 → 共识 → 持久化
CSI300_DATE=2026-06-01 ./board/node_modules/.bin/tsx board/src/run-board.ts
# 看结果
cat board/out/runs/index.json # 运行历史
cat board/out/runs/<runId>.json # 单次详情
cat board/out/macro_fetched/coverage_report_<T_tag>.json # 数据覆盖率
手动预取(可选,run-board 会自动做)
# CSI300 成分清单
node board/scripts/fetch_csi300.mjs 2026-06-01
# 宏观数据(双源:datapro + DB)
node board/scripts/fetch_macro.mjs 2026-06-01
UI 触发
cd /home/yongyi.xie/skills项目/analyst-skill-versions/web
npm run dev # http://localhost:3101
浏览器打开 → 时点输入框填 T → "触发新运行" → 自动 fetch CSI300 + spawn board(board 内部自动 fetch macro)→ 跳转 live 详情页。
看板
- 运行历史页
/:列表(runId/T/成功数/Top1)。 - 详情页
/runs/[runId]:概览栏 + 分析师卡网格(点击展开完整输出)+ 共识热度图(16×50) + Top 50 表(含 categories 分布)。
文件结构
analyst-skill-versions/
├── AGENTS.md ← 本文件
├── opencode.json ← analyst agent 配置(model=glm-5.2, skill 禁用, steps=50)
├── analyst-*/ ← 16 个 skill(SKILL.md + references/ + scripts/screen.py + assets/空)
├── data-fetch/ ← 数据获取 skill(csi300_constituents.py + data_fetcher.py)
├── board/
│ ├── README.md ← board 脚本说明
│ ├── src/
│ │ ├── run-board.ts ← 主跑脚本(16 路两段式 + 并发4 + 重试 + 共识 + 持久化 + ensureMacroData + findCSI300)
│ │ ├── config.ts ← 花名册(扫描 analyst-*/,提取 skillMdPath/skillDir/category)
│ │ ├── schema.ts ← outputSchema(必填含 category, 恰好10, weight=1)+ probeSchema
│ │ └── archive/ ← Phase 1 探针脚本(归档参考)
│ ├── scripts/
│ │ ├── fetch_csi300.mjs ← Phase 0a: 跑 csi300_constituents.py 取真 300 → board/data/
│ │ │ (非交易日回退到实际交易日,文件名和 as_of 用实际日期)
│ │ ├── fetch_macro.mjs ← Phase 0b: 双源实取宏观数据 → board/out/macro_fetched/
│ │ │ (datapro HTTP + data_fetcher.py DB SQL + 0-fill 降级 + coverage_report)
│ │ ├── verify_skills.mjs ← 16 skill name 校验 + screen.py 全量实测
│ │ └── enrich_run.mjs ← 对已有 run 补 categories/index/matrix
│ ├── data/
│ │ ├── field_db_mapping.json ← 字段→DB 映射表(国债收益率/回购利率/指数收盘价/成交额/商品价格)
│ │ └── csi300_<actual_day>.json ← 真 300 成分(按实际交易日命名)
│ └── out/
│ ├── runs/ ← 运行结果(<runId>.json + index.json + <runId>_matrix.json + <runId>_console.txt)
│ └── macro_fetched/ ← 预取宏观数据(<skill>_<T_tag>.json + coverage_report_<T_tag>.json)
├── web/ ← Next.js 看板前端
│ └── src/
│ ├── app/
│ │ ├── page.tsx ← 运行历史页(触发按钮 + 列表)
│ │ ├── runs/[runId]/page.tsx ← 详情页(4 区块 + 轮询 + 分析师卡可展开)
│ │ └── api/runs/ ← API route (GET index / GET run / GET matrix / POST trigger+auto-fetch)
│ └── types.ts ← Board 数据类型 + CATEGORY_ORDER/COLORS/LABELS
└── Plans/ ← 各阶段方案文件
关键决策与踩坑
- version-lock 不靠 name:skill 工具按 name 加载、别处同名无法区分 → 禁用 skill 工具 + 路径指针直读 versions 目录。
- format 抑制工具:
format: json_schema让 agent 跳过 screen.py → 两段式(Turn1 plain 工作流 / Turn2 format 选股)。 - content-filter 偶发:glm-5.2 阿里云审核偶发误杀 → 全 error 重试(StructuredOutputError 也重试,不 break)。
- model 必须显式 pin:全局默认
qwen3.6-plus失效 → opencode.jsonagent.analyst.model: glm-5.2。 - 端口 4321:board 用独立端口,不碰 4096(常驻 opencode server)。
- v2 SDK:
@opencode-ai/sdk/v2(v1 无 format 字段);参数扁平(sessionID/parts/format 均为顶层,无 path/body 包裹);结构化输出在info.structured(非文档写的structured_output)。 - server.close() 必须 finally:抛错后不关 server 留孤儿阻断重跑。
- 路径方案零 skill 编辑:env 提示 skill 目录绝对路径 + cd 指令;skill 内部相对路径在 cd 后成立。
- agent 不能自己查 datapro:22 次 MCP 查询 × ~12KB/次 = ~264KB context → bailian quota 超限 → answer 为空。解法:board 侧
fetch_macro.mjs预取(HTTP 直调 datapro,不走 agent)。 - session.prompt 有 ~5min HTTP 超时:Bun server 请求超时。同步
session.prompt跑超 5min 会返回空 error。长任务需改用promptAsync+ 轮询。 - 0-fill 不崩:screen.py 对 None 数组做算术会 TypeError 崩溃;填 0 不崩(0-0=0)。降级 meta 标注真假字段。
- CSI300 非交易日回退:
csi300_constituents.py自动回退到 ≤ T 的最近交易日,输出文件名用实际日期。fetch_csi300.mjs扫 TMP 目录找实际产出文件。board 的findCSI300()扫board/data/找 ≤ T 的最新。 - 跨运行 ABC 隔离:T-tagged 文件名 + _meta.T 校验 + ensureMacroData 自动前置 → 不同 T 的 macro 数据不交叉污染。