#!/usr/bin/env python3
"""
per_stock_reassess.py — 按个股触发重评

对每只传进来的 code 执行 reassess_with_context()，然后写入
DB holding_strategies 表（纯DB模式，已移除JSON依赖）。
"""
import sys, json, os, re
from datetime import datetime

COOLDOWN_HOURS_TRADING = 1     # 交易时段冷却（1小时）
COOLDOWN_HOURS_NONTRADING = 24 # 非交易时段冷却

def _in_cooldown(code):
    """检查个股是否在重评冷却期内"""
    try:
        import sqlite3
        conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db")
        r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active' ORDER BY id DESC LIMIT 1", (code,)).fetchone()
        conn.close()
        if not r or not r[0]:
            return False  # 从未重评，立即执行
        last = datetime.fromisoformat(r[0])
        now = datetime.now()
        # 交易时段 vs 非交易时段
        if 9 <= now.hour < 15:
            hours = COOLDOWN_HOURS_TRADING
        else:
            hours = COOLDOWN_HOURS_NONTRADING
        diff = (now - last).total_seconds() / 3600
        return diff < hours
    except:
        return False

sys.path.insert(0, "/home/hmo/web-dashboard")
sys.path.insert(0, "/home/hmo/MoFin")
from strategy_lifecycle import reassess_with_context as reassess_strategy
from mo_data import read_decisions, read_portfolio


def _build_full_analysis(code, entry, result):
    """从重评结果构建完整九维分析文本"""
    if not result:
        return ""
    lines = []
    name = entry.get("name", code)
    price = result.get("price") or entry.get("price", 0)
    
    tech = result.get("tech_snapshot") or entry.get("tech_snapshot", "")
    sector = result.get("sector_context") or entry.get("sector_context", "")
    signal = result.get("timing_signal") or entry.get("timing_signal", "")
    category = result.get("stock_category") or entry.get("stock_category", "")
    
    el = result.get("entry_low") or entry.get("entry_low", 0)
    eh = result.get("entry_high") or entry.get("entry_high", 0)
    sl = result.get("stop_loss") or entry.get("stop_loss", 0)
    tp = result.get("take_profit") or entry.get("take_profit", 0)
    rr = result.get("rr_ratio") or entry.get("rr_ratio", 0)
    act = result.get("action", "")
    
    # ── 从DB拉取大盘、基本面、资金流 ──
    macro_desc = ""
    pe_val = pb_val = ""
    try:
        import sqlite3 as _sq, json as _j
        _db = _sq.connect("/home/hmo/MoFin/data/mofin.db")
        # 大盘（从structure列读取）
        _m = _db.execute("SELECT structure, sector_mood FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone()
        if _m and _m[0]:
            _st = _j.loads(_m[0])
            _ix = _st.get("indices", {})
            _desc = _st.get("description", "")
            if _ix:
                _parts = []
                for _name in ["上证指数", "深证成指", "创业板指", "科创50", "恒生指数"]:
                    if _name in _ix:
                        _d = _ix[_name]
                        if isinstance(_d, dict):
                            _p = _d.get("price", 0)
                            _c = _d.get("change_pct", 0)
                            _parts.append(f"{_name}({_p:.0f},{_c:+.1f}%)")
                        elif isinstance(_d, (int, float)):
                            _parts.append(f"{_name}({_d})")
                macro_desc = " ".join(_parts)
            elif _desc:
                macro_desc = _desc
            _mood = str(_m[1] or "")
            if _mood and not macro_desc:
                macro_desc = f"情绪={_mood}"
            elif _mood:
                macro_desc += f" 情绪={_mood}"
        if not macro_desc:
            # fallback: 直接用腾讯API拉大盘
            try:
                _r2 = __import__('subprocess').run(["curl", "-s", "http://qt.gtimg.cn/q=sh000001,sz399001,sz399006,sh000688"],
                                                  capture_output=True, timeout=10)
                _txt = _r2.stdout.decode("gbk", errors="ignore")
                _parts = []
                for _line in _txt.strip().split("\n"):
                    if "~" not in _line: continue
                    _p = _line.split("~")
                    if len(_p) < 4: continue
                    _name2 = _p[1]
                    _price2 = _p[3]
                    _chg2 = _p[32] if len(_p) > 32 else "0"
                    _parts.append(f"{_name2}({_price2},{_chg2}%)")
                if _parts:
                    macro_desc = "腾讯实时 " + " ".join(_parts[:3])
            except:
                pass
        # 基本面+实时价：直接从腾讯API拉（盘后也有收盘价）
        try:
            _pfx = "sh" if str(code).startswith(("6", "9")) else "sz"
            _r3 = __import__('subprocess').run(["curl", "-s", f"http://qt.gtimg.cn/q={_pfx}{code}"],
                                              capture_output=True, timeout=10)
            _txt3 = _r3.stdout.decode("gbk", errors="ignore")
            _p3 = _txt3.split("~")
            if len(_p3) > 45:
                _pe = _p3[39] if _p3[39] else ""
                _pb = _p3[40] if len(_p3) > 40 and _p3[40] else ""
                _mcap = _p3[44] if len(_p3) > 44 and _p3[44] else ""
                _price_now = float(_p3[3]) if _p3[3] else 0
                _chg_now = float(_p3[32]) if len(_p3) > 32 and _p3[32] else 0
                if _price_now > 0:
                    price = _price_now  # 覆盖策略中的price=0
                if _pe: pe_val = f"PE={_pe}"
                if _pb: pb_val = f"PB={_pb}"
                if _mcap: 
                    mcap_val = f"市值{float(_mcap)/10000:.1f}亿" if float(_mcap) > 10000 else f"市值{_mcap}万"
                    pe_val += f" {mcap_val}" if pe_val else mcap_val
        except:
            pass
        _db.close()
    except Exception as _e:
        pass
    
    # ── 从tech_snapshot提取MA和支撑阻力 ──
    import re
    ma5 = ma10 = ma20 = ma60 = "?"
    ma_match = re.search(r'MA5=([\d.]+).*?MA10=([\d.]+).*?MA20=([\d.]+).*?MA60=([\d.]+)', tech)
    if ma_match:
        ma5, ma10, ma20, ma60 = ma_match.groups()
    
    lines.append(f"【{name}({code} 九维全析)】")
    lines.append("")
    if macro_desc:
        lines.append(f"① 大盘环境（当日实时）：{macro_desc}")
    else:
        lines.append(f"① 大盘环境（当日实时）：数据待刷新")
    if pe_val or pb_val:
        lines.append(f"② 个股基本面（最新财报）：{pe_val} {pb_val}")
    else:
        lines.append(f"② 个股基本面（最新财报）：数据待补充")
    lines.append(f"③ 技术面（MA5/10/20/60日 支撑阻力近20日）：MA5={ma5} MA10={ma10} MA20={ma20} MA60={ma60}")
    if el and eh and price > 0:
        pos = "在买入区内" if el <= price <= eh else (f"低于买入区{(1-price/el)*100:.0f}%" if price < el else f"高于买入区{(price/eh-1)*100:.0f}%")
        lines.append(f"④ 价格位置：{price} {pos} 区间{el}~{eh}")
    else:
        lines.append(f"④ 价格位置：数据待刷新")
    if sl and tp and rr:
        lines.append(f"⑤ 风报比：止损{sl} 止盈{tp} RR={rr:.1f}")
    # 支撑阻力
    sr_m = re.search(r'强撑:([\d.]+).*?弱撑:([\d.]+).*?弱压:([\d.]+).*?强压:([\d.]+)', tech)
    if sr_m:
        lines.append(f"⑥ 支撑阻力：强撑{sr_m.group(1)}→弱撑{sr_m.group(2)}→弱压{sr_m.group(3)}→强压{sr_m.group(4)}")
    if sector:
        lines.append(f"⑦ 行业背景：{sector}")
    else:
        # 从stock_sectors表补行业
        try:
            _s2 = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
            _sr = _s2.execute("SELECT sector_name FROM stock_sectors WHERE code=? LIMIT 1", (code,)).fetchone()
            if _sr and _sr[0]:
                lines.append(f"⑦ 行业背景：{_sr[0]}")
            _s2.close()
        except:
            pass
    
    # 消息面：从signal_news读最新信号
    news_lines = []
    try:
        _n_db = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
        _nr = _n_db.execute(
            "SELECT summary, overall_sentiment, created_at FROM signal_news "
            "WHERE (sector LIKE ? OR sector LIKE ?) AND overall_sentiment IN ('利好','利空') "
            "ORDER BY id DESC LIMIT 2",
            (f'%{code}%', f'%{name[:4]}%')
        ).fetchall()
        for _ns in _nr:
            _sent = _ns[1]
            _icon = '📈' if '利好' in str(_sent) else '📉'
            news_lines.append(f"{_icon} {_ns[0][:60]} ({str(_ns[2])[:10]})")
        _n_db.close()
    except:
        pass
    if category:
        lines.append(f"⑧ 分类评级：{category}")
    lines.append(f"⑨ 策略信号：{signal}")
    if news_lines:
        lines.append("")
        lines.extend(news_lines)
    if act:
        lines.append(f"\n策略详情：{act[:200]}")
    
    return "\n".join(lines)


def main():
    codes = [a for a in sys.argv[1:] if not a.startswith("-")]
    if not codes:
        print("[FULL] 无指定编码，跑全量 regenerate_all()")
        from strategy_lifecycle import regenerate_all
        regenerate_all(stdout=False)
        print("[FULL] 全量重评完成")
        return

    # 读现有 decisions
    raw = read_decisions()
    decisions_map = {d["code"]: d for d in raw.get("decisions", []) if d.get("code")}

    ok = 0
    errors = 0
    skipped = 0
    for code in codes:
        # 冷却期检查
        if _in_cooldown(code):
            print(f"  ⏭ {code}: 冷却期内跳过")
            skipped += 1
            continue
        entry = decisions_map.get(code)
        if not entry:
            # 不在 decisions 中的自选股 → 从 holding_strategies 构建entry
            import sqlite3
            _db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
            _db.row_factory = sqlite3.Row
            _wl = _db.execute("SELECT * FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'", (code,)).fetchone()
            _db.close()
            if _wl:
                entry = {
                    "code": code,
                    "name": _wl["name"],
                    "price": _wl["price"] or 0,
                    "cost": 0,
                    "shares": 0,
                    "entry_low": _wl["entry_low"] or 0,
                    "entry_high": _wl["entry_high"] or 0,
                    "stop_loss": _wl["stop_loss"] or 0,
                    "take_profit": 0,
                    "action": "",
                    "type": "自选策略",
                    "is_watchlist": True,
                    "analysis": json.loads(_wl["analysis_json"]) if _wl["analysis_json"] else {}
                }
                print(f"[WL] {code} {_wl['name']}: 从自选表构建entry")
            if not entry:
                print(f"[SKIP] {code}: 不在 decisions 或 watchlist_stocks 中")
                errors += 1
                continue

        try:
            # Always fetch live price for accurate reassessment
            price = 0
            try:
                # 价格从 DB 读取（price_monitor 每2分钟更新，唯一价格入口）
                code_raw = entry.get("code", "")
                price = 0
                import sqlite3
                db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
                db.row_factory = sqlite3.Row
                row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (code_raw,)).fetchone()
                if not row:
                    row = db.execute("SELECT price FROM watchlist_stocks WHERE code=? AND is_active=1", (code_raw,)).fetchone()
                if not row:
                    row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (code_raw,)).fetchone()
                if row:
                    price = row['price'] or 0
                db.close()
                if price > 0:
                    print(f"  实时价: {price} (来自DB)")
                else:
                    # fallback to DB portfolio data
                    _pf_data = read_portfolio()
                    for _h in _pf_data.get("holdings", []):
                        if _h["code"] == code_raw:
                            price = float(_h.get("price", 0))
                            break
                if price <= 0:
                    price = entry.get("current_price") or entry.get("price") or 0
            except Exception as e:
                print(f"  价格获取失败: {e}", file=sys.stderr)
                price = entry.get("current_price") or entry.get("price") or 0

            # Price diff debounce: skip reassessment if price changed < 1% since last update
            last_price = entry.get("last_reassessed_price") or 0
            if last_price > 0 and price > 0:
                diff_pct = abs(price - last_price) / last_price * 100
                if diff_pct < 1.0:
                    print(f"  价差仅{diff_pct:.2f}% (<1%)，跳过重评（上次价={last_price}，现价={price}）")
                    skipped += 1
                    continue
            # 打印参数调试
            if entry is None:
                print(f"  DEBUG: code={code} ENTRY=NONE 跳过")
                print(f"  [SKIP] {code} 策略数据不存在")
                skipped += 1
                continue
            entry_action = str(entry.get('action') or '')
            print(f"  DEBUG: code={code} name={entry.get('name','')} price={price} cost={entry.get('cost')} shares={entry.get('shares')} action={entry_action[:30]} is_wl={entry.get('type','') in ('自选策略','watchlist')}", flush=True)
            result = reassess_strategy(
                code=code,
                name=entry.get("name", ""),
                price=price or 0,
                cost=entry.get("cost") or 0,
                shares=entry.get("shares") or 0,
                current_action=entry.get("action", ""),
                is_watchlist=entry.get("type", "") in ("自选策略", "watchlist"),
            )
            if result and result.get("action"):
                # 持仓股止损不下移（移动止损规则）：已有仓位的止损只上不下
                is_held = (entry.get("cost") or 0) > 0 and (entry.get("shares") or 0) > 0 and \
                          entry.get("type", "") not in ("自选策略", "watchlist")
                old_stop = entry.get("stop_loss") or 0
                new_stop = result.get("stop_loss") or 0
                if is_held and old_stop > 0 and new_stop > 0 and new_stop < old_stop:
                    print(f"  移动止损保护: {new_stop}→保持{old_stop} (持仓止损不下移)")
                    result["stop_loss"] = old_stop
                    # 同时更新 action 字符串中的止损值
                    act = result.get("action", "")
                    if act:
                        act = re.sub(r'止损[\d.]+', f'止损{old_stop}', act)
                        result["action"] = act

                # ── 写入 DB holding_strategies 表（替代 decisions.json）──
                try:
                    from mofin_db import get_conn, write_holding_strategy
                    _conn = get_conn()
                    _db_entry = {
                        "code": code,
                        "name": entry.get("name", ""),
                        "price": price,
                        "cost": entry.get("cost", 0),
                        "shares": entry.get("shares", 0),
                        "stop_loss": result.get("stop_loss", entry.get("stop_loss")),
                        "take_profit": result.get("take_profit", entry.get("take_profit")),
                        "entry_low": result.get("entry_low", entry.get("entry_low")),
                        "entry_high": result.get("entry_high", entry.get("entry_high")),
                        "currency": "HKD" if (len(str(code)) == 5 and str(code)[0] in '01') else "CNY",
                        "strategy_type": "自选策略" if entry.get("type", "") in ("自选策略", "watchlist") else "持仓策略",
                        "action": result.get("action", ""),
                        "timing_signal": result.get("timing_signal", entry.get("timing_signal", "")),
                        "rr_ratio": result.get("rr_ratio", entry.get("rr_ratio", 0)),
                        "tech_snapshot": result.get("tech_snapshot", entry.get("tech_snapshot", "")),
                        "stock_category": result.get("stock_category", entry.get("stock_category", "")),
                        "sector_context": result.get("sector_context", entry.get("sector_context", "")),
                        "status": result.get("status", "active"),
                        "source": entry.get("source", "auto"),
                        "reason": result.get("action_note", ""),
                        "version": entry.get("version", 1),
                        "full_analysis": _build_full_analysis(code, entry, result) if result else "",
                    }
                    write_holding_strategy(_conn, code, entry.get("name", ""), _db_entry)
                    _conn.commit()
                    _conn.close()
                    # 验证写入
                    _fa_check = _db_entry.get("full_analysis", "")
                    print(f"  DEBUG: full_analysis长度={len(_fa_check)} 内容=[{_fa_check[:100]}]")
                    # 直接用SQL写入full_analysis
                    try:
                        _fa_conn = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
                        _fa_conn.execute("UPDATE holding_strategies SET full_analysis=? WHERE code=? AND status='active'", (_fa_check, code))
                        _fa_conn.commit()
                        _fa_conn.close()
                        print(f"  ✅ full_analysis直接SQL写入成功")
                    except Exception as _fa_e:
                        print(f"  ⚠️ 直接SQL写入失败: {_fa_e}")
                    _v = __import__('sqlite3').connect(str(__import__('pathlib').Path("/home/hmo/MoFin/data/mofin.db")))
                    _fa = _v.execute("SELECT full_analysis FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
                    if _fa and _fa[0]: print(f"  ✅ full_analysis已写入({len(_fa[0])}字)")
                    else: print(f"  ⚠️ full_analysis为空")
                    _v.close()
                    # LLM生成完整九维分析
                    _macro_desc = ""
                    _pe_val = ""
                    _pb_val = ""
                    try:
                        _mdb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
                        _mr = _mdb.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone()
                        if _mr and _mr[0]:
                            _st = __import__('json').loads(_mr[0])
                            _macro_desc = _st.get("description", "")
                            _mood = _mr[1] if len(_mr) > 1 else ""
                            if _mood: _macro_desc += f" 情绪={_mood}"
                        # 基本面从腾讯API
                        _p = "sh" if str(code).startswith(("6","9")) else "sz"
                        _cr = __import__('subprocess').run(["curl","-s",f"http://qt.gtimg.cn/q={_p}{code}"], capture_output=True, timeout=10)
                        _ct = _cr.stdout.decode("gbk", errors="ignore").split("~")
                        if len(_ct) > 39 and _ct[39]: _pe_val = f"PE={_ct[39]}"
                        if len(_ct) > 44 and _ct[44]: _pb_val = f"PB≈{float(_ct[44])/10000:.1f}亿"
                        _mdb.close()
                    except:
                        pass
                    
                    # 拉取资金流数据
                    _flow_note = "暂无资金流数据"
                    try:
                        _fdb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
                        _fr = _fdb.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone()
                        if _fr and _fr[0]:
                            _fc = __import__('json').loads(_fr[0])
                            _s = _fc.get("stocks", {}).get(code, {})
                            if _s and _s.get("analysis"):
                                _a = _s["analysis"]
                                _flow_note = f"净流入{_a.get('net_flow',0):.0f}万 主力{_a.get('main_force',0):.0f}万 趋势{_a.get('trend','中性')}"
                        _fdb.close()
                    except:
                        pass
                    
                    # 拉取近期消息面
                    _news_note = "暂无近期消息"
                    try:
                        _ndb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
                        _nr2 = _ndb.execute(
                            "SELECT summary, overall_sentiment, created_at FROM signal_news "
                            "WHERE (code=? OR sector LIKE ?) AND overall_sentiment IN ('利好','利空') "
                            "ORDER BY id DESC LIMIT 3",
                            (code, f'%{entry.get("name","")[:4]}%')
                        ).fetchall()
                        if _nr2:
                            _news_note = " | ".join([f"{r[2][:10]} {r[1]} {r[0][:40]}" for r in _nr2])
                        _ndb.close()
                    except:
                        pass
                    
                    _prompt = f"""你是一个资深股票分析师。请对股票{code}做一个完整的12维矩阵分析（3横×4纵：大盘/行业/个股 × 基本面/消息面/技术面/资金面）。

⚠️ 重要：12个维度必须交叉对比，找出矛盾/共振点，给出综合判断。

当前数据（实时API，每条标注时间窗口，禁止使用模型训练数据）：
大盘={_macro_desc or "震荡"}（当日实时） | PE/市值={_pe_val} {_pb_val}（最新财报） | 价格={price} 区间={entry.get("entry_low",0)}~{entry.get("entry_high",0)} 止损={entry.get("stop_loss",0)} 止盈={entry.get("take_profit",0)} RR={result.get("rr_ratio",entry.get("rr_ratio",0))} | 信号={result.get("timing_signal") or entry.get("timing_signal","")} | 行业={(result.get("sector_context") or entry.get("sector_context",""))[:50]}（当日实时）
策略={(result.get("action") or entry.get("action",""))[:200]}
技术={(result.get("tech_snapshot") or entry.get("tech_snapshot",""))[:200]}（MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势）
资金流={_flow_note}（近5日累计）
消息面={_news_note}（最近3条，自动标注抓取时间）

格式：
【交叉分析】哪些维度矛盾/共振，关键信号
① 大盘×基本面 ② 大盘×消息面 ③ 大盘×技术面 ④ 大盘×资金面
⑤ 行业×基本面 ⑥ 行业×消息面 ⑦ 行业×技术面 ⑧ 行业×资金面
⑨ 个股×基本面 ⑩ 个股×消息面 ⑪ 个股×技术面 ⑫ 个股×资金面

最后必须输出：
【综合结论】(买入/关注/观望/卖出)
【操作建议】
【建议止损】
【建议止盈】"""
                    try:
                        _ur = __import__('urllib.request', fromlist=['Request'])
                        _req = _ur.Request("http://127.0.0.1:8643/v1/chat/completions",
                            data=__import__('json').dumps({"model":"deepseek-v4-flash","messages":[{"role":"user","content":_prompt}],"max_tokens":1024}).encode(),
                            headers={"Content-Type":"application/json","Authorization":"Bearer hermes123"})
                        _resp = _ur.build_opener(_ur.ProxyHandler({})).open(_req, timeout=300)
                        _llm_out = __import__('json').loads(_resp.read().decode())["choices"][0]["message"]["content"]
                        _full_analysis_text = _llm_out
                        print(f"  ✅ LLM12维分析完成({len(_full_analysis_text)}字)", flush=True)
                    except Exception as _e:
                        print(f"  ❌ LLM12维分析失败: {_e}", file=__import__('sys').stderr)
                        _full_analysis_text = None

                    # 保存到DB
                    _fa_conn = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db")
                    _fa_conn.execute("UPDATE holding_strategies SET full_analysis=?, reassessed_at=? WHERE code=? AND status='active'", (_full_analysis_text, __import__('datetime').datetime.now().isoformat(), code))
                    _fa_conn.commit()
                    _fa_conn.close()
                    print(f"  ✅ 完整12维分析已保存({len(_full_analysis_text)}字)" if _full_analysis_text else f"  ⚠️ 12维分析未完成，跳过保存")
                    print(f"  [DB] holding_strategies 已更新: {code}")
                    # 从LLM输出提取信号
                    if _full_analysis_text and '【综合结论】' in _full_analysis_text:
                        try:
                            _sig_line = [l for l in _full_analysis_text.split('\n') if '综合结论' in l]
                            if _sig_line:
                                _sig = '买入' if '买入' in _sig_line[0] else '关注' if '关注' in _sig_line[0] else '观望' if '观望' in _sig_line[0] else '卖出' if '卖出' in _sig_line[0] else ''
                                if _sig:
                                    __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute(
                                        "UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code)).connection.commit()
                                    print(f"  ✅ LLM信号={_sig} 已写入")
                                    # 买入信号→推XMPP
                                    if _sig == "买入":
                                        try:
                                            _nr2 = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute(
                                                "SELECT name, price, entry_low, entry_high, stop_loss, take_profit, position_advice FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
                                            if _nr2:
                                                _xm = f"📈 {_nr2[0] or code}({code}) 价{_nr2[1]}→12维买入信号！区间{_nr2[2]}~{_nr2[3]} 损{_nr2[4]} 盈{_nr2[5]} 仓位{_nr2[6] or '-'}"
                                                _xr = __import__('urllib.request').Request("http://127.0.0.1:5805/",
                                                    data=__import__('json').dumps({"body": _xm, "to": "hmo@yoin.fun", "type": "chat"}).encode(),
                                                    headers={"Content-Type": "application/json"})
                                                __import__('urllib.request').urlopen(_xr, timeout=5)
                                                print(f"  📨 XMPP推送买入信号")
                                        except: pass
                        except: pass
                    # 冷却期已更新（reassessed_at写入）
                except Exception as _dbe:
                    print(f"  [DB FAIL] holding_strategies 写入失败: {_dbe}", file=sys.stderr)

                # 更新 decisions_map 中对应的条目
                updated = entry.copy()
                # 币种标记：HK股保留HKD原始值，A股为CNY
                is_hk = len(str(code)) == 5 and str(code)[0] in '01'
                updated.update({
                    "action": result["action"],
                    "stop_loss": result.get("stop_loss", entry.get("stop_loss")),
                    "entry_low": result.get("entry_low", entry.get("entry_low")),
                    "entry_high": result.get("entry_high", entry.get("entry_high")),
                    "take_profit": result.get("take_profit"),
                    "tech_snapshot": result.get("tech_snapshot", entry.get("tech_snapshot")),
                    "timing_signal": result.get("timing_signal", entry.get("timing_signal")),
                    "rr_ratio": result.get("rr_ratio", entry.get("rr_ratio", 0)),
                    "status": result.get("status", "updated"),
                    "price": price,
                    "currency": "HKD" if is_hk else "CNY",
                })
                # Save last reassessed price for debounce tracking
                updated["last_reassessed_price"] = price
                decisions_map[code] = updated
                # ——— 初始化多分支策略树 ———
                try:
                    sys.path.insert(0, '/home/hmo/MoFin')
                    from strategy_tree import init_default_branches
                    branches = init_default_branches(
                        code,
                        entry.get('name', ''),
                        result.get('entry_low', 0),
                        result.get('entry_high', 0),
                        result.get('stop_loss', 0),
                        result.get('take_profit', 0),
                    )
                    st = updated.setdefault('strategy_tree', {})
                    st['branches'] = branches
                except Exception:
                    pass
                print(f"[OK] {code} {entry.get('name','')}: {result['action'][:80]}")
                ok += 1
            else:
                print(f"[SYNCED] {code}: 无变更")
                ok += 1
        except Exception as e:
            print(f"[ERROR] {code}: {e}", file=sys.stderr)
            import traceback
            traceback.print_exc(file=sys.stderr)
            errors += 1

    # 同步自选股更新回 watchlist_stocks 表（持仓策略已通过 write_holding_strategy 写入 DB）
    try:
        from datetime import datetime as _dt
        import sqlite3
        _db2 = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
        for _code in codes:
            _entry = decisions_map.get(_code)
            if _entry and _entry.get("is_watchlist"):
                _db2.execute("""
                    UPDATE watchlist_stocks 
                    SET entry_low=?, entry_high=?, stop_loss=?, price=?,
                        analysis_json=json(?)
                    WHERE code=? AND is_active=1
                """, (
                    _entry.get("entry_low", 0),
                    _entry.get("entry_high", 0),
                    _entry.get("stop_loss", 0),
                    _entry.get("price", 0),
                    json.dumps({
                        "action": _entry.get("action",""),
                        "take_profit": _entry.get("take_profit", 0),
                        "stop_loss": _entry.get("stop_loss", 0),
                        "tech_snapshot": _entry.get("tech_snapshot", ""),
                        "rr": _entry.get("rr_ratio", 0),
                        "reassessed_at": _dt.now().strftime("%Y-%m-%d")
                    }, ensure_ascii=False),
                    _code
                ))
        _db2.commit()
        _db2.close()
        if any(e.get("is_watchlist") for e in [decisions_map.get(c) for c in codes] if e):
            print("[SYNC] 自选股策略已同步回 watchlist_stocks 表")
    except Exception as e:
        print(f"[SYNC FAIL] watchlist_stocks 同步失败: {e}", file=sys.stderr)

    print(f"[DONE] {ok}成功 {skipped}跳过 {errors}失败")

    # ── 第二步：扫描自选股（watchlist），价格偏离买入区>20%触发重评 ──
    scan_watchlist_stocks()


# ════════════════════════════════════════════════════════════════════
# 自选股扫描
# ════════════════════════════════════════════════════════════════════

def scan_watchlist_stocks():
    """扫描自选股表 (watchlist_stocks)，对价格偏离买入区 >20% 的股票自动重评。

    偏离公式: max(|price - entry_low|, |price - entry_high|) / entry_low * 100 > 20

    通过 technical_analysis.full_analysis() 获取最新支撑/阻力位，
    更新 entry_low / entry_high / stop_loss / price / analysis_json。
    每轮最多处理 3 只，超过时标记剩余数量待下次扫描。
    """
    import sqlite3, json
    from datetime import datetime
    from technical_analysis import full_analysis
    from mo_models import is_hk_stock

    DB = '/home/hmo/web-dashboard/data/mofin.db'
    db = sqlite3.connect(DB)
    db.row_factory = sqlite3.Row

    rows = db.execute(
        "SELECT * FROM watchlist_stocks WHERE is_active=1"
    ).fetchall()

    if not rows:
        print("[WL-SCAN] 自选股表为空，跳过")
        db.close()
        return

    # ── 筛选偏离 >20% 的股票 ──
    candidates = []   # (code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json)
    for r in rows:
        code = r["code"]
        name = r["name"]
        price = r["price"] or 0
        entry_low = r["entry_low"] or 0
        entry_high = r["entry_high"] or 0
        stop_loss = r["stop_loss"] or 0
        analysis_json = r["analysis_json"]

        if entry_low <= 0 or price <= 0:
            continue

        dev_low = abs(price - entry_low)
        dev_high = abs(price - entry_high)
        deviation = max(dev_low, dev_high) / entry_low * 100

        if deviation > 20:
            candidates.append((code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json))

    total_needed = len(candidates)
    print(f"[WL-SCAN] 自选股共{len(rows)}只，偏离>20%需重评: {total_needed}只")

    MAX_PER_RUN = 3
    to_process = candidates[:MAX_PER_RUN]
    remaining = max(0, total_needed - MAX_PER_RUN)

    if remaining > 0:
        print(f"[WL-SCAN] 本轮限{MAX_PER_RUN}只，剩余{remaining}只待下次扫描")

    if not to_process:
        print("[WL-SCAN] 无需重评")
        db.close()
        return

    ok = 0
    errors = 0
    for code, name, price, old_low, old_high, old_stop, deviation, old_analysis_json in to_process:
        print(f"[WL-REASSESS] {code} {name}: 偏离{deviation:.1f}%，触发重评")
        try:
            ta = full_analysis(code)
            if not ta or "error" in ta:
                print(f"  [WARN] TA失败: {ta}")
                errors += 1
                continue

            sr = ta.get("support_resistance", {})
            if "error" in sr:
                print(f"  [WARN] 支撑/阻力计算失败: {sr}")
                errors += 1
                continue

            new_price = ta.get("quote", {}).get("price", price)
            new_entry_low = round(sr.get("weak_support", old_low), 2)
            new_entry_high = round(sr.get("weak_resist", old_high), 2)
            new_stop_loss = round(sr.get("strong_support", old_stop), 2)
            new_take_profit = round(sr.get("strong_resist", 0), 2)

            # ── 更新 analysis_json + changelog ──
            old_analysis = json.loads(old_analysis_json) if old_analysis_json else {}
            changelog = old_analysis.get("changelog", [])
            changelog.append({
                "action": "auto_reassess_watchlist",
                "reason": f"价格偏离买入区{deviation:.1f}%",
                "old_entry_low": old_low,
                "old_entry_high": old_high,
                "new_entry_low": new_entry_low,
                "new_entry_high": new_entry_high,
                "old_stop_loss": old_stop,
                "new_stop_loss": new_stop_loss,
                "take_profit": new_take_profit,
                "price": new_price,
                "deviation_pct": round(deviation, 1),
                "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
            })

            new_analysis = {
                **old_analysis,
                "take_profit": new_take_profit,
                "tech_snapshot": {
                    "support_resistance": sr,
                    "candlestick": ta.get("candlestick", {}),
                    "volume": ta.get("volume", {}),
                    "analyzed_at": ta.get("analyzed_at", ""),
                },
                "reassessed_at": datetime.now().strftime("%Y-%m-%d"),
                "changelog": changelog,
            }

            currency = "HKD" if is_hk_stock(code) else "CNY"

            db.execute("""
                UPDATE watchlist_stocks
                SET entry_low=?, entry_high=?, stop_loss=?, price=?,
                    currency=?, analysis_json=?
                WHERE code=? AND is_active=1
            """, (
                new_entry_low, new_entry_high, new_stop_loss,
                new_price, currency, json.dumps(new_analysis, ensure_ascii=False),
                code,
            ))
            db.commit()
            print(f"  [OK] {code} {name}: 买入区{old_low}-{old_high} -> {new_entry_low}-{new_entry_high}, "
                  f"止损{new_stop_loss}, 止盈{new_take_profit}")
            ok += 1
        except Exception as e:
            import traceback
            print(f"  [ERROR] {code}: {e}", file=sys.stderr)
            traceback.print_exc(file=sys.stderr)
            errors += 1

    db.close()
    remaining_msg = f" (剩余{remaining}只)" if remaining else ""
    print(f"[WL-SCAN] DONE: {ok}成功 {errors}失败{remaining_msg}")


if __name__ == "__main__":
    main()
