diff --git a/scripts/batch_reassess.py b/scripts/batch_reassess.py
new file mode 100644
index 00000000..5cef291f
--- /dev/null
+++ b/scripts/batch_reassess.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python3
+"""batch_reassess.py — 批量补全九维分析(逐只处理,间隔防限流)
+
+用法: python3 batch_reassess.py [--all] [--code XXXXXX]
+
+流程:收集最新数据 → 调LLM(gateway)写九维分析+策略 → 保存到DB
+"""
+import sys, json, subprocess, sqlite3, re, time
+from datetime import datetime
+
+DB = "/home/hmo/MoFin/data/mofin.db"
+GATEWAY = "http://127.0.0.1:8643/v1/chat/completions"
+COOLDOWN_HOURS = 1
+
+def has_llm_analysis(code):
+ """检查是否为LLM生成的九维分析(>500字)"""
+ conn = sqlite3.connect(DB)
+ r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
+ conn.close()
+ return r and r[0] and r[0] > 500
+
+def in_cooldown(code):
+ """冷却期检查"""
+ conn = sqlite3.connect(DB)
+ r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
+ conn.close()
+ if not r or not r[0]:
+ return False
+ try:
+ last = datetime.fromisoformat(r[0])
+ diff = (datetime.now() - last).total_seconds() / 3600
+ return diff < COOLDOWN_HOURS
+ except:
+ return False
+
+def collect_data(code):
+ """收集最新数据"""
+ data = {"code": code}
+
+ # 从DB读策略
+ conn = sqlite3.connect(DB)
+ r = conn.execute("SELECT name, entry_low, entry_high, stop_loss, take_profit, timing_signal, action, rr_ratio, tech_snapshot, sector_context, stock_category FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
+ if r:
+ data["name"] = r[0]
+ data["entry_low"] = r[1] or 0
+ data["entry_high"] = r[2] or 0
+ data["stop_loss"] = r[3] or 0
+ data["take_profit"] = r[4] or 0
+ data["timing_signal"] = r[5] or ""
+ data["action"] = r[6] or ""
+ data["rr_ratio"] = r[7] or 0
+ data["tech_snapshot"] = r[8] or ""
+ data["sector_context"] = r[9] or ""
+ data["stock_category"] = r[10] or ""
+ conn.close()
+
+ # 从腾讯API拉最新价和基本面
+ prefix = "sh" if str(code).startswith(("6","9")) else "sz"
+ try:
+ r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10)
+ parts = r.stdout.decode("gbk", errors="ignore").split("~")
+ data["price"] = float(parts[3]) if len(parts) > 3 and parts[3] else 0
+ data["pe"] = parts[39] if len(parts) > 39 and parts[39] else ""
+ data["mcap"] = parts[44] if len(parts) > 44 and parts[44] else ""
+ data["change_pct"] = parts[32] if len(parts) > 32 and parts[32] else "0"
+ except:
+ data["price"] = 0
+
+ # 大盘
+ try:
+ conn = sqlite3.connect(DB)
+ mr = conn.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone()
+ if mr and mr[0]:
+ s = json.loads(mr[0])
+ data["macro"] = s.get("description", "大盘震荡")
+ conn.close()
+ except:
+ data["macro"] = "大盘震荡"
+
+ return data
+
+def build_prompt(data):
+ """构建LLM prompt,要求输出完整策略"""
+ return f"""你是一个资深A股分析师。请对{data['code']} {data.get('name','')}做一个完整的九维矩阵分析,并输出策略参数。
+
+当前数据:
+大盘:{data.get('macro','震荡')}
+最新价:{data.get('price',0)} 涨跌:{data.get('change_pct','0')}%
+PE={data.get('pe','?')} 市值={data.get('mcap','?')}亿
+行业:{data.get('sector_context','?')}
+技术面:{data.get('tech_snapshot','')[:200]}
+当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')}
+原策略:{(data.get('action','') or '')[:200]}
+
+请严格按以下格式输出:
+
+① 大盘×基本面 [一句话]
+② 大盘×消息面 [一句话]
+③ 大盘×技术面 [一句话]
+④ 大盘×资金流 [一句话]
+⑤ 行业×基本面 [一句话]
+⑥ 行业×消息面 [一句话]
+⑦ 行业×技术面 [一句话]
+⑧ 个股×基本面 [一句话]
+⑨ 个股×消息面 [一句话]
+
+【综合结论】(买入/关注/观望/卖出)
+【操作建议】具体操作建议
+【买入区间】最低价~最高价
+【建议止损】数字
+【建议止盈】数字
+【建议仓位】轻仓/中等仓位/重仓(并说明理由)"""
+
+def parse_response(text):
+ """从LLM回复中提取策略参数"""
+ result = {"signal": "", "entry_low": 0, "entry_high": 0, "stop_loss": 0, "take_profit": 0, "position": ""}
+
+ # 信号
+ sl = [l for l in text.split("\n") if "综合结论" in l]
+ if sl:
+ for kw in ["买入","关注","观望","卖出"]:
+ if kw in sl[0]:
+ result["signal"] = kw
+ break
+
+ # 买入区间
+ zl = [l for l in text.split("\n") if "买入区间" in l]
+ if zl:
+ nums = re.findall(r'[\d.]+', zl[0])
+ if len(nums) >= 2:
+ result["entry_low"] = float(nums[0])
+ result["entry_high"] = float(nums[1])
+
+ # 止损
+ for l in text.split("\n"):
+ if "建议止损" in l:
+ nums = re.findall(r'[\d.]+', l)
+ if nums: result["stop_loss"] = float(nums[0])
+
+ # 止盈
+ for l in text.split("\n"):
+ if "建议止盈" in l:
+ nums = re.findall(r'[\d.]+', l)
+ if nums: result["take_profit"] = float(nums[0])
+
+ # 仓位
+ for l in text.split("\n"):
+ if "建议仓位" in l:
+ result["position"] = l.replace("建议仓位","").strip()[:100]
+
+ return result
+
+def save_result(code, full_text, parsed):
+ """保存LLM结果到DB"""
+ conn = sqlite3.connect(DB)
+ now = datetime.now().isoformat()
+
+ updates = ["full_analysis=?", "reassessed_at=?"]
+ params = [full_text, now]
+
+ if parsed["signal"]:
+ updates.append("timing_signal=?")
+ params.append(parsed["signal"])
+ if parsed["entry_low"] > 0:
+ updates.append("entry_low=?")
+ params.append(parsed["entry_low"])
+ if parsed["entry_high"] > 0:
+ updates.append("entry_high=?")
+ params.append(parsed["entry_high"])
+ if parsed["stop_loss"] > 0:
+ updates.append("stop_loss=?")
+ params.append(parsed["stop_loss"])
+ if parsed["take_profit"] > 0:
+ updates.append("take_profit=?")
+ params.append(parsed["take_profit"])
+ if parsed["position"]:
+ updates.append("position_advice=?")
+ params.append(parsed["position"])
+
+ params.append(code)
+ sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'"
+ conn.execute(sql, params)
+ conn.commit()
+ conn.close()
+
+def process_stock(code):
+ """处理单只股票"""
+ print(f"\n{'='*50}")
+ print(f"处理: {code}")
+ print(f"{'='*50}")
+
+ if has_llm_analysis(code):
+ print(f" ⏭ 已有LLM九维分析,跳过")
+ return False
+
+ if in_cooldown(code):
+ print(f" ⏭ 冷却期内,跳过")
+ return False
+
+ print(f" 收集数据...", flush=True)
+ data = collect_data(code)
+ if not data.get("price"):
+ print(f" ⚠️ 无价格数据,跳过")
+ return False
+
+ print(f" 调LLM生成九维分析...", flush=True)
+ prompt = build_prompt(data)
+
+ try:
+ r = subprocess.run(["curl", "-s", "--max-time", "300",
+ "-H", "Content-Type: application/json",
+ "-H", "Authorization: Bearer hermes123",
+ "-d", json.dumps({"model":"deepseek-v4-flash","messages":[{"role":"user","content":prompt}],"max_tokens":2048}),
+ GATEWAY], capture_output=True, timeout=310)
+
+ if r.returncode != 0:
+ print(f" ❌ curl失败: {r.stderr.decode()[:100]}")
+ return False
+
+ resp = json.loads(r.stdout)
+ if "choices" not in resp:
+ print(f" ❌ API异常: {str(resp)[:200]}")
+ return False
+
+ full_text = resp["choices"][0]["message"]["content"]
+ print(f" ✅ LLM返回({len(full_text)}字)", flush=True)
+
+ parsed = parse_response(full_text)
+ print(f" 信号={parsed['signal']} 区间={parsed['entry_low']}~{parsed['entry_high']} 损={parsed['stop_loss']} 盈={parsed['take_profit']} 仓位={parsed['position']}")
+
+ save_result(code, full_text, parsed)
+ print(f" ✅ 已保存到DB")
+ return True
+
+ except subprocess.TimeoutExpired:
+ print(f" ❌ 超时")
+ return False
+ except Exception as e:
+ print(f" ❌ 错误: {e}")
+ return False
+
+def main():
+ codes = []
+ if "--code" in sys.argv:
+ idx = sys.argv.index("--code")
+ codes = [sys.argv[idx+1]]
+ else:
+ # 所有自选策略
+ conn = sqlite3.connect(DB)
+ rows = conn.execute("SELECT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略' ORDER BY code").fetchall()
+ conn.close()
+ codes = [r[0] for r in rows]
+
+ print(f"待处理: {len(codes)}只")
+
+ ok = 0
+ fail = 0
+ skip = 0
+ for i, code in enumerate(codes):
+ if has_llm_analysis(code):
+ print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有LLM分析")
+ skip += 1
+ continue
+
+ print(f" [{i+1}/{len(codes)}] ", end="", flush=True)
+ if process_stock(code):
+ ok += 1
+ else:
+ fail += 1
+
+ # 间隔15秒(防gateway过载)
+ if i < len(codes) - 1:
+ print(f" 等待15秒...", flush=True)
+ time.sleep(15)
+
+ print(f"\n{'='*50}")
+ print(f"完成: {ok}成功, {fail}失败, {skip}跳过")
+ print(f"{'='*50}")
+
+if __name__ == "__main__":
+ main()
diff --git a/server.py b/server.py
index e81f035d..6f70df04 100644
--- a/server.py
+++ b/server.py
@@ -111,7 +111,7 @@ def api_watchlist():
rows = conn.execute("""
SELECT hs.code, hs.name, hs.entry_low, hs.entry_high, hs.stop_loss, hs.take_profit,
hs.timing_signal, hs.action, lp.price, lp.change_pct, hs.rr_ratio, hs.updated_at,
- hs.tech_snapshot, hs.sector_context
+ hs.tech_snapshot, hs.sector_context, hs.full_analysis, hs.position_advice
FROM holding_strategies hs
LEFT JOIN live_prices lp ON hs.code = lp.code
WHERE hs.status='active' AND hs.decision_type='自选策略'
diff --git a/static/index.html b/static/index.html
index 5f01fa3b..85b876f6 100644
--- a/static/index.html
+++ b/static/index.html
@@ -320,7 +320,7 @@ function renderWatchlist() {
股票 现价
涨跌% 买入区间
- RR 信号
+ RR 信号 仓位