#!/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,要求输出完整策略""" cash = 321271 # 可用现金(从DB读取) total = 952879 # 总资产 # 拉取资金流数据 _flow_note = "暂无资金流数据" try: import sqlite3 as _sq, json as _j _db = _sq.connect("/home/hmo/MoFin/data/mofin.db") _fr = _db.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone() if _fr and _fr[0]: _fc = _j.loads(_fr[0]) _stocks = _fc.get("stocks", {}) _s = _stocks.get(data['code'], {}) if _s and _s.get("analysis"): _a = _s["analysis"] _net = _a.get("net_flow", 0) _main = _a.get("main_force", 0) _retail = _a.get("retail_flow", 0) _trend = _a.get("trend", "中性") _flow_note = f"净流入{_net:.0f}万 主力{_main:.0f}万 散户{_retail:.0f}万 趋势{_trend}" _db.close() except: pass # 拉取近期消息面 _news_note = "暂无近期消息" try: import sqlite3 as _sq _db = _sq.connect("/home/hmo/MoFin/data/mofin.db") _nr = _db.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", (data['code'], f'%{data.get("name","")[:4]}%') ).fetchall() if _nr: _news_note = " | ".join([f"{r[2][:10]} {r[1]} {r[0][:40]}" for r in _nr]) _db.close() except: pass return f"""你是一个资深A股分析师。请对{data['code']} {data.get('name','')}做一个完整的九维矩阵分析,并输出策略参数。 ⚠️ 重要:以下9个维度不是独立分析的,你必须交叉对比后给出综合结论。 例如:如果消息面利好但资金流在流出,说明利好可能是出货;如果基本面强但技术面破位,说明估值可能还没到底。 当前数据(以下数据均来自实时API,每条标注时间窗口,禁止使用模型内部训练数据): 大盘:{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','')[:300]}(MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势) 资金流:{_flow_note}(近5日累计) 消息面:{_news_note}(最近3条,自动标注抓取时间) 当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')} 原策略:{(data.get('action','') or '')[:200]} 我的总资产={total}元,可用现金={cash}元。 请严格按以下格式输出: 【交叉分析】用2-3句话说明哪些维度出现矛盾/共振,最关键的信号是什么 ① 大盘×基本面 [一句话,说明矛盾关系] ② 大盘×消息面 [一句话] ③ 大盘×技术面 [一句话] ④ 大盘×资金面 [一句话] ⑤ 行业×基本面 [一句话] ⑥ 行业×消息面 [一句话] ⑦ 行业×技术面 [一句话] ⑧ 行业×资金面 [一句话] ⑨ 个股×基本面 [一句话] ⑩ 个股×消息面 [一句话] ⑪ 个股×技术面 [一句话] ⑫ 个股×资金面 [一句话] 【综合结论】(买入/关注/观望/卖出) 【操作建议】具体操作建议 【买入区间】最低价~最高价 【建议止损】数字 【建议止盈】数字 【建议仓位】只有综合结论为"买入"时才输出此项。仓位计算公式: 基础仓位按RR确定:RR<1.5→不推荐,RR1.5~3→8%,RR3~5→12%,RR5+→15% 大盘偏弱×0.8,大盘偏强×1.15 蓝筹/白马×1.2,成长×0.85,题材/短线×0.6 最终仓位范围:5%~20% 同时考虑:现金{cash}元足够买多少手。 输出格式:"X%(理由:一句话说明为什么这个仓位)""" 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]) # 仓位:只有买入信号才需要,提取百分比数字 result["position"] = "" if result["signal"] == "买入": for l in text.split("\n"): if "建议仓位" in l: nums = re.findall(r'[\d.]+', l) for n in nums: f = float(n) if 1 <= f <= 30: # 合理的仓位范围 result["position"] = f"{f:.0f}%" break break 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() # 买入信号→推XMPP通知(在conn close前执行) if parsed.get("signal") == "买入": try: _nr = conn.execute("SELECT name, price FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() _name = _nr[0] if _nr else code _p = _nr[1] if _nr else 0 _el = parsed.get("entry_low", 0) _eh = parsed.get("entry_high", 0) _sl = parsed.get("stop_loss", 0) _tp = parsed.get("take_profit", 0) _pos = parsed.get("position", "") _msg = f"📈 {_name}({code}) 价{_p}→12维分析生成买入信号!区间{_el}~{_eh} 损{_sl} 盈{_tp} 仓位{_pos}" import urllib.request, json as _jj _req = urllib.request.Request("http://127.0.0.1:5805/", data=_jj.dumps({"body": _msg, "to": "hmo@yoin.fun", "type": "chat"}).encode(), headers={"Content-Type": "application/json"}) urllib.request.urlopen(_req, timeout=5) print(f" 📨 XMPP推送成功: {_msg[:60]}") except Exception as _e: print(f" ⚠️ XMPP推送失败: {_e}") 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()