Skill: modestyrichards/modesty-a-stock-trading-assistant (version 1.0.0)

--- name: a-stock-trading-assistant version: "1.0.0" description: A股股票智能交易助手，专服务中国大陆股票市场。当用户询问A股股票行情、个股分析、大盘情绪、热点板块、交易策略、价格预警、涨跌幅、成交量、技术面或基本面分析时触发。只处理沪深A股（代码以6/00/30/68开头），自动忽略港股和美股。所有数据实时从东方财富、新浪财经、同花顺、雪球抓取。Use when: user asks about Chinese A-share stocks, 股票行情, 个股分析, 大盘分析, 板块热点, 买卖点, 止盈止损, 仓位建议, or any A-share trading-related query. --- # A股股票智能交易助手 ## 角色定位 你是专业A股交易助手，只服务中国大陆A股市场（沪深两市）。数据全部实时联网获取，数据源优先级：东方财富 → 新浪财经 → 同花顺 → 雪球。 ## 股票代码识别规则 | 前缀 | 市场 | 示例 | |------|------|------| | 60xxxx | 上交所主板 | 600519 贵州茅台 | | 00xxxx | 深交所主板 | 000001 平安银行 | | 30xxxx | 创业板 | 300750 宁德时代 | | 68xxxx | 科创板 | 688981 中芯国际 | - 自动忽略港股（.HK）、美股（NASDAQ/NYSE）及其他境外市场 - 用户输入不带前缀时，根据数字范围自动判断市场 ## 数据获取方式 优先用 `scripts/fetch_stock.py` 脚本获取实时数据。如脚本执行失败，改用 `web_fetch` 直接访问数据源。 详细 API 端点见 `references/data-sources.md`。 ### 快速调用脚本 ```bash # 查询单只股票实时行情 python3 /app/skills/a-stock-trading-assistant/scripts/fetch_stock.py --code 600519 # 查询大盘指数 python3 /app/skills/a-stock-trading-assistant/scripts/fetch_stock.py --index # 查询热点板块 python3 /app/skills/a-stock-trading-assistant/scripts/fetch_stock.py --hot-sectors ``` ## 六大核心能力工作流 ### 1. 实时行情查询 1. 运行 `fetch_stock.py --code <代码>` 获取实时数据 2. 展示：当前价、涨跌幅、涨跌额、成交量、成交额、换手率、振幅、52周高/低 3. 附上分时走势摘要（涨跌节奏描述） ### 2. 个股综合分析 先获取实时行情，再分析： - **技术面**：均线系统（MA5/10/20/60）、趋势判断、支撑位/压力位、量价结构、MACD/KDJ信号 - **基本面**：PE/PB估值、近期业绩、行业地位、主要风险点 - 技术面与基本面结合，给出综合判断（看多/看空/中性） 详细分析方法见 `references/analysis.md`。 ### 3. 大盘情绪与风险判断 获取上证指数、深证成指、创业板指实时数据，分析： - 大盘强弱（趋势、量能、板块轮动） - 市场情绪指数（赚钱效应、涨跌比） - 风险等级（低/中/高）及应对建议 ### 4. 热点板块与龙头股 1. 运行 `fetch_stock.py --hot-sectors` 获取涨幅榜板块 2. 识别：主线板块（连续性强）、情绪板块（短期热点） 3. 每个热点板块列出核心龙头股（涨停、强势领涨） ### 5. 交易策略与建议 基于用户的持仓/意向股，给出： - **短线**（1-5天）：催化剂、入场区间、止损位、止盈位 - **中线**（1-3月）：趋势判断、分批建仓节奏、仓位比例 - 始终标注风险提示 格式模板： ``` 【操作建议】XX股（XXXXXX） 方向：做多/观望/回避 入场区间：XX.XX - XX.XX 元 止损位：XX.XX 元（跌破离场） 止盈位：XX.XX 元（分批减仓） 仓位：XX%（轻/中/重仓） 逻辑：[核心理由2-3条] 风险：[主要风险1-2条] ``` ### 6. 价格预警监控 当用户设置预警时： - 记录目标价、预警条件（突破/跌破/放量）到 `references/watchlist.md` - 建议用户配合券商App实时推送，本工具做辅助分析 - 在后续对话中主动核对预警状态 ## 输出规范 - 数据必须标注来源和获取时间（精确到分钟） - 所有价格建议必须附风险提示 - 避免绝对化表述（"必涨"/"稳赚"），用概率/可能性描述 - 数字精确到小数点后2位，成交额以亿元为单位 - 大盘/个股分析结构清晰，使用简洁表格或分项列出

path: references/analysis.md
content: # 个股分析方法参考

## 技术面分析框架

### 均线系统判断

| 形态 | 条件 | 含义 |
|------|------|------|
| 多头排列 | MA5>MA10>MA20>MA60 | 上升趋势强，可做多 |
| 空头排列 | MA5<MA10<MA20<MA60 | 下跌趋势中，回避 |
| 均线粘合 | 各均线收敛 | 方向待定，等待突破 |
| 金叉 | MA5上穿MA10/MA20 | 短期买入信号 |
| 死叉 | MA5下穿MA10/MA20 | 短期卖出信号 |

### 量价结构分析

- **放量上涨**：主力资金入场，趋势确认，可跟进
- **缩量上涨**：小心，动能不足，谨慎追高
- **放量下跌**：恐慌性出逃或主力砸盘，注意风险
- **缩量下跌**：调整中，无大资金出逃，可等底部
- **天量天价**：极端放量后往往是阶段顶部，注意减仓
- **地量地价**：成交极度萎缩后往往是底部信号

### 支撑位 / 压力位识别

1. **整数关口**：100/200/300元等心理价位
2. **前期高点/低点**：突破前高=突破压力；跌破前低=下一支撑
3. **均线支撑**：MA20（月线）、MA60（季线）是重要支撑
4. **成交密集区**：历史上大量换手的价格区间，构成强支撑/压力

### MACD 信号

- **金叉+零轴上方**：强烈买入信号
- **死叉+零轴下方**：强烈卖出信号
- **顶/底背离**：价格创新高但MACD不创新高 = 顶背离（卖）；价格创新低但MACD不创新低 = 底背离（买）
- **红柱/绿柱缩短**：动能减弱，可能转向

### KDJ 信号

- KDJ>80：超买区，注意回调
- KDJ<20：超卖区，反弹机会
- K线上穿D线（金叉）：买入
- K线下穿D线（死叉）：卖出

---

## 基本面分析框架

### 估值判断

| 指标 | 低估 | 合理 | 高估 |
|------|------|------|------|
| PE（市盈率） | <行业均值50% | 接近行业均值 | >行业均值2倍 |
| PB（市净率） | <1 | 1-3 | >5 |

- 消费/医药类：参考PE估值
- 银行/地产类：参考PB估值
- 成长类（科技/新能源）：参考PEG（<1为合理）

### 业绩评估要点

- 近4个季度营收/净利增速趋势
- 毛利率变化（毛利率提升=竞争力增强）
- 净利率水平（与同行比较）
- 现金流情况（经营活动现金流>净利润=高质量利润）

### 行业地位

- 市占率排名（行业前3更优）
- 护城河类型：品牌/技术/成本/网络效应
- 政策支持方向（新能源/半导体/AI等优先）

---

## 大盘分析框架

### 市场情绪量化

| 指标 | 计算方式 | 情绪判断 |
|------|---------|---------|
| 赚钱效应 | 上涨股/总股数 | >60%强，<40%弱 |
| 涨停数量 | 当日涨停家数 | >100家热，<30家冷 |
| 量能比 | 今日量/5日均量 | >1.2放量，<0.8缩量 |

### 风险等级定义

| 级别 | 特征 | 建议仓位 |
|------|------|---------|
| 低风险 | 大盘趋势向上，量能充裕，涨多跌少 | 7-9成仓 |
| 中风险 | 方向不明，震荡整理 | 3-6成仓 |
| 高风险 | 趋势向下，缩量或恐慌放量 | 0-2成仓 |

---

## 热点板块判断标准

### 主线板块（可重点跟踪）
- 连续3天以上保持热度
- 有政策/事件催化
- 龙头股有持续涨停

### 情绪板块（短线机会）
- 单日暴热，次日需观察持续性
- 题材性质（概念股）
- 跟风盘多，需快进快出

### 龙头股识别
- 板块内首板/连板的核心票
- 换手率高但不崩盘
- 量价配合良好


path: references/data-sources.md
content: # 数据源 API 参考

## 1. 新浪财经（最稳定，优先使用）

### 实时行情

```
# 单股（沪市加sh，深市加sz）
GET http://hq.sinajs.cn/list=sh600519
GET http://hq.sinajs.cn/list=sz000001

# 多股同时查询
GET http://hq.sinajs.cn/list=sh600519,sz000001,sz300750
```

返回格式（逗号分隔的字符串）：
```
var hq_str_sh600519="贵州茅台,1788.00,1785.00,1800.00,1810.00,1780.00,1799.00,1800.00,3456789,6234567890.00,100,1799.00,...,2024-01-15,15:00:00,00";
```
字段顺序：股票名,昨收,今开,当前价,最高,最低,买一价,卖一价,成交量(手),成交额,买一量,买一价,...,日期,时间

### 大盘指数

```
GET http://hq.sinajs.cn/list=s_sh000001,s_sz399001,s_sz399006
# 上证指数,深证成指,创业板指
```

---

## 2. 东方财富（数据全面，适合深度查询）

### 实时行情

```
GET http://push2.eastmoney.com/api/qt/stock/get?secid={market}.{code}&fields=f43,f44,f45,f46,f47,f48,f57,f58,f60,f107,f169,f170,f171,f530
```

market: 1=沪市, 0=深市

关键字段：
- f43: 最新价（×0.01）
- f44: 最高价
- f45: 最低价
- f46: 今开
- f47: 成交量（手）
- f48: 成交额（元）
- f57: 股票代码
- f58: 股票名称
- f60: 昨收
- f170: 涨跌幅（%×100）
- f169: 涨跌额
- f171: 换手率（%×100）

### 涨幅榜/板块榜

```
# A股涨幅榜（前50）
GET http://push2.eastmoney.com/api/qt/clist/get?pn=1&pz=50&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&fid=f3&fs=m:0+t:6,m:0+t:13,m:0+t:80,m:1+t:2,m:1+t:23&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18

# 板块涨跌榜（概念/行业）
GET http://push2.eastmoney.com/api/qt/clist/get?pn=1&pz=20&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&fid=f3&fs=m:90+t:2+f:!50&fields=f1,f2,f3,f4,f5,f6,f7,f8,f12,f14,f20,f21
```

### 个股分时数据

```
GET http://push2.eastmoney.com/api/qt/stock/trends2/get?secid={market}.{code}&fields1=f1,f2,f3,f4,f5&fields2=f51,f52,f53,f54,f55,f56,f57,f58&iscr=0&iscca=0
```

---

## 3. 同花顺

### 实时行情

```
GET https://d.10jqka.com.cn/v4/time/hs_{code}/today.js
# code: 直接用6位数字，如 600519
```

注意：请求需要带 Referer: https://www.10jqka.com.cn

---

## 4. 雪球

### 实时行情

```
GET https://stock.xueqiu.com/v5/stock/quote.json?symbol={symbol}&extend=detail
# symbol: SH600519 / SZ000001 / SZ300750
```

注意：需要先访问 https://xueqiu.com 获取 cookie（session），再请求数据接口。脚本中已处理。

---

## 市场代码对照

| 代码前缀 | 新浪前缀 | 东财market | 雪球前缀 |
|---------|---------|-----------|---------|
| 600xxx / 601xxx / 603xxx / 605xxx / 688xxx | sh | 1 | SH |
| 000xxx / 001xxx / 002xxx / 003xxx / 300xxx / 301xxx | sz | 0 | SZ |

## 大盘指数代码

| 指数 | 新浪 | 东财secid |
|------|------|----------|
| 上证指数 | sh000001 | 1.000001 |
| 深证成指 | sz399001 | 0.399001 |
| 创业板指 | sz399006 | 0.399006 |
| 科创50 | sh000688 | 1.000688 |
| 北证50 | bj899050 | — |


path: scripts/fetch_stock.py
content: #!/usr/bin/env python3
"""
A股实时行情抓取脚本
数据源：新浪财经（主）、东方财富（备）
用法：
  python3 fetch_stock.py --code 600519
  python3 fetch_stock.py --code 000001 sz000001 300750
  python3 fetch_stock.py --index
  python3 fetch_stock.py --hot-sectors
"""

import argparse
import json
import re
import sys
import urllib.request
import urllib.error
from datetime import datetime


HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Referer": "https://finance.sina.com.cn",
    "Accept-Language": "zh-CN,zh;q=0.9",
}


def get_market_prefix(code: str) -> str:
    """根据股票代码判断市场前缀"""
    code = code.strip().upper()
    if code.startswith("SH") or code.startswith("SZ"):
        return code[:2].lower(), code[2:]
    code = re.sub(r"[^0-9]", "", code)
    if code.startswith(("60", "68", "51", "58", "11")):
        return "sh", code
    elif code.startswith(("00", "30", "15", "12", "16", "13")):
        return "sz", code
    return "sh", code  # default


def fetch_url(url: str, extra_headers: dict = None) -> str:
    req = urllib.request.Request(url, headers=HEADERS)
    if extra_headers:
        for k, v in extra_headers.items():
            req.add_header(k, v)
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            charset = "gbk" if "sina" in url or "sinajs" in url else "utf-8"
            return resp.read().decode(charset, errors="replace")
    except Exception as e:
        return ""


def parse_sina_stock(raw: str, symbol: str) -> dict:
    """解析新浪财经股票数据"""
    match = re.search(r'"([^"]*)"', raw)
    if not match:
        return {}
    parts = match.group(1).split(",")
    if len(parts) < 32:
        return {}
    try:
        name = parts[0]
        prev_close = float(parts[2]) if parts[2] else 0
        open_price = float(parts[1]) if parts[1] else 0
        current = float(parts[3]) if parts[3] else 0
        high = float(parts[4]) if parts[4] else 0
        low = float(parts[5]) if parts[5] else 0
        volume = int(parts[8]) if parts[8] else 0  # 手
        amount = float(parts[9]) if parts[9] else 0  # 元
        date_str = parts[30] if len(parts) > 30 else ""
        time_str = parts[31] if len(parts) > 31 else ""

        change = current - prev_close
        change_pct = (change / prev_close * 100) if prev_close else 0
        turnover_approx = volume / 1000  # 粗略换手（无流通股数据）

        return {
            "symbol": symbol,
            "name": name,
            "current": round(current, 2),
            "change": round(change, 2),
            "change_pct": round(change_pct, 2),
            "open": round(open_price, 2),
            "high": round(high, 2),
            "low": round(low, 2),
            "prev_close": round(prev_close, 2),
            "volume_lot": volume,  # 手
            "amount_yuan": round(amount, 2),
            "amount_yi": round(amount / 1e8, 2),
            "date": date_str,
            "time": time_str,
            "source": "新浪财经",
            "fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }
    except (ValueError, IndexError):
        return {}


def fetch_single_stock(code: str) -> dict:
    """抓取单只股票行情"""
    prefix, clean_code = get_market_prefix(code)
    symbol = f"{prefix}{clean_code}"
    url = f"http://hq.sinajs.cn/list={symbol}"
    raw = fetch_url(url)
    if raw:
        data = parse_sina_stock(raw, symbol)
        if data:
            return data

    # 备用：东方财富
    market = 1 if prefix == "sh" else 0
    url2 = (
        f"http://push2.eastmoney.com/api/qt/stock/get"
        f"?secid={market}.{clean_code}"
        f"&fields=f43,f44,f45,f46,f47,f48,f57,f58,f60,f107,f169,f170,f171"
    )
    raw2 = fetch_url(url2, {"Referer": "https://www.eastmoney.com"})
    if raw2:
        try:
            obj = json.loads(raw2)
            d = obj.get("data", {}) or {}
            if d.get("f43"):
                prev = d["f60"] / 100
                curr = d["f43"] / 100
                chg = d["f169"] / 100
                chg_pct = d["f170"] / 100
                return {
                    "symbol": symbol,
                    "name": d.get("f58", ""),
                    "current": round(curr, 2),
                    "change": round(chg, 2),
                    "change_pct": round(chg_pct, 2),
                    "open": round(d["f46"] / 100, 2),
                    "high": round(d["f44"] / 100, 2),
                    "low": round(d["f45"] / 100, 2),
                    "prev_close": round(prev, 2),
                    "volume_lot": d.get("f47", 0),
                    "amount_yuan": d.get("f48", 0),
                    "amount_yi": round(d.get("f48", 0) / 1e8, 2),
                    "turnover_pct": round(d.get("f171", 0) / 100, 2),
                    "source": "东方财富",
                    "fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                }
        except Exception:
            pass

    return {"error": f"无法获取 {code} 的行情数据", "symbol": symbol}


def fetch_index() -> list:
    """抓取主要大盘指数"""
    symbols = "s_sh000001,s_sz399001,s_sz399006,s_sh000688"
    names_map = {
        "s_sh000001": "上证指数",
        "s_sz399001": "深证成指",
        "s_sz399006": "创业板指",
        "s_sh000688": "科创50",
    }
    url = f"http://hq.sinajs.cn/list={symbols}"
    raw = fetch_url(url)
    results = []
    if raw:
        for sym, name in names_map.items():
            pattern = rf'hq_str_{re.escape(sym)}="([^"]*)"'
            m = re.search(pattern, raw)
            if m:
                parts = m.group(1).split(",")
                if len(parts) >= 5:
                    try:
                        results.append({
                            "name": parts[0] or name,
                            "current": float(parts[1]),
                            "change": float(parts[2]),
                            "change_pct": float(parts[3]),
                            "volume_yi_lot": round(float(parts[4]) / 1e8, 2),
                            "amount_yi": round(float(parts[5]) / 1e8, 2) if len(parts) > 5 else 0,
                            "fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                            "source": "新浪财经",
                        })
                    except (ValueError, IndexError):
                        pass
    return results


def fetch_hot_sectors() -> list:
    """抓取热点板块（东方财富概念板块涨幅榜）"""
    url = (
        "http://push2.eastmoney.com/api/qt/clist/get"
        "?pn=1&pz=20&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281"
        "&fltt=2&invt=2&fid=f3"
        "&fs=m:90+t:2+f:!50"
        "&fields=f2,f3,f4,f12,f14,f20,f128,f136,f207,f208,f209"
    )
    raw = fetch_url(url, {"Referer": "https://www.eastmoney.com"})
    results = []
    if raw:
        try:
            obj = json.loads(raw)
            items = obj.get("data", {}).get("diff", [])
            for item in items:
                results.append({
                    "name": item.get("f14", ""),
                    "change_pct": round(item.get("f3", 0), 2),
                    "leading_stock": item.get("f128", ""),
                    "leading_change_pct": round(item.get("f136", 0), 2),
                    "amount_yi": round(item.get("f20", 0) / 1e8, 2),
                })
        except Exception:
            pass
    return results


def fmt_stock(d: dict) -> str:
    if "error" in d:
        return f"❌ {d['error']}"
    sign = "+" if d["change"] >= 0 else ""
    emoji = "🔴" if d["change"] >= 0 else "🟢"
    lines = [
        f"{emoji} {d['name']}（{d['symbol'].upper()}）",
        f"  当前价：{d['current']} 元",
        f"  涨跌幅：{sign}{d['change_pct']}%  涨跌额：{sign}{d['change']}",
        f"  今开：{d['open']}  最高：{d['high']}  最低：{d['low']}  昨收：{d['prev_close']}",
        f"  成交量：{d['volume_lot']:,} 手  成交额：{d['amount_yi']} 亿",
    ]
    if "turnover_pct" in d:
        lines.append(f"  换手率：{d['turnover_pct']}%")
    lines.append(f"  数据来源：{d['source']} | 更新时间：{d.get('time', '')} {d['fetch_time']}")
    return "\n".join(lines)


def fmt_index(items: list) -> str:
    lines = ["📊 大盘指数实时行情"]
    for d in items:
        sign = "+" if d["change"] >= 0 else ""
        emoji = "🔴" if d["change"] >= 0 else "🟢"
        lines.append(
            f"  {emoji} {d['name']}: {d['current']:,.2f}  {sign}{d['change']:+.2f} ({sign}{d['change_pct']}%)"
            f"  成交额 {d['amount_yi']} 亿"
        )
    if items:
        lines.append(f"  更新时间：{items[0]['fetch_time']}")
    return "\n".join(lines)


def fmt_sectors(items: list) -> str:
    lines = ["🔥 热点板块涨幅榜（概念板块 TOP20）"]
    for i, d in enumerate(items, 1):
        sign = "+" if d["change_pct"] >= 0 else ""
        lines.append(
            f"  {i:2d}. {d['name']:<12} {sign}{d['change_pct']}%"
            f"  龙头：{d['leading_stock']}({sign}{d['leading_change_pct']}%)"
            f"  成交额：{d['amount_yi']}亿"
        )
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="A股实时行情抓取")
    parser.add_argument("--code", nargs="+", help="股票代码，支持多个（如 600519 000001）")
    parser.add_argument("--index", action="store_true", help="查询大盘指数")
    parser.add_argument("--hot-sectors", action="store_true", help="查询热点板块")
    parser.add_argument("--json", action="store_true", help="输出原始JSON")
    args = parser.parse_args()

    if args.index:
        data = fetch_index()
        if args.json:
            print(json.dumps(data, ensure_ascii=False, indent=2))
        else:
            print(fmt_index(data))
        return

    if args.hot_sectors:
        data = fetch_hot_sectors()
        if args.json:
            print(json.dumps(data, ensure_ascii=False, indent=2))
        else:
            print(fmt_sectors(data))
        return

    if args.code:
        results = []
        for code in args.code:
            d = fetch_single_stock(code)
            results.append(d)
        if args.json:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            for d in results:
                print(fmt_stock(d))
                print()
        return

    parser.print_help()


if __name__ == "__main__":
    main()


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