Skill: litiao1224/a-stock-analysis-bak (version 1.0.0)

--- name: a-stock-analysis description: A股实时行情与分时量能分析。获取沪深股票实时价格、涨跌、成交量，分析分时量能分布（早盘/尾盘放量）、主力动向（抢筹/出货信号）、涨停封单。支持持仓管理和盈亏分析。Use when: (1) 查询A股实时行情, (2) 分析主力资金动向, (3) 查看分时成交量分布, (4) 管理股票持仓, (5) 分析持仓盈亏。 --- # A股实时行情与分时量能分析 使用东方财富和新浪财经接口获取A股数据，支持实时行情、分时K线、量能分析。 ## 快速开始 ### 实时行情 ```bash # 单只股票 uv run {baseDir}/scripts/analyze.py 600789 # 多只股票 uv run {baseDir}/scripts/analyze.py 600789 002446 002342 # 含分时量能分析 uv run {baseDir}/scripts/analyze.py 600789 --minute # JSON输出 uv run {baseDir}/scripts/analyze.py 600789 --json ``` ### 持仓管理 ```bash # 显示持仓 uv run {baseDir}/scripts/portfolio.py show # 添加持仓 uv run {baseDir}/scripts/portfolio.py add 600789 --cost 10.416 --qty 3400 # 更新持仓 uv run {baseDir}/scripts/portfolio.py update 600789 --cost 10.2 # 删除持仓 uv run {baseDir}/scripts/portfolio.py remove 600789 # 分析所有持仓（含分时量能） uv run {baseDir}/scripts/portfolio.py analyze ``` ## 股票代码格式 直接使用6位数字代码即可，系统自动识别市场： - **沪市**: 6开头 (如 600789) - **深市**: 0或3开头 (如 002446, 300001) - **北交所**: 8或4开头 ## 分时量能分析说明 ### 成交分布时段 | 时段 | 说明 | |------|------| | 早盘30分 (9:30-10:00) | 主力早盘动作，>30%为抢筹信号 | | 上午中段 (10:00-11:30) | 正常交易 | | 下午中段 (13:00-14:30) | 正常交易 | | 尾盘30分 (14:30-15:00) | >25%为尾盘异动信号 | ### 主力动向信号 - **早盘放量 >30%**: 主力早盘抢筹 - **早盘放量 >40%**: 主力强势介入 - **尾盘放量 >15%**: 尾盘有一定放量 - **尾盘放量 >25%**: 可能有主力抢筹或出货 - **封板状态**: 涨停/跌停，关注封单量 ### 放量时段 TOP 10 显示当日成交量最大的10个分钟，用于识别： - 主力建仓/出货时机 - 重大消息发布时点 - 涨停封板/开板时刻 ## 数据源 统一使用新浪财经接口，数据一致性好： | 数据 | 接口 | 延迟 | |------|------|------| | 实时行情 | `hq.sinajs.cn` | ~3秒 | | 分时K线 | `CN_MarketDataService.getKLineData` | ~1分钟 | 支持批量获取，减少请求次数。 ## 输出示例 ``` ============================================================ 股票: 鲁抗医药 (600789) ============================================================ 【实时行情】 现价: 10.32 涨跌: +0.88% 今开: 10.20 最高: 10.77 最低: 9.82 昨收: 10.23 换手: 2.38% 成交量: 218.2万手 成交额: 22.51亿 【分时量能分析】鲁抗医药 全天成交: 2181838手 (225105.3万元) 成交分布: 早盘30分(9:30-10:00): 907228手 (41.6%) 上午中段(10:00-11:30): 866457手 (39.7%) 下午中段(13:00-14:30): 181321手 (8.3%) 尾盘30分(14:30-15:00): 225180手 (10.3%) 放量时段 TOP 10: 09:31:00 价格:10.03 成交:216545手 金额:21965.3万 ... 【主力动向判断】 🔥 早盘主力抢筹明显 ``` ## 持仓文件 持仓数据存储在: `~/.clawdbot/skills/a-stock-analysis/portfolio.json` ## 限制 - 仅支持A股（沪深北交所） - 交易时段外获取的是收盘数据 - 分时数据最多250条（约1个交易日）

path: _meta.json
content: ownerId: kn707vekcz9f58mg3b3cjs3jc17zye0v
slug: a-stock-analysis
version: 1.0.0
publishedAt: 1769506981203

path: scripts/analyze.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
A股实时行情与分时量能分析工具

数据源：新浪财经（统一接口）
支持：沪市(sh)、深市(sz) 股票

Usage:
    uv run analyze.py 600789              # 单只股票
    uv run analyze.py 600789 002446       # 多只股票
    uv run analyze.py 600789 --minute     # 分时量能分析
    uv run analyze.py 600789 --json       # JSON输出
"""

import argparse
import json
import re
import sys
import urllib.request
from datetime import datetime
from typing import Optional


def get_sina_symbol(code: str) -> str:
    """根据股票代码生成新浪格式代码"""
    code = code.upper().replace("SH", "").replace("SZ", "").replace(".", "")
    
    # 沪市: 6开头
    if code.startswith("6"):
        return "sh" + code
    # 深市: 0/3开头
    elif code.startswith(("0", "3")):
        return "sz" + code
    # 北交所: 8/4开头
    elif code.startswith(("8", "4")):
        return "bj" + code
    else:
        return "sh" + code


def fetch_realtime_sina(symbols: list[str]) -> dict[str, dict]:
    """从新浪获取实时行情（支持批量）
    
    新浪接口返回格式:
    var hq_str_sh600789="名称,今开,昨收,现价,最高,最低,买一,卖一,成交量(股),成交额(元),...";
    
    字段说明:
    0: 名称
    1: 今开
    2: 昨收  
    3: 现价
    4: 最高
    5: 最低
    6: 买一价
    7: 卖一价
    8: 成交量(股)
    9: 成交额(元)
    """
    result = {}
    
    try:
        codes_str = ",".join(symbols)
        url = f"https://hq.sinajs.cn/list={codes_str}"
        
        req = urllib.request.Request(url, headers={
            "Referer": "https://finance.sina.com.cn",
            "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",
        })
        resp = urllib.request.urlopen(req, timeout=10)
        text = resp.read().decode("gbk")
        
        # 解析每行
        for line in text.strip().split("\n"):
            line = line.strip()
            if not line:
                continue
            
            # var hq_str_sh600789="数据";
            match = re.match(r'var hq_str_(\w+)="([^"]*)"', line)
            if not match:
                continue
            
            symbol = match.group(1)
            data_str = match.group(2)
            
            if not data_str:
                continue
            
            fields = data_str.split(",")
            if len(fields) < 32:
                continue
            
            name = fields[0]
            open_price = float(fields[1]) if fields[1] else None
            pre_close = float(fields[2]) if fields[2] else None
            price = float(fields[3]) if fields[3] else None
            high = float(fields[4]) if fields[4] else None
            low = float(fields[5]) if fields[5] else None
            volume = int(float(fields[8])) if fields[8] else 0  # 股
            amount = float(fields[9]) if fields[9] else 0  # 元
            
            if not price or price <= 0:
                continue
            
            # 计算涨跌
            change_amt = price - pre_close if pre_close else 0
            change_pct = (change_amt / pre_close * 100) if pre_close and pre_close > 0 else 0
            
            # 换手率需要总股本，这里先留空
            result[symbol] = {
                "code": symbol[2:],  # 去掉sh/sz前缀
                "name": name,
                "price": price,
                "open": open_price,
                "pre_close": pre_close,
                "high": high,
                "low": low,
                "volume": volume // 100,  # 转换为手
                "amount": amount,
                "change_amt": round(change_amt, 2),
                "change_pct": round(change_pct, 2),
                "turnover": None,  # 新浪实时接口不提供换手率
            }
            
    except Exception as e:
        print(f"新浪实时接口错误: {e}", file=sys.stderr)
    
    return result


def fetch_minute_data_sina(symbol: str, count: int = 250) -> list[dict]:
    """从新浪获取分时K线数据
    
    接口: CN_MarketDataService.getKLineData
    返回JSON数组，每条记录包含:
    - day: 时间 (2026-01-27 09:31:00)
    - open/high/low/close: OHLC价格
    - volume: 成交量(股)
    - amount: 成交额(元)
    """
    url = f"https://quotes.sina.cn/cn/api/jsonp_v2.php/var%20_{symbol}=/CN_MarketDataService.getKLineData?symbol={symbol}&scale=1&ma=no&datalen={count}"
    
    try:
        req = urllib.request.Request(url, headers={
            "Referer": "https://finance.sina.com.cn",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        })
        resp = urllib.request.urlopen(req, timeout=10)
        text = resp.read().decode("utf-8")
        
        # 解析JSONP: var _xxx=([...])
        match = re.search(r"\(\[(.*)\]\)", text, re.DOTALL)
        if not match:
            return []
        
        data = json.loads("[" + match.group(1) + "]")
        result = []
        for item in data:
            result.append({
                "time": item["day"],
                "open": float(item["open"]),
                "high": float(item["high"]),
                "low": float(item["low"]),
                "close": float(item["close"]),
                "volume": int(item["volume"]),  # 股
                "amount": float(item["amount"]),  # 元
            })
        return result
        
    except Exception as e:
        print(f"新浪分时接口错误: {e}", file=sys.stderr)
    
    return []


def analyze_minute_volume(minute_data: list[dict]) -> dict:
    """分析分时量能"""
    if not minute_data:
        return {"error": "无分时数据"}
    
    # 过滤交易时段数据
    trading_data = [
        d for d in minute_data
        if d["volume"] > 0 and "09:25" <= d["time"][-8:-3] <= "15:00"
    ]
    
    if not trading_data:
        return {"error": "无有效交易数据"}
    
    # 统计各时段成交量
    total_vol = sum(d["volume"] for d in trading_data)
    
    def period_vol(start: str, end: str) -> int:
        return sum(
            d["volume"] for d in trading_data
            if start <= d["time"][-8:-3] < end
        )
    
    open_30 = period_vol("09:30", "10:00")
    mid_am = period_vol("10:00", "11:30")
    mid_pm = period_vol("13:00", "14:30")
    close_30 = period_vol("14:30", "15:01")
    
    # 放量时段 TOP 10
    sorted_by_vol = sorted(trading_data, key=lambda x: x["volume"], reverse=True)[:10]
    top_volumes = [
        {
            "time": d["time"][-8:],
            "price": d["close"],
            "volume": d["volume"] // 100,  # 转换为手
            "amount": d["amount"],
        }
        for d in sorted_by_vol
    ]
    
    # 主力动向判断
    signals = []
    if total_vol > 0:
        if close_30 / total_vol > 0.25:
            signals.append("尾盘大幅放量，可能有主力抢筹或出货")
        elif close_30 / total_vol > 0.15:
            signals.append("尾盘有一定放量")
        if open_30 / total_vol > 0.30:
            signals.append("早盘主力抢筹明显")
        if open_30 / total_vol > 0.40:
            signals.append("早盘放量异常，主力强势介入")
    
    # 检测涨停/跌停
    last_price = trading_data[-1]["close"] if trading_data else 0
    highest_vol_price = sorted_by_vol[0]["close"] if sorted_by_vol else 0
    if last_price > 0 and abs(last_price - highest_vol_price) < 0.01:
        signals.append("封板状态，关注封单量")
    
    return {
        "total_volume": total_vol // 100,  # 手
        "total_amount": sum(d["amount"] for d in trading_data),
        "distribution": {
            "open_30min": {
                "volume": open_30 // 100,
                "percent": round(open_30 / total_vol * 100, 1) if total_vol else 0,
            },
            "mid_am": {
                "volume": mid_am // 100,
                "percent": round(mid_am / total_vol * 100, 1) if total_vol else 0,
            },
            "mid_pm": {
                "volume": mid_pm // 100,
                "percent": round(mid_pm / total_vol * 100, 1) if total_vol else 0,
            },
            "close_30min": {
                "volume": close_30 // 100,
                "percent": round(close_30 / total_vol * 100, 1) if total_vol else 0,
            },
        },
        "top_volumes": top_volumes,
        "signals": signals,
    }


def format_realtime(data: dict) -> str:
    """格式化实时行情输出"""
    change_symbol = "+" if data["change_pct"] >= 0 else ""
    turnover_str = f"换手: {data['turnover']:.2f}%" if data.get("turnover") else ""
    
    lines = [
        f"{'='*60}",
        f"股票: {data['name']} ({data['code']})",
        f"{'='*60}",
        f"",
        f"【实时行情】",
        f"  现价: {data['price']:.2f}  涨跌: {change_symbol}{data['change_pct']:.2f}%",
        f"  今开: {data['open']:.2f}  最高: {data['high']:.2f}  最低: {data['low']:.2f}",
        f"  昨收: {data['pre_close']:.2f}  {turnover_str}",
        f"  成交量: {data['volume']/10000:.1f}万手  成交额: {data['amount']/100000000:.2f}亿",
    ]
    return "\n".join(lines)


def format_minute_analysis(analysis: dict, name: str = "") -> str:
    """格式化分时分析输出"""
    if "error" in analysis:
        return f"分时分析错误: {analysis['error']}"
    
    lines = [
        f"",
        f"【分时量能分析】{name}",
        f"  全天成交: {analysis['total_volume']}手 ({analysis['total_amount']/10000:.1f}万元)",
        f"",
        f"  成交分布:",
        f"    早盘30分(9:30-10:00): {analysis['distribution']['open_30min']['volume']}手 ({analysis['distribution']['open_30min']['percent']}%)",
        f"    上午中段(10:00-11:30): {analysis['distribution']['mid_am']['volume']}手 ({analysis['distribution']['mid_am']['percent']}%)",
        f"    下午中段(13:00-14:30): {analysis['distribution']['mid_pm']['volume']}手 ({analysis['distribution']['mid_pm']['percent']}%)",
        f"    尾盘30分(14:30-15:00): {analysis['distribution']['close_30min']['volume']}手 ({analysis['distribution']['close_30min']['percent']}%)",
        f"",
        f"  放量时段 TOP 10:",
    ]
    
    for item in analysis["top_volumes"]:
        lines.append(f"    {item['time']} 价格:{item['price']:.2f} 成交:{item['volume']}手 金额:{item['amount']/10000:.1f}万")
    
    if analysis["signals"]:
        lines.append(f"")
        lines.append(f"  【主力动向判断】")
        for signal in analysis["signals"]:
            lines.append(f"    🔥 {signal}")
    
    return "\n".join(lines)


def analyze_stock(code: str, with_minute: bool = False, realtime_cache: dict = None) -> dict:
    """分析单只股票"""
    sina_symbol = get_sina_symbol(code)
    
    # 获取实时行情（支持缓存以批量获取）
    if realtime_cache and sina_symbol in realtime_cache:
        realtime = realtime_cache[sina_symbol]
    else:
        realtime_data = fetch_realtime_sina([sina_symbol])
        realtime = realtime_data.get(sina_symbol)
    
    if not realtime:
        return {"error": f"无法获取 {code} 的行情数据"}
    
    result = {
        "code": code,
        "name": realtime["name"],
        "realtime": realtime,
        "updated_at": datetime.now().isoformat(),
    }
    
    # 分时分析
    if with_minute:
        minute_data = fetch_minute_data_sina(sina_symbol)
        minute_analysis = analyze_minute_volume(minute_data)
        result["minute_analysis"] = minute_analysis
    
    return result


def main():
    parser = argparse.ArgumentParser(description="A股实时行情与分时量能分析")
    parser.add_argument("codes", nargs="+", help="股票代码，如 600789 002446")
    parser.add_argument("--minute", "-m", action="store_true", help="包含分时量能分析")
    parser.add_argument("--json", "-j", action="store_true", help="JSON格式输出")
    
    args = parser.parse_args()
    
    # 批量获取实时行情
    sina_symbols = [get_sina_symbol(code) for code in args.codes]
    realtime_cache = fetch_realtime_sina(sina_symbols)
    
    results = []
    for code in args.codes:
        result = analyze_stock(code, with_minute=args.minute, realtime_cache=realtime_cache)
        results.append(result)
    
    if args.json:
        print(json.dumps(results, ensure_ascii=False, indent=2))
    else:
        for result in results:
            if "error" in result:
                print(f"错误: {result['error']}")
                continue
            
            print(format_realtime(result["realtime"]))
            
            if args.minute and "minute_analysis" in result:
                print(format_minute_analysis(result["minute_analysis"], result["name"]))
            
            print()


if __name__ == "__main__":
    main()


path: scripts/portfolio.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
A股持仓管理工具

Usage:
    uv run portfolio.py show                           # 显示持仓
    uv run portfolio.py add 600789 --cost 10.5 --qty 1000   # 添加持仓
    uv run portfolio.py update 600789 --cost 10.2          # 更新成本
    uv run portfolio.py remove 600789                      # 删除持仓
    uv run portfolio.py analyze                            # 持仓分析(含分时)
"""

import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path

# 导入同目录的 analyze 模块
sys.path.insert(0, os.path.dirname(__file__))
from analyze import analyze_stock, format_realtime, format_minute_analysis


PORTFOLIO_FILE = Path.home() / ".clawdbot" / "skills" / "a-stock-analysis" / "portfolio.json"


def load_portfolio() -> dict:
    """加载持仓数据"""
    if PORTFOLIO_FILE.exists():
        with open(PORTFOLIO_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"positions": [], "updated_at": None}


def save_portfolio(data: dict):
    """保存持仓数据"""
    PORTFOLIO_FILE.parent.mkdir(parents=True, exist_ok=True)
    data["updated_at"] = datetime.now().isoformat()
    with open(PORTFOLIO_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def show_portfolio():
    """显示持仓"""
    portfolio = load_portfolio()
    positions = portfolio.get("positions", [])
    
    if not positions:
        print("暂无持仓记录")
        return
    
    print("=" * 70)
    print("当前持仓")
    print("=" * 70)
    print(f"{'股票':<10} {'代码':<8} {'成本':<8} {'数量':<8} {'市值估算':<12}")
    print("-" * 70)
    
    total_cost = 0
    for pos in positions:
        cost_value = pos["cost"] * pos["quantity"]
        total_cost += cost_value
        print(f"{pos.get('name', 'N/A'):<10} {pos['code']:<8} {pos['cost']:<8.3f} {pos['quantity']:<8} {cost_value:<12.0f}")
    
    print("-" * 70)
    print(f"总成本: {total_cost:.0f} 元")
    print(f"更新时间: {portfolio.get('updated_at', 'N/A')}")


def add_position(code: str, cost: float, quantity: int, name: str = ""):
    """添加持仓"""
    portfolio = load_portfolio()
    positions = portfolio.get("positions", [])
    
    # 检查是否已存在
    for pos in positions:
        if pos["code"] == code:
            print(f"持仓已存在: {code}，请使用 update 命令更新")
            return
    
    positions.append({
        "code": code,
        "name": name,
        "cost": cost,
        "quantity": quantity,
        "added_at": datetime.now().isoformat(),
    })
    
    portfolio["positions"] = positions
    save_portfolio(portfolio)
    print(f"已添加: {code} 成本{cost} 数量{quantity}")


def update_position(code: str, cost: float = None, quantity: int = None, name: str = None):
    """更新持仓"""
    portfolio = load_portfolio()
    positions = portfolio.get("positions", [])
    
    for pos in positions:
        if pos["code"] == code:
            if cost is not None:
                pos["cost"] = cost
            if quantity is not None:
                pos["quantity"] = quantity
            if name is not None:
                pos["name"] = name
            pos["updated_at"] = datetime.now().isoformat()
            save_portfolio(portfolio)
            print(f"已更新: {code}")
            return
    
    print(f"未找到持仓: {code}")


def remove_position(code: str):
    """删除持仓"""
    portfolio = load_portfolio()
    positions = portfolio.get("positions", [])
    
    new_positions = [p for p in positions if p["code"] != code]
    
    if len(new_positions) == len(positions):
        print(f"未找到持仓: {code}")
        return
    
    portfolio["positions"] = new_positions
    save_portfolio(portfolio)
    print(f"已删除: {code}")


def analyze_portfolio():
    """分析所有持仓"""
    portfolio = load_portfolio()
    positions = portfolio.get("positions", [])
    
    if not positions:
        print("暂无持仓记录")
        return
    
    print("=" * 70)
    print(f"持仓分析 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    print("=" * 70)
    
    total_cost = 0
    total_value = 0
    
    results = []
    
    for pos in positions:
        code = pos["code"]
        result = analyze_stock(code, with_minute=True)
        
        if "error" in result:
            print(f"\n{code}: {result['error']}")
            continue
        
        # 更新持仓名称
        if not pos.get("name") and result.get("name"):
            pos["name"] = result["name"]
        
        realtime = result["realtime"]
        cost = pos["cost"]
        qty = pos["quantity"]
        
        cost_value = cost * qty
        market_value = realtime["price"] * qty
        pnl = market_value - cost_value
        pnl_pct = (realtime["price"] / cost - 1) * 100
        
        total_cost += cost_value
        total_value += market_value
        
        print(format_realtime(realtime))
        print(f"\n【持仓盈亏】")
        print(f"  成本: {cost:.3f}  持仓: {qty}股")
        print(f"  成本市值: {cost_value:.0f}  当前市值: {market_value:.0f}")
        print(f"  盈亏: {pnl:+.0f} ({pnl_pct:+.2f}%)")
        
        if "minute_analysis" in result:
            print(format_minute_analysis(result["minute_analysis"], result["name"]))
        
        print()
        results.append({
            "position": pos,
            "result": result,
            "pnl": pnl,
            "pnl_pct": pnl_pct,
        })
    
    # 汇总
    print("=" * 70)
    print("【持仓汇总】")
    print(f"  总成本: {total_cost:.0f} 元")
    print(f"  总市值: {total_value:.0f} 元")
    print(f"  总盈亏: {total_value - total_cost:+.0f} 元 ({(total_value/total_cost-1)*100:+.2f}%)")
    
    # 保存更新后的名称
    save_portfolio(portfolio)
    
    return results


def main():
    parser = argparse.ArgumentParser(description="A股持仓管理")
    subparsers = parser.add_subparsers(dest="command", help="命令")
    
    # show
    subparsers.add_parser("show", help="显示持仓")
    
    # add
    add_parser = subparsers.add_parser("add", help="添加持仓")
    add_parser.add_argument("code", help="股票代码")
    add_parser.add_argument("--cost", "-c", type=float, required=True, help="成本价")
    add_parser.add_argument("--qty", "-q", type=int, required=True, help="持仓数量")
    add_parser.add_argument("--name", "-n", default="", help="股票名称")
    
    # update
    update_parser = subparsers.add_parser("update", help="更新持仓")
    update_parser.add_argument("code", help="股票代码")
    update_parser.add_argument("--cost", "-c", type=float, help="成本价")
    update_parser.add_argument("--qty", "-q", type=int, help="持仓数量")
    update_parser.add_argument("--name", "-n", help="股票名称")
    
    # remove
    remove_parser = subparsers.add_parser("remove", help="删除持仓")
    remove_parser.add_argument("code", help="股票代码")
    
    # analyze
    subparsers.add_parser("analyze", help="持仓分析")
    
    args = parser.parse_args()
    
    if args.command == "show":
        show_portfolio()
    elif args.command == "add":
        add_position(args.code, args.cost, args.qty, args.name)
    elif args.command == "update":
        update_position(args.code, args.cost, args.qty, args.name)
    elif args.command == "remove":
        remove_position(args.code)
    elif args.command == "analyze":
        analyze_portfolio()
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


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