Skill: xsqorange/stock-monitor-orange (version 1.0.10)

--- name: stock-monitor description: 股票监控分析技能 - 自定义股票池监控、实时行情、技术指标分析、涨跌趋势预测、信号提醒 version: 1.0.0 --- # Metadata ```yaml openclaw: requires: files: - ~/.openclaw/stock-pool.json # 股票监控池配置（公开数据） - ~/.openclaw/stock-positions.json # 持仓记录（个人数据，仅本地使用） - ~/.openclaw/stock-trades.json # 交易记录（个人数据，仅本地使用） - ~/.openclaw/stock-alerts.json # 预警配置 - ~/.openclaw/cron/jobs.json # 定时任务配置 apis: - 腾讯行情API (qt.gtimg.cn) # 实时行情（主要数据源） - 东方财富API (eastmoney.com) # K线数据、资讯 - 新浪财经API (sina.com.cn) # 港股资讯 network: true python: true ``` # Stock Monitor - 股票监控分析技能 --- ## 核心特色 - 多维度技术分析：ADX趋势、BOLL轨道、MACD/KDJ/RSI/OBV/DMI/WR指标 - 资讯情感分析：东方财富+新浪财经+雪球 - 持仓智能管理：自动追踪成本、盈亏、FIFO同步 - **买卖点位建议**：基于支撑/阻力位+BOLL+均线的精确入场/出场/止损/目标价位 - 中国股市习惯：🔴红色=上涨/盈利 | 🟢绿色=下跌/亏损 --- ## 目录结构 ``` stock-monitor/ ├── SKILL.md 本文件 ├── references/ │ ├── commands.md CLI命令速查 │ ├── config.md 配置文件说明 │ ├── index.md 指标说明 │ ├── scheduled-tasks.md 定时任务配置 │ └── troubleshooting.md 故障排查 ├── reports/ │ ├── prompts.md Cron任务Prompt模板 │ └── templates.md 报告模板（完整模板） └── scripts/ ├── stock_monitor.py 主监控脚本 └── stock_capital.py 资金流向脚本 ``` --- ## CLI 命令 > 工作目录：`scripts` | 格式：`python stock_monitor.py <command> [args]` ### 行情与指数 | 命令 | 说明 | |------|------| | `quote <code>` | 查询单只股票行情 | | `index` | 查询大盘指数 | ### 技术分析 | 命令 | 说明 | |------|------| | `analyze <code>` | 分析股票技术指标 | | `monitor-a` | 监控所有A股 | | `monitor-hk` | 监控所有港股 | ### 综合报告 | 命令 | 说明 | |------|------| | `report` | 生成完整综合报告 | | `report-a` | 生成A股综合报告 | | `report-hk` | 生成港股综合报告 | ### 持仓管理 | 命令 | 说明 | |------|------| | `position add <code> <qty> <cost>` | 添加/更新持仓 | | `position remove <code>` | 清除持仓 | | `position list` | 查看持仓 | ### 交易记录 | 命令 | 说明 | |------|------| | `trade buy <code> <qty> <price>` | 记录买入 | | `trade sell <code> <qty> <price>` | 记录卖出 | | `trades [code]` | 查看交易记录 | --- ## 重要规则 ### 数据优先级（强制） 1. 脚本行情数据（`index`/`monitor-a`/`monitor-hk`）— 权威数据 2. 搜索资讯数据（`web_fetch`）— 仅用于资讯分析 **【强制】报告中所有"现价/涨跌幅/涨跌额"必须使用脚本返回的数据，禁止使用搜索结果中的价格。** ### 技术指标（必须包含） 每只股票：现价、涨跌幅、MA均线多空、MACD状态、KDJ状态、RSI数值、BOLL位置、OBV背离、DMI趋势、WR威廉、综合信号 ### 综合信号评级 | 评分差 | 信号 | |--------|------| | buy > sell + 3 | ⭐⭐⭐ 强烈买入 | | buy > sell | ⭐⭐ 建议买入 | | abs差 ≤ 3 | ⚪ 观望 | | sell > buy | ⭐ 建议卖出 | | sell > buy + 3 | ⭐⭐ 强烈卖出 | ### 买卖点位计算规则 | 场景 | 买入点位 | 买入区间 | 卖出点位 | 卖出区间 | 止损位 | 目标位 | |------|---------|---------|---------|---------|--------|--------| | 回踩支撑 | 支撑位 | 支撑×0.98~1.02 | - | - | 支撑下方3% | 阻力位/BOLL上轨 | | 突破阻力 | 突破后确认 | 阻力~阻力×1.02 | - | - | 阻力下方3% | 上一阻力位 | | RSI超卖 | BOLL下轨/支撑 | 下轨×0.98~1.02 | - | - | 前低下方3% | BOLL上轨/阻力位 | | RSI超买 | - | - | BOLL上轨/阻力位 | 上轨×0.97~1.01 | - | - | | MACD死叉 | - | - | 短线阻力位 | 阻力×0.97~1.01 | - | - | **计算依据**： - 支撑位：近30日低点、BOLL下轨、60日均线 - 阻力位：近30日高点、BOLL上轨、近期反弹高点 - 止损：支撑/均线下方3%（保守可设2%） - 目标位：阻力位、BOLL上轨、量度涨幅（前一高低点差） - **买入区间**：在点位附近±2%范围分批建仓 - **卖出区间**：在点位附近±3%范围分批止盈 --- ## 报告模板 详见 `reports/templates.md` ### 报告生成流程 1. `python stock_monitor.py index` 获取大盘指数 2. 读取 `~/.openclaw/stock-pool.json` 获取股票列表 3. `python stock_monitor.py monitor-a/hk` 获取技术面数据 4. `web_fetch` 获取资讯（优先东方财富/新浪/雪球） 5. 生成报告（价格数据必须用步骤3返回的数据） 6. 推送至飞书群聊 --- ## 配置文件 | 文件 | 路径 | |------|------| | 股票池 | `~/.openclaw/stock-pool.json` | | 持仓记录 | `~/.openclaw/stock-positions.json` | | 交易记录 | `~/.openclaw/stock-trades.json` | | 预警配置 | `~/.openclaw/stock-alerts.json` | | Cron任务 | `~/.openclaw/cron/jobs.json` | --- ## 使用提示 - 技术指标有滞后性，用于确认趋势而非预测 - 多条件共振更可靠，单一指标容易假信号 - 动态止盈：回撤5%减仓、10%清仓（建议，根据市场灵活调整） - **核心原则**：预警系统目标是"不错过大机会，不犯大错误"

path: scripts/hk_analyze.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Direct HK stock analyzer - bypasses code format bugs in main monitor"""
import sys
import json
import time
import urllib.request
from datetime import datetime

sys.stdout.reconfigure(encoding='utf-8')

HK_CODES = ["00981", "00992", "01088", "00005", "00941", "06613"]
HK_NAMES = {
    "00981": "中芯国际",
    "00992": "联想集团", 
    "01088": "中国神华",
    "00005": "汇丰控股",
    "00941": "中国移动",
    "06613": "上海复旦",
}

def get_quote(code_5):
    """Get realtime quote from Tencent"""
    url = f"https://qt.gtimg.cn/q=hk{code_5}"
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    with urllib.request.urlopen(req, timeout=10) as r:
        raw = r.read().decode('gbk', errors='ignore')
    # Parse: v_hkXXXXX="100~name~code~price~prev_close~open~volume~..."
    parts = raw.split('"')[1].split('~')
    name = parts[1]
    price = float(parts[3])
    prev_close = float(parts[4])
    change = price - prev_close
    pct = (change / prev_close) * 100
    update_time = parts[30]
    return {"name": name, "price": price, "prev_close": prev_close, "change": change, "pct": pct, "time": update_time}

def get_kline(code_5, days=60):
    """Get K-line from Tencent ifzq"""
    url = f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=hk{code_5},day,,,{days},qfq"
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    with urllib.request.urlopen(req, timeout=15) as r:
        content = r.read().decode('utf-8', errors='ignore')
    data = json.loads(content)
    stock_data = data.get('data', {})
    key = f'hk{code_5}'
    if key not in stock_data:
        # try without hk prefix in key
        for k in stock_data:
            if k.startswith('hk'):
                key = k
                break
    day_data = stock_data.get(key, {}).get('day', [])
    if not day_data:
        return None
    klines = []
    for item in day_data:
        klines.append({
            "date": item[0],
            "open": float(item[1]),
            "close": float(item[2]),
            "high": float(item[3]),
            "low": float(item[4]),
            "volume": float(item[5])
        })
    return klines

def calc_ma(closes, n):
    if len(closes) < n:
        return None
    return sum(closes[-n:]) / n

def calc_macd(closes, fast=12, slow=26, signal=9):
    if len(closes) < slow + signal:
        return None
    ema_fast = closes[0]
    ema_slow = closes[0]
    alpha_fast = 2 / (fast + 1)
    alpha_slow = 2 / (slow + 1)
    for c in closes[1:]:
        ema_fast = c * alpha_fast + ema_fast * (1 - alpha_fast)
        ema_slow = c * alpha_slow + ema_slow * (1 - alpha_slow)
    dif = ema_fast - ema_slow
    dea = dif * 0.8 + 0  # simplified
    macd_bar = 2 * (dif - dea)
    return {"dif": dif, "dea": dea, "bar": macd_bar, "histogram": macd_bar}

def calc_kdj(closes, highs, lows, n=9):
    if len(closes) < n:
        return None
    recent_closes = closes[-n:]
    recent_highs = highs[-n:]
    recent_lows = lows[-n:]
    low_n = min(recent_lows)
    high_n = max(recent_highs)
    if high_n == low_n:
        k = 50
    else:
        rsv = 100 * (closes[-1] - low_n) / (high_n - low_n)
        k = 2/3 * 50 + 1/3 * rsv
        d = 2/3 * 50 + 1/3 * k
        j = 3 * k - 2 * d
    return {"k": k, "d": d, "j": j, "rsv": rsv if 'rsv' in dir() else k}

def calc_rsi(closes, n=6):
    if len(closes) < n + 1:
        return None
    gains = []
    losses = []
    for i in range(1, len(closes)):
        diff = closes[i] - closes[i-1]
        if diff > 0:
            gains.append(diff)
            losses.append(0)
        else:
            gains.append(0)
            losses.append(abs(diff))
    avg_gain = sum(gains[-n:]) / n
    avg_loss = sum(losses[-n:]) / n
    if avg_loss == 0:
        rsi = 100
    else:
        rs = avg_gain / avg_loss
        rsi = 100 - 100 / (1 + rs)
    return {"rsi6": rsi, "rsi12": None, "rsi24": None}

def calc_boll(closes, n=20, k=2):
    if len(closes) < n:
        return None
    recent = closes[-n:]
    mid = sum(recent) / n
    variance = sum((c - mid) ** 2 for c in recent) / n
    std = variance ** 0.5
    upper = mid + k * std
    lower = mid - k * std
    return {"upper": upper, "mid": mid, "lower": lower, "position": (closes[-1] - lower) / (upper - lower) if upper != lower else 0.5}

def calc_obv(closes, volumes):
    if len(closes) < 2:
        return {"signal": "数据不足"}
    obv = 0
    prev_close = closes[0]
    for i in range(1, len(closes)):
        if closes[i] > prev_close:
            obv += volumes[i]
        elif closes[i] < prev_close:
            obv -= volumes[i]
        prev_close = closes[i]
    # Check divergence: last 10 vs price
    if len(closes) >= 20:
        price_trend = closes[-1] - closes[-10]
        vol_trend = sum(volumes[-5:]) / 5 - sum(volumes[-10:-5]) / 5
        if price_trend < 0 and vol_trend > 0:
            signal = "底背离↑"
        elif price_trend > 0 and vol_trend < 0:
            signal = "顶背离↓"
        else:
            signal = "无背离"
    else:
        signal = "无背离"
    return {"obv": obv, "signal": signal}

def calc_dmi(highs, lows, closes, n=14):
    if len(closes) < n + 1:
        return {"pdi": None, "mdi": None, "adx": None, "signal": "数据不足"}
    # Simplified DMI
    plus_dm = 0
    minus_dm = 0
    tr = 0
    for i in range(-n, 0):
        high_diff = highs[i+1] - highs[i] if i+1 < len(highs) else 0
        low_diff = lows[i] - lows[i+1] if i+1 < len(lows) else 0
        if high_diff > low_diff and high_diff > 0:
            plus_dm += high_diff
        if low_diff > high_diff and low_diff > 0:
            minus_dm += low_diff
    return {"pdi": plus_dm / n, "mdi": minus_dm / n, "adx": None, "signal": "中性"}

def calc_wr(highs, lows, closes, n=10):
    if len(closes) < n:
        return None
    recent_highs = highs[-n:]
    recent_lows = lows[-n:]
    highest = max(recent_highs)
    lowest = min(recent_lows)
    if highest == lowest:
        wr = -50
    else:
        wr = -100 * (highest - closes[-1]) / (highest - lowest)
    return {"wr10": wr}

def analyze(code_5):
    quote = get_quote(code_5)
    klines = get_kline(code_5, 60)
    if not klines or len(klines) < 20:
        return {"error": f"无法获取 {code_5} K线数据", "quote": quote}
    
    closes = [k['close'] for k in klines]
    highs = [k['high'] for k in klines]
    lows = [k['low'] for k in klines]
    volumes = [k['volume'] for k in klines]
    
    ma5 = calc_ma(closes, 5)
    ma10 = calc_ma(closes, 10)
    ma20 = calc_ma(closes, 20)
    ma60 = calc_ma(closes, 60) if len(closes) >= 60 else None
    
    macd = calc_macd(closes)
    kdj = calc_kdj(closes, highs, lows)
    rsi = calc_rsi(closes)
    boll = calc_boll(closes)
    obv = calc_obv(closes, volumes)
    dmi = calc_dmi(highs, lows, closes)
    wr = calc_wr(highs, lows, closes)
    
    # Signals
    buy_score = 0
    sell_score = 0
    signals = []
    
    # MA signals
    if ma5 and ma10:
        if ma5 > ma10:
            buy_score += 1
            ma_signal = f"MA5({ma5:.2})>MA10({ma10:.2}) 多头 ↑"
        else:
            sell_score += 1
            ma_signal = f"MA5({ma5:.2})<MA10({ma10:.2}) 空头 ↓"
    else:
        ma_signal = "MA信号不足"
    
    # MACD signals
    if macd:
        if macd['dif'] > macd['dea']:
            buy_score += 1
            macd_signal = f"DIF({macd['dif']:.4f})>DEA({macd['dea']:.4f}) 金叉↑ 红柱"
        else:
            sell_score += 1
            macd_signal = f"DIF({macd['dif']:.4f})<DEA({macd['dea']:.4f}) 死叉↓ 绿柱"
    else:
        macd_signal = "MACD数据不足"
    
    # KDJ signals
    if kdj:
        if kdj['k'] > kdj['d']:
            buy_score += 0.5
            kdj_signal = f"K:{kdj['k']:.1f} D:{kdj['d']:.1f} J:{kdj['j']:.1f} | 金叉"
        else:
            sell_score += 0.5
            kdj_signal = f"K:{kdj['k']:.1f} D:{kdj['d']:.1f} J:{kdj['j']:.1f} | 死叉"
        if kdj['j'] < 20:
            kdj_signal += " 超卖"
        elif kdj['j'] > 80:
            kdj_signal += " 超买"
    else:
        kdj_signal = "KDJ数据不足"
    
    # RSI signals
    if rsi:
        rsi_val = rsi['rsi6']
        if rsi_val < 30:
            sell_score += 0.5
            rsi_signal = f"RSI6:{rsi_val:.1f} | 偏弱(超卖)"
        elif rsi_val > 70:
            buy_score += 0.5
            rsi_signal = f"RSI6:{rsi_val:.1f} | 偏强(超买)"
        else:
            rsi_signal = f"RSI6:{rsi_val:.1f} | 中性"
    else:
        rsi_signal = "RSI数据不足"
    
    # BOLL signals
    if boll:
        price = closes[-1]
        if price > boll['mid']:
            buy_score += 0.5
            boll_signal = f"中轨{boll['mid']:.2f}上方 通道平稳"
        else:
            sell_score += 0.5
            boll_signal = f"中轨{boll['mid']:.2f}下方 偏弱"
    else:
        boll_signal = "BOLL数据不足"
    
    overall = "买入" if buy_score > sell_score + 1 else "卖出" if sell_score > buy_score + 1 else "观望"
    stars = "⭐" * max(1, min(3, int(buy_score)))
    
    return {
        "code": code_5,
        "name": HK_NAMES.get(code_5, quote.get('name', code_5)),
        "quote": quote,
        "ma": {"ma5": ma5, "ma10": ma10, "ma20": ma20, "ma60": ma60},
        "ma_signal": ma_signal,
        "macd": macd,
        "macd_signal": macd_signal,
        "kdj": kdj,
        "kdj_signal": kdj_signal,
        "rsi": rsi,
        "rsi_signal": rsi_signal,
        "boll": boll,
        "boll_signal": boll_signal,
        "obv": obv,
        "dmi": dmi,
        "wr": wr,
        "buy_score": buy_score,
        "sell_score": sell_score,
        "overall": overall,
        "stars": stars,
        "klines": klines[-5:],  # last 5 for debugging
    }

if __name__ == "__main__":
    results = []
    for code in HK_CODES:
        try:
            r = analyze(code)
            results.append(r)
            print(f"✅ {code} {r.get('name','?')} 完成", flush=True)
        except Exception as e:
            print(f"❌ {code} 错误: {e}", flush=True)
        time.sleep(1)
    
    print("\n=== JSON RESULTS ===")
    print(json.dumps(results, ensure_ascii=False, indent=2))


path: scripts/stock_capital.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stock Monitor - 资金流向、龙虎榜、北向资金功能扩展
"""

import json
import os
import sys
import urllib.request
import urllib.parse
from datetime import datetime, timedelta
from typing import Dict, List, Optional

# ============ 资金流向功能 ============

def get_capital_flow(code: str) -> Optional[Dict]:
    """
    获取个股资金流向 (东方财富)
    """
    try:
        # 判断市场
        if code.startswith("6"):
            secid = f"1.{code}"
        elif code.startswith(("0", "3")):
            secid = f"0.{code}"
        else:
            return None
        
        url = f"http://push2ex.eastmoney.com/getTopicZDFenBu?ut=7eea3edcaed734bea9cbfc24409ed989&dession=01&mession=01&vessionName=&SECID={secid}"
        
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        req.add_header('Referer', 'https://www.eastmoney.com/')
        
        with urllib.request.urlopen(req, timeout=10) as r:
            content = r.read().decode('utf-8', errors='ignore')
            data = json.loads(content)
            
            if data.get("data"):
                fenbu = data["data"]
                return {
                    "code": code,
                    "net_inflow_main": fenbu.get("net_inflow_main", 0),  # 主力净流入
                    "net_inflow_huge": fenbu.get("net_inflow_huge", 0),  # 超大单净流入
                    "net_inflow_big": fenbu.get("net_inflow_big", 0),    # 大单净流入
                    "net_inflow_mid": fenbu.get("net_inflow_mid", 0),     # 中单净流入
                    "net_inflow_small": fenbu.get("net_inflow_small", 0), # 小单净流入
                }
    except Exception as e:
        print(f"获取资金流向失败: {code}, {e}")
    
    return None

def get_sector_capital_flow(sector_name: str) -> Optional[Dict]:
    """
    获取板块资金流向
    """
    sector_map = {
        "芯片": "bk0455",
        "新能源": "bk0493",
        "医药": "bk0465",
        "白酒": "bk0437",
        "银行": "bk0435",
        "房地产": "bk0451",
        "军工": "bk0436",
    }
    
    bk_code = sector_map.get(sector_name)
    if not bk_code:
        return None
    
    try:
        url = f"http://push2ex.eastmoney.com/getTopicZDFenBu?ut=7eea3edcaed734bea9cbfc24409ed989&dession=01&mession=01&vessionName=&SECID=90.{bk_code}"
        
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        req.add_header('Referer', 'https://www.eastmoney.com/')
        
        with urllib.request.urlopen(req, timeout=10) as r:
            content = r.read().decode('utf-8', errors='ignore')
            data = json.loads(content)
            
            if data.get("data"):
                return {
                    "sector": sector_name,
                    "data": data["data"]
                }
    except Exception as e:
        print(f"获取板块资金流向失败: {sector_name}, {e}")
    
    return None

def format_capital_flow(data: Dict, stock_name: str = "") -> str:
    """格式化资金流向报告"""
    if not data:
        return "暂无资金流向数据"
    
    lines = [f"💰 资金流向 - {stock_name or data.get('code', '')}", "-" * 40]
    
    # 格式化数值（转为亿/万元）
    def fmt_money(val):
        if val is None:
            return "--"
        val = float(val)
        if abs(val) >= 100000000:
            return f"{val/100000000:.2f}亿"
        elif abs(val) >= 10000:
            return f"{val/10000:.2f}万"
        else:
            return f"{val:.0f}元"
    
    main = data.get("net_inflow_main", 0) or 0
    huge = data.get("net_inflow_huge", 0) or 0
    big = data.get("net_inflow_big", 0) or 0
    mid = data.get("net_inflow_mid", 0) or 0
    small = data.get("net_inflow_small", 0) or 0
    
    main_emoji = "🟢" if main > 0 else "🔴"
    
    lines.append(f"{main_emoji} 主力净流入: {fmt_money(main)}")
    lines.append(f"   超大单: {fmt_money(huge)}")
    lines.append(f"   大单:   {fmt_money(big)}")
    lines.append(f"   中单:   {fmt_money(mid)}")
    lines.append(f"   小单:   {fmt_money(small)}")
    
    return "\n".join(lines)


# ============ 龙虎榜功能 ============

def get_longhub_list(date: str = None) -> List[Dict]:
    """
    获取龙虎榜数据 (东方财富)
    """
    if not date:
        date = datetime.now().strftime("%Y%m%d")
    
    # 尝试多个API
    urls = [
        f"http://push2ex.eastmoney.com/getTopicLHBDetail?ut=7eea3edcaed734bea9cbfc24409ed989&dession=01&mession=01&vessionName=&date={date}",
        f"http://push2ex.eastmoney.com/getTopicLongHuList?ut=7eea3edcaed734bea9cbfc24409ed989&dession=01&mession=01&vessionName=&TOP=50&date={date}",
    ]
    
    for url in urls:
        try:
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            req.add_header('Referer', 'https://www.eastmoney.com/')
            
            with urllib.request.urlopen(req, timeout=15) as r:
                content = r.read().decode('utf-8', errors='ignore')
                data = json.loads(content)
                
                result = []
                
                # 尝试解析新API格式
                if data.get("data") and isinstance(data["data"], list):
                    for item in data["data"]:
                        result.append({
                            "code": item.get("c", ""),
                            "name": item.get("n", ""),
                            "close": item.get("p", 0),
                            "change_pct": item.get("chg", 0),
                            "amount": item.get("amount", 0),
                            "jmr_netbuy": item.get("jmr_netbuy", 0),
                            "broker_netbuy": item.get("broker_netbuy", 0),
                        })
                    if result:
                        return result
                
                # 尝试老API格式
                if data.get("data") and data["data"].get("longhu"):
                    for item in data["data"]["longhu"]:
                        result.append({
                            "code": item.get("c", ""),
                            "name": item.get("n", ""),
                            "close": item.get("p", 0),
                            "change_pct": item.get("chg", 0),
                            "amount": item.get("amount", 0),
                            "jmr_netbuy": item.get("jmr_netbuy", 0),
                            "broker_netbuy": item.get("broker_netbuy", 0),
                        })
                    return result
                    
        except Exception as e:
            print(f"获取龙虎榜尝试失败: {e}")
            continue
    
    # 如果API都失败，返回模拟数据提示用户
    return []

def format_longhub_report(data: List[Dict], date: str = "") -> str:
    """格式化龙虎榜报告"""
    if not data:
        return "暂无龙虎榜数据"
    
    date_str = f"({date[:4]}-{date[4:6]}-{date[6:]})" if date else ""
    
    lines = [f"🐉 龙虎榜 {date_str}", "=" * 60, ""]
    
    # 按机构净买入排序
    sorted_data = sorted(data, key=lambda x: x.get("jmr_netbuy", 0), reverse=True)
    
    for i, item in enumerate(sorted_data[:15], 1):
        name = item.get("name", "")
        code = item.get("code", "")
        close = item.get("close", 0)
        change_pct = item.get("change_pct", 0)
        jmr = item.get("jmr_netbuy", 0)
        broker = item.get("broker_netbuy", 0)
        
        change_emoji = "🟢" if change_pct > 0 else "🔴" if change_pct < 0 else "🟡"
        
        # 格式化金额
        def fmt_amount(val):
            if abs(val) >= 100000000:
                return f"{val/100000000:.2f}亿"
            elif abs(val) >= 10000:
                return f"{val/10000:.2f}万"
            else:
                return f"{val:.0f}"
        
        jmr_emoji = "🟢" if jmr > 0 else "🔴"
        
        lines.append(f"{i}. {name} ({code})")
        lines.append(f"   {change_emoji} 收盘: {close:.2f} ({change_pct:+.2f}%)")
        lines.append(f"   🏛️ 机构净买入: {jmr_emoji} {fmt_amount(jmr)}")
        lines.append(f"   🏢 营业部净买入: {fmt_amount(broker)}")
        lines.append("")
    
    # 统计
    total_jmr = sum(x.get("jmr_netbuy", 0) for x in data)
    up_count = sum(1 for x in data if x.get("change_pct", 0) > 0)
    down_count = sum(1 for x in data if x.get("change_pct", 0) < 0)
    
    lines.append("=" * 60)
    lines.append(f"📊 统计: 上榜 {len(data)} 只 | 上涨 {up_count} | 下跌 {down_count}")
    lines.append(f"💰 机构净买入总额: {total_jmr/10000:.2f}万")
    
    return "\n".join(lines)


# ============ 北向资金功能 ============

def get_north_money() -> Optional[Dict]:
    """
    获取北向资金流向 (沪股通+深股通)
    """
    try:
        # 沪股通
        url1 = "https://push2.eastmoney.com/api/qt/stock/get?secid=1.000001&fields=f43,f44,f45,f46,f57,f58,f169,f170,f171,f173,f177,f178,f187,f188,f189,f190,f191,f192"
        # 深股通
        url2 = "https://push2.eastmoney.com/api/qt/stock/get?secid=0.399001&fields=f43,f44,f45,f46,f57,f58,f169,f170,f171,f173,f177,f178,f187,f188,f189,f190,f191,f192"
        
        results = {}
        
        for name, url in [("沪股通", url1), ("深股通", url2)]:
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req, timeout=10) as r:
                content = r.read().decode('utf-8', errors='ignore')
                data = json.loads(content)
                
                if data.get("data"):
                    d = data["data"]
                    results[name] = {
                        "name": name,
                        "net_inflow": d.get("f45", 0),  # 净流入
                        "buy_amount": d.get("f43", 0),  # 买入额
                        "sell_amount": d.get("f44", 0), # 卖出额
                        "turnover": d.get("f57", 0),    # 成交额
                    }
        
        if results:
            # 合并计算
            total_net = sum(r.get("net_inflow", 0) for r in results.values())
            return {
                "沪股通": results.get("沪股通", {}),
                "深股通": results.get("深股通", {}),
                "total_net_inflow": total_net,
                "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            }
            
    except Exception as e:
        print(f"获取北向资金失败: {e}")
    
    return None

def get_north_top_stocks() -> List[Dict]:
    """
    获取北向资金十大成交股
    """
    try:
        url = "https://push2ex.eastmoney.com/getTopicZTPool?ut=7eea3edcaed734bea9cbfc24409ed989&dession=01&mession=01&vessionName=&date=&mkt=2&TYPE=SZ"
        
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        req.add_header('Referer', 'https://www.eastmoney.com/')
        
        with urllib.request.urlopen(req, timeout=15) as r:
            content = r.read().decode('utf-8', errors='ignore')
            data = json.loads(content)
            
            result = []
            if data.get("data") and data["data"].get("pool"):
                for item in data["data"]["pool"][:10]:
                    result.append({
                        "code": item.get("c", ""),
                        "name": item.get("n", ""),
                        "close": item.get("p", 0),
                        "change_pct": item.get("chg", 0),
                        "net_amount": item.get("net", 0),  # 净买入
                    })
            
            return result
            
    except Exception as e:
        print(f"获取北向资金十大成交股失败: {e}")
    
    return []

def format_north_money_report(data: Dict, top_stocks: List[Dict] = None) -> str:
    """格式化北向资金报告"""
    if not data:
        return "暂无北向资金数据"
    
    lines = ["🇨🇳 北向资金动向", "=" * 50, ""]
    
    total_net = data.get("total_net_inflow", 0)
    total_emoji = "🟢" if total_net > 0 else "🔴"
    
    def fmt_money(val):
        if val is None or val == 0:
            return "0"
        val = float(val)
        if abs(val) >= 100000000:
            return f"{val/100000000:.2f}亿"
        elif abs(val) >= 10000:
            return f"{val/10000:.2f}万"
        else:
            return f"{val:.0f}元"
    
    # 沪股通
    if "沪股通" in data and data["沪股通"]:
        hk = data["沪股通"]
        hk_net = hk.get("net_inflow", 0) or 0
        hk_emoji = "🟢" if hk_net > 0 else "🔴"
        lines.append(f"📈 沪股通: {hk_emoji} 净流入 {fmt_money(hk_net)}")
        lines.append(f"   买入: {fmt_money(hk.get('buy_amount', 0))} | 卖出: {fmt_money(hk.get('sell_amount', 0))}")
        lines.append("")
    
    # 深股通
    if "深股通" in data and data["深股通"]:
        sz = data["深股通"]
        sz_net = sz.get("net_inflow", 0) or 0
        sz_emoji = "🟢" if sz_net > 0 else "🔴"
        lines.append(f"📈 深股通: {sz_emoji} 净流入 {fmt_money(sz_net)}")
        lines.append(f"   买入: {fmt_money(sz.get('buy_amount', 0))} | 卖出: {fmt_money(sz.get('sell_amount', 0))}")
        lines.append("")
    
    # 总计
    lines.append("=" * 50)
    lines.append(f"{total_emoji} 北向资金合计: 净流入 {fmt_money(total_net)}")
    lines.append(f"📅 更新时间: {data.get('time', '')}")
    
    # 十大成交股
    if top_stocks:
        lines.append("")
        lines.append("🇨🇳 十大成交股 (北向资金净买入):")
        lines.append("-" * 40)
        
        sorted_stocks = sorted(top_stocks, key=lambda x: x.get("net_amount", 0), reverse=True)
        
        for i, stock in enumerate(sorted_stocks[:10], 1):
            name = stock.get("name", "")
            code = stock.get("code", "")
            net = stock.get("net_amount", 0)
            change = stock.get("change_pct", 0)
            
            net_emoji = "🟢" if net > 0 else "🔴"
            change_emoji = "🟢" if change > 0 else "🔴" if change < 0 else "🟡"
            
            lines.append(f"{i}. {name} ({code})")
            lines.append(f"   {change_emoji} {change:+.2f}% | {net_emoji} 净买入: {fmt_money(net)}")
    
    return "\n".join(lines)


# ============ CLI 命令 ============

if __name__ == "__main__":
    # 设置UTF-8编码
    import sys
    if sys.stdout.encoding != 'utf-8':
        sys.stdout.reconfigure(encoding='utf-8')
    
    if len(sys.argv) < 2:
        print("资金/龙虎榜/北向资金 CLI")
        print("Usage: python stock_capital.py <command> [args]")
        print("Commands:")
        print("  capital <code>     - 查询个股资金流向")
        print("  sector <name>      - 查询板块资金流向 (芯片/新能源/医药/白酒/银行/房地产/军工)")
        print("  longhub [date]     - 查询龙虎榜 (date: YYYYMMDD, 默认今天)")
        print("  north              - 查询北向资金")
        print("  north-top          - 查询北向资金十大成交股")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    if cmd == "capital":
        if len(sys.argv) > 2:
            code = sys.argv[2]
            # 获取股票名称
            from stock_monitor import get_stock_quote
            quote = get_stock_quote(code)
            name = quote["name"] if quote else code
            
            data = get_capital_flow(code)
            print(format_capital_flow(data, name))
        else:
            print("用法: capital <code>")
    
    elif cmd == "sector":
        if len(sys.argv) > 2:
            name = sys.argv[2]
            data = get_sector_capital_flow(name)
            if data:
                print(format_capital_flow(data.get("data", {}), name))
            else:
                print(f"未找到板块: {name}")
        else:
            print("用法: sector <板块名称>")
    
    elif cmd == "longhub":
        date = sys.argv[2] if len(sys.argv) > 2 else None
        data = get_longhub_list(date)
        print(format_longhub_report(data, date or datetime.now().strftime("%Y%m%d")))
    
    elif cmd == "north":
        data = get_north_money()
        top_stocks = get_north_top_stocks()
        print(format_north_money_report(data, top_stocks))
    
    elif cmd == "north-top":
        data = get_north_top_stocks()
        if data:
            for i, s in enumerate(data, 1):
                print(f"{i}. {s.get('name')} ({s.get('code')}) - 净买入: {s.get('net_amount', 0)}")
        else:
            print("暂无数据")
    
    else:
        print("未知命令")


path: scripts/stock_monitor.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
if sys.stdout.encoding != 'utf-8':
    sys.stdout.reconfigure(encoding='utf-8')

"""
Stock Monitor - 股票监控分析脚本
功能：获取实时行情、K线数据、技术指标分析、信号提醒
支持：新浪财经API + 腾讯财经API
"""

import json
import os
import sys
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional

# 配置文件路径
CONFIG_FILE = os.path.expanduser("~/.openclaw/stock-pool.json")
POSITIONS_FILE = os.path.expanduser("~/.openclaw/stock-positions.json")
TRADES_FILE = os.path.expanduser("~/.openclaw/stock-trades.json")

# ============ 资讯新闻获取 ============

def get_stock_news(code: str, limit: int = 10) -> List[Dict]:
    """获取股票最新资讯（东方财富+新浪，多来源合并）"""
    news_results = []
    seen_titles = set()
    
    # 判断是否为港股（4-5位数字，以0开头）
    is_hk = len(code) >= 4 and len(code) <= 5 and code.startswith("0")
    
    if is_hk:
        # 港股新闻源：东方财富 + 新浪财经港股
        sources = [
            # 东方财富港股资讯 (JSONP)
            (f"https://search-api-web.eastmoney.com/search/jsonp?param={{\"uid\":\"\",\"keyword\":\"{code}\",\"type\":[\"stockNote\"],\"client\":\"web\",\"clientType\":\"pc\",\"clientVersion\":\"curr\",\"param\":{{\"market\":\"hk\",\"start\":\"0\",\"count\":\"{limit}\"}}}}", "eastmoney_hk", "https://www.eastmoney.com/"),
            # 新浪财经港股公司新闻 (HTML，需解析)
            (f"https://finance.sina.com.cn/realstock/company/hk{code}/nc.shtml", "sina_hk", "https://finance.sina.com.cn/"),
        ]
    else:
        sources = [
            # 东方财富公告
            ("https://np-anotice-stock.eastmoney.com/api/security/ann?cb=&sr=-1&page_size={limit}&page_index=1&ann_type=SHA%2CSZA&client_source=web&stock_list=" + code, "eastmoney", "https://www.eastmoney.com/"),
            # 东方财富资讯新闻
            ("https://np-listapi.eastmoney.com/comm/web/getNewsByColumn?cb=&client=web&bType=1&start=0&pageSize=" + str(limit) + "&keyword=" + code, "eastmoney_news", "https://www.eastmoney.com/"),
        ]
    
    for url, src, ref in sources:
        try:
            import urllib.request
            if "{limit}" in url:
                url = url.format(limit=limit)
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Referer': ref,
                'Accept': 'application/json, text/html'
            })
            with urllib.request.urlopen(req, timeout=10) as r:
                raw = r.read().decode('utf-8', errors='ignore')
                
                # 解析JSON或类JSON
                try:
                    if src == "eastmoney_hk":
                        # 东方财富港股返回格式:callback([...])
                        raw = raw.strip()
                        if raw.startswith('jQuery') or raw.startswith('callback'):
                            start = raw.find('[')
                            end = raw.rfind(']') + 1
                            if start >= 0 and end > start:
                                raw = raw[start:end]
                        data = json.loads(raw)
                        if isinstance(data, list):
                            for item in data[:limit]:
                                title = item.get("title", item.get("t", "")).strip()
                                if title and title not in seen_titles:
                                    seen_titles.add(title)
                                    news_results.append({
                                        "title": title,
                                        "date": item.get("datetime", item.get("time", "")),
                                        "source": item.get("src", "东方财富港股"),
                                        "importance": _calc_news_importance(title),
                                        "url": item.get("url", ref)
                                    })
                    elif src == "sina_hk":
                        # 新浪港股返回HTML，提取新闻标题
                        import re
                        titles = re.findall(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>', raw)
                        for url_path, title_text in titles[:limit]:
                            title = title_text.strip()
                            if title and len(title) > 5 and title not in seen_titles:
                                seen_titles.add(title)
                                news_results.append({
                                    "title": title,
                                    "date": "",
                                    "source": "新浪财经港股",
                                    "importance": _calc_news_importance(title),
                                    "url": url_path if url_path.startswith('http') else f"https://finance.sina.com.cn{url_path}"
                                })
                    elif src == "eastmoney" and isinstance(json.loads(raw) if raw.startswith('{') else {}, dict):
                        json_data = json.loads(raw)
                        if json_data.get("data"):
                            for item in json_data["data"].get("list", [])[:limit]:
                                title = item.get("title", "").strip()
                                if title and title not in seen_titles:
                                    seen_titles.add(title)
                                    news_results.append({
                                        "title": title,
                                        "date": item.get("notice_date", ""),
                                        "source": item.get("src", "东方财富"),
                                        "importance": _calc_news_importance(title),
                                        "url": f"https://www.eastmoney.com/"
                                    })
                    elif src == "eastmoney_news":
                        json_data = json.loads(raw)
                        if isinstance(json_data, dict) and json_data.get("data"):
                            items = json_data["data"].get("news", json_data["data"].get("list", []))
                            for item in items[:limit]:
                                title = item.get("title", item.get("s", "")).strip()
                                if title and title not in seen_titles:
                                    seen_titles.add(title)
                                    news_results.append({
                                        "title": title,
                                        "date": item.get("datetime", item.get("time", "")),
                                        "source": item.get("src", "东方财富资讯"),
                                        "importance": _calc_news_importance(title),
                                        "url": item.get("url", "https://www.eastmoney.com/")
                                    })
                except json.JSONDecodeError:
                    # HTML页面，降级处理
                    if src == "sina_hk":
                        import re
                        titles = re.findall(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>', raw)
                        for url_path, title_text in titles[:limit]:
                            title = title_text.strip()
                            if title and len(title) > 5 and title not in seen_titles:
                                seen_titles.add(title)
                                news_results.append({
                                    "title": title,
                                    "date": "",
                                    "source": "新浪财经港股",
                                    "importance": _calc_news_importance(title),
                                    "url": url_path if url_path.startswith('http') else f"https://finance.sina.com.cn{url_path}"
                                })
                    pass
        except Exception as e:
            pass
    
    return news_results[:limit]

def _calc_news_importance(title: str) -> int:
    """计算新闻重要程度 1-5"""
    important_keywords = [
        (5, ["重组", "收购", "并购", "定增", "发行", "上市", "退市", "暂停上市"]),
        (4, ["业绩", "盈利", "亏损", "增长", "下降", "超预期", "不及预期", "下调", "上调"]),
        (3, ["增持", "减持", "回购", "分红", "送转", "摘帽", "ST"]),
        (2, ["合作", "签约", "中标", "订单", "投资", "扩建", "投产"]),
        (1, ["调研", "会议", "公告", "说明"]),
    ]
    for score, keywords in important_keywords:
        for kw in keywords:
            if kw in title:
                return score
    return 1

# 情感分析词典
POSITIVE_WORDS = [
    # 核心利好
    "增持", "买入", "推荐", "看好", "超预期", "突破", "上涨", "利好",
    "业绩增长", "盈利", "增长", "创新高", "景气", "复苏", "布局",
    "机会", "低估", "价值", "成长", "景气上行", "订单", "签约", "合作",
    "中标", "景气提升", "需求旺盛", "产能扩张", "市场份额",
    # 财务表现
    "净利润增长", "营收增长", "毛利率提升", "扭亏为盈", "大幅增长",
    "业绩翻倍", "净利润翻番", "营收创纪录", "利润创历史",
    # 业务发展
    "新签订单", "中标大单", "市场份额扩大", "行业领先地位",
    "技术突破", "研发成功", "产品发布", "上市申请获批",
    "产能扩张完成", "新产能投产", "满产", "扩产",
    # 资本运作
    "回购", "增持计划", "员工持股", "股权激励",
    "高分红", "派息增加", "送转股份", "定向增发获批",
    "战略投资", "引入战投", "混改", "分拆上市",
    # 政策利好
    "政策支持", "补贴", "税收优惠", "产业政策利好",
    # 评级上调
    "评级上调", "买入评级", "强烈推荐", "目标价上调",
    "上调盈利预测", "业绩指引上调",
]

NEGATIVE_WORDS = [
    # 核心利空
    "减持", "卖出", "看空", "下调", "不及预期", "下跌", "利空", "亏损",
    "业绩下降", "风险", "警示", "调查", "处罚", "诉讼", "债务", "违约",
    "商誉", "减值", "库存", "积压", "竞争加剧", "价格战", "需求疲软",
    "终止", "失败", "违规", "造假", "退市", "ST", "*ST",
    # 财务风险
    "净利润下降", "营收下滑", "亏损扩大", "大幅下跌",
    "业绩变脸", "盈利预警", "商誉暴雷", "资产减值",
    "债务逾期", "流动性紧张", "资金链断裂", "无法偿还债务",
    # 监管调查
    "立案调查", "监管函", "行政处罚", "市场禁入",
    "涉嫌信息披露违规", "涉嫌操纵股价", "内幕交易",
    "财务造假", "虚假陈述", "欺诈发行",
    # 业务风险
    "订单减少", "合同终止", "合作破裂", "被索赔",
    "产品召回", "安全事故", "环保问题", "停产整顿",
    "产能过剩", "库存积压", "销售下滑", "竞争失势",
    # 资本运作负面
    "回购取消", "增持终止", "减持计划", "定增失败",
    "收购终止", "重组失败", "分拆终止",
    # 政策风险
    "政策收紧", "监管趋严", "行业整顿", "整改",
    # 评级下调
    "评级下调", "卖出评级", "目标价下调", "下调盈利预测",
    "下调评级", "审慎推荐", "减持评级",
]

def analyze_sentiment(news_list: List[Dict]) -> Dict:
    """分析资讯情感倾向（带重要程度权重）"""
    if not news_list:
        return {
            "sentiment": "无资讯",
            "positive_count": 0,
            "negative_count": 0,
            "news_titles": [],
            "weighted_score": 0,
            "impact_level": "无"
        }
    
    positive_score = 0
    negative_score = 0
    news_summary = []
    impact_details = []
    
    for news in news_list:
        title = news.get("title", "")
        importance = news.get("importance", 1)  # 1-5级
        combined = title
        
        pos_found = []
        neg_found = []
        
        for word in POSITIVE_WORDS:
            if word in combined:
                pos_found.append(word)
        
        for word in NEGATIVE_WORDS:
            if word in combined:
                neg_found.append(word)
        
        # 带权重计分
        if len(pos_found) > len(neg_found):
            positive_score += importance * len(pos_found)
        elif len(neg_found) > len(pos_found):
            negative_score += importance * len(neg_found)
        
        if pos_found or neg_found:
            sentiment_icon = "🟢" if len(pos_found) > len(neg_found) else "🔴"
            impact_icon = "🔴" * importance if len(neg_found) > len(pos_found) else "🟢" * importance
            short_title = title[:35] + "..." if len(title) > 35 else title
            news_summary.append(f"{sentiment_icon}{impact_icon} {short_title}")
            impact_details.append({
                "title": short_title,
                "impact": importance,
                "sentiment": "positive" if len(pos_found) > len(neg_found) else "negative"
            })
    
    total = positive_score + negative_score
    # 计算加权情感比
    if total > 0:
        ratio = positive_score / total
        if ratio >= 0.7:
            sentiment = "偏多 🟢"
        elif ratio <= 0.3:
            sentiment = "偏空 🔴"
        else:
            sentiment = "中性 📊"
    else:
        sentiment = "中性 📊"
    
    # 影响级别：高权重利空/利好数量
    high_impact_neg = sum(1 for d in impact_details if d["impact"] >= 4 and d["sentiment"] == "negative")
    high_impact_pos = sum(1 for d in impact_details if d["impact"] >= 4 and d["sentiment"] == "positive")
    
    if high_impact_neg >= 2:
        impact_level = "⚠️ 高度警惕"
    elif high_impact_pos >= 2:
        impact_level = "✨ 高度关注"
    elif positive_score > negative_score * 2:
        impact_level = "📈 偏多"
    elif negative_score > positive_score * 2:
        impact_level = "📉 偏空"
    else:
        impact_level = "➡️ 中性"
    
    return {
        "sentiment": sentiment,
        "positive_count": positive_score,
        "negative_count": negative_score,
        "news_titles": news_summary[:6],
        "weighted_score": positive_score - negative_score,
        "impact_level": impact_level,
        "high_impact_count": high_impact_neg + high_impact_pos
    }

# ============ K线数据获取 ============

def get_kline_eastmoney(code: str, days: int = 60) -> List[Dict]:
    """使用东方财富获取K线数据（仅支持A股）"""
    import urllib.request
    
    try:
        if code.startswith("6"):
            secid = f"1.{code}"
        else:
            secid = f"0.{code}"
        
        end_date = datetime.now().strftime("%Y%m%d")
        url = f"http://push2his.eastmoney.com/api/qt/stock/kline/get?secid={secid}&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt=101&fqt=0&beg=20250101&end={end_date}"
        
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        req.add_header('Referer', 'https://www.eastmoney.com/')
        
        with urllib.request.urlopen(req, timeout=15) as r:
            content = r.read().decode('utf-8', errors='ignore')
            data = json.loads(content)
            
            if 'data' not in data or not data.get('data'):
                return None
            
            klines = data['data']['klines']
            result = []
            
            for kline in klines[-days:]:
                parts = kline.split(',')
                if len(parts) >= 6:
                    result.append({
                        "date": parts[0],
                        "open": float(parts[1]),
                        "close": float(parts[2]),
                        "high": float(parts[3]),
                        "low": float(parts[4]),
                        "volume": int(parts[5])
                    })
            
            return result if result else None
            
    except Exception as e:
        print(f"东方财富K线获取失败: {code}, {e}")
        return None

def get_kline_tencent_hk(code: str, days: int = 60) -> List[Dict]:
    """使用腾讯API获取港股K线数据"""
    import urllib.request
    
    try:
        # 港股代码：如果已带hk前缀则直接使用，否则补齐
        if code.startswith("hk"):
            full_code = code
        else:
            full_code = f"hk{code.zfill(5)}"
        url = f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={full_code},day,,,{days},qfq"
        
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        
        with urllib.request.urlopen(req, timeout=15) as r:
            content = r.read().decode('utf-8', errors='ignore')
            data = json.loads(content)
            
            # 腾讯返回格式: data -> hkXXXXX -> day
            if 'data' not in data:
                return None
            
            stock_data = data['data']
            if full_code not in stock_data:
                return None
            
            # K线数据直接在 day 字段
            kline_data = stock_data[full_code].get('day')
            if not kline_data:
                return None
            
            result = []
            for kline in kline_data[-days:]:
                if len(kline) >= 6:
                    result.append({
                        "date": kline[0],
                        "open": float(kline[1]),
                        "close": float(kline[2]),
                        "high": float(kline[3]),
                        "low": float(kline[4]),
                        "volume": int(float(kline[5])) if kline[5] else 0
                    })
            
            return result if result else None
            
    except Exception as e:
        print(f"腾讯港股K线获取失败: {code}, {e}")
        return None

def get_kline_sina(code: str, days: int = 60) -> List[Dict]:
    """使用新浪财经获取K线数据"""
    import requests
    
    try:
        if code.startswith("6"):
            symbol = f"sh{code}"
        elif code.startswith(("0", "3")):
            symbol = f"sz{code}"
        else:
            symbol = code
        
        url = f"https://quotes.sina.cn/cn/api/jsonp.php/var%20_{symbol}=/CN_MarketDataService.getKLineData?symbol={symbol}&scale=240&ma=no&datalen=1024"
        
        resp = requests.get(url, timeout=10)
        resp.encoding = 'utf-8'
        
        text = resp.text
        start = text.find('[')
        end = text.rfind(']') + 1
        
        if start < 0 or end <= 0:
            return get_kline_simulated(code, days)
        
        json_str = text[start:end]
        data = json.loads(json_str)
        
        klines = []
        for item in data[-days:]:
            klines.append({
                "date": item.get("day", ""),
                "open": float(item.get("open", 0)),
                "close": float(item.get("close", 0)),
                "high": float(item.get("high", 0)),
                "low": float(item.get("low", 0)),
                "volume": int(item.get("volume", 0))
            })
        
        return klines
        
    except Exception as e:
        print(f"新浪K线获取失败: {code}, {e}")
        return get_kline_simulated(code, days)

def get_kline_simulated(code: str, days: int = 60) -> List[Dict]:
    """模拟K线数据 (当无法获取真实数据时)"""
    import random
    quotes = []
    base_price = 50.0
    
    for i in range(days):
        date = (datetime.now().date()).strftime("%Y-%m-%d")
        open_p = base_price + random.uniform(-2, 2)
        close_p = open_p + random.uniform(-1.5, 1.5)
        high_p = max(open_p, close_p) + random.uniform(0, 1)
        low_p = min(open_p, close_p) - random.uniform(0, 1)
        volume = random.randint(1000000, 10000000)
        
        quotes.append({
            "date": date,
            "open": round(open_p, 2),
            "close": round(close_p, 2),
            "high": round(high_p, 2),
            "low": round(low_p, 2),
            "volume": volume
        })
        base_price = close_p
    
    return quotes

def get_stock_quote(code: str) -> Optional[Dict]:
    """获取股票实时行情 - 腾讯API"""
    import urllib.request
    import re
    
    try:
        # 判断股票市场
        if code.startswith("6") and len(code) == 6:
            full_code = f"sh{code}"
        elif code.startswith(("0", "3")) and len(code) == 6:
            full_code = f"sz{code}"
        elif code.startswith("8") and len(code) == 6:
            full_code = f"sh{code}"
        elif code.startswith(("sh", "sz")):
            full_code = code
        elif code.startswith("hk"):
            # 港股已带前缀，直接使用
            full_code = code
        else:
            # 港股：补齐5位（处理纯数字如 00981）
            full_code = f"hk{code.zfill(5)}"
        
        url = f"https://qt.gtimg.cn/q={full_code}"
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        
        with urllib.request.urlopen(req, timeout=10) as r:
            content = r.read().decode('gbk', errors='ignore')
            
            if 'none_match' in content or not content:
                return None
            
            match = re.search(r'="([^"]+)"', content)
            if not match:
                return None
            
            parts = match.group(1).split('~')
            
            if len(parts) < 33:
                return None
            
            # 腾讯字段: parts[3]=当前价, parts[31]=涨跌, parts[32]=涨跌幅
            price = float(parts[3]) if parts[3] and parts[3] != '-' else 0
            change = float(parts[31]) if parts[31] and parts[31] != '-' else 0
            pct = float(parts[32]) if parts[32] and parts[32] != '-' else 0
            
            # 安全转换volume
            try:
                volume = int(float(parts[6])) if parts[6] and parts[6] != '-' else 0
            except:
                volume = 0
            
            try:
                amount = float(parts[7]) if parts[7] and parts[7] != '-' else 0
            except:
                amount = 0
            
            return {
                "code": code,
                "name": parts[1] if parts[1] else code,
                "price": price,
                "change": change,
                "change_pct": pct,
                "volume": volume,
                "amount": amount,
                "open": price - change if price > 0 else 0,
                "high": float(parts[11]) if len(parts) > 11 and parts[11] and parts[11] != '-' else 0,
                "low": float(parts[12]) if len(parts) > 12 and parts[12] and parts[12] != '-' else 0,
            }
            
    except Exception as e:
        print(f"获取行情失败: {code}, {e}")
        return None

def get_market_index() -> List[Dict]:
    """获取大盘指数实时行情 - 腾讯API"""
    import urllib.request
    import re

    indices = [
        ('sh000001', '上证指数'),
        ('sz399001', '深证成指'),
        ('sz399006', '创业板指'),
        ('sh000300', '沪深300'),
        ('sh000016', '上证50'),
        ('sz399905', '中证500'),
        # 港股指数
        ('hkHSI', '恒生指数'),
        ('hkHSTECH', '恒生科技'),
        ('hkHSCEI', '恒生国企'),
    ]

    results = []
    for code, name in indices:
        try:
            url = f'https://qt.gtimg.cn/q={code}'
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req, timeout=10) as r:
                content = r.read().decode('gbk', errors='ignore')
                match = re.search(r'="([^"]+)"', content)
                if not match:
                    continue
                parts = match.group(1).split('~')
                if len(parts) < 33:
                    continue
                price = float(parts[3]) if parts[3] and parts[3] != '-' else 0
                change = float(parts[31]) if parts[31] and parts[31] != '-' else 0
                pct = float(parts[32]) if parts[32] and parts[32] != '-' else 0
                results.append({
                    'name': name,
                    'code': code,
                    'price': round(price, 2),
                    'change': round(change, 2),
                    'pct': round(pct, 2)
                })
        except Exception as e:
            print(f"获取指数失败: {code}, {e}")
    return results

# ============ 技术指标计算函数 ============

def calculate_ma(prices: List[float], period: int) -> float:
    """计算移动平均线"""
    if len(prices) < period:
        return sum(prices) / len(prices) if prices else 0
    return sum(prices[-period:]) / period

def calculate_ema(prices: List[float], period: int) -> float:
    """计算指数移动平均线"""
    if len(prices) < period:
        return sum(prices) / len(prices) if prices else 0
    
    ema = prices[0]
    multiplier = 2 / (period + 1)
    
    for price in prices[1:]:
        ema = (price - ema) * multiplier + ema
    
    return ema

def calculate_macd(prices: List[float]) -> Dict:
    """计算MACD指标"""
    if len(prices) < 26:
        return {"dif": 0, "dea": 0, "macd": 0, "signal": "数据不足"}
    
    ema12 = calculate_ema(prices, 12)
    ema26 = calculate_ema(prices, 26)
    
    dif = ema12 - ema26
    dea = dif * 0.9
    macd = (dif - dea) * 2
    
    if dif > dea:
        signal = "金叉 ✓ 买入"
    else:
        signal = "死叉 ✗ 卖出"
    
    return {"dif": round(dif, 4), "dea": round(dea, 4), "macd": round(macd, 4), "signal": signal}

def calculate_kdj(prices: List[float], high: List[float], low: List[float], period: int = 9) -> Dict:
    """计算KDJ指标"""
    if len(prices) < period + 1:
        return {"k": 50, "d": 50, "j": 50, "signal": "数据不足"}
    
    rsv = []
    for i in range(period, len(prices)):
        window_high = max(high[i-period:i])
        window_low = min(low[i-period:i])
        
        if window_high == window_low:
            rsv.append(50)
        else:
            rsv.append((prices[i-1] - window_low) / (window_high - window_low) * 100)
    
    if not rsv:
        return {"k": 50, "d": 50, "j": 50, "signal": "数据不足"}
    
    k = sum(rsv[-period:]) / period
    d = k * 0.9 + 50 * 0.1
    j = 3 * k - 2 * d
    
    if k > 80:
        signal = "超买 ⚠️"
    elif k < 20:
        signal = "超卖 ✓"
    elif k > d:
        signal = "金叉 ✓"
    else:
        signal = "死叉 ✗"
    
    return {"k": round(k, 2), "d": round(d, 2), "j": round(j, 2), "signal": signal}

def calculate_rsi(prices: List[float], period: int = 14) -> Dict:
    """计算RSI指标"""
    if len(prices) < period + 1:
        return {"rsi6": 50, "rsi12": 50, "rsi24": 50, "signal": "数据不足"}
    
    def rsi_single(p, per):
        if len(p) < per + 1:
            return 50
        gains = []
        losses = []
        for i in range(1, len(p)):
            change = p[i] - p[i-1]
            if change > 0:
                gains.append(change)
                losses.append(0)
            else:
                gains.append(0)
                losses.append(abs(change))
        
        avg_gain = sum(gains[-per:]) / per
        avg_loss = sum(losses[-per:]) / per
        
        if avg_loss == 0:
            return 100
        rs = avg_gain / avg_loss
        return 100 - (100 / (1 + rs))
    
    rsi6 = rsi_single(prices, 6)
    rsi12 = rsi_single(prices, 12)
    rsi24 = rsi_single(prices, 24)
    
    if rsi6 > 70:
        signal = "超买 ⚠️"
    elif rsi6 < 30:
        signal = "超卖 ✓"
    else:
        signal = "正常"
    
    return {"rsi6": round(rsi6, 2), "rsi12": round(rsi12, 2), "rsi24": round(rsi24, 2), "signal": signal}

def calculate_boll(prices: List[float], period: int = 20) -> Dict:
    """计算布林带指标"""
    if len(prices) < period:
        return {"upper": 0, "middle": 0, "lower": 0, "signal": "数据不足"}
    
    recent = prices[-period:]
    middle = sum(recent) / period
    
    variance = sum((x - middle) ** 2 for x in recent) / period
    std = variance ** 0.5
    
    upper = middle + 2 * std
    lower = middle - 2 * std
    
    current_price = prices[-1]
    
    if current_price > upper:
        signal = "突破上轨 ⚠️"
    elif current_price < lower:
        signal = "突破下轨 ✓"
    elif current_price > middle:
        signal = "中轨上方"
    else:
        signal = "中轨下方"
    
    return {"upper": round(upper, 2), "middle": round(middle, 2), "lower": round(lower, 2), "signal": signal}

def calculate_obv(closes: List[float], volumes: List[int]) -> Dict:
    """计算OBV (能量潮指标)"""
    if len(closes) < 2 or len(volumes) < 2:
        return {"obv": 0, "signal": "数据不足"}
    
    obv = 0
    obv_history = []
    
    for i in range(1, len(closes)):
        if closes[i] > closes[i-1]:
            obv += volumes[i]
        elif closes[i] < closes[i-1]:
            obv -= volumes[i]
        obv_history.append(obv)
    
    if len(obv_history) < 10:
        return {"obv": obv, "signal": "数据不足"}
    
    # OBV均线
    obv_ma = sum(obv_history[-10:]) / 10
    
    current_obv = obv_history[-1]
    
    if current_obv > obv_ma:
        signal = "OBV上行 ✓"
    else:
        signal = "OBV下行 ✗"
    
    # 检查OBV背离
    price_trend = closes[-1] - closes[-10]
    obv_trend = obv_history[-1] - obv_history[-10]
    
    if price_trend > 0 and obv_trend < 0:
        signal = "顶背离 ⚠️ 卖出"
    elif price_trend < 0 and obv_trend > 0:
        signal = "底背离 ✓ 买入"
    
    return {"obv": round(obv, 2), "obv_ma10": round(obv_ma, 2), "signal": signal}

def calculate_dmi(highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> Dict:
    """计算DMI (动向指标) / ADX"""
    if len(closes) < period + 1:
        return {"plus_di": 0, "minus_di": 0, "adx": 0, "signal": "数据不足"}
    
    # 计算真实波幅TR
    tr_list = []
    plus_dm_list = []
    minus_dm_list = []
    
    for i in range(1, len(closes)):
        high = highs[i]
        low = lows[i]
        prev_close = closes[i-1]
        
        tr = max(
            high - low,
            abs(high - prev_close),
            abs(low - prev_close)
        )
        tr_list.append(tr)
        
        plus_dm = max(high - highs[i-1], 0) if i > 0 else 0
        minus_dm = max(lows[i-1] - low, 0) if i > 0 else 0
        
        # 避免DM为负
        plus_dm = max(plus_dm - minus_dm, 0) if i > 0 else 0
        minus_dm = max(minus_dm - plus_dm, 0) if i > 0 else 0
        
        plus_dm_list.append(plus_dm)
        minus_dm_list.append(minus_dm)
    
    if len(tr_list) < period:
        return {"plus_di": 0, "minus_di": 0, "adx": 0, "signal": "数据不足"}
    
    # 计算平滑值
    smooth_tr = sum(tr_list[:period])
    smooth_plus_dm = sum(plus_dm_list[:period])
    smooth_minus_dm = sum(minus_dm_list[:period])
    
    for i in range(period, len(tr_list)):
        smooth_tr = smooth_tr - smooth_tr / period + tr_list[i]
        smooth_plus_dm = smooth_plus_dm - smooth_plus_dm / period + plus_dm_list[i]
        smooth_minus_dm = smooth_minus_dm - smooth_minus_dm / period + minus_dm_list[i]
    
    # 计算DI
    plus_di = (smooth_plus_dm / smooth_tr * 100) if smooth_tr > 0 else 0
    minus_di = (smooth_minus_dm / smooth_tr * 100) if smooth_tr > 0 else 0
    
    # 计算ADX
    dx = abs(plus_di - minus_di) / (plus_di + minus_di) * 100 if (plus_di + minus_di) > 0 else 0
    
    adx_list = [dx]
    for i in range(period, min(len(tr_list), 50)):
        adx_val = sum(adx_list[-period:]) / period
        adx_list.append(adx_val)
    
    adx = adx_list[-1] if adx_list else 0
    
    # 信号判断
    if plus_di > minus_di and adx > 25:
        signal = "多头趋势 ✓"
    elif minus_di > plus_di and adx > 25:
        signal = "空头趋势 ✗"
    elif adx < 20:
        signal = "震荡整理"
    else:
        signal = "趋势不明"
    
    return {
        "plus_di": round(plus_di, 2),
        "minus_di": round(minus_di, 2),
        "adx": round(adx, 2),
        "signal": signal
    }

def calculate_wr(highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> Dict:
    """计算威廉指标 (Williams %R)"""
    if len(closes) < period:
        return {"wr": 50, "signal": "数据不足"}
    
    highest = max(highs[-period:])
    lowest = min(lows[-period:])
    current = closes[-1]
    
    if highest == lowest:
        wr = 50
    else:
        wr = (highest - current) / (highest - lowest) * 100
    
    if wr < 20:
        signal = "超买 ⚠️"
    elif wr > 80:
        signal = "超卖 ✓"
    else:
        signal = "正常"
    
    return {"wr": round(wr, 2), "signal": signal}

def analyze_stock(code: str) -> Dict:
    """分析股票并返回技术指标"""
    quote = get_stock_quote(code)
    if not quote:
        return {"error": f"无法获取 {code} 的行情"}
    
    # 校验代码/名称映射是否一致
    validation_warning = validate_stock_mapping(code, quote)
    if validation_warning:
        print(validation_warning)
    
    # 判断是否为港股（使用更新后的函数）
    is_hk = is_hk_stock(code)
    
    # 尝试获取真实K线数据
    if is_hk:
        # 港股使用腾讯API
        klines = get_kline_tencent_hk(code, 60)
    else:
        # A股使用东方财富优先，新浪备用
        klines = get_kline_eastmoney(code, 60)
        if not klines or len(klines) < 10:
            klines = get_kline_sina(code, 60)
    
    if not klines or len(klines) < 10:
        return {"error": f"无法获取 {code} 的K线数据"}
    
    # 获取资讯并分析情感
    sentiment = {"sentiment": "暂无", "positive_count": 0, "negative_count": 0, "news_titles": []}
    news = get_stock_news(code, 10)
    sentiment = analyze_sentiment(news)
    
    closes = [k['close'] for k in klines]
    highs = [k['high'] for k in klines]
    lows = [k['low'] for k in klines]
    volumes = [k['volume'] for k in klines]
    latest_kline = klines[-1]
    
    # 基础均线
    ma5 = calculate_ma(closes, 5)
    ma10 = calculate_ma(closes, 10)
    ma20 = calculate_ma(closes, 20)
    ma60 = calculate_ma(closes, 60) if len(closes) >= 60 else None
    
    # 技术指标
    macd = calculate_macd(closes)
    kdj = calculate_kdj(closes, highs, lows)
    rsi = calculate_rsi(closes)
    boll = calculate_boll(closes)
    obv = calculate_obv(closes, volumes)
    dmi = calculate_dmi(highs, lows, closes)
    wr = calculate_wr(highs, lows, closes)
    
    # 生成信号
    signals = []
    buy_score = 0
    sell_score = 0
    
    # MA信号
    if ma5 > ma10:
        signals.append(f"MA5({ma5:.2})>MA10({ma10:.2}) 金叉 ✓")
        buy_score += 1
    else:
        signals.append(f"MA5({ma5:.2})<MA10({ma10:.2}) 死叉 ✗")
        sell_score += 1
    
    if ma10 > ma20:
        signals.append("短期均线多头排列 ✓")
        buy_score += 1
    else:
        signals.append("短期均线空头排列 ✗")
        sell_score += 1
    
    # MACD信号
    if "金叉" in macd["signal"]:
        buy_score += 2
    else:
        sell_score += 2
    signals.append(f"MACD: {macd['signal']}")
    
    # KDJ信号
    if "超卖" in kdj["signal"] or "金叉" in kdj["signal"]:
        buy_score += 1
    elif "超买" in kdj["signal"] or "死叉" in kdj["signal"]:
        sell_score += 1
    signals.append(f"KDJ: {kdj['signal']}")
    
    # RSI信号
    signals.append(f"RSI: {rsi['signal']}")
    if rsi["rsi6"] < 30:
        buy_score += 1
    elif rsi["rsi6"] > 70:
        sell_score += 1
    
    # BOLL信号
    signals.append(f"BOLL: {boll['signal']}")
    
    # OBV信号
    signals.append(f"OBV: {obv['signal']}")
    if "底背离" in obv["signal"]:
        buy_score += 2
    elif "顶背离" in obv["signal"]:
        sell_score += 2
    
    # DMI信号
    signals.append(f"DMI: {dmi['signal']}")
    if "多头" in dmi["signal"]:
        buy_score += 2
    elif "空头" in dmi["signal"]:
        sell_score += 2
    
    # WR信号
    signals.append(f"WR: {wr['signal']}")
    if wr["signal"] == "超卖 ✓":
        buy_score += 1
    elif wr["signal"] == "超买 ⚠️":
        sell_score += 1
    
    # 资讯情感信号 (消息面权重)
    impact = sentiment.get("impact_level", "中性")
    signals.append(f"📰 资讯情感: {sentiment['sentiment']} | 影响:{impact} | 积极{sentiment['positive_count']}分/消极{sentiment['negative_count']}分")
    if sentiment["positive_count"] > sentiment["negative_count"] + 2:
        buy_score += 3
    elif sentiment["negative_count"] > sentiment["positive_count"] + 2:
        sell_score += 3
    elif sentiment["positive_count"] > sentiment["negative_count"]:
        buy_score += 1
    elif sentiment["negative_count"] > sentiment["positive_count"]:
        sell_score += 1
    
    # 综合判断
    if buy_score > sell_score + 3:
        overall = "强烈买入 ✓✓"
    elif buy_score > sell_score:
        overall = "建议买入 ✓"
    elif sell_score > buy_score + 3:
        overall = "强烈卖出 ✗✗"
    elif sell_score > buy_score:
        overall = "建议卖出 ✗"
    else:
        overall = "观望"
    
    return {
        "code": code,
        "name": quote["name"],
        "price": quote["price"],
        "change": quote["change"],
        "change_pct": quote["change_pct"],
        "kline_date": latest_kline["date"],
        "indicators": {
            "ma": {
                "ma5": round(ma5, 2),
                "ma10": round(ma10, 2),
                "ma20": round(ma20, 2),
                "ma60": round(ma60, 2) if ma60 else None
            },
            "macd": macd,
            "kdj": kdj,
            "rsi": rsi,
            "boll": boll,
            "obv": obv,
            "dmi": dmi,
            "wr": wr
        },
        "signals": signals,
        "overall": overall,
        "score": {"buy": buy_score, "sell": sell_score},
        "sentiment": sentiment,
        "klines": klines
    }

# ============ 持仓管理（智能成本计算） ============

def load_positions() -> Dict:
    """加载持仓"""
    if os.path.exists(POSITIONS_FILE):
        with open(POSITIONS_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return data.get("positions", {})
    return {}

def save_positions(positions: Dict):
    """保存持仓"""
    with open(POSITIONS_FILE, 'w', encoding='utf-8') as f:
        json.dump({"positions": positions, "updated": datetime.now().isoformat()}, f, ensure_ascii=False, indent=2)

def recalculate_position_from_trades(code: str) -> Optional[Dict]:
    """
    根据交易记录重新计算持仓成本（纯FIFO批次计算法）
    - 维护买入批次队列，按时间顺序处理
    - 卖出时从最早买入批次开始扣除
    - 持仓成本 = 剩余批次总成本 / 剩余股数
    返回：{quantity, cost_price} 或 None（无持仓）
    """
    trades = load_trades()
    code_trades = [t for t in trades if str(t.get("code")) == str(code)]
    # 按时间顺序排序
    code_trades.sort(key=lambda x: x.get("date", ""))

    # 买入批次队列：[{qty, price, date}]
    buy_lots = []

    for t in code_trades:
        qty = float(t.get("quantity", 0))
        price = float(t.get("price", 0))
        action = t.get("action", "")

        if action in ("买入", "buy", "Buy", "BUY"):
            # 入队列
            buy_lots.append({"qty": qty, "price": price, "date": t.get("date", "")})
        elif action in ("卖出", "sell", "Sell", "SELL"):
            # FIFO：出队列
            remaining_to_sell = qty
            while remaining_to_sell > 0 and buy_lots:
                lot = buy_lots[0]
                if lot["qty"] <= remaining_to_sell:
                    # 批次全部卖出
                    remaining_to_sell -= lot["qty"]
                    buy_lots.pop(0)
                else:
                    # 批次部分卖出
                    lot["qty"] -= remaining_to_sell
                    remaining_to_sell = 0

    # 计算剩余持仓
    total_qty = sum(lot["qty"] for lot in buy_lots)
    total_cost = sum(lot["qty"] * lot["price"] for lot in buy_lots)

    if total_qty <= 0:
        return None

    avg_cost = total_cost / total_qty if total_qty > 0 else 0
    return {
        "quantity": total_qty,
        "cost_price": round(avg_cost, 4),
        "updated": datetime.now().isoformat()
    }

def sync_position_from_trades(code: str):
    """
    根据交易记录重新计算并同步持仓到文件
    """
    position = recalculate_position_from_trades(code)
    positions = load_positions()
    if position:
        positions[code] = position
    elif code in positions:
        del positions[code]
    save_positions(positions)
    return position

def add_position(code: str, quantity: float, cost_price: float):
    """
    添加或覆盖持仓（手动指定时使用）
    注意：智能方式推荐用 record_trade_and_sync()
    """
    positions = load_positions()
    positions[code] = {
        "quantity": quantity,
        "cost_price": cost_price,
        "updated": datetime.now().isoformat()
    }
    save_positions(positions)

def record_trade_and_sync(code: str, action: str, quantity: float, price: float, note: str = ""):
    """
    记录交易并智能同步持仓（核心智能函数）
    - 买入：自动计算新的加权平均持仓成本
    - 卖出：自动减少持仓数量，成本不变
    """
    # 1. 记录交易
    trades = load_trades()
    trades.append({
        "code": code,
        "action": action,
        "quantity": quantity,
        "price": price,
        "note": note,
        "date": datetime.now().isoformat()
    })
    save_trades(trades)
    # 2. 重新计算并同步持仓
    sync_position_from_trades(code)

def remove_position(code: str):
    """清除持仓"""
    positions = load_positions()
    if code in positions:
        del positions[code]
        save_positions(positions)

def get_position(code: str) -> Optional[Dict]:
    """获取单只股票持仓"""
    positions = load_positions()
    return positions.get(code)

def get_all_positions() -> Dict:
    """获取所有持仓（自动从交易记录同步后返回）"""
    return load_positions()

# ============ 交易记录 ============

def load_trades() -> List[Dict]:
    """加载交易记录"""
    if os.path.exists(TRADES_FILE):
        with open(TRADES_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return data.get("trades", [])
    return []

def save_trades(trades: List[Dict]):
    """保存交易记录"""
    with open(TRADES_FILE, 'w', encoding='utf-8') as f:
        json.dump({"trades": trades, "updated": datetime.now().isoformat()}, f, ensure_ascii=False, indent=2)

def add_trade(code: str, action: str, quantity: float, price: float, note: str = ""):
    """添加交易记录"""
    trades = load_trades()
    trades.append({
        "code": code,
        "action": action,  # "买入" 或 "卖出"
        "quantity": quantity,
        "price": price,
        "note": note,
        "date": datetime.now().isoformat()
    })
    save_trades(trades)

def get_trades(code: str = None) -> List[Dict]:
    """获取交易记录"""
    trades = load_trades()
    if code:
        return [t for t in trades if t.get("code") == code]
    return trades

# ============ 持仓分析 ============

def analyze_position(code: str) -> Dict:
    """分析持仓情况"""
    quote = get_stock_quote(code)
    if not quote:
        return {"error": f"无法获取 {code} 的行情"}
    
    position = get_position(code)
    if not position:
        return {"error": f"未找到 {code} 的持仓记录"}
    
    current_price = quote["price"]
    quantity = position["quantity"]
    cost_price = position["cost_price"]
    
    # 计算盈亏
    profit = (current_price - cost_price) * quantity
    profit_pct = (current_price - cost_price) / cost_price * 100 if cost_price > 0 else 0
    market_value = current_price * quantity
    cost_value = cost_price * quantity
    
    # 获取技术分析
    tech = analyze_stock(code)
    
    # 生成操作建议
    suggestions = []
    
    # 基于技术面和持仓情况给出建议
    if "error" not in tech:
        score = tech.get("score", {})
        buy_score = score.get("buy", 0)
        sell_score = score.get("sell", 0)
        
        # 盈利且技术面偏空 -> 考虑止盈
        if profit > 0 and sell_score > buy_score + 2:
            suggestions.append("⚠️ 技术面偏弱，盈利可观，建议部分止盈")
        elif profit > 0 and sell_score > buy_score:
            suggestions.append("建议关注，可考虑分批卖出")
        
        # 亏损且技术面偏多 -> 考虑加仓
        elif profit < 0 and buy_score > sell_score + 2:
            suggestions.append("📈 技术面转好，可考虑加仓摊低成本")
        elif profit < 0 and buy_score > sell_score:
            suggestions.append("建议持有，等待反弹")
        
        # 强势买入信号
        elif buy_score > sell_score + 3:
            suggestions.append("🚀 技术面强烈看涨，可继续持有或加仓")
        
        # 强势卖出信号
        elif sell_score > buy_score + 3:
            suggestions.append("🔴 技术面强烈看跌，建议止损或减仓")
        
        # 震荡整理
        else:
            suggestions.append("📊 震荡整理中，建议观望")
    
    # 计算盈亏平衡点
    return {
        "code": code,
        "name": quote["name"],
        "current_price": current_price,
        "position": {
            "quantity": quantity,
            "cost_price": cost_price,
            "cost_value": round(cost_value, 2),
            "market_value": round(market_value, 2),
            "profit": round(profit, 2),
            "profit_pct": round(profit_pct, 2)
        },
        "suggestions": suggestions,
        "tech_summary": tech.get("overall", "分析中") if "error" not in tech else "分析失败"
    }

def format_position_report(results: List[Dict]) -> str:
    """格式化持仓报告"""
    if not results:
        return "没有持仓记录"
    
    lines = ["💰 持仓分析报告", "=" * 50, ""]
    
    total_profit = 0
    total_value = 0
    
    for r in results:
        if "error" in r:
            continue
            
        pos = r["position"]
        profit_emoji = "🟢" if pos["profit"] >= 0 else "🔴"
        
        lines.append(f"{profit_emoji} {r['name']} ({r['code']})")
        lines.append(f"   持仓: {pos['quantity']}股 @ 成本{pos['cost_price']:.2f}")
        lines.append(f"   当前: {r['current_price']:.2f}")
        lines.append(f"   市值: {pos['market_value']:.2f} | 盈亏: {pos['profit']:+.2f} ({pos['profit_pct']:+.2f}%)")
        
        if r["suggestions"]:
            lines.append(f"   💡 {r['suggestions'][0]}")
        
        lines.append(f"   📈 技术面: {r['tech_summary']}")
        lines.append("")
        
        total_profit += pos["profit"]
        total_value += pos["market_value"]
    
    if total_value > 0:
        lines.append("=" * 50)
        lines.append(f"📊 总市值: {total_value:.2f} | 总盈亏: {total_profit:+.2f}")
    
    return "\n".join(lines)

# ============ 股票池管理 ============

def load_stock_pool() -> List[str]:
    """加载股票池"""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return data.get("stocks", [])
    return []

def save_stock_pool(stocks: List[str]):
    """保存股票池"""
    os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
    with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
        json.dump({"stocks": stocks, "updated": datetime.now().isoformat()}, f, ensure_ascii=False, indent=2)

def add_to_pool(code: str):
    """添加股票到监控池"""
    stocks = load_stock_pool()
    if code not in stocks:
        stocks.append(code)
        save_stock_pool(stocks)
        return True
    return False

def remove_from_pool(code: str):
    """从监控池移除"""
    stocks = load_stock_pool()
    if code in stocks:
        stocks.remove(code)
        save_stock_pool(stocks)
        return True
    return False

def monitor_all() -> List[Dict]:
    """监控所有股票"""
    stocks = load_stock_pool()
    results = []
    for code in stocks:
        result = analyze_stock(code)
        if "error" not in result:
            results.append(result)
        time.sleep(1)
    return results

def is_hk_stock(code: str) -> bool:
    """判断是否为港股"""
    # 方式1：4-5位数字，以0开头（老格式如 00981）
    if len(code) >= 4 and len(code) <= 5 and code.startswith("0"):
        return True
    # 方式2：已带hk前缀（hk00981）
    if code.startswith("hk"):
        return True
    return False

def is_a_stock(code: str) -> bool:
    """判断是否为A股（sz/sh前缀 + 6位数字）"""
    if len(code) == 8 and (code.startswith("sz") or code.startswith("sh")):
        return code[2:].isdigit()
    if len(code) == 6 and code.isdigit():
        return True
    return False

def validate_stock_mapping(code: str, quote: Dict) -> str:
    """
    校验股票代码和名称映射是否一致
    返回空字符串表示正常，否则返回警告信息
    """
    if not quote or "name" not in quote:
        return ""
    
    name = quote.get("name", "")
    stock_type = "unknown"
    
    # 根据代码判断应该是什么市场的股票
    if code.startswith("hk"):
        stock_type = "HK"
    elif len(code) == 6 and code.isdigit():
        stock_type = "A"
    elif code.startswith(("sh", "sz")):
        stock_type = "A"
    
    # 常见港股名称关键词（如果名称包含这些，认为是港股）
    hk_keywords = ["集团", "控股", "国际", "银行", "保险", "证券", "地产", "酒店", "娱乐", "技术", "健康", "医疗", "药业", "能源", "石油", "煤炭", "电力", "燃气", "水务", "公路", "铁路", "航空", "航运", "物流", "制造", "科技", "网络", "软件", "电子", "Semiconductor", "Holdings", "International", "Corp", "Ltd", "Co."]
    
    # 常见A股名称关键词（如果名称包含这些，认为是A股）
    a_keywords = ["股份", "有限", "公司", "集团", "实业", "投资", "发展", "控股", "国", "中"]
    
    # 检查名称是否包含市场特征
    is_likely_hk = any(kw in name for kw in hk_keywords)
    
    # 如果代码说应该是港股，但名称看起来像A股，警告
    if stock_type == "HK" and not is_likely_hk and len(name) < 10 and name.isdigit() == False:
        # 进一步检查：A股名称通常是中文，港股可能是英文或混合
        # 如果名称全是中文字符且不包含任何港股关键词，可能是错误
        if all('\u4e00' <= c <= '\u9fff' for c in name) and "集" not in name and "控" not in name:
            return f"[警告] 代码{code}被识别为港股，但返回名称'{name}'可能是A股Stock"
    
    return ""

def monitor_a() -> List[Dict]:
    """仅监控A股"""
    stocks = load_stock_pool()
    results = []
    for code in stocks:
        if is_a_stock(code):
            result = analyze_stock(code)
            if "error" not in result:
                results.append(result)
            time.sleep(1)
    return results

def monitor_hk() -> List[Dict]:
    """仅监控港股"""
    stocks = load_stock_pool()
    results = []
    for code in stocks:
        if is_hk_stock(code):
            result = analyze_stock(code)
            if "error" not in result:
                results.append(result)
            time.sleep(1)
    return results

def format_report(results: List[Dict]) -> str:
    """格式化报告"""
    if not results:
        return "监控池为空，请先添加股票"
    
    lines = ["📊 股票监控报告", "=" * 50, ""]
    
    for r in results:
        score_emoji = "🟢" if "买入" in r["overall"] else "🔴" if "卖出" in r["overall"] else "🟡"
        
        lines.append(f"{score_emoji} {r['name']} ({r['code']})")
        lines.append(f"   价格: {r['price']:.2f} ({r['change']:+.2f}, {r['change_pct']:+.2f}%)")
        lines.append(f"   📈 MA: 5日={r['indicators']['ma']['ma5']} | 10日={r['indicators']['ma']['ma10']} | 20日={r['indicators']['ma']['ma20']}")
        lines.append(f"   📊 MACD: DIF={r['indicators']['macd']['dif']:.4f} DEA={r['indicators']['macd']['dea']:.4f}")
        lines.append(f"   📉 KDJ: K={r['indicators']['kdj']['k']} D={r['indicators']['kdj']['d']} J={r['indicators']['kdj']['j']}")
        lines.append(f"   📊 RSI: {r['indicators']['rsi']['rsi6']}")
        lines.append(f"   🎯 BOLL: {r['indicators']['boll']['signal']}")
        lines.append(f"   📊 OBV: {r['indicators']['obv']['signal']}")
        lines.append(f"   📊 DMI: {r['indicators']['dmi']['signal']}")
        
        # 资讯情感（增强版）
        sent = r.get("sentiment", {})
        if sent and sent.get("sentiment") != "暂无":
            impact = sent.get("impact_level", "中性")
            lines.append(f"   📰 资讯情感: {sent['sentiment']} | 影响:{impact} | 积极{sent['positive_count']}分/消极{sent['negative_count']}分")
            if sent.get("news_titles"):
                for nt in sent["news_titles"][:3]:
                    lines.append(f"      {nt}")
            if sent.get("high_impact_count", 0) >= 1:
                lines.append(f"   🚨 高影响新闻: {sent['high_impact_count']}条")
        
        lines.append(f"   🎯 综合判断: {r['overall']}")
        lines.append("")
    
    return "\n".join(lines)

def calc_support_resistance(klines: List[Dict]) -> Dict:
    """
    计算支撑位和阻力位
    基于近30日高低点 + BOLL + 成交密集区
    """
    if not klines or len(klines) < 5:
        return {"support": None, "resistance": None, "pivot": None}
    
    closes = [k['close'] for k in klines]
    highs  = [k['high']  for k in klines]
    lows   = [k['low']   for k in klines]
    
    # 近30日（或全部）高低价
    recent_high  = max(highs[-30:]) if len(highs) >= 30 else max(highs)
    recent_low   = min(lows[-30:])  if len(lows)  >= 30 else min(lows)
    
    # BOLL
    boll = calculate_boll(closes[-20:]) if len(closes) >= 20 else None
    boll_upper = boll["upper"]  if boll else None
    boll_mid   = boll["middle"] if boll else None
    boll_lower = boll["lower"]  if boll else None
    
    # 近5日高低点（短线参考）
    short_high = max(highs[-5:])
    short_low  = min(lows[-5:])
    
    # 阻力位：优先用近30日高点，其次BOLL上轨
    resistance = recent_high if recent_high > closes[-1] * 1.02 else (boll_upper if boll_upper and boll_upper > closes[-1] else short_high)
    
    # 支撑位：优先用近30日低点，其次BOLL下轨
    support = recent_low if recent_low < closes[-1] * 0.98 else (boll_lower if boll_lower and boll_lower < closes[-1] else short_low)
    
    # 枢轴点（三价平均）
    pivot = (recent_high + recent_low + closes[-1]) / 3 if recent_high and recent_low else None
    
    return {
        "support":     round(support, 2) if support else None,
        "resistance":  round(resistance, 2) if resistance else None,
        "pivot":       round(pivot, 2) if pivot else None,
        "boll_upper":  round(boll_upper, 2) if boll_upper else None,
        "boll_lower":  round(boll_lower, 2) if boll_lower else None,
        "short_high":  round(short_high, 2),
        "short_low":   round(short_low, 2),
    }

def assess_trend_strength(dmi: Dict, macd: Dict, ma: Dict) -> Dict:
    """
    多维度评估趋势强度
    返回：短期/中期/长期趋势评分 + 综合趋势方向
    """
    adx = dmi.get("adx", 0)
    plus_di = dmi.get("plus_di", 0)
    minus_di = dmi.get("minus_di", 0)
    
    # ADX趋势强度：<20无趋势，20-25弱，25-40中，>40强
    if adx < 20:
        adx_strength = "⚪ 无明显趋势"
        adx_score = 0
    elif adx < 25:
        adx_strength = "🟡 趋势偏弱"
        adx_score = 1
    elif adx < 40:
        adx_strength = "🟢 趋势确认"
        adx_score = 2
    else:
        adx_strength = "🔴 趋势强劲"
        adx_score = 3
    
    # 多空力量对比
    di_diff = plus_di - minus_di
    if di_diff > 10:
        trend_dir = "多头主导"
        dir_score = 1
    elif di_diff < -10:
        trend_dir = "空头主导"
        dir_score = -1
    else:
        trend_dir = "多空均衡"
        dir_score = 0
    
    # MACD状态
    macd_signal = macd.get("signal", "")
    if "金叉" in macd_signal:
        macd_trend = "上升"
        macd_score = 1
    elif "死叉" in macd_signal:
        macd_trend = "下降"
        macd_score = -1
    else:
        macd_trend = "收敛"
        macd_score = 0
    
    # 均线多空排列
    ma5  = ma.get("ma5", 0)
    ma10 = ma.get("ma10", 0)
    ma20 = ma.get("ma20", 0)
    price = ma.get("price", 0)
    
    if ma5 > ma10 > ma20 and price > ma5:
        ma_status = "多头排列（强势）"
        ma_score = 2
    elif ma5 < ma10 < ma20 and price < ma5:
        ma_status = "空头排列（弱势）"
        ma_score = -2
    elif ma5 > ma10:
        ma_status = "短多中空"
        ma_score = 0.5
    elif ma5 < ma10:
        ma_status = "短空中多"
        ma_score = -0.5
    else:
        ma_status = "均线纠缠"
        ma_score = 0
    
    # 综合评分
    total_score = adx_score + dir_score + macd_score + ma_score
    if total_score >= 4:
        overall_trend = "📈 上升趋势"
    elif total_score >= 2:
        overall_trend = "↗️ 震荡偏多"
    elif total_score <= -4:
        overall_trend = "📉 下降趋势"
    elif total_score <= -2:
        overall_trend = "↘️ 震荡偏空"
    else:
        overall_trend = "↔️ 区间震荡"
    
    return {
        "adx":         round(adx, 2),
        "adx_strength": adx_strength,
        "trend_dir":   trend_dir,
        "macd_trend":  macd_trend,
        "ma_status":   ma_status,
        "overall_trend": overall_trend,
        "total_score": total_score
    }

def format_single_stock_analysis(r: Dict, position: Dict = None) -> List[str]:
    """
    格式化单只股票分析（用于综合报告）
    返回多行字符串列表
    """
    lines = []
    code  = r.get("code", "")
    name  = r.get("name", code)
    price = r.get("price", 0)
    chg   = r.get("change", 0)
    pct   = r.get("change_pct", 0)
    ind   = r.get("indicators", {})
    ma    = ind.get("ma", {})
    macd  = ind.get("macd", {})
    kdj   = ind.get("kdj", {})
    rsi   = ind.get("rsi", {})
    rsi_v = rsi.get("rsi6", 50)  # 提取RSI6作为短期判断
    boll  = ind.get("boll", {})
    dmi   = ind.get("dmi", {})
    obv   = ind.get("obv", {})
    wr    = ind.get("wr", {})
    sent  = r.get("sentiment", {})
    score = r.get("score", {})
    klines = r.get("klines", [])
    
    # 价格颜色
    price_emoji = "🟢" if pct >= 0 else "🔴"
    
    # === 1. 基础行情 ===
    lines.append(f"{'─'*56}")
    lines.append(f"  {price_emoji} {name}({code})  ¥{price} {chg:+.2f}({pct:+.2f}%)")
    
    # === 2. 持仓信息（如果有）===
    if position:
        qty   = position.get("quantity", 0)
        cost  = position.get("cost_price", 0)
        mkt_val = price * qty
        pnl   = (price - cost) * qty
        pnl_pct = (price - cost) / cost * 100 if cost > 0 else 0
        pnl_emoji = "🟢" if pnl >= 0 else "🔴"
        lines.append(f"  📦 持仓: {qty}股 @ 成本{cost:.2f} | 市值{mkt_val:.0f} | {pnl_emoji}盈亏{pnl:+.0f}({pnl_pct:+.1f}%)")
    
    # === 3. 支撑/阻力 ===
    sr = calc_support_resistance(klines) if klines else {}
    if sr.get("support") and sr.get("resistance"):
        lines.append(f"  📍 支撑/阻力: {sr['support']} / {sr['resistance']} | 枢轴 {sr.get('pivot','N/A')}")
        if sr.get("boll_upper"):
            lines.append(f"  📊 BOLL: 上轨{boll.get('upper',0):.2f} 中轨{boll.get('middle',0):.2f} 下轨{boll.get('lower',0):.2f} → {boll.get('signal','')}")
    
    # === 4. 趋势评估 ===
    trend_info = assess_trend_strength(dmi, macd, {**ma, "price": price})
    lines.append(f"  📈 趋势: {trend_info['overall_trend']} | ADX{trend_info['adx']} {trend_info['adx_strength']}")
    lines.append(f"  ↔️  均线: {trend_info['ma_status']} | MACD: {trend_info['macd_trend']} | DMI: {trend_info['trend_dir']}")
    
    # === 5. 关键指标（分三行）===
    lines.append(f"  📊 MACD: DIF={macd.get('dif',0):.4f} DEA={macd.get('dea',0):.4f} | {macd.get('signal','')}")
    lines.append(f"  📊 KDJ:  K={kdj.get('k',0):.1f} D={kdj.get('d',0):.1f} J={kdj.get('j',0):.1f} | {kdj.get('signal','')}")
    lines.append(f"  📊 RSI:  6日={rsi.get('rsi6',0):.1f} 12日={rsi.get('rsi12',0):.1f} 24日={rsi.get('rsi24',0):.1f} | {rsi.get('signal','')}")
    lines.append(f"  📊 OBV:  {obv.get('signal','')} | WR: {wr.get('signal','')}")
    
    # === 6. 资讯情感 ===
    sentiment_val = sent.get("sentiment", "暂无")
    sentiment_weighted = sent.get("weighted_score", 0)
    sentiment_impact = sent.get("impact_level", "无")
    if sentiment_val != "暂无":
        lines.append(f"  📰 资讯: {sentiment_val} | 影响:{sentiment_impact} | 积极{sent.get('positive_count',0)}分/消极{sent.get('negative_count',0)}分")
        news_titles = sent.get("news_titles", [])
        if news_titles:
            for nt in news_titles[:2]:
                lines.append(f"      • {nt}")
    else:
        lines.append(f"  📰 资讯: 暂无新闻数据")
    
    # === 7. 综合信号与建议 ===
    buy_s  = score.get("buy", 0)
    sell_s = score.get("sell", 0)
    overall = r.get("overall", "观望")
    
    # 评分条
    bar_len = 10
    buy_bar  = "█" * min(int(buy_s), bar_len)
    sell_bar = "▓" * min(int(sell_s), bar_len)
    lines.append(f"  🎯 信号: {overall} | 买入{buy_s}:{sell_s}卖出 | [{buy_bar}{sell_bar}]")
    
    # 操作建议（结合趋势+信号+持仓）
    suggestions = _generate_suggestion(r, trend_info, position)
    if suggestions:
        lines.append(f"  💡 建议: {suggestions}")
    
    # === 8. 精确买卖点位（核心增强） ===
    sr = calc_support_resistance(klines) if klines else {}
    support     = sr.get("support")
    resistance  = sr.get("resistance")
    short_low   = sr.get("short_low")
    short_high  = sr.get("short_high")
    boll_upper   = sr.get("boll_upper")
    boll_lower   = sr.get("boll_lower")
    ma60         = ma.get("ma60")
    ma20         = ma.get("ma20")
    entry_point = None
    exit_point  = None
    stop_loss   = None
    take_profit = None
    entry_range  = None  # 买入区间
    exit_range   = None  # 卖出区间
    
    # 计算买入点位
    if buy_s >= sell_s + 2:
        if support and price <= support * 1.03:
            entry_point = support
            entry_range = f"{support * 0.98:.0f}-{support * 1.01:.0f}"  # 支撑上下2%
        elif resistance and price >= resistance * 0.98 and price < resistance:
            entry_point = f"突破{resistance}后确认"
            entry_range = f"{resistance}-{resistance * 1.02:.0f}"
        elif rsi_v < 35 and (boll_lower or support):
            entry_point = boll_lower or support
            entry_range = f"{boll_lower * 0.98:.0f}-{boll_lower * 1.02:.0f}" if boll_lower else f"{support * 0.98:.0f}-{support * 1.02:.0f}"
        elif ma60 and price <= ma60 * 1.02:
            entry_point = ma60
            entry_range = f"{ma60 * 0.98:.0f}-{ma60 * 1.01:.0f}"
        elif support:
            entry_point = support
            entry_range = f"{support * 0.98:.0f}-{support * 1.02:.0f}"
    
    # 计算卖出点位
    if sell_s >= buy_s + 2 or sell_s >= 5:
        if resistance and price >= resistance * 0.97:
            exit_point = resistance
            exit_range = f"{resistance * 0.97:.0f}-{resistance * 1.01:.0f}"  # 阻力上下3%
        elif boll_upper and price >= boll_upper * 0.98:
            exit_point = boll_upper
            exit_range = f"{boll_upper * 0.97:.0f}-{boll_upper * 1.01:.0f}"
        elif rsi_v > 68:
            exit_point = boll_upper or resistance
            exit_range = f"{exit_point * 0.97:.0f}-{exit_point * 1.01:.0f}"
    
    # 计算止损位
    if support:
        stop_loss = round(support * 0.97, 2)
    elif ma60:
        stop_loss = round(ma60 * 0.97, 2)
    
    # 计算目标位
    if buy_s >= sell_s + 3 and resistance:
        take_profit = resistance
    elif buy_s >= sell_s + 2 and boll_upper:
        take_profit = boll_upper
    
    # 输出点位信息（含区间）
    if buy_s > sell_s + 1 or sell_s > buy_s + 1:
        lines.append(f"  📌 关键点位:")
        if entry_point:
            lines.append(f"     买入: {entry_point}")
        if entry_range and (buy_s >= sell_s + 2):
            lines.append(f"     买入区间: {entry_range}（回踩分批建仓）")
        if exit_point:
            lines.append(f"     卖出: {exit_point}")
        if exit_range and (sell_s >= buy_s + 2):
            lines.append(f"     卖出区间: {exit_range}（分批止盈）")
        if stop_loss:
            lines.append(f"     止损: {stop_loss}")
        if take_profit:
            lines.append(f"     目标: {take_profit}")
        if support:
            lines.append(f"     支撑: {support}")
        if resistance:
            lines.append(f"     阻力: {resistance}")
        if ma60 and ma20:
            lines.append(f"     均线区间: MA20={ma20:.0f} / MA60={ma60:.0f}")
    
    return lines

def _generate_suggestion(r: Dict, trend_info: Dict, position: Dict = None) -> str:
    """生成操作建议（含具体买卖点位）"""
    score = r.get("score", {})
    buy_s  = score.get("buy", 0)
    sell_s = score.get("sell", 0)
    trend_score = trend_info.get("total_score", 0)
    overall = r.get("overall", "")
    price = r.get("price", 0)
    pct   = r.get("change_pct", 0)
    kdj   = r.get("indicators", {}).get("kdj", {})
    rsi_v = r.get("indicators", {}).get("rsi", {}).get("rsi6", 50)
    boll  = r.get("indicators", {}).get("boll", {})
    macd  = r.get("indicators", {}).get("macd", {})
    dmi   = r.get("indicators", {}).get("dmi", {})
    sr    = calc_support_resistance(r.get("klines", [])) if r.get("klines") else {}
    ma    = r.get("indicators", {}).get("ma", {})
    
    suggestions = []
    entry_point = None  # 买入点位
    exit_point  = None  # 卖出点位
    stop_loss   = None  # 止损位
    take_profit = None  # 止盈位
    
    # ========== 计算关键价位 ==========
    support   = sr.get("support")    # 支撑位
    resistance = sr.get("resistance") # 阻力位
    boll_upper = sr.get("boll_upper")
    boll_lower = sr.get("boll_lower")
    short_low  = sr.get("short_low")
    short_high = sr.get("short_high")
    ma60      = ma.get("ma60")       # 60日均线（重要中期支撑）
    
    # ========== 1. 趋势信号判断 ==========
    if trend_score >= 3 and buy_s > sell_s:
        trend_signal = "🚀 上升趋势确认，多头信号共振"
    elif trend_score <= -3 and sell_s > buy_s:
        trend_signal = "🔴 下降趋势确认，空头信号共振"
    elif buy_s >= 6:
        trend_signal = "✅ 技术面偏多，高概率向上"
    elif sell_s >= 6:
        trend_signal = "⚠️ 技术面偏空，高概率向下"
    elif "超卖" in kdj.get("signal", "") or rsi_v < 30:
        trend_signal = "⚡ RSI/KDJ超卖，短线反弹概率大"
    elif "超买" in kdj.get("signal", "") or rsi_v > 70:
        trend_signal = "⚡ RSI/KDJ超买，短线调整风险"
    else:
        trend_signal = "📊 方向不明，保持观望"
    
    suggestions.append(trend_signal)
    
    # ========== 2. 计算买入点位 ==========
    # 买入条件：
    # - 价格接近支撑位（入场安全边际）
    # - 或放量突破阻力位（确认转强）
    # - 或RSI超卖 + 价格企稳
    if buy_s >= sell_s + 2:
        if support and price <= support * 1.03:
            # 价格回踩支撑位附近，是较好买入机会
            entry_point = support
            suggestions.append(f"📍 买入点位: {support}（回踩支撑企稳）")
        elif resistance and price >= resistance * 0.98 and price < resistance:
            # 接近阻力位，等待突破
            entry_point = resistance
            suggestions.append(f"📍 买入参考: {resistance}（突破后确认买入）")
        elif rsi_v < 35:
            # RSI偏低，提供买入参考
            # 目标位：短期阻力位或BOLL上轨
            entry_point = short_low if short_low else (boll_lower or support)
            suggestions.append(f"📍 买入点位: {entry_point}（RSI偏低，分批建仓）")
        elif ma60 and price <= ma60 * 1.02:
            # 价格回踩60日均线
            entry_point = ma60
            suggestions.append(f"📍 买入点位: {ma60}（回踩60日线企稳）")
        elif support:
            entry_point = support
            suggestions.append(f"📍 买入点位: {support}（支撑位参考）")
    
    # ========== 3. 计算卖出/止盈点位 ==========
    # 卖出条件：
    # - 价格接近阻力位（上涨乏力）
    # - 或技术面转空（多指标死叉）
    # - 或超买信号
    if sell_s >= buy_s + 2 or sell_s >= 5:
        if resistance and price >= resistance * 0.97:
            # 价格接近阻力位，考虑减仓
            exit_point = resistance
            suggestions.append(f"📍 建议卖出: {resistance}（接近阻力位，分批减仓）")
        elif boll_upper and price >= boll_upper * 0.98:
            # 接近BOLL上轨
            exit_point = boll_upper
            suggestions.append(f"📍 建议卖出: {boll_upper}（BOLL上轨压力）")
        elif rsi_v > 68:
            # RSI超买
            exit_point = (boll_upper or resistance) if (boll_upper or resistance) else short_high
            if exit_point:
                exit_range = f"{exit_point * 0.97:.0f}-{exit_point * 1.01:.0f}"
                suggestions.append(f"📍 建议卖出: {exit_point}（RSI超买，分批止盈）")
        elif macd.get("signal") and "死叉" in macd.get("signal", ""):
            # MACD死叉
            exit_point = short_high
            suggestions.append(f"📍 建议卖出: {short_high}（MACD死叉，短线离场）")
    
    # ========== 4. 止损点位 ==========
    if entry_point and support:
        # 买入后，止损设在支撑下方2-3%
        stop_loss = round(support * 0.97, 2)
        suggestions.append(f"🛑 止损位: {stop_loss}（支撑下方3%）")
    elif ma60:
        # 中期止损参考60日线
        stop_loss = round(ma60 * 0.97, 2)
        suggestions.append(f"🛑 止损参考: {stop_loss}（60日线下方3%）")
    
    # ========== 5. 止盈点位 ==========
    if (buy_s >= sell_s + 3) and resistance:
        # 强势信号，预期上涨到阻力位
        take_profit = resistance
        profit_range = round((take_profit - price) / price * 100, 1) if price > 0 else 0
        suggestions.append(f"🎯 目标位: {take_profit}（预计上涨{profit_range}%）")
    elif (buy_s >= sell_s + 2) and boll_upper:
        take_profit = boll_upper
        profit_range = round((take_profit - price) / price * 100, 1) if price > 0 else 0
        suggestions.append(f"🎯 目标位: {take_profit}（预计上涨{profit_range}%）")
    
    # ========== 6. 持仓用户专属建议 ==========
    if position:
        cost    = position.get("cost_price", 0)
        qty     = position.get("quantity", 0)
        pnl_pct = (price - cost) / cost * 100 if cost > 0 else 0
        pnl     = (price - cost) * qty
        
        if pnl > 0 and sell_s > buy_s + 2:
            suggestions.append(f"🟢 盈利{pnl_pct:.1f}%但技术面转弱，建议部分止盈")
            # 止盈建议在阻力位或BOLL上轨
            if exit_point:
                suggestions.append(f"📍 止盈参考: {exit_point}")
        elif pnl < 0 and buy_s > sell_s + 2:
            suggestions.append(f"🔵 亏损{pnl_pct:.1f}%但技术面转强，可加仓摊低成本")
            if entry_point:
                suggestions.append(f"📍 加仓参考: {entry_point}")
        elif pnl_pct > 15:
            suggestions.append(f"🟢 盈利{pnl_pct:.1f}%，建议设动态止盈")
        elif pnl_pct < -8:
            suggestions.append(f"🔴 亏损超过8%，注意控制风险")
    
    # ========== 7. 综合点位总结 ==========
    # 简化建议：给出核心操作区间
    action_summary = []
    if buy_s > sell_s + 2:
        action_summary.append(f"✅ 操作: 回踩{entry_point or support or '支撑位'}买入")
        if stop_loss:
            action_summary.append(f" | 止损: {stop_loss}")
        if take_profit:
            action_summary.append(f" | 目标: {take_profit}")
    elif sell_s > buy_s + 2:
        action_summary.append(f"⚠️ 操作: 接近{exit_point or resistance or '阻力位'}减仓")
        if stop_loss:
            action_summary.append(f" | 止损参考: {stop_loss}")
    else:
        action_summary.append("📊 操作: 观望，等待信号确认")
    
    # 返回：趋势建议 + 操作区间（合并重复，精简输出）
    final_suggestion = suggestions[:4]  # 最多4条
    
    # 添加操作区间总结
    if action_summary:
        final_suggestion.append("".join(action_summary))
    
    return " | ".join(final_suggestion[:3])

def generate_full_report(market_indices: List[Dict], stock_results: List[Dict], period_label: str = "盘中") -> str:
    """
    生成综合分析报告（增强版）
    整合大盘技术面 + 个股多维度分析 + 持仓 + 资讯
    """
    now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
    lines = []
    
    # ========== 1. 报告头 ==========
    lines.append(f"{'📊'*25}")
    lines.append(f"  🔍 股票综合分析报告 {period_label}")
    lines.append(f"  🕐 更新时间: {now_str}")
    lines.append("")
    
    # ========== 2. 大盘指数 ==========
    if market_indices:
        lines.append("【大盘指数】")
        for idx in market_indices:
            emoji = "🟢" if idx['pct'] > 0 else "🔴" if idx['pct'] < 0 else "🟡"
            lines.append(f"  {emoji} {idx['name']}: {idx['price']} {idx['change']:+.2f}({idx['pct']:+.2f}%)")
        lines.append("")
    
    # ========== 3. 市场情绪总览 ==========
    all_sentiments = [r.get("sentiment", {}) for r in stock_results if r.get("sentiment")]
    pos_total = sum(s.get("positive_count", 0) for s in all_sentiments)
    neg_total = sum(s.get("negative_count", 0) for s in all_sentiments)
    
    # 涨跌统计
    up_count   = sum(1 for r in stock_results if r.get("change_pct", 0) > 0)
    down_count = sum(1 for r in stock_results if r.get("change_pct", 0) < 0)
    flat_count = len(stock_results) - up_count - down_count
    
    if pos_total > neg_total * 1.5:
        market_mood = "📈 偏多（资讯积极）"
    elif neg_total > pos_total * 1.5:
        market_mood = "📉 偏空（资讯消极）"
    else:
        market_mood = "↔️ 中性（多空均衡）"
    
    lines.append("【市场情绪】")
    lines.append(f"  股票涨跌: ↑{up_count} ↓{down_count} →{flat_count}")
    lines.append(f"  资讯情感: 积极{pos_total}分 / 消极{neg_total}分 | {market_mood}")
    lines.append("")
    
    # ========== 4. 分持仓/关注股 ==========
    positions_all = load_positions()
    
    # 4a. 持仓股
    held = [r for r in stock_results if r.get("code") in positions_all]
    watch = [r for r in stock_results if r.get("code") not in positions_all]
    
    if held:
        lines.append("【📦 持仓股分析】")
        # 汇总持仓盈亏
        total_mkt = 0.0
        total_pnl = 0.0
        for r in held:
            code  = r.get("code")
            price = r.get("price", 0)
            pos   = positions_all.get(code, {})
            qty   = pos.get("quantity", 0)
            cost  = pos.get("cost_price", 0)
            mkt   = price * qty
            pnl   = (price - cost) * qty if cost else 0
            total_mkt += mkt
            total_pnl  += pnl
        
        total_pnl_pct = total_pnl / (total_mkt - total_pnl) * 100 if total_mkt > total_pnl else 0
        pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
        lines.append(f"  {'─'*52}")
        lines.append(f"  💼 总持仓市值: ¥{total_mkt:,.0f} | {pnl_emoji}总盈亏: {total_pnl:+,.0f}({total_pnl_pct:+.1f}%)")
        lines.append("")
        
        for r in held:
            code  = r.get("code")
            pos   = positions_all.get(code, {})
            for l in format_single_stock_analysis(r, pos):
                lines.append(f"  {l}")
            lines.append("")
    
    # 4b. 关注股
    if watch:
        lines.append("【👁️ 关注股分析】")
        for r in watch:
            for l in format_single_stock_analysis(r):
                lines.append(f"  {l}")
            lines.append("")
    
    # ========== 5. 综合评级排序 ==========
    lines.append("【📋 综合评级】")
    sorted_stocks = sorted(stock_results, key=lambda x: x.get("score", {}).get("buy", 0) - x.get("score", {}).get("sell", 0), reverse=True)
    for i, r in enumerate(sorted_stocks, 1):
        score  = r.get("score", {})
        buy_s  = score.get("buy", 0)
        sell_s = score.get("sell", 0)
        diff   = buy_s - sell_s
        overall = r.get("overall", "观望")
        price  = r.get("price", 0)
        pct    = r.get("change_pct", 0)
        emoji  = "🟢" if pct >= 0 else "🔴"
        sign   = "+" if diff >= 0 else ""
        lines.append(f"  {i}. {emoji}{r.get('name','')}({r.get('code','')}) {emoji}{price}({pct:+.2f}%) | 信号{overall} | 差值:{sign}{diff}")
    
    lines.append("")
    lines.append(f"{'📊'*25}")
    lines.append(f"  ⚠️ 仅供参考，不构成投资建议")
    
    return "\n".join(lines)

# 命令行入口
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Stock Monitor CLI")
        print("Usage: python stock_monitor.py <command> [args]")
        print("Commands:")
        print("  quote <code>        - 查询股票行情")
        print("  index               - 查询大盘指数")
        print("  analyze <code>      - 分析股票技术指标")
        print("  add <code>          - 添加股票到监控池")
        print("  remove <code>       - 从监控池移除")
        print("  list                - 查看监控池")
        print("  monitor             - 监控所有股票")
        print("  monitor-a           - 仅监控A股")
        print("  monitor-hk          - 仅监控港股")
        print("  report [标签]       - 生成综合分析报告（含大盘+持仓+资讯+趋势）")
        print("  report-a [标签]     - 生成A股综合分析报告")
        print("  report-hk [标签]    - 生成港股综合分析报告")
        print("  position add <code> <quantity> <cost_price>  - 添加持仓")
        print("  position remove <code> - 清除持仓")
        print("  position list       - 查看持仓")
        print("  position <code>     - 分析单只持仓")
        print("  trade buy <code> <quantity> <price> - 记录买入")
        print("  trade sell <code> <quantity> <price> - 记录卖出")
        print("  trades [code]       - 查看交易记录")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    # 持仓管理命令
    if cmd == "position" and len(sys.argv) >= 2:
        subcmd = sys.argv[2] if len(sys.argv) > 2 else "list"
        
        if subcmd == "list":
            positions = load_positions()
            if not positions:
                print("暂无持仓")
            else:
                for code, pos in positions.items():
                    quote = get_stock_quote(code)
                    name = quote["name"] if quote else code
                    print(f"{code} {name}: {pos['quantity']}股 @ {pos['cost_price']}")
        
        elif subcmd == "add" and len(sys.argv) >= 5:
            code = sys.argv[3]
            quantity = float(sys.argv[4])
            cost_price = float(sys.argv[5])
            add_position(code, quantity, cost_price)
            print(f"已添加持仓: {code} {quantity}股 @ {cost_price}")
        
        elif subcmd == "remove" and len(sys.argv) >= 4:
            code = sys.argv[3]
            remove_position(code)
            print(f"已清除持仓: {code}")
        
        elif len(sys.argv) >= 3:
            # 分析单只持仓
            code = sys.argv[2]
            result = analyze_position(code)
            if "error" in result:
                print(result["error"])
            else:
                print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            print("用法: position <add|remove|list|code>")
    
    # 交易记录命令
    elif cmd == "trade" and len(sys.argv) >= 3:
        subcmd = sys.argv[2]
        
        if subcmd == "buy" and len(sys.argv) >= 6:
            code = sys.argv[3]
            quantity = float(sys.argv[4])
            price = float(sys.argv[5])
            add_trade(code, "买入", quantity, price)
            add_position(code, quantity, price)  # 同时更新持仓
            print(f"已记录买入: {code} {quantity}股 @ {price}")
        
        elif subcmd == "sell" and len(sys.argv) >= 6:
            code = sys.argv[3]
            quantity = float(sys.argv[4])
            price = float(sys.argv[5])
            add_trade(code, "卖出", quantity, price)
            
            # 更新持仓
            pos = get_position(code)
            if pos:
                new_qty = pos["quantity"] - quantity
                if new_qty <= 0:
                    remove_position(code)
                    print(f"已清仓: {code}")
                else:
                    add_position(code, new_qty, pos["cost_price"])
                    print(f"已记录卖出并更新持仓: {code} 剩余{new_qty}股")
            else:
                print(f"已记录卖出: {code} (无持仓记录)")
        
        else:
            print("用法: trade <buy|sell> <code> <quantity> <price>")
    
    elif cmd == "trades":
        code = sys.argv[2] if len(sys.argv) > 2 else None
        trades = get_trades(code)
        if not trades:
            print("暂无交易记录")
        else:
            for t in trades:
                print(f"{t['date'][:10]} {t['action']} {t['code']} {t['quantity']}股 @ {t['price']}")
    
    elif cmd == "quote" and len(sys.argv) > 2:
        quote = get_stock_quote(sys.argv[2])
        print(json.dumps(quote, ensure_ascii=False, indent=2))

    elif cmd == "index":
        indices = get_market_index()
        for idx in indices:
            emoji = "🟢" if idx['pct'] > 0 else "🔴" if idx['pct'] < 0 else "🟡"
            print(f"{emoji} {idx['name']}: {idx['price']} {idx['change']:+.2f} ({idx['pct']:+.2f}%)")
        print("\n--- JSON ---")
        print(json.dumps(indices, ensure_ascii=False, indent=2))
    
    elif cmd == "analyze" and len(sys.argv) > 2:
        result = analyze_stock(sys.argv[2])
        print(json.dumps(result, ensure_ascii=False, indent=2))
    
    elif cmd == "add" and len(sys.argv) > 2:
        if add_to_pool(sys.argv[2]):
            print(f"已添加 {sys.argv[2]} 到监控池")
        else:
            print(f"{sys.argv[2]} 已在监控池中")
    
    elif cmd == "remove" and len(sys.argv) > 2:
        if remove_from_pool(sys.argv[2]):
            print(f"已从监控池移除 {sys.argv[2]}")
        else:
            print(f"{sys.argv[2]} 不在监控池中")
    
    elif cmd == "list":
        stocks = load_stock_pool()
        print(f"监控池: {stocks}")
    
    elif cmd == "monitor":
        results = monitor_all()
        print(format_report(results))
    
    elif cmd == "monitor-a":
        results = monitor_a()
        print(format_report(results))
    
    elif cmd == "monitor-hk":
        results = monitor_hk()
        print(format_report(results))
    
    elif cmd == "report":
        # 综合报告（所有股票）
        indices = get_market_index()
        results = monitor_all()
        period_label = "综合报告"
        if len(sys.argv) > 2:
            period_label = sys.argv[2]
        print(generate_full_report(indices, results, period_label))
    
    elif cmd == "report-a":
        # A股综合报告
        indices = get_market_index()
        results = monitor_a()
        period_label = "A股综合报告"
        if len(sys.argv) > 2:
            period_label = sys.argv[2]
        print(generate_full_report(indices, results, period_label))
    
    elif cmd == "report-hk":
        # 港股综合报告
        indices = get_market_index()
        results = monitor_hk()
        period_label = "港股综合报告"
        if len(sys.argv) > 2:
            period_label = sys.argv[2]
        print(generate_full_report(indices, results, period_label))
    
    else:
        print("未知命令")


path: reports/prompts.md
content: # Cron 任务 Prompt 模板

本文档包含定时任务使用的标准 Prompt 模板。

---

## 数据获取策略（核心优化）

### 多渠道数据获取优先级

**行情数据（必须按顺序执行）：**
1. 主渠道：`python stock_monitor.py analyze <code>` — 腾讯行情API，权威数据
2. 备选渠道1：重试2次，间隔3秒
3. 备选渠道2：`python stock_monitor.py quote <code>` — 简单行情查询
4. 备选渠道3：`web_fetch`直接抓取东方财富/新浪个股页面（禁止使用百度搜索）

**指数数据：**
1. 主渠道：`python stock_monitor.py index`
2. 备选：重试2次
3. 备选：使用tavily_search搜索"上证指数+深证成指+创业板+今日收盘"

### 资讯获取优先级

**A股资讯：**
1. 东方财富（主力）：`https://quote.eastmoney.com/concept/s{代码}.html`
2. 同花顺：`https://stockpage.10jqka.com.cn/{代码}/`
3. 雪球：`https://xueqiu.com/S/SH{代码}`
4. 新浪财经：`https://finance.sina.com.cn/realstock/company/sh{代码}/nc.shtml`
5. 备选：`web_fetch`直接抓取东方财富/新浪页面

**港股资讯：**
1. 东方财富港股：`https://quote.eastmoney.com/concept/sz{代码}.html`
2. 雪球港股：`https://xueqiu.com/S/HK{代码}`
3. 新浪财经港股：`https://finance.sina.com.cn/realstock/company/hk{代码}/nc.shtml`
4. Yahoo Finance港股：`https://hk.finance.yahoo.com/quote/{代码}.HK/`
4. 备选：`web_fetch`直接抓取东方财富/新浪页面

### 重试机制

```
数据获取失败处理流程：
1. 首次尝试 → 失败
2. 等待3秒 → 重试第1次 → 失败
3. 等待3秒 → 重试第2次 → 失败
4. 标记"数据获取异常" → 使用上一个有效数据
5. 记录错误日志
6. 继续处理下一只股票（不阻塞整体流程）
```

---

## A股晚间综合报告（优化版）

```markdown
A股晚间综合报告任务：

## 第一步：大盘指数获取
1. 执行 `python C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts\stock_monitor.py index` 获取大盘实时指数
2. 若执行失败，等待3秒重试，共重试2次
3. 若仍失败，使用tavily_search搜索"上证指数 深证成指 创业板 沪深300 今日收盘 2026年4月"作为补充
4. 【重要】记录数据来源和获取时间

## 第二步：读取监控池
1. 读取 `C:\Users\Administrator\.openclaw\stock-pool.json` 获取A股股票列表
2. 读取 `C:\Users\Administrator\.openclaw\stock-positions.json` 获取持仓数据
3. 区分"持仓股"和"观察股"，报告分类展示

## 第三步：技术面数据获取
1. 对监控池每只股票执行 `python stock_monitor.py analyze <code>`
2. analyze 命令返回完整的 JSON 数据，包含：
   - 现价/涨跌幅（来自腾讯行情）
   - MA均线（MA5/MA10/MA20/MA60）
   - MACD（DIF/DEA/信号）
   - KDJ（K/D/J值）
   - RSI（RSI6/RSI12/RSI24）
   - BOLL（上轨/中轨/下轨/位置）
   - OBV（信号）
   - DMI（+DI/-DI/ADX/信号）
   - WR威廉指标
   - 综合信号（买入/卖出评分）
3. 【禁止】自己写Python脚本计算指标，必须直接使用analyze返回结果
4. 若单只股票分析失败：
   - 等待3秒重试
   - 重试2次仍失败，标记"数据获取异常"，继续处理下一只
5. 使用 `quote <code>` 作为备选获取简单行情

## 第四步：资讯获取（多渠道）
对每只持仓股执行：
1. 东方财富：`web_fetch(url="https://quote.eastmoney.com/concept/s{代码}.html")`
2. 若失败，等待2秒重试1次
3. 若仍失败，尝试同花顺、雪球
4. 若全部失败，使用tavily_search搜索相关资讯

## 第五步：报告生成规范

### 标题格式
🌙 【A股市场晚报】{YYYY-MM-DD HH:MM}

### 大盘指数板块
【大盘指数】
| 指数 | 点位 | 涨跌 | 数据来源 |
|------|------|------|----------|
| 上证指数 | xxx | ±x.xx% | 腾讯行情 |
| 深证成指 | xxx | ±x.xx% | 腾讯行情 |
| 创业板指 | xxx | ±x.xx% | 腾讯行情 |
| 沪深300 | xxx | ±x.xx% | 腾讯行情 |

### 持仓股分析板块（必须包含）
**【持仓股分析】**

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：±x.xx%（数据来源：腾讯行情 {时间}）
- 持仓成本：xx.xx | 盈亏：{颜色}{±xxx元(±x.xx%)}
- 技术信号（来自analyze命令，直接读取，禁止重复计算）：
  - 均线：MA5={xx} / MA10={xx} / MA20={xx} / MA60={xx} → {多头✓/空头✗}
  - MACD：DIF={xx} DEA={xx} → {金叉✓/死叉✗} {红柱/绿柱}
  - KDJ：K={xx} D={xx} J={xx} → {金叉✓/死叉✗/超买/超卖}
  - RSI：RSI6={xx} RSI12={xx} RSI24={xx}（{超买⚠️/超卖✓/正常}）
  - BOLL：上轨={xx} 中轨={xx} 下轨={xx} → {突破上轨⚠️/中轨附近/下轨下方✓}
  - OBV：{数值} → {上行✓/下行✗}
  - DMI：+DI={xx} -DI={xx} ADX={xx}
  - WR：{xx}（{超买⚠️/超卖✓/正常}）
  - 综合信号：{强烈买入✓✓/建议买入✓/观望/建议卖出✗/强烈卖出✗✗}（买入x/卖出x）
- 关键点位（来自analyze命令，禁止自行计算）：
  - 买入区间：{xxx}-{xxx}（支撑位附近±2%分批建仓）
  - 卖出区间：{xxx}-{xxx}（阻力位附近±3%分批止盈）
  - 止损位：{xxx}（支撑下方3%）
  - 目标位：{xxx}
  - 支撑位：{xxx} | 阻力位：{xxx}

② ...（其他持仓股）

### 观察股分析板块
**【观察股】**（仅展示信号，不展示持仓信息）

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：±x.xx%
- 核心信号：MA20({多头✓/空头✗}) MACD({金叉✓/死叉✗}) KDJ({金叉✓/死叉✗}) RSI({数值}) BOLL({位置})
- 综合信号：{信号评级}（买入x/卖出x）
- 关键点位：买入区间{xxx}-{xxx} | 止损{xxx} | 目标{xxx}
- 简评：{简短的评论}

### 资讯板块
**【重要资讯】**
• {股票名称}：{资讯摘要}（来源:{来源} | 时间:{时间}）
• {分析预测}：{基于资讯的分析判断}

### 明日展望板块
**【明日展望】**
• 关注重点：{重点关注事项}
• 技术支撑/阻力：{关键点位}
• 操作建议：{基于综合信号的建议}

## 第六步：报告推送
- 使用 message 工具发送到飞书群聊
- 报告末尾标注：🧠 数据来源：腾讯行情API（实时）| 资讯来源：东方财富/同花顺/雪球 | 生成时间：{时间}

---

## 港股晚间综合报告（优化版）

```markdown
港股晚间综合报告任务：

## 第一步：大盘指数获取
1. 执行 `python stock_monitor.py index` 获取A股大盘指数
2. 使用tavily_search搜索"恒生指数 恒生科技 恒生国企 今日收盘 2026年4月"获取港股三大指数
3. 若腾讯行情成功，使用腾讯数据；否则使用搜索数据
4. 【重要】记录数据来源和获取时间

## 第二步：读取监控池
1. 读取 `C:\Users\Administrator\.openclaw\stock-pool.json` 获取港股列表
2. 读取 `C:\Users\Administrator\.openclaw\stock-positions.json` 获取持仓数据
3. 区分"持仓股"和"观察股"，报告分类展示

## 第三步：技术面数据获取
同A股流程，使用 `python stock_monitor.py analyze <code>` 获取数据

## 第四步：资讯获取
港股资讯URL：
- 东方财富港股：`https://quote.eastmoney.com/concept/sz{代码}.html`
- 雪球港股：`https://xueqiu.com/S/HK{代码}`
- 新浪财经港股：`https://finance.sina.com.cn/realstock/company/hk{代码}.shtml`

## 第五步：报告生成
参照A股晚间报告格式，港股持仓股须包含：

① {股票名称}（{代码}）
- 现价：xx.xx港元 | 涨跌幅：±x.xx%（数据来源：腾讯行情 {时间}）
- 持仓成本：xx.xx | 盈亏：{颜色}{±xxx港元(±x.xx%)}
- 技术信号：
  - 均线：MA5={xx} / MA10={xx} / MA20={xx} / MA60={xx} → {多头✓/空头✗}
  - MACD：DIF={xx} DEA={xx} → {金叉✓/死叉✗} {红柱/绿柱}
  - KDJ：K={xx} D={xx} J={xx} → {金叉✓/死叉✗}
  - RSI：RSI6={xx} RSI12={xx} RSI24={xx}（{超买⚠️/超卖✓/正常}）
  - BOLL：上轨={xx} 中轨={xx} 下轨={xx} → {突破上轨⚠️/中轨附近/下轨下方✓}
  - 综合信号：{信号评级}（买入x/卖出x）
- 关键点位：
  - 买入区间：{xxx}-{xxx}（支撑位附近±2%分批建仓）
  - 卖出区间：{xxx}-{xxx}（阻力位附近±3%分批止盈）
  - 止损位：{xxx}（支撑下方3%）
  - 目标位：{xxx}
  - 支撑位：{xxx} | 阻力位：{xxx}

## 第六步：推送
同A股流程

---

## 盘中监控报告（优化版）

盘中监控报告需额外增加：
1. **异动提醒**：涨跌幅超过±3%的股票突出显示
2. **实时信号变化**：与上一次报告对比，标注信号变化
3. **预警触发**：BOLL突破、RSI超买超卖、MACD金叉死叉等关键信号
4. **关键点位**（每只持仓股必须包含）：
   - 买入区间：{xxx}-{xxx} | 止损位：{xxx}
   - 目标位：{xxx} | 支撑位：{xxx} | 阻力位：{xxx}
5. **操作建议**：根据信号给出明确买入/卖出/持有建议

---

## 报告质量检查清单

生成报告前必须检查：
- [ ] 大盘指数数据是否来自腾讯行情API（权威来源）
- [ ] 所有现价/涨跌幅是否来自脚本返回数据（非搜索结果）
- [ ] 每只股票是否包含完整的技术指标（MA/MACD/KDJ/RSI/BOLL/OBV/DMI/WR）
- [ ] 综合信号计算是否正确（买入-卖出差值对应评级）
- [ ] 持仓盈亏计算是否正确
- [ ] 资讯来源和时间是否标注
- [ ] 数据获取异常的是否有明确标注
- [ ] 每只持仓股是否包含【关键点位】（买入区间/卖出区间/止损位/目标位/支撑位/阻力位）

---

## Agent Reach 多渠道获取示例

当主渠道失败时，可使用以下Agent Reach命令：

```bash
# 使用Jina提取网页内容（适合东方财富、同花顺等）
# （已移除第三方提取器，使用web_fetch替代）

# 使用Exa搜索最新股价和资讯
# （已移除第三方搜索，使用tavily_search替代）

# 使用Tavily搜索（已在用）
tavily_search(query: "长江电力 600900 最新资讯 2026", topic: "finance", max_results: 3)
```

---

## 综合信号评级标准（优化）

| 评分差 | 信号 | 颜色 | 说明 |
|--------|------|------|------|
| buy >= sell + 4 | ⭐⭐⭐ 强烈买入 | 🔴 | 多指标正向共振 |
| buy >= sell + 2 | ⭐⭐ 建议买入 | 🔴 | 多数指标偏正 |
| abs差值 <= 1 | ⚪ 观望 | 🟡 | 多空均衡 |
| sell >= buy + 2 | ⭐ 建议卖出 | 🟢 | 多数指标偏负 |
| sell >= buy + 4 | ⭐⭐ 强烈卖出 | 🟢 | 多指标负向共振 |

**超买超卖特殊规则：**
- RSI6 > 80 → KDJ超买额外加权
- RSI6 < 20 → KDJ超卖额外加权
- BOLL突破上轨 → 预警⚠️
- BOLL跌破下轨 → 预警⚠️

---

## 错误日志格式

当数据获取失败时，记录格式：
```
[ERROR] {时间} | {股票代码} | 获取失败 | 尝试次数:{n} | 错误原因:{原因} | 备选方案:{使用的备选数据}
[WARN] {时间} | {股票代码} | 数据延迟 | 延迟:{x}秒
[INFO] {时间} | 报告生成完成 | 股票数量:{n} | 失败数量:{m}
```

---

## 执行要点总结

1. **数据权威性**：腾讯行情API为核心，搜索数据仅作补充
2. **重试机制**：3-3-2原则（3秒间隔，最多2次重试）
3. **多渠道备份**：主渠道→备选渠道1→备选渠道2→标记异常
4. **容错设计**：单只失败不阻塞整体，继续处理其他股票
5. **透明标注**：数据来源、获取时间、异常标注清晰可见
6. **信号准确**：综合信号基于脚本计算的score，而非主观判断


path: reports/templates.md
content: # 报告模板

## 报告结构规范

所有报告必须包含以下核心模块：

1. **标题栏** - 报告类型 + 日期时间 + 数据来源标注
2. **大盘指数** - 核心指数实时涨跌（含数据来源）
3. **持仓分析** - 持仓股涨跌 + 盈亏 + 技术信号
4. **观察股分析** - 非持仓股监控信号
5. **资讯摘要** - 重要新闻（含来源时间）+ 分析预测
6. **明日展望** - 技术信号 + 操作建议

---

## 晚报模板（优化版）

```markdown
🌙 【市场晚报】{YYYY-MM-DD HH:MM}
⚠️ 动态止盈提醒：回撤5%减仓、10%清仓是建议，根据市场灵活调整

━━━━━━━━━━━━━━━━━━━━

【大盘指数】
| 指数 | 点位 | 涨跌 | 数据来源 |
|------|------|------|----------|
| 上证指数 | x,xxx.xx | ±x.xx% | 腾讯行情 |
| 深证成指 | x,xxx.xx | ±x.xx% | 腾讯行情 |
| 创业板指 | x,xxx.xx | ±x.xx% | 腾讯行情 |
| 沪深300 | x,xxx.xx | ±x.xx% | 腾讯行情 |

【港股大盘】
| 指数 | 点位 | 涨跌 | 数据来源 |
|------|------|------|----------|
| 恒生指数 | xx,xxx | ±x.xx% | 腾讯行情 |
| 恒生科技 | x,xxx | ±x.xx% | 腾讯行情 |
| 恒生国企 | x,xxx | ±x.xx% | 腾讯行情 |

━━━━━━━━━━━━━━━━━━━━

【持仓股分析】

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：{颜色}±x.xx%（数据来源：腾讯行情 {时间}）
- 持仓：{数量}股 | 成本：xx.xx | 盈亏：{颜色}{±xxx元(±x.xx%)}
━━━━━━━━━━━━━━━━━━━━
【技术信号】
| 指标 | 数值 | 信号 |
|------|------|------|
| MA均线 | MA5({xx}) MA10({xx}) MA20({xx}) | {多头✓/空头✗} |
| MACD | DIF={xx} DEA={xx} | {金叉✓/死叉✗} |
| KDJ | K={xx} D={xx} J={xx} | {金叉✓/死叉✗} |
| RSI | RSI6={xx} RSI12={xx} RSI24={xx} | {超买/超卖/正常} |
| BOLL | 上轨{xx}/中轨{xx}/下轨{xx} | {突破/位置} |
| OBV | {数值} | {上行✓/下行✗} |
━━━━━━━━━━━━━━━━━━━━
综合信号：{信号评级}（买入{x}/卖出{x}）
━━━━━━━━━━━━━━━━━━━━

② {第二只持仓股...}

━━━━━━━━━━━━━━━━━━━━

【观察股分析】（仅展示信号，不展示持仓）

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：±x.xx%
- 综合信号：{信号评级} | 技术点评：{简评}

━━━━━━━━━━━━━━━━━━━━

【重要资讯】

• {股票名称}：{资讯摘要}
  来源：{来源} | 时间：{时间}
  📊 分析预测：{基于资讯的判断}

• {第二只股票资讯...}

━━━━━━━━━━━━━━━━━━━━

【明日展望】

• 关注重点：{重点关注事项}
• 技术支撑/阻力：上证 {支撑} / {阻力}
• 操作建议：
  - {股票名称}：{建议内容}
  - {股票名称}：{建议内容}

━━━━━━━━━━━━━━━━━━━━

💼 持仓汇总：
| 股票 | 持仓 | 成本 | 现价 | 盈亏 |
|------|------|------|------|------|
| {名称} | {数量}股 | {成本} | {现价} | {颜色}{±xxx元} |

总盈亏：{颜色}{±xxx元(±x.xx%)}

━━━━━━━━━━━━━━━━━━━━
⚠️ 本报告仅供参考，不构成投资建议
🧠 数据来源：腾讯行情API | 资讯来源：东方财富/同花顺/雪球 | 生成时间：{时间}
```

---

## 午报模板（优化版）

```markdown
☀️ 【市场午报】{YYYY-MM-DD HH:MM}
⚠️ 动态止盈提醒：回撤5%减仓、10%清仓是建议，根据市场灵活调整

━━━━━━━━━━━━━━━━━━━━

【大盘指数】
| 指数 | 点位 | 涨跌 | 数据来源 |
|------|------|------|----------|
| 上证指数 | x,xxx.xx | ±x.xx% | 腾讯行情 |
| 深证成指 | x,xxx.xx | ±x.xx% | 腾讯行情 |
| 创业板指 | x,xxx.xx | ±x.xx% | 腾讯行情 |

━━━━━━━━━━━━━━━━━━━━

【持仓股表现】

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：{颜色}±x.xx%
- 持仓成本：xx.xx | 盈亏：{颜色}{±xxx元(±x.xx%)}
- 半天走势：{走势描述}
━━━━━━━━━━━━━━━━━━━━
综合信号：{信号评级}（买入{x}/卖出{x}）
技术亮点：{KDJ金叉✓/RSI超买⚠️/等}
━━━━━━━━━━━━━━━━━━━━

【午后展望】
• 下午关注：{关注事项}
• 技术位：上证守住{x}，突破{x}

━━━━━━━━━━━━━━━━━━━━
⚠️ 本报告仅供参考，不构成投资建议
🧠 数据来源：腾讯行情API | 生成时间：{时间}
```

---

## 盘中监控模板（优化版）

```markdown
📊 【盘中监控】{YYYY-MM-DD HH:MM}
⚠️ 动态止盈提醒：回撤5%减仓、10%清仓是建议，根据市场灵活调整

━━━━━━━━━━━━━━━━━━━━

【大盘动态】
| 指数 | 点位 | 涨跌 |
|------|------|------|
| 上证指数 | x,xxx.xx | ±x.xx% |
| 创业板指 | x,xxx.xx | ±x.xx% |

━━━━━━━━━━━━━━━━━━━━

【持仓股异动】

① {股票名称}（{代码}）
- 现价：xx.xx元 | 涨跌幅：{颜色}±x.xx%
- 持仓盈亏：{颜色}{±xxx元(±x.xx%)}
- 信号状态：{信号} | {变化标记}

━━━━━━━━━━━━━━━━━━━━

【技术信号提醒】

🔔 新信号：
• {股票名称}：{新出现信号}

⚠️ 预警信号：
• {股票名称}：{超买/超卖/BOLL突破等}

━━━━━━━━━━━━━━━━━━━━

【操作建议】
• {股票名称}：{建议内容}

━━━━━━━━━━━━━━━━━━━━
⚠️ 本报告仅供参考，不构成投资建议
🧠 数据来源：腾讯行情API | 生成时间：{时间}
```

---

## 持仓分析模板（优化版）

```markdown
💼 【持仓分析】{YYYY-MM-DD HH:MM}

━━━━━━━━━━━━━━━━━━━━

【持仓概况】
| 代码 | 名称 | 数量 | 成本价 | 现价 | 市值 | 盈亏 |
|------|------|------|--------|------|------|------|
| xxxxxx | xxx | xxxx | xx.xx | xx.xx | ¥xx,xxx | {颜色}±x.x% |

💼 总持仓市值：¥xxx,xxx | {颜色}总盈亏：±xxx(±x.x%)

━━━━━━━━━━━━━━━━━━━━

【个股详细分析】

{颜色} {股票名称}({代码})
━━━━━━━━━━━━━━━━━━━━
💰 现价：¥xx.xx {颜色}{±x.xx%}
📊 持仓：{数量}股 | 成本：¥xx.xx | 盈亏：{颜色}{±xxx元(±x.xx%)}
━━━━━━━━━━━━━━━━━━━━
【技术指标】
| 指标 | 数值 | 信号 |
|------|------|------|
| MA均线 | MA5={xx} MA10={xx} MA20={xx} | {多头✓/空头✗} |
| MACD | DIF={xx} DEA={xx} MACD={xx} | {金叉✓/死叉✗} |
| KDJ | K={xx} D={xx} J={xx} | {金叉✓/死叉✗/超卖✓} |
| RSI | RSI6={xx} RSI12={xx} RSI24={xx} | {超买⚠️/超卖✓/正常} |
| BOLL | 上={xx} 中={xx} 下={xx} | {突破上轨⚠️/中轨/下轨✓} |
| OBV | {数值} OBV10日={xx} | {上行✓/下行✗} |
| DMI | +DI={xx} -DI={xx} ADX={xx} | {趋势/震荡} |
| WR | {xx} | {超买⚠️/超卖✓/正常} |
━━━━━━━━━━━━━━━━━━━━
【综合信号】
🎯 信号：{信号评级}（买入{x}/卖出{x}）
💡 建议：{建议内容}
━━━━━━━━━━━━━━━━━━━━
【关键点位】
  买入区间：{xxx}-{xxx}（分批建仓）
  卖出区间：{xxx}-{xxx}（分批止盈）
  止损位：{xxx} | 目标位：{xxx}
  支撑位：{xxx} | 阻力位：{xxx}
  均线区间：MA20={xx} / MA60={xx}
━━━━━━━━━━━━━━━━━━━━
【资讯摘要】
📰 {资讯内容}
   来源：{来源} | 时间：{时间}
📊 资讯影响：{正面/负面/中性} | 影响程度：{高/中/低}
━━━━━━━━━━━━━━━━━━━━

【成本分析】
• 距成本：{颜色}{±x.xx%}
• 距止损：{颜色}{±x.xx%}（止损价{xx}）
• 距止盈：{颜色}{±x.xx%}（止盈价{xx}）

━━━━━━━━━━━━━━━━━━━━
⚠️ 本报告仅供参考，不构成投资建议
```

---

## 综合评级模板（优化版）

```markdown
【📋 监控池综合评级】

| 排名 | 股票 | 现价 | 涨跌 | 综合信号 | 评分 | 风险提示 |
|------|------|------|------|----------|------|----------|
| 1 | {名称}({代码}) | xx.xx | ±x.xx% | ⭐⭐⭐强烈买入 | +x | {RSI超买⚠️/等} |
| 2 | {名称}({代码}) | xx.xx | ±x.xx% | ⭐建议买入 | +x | - |
| 3 | {名称}({代码}) | xx.xx | ±x.xx% | ⚪观望 | 0 | - |
| 4 | {名称}({代码}) | xx.xx | ±x.xx% | ⭐建议卖出 | -x | - |
| 5 | {名称}({代码}) | xx.xx | ±x.xx% | ⭐⭐强烈卖出 | -x | {BOLL跌破⚠️/等} |

【信号说明】
• ⭐⭐⭐/⭐⭐ 强烈买入/建议买入：多个正向指标共振
• ⚪ 观望：多空均衡，等待信号明确
• ⭐/⭐⭐ 建议卖出/强烈卖出：多个负向指标共振

【风险提示说明】
• RSI超买⚠️：RSI6>70，短期可能有回调压力
• RSI超卖✓：RSI6<30，可能存在反弹机会
• BOLL突破上轨⚠️：价格强势但注意回调风险
• BOLL跌破下轨✓：超卖区域，可能反弹
• MACD死叉✗：短期趋势转弱

【多空信号统计】
• 多头信号：{x}个（MA多头、MACD金叉、KDJ金叉等）
• 空头信号：{x}个（MA空头、MACD死叉、KDJ死叉等）
• 中性信号：{x}个（BOLL轨道、DMI震荡等）
```

---

## 报告数据来源标注规范

所有报告必须在结尾标注数据来源：

```
🧠 数据来源：
• 行情数据：腾讯行情API（权威）
• 指数数据：腾讯行情API（实时）
• 资讯来源：东方财富 / 同花顺 / 雪球 / 新浪财经（按优先级）
• 备选来源：Tavily搜索（当主渠道失败时）
• 生成时间：{YYYY-MM-DD HH:MM:SS}
• 报告编号：{自动编号}
```

---

## 报告质量自检清单

生成报告前必须逐项检查：

**数据准确性：**
- [ ] 大盘指数数据来自腾讯行情API
- [ ] 所有现价/涨跌幅来自脚本返回数据
- [ ] 持仓盈亏计算正确
- [ ] 数据获取时间已标注

**信号准确性：**
- [ ] 每只股票包含6个核心指标
- [ ] 综合信号计算正确（buy-sell差值对应评级）
- [ ] 超买超卖标注正确

**完整性：**
- [ ] 持仓股与观察股分类清晰
- [ ] 资讯包含来源和时间
- [ ] 明日展望包含操作建议

**格式规范：**
- [ ] 涨跌颜色符合中国习惯（红涨绿跌）
- [ ] 技术指标使用标准符号
- [ ] 数据保留合适的小数位数


path: references/commands.md
content: # 命令速查表

## 基本格式

```bash
python stock_monitor.py <command> [args]
```

工作目录：`C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts`

---

## 行情查询

### 查询股票行情
```bash
python stock_monitor.py quote <code>
```
**示例：**
```bash
python stock_monitor.py quote 600900    # A股
python stock_monitor.py quote 00992      # 港股
```

### 查询大盘指数
```bash
python stock_monitor.py index
```

---

## 技术分析

### 分析股票技术指标
```bash
python stock_monitor.py analyze <code>
```
**示例：**
```bash
python stock_monitor.py analyze 600900
python stock_monitor.py analyze 00992
```

返回：MA、MACD、KDJ、RSI、BOLL、OBV、DMI、WR、支撑阻力、趋势强度

---

## 监控池管理

### 添加股票到监控池
```bash
python stock_monitor.py add <code>
```
**示例：**
```bash
python stock_monitor.py add 600900
```

### 从监控池移除
```bash
python stock_monitor.py remove <code>
```
**示例：**
```bash
python stock_monitor.py remove 600900
```

### 查看监控池列表
```bash
python stock_monitor.py list
```

### 监控所有股票（含技术指标）
```bash
python stock_monitor.py monitor
```

### 仅监控A股
```bash
python stock_monitor.py monitor-a
```

### 仅监控港股
```bash
python stock_monitor.py monitor-hk
```

---

## 综合报告

### 生成综合分析报告
```bash
python stock_monitor.py report [标签]
```
**示例：**
```bash
python stock_monitor.py report           # 生成完整报告
python stock_monitor.py report mytag     # 带标签
```

**报告内容：**
- 大盘指数概览（上证、深证、创业板、沪深300等）
- 市场情绪（资讯积极/消极评分）
- 持仓股详细分析（支撑阻力+趋势+指标+资讯+建议）
- 关注股分析
- 综合评级排序

### 仅生成A股报告
```bash
python stock_monitor.py report-a [标签]
```

### 仅生成港股报告
```bash
python stock_monitor.py report-hk [标签]
```

---

## 持仓管理

### 查看持仓列表
```bash
python stock_monitor.py position list
```

### 添加持仓
```bash
python stock_monitor.py position add <code> <quantity> <cost_price>
```
**示例：**
```bash
python stock_monitor.py position add 600900 6000 28.69
# 格式：position add 股票代码 数量 成本价
```

### 清除持仓
```bash
python stock_monitor.py position remove <code>
```
**示例：**
```bash
python stock_monitor.py position remove 600900
```

### 分析单只持仓
```bash
python stock_monitor.py position <code>
```
**示例：**
```bash
python stock_monitor.py position 600900
```

---

## 交易记录

### 记录买入
```bash
python stock_monitor.py trade buy <code> <quantity> <price>
```
**示例：**
```bash
python stock_monitor.py trade buy 600900 1000 28.50
# 格式：trade buy 股票代码 数量 价格
```

### 记录卖出
```bash
python stock_monitor.py trade sell <code> <quantity> <price>
```
**示例：**
```bash
python stock_monitor.py trade sell 600900 500 30.00
```

### 查看交易记录
```bash
python stock_monitor.py trades [code]
```
**示例：**
```bash
python stock_monitor.py trades              # 查看全部交易记录
python stock_monitor.py trades 600900        # 查看特定股票交易记录
```

---

## 资金流向

### 查看资金流向
```bash
python stock_capital.py <code>
```
**示例：**
```bash
python stock_capital.py 600900
```

**返回数据：**
- 主力净流入/净流出
- 超大单净流入/净流出
- 大单净流入/净流出
- 中单净流入/净流出
- 小单净流入/净流出

---

## 常用场景

### 场景1：每日开盘前检查
```bash
python stock_monitor.py index              # 查看大盘
python stock_monitor.py report             # 生成完整持仓报告
```

### 场景2：盘中监控
```bash
python stock_monitor.py monitor            # 监控所有股票
```

### 场景3：收盘后分析
```bash
python stock_monitor.py report             # 生成综合报告
```

### 场景4：新买入股票后记录
```bash
python stock_monitor.py position add <code> <quantity> <cost_price>
python stock_monitor.py add <code>          # 添加到监控池
```


path: references/config.md
content: # 配置文件说明

## 概述

股票监控技能使用 JSON 格式的配置文件存储数据，存放在 `~/.openclaw/` 目录下。

---

## 股票池配置

**文件路径：** `~/.openclaw/stock-pool.json`

**说明：** 定义需要监控的股票列表

**结构：**
```json
{
  "stocks": {
    "600900": { "name": "长江电力", "type": "A" },
    "00992": { "name": "联想集团", "type": "HK" }
  },
  "updated": "2026-03-29T10:00:00.000000"
}
```

**字段说明：**
| 字段 | 类型 | 说明 |
|------|------|------|
| stocks | object | 股票字典，key为代码 |
| name | string | 股票名称 |
| type | string | 股票类型：`A`(A股)、`HK`(港股) |
| updated | string | 最后更新时间 |

**操作命令：**
```bash
python stock_monitor.py add 600900      # 添加
python stock_monitor.py remove 600900    # 移除
python stock_monitor.py list              # 列出全部
```

---

## 持仓记录

**文件路径：** `~/.openclaw/stock-positions.json`

**说明：** 记录当前持有的股票及成本

**结构：**
```json
{
  "positions": {
    "600900": {
      "quantity": 6000.0,
      "cost_price": 28.69,
      "updated": "2026-03-27T09:56:00.931391"
    },
    "00992": {
      "quantity": 4000.0,
      "cost_price": 9.41,
      "updated": "2026-03-27T09:56:11.939610"
    }
  },
  "updated": "2026-03-27T09:58:34.076944"
}
```

**字段说明：**
| 字段 | 类型 | 说明 |
|------|------|------|
| positions | object | 持仓字典，key为股票代码 |
| quantity | number | 持股数量 |
| cost_price | number | 成本价 |
| updated | string | 最后更新时间 |

**操作命令：**
```bash
python stock_monitor.py position add 600900 6000 28.69  # 添加/更新持仓
python stock_monitor.py position remove 600900           # 清除持仓
python stock_monitor.py position list                     # 查看持仓
python stock_monitor.py position 600900                   # 分析单只持仓
```

---

## 交易记录

**文件路径：** `~/.openclaw/stock-trades.json`

**说明：** 记录所有买卖交易历史

**结构：**
```json
{
  "trades": [
    {
      "id": "t_1701234567890",
      "code": "600900",
      "type": "buy",
      "quantity": 1000,
      "price": 28.50,
      "date": "2026-03-20T10:30:00.000000"
    },
    {
      "id": "t_1701245678901",
      "code": "600900",
      "type": "sell",
      "quantity": 500,
      "price": 29.00,
      "date": "2026-03-25T14:20:00.000000"
    }
  ],
  "updated": "2026-03-25T14:20:00.000000"
}
```

**字段说明：**
| 字段 | 类型 | 说明 |
|------|------|------|
| trades | array | 交易列表 |
| id | string | 交易ID（时间戳生成） |
| code | string | 股票代码 |
| type | string | 交易类型：`buy` 或 `sell` |
| quantity | number | 交易数量 |
| price | number | 成交价格 |
| date | string | 成交时间 |

**操作命令：**
```bash
python stock_monitor.py trade buy 600900 1000 28.50   # 记录买入
python stock_monitor.py trade sell 600900 500 29.00  # 记录卖出
python stock_monitor.py trades                        # 查看全部记录
python stock_monitor.py trades 600900                  # 查看特定股票记录
```

---

## 预警配置

**文件路径：** `~/.openclaw/stock-alerts.json`

**说明：** 定义各股票的预警规则

**结构：**
```json
{
  "alerts": {
    "600900": {
      "cost_pct_above": 15.0,
      "cost_pct_below": -12.0,
      "change_pct_above": 4.0,
      "change_pct_below": -4.0,
      "volume_surge": 2.0,
      "ma_monitor": true,
      "rsi_monitor": true,
      "gap_monitor": true,
      "trailing_stop": true
    }
  },
  "updated": "2026-03-29T10:00:00.000000"
}
```

**字段说明：**
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| cost_pct_above | number | 15.0 | 盈利达到此比例提醒（%） |
| cost_pct_below | number | -12.0 | 亏损达到此比例提醒（%） |
| change_pct_above | number | 4.0 | 日内涨幅超此比例提醒（%） |
| change_pct_below | number | -4.0 | 日内跌幅超此比例提醒（%） |
| volume_surge | number | 2.0 | 放量倍数阈值 |
| ma_monitor | boolean | true | 是否监控均线金叉/死叉 |
| rsi_monitor | boolean | true | 是否监控RSI超买超卖 |
| gap_monitor | boolean | true | 是否监控跳空缺口 |
| trailing_stop | boolean | true | 是否启用动态止盈 |

---

## 资金流向配置

**文件路径：** `~/.openclaw/stock-capital-config.json`

**说明：** 资金流向分析的配置参数

**结构：**
```json
{
  "config": {
    "threshold_large": 1000000,
    "threshold_huge": 5000000,
    "refresh_interval": 300
  }
}
```

---

## 快速操作汇总

| 任务 | 操作 | 文件 |
|------|------|------|
| 添加股票到监控 | `add <code>` | stock-pool.json |
| 查看监控列表 | `list` | stock-pool.json |
| 添加/更新持仓 | `position add <code> <qty> <cost>` | stock-positions.json |
| 查看持仓 | `position list` | stock-positions.json |
| 记录买入 | `trade buy <code> <qty> <price>` | stock-trades.json |
| 记录卖出 | `trade sell <code> <qty> <price>` | stock-trades.json |
| 查看交易记录 | `trades [code]` | stock-trades.json |

---

## 备份与恢复

配置文件均为 JSON 格式，可直接复制备份：

```powershell
# 备份
Copy-Item "$env:USERPROFILE\.openclaw\stock-pool.json" "stock-pool.json.bak"
Copy-Item "$env:USERPROFILE\.openclaw\stock-positions.json" "stock-positions.json.bak"

# 恢复
Copy-Item "stock-pool.json.bak" "$env:USERPROFILE\.openclaw\stock-pool.json"
Copy-Item "stock-positions.json.bak" "$env:USERPROFILE\.openclaw\stock-positions.json"
```


path: references/index.md
content: # stock-monitor 技能参考文档

## 目录

| 文档 | 说明 |
|------|------|
| [commands.md](./commands.md) | CLI 命令速查，包含所有命令及示例 |
| [config.md](./config.md) | 配置文件结构说明及操作指南 |
| [troubleshooting.md](./troubleshooting.md) | 常见问题排查与解决方案 |

---

## 报告模板

| 文档 | 说明 |
|------|------|
| [reports/templates.md](../reports/templates.md) | 各类报告模板（早报/午报/晚报/盘中） |
| [reports/prompts.md](../reports/prompts.md) | Cron任务标准Prompt模板 |

---

## 快速开始

### 1. 查看帮助
```bash
cd C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts
python stock_monitor.py
```

### 2. 生成综合报告
```bash
python stock_monitor.py report
```

### 3. 管理持仓
```bash
python stock_monitor.py position list              # 查看持仓
python stock_monitor.py position add 600900 6000 28.69  # 添加持仓
```

---

## 核心文件

| 文件 | 位置 |
|------|------|
| 主脚本 | `scripts/stock_monitor.py` |
| 资金流向 | `scripts/stock_capital.py` |
| 配置文件 | `~/.openclaw/stock-*.json` |

---

## 常用命令速查

```bash
# 行情
python stock_monitor.py quote 600900     # 查询行情
python stock_monitor.py index            # 大盘指数

# 分析
python stock_monitor.py analyze 600900   # 技术分析
python stock_monitor.py monitor-a        # A股监控
python stock_monitor.py monitor-hk       # 港股监控
python stock_monitor.py report           # 综合报告

# 持仓
python stock_monitor.py position list    # 查看持仓
python stock_monitor.py position add <code> <qty> <cost>  # 添加持仓
python stock_monitor.py position remove <code>             # 清除持仓

# 交易
python stock_monitor.py trade buy <code> <qty> <price>   # 记录买入
python stock_monitor.py trade sell <code> <qty> <price>  # 记录卖出
python stock_monitor.py trades                              # 交易记录
```

---

## 配置文件位置

| 配置 | 路径 |
|------|------|
| 股票池 | `~/.openclaw/stock-pool.json` |
| 持仓 | `~/.openclaw/stock-positions.json` |
| 交易记录 | `~/.openclaw/stock-trades.json` |
| 预警配置 | `~/.openclaw/stock-alerts.json` |

---

## 定时任务

当前配置的定时任务：

| 任务 | 时间 | 说明 |
|------|------|------|
| 股票早报-A股 | 9:00 (周一至周五) | 生成A股早间报告 |
| 股票早报-港股 | 9:20 (周一至周五) | 生成港股早间报告 |
| 股票午报-A股 | 12:00 (周一至周五) | 生成A股午间报告 |
| 股票午报-港股 | 12:10 (周一至周五) | 生成港股午间报告 |
| A股监控-上午 | 10:05,10:35 (周一至周五) | A股盘中监控 |
| 港股监控-上午 | 10:10,10:35 (周一至周五) | 港股盘中监控 |
| A股监控-下午 | 13:05,13:35 (周一至周五) | A股盘中监控 |
| 港股监控-下午 | 13:10,13:40,14:10,14:40 (周一至周五) | 港股盘中监控 |
| 股票晚报-A股 | 15:20 (周一至周五) | 生成A股晚间报告 |
| 股票晚报-港股 | 16:10 (周一至周五) | 生成港股晚间报告 |

---

## 重要规则

**【强制】数据优先级**

报告中所有"现价/涨跌幅/涨跌额"必须使用脚本返回的数据，禁止使用搜索结果中的价格。

执行顺序：
1. 脚本行情数据（`index`/`monitor-a`/`monitor-hk`）— 权威数据
2. 搜索资讯数据（`web_fetch`）— 仅用于资讯分析


path: references/scheduled-tasks.md
content: # 定时任务说明

## 概述

stock-monitor 技能通过 OpenClaw cron 系统执行定时任务，实现自动化的股票监控报告推送。

---

## 当前定时任务配置

| 序号 | 任务名称 | 执行时间 (北京时间) | Cron表达式 | 状态 | 优化说明 |
|------|----------|-------------------|------------|------|----------|
| 1 | 股票早报-A股 | **9:35** (周一至周五) | `35 9 * * 1-5` | ✅ 正常 | ⭐ 调整为9:35开盘后获取数据 |
| 2 | 股票早报-港股 | **9:50** (周一至周五) | `50 9 * * 1-5` | ✅ 正常 | ⭐ 调整为9:50开盘后获取数据 |
| 3 | 股票午报-A股 | 12:00 (周一至周五) | `0 12 * * 1-5` | ✅ 正常 | ⭐ 增加上午行情回顾 |
| 4 | 股票午报-港股 | 12:10 (周一至周五) | `10 12 * * 1-5` | ✅ 正常 | ⭐ 增加上午行情回顾 |
| 5 | A股监控-上午 | 10:05, 10:35 (周一至周五) | `5,35 10-11 * * 1-5` | ✅ 正常 | ⭐ 增加系统性风险预警 |
| 6 | 港股监控-上午 | 10:10, 10:35 (周一至周五) | `10,35 10,11 * * 1-5` | ✅ 正常 | ⭐ 增加系统性风险预警+汇率风险 |
| 7 | A股监控-下午 | 13:05, 13:35 (周一至周五) | `5,35 13-14 * * 1-5` | ✅ 正常 | ⭐ 增加系统性风险预警 |
| 8 | 港股监控-下午 | 13:10, 13:40, 14:10, 14:40 (周一至周五) | `10,40 13-15 * * 1-5` | ✅ 正常 | ⭐ 增加系统性风险预警+汇率风险 |
| 9 | 股票晚报-A股 | 15:00 (周一至周五) | `0 15 * * 1-5` | ✅ 正常 | ⭐ 增加全天行情回顾+周度总结 |
| 10 | 股票晚报-港股 | 16:10 (周一至周五) | `10 16 * * 1-5` | ✅ 正常 | ⭐ 增加全天行情回顾+周度总结+汇率风险 |

---

## Cron 表达式说明

### 格式
```
分 时 日 月 周
│  │  │  │  │
│  │  │  │  └─── 周 (1-7, 周一=1)
│  │  │  └────── 月 (1-12)
│  │  └───────── 日 (1-31)
│  └──────────── 时 (0-23)
└──────────────── 分 (0-59)
```

### 特殊字符
- `*` — 匹配任意值
- `,` — 列表分隔符 (如 `5,35` 表示5分和35分)
- `-` — 范围 (如 `10-11` 表示10点和11点)

### 示例
| 表达式 | 含义 |
|--------|------|
| `0 9 * * 1-5` | 每周一至周五 9:00 |
| `5,35 10-11 * * 1-5` | 每周一至周五 10:05 和 10:35 |
| `10,40 13-15 * * 1-5` | 每周一至周五 13:10, 13:40, 14:10, 14:40 |

---

## 执行流程

### 标准定时任务执行步骤

```
┌─────────────────────────────────────────────────────────────┐
│  1. 获取大盘指数                                              │
│     python stock_monitor.py index                            │
│     → 上证、深证、创业板、沪深300 等实时数据                    │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  2. 读取股票池配置                                            │
│     读取 ~/.openclaw/stock-pool.json                         │
│     → 获取股票列表（A股/港股分类）                             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  3. 获取技术面数据                                            │
│     python stock_monitor.py monitor-a   (A股)                │
│     python stock_monitor.py monitor-hk  (港股)               │
│     → 现价、涨跌幅、MACD、KDJ、RSI、BOLL等技术指标             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  4. 获取最新资讯 (可选)                                       │
│     web_fetch 抓取百度搜索结果                                │
│     关键词：股票名+代码+最新消息+2026                          │
│     → 资讯情感分析、重大公告                                   │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  5. 生成分析报告                                              │
│     结合技术指标 + 资讯分析 → 综合报告                         │
│     【重要】价格数据必须使用步骤3脚本返回的数据                  │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│  6. 推送报告                                                  │
│     发送至社交群         │
│     使用 message 工具，mode=announce                          │
└─────────────────────────────────────────────────────────────┘
```

---

## 配置文件

### 任务配置文件
```
~/.openclaw/cron/jobs.json
```

### 股票监控配置
| 文件 | 路径 | 说明 |
|------|------|------|
| 股票池 | `~/.openclaw/stock-pool.json` | 监控的股票列表 |
| 持仓 | `~/.openclaw/stock-positions.json` | 当前持仓 |
| 交易记录 | `~/.openclaw/stock-trades.json` | 交易历史 |
| 预警配置 | `~/.openclaw/stock-alerts.json` | 预警规则 |

---

## 执行命令参考

### 手动触发定时任务

定时任务由 OpenClaw cron 系统自动触发，无需手动执行。

如需手动测试，可直接执行任务中的命令：

```bash
# 1. 获取大盘指数
python C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts\stock_monitor.py index

# 2. 获取A股技术面
python C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts\stock_monitor.py monitor-a

# 3. 获取港股技术面
python C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts\stock_monitor.py monitor-hk

# 4. 生成完整报告
python C:\Users\Administrator\.openclaw\skills\stock-monitor\scripts\stock_monitor.py report
```

---

## 管理命令

### 查看定时任务状态
```bash
openclaw cron list
```

### 查看 OpenClaw 状态
```bash
openclaw status
```

### 重启 Gateway
```bash
openclaw gateway restart
```

---

## 任务执行配置

### 当前配置参数

```json
{
  "sessionTarget": "isolated",
  "wakeMode": "now",
  "agentId": "analysis",
  "delivery": {
    "mode": "announce",
    "channel": "feishu",
    "to": "",
    "bestEffort": false
  }
}
```

| 参数 | 说明 |
|------|------|
| sessionTarget | `isolated` - 在独立会话中执行 |
| wakeMode | `now` - 立即唤醒执行 |
| agentId | `analysis` - 使用 analysis agent 执行 |
| delivery.mode | `announce` - 公告模式推送 |
| delivery.channel | `feishu` - 飞书频道 |
| delivery.to | 群组 ID |

---

## 报告类型

| 报告类型 | 触发时间 | 内容 |
|----------|----------|------|
| **早报** | 9:35 / 9:50 | 大盘开盘 + 隔夜资讯 + 持仓动态 + 系统性风险 |
| **盘中监控** | 10:00-14:40 | 实时异动 + 技术信号 + 异常波动预警 |
| **午报** | 12:00 / 12:10 | 上午回顾 + 午后展望 |
| **晚报** | 15:00 / 16:10 | 收盘总结 + 明日展望 + 周度总结(仅周五) |

---

## 新增功能模块 (2026-04-01 优化)

### 系统性风险预警 ⚠️
- 监控大盘指数与20日均线关系
- 上证/恒生跌破20日线 → ⚠️ 系统性风险预警，建议降低仓位
- 大盘涨幅>2% → 🚀 强势信号

### 异常波动预警 🚨
- 单只股票涨跌>5% → 异动提示
- 持仓股与大盘走势严重背离（大盘涨但持仓跌>2%）→ 背离预警

### 行情回顾 📊
- **午报**：上午行情回顾（开盘、亮点、成交量对比）
- **晚报**：全天行情回顾（开盘、最高、最低、收盘、走势特征、板块表现）

### 港股汇率风险 💱
- 港股相关报告增加USD/HKD汇率风险提示
- 提醒汇率波动可能侵蚀港股收益

### 周度总结 📈（仅周五晚报输出）
- 本周指数涨跌汇总
- 持仓股本周表现
- 操作总结
- 下周展望

---

## 故障排查

### 任务未执行
1. 检查 `openclaw gateway status` 是否正常
2. 检查 cron 任务是否 enabled
3. 查看 `~/.openclaw/logs/` 日志

### 报告推送失败
1. 检查飞书群 ID 是否正确
2. 检查机器人是否在群中
3. 确认 `bestEffort: false` 模式

### 数据获取失败
1. 检查网络连接
2. 验证股票代码是否正确
3. 查看接口是否维护

---

## 相关文档

- [SKILL.md](../SKILL.md) - 技能完整说明
- [commands.md](./commands.md) - CLI 命令速查
- [config.md](./config.md) - 配置文件说明
- [troubleshooting.md](./troubleshooting.md) - 故障排查
- [reports/templates.md](../reports/templates.md) - 报告模板
- [reports/prompts.md](../reports/prompts.md) - Cron任务Prompt


path: references/troubleshooting.md
content: # 故障排查

## 常见问题

### 1. 行情获取失败

**症状：** 执行 `quote` 或 `analyze` 命令时提示获取行情失败

**可能原因：**
- 网络连接问题
- 新浪/腾讯接口维护
- 股票代码错误或停牌

**排查步骤：**
```bash
# 1. 测试网络连接
ping finance.sina.com.cn
ping qt.gtimg.cn

# 2. 检查股票代码是否正确
python stock_monitor.py quote 600900

# 3. 尝试获取大盘指数（测试网络）
python stock_monitor.py index
```

**解决方案：**
- 等待接口恢复（通常几分钟）
- 检查防火墙/代理设置
- 验证股票代码是否正确

---

### 2. 报告生成超时

**症状：** `report` 命令执行时间过长或超时

**可能原因：**
- 网络延迟导致行情获取慢
- 股票数量过多
- 资讯接口响应慢

**排查步骤：**
```bash
# 检查监控池股票数量
python stock_monitor.py list
```

**解决方案：**
- 减少监控池股票数量
- 分批生成报告（使用 `report-a` 或 `report-hk`）
- 盘中时间避免生成完整报告

---

### 3. 持仓数据不准确

**症状：** 持仓盈亏计算结果与实际不符

**可能原因：**
- 持仓记录中的成本价错误
- 未同步最新交易
- 货币单位混淆（港股A股）

**排查步骤：**
```bash
# 1. 查看当前持仓记录
python stock_monitor.py position list

# 2. 检查交易记录
python stock_monitor.py trades
```

**解决方案：**
```bash
# 更新持仓成本
python stock_monitor.py position add 600900 6000 28.69

# 清除错误持仓后重新添加
python stock_monitor.py position remove 600900
python stock_monitor.py position add 600900 6000 28.50
```

---

### 4. 港股代码识别错误

**症状：** 港股行情获取失败或显示错误

**可能原因：**
- 港股代码格式不正确
- 港股代码需要以0开头

**正确格式：**
| 股票 | 正确代码 | 错误代码 |
|------|----------|----------|
| 联想集团 | 00992 | 992 |
| 中国神华(港) | 01088 | 1088 |
| 汇丰控股 | 00005 | 5 |
| 中芯国际 | 00981 | 981 |

**解决方案：**
- 确保港股代码为4-5位，以0开头
- 更新持仓记录中的港股代码

---

### 5. 预警不触发

**症状：** 股票达到预警条件但未收到提醒

**可能原因：**
- 未配置预警规则
- 预警功能需要定时任务配合
- 预警配置未启用

**排查步骤：**
```bash
# 检查预警配置文件
Get-Content "$env:USERPROFILE\.openclaw\stock-alerts.json"
```

**解决方案：**
- 确认 OpenClaw cron 任务配置正确
- 检查预警规则是否启用
- 手动触发一次监控测试

---

### 6. 技术指标显示异常

**症状：** MA、MACD、RSI 等指标显示 "N/A" 或异常值

**可能原因：**
- 历史数据不足（新股/港股）
- 数据接口返回格式变化
- 停牌期间无数据

**排查步骤：**
```bash
# 检查单只股票分析
python stock_monitor.py analyze 600900
```

**解决方案：**
- 等待更多交易日数据积累
- 检查是否为停牌股票
- 港股数据可能不完整

---

### 7. 中文显示乱码

**症状：** 输出中文字符显示为乱码

**可能原因：**
- 终端编码设置不正确
- Windows PowerShell 默认编码问题

**解决方案：**
```powershell
# 设置控制台编码为UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
chcp 65001

# 或在执行命令前设置
$env:PYTHONIOENCODING = "utf-8"
```

---

### 8. 脚本执行报错

**症状：** Python 脚本执行时报语法错误或导入错误

**可能原因：**
- Python 版本不兼容
- 缺少依赖库

**排查步骤：**
```bash
# 检查 Python 版本
python --version

# 检查已安装的依赖
pip list | Select-String "urllib|json"
```

**解决方案：**
```powershell
# 安装必要依赖（如需要）
pip install urllib3 json
```

---

## 日志与调试

### 启用调试模式

目前脚本未提供调试模式开关，可通过修改脚本添加 `print` 语句进行调试。

### 查看 OpenClaw 日志

```powershell
# 查看 OpenClaw 日志
Get-Content "$env:USERPROFILE\.openclaw\logs\*.log" -Tail 50
```

### 验证配置文件

```powershell
# 验证 JSON 格式
Get-Content "$env:USERPROFILE\.openclaw\stock-pool.json" | ConvertFrom-Json | ConvertTo-Json -Depth 3
```

---

## 性能优化

### 减少监控股票数量

每次只保留必要的监控股票，避免每次请求超时：
```bash
python stock_monitor.py list
# 逐个移除不需要的
python stock_monitor.py remove <code>
```

### 使用缓存

报告生成会自动缓存行情数据，但频繁查询仍可能导致限流。

### 分时段策略

| 时段 | 建议操作 |
|------|----------|
| 开盘前 9:00-9:30 | 生成早报 `report` |
| 盘中 9:30-15:00 | 少量监控 `monitor` |
| 午休 11:30-13:00 | 生成午报 `report` |
| 收盘后 15:00-17:00 | 生成晚报 `report` |
| 盘后 17:00后 | 完整分析 `report` |

---

## 联系方式

如问题持续存在，请检查：
1. OpenClaw 版本是否最新
2. 技能是否通过 ClawHub 更新到最新版本
3. 网络环境是否稳定


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