#!/usr/bin/env python3
"""策略生命周期管理系统 — 技术面驱动版本 v2

核心原则：
1. 止损放在合理的技术位，不拍数字
2. 新买入推荐：止损=弱支撑(约3%跌幅)，止盈=强压力，盈亏比≥2:1
3. 已持仓：止损=强支撑(约5-8%跌幅)，目标=强压力
4. 买入区间：弱支撑~弱压力之间
5. 买入时机：量价齐跌不买，缩量至支撑买，量价齐升追买
"""

import json
import urllib.request
import os
import sys
import re
from datetime import datetime
import technical_analysis as ta
import multi_timeframe as mtf

# 提示词版本追踪
try:
    from prompt_manager.tracking import record_strategy_generation
    HAS_PROMPT_TRACKING = True
except ImportError:
    HAS_PROMPT_TRACKING = False

PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"

def safe_json_load(path, default=None):
    """安全加载 JSON，遇到坏数据自动修复"""
    if not os.path.exists(path):
        return default if default is not None else {}
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError:
        # 尝试修复：替换字符串内未转义的换行符，去多余括号
        with open(path, "r", encoding="utf-8") as f:
            raw = f.read()
        fixed = raw
        
        # 修复1: 字符串内未转义的换行 -> \\n
        result = []
        in_str = False
        for ch in fixed:
            if ch == '"':
                in_str = not in_str
                result.append(ch)
            elif in_str and ch in '\n\r':
                result.append('\\n')
            else:
                result.append(ch)
        fixed = ''.join(result)
        
        # 修复2: 去掉多余的尾部括号
        fixed = fixed.rstrip('}')
        # 补回正确的闭合
        if not fixed.endswith('}'):
            fixed += '}'
        
        try:
            return json.loads(fixed)
        except json.JSONDecodeError as e:
            print(f"[WARN] watchlist.json 自动修复失败: {e}", file=sys.stderr)
            return default if default is not None else {}
KNOWLEDGE_LOG = "/home/hmo/Obsidian/knowledge/finance/analyst-knowledge-log.md"
MACRO_CONTEXT_PATH = "/home/hmo/web-dashboard/data/macro_context.json"
MARKET_CONTEXT_PATH = "/home/hmo/web-dashboard/data/market.json"
STOCK_SECTOR_MAP_PATH = "/home/hmo/web-dashboard/data/stock_sector_map.json"


def load_stock_sector_map():
    """读取个股归属行业映射
    
    stock_sector_map.json 格式: {code: [sector1, sector2, ...]}
    跳过 _note, _created_at 等元数据键。
    """
    # 优先从 SQLite 读取
    try:
        from mofin_db import get_conn, query_sector_stocks
        conn = get_conn()
        # 从 stock_sectors 表反向构建 code→[sectors] 映射
        rows = conn.execute("SELECT code, sector_name FROM stock_sectors ORDER BY code").fetchall()
        conn.close()
        code_to_sectors = {}
        for code, sector in rows:
            if code not in code_to_sectors:
                code_to_sectors[code] = []
            code_to_sectors[code].append(sector)
        return code_to_sectors
    except Exception:
        pass
    try:
        with open(STOCK_SECTOR_MAP_PATH) as f:
            data = json.load(f)
        code_to_sectors = {}
        for key, value in data.items():
            if key.startswith("_"):
                continue
            if isinstance(value, list):
                code_to_sectors[key] = value
        return code_to_sectors
    except Exception:
        return {}


def load_market_context():
    """读取市场上下文，优先 SQLite，回退 market.json"""
    # 优先从 SQLite 读取
    try:
        from mofin_db import get_conn, query_latest_market
        conn = get_conn()
        market = query_latest_market(conn)
        conn.close()
        if market and market.get("sectors"):
            sector_perf = {}
            for s in market["sectors"]:
                name = s.get("name", "")
                if name:
                    sector_perf[name] = {
                        "change": s.get("change_pct", 0),
                        "up_count": s.get("up_count", 0),
                        "down_count": s.get("down_count", 0),
                        "net_inflow": s.get("net_inflow", 0),
                        "lead_stock": s.get("lead_stock", ""),
                        "lead_stock_change": s.get("lead_stock_change", 0),
                    }
            return {
                "sector_perf": sector_perf,
                "breadth": market.get("up_ratio", 50),
                "mood": market.get("mood", "neutral"),
                "top_gainers": {g["name"]: g["change_pct"] for g in market.get("top_gainers", [])},
                "top_losers": {g["name"]: g["change_pct"] for g in market.get("top_losers", [])},
                "total_sectors": len(market["sectors"]),
                "market_timestamp": market.get("timestamp", ""),
            }
    except Exception:
        pass
    try:
        with open(MARKET_CONTEXT_PATH) as f:
            market = json.load(f)
        sectors = market.get("sectors", [])
        sector_perf = {}
        for s in sectors:
            name = s.get("name", "")
            if name:
                sector_perf[name] = {
                    "change": s.get("change", 0),
                    "up_count": s.get("up_count", 0),
                    "down_count": s.get("down_count", 0),
                    "net_inflow": s.get("net_inflow", 0),
                    "lead_stock": s.get("lead_stock", ""),
                    "lead_stock_change": s.get("lead_stock_change", 0),
                }
        top_gainers = {s.get("name", ""): s.get("change", 0)
                       for s in market.get("top_gainers", [])}
        top_losers = {s.get("name", ""): s.get("change", 0)
                      for s in market.get("top_losers", [])}
        return {
            "sector_perf": sector_perf,
            "breadth": market.get("up_ratio", 50),
            "mood": market.get("mood", "neutral"),
            "top_gainers": top_gainers,
            "top_losers": top_losers,
            "total_sectors": market.get("total_sectors", 0),
            "market_timestamp": market.get("timestamp", ""),
        }
    except Exception:
        return {
            "sector_perf": {},
            "breadth": 50,
            "mood": "neutral",
            "top_gainers": {},
            "top_losers": {},
            "total_sectors": 0,
            "market_timestamp": "",
        }


def compute_sector_adjustment(code, market_ctx, stock_sector_map):
    """根据个股所属行业的市场表现+小果情感，返回调整系数
    
    返回 dict:
      stop_bias: 止损调整系数（<1.0收紧, >1.0放宽）
      target_bias: 止盈调整系数
      note: 行业背景一句话
      sector_name: 匹配到的行业名称
      sector_change: 行业涨跌幅
    """
    # 默认无调整
    adj = {"stop_bias": 1.0, "target_bias": 1.0, "note": "",
           "sector_name": "", "sector_change": 0}
    
    sectors_for_code = stock_sector_map.get(code, [])
    if not sectors_for_code:
        return adj
    
    sector_perf = market_ctx.get("sector_perf", {})
    breadth = market_ctx.get("breadth", 50)
    
    # 找第一个能匹配到的行业
    for sec in sectors_for_code:
        if sec in sector_perf:
            perf = sector_perf[sec]
            chg = perf.get("change", 0)
            adj["sector_name"] = sec
            adj["sector_change"] = chg
            
            # 行业暴跌 > 3%
            if chg <= -3:
                adj["stop_bias"] = 0.92  # 止损收紧8%
                adj["target_bias"] = 0.90  # 止盈下调10%
                adj["note"] = f"行业{sec}大跌{chg:+.1f}%，收紧止损"
            # 行业大跌 1~3%
            elif chg <= -1:
                adj["stop_bias"] = 0.96
                adj["target_bias"] = 0.95
                adj["note"] = f"行业{sec}下跌{chg:+.1f}%，适度防御"
            # 行业大涨 > 3%
            elif chg >= 3:
                adj["stop_bias"] = 1.05  # 止损放宽5%（给趋势空间）
                adj["target_bias"] = 1.03
                adj["note"] = f"行业{sec}大涨{chg:+.1f}%，可适度积极"
            # 行业上涨 1~3%
            elif chg >= 1:
                adj["stop_bias"] = 1.02
                adj["note"] = f"行业{sec}上涨{chg:+.1f}%，正常"
            else:
                adj["note"] = f"行业{sec}{chg:+.1f}%，中性"
            break
        # 尝试处理命名差异：market.json中的行业名可能多了"板块"后缀
        for market_sec_name in sector_perf:
            if sec in market_sec_name or market_sec_name in sec:
                perf = sector_perf[market_sec_name]
                chg = perf.get("change", 0)
                adj["sector_name"] = market_sec_name
                adj["sector_change"] = chg
                if chg <= -3:
                    adj["stop_bias"] = 0.92
                    adj["target_bias"] = 0.90
                    adj["note"] = f"行业{market_sec_name}大跌{chg:+.1f}%，收紧止损"
                elif chg <= -1:
                    adj["stop_bias"] = 0.96
                    adj["target_bias"] = 0.95
                    adj["note"] = f"行业{market_sec_name}下跌{chg:+.1f}%，适度防御"
                elif chg >= 3:
                    adj["stop_bias"] = 1.05
                    adj["target_bias"] = 1.03
                    adj["note"] = f"行业{market_sec_name}大涨{chg:+.1f}%，可适度积极"
                elif chg >= 1:
                    adj["stop_bias"] = 1.02
                    adj["note"] = f"行业{market_sec_name}上涨{chg:+.1f}%，正常"
                else:
                    adj["note"] = f"行业{market_sec_name}{chg:+.1f}%，中性"
                break
    
    # 如果breath<30% (大盘极弱)，再加一层收紧
    if breadth < 30:
        adj["stop_bias"] *= 0.97  # 再收紧3%
        breadth_note = "大盘仅{}%个股上涨".format(int(breadth))
        adj["note"] = (adj["note"] + " | " + breadth_note) if adj["note"] else breadth_note
    elif breadth < 40:
        adj["stop_bias"] *= 0.99
        breadth_note = "大盘偏弱({}%上涨)".format(int(breadth))
        adj["note"] = (adj["note"] + " | " + breadth_note) if adj["note"] else breadth_note

    # 小果情感约束：利空置信度>80%时收紧止损
    try:
        xiaoguo_path = "/home/hmo/web-dashboard/data/xiaoguo_sentiment.json"
        if os.path.exists(xiaoguo_path):
            xg = json.load(open(xiaoguo_path))
            stock_sentiment = xg.get("stocks", {}).get(code, {})
            if stock_sentiment:
                sentiment = stock_sentiment.get("sentiment", "")
                confidence = stock_sentiment.get("confidence", 0)
                summary = stock_sentiment.get("summary", "")
                if sentiment == "negative" and confidence > 0.8:
                    adj["stop_bias"] = min(adj["stop_bias"], 0.95)
                    adj["note"] += f" | 小果利空{confidence:.0%}:{summary[:30]}"
    except Exception:
        pass

    return adj


def load_macro_context():
    """读取宏观上下文，返回 (bias, desc)，bias 取 0.8/1.0/1.1 分别对应 bearish/neutral/bullish"""
    try:
        with open(MACRO_CONTEXT_PATH) as f:
            ctx = json.load(f)
        overall = ctx.get("structure", {}).get("overall", "neutral")
        desc = ctx.get("structure", {}).get("description", "")
        if "bearish" in overall:
            return 0.8, f"宏观{desc}"
        elif overall == "bullish":
            return 1.05, f"宏观{desc}"
        elif overall == "strong_bullish":
            return 1.1, f"宏观{desc}"
        else:
            return 1.0, f"宏观{desc}"
    except Exception:
        return 1.0, "宏观未加载"


def batch_fetch_prices(codes):
    """批量获取实时价格，合并为一次API调用（自动分批，每批15只）"""
    if not codes:
        return {}
    
    # 分批处理，避免单次请求过大导致超时
    batch_size = 15
    all_results = {}
    for batch_start in range(0, len(codes), batch_size):
        batch = codes[batch_start:batch_start + batch_size]
        symbols = []
        code_map = {}
        for raw_code in batch:
            raw_code = str(raw_code).split('_')[0]
            if not raw_code:
                continue
            if len(raw_code) == 5 and raw_code.isdigit():
                prefix = "hk"
            elif raw_code.startswith(("6", "5")):
                prefix = "sh"
            else:
                prefix = "sz"
            sym = f"{prefix}{raw_code}"
            symbols.append(sym)
            code_map[sym] = raw_code
        if not symbols:
            continue
        
        url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
        max_retries = 2
        for attempt in range(max_retries + 1):
            try:
                r = urllib.request.urlopen(url, timeout=10)
                text = r.read().decode("gbk")
            except Exception as e:
                if attempt < max_retries:
                    continue
                print(f"  batch_fetch_prices error: {e}", file=sys.stderr)
                continue
            
            for line in text.strip().split("\n"):
                line = line.strip()
                if not line or "=" not in line:
                    continue
                try:
                    sym = line.split("=", 1)[0].strip().lstrip("v_")
                    raw_value = line.split("=", 1)[1].strip().strip('"').strip(";")
                    fields = raw_value.split("~")
                    if len(fields) < 35:
                        continue
                    orig_code = code_map.get(sym)
                    if not orig_code:
                        continue
                    def f(i):
                        try:
                            return float(fields[i]) if fields[i].strip() else 0.0
                        except:
                            return 0.0
                    all_results[orig_code] = {
                        "price": f(3), "close": f(4), "high": f(33), "low": f(34),
                        "code": orig_code,
                    }
                except Exception:
                    continue
            break  # Success - break retry loop

    return all_results


def get_price_tencent(code):
    """获取实时价格，自动识别A股/港股"""
    try:
        raw_code = code.split('_')[0]
        if not raw_code:
            return None
        if len(raw_code) == 5 and raw_code.isdigit():
            prefix = "hk"
        elif raw_code.startswith("6") or raw_code.startswith("5"):
            prefix = "sh"
        else:
            prefix = "sz"
        url = f"http://qt.gtimg.cn/q={prefix}{raw_code}"
        r = urllib.request.urlopen(url, timeout=5)
        fields = r.read().decode("gbk").split('"')[1].split("~")
        def f(i):
            try:
                return float(fields[i]) if fields[i].strip() else 0.0
            except:
                return 0.0
        return {
            "price": f(3), "close": f(4), "high": f(33), "low": f(34),
            "code": raw_code,
        }
    except Exception as e:
        print(f"  get_price error {code}: {e}", file=sys.stderr)
        return None


def reassess_strategy(code, name, price, cost, shares, current_action,
                      volume_signal="", sentiment="neutral",
                      is_watchlist=False):
    """根据技术分析重评策略"""

    tech = ta.full_analysis(code)
    if tech and "support_resistance" in tech:
        sr = tech["support_resistance"]
        candle = tech.get("candlestick", {})
        vol = tech.get("volume", {})
        ss = sr.get("strong_support")
        ws = sr.get("weak_support")
        wr = sr.get("weak_resist")
        sr_resist = sr.get("strong_resist")
        pivot = sr.get("pivot")
        effective_range = sr.get("effective_range")
        print(f"  TECH: 强撑={ss} 弱撑={ws} 枢轴={pivot} 弱压={wr} 强压={sr_resist} 有效区间={effective_range}")
    else:
        print(f"  ⚠️ 技术分析不可用", file=sys.stderr)
        ss = ws = wr = sr_resist = pivot = None
        candle = {}
        vol = {}

    # ----- 多周期技术分析（周线/月线/均线） -----
    mtf_analysis = {}
    mtf_adj = {}
    try:
        mtf_result = mtf.full_multi_tf_analysis(code)
        if mtf_result.get("daily") and mtf_result["daily"].get("count", 0) >= 5:
            mtf_analysis = mtf_result
            mtf_adj = mtf_result.get("strategy_adjustment", {})
            daily_mas = mtf_result.get("daily", {}).get("mas", {})
            weekly = mtf_result.get("weekly", {})
            monthly = mtf_result.get("monthly", {})
            trend_align = mtf_adj.get("trend_alignment", "未知")
            print(f"  多周期: {trend_align} | "
                  f"MA5={daily_mas.get('ma5','?')} MA20={daily_mas.get('ma20','?')} MA60={daily_mas.get('ma60','?')} | "
                  f"周线{weekly.get('trend',{}).get('description','?')} 月线{monthly.get('trend',{}).get('description','?')}")
    except Exception as e:
        print(f"  多周期分析失败: {e}", file=sys.stderr)

    profit_pct = (price - cost) / cost * 100 if cost else 0
    is_new_entry = (cost == 0) or (shares == 0)
    is_deep_loss = profit_pct < -20

    # ----- 股票分类（短炒/中短线/中长线/弱势/深套） -----
    stock_category = "中短线"
    time_horizon = "2周~3月"
    position_advice = "中等仓位"
    try:
        mtf_cache = json.load(open("/home/hmo/web-dashboard/data/multi_tf_cache.json"))
        stock_data = mtf_cache.get(code, {})
        daily_klines = stock_data.get("daily", [])
        fund = stock_data.get("fundamentals", {})
        closes = [d["close"] for d in daily_klines] if daily_klines else []

        if len(closes) >= 10:
            cur = closes[-1]
            ma20 = sum(closes[-20:])/20 if len(closes)>=20 else 0
            ma60 = sum(closes[-60:])/60 if len(closes)>=60 else 0
            highs = [d["high"] for d in daily_klines[-20:]]
            lows = [d["low"] for d in daily_klines[-20:]]
            volatility = ((max(highs)-min(lows))/min(lows)*100) if min(lows)>0 else 0
            pe = fund.get("pe") or 0
            eps = fund.get("eps") or 0
            mcap = fund.get("mcap_total") or 0
            is_high_vol = volatility > 30
            is_high_pe = pe > 100 or pe < 0
            is_value = 0 < pe < 20 and eps > 0.5

            if is_deep_loss:
                stock_category = "深套"
                time_horizon = "长期"
                position_advice = "不补不割"
            elif is_high_vol and is_high_pe:
                stock_category = "短炒"
                time_horizon = "数日~2周"
                position_advice = "小仓快进快出"
            elif cur < ma20 and cur < ma60 and ma20 > 0:
                stock_category = "弱势"
                time_horizon = "观望"
                position_advice = "减仓或观望"
            elif (is_value or mcap > 1000) and cur > ma20:
                stock_category = "中长线"
                time_horizon = "数月~1年"
                position_advice = "正常配置"
            elif volatility > 20:
                stock_category = "中短线"
                time_horizon = "2~6周"
                position_advice = "中等仓位"
    except Exception:
        pass

    print(f"  分类: {stock_category} | {time_horizon} | {position_advice}")

    # ----- 短炒+强趋势检测：短炒分类但多周期多头时用移动止损代替弱支撑止损 -----
    is_short_term_strong_trend = False
    if stock_category == "短炒":
        trend_align = mtf_adj.get("trend_alignment", "")
        strong_trend_indicators = ["多周期看多", "多周期多头", "上升"]
        if any(ind in trend_align for ind in strong_trend_indicators):
            is_short_term_strong_trend = True
            print(f"  ⚡ 短炒+强趋势检测: 趋势={trend_align} → 启用移动止损, 不止盈")
            position_advice = "小仓强趋势让利润跑"

    # ----- 止损设置（含最小距离3%保护） -----
    if is_new_entry:
        # 新买入推荐：止损 = 弱支撑（约2-3%跌幅，合理可控）
        if ws and ws > 0:
            new_stop = round(ws, 2)
        else:
            new_stop = round(price * 0.96, 2)
    elif is_deep_loss:
        # 深套：止损 = 强支撑再下移（不轻易割）
        if ss and ss > 0:
            new_stop = round(min(ss, price * 0.85), 2)
        else:
            new_stop = round(price * 0.85, 2)
    else:
        # 已持仓正常：止损 = 强支撑
        if is_short_term_strong_trend:
            # 短炒+强趋势：用移动止损（距现价-5%），不止盈让利润跑
            trailing_sl = round(max(ws or 0, price * 0.95), 2) if ws else round(price * 0.95, 2)
            new_stop = trailing_sl
            print(f"  短炒强趋势移动止损: {new_stop} (距现价-{(1-new_stop/price)*100:.1f}%)")
        elif ss and ss > 0:
            new_stop = round(ss, 2)
        else:
            new_stop = round(price * 0.88, 2)

    # 已盈利仓位（>5%）：用较紧的移动止损保护利润，但不超过成本线
    if profit_pct > 5 and not is_new_entry and not is_deep_loss:
        # 取 max(弱支撑, 成本线, 当前价×0.95) 作为止损
        cost_protect = cost if cost > 0 else 0
        trailing_stop = round(max(ws or 0, cost_protect, price * 0.95), 2)
        if trailing_stop > new_stop:
            new_stop = trailing_stop
            print(f"  已启用移动止损: {new_stop} (保护+{profit_pct:.1f}%利润)", file=sys.stderr)

    # 最小止损距离：止损不能高于现价的97%（即至少3%下行空间）
    min_stop = round(price * 0.97, 2)
    if new_stop > min_stop and not is_deep_loss:
        new_stop = min_stop

    # ----- 止盈设置 -----
    if is_short_term_strong_trend and not is_new_entry:
        # 短炒+强趋势：不止盈让利润跑，止盈设为远位（多周期强阻或价×1.5）
        mtf_tp = mtf_adj.get("take_profit_reference", {})
        if mtf_tp and mtf_tp.get("level", 0) > price * 1.2:
            new_target = round(mtf_tp["level"], 2)
        else:
            new_target = round(price * 1.5, 2)
        print(f"  短炒强趋势不止盈: 止盈设为{new_target} (+{(new_target/price-1)*100:.0f}%)")
    elif sr_resist and sr_resist > 0:
        new_target = round(sr_resist, 2)
    else:
        new_target = round(price * 1.15, 2)

    # ----- 风险回报比校验 -----
    stop_distance = price - new_stop if price > new_stop else price * 0.02
    target_distance = new_target - price if new_target > price else 0

    # 1:2 检查
    min_target_distance = stop_distance * 2.0
    if target_distance < min_target_distance:
        # 尝试更高的阻力位，但不超过下一个真实压力位
        candidate_targets = []
        if wr and wr > price and wr != sr_resist:
            candidate_targets.append(wr)
        if sr_resist and sr_resist > price:
            candidate_targets.append(sr_resist)
        # 检查有效区间，如果有更高的自然目标位
        if effective_range and price < effective_range * 0.9:
            candidate_targets.append(effective_range)

        found = False
        for level in candidate_targets:
            if (level - price) >= min_target_distance:
                new_target = level
                found = True
                break

        # 如果仍然不满足，检查是否至少能到 1:1.5
        min15_distance = stop_distance * 1.5
        if not found:
            for level in candidate_targets:
                if (level - price) >= min15_distance:
                    new_target = level
                    found = True
                    break

    # ----- 风险回报比最终计算 -----
    risk = max(price - new_stop, price * 0.01)
    reward = max(new_target - price, 0)
    rr_ratio = reward / risk if risk > 0 else 0

    # ----- 状态判断 -----
    if is_deep_loss:
        status = "updated"
        action_note = "深套持有"
    elif is_new_entry:
        if rr_ratio < 1.5:
            status = "review"
            action_note = "⚠️盈亏比不足1:1.5，不建议买入"
        elif rr_ratio < 2.0:
            status = "updated"
            action_note = "⚠️盈亏比偏低(1:{:.1f})，谨慎买入".format(rr_ratio)
        else:
            status = "updated"
            action_note = ""
    else:
        if rr_ratio < 0.5:
            status = "updated"
            action_note = "⚠️盈亏比极低，关注"
        elif rr_ratio < 1.5:
            status = "updated"
            action_note = "⚠️盈亏比偏低(1:{:.1f})，不建议加仓".format(rr_ratio)
        else:
            status = "updated"
            action_note = ""

    # 短炒+强趋势：在action_note追加标记
    if is_short_term_strong_trend and not is_new_entry and not is_deep_loss:
        extra_note = "短炒强趋势持" if "深套" not in action_note else ""
        if extra_note:
            action_note = f"{action_note} | {extra_note}" if action_note else extra_note

    # ----- 买入区间（有盈亏比严格约束） -----
    max_acceptable_entry = None  # 最大可接受买入价（满足R/R约束）

    if new_target and new_stop and new_target > new_stop and not is_deep_loss:
        # 买入价的R/R约束：
        # 要求 (target - entry) / (entry - stop) >= min_rr
        # 即 entry <= (target + min_rr * stop) / (1 + min_rr)
        min_rr = 1.0  # 至少1:1，才不亏
        recommend_rr = 1.5  # 推荐1:1.5以上

        max_for_recommend = (new_target + recommend_rr * new_stop) / (1 + recommend_rr)
        max_for_neutral = (new_target + min_rr * new_stop) / (1 + min_rr)

        if is_new_entry:
            # 新买入：要求1:1.5+
            max_acceptable_entry = max_for_recommend
        else:
            # 已持仓加仓：至少1:1
            max_acceptable_entry = max_for_neutral

    if is_new_entry:
        # 新买入：买入区 = 弱支撑附近（不是当前价附近！）
        # 只在价格跌到弱支撑附近时才推买入
        entry_low = round(price * 0.98, 2)
        entry_high = round(price * 1.02, 2)
        if max_acceptable_entry and entry_high > max_acceptable_entry:
            entry_high = round(max_acceptable_entry, 2)
        # 确保买入区不小于1%
        if entry_high - entry_low < price * 0.01:
            if max_acceptable_entry and price <= max_acceptable_entry:
                entry_low = round(max(price * 0.99, new_stop), 2)
                entry_high = round(min(price * 1.01, max_acceptable_entry), 2)
    elif ws and ws > 0 and wr and wr > 0 and not is_deep_loss:
        # 已持仓正常：买入区 = 弱支撑~弱支撑上方5%（给合理回调空间）
        # 上限不能低于成本价×0.95（保护已有持仓不被高位逼空）
        entry_low = round(ws, 2)
        entry_max = round(ws * 1.05, 2)  # 比弱支撑高5%，有足够空间
        # 如果当前价已远离买入区，保持买入区不变（不因价格涨了就收窄）
        min_upper = round(cost * 0.95, 2) if cost > 0 else 0
        if entry_max < min_upper:
            entry_max = min_upper
        if max_acceptable_entry:
            entry_high = round(min(entry_max, max_acceptable_entry), 2)
        else:
            entry_high = entry_max
        # 如果当前价已远离买入区（高于买入区上沿），禁止加仓推荐
        if price > entry_high:
            # 买入区锁定在弱支撑位，但标记为"价格远离"
            pass
        # 如果买入区过窄，标记但不扩展（加仓必须在支撑位）
        if entry_high - entry_low < price * 0.005:
            entry_low = round(ws * 0.995, 2)
            entry_high = round(ws * 1.005, 2)
    else:
        entry_low = round(price * 0.90, 2)
        entry_high = round(price * 1.05, 2)

    # 买入区间稳定性保护：上边界单次变动不超过5%
    if 'entry_high' in dir() and entry_high:
        # 读取当前策略中已有的买入区上界，如果有且变化过大则限制
        old_entry_high = None
        if 'current_action' in dir() and current_action:
            import re
            m = re.search(r'买入区[\d.]+~([\d.]+)', current_action)
            if m:
                old_entry_high = float(m.group(1))
        if old_entry_high and old_entry_high > 0:
            max_change = old_entry_high * 0.95  # 单次最多下降5%
            if entry_high < max_change:
                entry_high = round(max_change, 2)

    # ----- 买入时机信号 -----
    volume_signal = vol.get("volume_signal", "")
    candlestick_sentiment = candle.get("sentiment", "neutral")
    timing_signal = "neutral"

    if is_new_entry:
        # 新买入时机
        if volume_signal == "主动买盘占优" and candlestick_sentiment == "bullish":
            timing_signal = "量价齐升，可买入"
        elif volume_signal == "主动卖盘占优":
            timing_signal = "放量下跌，等企稳再入"
        elif volume_signal == "买卖均衡" and ws and price <= ws * 1.03:
            timing_signal = "缩量回踩支撑，可买入"
        elif candlestick_sentiment == "bullish":
            timing_signal = "阳线企稳，可买入"
        elif ws and price < ws * 1.02:
            timing_signal = "接近支撑位，关注"
    else:
        # 已持仓时机（用于加仓/减仓参考）
        if is_short_term_strong_trend:
            # 短炒+强趋势：强趋势持有，禁止加仓信号
            timing_signal = "强趋势持"
        elif profit_pct > 5:
            # 已盈利
            if volume_signal == "主动买盘占优":
                timing_signal = "放量走强，持有"
            elif volume_signal == "主动卖盘占优" and not is_new_entry:
                timing_signal = "获利盘涌出，关注"
            else:
                timing_signal = "持有"
        elif profit_pct > 0:
            # 微盈
            if volume_signal == "主动买盘占优":
                timing_signal = "温和放量，持有"
            elif ws and price <= ws * 1.02:
                timing_signal = "回踩支撑，可加仓"
            else:
                timing_signal = "持有"
        else:
            # 浮亏
            if volume_signal == "主动卖盘占优" and ss and price <= ss * 1.03:
                timing_signal = "量价齐跌近强撑，关注"
            elif volume_signal == "主动买盘占优" and sr_resist and price >= sr_resist * 0.97:
                timing_signal = "放量上涨近强压，关注止盈"
            elif volume_signal == "买卖均衡" and ws and price <= ws * 1.02:
                timing_signal = "缩量回踩弱支撑，可加仓"
            else:
                timing_signal = "持有"

    # ----- 【v3.2新增】分类约束：弱势/深套禁止输出买入/加仓类信号 -----
    if stock_category == "弱势" or is_deep_loss:
        buy_signals = ["可加仓", "可买入", "可追", "买"]
        if any(s in timing_signal for s in buy_signals):
            old_signal = timing_signal
            timing_signal = "弱势持有" if stock_category == "弱势" else "深套持有"
            print(f"  分类约束: {stock_category} 原信号\"{old_signal}\" → \"{timing_signal}\"")

    # ----- 构造 action 描述（供 cron prompt 使用） -----
    action_parts = []
    if profit_pct < -20:
        action_parts.append("深套持有")
    elif profit_pct < -10:
        action_parts.append("持有观察")
    elif profit_pct < 0:
        action_parts.append("持有观察")
    elif profit_pct < 5:
        action_parts.append("盈利持有")
    else:
        action_parts.append("盈利良好")

    if action_note:
        action_parts.append(action_note)

    if is_watchlist:
        # 自选股（未入场）：只有买入区+止损参考，无止盈
        action_parts.append(f"止损参考{new_stop}")
        action_parts.append(f"买入区{entry_low}~{entry_high}")
    elif is_new_entry:
        action_parts.append(f"损{new_stop}")
        action_parts.append(f"盈{new_target}")
        action_parts.append(f"买{entry_low}~{entry_high}")
    else:
        action_parts.append(f"止损{new_stop}")
        action_parts.append(f"目标{new_target}")
        action_parts.append(f"买入区{entry_low}~{entry_high}")

    if timing_signal != "neutral":
        action_parts.append(f"信号:{timing_signal}")

    new_action = " | ".join(action_parts)

    # 技术面快照
    tech_snapshot = ""
    if candle:
        tech_snapshot = (f"形态:{candle.get('pattern','?')}/{candle.get('sentiment','?')} "
                        f"量价:{vol.get('volume_signal','?')} "
                        f"强撑:{ss} 弱撑:{ws} 弱压:{wr} 强压:{sr_resist}")

    # 多周期快照（追加到 tech_snapshot）
    mtf_context = ""
    if mtf_adj:
        trend_align = mtf_adj.get("trend_alignment", "")
        daily_mas = mtf_analysis.get("daily", {}).get("mas", {})
        ma20 = daily_mas.get("ma20")
        ma60 = daily_mas.get("ma60")
        stop_ref = mtf_adj.get("stop_loss_reference", {})
        take_ref = mtf_adj.get("take_profit_reference", {})

        parts = []
        if trend_align:
            parts.append(trend_align)
        if ma20:
            parts.append(f"MA20={ma20}")
        if ma60:
            parts.append(f"MA60={ma60}")
        if stop_ref:
            parts.append(f"长撑:{stop_ref.get('source','?')}={stop_ref['level']}")
        if take_ref:
            parts.append(f"长压:{take_ref.get('source','?')}={take_ref['level']}")
        mtf_context = " | ".join(parts)

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M')
    return {
        'stop_loss': new_stop,
        'take_profit': new_target,
        'entry_low': entry_low,
        'entry_high': entry_high,
        'action': new_action,
        'status': status,
        'tech_snapshot': tech_snapshot,
        'timing_signal': timing_signal,
        'rr_ratio': round(rr_ratio, 2),
        'action_note': action_note,
        'reassessed_at': now_str,
        'multi_tf_context': mtf_context,  # 多周期上下文
        'stock_category': stock_category,  # 股票分类：短炒/中短线/中长线/弱势/深套
        'time_horizon': time_horizon,      # 时间跨度
        'position_advice': position_advice, # 仓位建议
    }


def new_action_needs_refresh(result, old_entry, price):
    """判断宏观/行业调整后是否需要刷新action文本"""
    # 自选股和手动策略不做调整，不需要刷新
    if old_entry.get("source") == "manual":
        return False
    return True


def _refresh_action_text(result, price, name):
    """根据调整后的止损/止盈重建action文本"""
    sl = result.get("stop_loss", 0)
    tp = result.get("take_profit", 0)
    el = result.get("entry_low", 0)
    eh = result.get("entry_high", 0)
    ts = result.get("timing_signal", "")
    an = result.get("action_note", "")
    old_action = result.get("action", "")
    
    # 保持原action的前缀（持有状态部分不变）
    # action格式一般是: "状态 | 止损X | 目标Y | 买入区X~Y | 信号:Z"
    parts = old_action.split(" | ")
    new_parts = []
    for p in parts:
        p = p.strip()
        # 替换止损数字
        if p.startswith("止损") or p.startswith("止损参考"):
            if sl:
                p = f"止损{sl}" if "止损参考" not in old_action.split(" | ")[0] else f"止损参考{sl}"
        # 替换目标/止盈数字
        if p.startswith("目标") or p.startswith("止盈"):
            if tp:
                p = f"目标{tp}"
        # 替换买入区数字
        if "买入区" in p and "~" in p:
            if el and eh:
                p = f"买入区{el}~{eh}"
        new_parts.append(p)
    result["action"] = " | ".join(new_parts)


def check_sector_alerts(market_ctx, stock_sector_map, holdings, wl):
    """行业轮动主动预警：检测板块崩盘级别信号→查持仓→输出预警
    
    返回 list of alerts: [{code, name, sector, chg, action}]
    """
    alerts = []
    if not market_ctx:
        return alerts
    
    sector_perf = market_ctx.get("sector_perf", {})
    
    # 找出所有跌幅>3%的行业
    crashing_sectors = {name: data for name, data in sector_perf.items()
                       if data.get("change", 0) <= -3}
    
    if not crashing_sectors:
        return alerts
    
    # 构建 code→持仓信息 的映射
    holding_map = {}
    for h in holdings:
        c = h.get("code", "")
        if c:
            holding_map[c] = {"name": h.get("name", c), "type": "持仓"}
    for s in wl.get("stocks", []):
        c = s.get("code", "")
        if c and c not in holding_map:
            holding_map[c] = {"name": s.get("name", c), "type": "自选"}
    
    # 对每个暴跌行业，查持仓中是否有股票属于该行业
    for sec_name, sec_data in sorted(crashing_sectors.items(),
                                      key=lambda x: x[1].get("change", 0)):
        chg = sec_data.get("change", 0)
        for code, sectors in stock_sector_map.items():
            if code in holding_map and sec_name in sectors:
                info = holding_map[code]
                alerts.append({
                    "code": code,
                    "name": info["name"],
                    "sector": sec_name,
                    "sector_change": chg,
                    "type": info["type"],
                    "action": f"行业{sec_name}跌{chg:+.1f}%，{info['type']}需关注",
                })
    
    alerts.sort(key=lambda a: a["sector_change"])
    return alerts


def regenerate_all(stdout=True):
    """全量重评所有持仓+自选策略"""
    # 优先从 SQLite 读取
    try:
        from mofin_db import get_conn, query_holdings, query_watchlist
        conn = get_conn()
        holdings = query_holdings(conn)
        wl_stocks = query_watchlist(conn)
        conn.close()
        pf = {"holdings": holdings}
        wl = {"stocks": wl_stocks}
    except Exception:
        pf = safe_json_load(PORTFOLIO_PATH, {})
        wl = safe_json_load(WATCHLIST_PATH, {})

    all_stocks = {}
    for item in pf.get("holdings", []):
        code = item.get("code", "")
        if code:
            all_stocks[code] = {"source": "portfolio", "data": item}
    for item in wl.get("stocks", []):
        code = item.get("code", "")
        if code and code not in all_stocks:
            all_stocks[code] = {"source": "watchlist", "data": item}

    total = len(all_stocks)
    ok = 0
    errors = 0
    results = []
    decisions = []

    # 加载现有 decisions.json 以便追踪变更
    decisions_path = "/home/hmo/web-dashboard/data/decisions.json"
    try:
        existing_decisions = {d["code"]: d for d in json.load(open(decisions_path)).get("decisions", []) if d.get("code")}
    except:
        existing_decisions = {}

    # 加载宏观上下文（影响策略参数调整）
    macro_bias, macro_desc = load_macro_context()
    if stdout:
        print(f"  宏观参考: {macro_desc} (bias={macro_bias})")
    
    # 加载市场上下文 — 行业板块表现 + 大盘宽度（策略参数调整用）
    market_ctx = load_market_context()
    stock_sector_map = load_stock_sector_map()
    market_breadth = market_ctx.get("breadth", 50)
    market_mood = market_ctx.get("mood", "neutral")
    if stdout:
        sectors_found = sum(1 for c in all_stocks if stock_sector_map.get(c))
        print(f"  市场参考: {market_mood} 上涨比{market_breadth}% 已匹配{sectors_found}/{total}只个股行业")

    # 批量预取所有价格（一次API调用 vs 之前N次）
    prices_map = batch_fetch_prices(list(all_stocks.keys()))
    if stdout:
        print(f"  批量获取价格: {len(prices_map)}/{total} 成功")

    for code, info in sorted(all_stocks.items()):
        stock = info["data"]
        name = stock.get("name", code)
        cost = stock.get("cost", 0) or 0
        shares = stock.get("shares", 0) or 0
        source = info["source"]

        q = prices_map.get(code)
        if not q or not q.get("price"):
            results.append({"code": code, "name": name, "error": "腾讯API无数据"})
            errors += 1
            if stdout:
                print(f"  ❌ {name}({code}): 腾讯API无数据")
            continue

        price = q["price"]
        profit_pct = (price - cost) / cost * 100 if cost else 0
        current_action = stock.get("analysis", {}).get("action", "")
        close_yest = q.get("close", 0)
        sentiment = "neutral"
        if close_yest and price > close_yest * 1.02:
            sentiment = "bullish"
        elif close_yest and price < close_yest * 0.98:
            sentiment = "bearish"

        try:
            is_wl = (source == "watchlist")
            result = reassess_strategy(
                code, name, price, cost, shares,
                current_action, volume_signal="中性", sentiment=sentiment,
                is_watchlist=(source == "watchlist"),
            )

            # --- Manual param preservation: 用户手动策略永不覆盖 ---
            old_entry = existing_decisions.get(code, {})
            if old_entry.get("source") == "manual":
                # 仅覆盖策略参数，技术分析/信号/价格照常保留
                for key in ["entry_low", "entry_high", "stop_loss", "take_profit"]:
                    if key in old_entry and old_entry[key] is not None:
                        result[key] = old_entry[key]
                # 重算盈亏比（基于手动参数）
                manual_stop = result.get("stop_loss", 0) or 0
                manual_target = result.get("take_profit", 0) or 0
                risk = max(price - manual_stop, price * 0.01) if manual_stop > 0 else price * 0.01
                reward = max(manual_target - price, 0) if manual_target > 0 else 0
                result["rr_ratio"] = round(reward / risk, 2) if risk > 0 else 0
                # 重建 action 文本（引用手动参数，不引用自动计算的）
                profit_pct = (price - cost) / cost * 100 if cost else 0
                manual_action_parts = []
                if profit_pct < -20:
                    manual_action_parts.append("深套持有")
                elif profit_pct < -10:
                    manual_action_parts.append("持有观察")
                elif profit_pct < 0:
                    manual_action_parts.append("持有观察")
                elif profit_pct < 5:
                    manual_action_parts.append("盈利持有")
                else:
                    manual_action_parts.append("盈利良好")
                if result.get("action_note"):
                    manual_action_parts.append(result["action_note"])
                if is_wl:
                    if manual_stop > 0:
                        manual_action_parts.append(f"止损参考{manual_stop}")
                    manual_action_parts.append(f"买入区{result['entry_low']}~{result['entry_high']}")
                else:
                    if manual_stop > 0:
                        manual_action_parts.append(f"止损{manual_stop}")
                    if manual_target > 0:
                        manual_action_parts.append(f"目标{manual_target}")
                    manual_action_parts.append(f"买入区{result['entry_low']}~{result['entry_high']}")
                ts = result.get("timing_signal", "")
                if ts and ts != "neutral":
                    manual_action_parts.append(f"信号:{ts}")
                result["action"] = " | ".join(manual_action_parts)
                result["status"] = "manual"  # 标记为手动管理，变更追踪不受影响
                if stdout:
                    print(f"  [手动保留] {name}({code}) 策略参数未覆盖")

            # 宏观偏差调整：收盘后重评时根据宏观方向微调止损/止盈
            # 自选股不做止盈宏观调整（无持仓）
            # 手动策略不做宏观偏差调整（尊重用户设定）
            if macro_bias != 1.0 and not is_wl and old_entry.get("source") != "manual":
                old_stop = result.get("stop_loss", 0)
                old_target = result.get("take_profit", 0)
                if macro_bias < 1.0 and old_stop > 0:  # 宏观偏弱 → 收紧止损
                    # 止损上移（但保留最小3%间距）
                    adjusted_stop = round(old_stop * (1 + (1 - macro_bias) * 0.3), 2)
                    min_stop = round(price * 0.97, 2)
                    result["stop_loss"] = min(adjusted_stop, min_stop)
                    if old_target > 0:
                        result["take_profit"] = round(old_target * (1 - (1 - macro_bias) * 0.2), 2)
                elif macro_bias > 1.0 and old_target > 0:  # 宏观偏强 → 止盈上调让利润跑
                    result["take_profit"] = round(old_target * (1 + (macro_bias - 1) * 0.3), 2)

            # 行业偏差调整：根据个股所在行业的市场表现微调止损/止盈
            # 手动策略不做行业调整（尊重用户设定）
            sector_adj = compute_sector_adjustment(code, market_ctx, stock_sector_map)
            sector_note = sector_adj.get("note", "")
            if sector_note and old_entry.get("source") != "manual":
                old_stop = result.get("stop_loss", 0)
                old_target = result.get("take_profit", 0)
                stop_bias = sector_adj.get("stop_bias", 1.0)
                target_bias = sector_adj.get("target_bias", 1.0)
                if stop_bias != 1.0 and old_stop > 0:
                    # 行业偏差调整（在宏观调整之后叠加）
                    adjusted = round(old_stop * stop_bias, 2)
                    # 保留最小3%间距
                    min_stop = round(price * 0.97, 2)
                    result["stop_loss"] = min(adjusted, min_stop)
                if target_bias != 1.0 and old_target > 0 and not is_wl:
                    result["take_profit"] = round(old_target * target_bias, 2)

            # 在宏观/行业调整后重建 action 文本（同步调整后的止损/止盈数字）
            if new_action_needs_refresh(result, old_entry, price):
                _refresh_action_text(result, price, name)

            extra = {
                "rr_ratio": result.get("rr_ratio"),
                "action_note": result.get("action_note", ""),
                "timing_signal": result.get("timing_signal", ""),
            }
            analysis = {
                "stop_loss": result["stop_loss"],
                "take_profit": result["take_profit"],
                "entry_low": result["entry_low"],
                "entry_high": result["entry_high"],
                "action": result["action"],
                "tech_snapshot": result.get("tech_snapshot", ""),
                "reassessed_at": result["reassessed_at"],
                "status": result["status"],
                **extra,
            }
            stock["analysis"] = analysis
            results.append({
                "code": code, "name": name,
                "price": price, "cost": cost,
                "action": result["action"],
                "stop_loss": result["stop_loss"],
                "take_profit": result["take_profit"],
                "rr_ratio": result["rr_ratio"],
            })
            ok += 1
            if stdout:
                rr_str = f" RR={result['rr_ratio']}" if "rr_ratio" in result else ""
                print(f"  ✅ {name}({code}) {price} {result['action']}{rr_str}")

            # 记录所有股票的决策日志（含变更追踪）
            status_display = result.get("status", "active")
            # 构建行业上下文
            sector_ctx_str = ""
            sec_name = sector_adj.get("sector_name", "")
            sec_chg = sector_adj.get("sector_change", 0)
            if sec_name:
                sector_ctx_str = f"行业{sec_name}{sec_chg:+.1f}%"
                if sector_adj.get("note"):
                    # note 已包含大盘宽度信息
                    sector_ctx_str = sector_adj["note"]
            elif market_breadth < 40:
                # 无行业映射时至少记录大盘宽度
                sector_ctx_str = f"大盘上涨比{market_breadth}%"
            new_entry = {
                "code": code, "name": name, "price": price, "cost": cost,
                "action": result["action"],
                "stop_loss": result.get("stop_loss"),
                "entry_low": result["entry_low"],
                "entry_high": result["entry_high"],
                "tech_snapshot": result.get("tech_snapshot", ""),
                "timing_signal": result.get("timing_signal", ""),
                "rr_ratio": result.get("rr_ratio", 0),
                "status": status_display,
                "note": result.get("action_note", ""),
                "timestamp": result["reassessed_at"],
                "updated_at": result["reassessed_at"],
                "type": "自选策略" if is_wl else "持仓策略",
                "source": old_entry.get("source", "auto"),  # manual/auto，继承旧标记
                "sector_context": sector_ctx_str,  # 市场上下文：行业表现+大盘宽度
                "stock_category": result.get("stock_category", "中短线"),  # 组合监测用
                "position_advice": result.get("position_advice", "中等仓位"),
                "time_horizon": result.get("time_horizon", "2周~3月"),
            }
            # created_at: 首次创建时设置，后续 preserve
            old_entry = existing_decisions.get(code, {})
            if old_entry.get("created_at"):
                new_entry["created_at"] = old_entry["created_at"]
            else:
                new_entry["created_at"] = result["reassessed_at"]
            # 自选股不写止盈，持仓股才写
            if not is_wl:
                new_entry["take_profit"] = result.get("take_profit")

            # --- 变更追踪 ---
            old_action = old_entry.get("action", "")
            old_stop = old_entry.get("stop_loss")
            old_target = old_entry.get("take_profit")

            # 构建旧策略摘要和变更理由
            update_reason = ""
            changelog_entry = None

            if old_action and old_action != result["action"]:
                # 策略有变化 → 记录变更
                old_summary = old_action
                new_summary = result["action"]

                # 判断触发原因
                if abs(price - old_entry.get("price", price)) / max(price, 0.01) > 0.03:
                    trigger = f"价格变动({old_entry.get('price','?')}→{price})"
                elif result.get("timing_signal") and result["timing_signal"] != old_entry.get("timing_signal", ""):
                    trigger = f"技术信号变化: {result['timing_signal']}"
                else:
                    trigger = "技术面重评"

                # 格式化的变更理由（自选股只看止损，不看止盈）
                diff_parts = []
                if old_stop and result["stop_loss"] != old_stop:
                    diff_parts.append(f"止损{old_stop}→{result['stop_loss']}")
                if not is_wl and old_target and result.get("take_profit") and result["take_profit"] != old_target:
                    diff_parts.append(f"止盈{old_target}→{result['take_profit']}")
                if diff_parts:
                    update_reason = f"{trigger}: {', '.join(diff_parts)} | {result.get('tech_snapshot','')[:60]}"
                else:
                    update_reason = f"{trigger}: 策略文字调整"

                changelog_entry = {
                    "date": result["reassessed_at"],
                    "old_action": old_action,
                    "new_action": result["action"],
                    "reason": update_reason,
                    "trigger": trigger,
                }
                new_entry["updated_reason"] = update_reason

            elif not old_action:
                # 首次创建策略
                update_reason = f"初始策略创建 | {result.get('tech_snapshot','')[:60]}"
                changelog_entry = {
                    "date": result["reassessed_at"],
                    "old_action": "",
                    "new_action": result["action"],
                    "reason": update_reason,
                    "trigger": "初始创建",
                }

            # 合并changelog
            old_changelog = old_entry.get("changelog", []) if old_entry else []
            if changelog_entry:
                new_entry["changelog"] = old_changelog + [changelog_entry]
            else:
                new_entry["changelog"] = old_changelog

            # 保留执行记录
            if old_entry and old_entry.get("execution"):
                new_entry["execution"] = old_entry["execution"]
            elif stock.get("analysis", {}).get("status") == "executing":
                new_entry["execution"] = {
                    "status": "executing",
                    "entry_price": cost if cost else 0,
                    "shares": shares,
                    "notes": "",
                }

            # --- 自动标记 current_recommend ---
            # 只在真正执行中的持仓才自动推荐：execution.status 为 executing 或 partial_exit
            exec_status = old_entry.get("execution", {}).get("status", "") if old_entry else ""
            is_active = exec_status in ("executing", "partial_exit")

            profit_pct = (price - cost) / cost * 100 if cost else 0
            is_deep_loss_stock = profit_pct < -20
            rr = result.get("rr_ratio", 0)
            ts = result.get("timing_signal", "")
            note = result.get("action_note", "")

            # 计算是否在/接近买入区
            entry_low_val = result.get("entry_low", 0)
            entry_high_val = result.get("entry_high", 0)
            in_buy_zone = (entry_low_val > 0 and entry_high_val > 0 and
                          entry_low_val <= price <= entry_high_val)
            near_buy_zone_low = (entry_low_val > 0 and
                                price >= entry_low_val * 0.98 and
                                price <= entry_high_val)

            # 推荐条件：必须是执行中的持仓 + 基本面条件达标
            is_recommendable = (
                is_active
                and not is_deep_loss_stock
                and rr >= 1.5
                and ts != "neutral"
                and "不建议" not in note
            )
            if is_recommendable:
                new_entry["tag"] = "current_recommend"
            else:
                # 不清除 active_manual（用户手动标记），只清除自动推荐的
                old_tag = old_entry.get("tag", "") if old_entry else ""
                if old_tag != "active_manual":
                    new_entry.pop("tag", None)

            decisions.append(new_entry)

        except Exception as e:
            results.append({"code": code, "name": name, "error": str(e)})
            errors += 1
            if stdout:
                print(f"  ❌ {name}({code}): {e}")

    # 写回数据文件
    json.dump(pf, open(PORTFOLIO_PATH, "w"), ensure_ascii=False, indent=2)
    json.dump(wl, open(wl_path, "w"), ensure_ascii=False, indent=2)

    # 写 decisions.json
    decisions_path = "/home/hmo/web-dashboard/data/decisions.json"
    decisions_data = {
        "decisions": decisions,  # 全部保留
        "total": len(decisions),
        "regenerated_at": datetime.now().strftime('%Y-%m-%d %H:%M'),
    }
    json.dump(decisions_data, open(decisions_path, "w"), ensure_ascii=False, indent=2)

    # 记录策略→提示词版本关联
    if HAS_PROMPT_TRACKING:
        try:
            for d in decisions:
                if d.get("code") and d.get("action"):
                    record_strategy_generation(
                        d["code"], d.get("name", ""), d.get("action", "")
                    )
        except Exception as e:
            if stdout:
                print(f"  ⚠️ 提示词版本追踪失败: {e}", file=sys.stderr)

    # 刷新多周期缓存到磁盘
    try:
        import multi_timeframe as _mtf
        _mtf.flush_mtf_cache()
    except Exception:
        pass

    summary = {"total": total, "ok": ok, "errors": errors}
    if stdout:
        print(f"\n✅ 全量重评完成: {ok}/{total}成功, {errors}错误")
    return summary


if __name__ == "__main__":
    regenerate_all()
