From 016258bc09b0b5fb7f26e9b62a3ca6019d066130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Thu, 9 Jul 2026 23:16:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=B9=E9=87=8FLLM=E4=B9=9D=E7=BB=B4?= =?UTF-8?q?=E5=88=86=E6=9E=90+=E4=BB=93=E4=BD=8D=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E5=88=97+Dashboard=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/batch_reassess.py | 281 ++++++++++++++++++++++++++++++++++++++ server.py | 2 +- static/index.html | 5 +- 3 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 scripts/batch_reassess.py 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信号仓位 ${stocks.map(s => { const buyZone = (s.entry_low||0) + '~' + (s.entry_high||0); @@ -353,9 +353,10 @@ function renderWatchlist() { ${buyZone}
损${sl} 盈${tp} ${rr>0?rr.toFixed(2):'—'} ${signal||'—'} + ${s.position_advice || '—'} - ${detailDisplay || '—'} + ${detailDisplay || '—'} `; }).join('')}