Skill: t-atlas/a-share-stock-dossier (version 1.0.0)

--- name: a-share-stock-dossier description: Analyze A-share stocks and portfolios with analyst-grade, evidence-first reports. Use when the user asks for 个股分析、持仓复盘、逻辑是否还在、行业龙头、盘前/盘后策略、情绪+技术综合判断, especially when they want deep web verification and full process transparency (检索过程摘要 + 证据逐条分析) via web_search/web_fetch/browser plus Eastmoney/Tencent quote data. --- # A-Share Stock Dossier ## Overview Produce professional analyst-style stock reports that are **process-transparent, evidence-bound, and directly executable**. Always split conclusions into two layers: - 产业逻辑（fundamental/industry logic） - 交易逻辑（price/flow/sentiment logic） Default output mode is **long-form process report** unless user explicitly asks for a short summary. ## Workflow ### Step 1) Fix scope and objective - Extract stock list, cost, position size, horizon (日内 / 次日 / 5-10日), and risk preference. - Confirm output mode: - 单票深挖 - 组合分层（A/B/C） - 盘前执行单 - If screenshot is provided, parse first; ask only missing fields. ### Step 2) Pull structured baseline before any narrative Run: ```bash python skills/a-share-stock-dossier/scripts/a_share_snapshot.py \ --codes 603618,002149,002506,002475,002729,601116,601096 \ --with-indices --with-kline --kline-days 60 --pretty ``` Lock objective facts first: - Price/pct/range/volume/turnover - 5d/10d/20d return - MA5/MA10/MA20/MA60 context - Index mood and breadth proxy Field reference: - `references/eastmoney-fields.md` ### Step 3) Run deep-search loop (before and during writing) Use: - `references/source-checklist.md` - `references/search-depth-protocol.md` Mandatory minimum evidence per stock: 1. Structured quote/kline data 2. One official source (CNINFO/exchange/company IR) 3. One mainstream finance source 4. One sector/leader verification source Tool order: - `web_search` discover - `web_fetch` extract正文 - `browser` for JS-heavy/anti-bot/paginated/incomplete extraction ### Step 4) Maintain retrieval log (hard requirement) During analysis, build a step log `S1..Sn`. Each step must include: - 检索目标（why this search） - 查询/页面（query/url） - 摘要（1-3条关键事实） - 来源等级（官方/主流媒体/社区） - 对判断影响（supports/weakens/conflicts） If a conclusion appears without supporting steps, do not keep it in final recommendations. ### Step 5) Write stock analysis in fixed order For each stock, output strictly in this order: 1. 公司业务与收入/应用场景定位 2. 当前市场叙事与叙事阶段（启动/强化/分歧/退潮） 3. 行业龙头与板块阶段（强度/轮动/分化） 4. 技术面（趋势、关键位、失效位） 5. 舆情与事件（利多/利空/争议） 6. 双逻辑判断 - 产业逻辑：在 / 弱化 / 失效 - 交易逻辑：在 / 弱化 / 失效 7. 明日三情景（强/中/弱）触发条件 -> 动作 8. 证据绑定（E1/E2/E3/E4）+ 置信度 Use template: - `references/report-template.md` ### Step 6) Continuous-search triggers during writing Pause and re-search immediately if: - only one source supports a key claim - key event is stale (>7 days) and no update is checked - price/volume behavior conflicts with narrative - wording becomes uncertain（可能/大概/据说） - sector leader list mismatches same-day board behavior ### Step 7) Conflict resolution and stop rule - Unify basis first (timestamp, adj/non-adj, intraday/close) - Priority: official > exchange data > mainstream media > community - If unresolved, keep explicit uncertainty notes Stop searching only when: 1. each core conclusion has >=2 sources and >=1 official/preferred source 2. recent two re-search rounds add no high-value facts 3. conflicts are either resolved or explicitly marked ### Step 8) Portfolio decision + self-correction After all stocks: - Rank A/B/C: - A: 产业逻辑与交易逻辑同向 - B: 产业逻辑在、交易逻辑弱 - C: 交易逻辑受损 - Give one-line portfolio action (cut/hold/wait + why) - Add self-correction: - 2-3 weak points in this round - how to recalibrate thresholds next round ## Output requirements (must follow) Default to detailed analyst-report style with this top-level structure: 1. 检索过程纪要（S1..Sn） 2. 市场底色（结构化数据） 3. 逐股深度分析（证据逐条绑定） 4. 组合分层与执行重点 5. 本轮不确定性与下轮修正计划 Never output only short conclusions unless user asks explicitly.

path: references/eastmoney-fields.md
content: # Eastmoney Core Fields (常用字段速查)

用于 `push2.eastmoney.com/api/qt/ulist.np/get`：

- `f12` 代码
- `f14` 名称
- `f2` 最新价
- `f3` 涨跌幅(%)
- `f4` 涨跌额
- `f17` 今开
- `f15` 最高
- `f16` 最低
- `f18` 昨收
- `f5` 成交量
- `f6` 成交额
- `f7` 振幅
- `f8` 换手率
- `f9` 市盈率(TTM)
- `f10` 量比

指数扩展字段：
- `f104` 上涨家数
- `f105` 下跌家数

备注：
- 该接口返回实时快照，盘中会快速变化。
- 复盘时固定时间戳，避免“同一报告内口径漂移”。


path: references/report-template.md
content: # Report Template（专业分析师长版）

## 0) 检索过程纪要（必须）
> 逐步展示“怎么查到结论”。每一步 3~5 行。

### S1
- 检索目标：
- 查询/页面：
- 摘要事实：
- 来源等级：官方/主流媒体/社区
- 对结论影响：支持 / 削弱 / 冲突

### S2
- 检索目标：
- 查询/页面：
- 摘要事实：
- 来源等级：
- 对结论影响：

> 按需继续 S3...Sn

---

## 1) 市场情绪底色（结构化数据先行）
- 指数：上证/深成/创业板/科创（可选）当日涨跌
- 宽度：上涨家数 vs 下跌家数
- 成交与风格：缩量/放量；题材主导 or 权重主导
- 一句话判断：普涨 / 分化 / 退潮 / 修复

---

## 2) 逐股深度分析（每只同结构）

### {股票名}（{代码}）
1. **公司业务定位**
   - 主营、核心产品、应用场景、公司在产业链中的位置
2. **当前市场叙事**
   - 主叙事是什么
   - 叙事阶段：启动/强化/分歧/退潮
3. **行业龙头与板块阶段**
   - 近期龙头 2~4 只
   - 板块 5~10 日强弱 + 是否轮动/分化
4. **技术面**
   - 收盘、5/10/20日表现、MA5/10/20/60
   - 关键位：压力位/支撑位/失效位
5. **舆情与事件面**
   - 利多：
   - 利空：
   - 争议点：
6. **双逻辑判断（核心）**
   - 产业逻辑：在/弱化/失效（附原因）
   - 交易逻辑：在/弱化/失效（附原因）
7. **明日三情景动作**
   - 强：触发条件 -> 动作
   - 中：触发条件 -> 动作
   - 弱：触发条件 -> 动作
8. **证据卡片（逐条）**
   - E1（行情数据）：链接/来源 + 摘要 + 对判断影响
   - E2（官方披露）：链接/来源 + 摘要 + 对判断影响
   - E3（主流媒体）：链接/来源 + 摘要 + 对判断影响
   - E4（板块验证）：链接/来源 + 摘要 + 对判断影响
9. **置信度**
   - 高 / 中 / 低（并解释）

---

## 3) 组合层建议（若有持仓）
- A组：逻辑+情绪同向（可留/可加）
- B组：逻辑在但交易弱（控仓观察）
- C组：交易逻辑受损（反抽处理）
- 风险集中度：说明 2~3 个主要回撤来源

---

## 4) 不确定性与自我修正（必须）
- 本轮最不确定的 2~3 个点
- 可能导致错判的条件
- 下一轮补证据与阈值修正计划


path: references/search-depth-protocol.md
content: # Search Depth Protocol（深度搜索协议）

## 目标
把流程从“先写结论再补证据”改为“证据先行、写作中持续检索、冲突必复核”。
最终输出要能让读者看到：**你搜了什么、为什么搜、搜到什么、如何影响结论**。

## 一、三层搜索循环（每只股票都跑）

### Layer A：广域扫描（建立上下文）
回答 4 个问题：
1. 今天市场处在哪个情绪相位（普涨/分化/修复/退潮）？
2. 该股所属板块近 5~10 日强弱如何？
3. 板块近期龙头是谁，是否切换？
4. 该股近 7~15 日关键事件是什么（公告/减持/订单/异动）？

最低检索建议：
- 指数/市场复盘
- 板块走势/龙头
- 个股公告/事件
- 个股资金与异动

### Layer B：假设验证（围绕结论反查）
对每个核心结论做反向检索：
- 示例结论："交易逻辑弱化"
- 反查问题：是否有新催化/资金回流/公告澄清可推翻该结论？

要求：
- 每个核心结论 >=2 个独立来源
- 至少 1 个为官方或准官方披露

### Layer C：冲突复核（分歧必须处理）
当来源冲突：
1. 统一口径（时间点、前复权/不复权、盘中/收盘）
2. 按优先级裁决：官方 > 交易所数据 > 主流媒体 > 社区
3. 仍有冲突时写明“分歧点+不确定性”

## 二、写作中“动态补搜”触发器（硬规则）
出现以下情况必须立刻补搜：
- 结论只有单一来源支撑
- 核心事件证据超过 7 天且无更新
- 数据与叙事明显冲突（如放量下跌但舆情过热）
- 出现“可能/大概/据说”等低确定表达
- 板块龙头名单与当日盘面不一致

## 三、查询扩展方法（让搜索更深）
每个主题至少覆盖 3 组关键词：
1. `代码/简称 + 公告/异动/减持/中标`
2. `板块关键词 + 龙头 + 资金/轮动`
3. `行业关键词 + 政策/价格/需求`

时间窗建议：
- 当日（盘中/收盘）
- 近 5 日（短线节奏）
- 近 20 日（叙事是否衰减）

## 四、检索过程摘要（输出硬要求）
最终报告必须先给“检索过程纪要 S1..Sn”。
每一步至少包含：
- 检索目标
- 查询/链接
- 关键事实摘要（1~3条）
- 来源等级
- 对判断影响（支持/削弱/冲突）

## 五、证据绑定规则
每只股票输出：
- 结论（产业逻辑 / 交易逻辑）
- 证据ID（E1/E2/E3/E4）
- 来源链接与日期
- 置信度（高/中/低）

没有证据ID的判断，不进入最终交易建议。

## 六、停止条件（防过搜）
满足以下条件可停止该股搜索：
1. 核心结论满足“2源以上 + 1官方源”
2. 最近两轮补搜无新增高质量信息
3. 冲突项已被解释或标注不确定性


path: references/source-checklist.md
content: # Source Checklist (网络验证清单)

## 0) 目标
把“观点”变成“可验证证据链”。每只股票至少完成一套最小验证，再输出结论。
同时保留“检索过程摘要（S1..Sn）”，让读者能看到每一步检索如何影响判断。

## 1) 最小验证配额（每只个股）

1. **结构化行情数据 ×1**
   - 用 `scripts/a_share_snapshot.py` 拉取：收盘价、涨跌幅、近 5/10/20 日回报、MA、区间高低。
2. **官方信息源 ×1**
   - 优先：巨潮/CNINFO、交易所公告、公司官网投资者关系页面。
3. **主流财经媒体 ×1**
   - 优先：上海证券报、证券时报、财联社、新浪财经（注意“AI自动稿”标识）。
4. **板块/龙头验证 ×1**
   - 至少验证一次“行业近期龙头是谁、板块是普涨还是分化”。

> 建议目标：每只股票 4~6 个有效来源。

## 2) 数据源优先级

### A. 结构化行情（优先级最高）
- 东方财富 push2（实时快照）
- 腾讯 fqkline（日K、前复权）

### B. 官方披露（优先级高）
- 巨潮资讯（公告 PDF/网页）
- 上交所/深交所披露

### C. 财经网站（优先级中）
- 上海证券报（paper.cnstock）
- 证券时报
- 新浪财经

### D. 社区/情绪（优先级低）
- 股吧/雪球/复盘网站仅可作“情绪样本”，不能单独作为事实依据。

## 3) browser 与 web_fetch 的使用规则

1. **先用 web_fetch**
   - 适合 HTML 新闻正文、公告转载页、简讯。
2. **web_fetch 抽取失败或内容不全时，用 browser**
   - 场景：JS 动态渲染、反爬、分页列表、交互图表。
3. **PDF 文档优先找“同内容网页镜像”**
   - 如 cnstock/媒体转录版。若只有 PDF，提取失败时明确标注“PDF解析受限”。

## 4) 证据质量分级

- **高**：官方公告 + 行情数据一致。
- **中**：主流媒体多源一致，但缺官方原文。
- **低**：单一来源或社区传闻。

输出结论时给出 `置信度：高/中/低`。

## 5) 冲突处理

当来源冲突时，按顺序处理：
1. 官方披露 > 交易所数据 > 主流媒体 > 社区。
2. 同一数据口径（时间点、前复权/不复权）先统一再比较。
3. 无法统一时，保留分歧并写明“可能原因”。

## 6) 建议的每股证据记录格式

- `事实1（行情）`：xxx（来源：结构化数据）
- `事实2（公告）`：xxx（来源：URL）
- `事实3（媒体）`：xxx（来源：URL）
- `事实4（板块）`：xxx（来源：URL）
- `结论`：产业逻辑/交易逻辑是否仍在（置信度）


path: scripts/a_share_snapshot.py
content: #!/usr/bin/env python3
"""
A-share market snapshot helper.

Fetch:
1) Real-time quote snapshot from Eastmoney push2 API
2) Recent daily kline from Tencent fqkline API
3) Optional index/breadth baseline

No third-party dependencies (urllib only).
Compatible with Python 3.6+.
"""

import argparse
import datetime as dt
import json
import re
import statistics
import urllib.parse
import urllib.request


EM_QUOTE_API = "https://push2.eastmoney.com/api/qt/ulist.np/get"
TX_KLINE_API = "https://web.ifzq.gtimg.cn/appstock/app/fqkline/get"


def _http_get_json(url, timeout=15):
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8", "ignore"))


def normalize_code(code):
    c = code.strip().upper().replace(" ", "")

    # Accept SH/SZ prefixed forms
    if c.startswith("SH") and len(c) == 8 and c[2:].isdigit():
        c = c[2:]
    elif c.startswith("SZ") and len(c) == 8 and c[2:].isdigit():
        c = c[2:]

    # Accept code.exchange forms
    if "." in c:
        left, right = c.split(".", 1)
        if left.isdigit() and right in {"SH", "SS", "SZ"}:
            c = left

    if not re.fullmatch(r"\d{6}", c):
        raise ValueError("Unsupported code format: {}".format(code))
    return c


def code_to_secid(code6):
    # Eastmoney secid: 1 = SH, 0 = SZ/BJ (basic support for SH/SZ)
    if code6.startswith("6"):
        return "1." + code6
    return "0." + code6


def code_to_tencent_symbol(code6):
    if code6.startswith("6"):
        return "sh" + code6
    return "sz" + code6


def fetch_quotes(codes):
    secids = [code_to_secid(c) for c in codes]
    fields = "f12,f14,f2,f3,f4,f5,f6,f7,f8,f9,f10,f15,f16,f17,f18"
    url = EM_QUOTE_API + "?" + urllib.parse.urlencode(
        {
            "fltt": "2",
            "invt": "2",
            "fields": fields,
            "secids": ",".join(secids),
        }
    )
    obj = _http_get_json(url)
    diff = obj.get("data", {}).get("diff", [])

    out = []
    for it in diff:
        out.append(
            {
                "code": str(it.get("f12", "")),
                "name": it.get("f14"),
                "last": it.get("f2"),
                "pct": it.get("f3"),
                "chg": it.get("f4"),
                "open": it.get("f17"),
                "high": it.get("f15"),
                "low": it.get("f16"),
                "prev_close": it.get("f18"),
                "volume": it.get("f5"),
                "amount": it.get("f6"),
                "amplitude": it.get("f7"),
                "turnover": it.get("f8"),
                "pe_ttm": it.get("f9"),
                "volume_ratio": it.get("f10"),
            }
        )

    order = dict((c, i) for i, c in enumerate(codes))
    out.sort(key=lambda x: order.get(x.get("code"), 9999))
    return out


def fetch_index_baseline():
    # SH/SZ/ChiNext + up/down breadth fields where available
    secids = ["1.000001", "0.399001", "0.399006"]
    fields = "f12,f14,f2,f3,f4,f15,f16,f17,f18,f104,f105,f6,f7"
    url = EM_QUOTE_API + "?" + urllib.parse.urlencode(
        {
            "fltt": "2",
            "invt": "2",
            "fields": fields,
            "secids": ",".join(secids),
        }
    )
    obj = _http_get_json(url)
    diff = obj.get("data", {}).get("diff", [])
    out = []
    for it in diff:
        out.append(
            {
                "code": str(it.get("f12", "")),
                "name": it.get("f14"),
                "last": it.get("f2"),
                "pct": it.get("f3"),
                "chg": it.get("f4"),
                "high": it.get("f15"),
                "low": it.get("f16"),
                "open": it.get("f17"),
                "prev_close": it.get("f18"),
                "amount": it.get("f6"),
                "amplitude": it.get("f7"),
                "up_count": it.get("f104"),
                "down_count": it.get("f105"),
            }
        )
    return out


def _ma(vals, n):
    if len(vals) < n:
        return None
    return round(statistics.mean(vals[-n:]), 4)


def _ret(vals, n):
    if len(vals) <= n:
        return None
    base = vals[-(n + 1)]
    if not base:
        return None
    return round((vals[-1] / base - 1) * 100, 4)


def fetch_kline(code6, days=60):
    symbol = code_to_tencent_symbol(code6)
    url = TX_KLINE_API + "?" + urllib.parse.urlencode({"param": "{},day,,,{},qfq".format(symbol, days)})
    obj = _http_get_json(url)
    data = obj.get("data", {}).get(symbol, {})
    rows = data.get("qfqday") or data.get("day") or []

    klines = []
    closes = []
    for r in rows:
        # [date, open, close, high, low, vol]
        try:
            rec = {
                "date": r[0],
                "open": float(r[1]),
                "close": float(r[2]),
                "high": float(r[3]),
                "low": float(r[4]),
                "volume": float(r[5]),
            }
        except Exception:
            continue
        klines.append(rec)
        closes.append(rec["close"])

    metrics = {
        "latest_date": klines[-1]["date"] if klines else None,
        "close": closes[-1] if closes else None,
        "ret_5d_pct": _ret(closes, 5),
        "ret_10d_pct": _ret(closes, 10),
        "ret_20d_pct": _ret(closes, 20),
        "ma5": _ma(closes, 5),
        "ma10": _ma(closes, 10),
        "ma20": _ma(closes, 20),
        "high_10d": max(closes[-10:]) if len(closes) >= 10 else (max(closes) if closes else None),
        "low_10d": min(closes[-10:]) if len(closes) >= 10 else (min(closes) if closes else None),
    }
    return {"metrics": metrics, "klines": klines}


def parse_codes(raw):
    parts = [x for x in re.split(r"[,，\s]+", raw.strip()) if x]
    out = []
    for p in parts:
        c = normalize_code(p)
        if c not in out:
            out.append(c)
    return out


def main():
    parser = argparse.ArgumentParser(description="A-share snapshot helper")
    parser.add_argument("--codes", required=True, help="Comma/space separated 6-digit stock codes")
    parser.add_argument("--with-kline", action="store_true", help="Include daily kline and MA/return metrics")
    parser.add_argument("--kline-days", type=int, default=60, help="Kline lookback days (default: 60)")
    parser.add_argument("--with-indices", action="store_true", help="Include SH/SZ/ChiNext baseline")
    parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON")
    args = parser.parse_args()

    codes = parse_codes(args.codes)

    result = {
        "timestamp": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "codes": codes,
        "quotes": fetch_quotes(codes),
    }

    if args.with_indices:
        result["indices"] = fetch_index_baseline()

    if args.with_kline:
        kline = {}
        for c in codes:
            try:
                kline[c] = fetch_kline(c, days=args.kline_days)
            except Exception as e:
                kline[c] = {"error": str(e)}
        result["kline"] = kline

    print(json.dumps(result, ensure_ascii=False, indent=2 if args.pretty else None))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)