chore: latest batch_reassess/premarket (HK fix, 3600s timeout)

This commit is contained in:
知微
2026-07-20 08:51:31 +08:00
parent 0190c02064
commit f946728b7d
4 changed files with 409 additions and 488 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+369 -424
View File
@@ -1,424 +1,369 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""batch_reassess.py — 批量补全12维(九维矩阵)LLM分析(逐只处理,间隔防限流) """batch_reassess.py — 批量补全九维分析(逐只处理,间隔防限流)
用法: 用法: python3 batch_reassess.py [--all] [--code XXXXXX]
python3 batch_reassess.py # 所有缺分析/过期的 active 策略
python3 batch_reassess.py --type holding # 只处理持仓策略 流程:收集最新数据 → 调LLM(gateway)写九维分析+策略 → 保存到DB
python3 batch_reassess.py --type watchlist # 只处理自选策略 """
python3 batch_reassess.py --type holding --today # 持仓每日刷新(今早未评过的强制重评) import sys, json, subprocess, sqlite3, re, time
python3 batch_reassess.py --code XXXXXX # 单只 from datetime import datetime
流程:收集最新数据 → 调LLM(gateway)写12维分析+策略 → 保存到DB DB = "/home/hmo/MoFin/data/mofin.db"
""" GATEWAY = "http://127.0.0.1:8643/v1/chat/completions"
import sys, json, subprocess, sqlite3, re, time COOLDOWN_HOURS = 1
from datetime import datetime
def has_llm_analysis(code):
DB = "/home/hmo/MoFin/data/mofin.db" """检查是否为LLM生成的九维分析(>500字)"""
GATEWAY = "http://127.0.0.1:8643/v1/chat/completions" conn = sqlite3.connect(DB)
COOLDOWN_HOURS = 1 r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
STALE_HOURS = 20 # 分析超过20小时视为过期,需要重评 conn.close()
return r and r[0] and r[0] > 500
def has_llm_analysis(code):
"""检查是否为LLM生成的12维分析(>500字)""" def in_cooldown(code):
conn = sqlite3.connect(DB) """冷却期检查"""
r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() conn = sqlite3.connect(DB)
conn.close() r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
return r and r[0] and r[0] > 500 conn.close()
if not r or not r[0]:
def in_cooldown(code): return False
"""冷却期检查""" try:
conn = sqlite3.connect(DB) last = datetime.fromisoformat(r[0])
r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() diff = (datetime.now() - last).total_seconds() / 3600
conn.close() return diff < COOLDOWN_HOURS
if not r or not r[0]: except:
return False return False
try:
last = datetime.fromisoformat(r[0]) def collect_data(code):
diff = (datetime.now() - last).total_seconds() / 3600 """收集最新数据"""
return diff < COOLDOWN_HOURS data = {"code": code}
except:
return False # 从DB读策略
conn = sqlite3.connect(DB)
def analysis_stale(code, force_today=False): 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()
"""分析是否过期(>STALE_HOURS 或 force_today 时今早4点前未重评)""" if r:
conn = sqlite3.connect(DB) data["name"] = r[0]
r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() data["entry_low"] = r[1] or 0
conn.close() data["entry_high"] = r[2] or 0
if not r or not r[0]: data["stop_loss"] = r[3] or 0
return True data["take_profit"] = r[4] or 0
try: data["timing_signal"] = r[5] or ""
last = datetime.fromisoformat(r[0]) data["action"] = r[6] or ""
if force_today: data["rr_ratio"] = r[7] or 0
today4am = datetime.now().replace(hour=4, minute=0, second=0, microsecond=0) data["tech_snapshot"] = r[8] or ""
return last < today4am data["sector_context"] = r[9] or ""
return (datetime.now() - last).total_seconds() / 3600 > STALE_HOURS data["stock_category"] = r[10] or ""
except: conn.close()
return True
# 从腾讯API拉最新价和基本面
def get_portfolio(): prefix = "sh" if str(code).startswith(("6","9")) else "sz"
"""从 portfolio_summary 读实时现金/总资产(不再硬编码)""" try:
try: r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10)
conn = sqlite3.connect(DB) parts = r.stdout.decode("gbk", errors="ignore").split("~")
r = conn.execute("SELECT cash, total_assets FROM portfolio_summary WHERE id=1").fetchone() data["price"] = float(parts[3]) if len(parts) > 3 and parts[3] else 0
conn.close() data["pe"] = parts[39] if len(parts) > 39 and parts[39] else ""
if r and r[1]: data["mcap"] = parts[44] if len(parts) > 44 and parts[44] else ""
return int(r[0] or 0), int(r[1]) data["change_pct"] = parts[32] if len(parts) > 32 and parts[32] else "0"
except Exception: except:
pass data["price"] = 0
return 0, 0
# 大盘
def collect_data(code): try:
"""收集最新数据""" conn = sqlite3.connect(DB)
data = {"code": code} mr = conn.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone()
if mr and mr[0]:
# 从DB读策略 s = json.loads(mr[0])
conn = sqlite3.connect(DB) data["macro"] = s.get("description", "大盘震荡")
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() conn.close()
if r: except:
data["name"] = r[0] data["macro"] = "大盘震荡"
data["entry_low"] = r[1] or 0
data["entry_high"] = r[2] or 0 return data
data["stop_loss"] = r[3] or 0
data["take_profit"] = r[4] or 0 def build_prompt(data):
data["timing_signal"] = r[5] or "" """构建LLM prompt,要求输出完整策略"""
data["action"] = r[6] or "" cash = 321271 # 可用现金(从DB读取)
data["rr_ratio"] = r[7] or 0 total = 952879 # 总资产
data["tech_snapshot"] = r[8] or ""
data["sector_context"] = r[9] or "" # 拉取资金流数据
data["stock_category"] = r[10] or "" _flow_note = "暂无资金流数据"
conn.close() try:
import sqlite3 as _sq, json as _j
# 从腾讯API拉最新价和基本面 _db = _sq.connect("/home/hmo/MoFin/data/mofin.db")
# 代码前缀:5位=港股(hk)6/9开头=沪(sh),其他=深(sz) _fr = _db.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone()
_c = str(code) if _fr and _fr[0]:
if len(_c) == 5: _fc = _j.loads(_fr[0])
prefix = "hk" _stocks = _fc.get("stocks", {})
elif _c.startswith(("6", "9")): _s = _stocks.get(data['code'], {})
prefix = "sh" if _s and _s.get("analysis"):
else: _a = _s["analysis"]
prefix = "sz" _net = _a.get("net_flow", 0)
try: _main = _a.get("main_force", 0)
r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10) _retail = _a.get("retail_flow", 0)
parts = r.stdout.decode("gbk", errors="ignore").split("~") _trend = _a.get("trend", "中性")
data["price"] = float(parts[3]) if len(parts) > 3 and parts[3] else 0 _flow_note = f"净流入{_net:.0f}万 主力{_main:.0f}万 散户{_retail:.0f}万 趋势{_trend}"
data["pe"] = parts[39] if len(parts) > 39 and parts[39] else "" _db.close()
data["mcap"] = parts[44] if len(parts) > 44 and parts[44] else "" except:
data["change_pct"] = parts[32] if len(parts) > 32 and parts[32] else "0" pass
except:
data["price"] = 0 # 拉取近期消息面
_news_note = "暂无近期消息"
# 大盘 try:
try: import sqlite3 as _sq
conn = sqlite3.connect(DB) _db = _sq.connect("/home/hmo/MoFin/data/mofin.db")
mr = conn.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone() _nr = _db.execute(
if mr and mr[0]: "SELECT summary, overall_sentiment, created_at FROM signal_news "
s = json.loads(mr[0]) "WHERE (code=? OR sector LIKE ?) AND overall_sentiment IN ('利好','利空') "
data["macro"] = s.get("description", "大盘震荡") "ORDER BY id DESC LIMIT 3",
conn.close() (data['code'], f'%{data.get("name","")[:4]}%')
except: ).fetchall()
data["macro"] = "大盘震荡" if _nr:
_news_note = " | ".join([f"{r[2][:10]} {r[1]} {r[0][:40]}" for r in _nr])
return data _db.close()
except:
def build_prompt(data): pass
"""构建LLM prompt,要求输出完整策略"""
cash, total = get_portfolio() # 实时从 portfolio_summary 读 return f"""你是一个资深A股分析师。请对{data['code']} {data.get('name','')}做一个完整的九维矩阵分析,并输出策略参数。
if not total:
cash, total = 241330, 929727 # 兜底(DB读不到时) ⚠️ 重要:以下9个维度不是独立分析的,你必须交叉对比后给出综合结论。
例如:如果消息面利好但资金流在流出,说明利好可能是出货;如果基本面强但技术面破位,说明估值可能还没到底。
# 拉取资金流数据
_flow_note = "暂无资金流数据" 当前数据(以下数据均来自实时API,每条标注时间窗口,禁止使用模型内部训练数据):
try: 大盘:{data.get('macro','震荡')}(当日实时)
import sqlite3 as _sq, json as _j 最新价:{data.get('price',0)} 涨跌:{data.get('change_pct','0')}%(当日实时)
_db = _sq.connect("/home/hmo/MoFin/data/mofin.db") PE={data.get('pe','?')}(最新财报) 市值={data.get('mcap','?')}亿
_fr = _db.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone() 行业:{data.get('sector_context','?')}(当日实时)
if _fr and _fr[0]: 技术面:{data.get('tech_snapshot','')[:300]}MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势)
_fc = _j.loads(_fr[0]) 资金流:{_flow_note}(近5日累计)
_stocks = _fc.get("stocks", {}) 消息面:{_news_note}(最近3条,自动标注抓取时间)
_s = _stocks.get(data['code'], {}) 当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')}
if _s and _s.get("analysis"): 原策略:{(data.get('action','') or '')[:200]}
_a = _s["analysis"]
_net = _a.get("net_flow", 0) 我的总资产={total}元,可用现金={cash}元。
_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}" 【交叉分析】用2-3句话说明哪些维度出现矛盾/共振,最关键的信号是什么
_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 【建议仓位】只有综合结论为"买入"时才输出此项。仓位计算公式:
基础仓位按RR确定:RR<1.5→不推荐,RR1.5~3→8%RR3~5→12%RR5+→15%
return f"""你是一个资深A股分析师。请对{data['code']} {data.get('name','')}做一个完整的九维矩阵分析,并输出策略参数。 大盘偏弱×0.8,大盘偏强×1.15
蓝筹/白马×1.2,成长×0.85,题材/短线×0.6
⚠️ 重要:以下9个维度不是独立分析的,你必须交叉对比后给出综合结论。 最终仓位范围:5%~20%
例如:如果消息面利好但资金流在流出,说明利好可能是出货;如果基本面强但技术面破位,说明估值可能还没到底 同时考虑:现金{cash}元足够买多少手
输出格式:"X%(理由:一句话说明为什么这个仓位)"""
当前数据(以下数据均来自实时API,每条标注时间窗口,禁止使用模型内部训练数据): def parse_response(text):
大盘:{data.get('macro','震荡')}(当日实时) """从LLM回复中提取策略参数"""
最新价:{data.get('price',0)} 涨跌:{data.get('change_pct','0')}%(当日实时) result = {"signal": "", "entry_low": 0, "entry_high": 0, "stop_loss": 0, "take_profit": 0, "position": ""}
PE={data.get('pe','?')}(最新财报) 市值={data.get('mcap','?')}亿
行业:{data.get('sector_context','?')}(当日实时) # 信号
技术面:{data.get('tech_snapshot','')[:300]}MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势) sl = [l for l in text.split("\n") if "综合结论" in l]
资金流:{_flow_note}(近5日累计) if sl:
消息面:{_news_note}(最近3条,自动标注抓取时间) for kw in ["买入","关注","观望","卖出"]:
当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')} if kw in sl[0]:
原策略:{(data.get('action','') or '')[:200]} result["signal"] = kw
break
我的总资产={total}元,可用现金={cash}元。
# 买入区间
请严格按以下格式输出: zl = [l for l in text.split("\n") if "买入区间" in l]
if zl:
【交叉分析】用2-3句话说明哪些维度出现矛盾/共振,最关键的信号是什么 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"):
基础仓位按RR确定:RR<1.5→不推荐,RR1.5~3→8%RR3~5→12%RR5+→15% if "建议仓位" in l:
大盘偏弱×0.8,大盘偏强×1.15 nums = re.findall(r'[\d.]+', l)
蓝筹/白马×1.2,成长×0.85,题材/短线×0.6 for n in nums:
最终仓位范围:5%~20% f = float(n)
同时考虑:现金{cash}元足够买多少手。 if 1 <= f <= 30: # 合理的仓位范围
输出格式:"X%(理由:一句话说明为什么这个仓位)""" result["position"] = f"{f:.0f}%"
def parse_response(text): break
"""从LLM回复中提取策略参数""" break
result = {"signal": "", "entry_low": 0, "entry_high": 0, "stop_loss": 0, "take_profit": 0, "position": ""}
return result
# 信号
sl = [l for l in text.split("\n") if "综合结论" in l] def save_result(code, full_text, parsed):
if sl: """保存LLM结果到DB"""
for kw in ["买入","关注","观望","卖出"]: conn = sqlite3.connect(DB)
if kw in sl[0]: now = datetime.now().isoformat()
result["signal"] = kw
break updates = ["full_analysis=?", "reassessed_at=?"]
params = [full_text, now]
# 买入区间
zl = [l for l in text.split("\n") if "买入区间" in l] if parsed["signal"]:
if zl: updates.append("timing_signal=?")
nums = re.findall(r'[\d.]+', zl[0]) params.append(parsed["signal"])
if len(nums) >= 2: if parsed["entry_low"] > 0:
result["entry_low"] = float(nums[0]) updates.append("entry_low=?")
result["entry_high"] = float(nums[1]) params.append(parsed["entry_low"])
if parsed["entry_high"] > 0:
# 止损 updates.append("entry_high=?")
for l in text.split("\n"): params.append(parsed["entry_high"])
if "建议止损" in l: if parsed["stop_loss"] > 0:
nums = re.findall(r'[\d.]+', l) updates.append("stop_loss=?")
if nums: result["stop_loss"] = float(nums[0]) params.append(parsed["stop_loss"])
if parsed["take_profit"] > 0:
# 止盈 updates.append("take_profit=?")
for l in text.split("\n"): params.append(parsed["take_profit"])
if "建议止盈" in l: if parsed["position"]:
nums = re.findall(r'[\d.]+', l) updates.append("position_advice=?")
if nums: result["take_profit"] = float(nums[0]) params.append(parsed["position"])
# 仓位:只有买入信号才需要,提取百分比数字 params.append(code)
result["position"] = "" sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'"
if result["signal"] == "买入": conn.execute(sql, params)
for l in text.split("\n"): conn.commit()
if "建议仓位" in l:
nums = re.findall(r'[\d.]+', l) # 买入信号→推XMPP通知(在conn close前执行)
for n in nums: if parsed.get("signal") == "买入":
f = float(n) try:
if 1 <= f <= 30: # 合理的仓位范围 _nr = conn.execute("SELECT name, price FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
result["position"] = f"{f:.0f}%" _name = _nr[0] if _nr else code
break _p = _nr[1] if _nr else 0
break _el = parsed.get("entry_low", 0)
_eh = parsed.get("entry_high", 0)
return result _sl = parsed.get("stop_loss", 0)
_tp = parsed.get("take_profit", 0)
def save_result(code, full_text, parsed): _pos = parsed.get("position", "")
"""保存LLM结果到DB""" _msg = f"📈 {_name}({code}) 价{_p}→12维分析生成买入信号!区间{_el}~{_eh}{_sl}{_tp} 仓位{_pos}"
conn = sqlite3.connect(DB) import urllib.request, json as _jj
now = datetime.now().isoformat() _req = urllib.request.Request("http://127.0.0.1:5805/",
data=_jj.dumps({"body": _msg, "to": "hmo@yoin.fun", "type": "chat"}).encode(),
updates = ["full_analysis=?", "reassessed_at=?"] headers={"Content-Type": "application/json"})
params = [full_text, now] urllib.request.urlopen(_req, timeout=5)
print(f" 📨 XMPP推送成功: {_msg[:60]}")
if parsed["signal"]: except Exception as _e:
updates.append("timing_signal=?") print(f" ⚠️ XMPP推送失败: {_e}")
params.append(parsed["signal"])
if parsed["entry_low"] > 0: conn.close()
updates.append("entry_low=?")
params.append(parsed["entry_low"]) def process_stock(code):
if parsed["entry_high"] > 0: """处理单只股票"""
updates.append("entry_high=?") print(f"\n{'='*50}")
params.append(parsed["entry_high"]) print(f"处理: {code}")
if parsed["stop_loss"] > 0: print(f"{'='*50}")
updates.append("stop_loss=?")
params.append(parsed["stop_loss"]) if has_llm_analysis(code):
if parsed["take_profit"] > 0: print(f" ⏭ 已有LLM九维分析,跳过")
updates.append("take_profit=?") return False
params.append(parsed["take_profit"])
if parsed["position"]: if in_cooldown(code):
updates.append("position_advice=?") print(f" ⏭ 冷却期内,跳过")
params.append(parsed["position"]) return False
params.append(code) print(f" 收集数据...", flush=True)
sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'" data = collect_data(code)
conn.execute(sql, params) if not data.get("price"):
conn.commit() print(f" ⚠️ 无价格数据,跳过")
return False
# 买入信号→推XMPP通知(在conn close前执行)
if parsed.get("signal") == "买入": print(f" 调LLM生成九维分析...", flush=True)
try: prompt = build_prompt(data)
_nr = conn.execute("SELECT name, price FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
_name = _nr[0] if _nr else code try:
_p = _nr[1] if _nr else 0 r = subprocess.run(["curl", "-s", "--max-time", "300",
_el = parsed.get("entry_low", 0) "-H", "Content-Type: application/json",
_eh = parsed.get("entry_high", 0) "-H", "Authorization: Bearer hermes123",
_sl = parsed.get("stop_loss", 0) "-d", json.dumps({"model":"deepseek-v4-flash","messages":[{"role":"user","content":prompt}],"max_tokens":2048}),
_tp = parsed.get("take_profit", 0) GATEWAY], capture_output=True, timeout=310)
_pos = parsed.get("position", "")
_msg = f"📈 {_name}({code}) 价{_p}→12维分析生成买入信号!区间{_el}~{_eh}{_sl}{_tp} 仓位{_pos}" if r.returncode != 0:
import urllib.request, json as _jj print(f" ❌ curl失败: {r.stderr.decode()[:100]}")
_req = urllib.request.Request("http://127.0.0.1:5805/", return False
data=_jj.dumps({"body": _msg, "to": "hmo@yoin.fun", "type": "chat"}).encode(),
headers={"Content-Type": "application/json"}) resp = json.loads(r.stdout)
urllib.request.urlopen(_req, timeout=5) if "choices" not in resp:
print(f" 📨 XMPP推送成功: {_msg[:60]}") print(f" ❌ API异常: {str(resp)[:200]}")
except Exception as _e: return False
print(f" ⚠️ XMPP推送失败: {_e}")
full_text = resp["choices"][0]["message"]["content"]
conn.close() print(f" ✅ LLM返回({len(full_text)}字)", flush=True)
def process_stock(code, force_today=False): parsed = parse_response(full_text)
"""处理单只股票""" print(f" 信号={parsed['signal']} 区间={parsed['entry_low']}~{parsed['entry_high']} 损={parsed['stop_loss']} 盈={parsed['take_profit']} 仓位={parsed['position']}")
print(f"\n{'='*50}")
print(f"处理: {code}") save_result(code, full_text, parsed)
print(f"{'='*50}") print(f" ✅ 已保存到DB")
return True
if in_cooldown(code):
print(f" ⏭ 冷却期内,跳过") except subprocess.TimeoutExpired:
return False print(f" ❌ 超时")
return False
# 有分析且未过期 → 跳过(除非 force_today 且今早未评) except Exception as e:
if has_llm_analysis(code) and not analysis_stale(code, force_today): print(f" ❌ 错误: {e}")
print(f" ⏭ 已有12维分析且未过期,跳过") return False
return False
def main():
print(f" 收集数据...", flush=True) codes = []
data = collect_data(code) if "--code" in sys.argv:
if not data.get("price"): idx = sys.argv.index("--code")
print(f" ⚠️ 无价格数据,跳过") codes = [sys.argv[idx+1]]
return False else:
# 所有自选策略
print(f" 调LLM生成九维分析...", flush=True) conn = sqlite3.connect(DB)
prompt = build_prompt(data) rows = conn.execute("SELECT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略' ORDER BY code").fetchall()
conn.close()
try: codes = [r[0] for r in rows]
r = subprocess.run(["curl", "-s", "--max-time", "300",
"-H", "Content-Type: application/json", print(f"待处理: {len(codes)}")
"-H", "Authorization: Bearer hermes123",
"-d", json.dumps({"model":"deepseek-v4-flash","messages":[{"role":"user","content":prompt}],"max_tokens":2048}), ok = 0
GATEWAY], capture_output=True, timeout=310) fail = 0
skip = 0
if r.returncode != 0: for i, code in enumerate(codes):
print(f" ❌ curl失败: {r.stderr.decode()[:100]}") if has_llm_analysis(code):
return False print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有LLM分析")
skip += 1
resp = json.loads(r.stdout) continue
if "choices" not in resp:
print(f" ❌ API异常: {str(resp)[:200]}") print(f" [{i+1}/{len(codes)}] ", end="", flush=True)
return False if process_stock(code):
ok += 1
full_text = resp["choices"][0]["message"]["content"] else:
print(f" ✅ LLM返回({len(full_text)}字)", flush=True) fail += 1
parsed = parse_response(full_text) # 间隔15秒(防gateway过载)
print(f" 信号={parsed['signal']} 区间={parsed['entry_low']}~{parsed['entry_high']} 损={parsed['stop_loss']} 盈={parsed['take_profit']} 仓位={parsed['position']}") if i < len(codes) - 1:
print(f" 等待15秒...", flush=True)
save_result(code, full_text, parsed) time.sleep(15)
print(f" ✅ 已保存到DB")
return True print(f"\n{'='*50}")
print(f"完成: {ok}成功, {fail}失败, {skip}跳过")
except subprocess.TimeoutExpired: print(f"{'='*50}")
print(f" ❌ 超时")
return False if __name__ == "__main__":
except Exception as e: main()
print(f" ❌ 错误: {e}")
return False
def main():
codes = []
force_today = "--today" in sys.argv
dtype = None
if "--type" in sys.argv:
idx = sys.argv.index("--type")
dtype = sys.argv[idx + 1] # holding | watchlist | all
if "--code" in sys.argv:
idx = sys.argv.index("--code")
codes = [sys.argv[idx+1]]
else:
# 按类型筛选 active 策略
type_map = {"holding": "持仓策略", "watchlist": "自选策略"}
conn = sqlite3.connect(DB)
if dtype in type_map:
rows = conn.execute(
"SELECT code FROM holding_strategies WHERE status='active' AND decision_type=? ORDER BY code",
(type_map[dtype],)).fetchall()
else:
rows = conn.execute(
"SELECT code FROM holding_strategies WHERE status='active' ORDER BY decision_type, code").fetchall()
conn.close()
codes = [r[0] for r in rows]
print(f"待处理: {len(codes)}只 (type={dtype or 'all'}, force_today={force_today})")
ok = 0
fail = 0
skip = 0
for i, code in enumerate(codes):
if has_llm_analysis(code) and not analysis_stale(code, force_today):
print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有12维分析且未过期")
skip += 1
continue
print(f" [{i+1}/{len(codes)}] ", end="", flush=True)
if process_stock(code, force_today):
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()
+40 -64
View File
@@ -1,64 +1,40 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""premarket_full_review.py — 盘前全量重评 """premarket_full_review.py — 盘前全量重评
执行顺序: 执行顺序:
1. regenerate_all() 全量技术参数重评(持仓+自选) 1. regenerate_all() 全量技术分析重评(持仓+自选)
2. batch_reassess.py --type holding --today 持仓12维LLM分析(每日强制刷新) 2. watchlist_auto_exit() 自选退出检查
3. watchlist_auto_exit() 自选退出检查 3. 输出摘要
4. 输出摘要
调度:交易日 08:10(A股09:30开盘)
调度:交易日 08:10(A股09:30开盘) """
""" import sys, os, json
import sys, os, json sys.path.insert(0, '/home/hmo/MoFin')
sys.path.insert(0, '/home/hmo/MoFin')
# Step 1: 全量重评
# Step 1: 全量技术参数重评 print("=" * 50)
print("=" * 50) print("📊 盘前全量重评开始")
print("📊 盘前全量重评开始") print("=" * 50)
print("=" * 50) from strategy_lifecycle import regenerate_all
from strategy_lifecycle import regenerate_all result = regenerate_all(stdout=True)
result = regenerate_all(stdout=True) print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
# Step 2: 自选退出
# Step 1.5: 持仓 12 维 LLM 深度分析(每日强制,14只约8-10分钟) print("\n" + "=" * 50)
print("\n" + "=" * 50) print("🔍 自选退出检查")
print("🧠 持仓12维LLM分析(每日强制刷新)") print("=" * 50)
print("=" * 50) from scripts.watchlist_auto_exit import main as auto_exit
import subprocess as _sp exited = auto_exit(dry_run=False)
analysis_result = {"ok": 0, "fail": 0, "skip": 0}
try: # Step 3: 写入摘要供开盘简报引用
r = _sp.run( summary = {
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py", "premarket_at": __import__('datetime').datetime.now().isoformat(),
"--type", "holding", "--today"], "reassess": result,
capture_output=True, text=True, timeout=3600) "auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited],
print(r.stdout[-2000:] if len(r.stdout) > 2000 else r.stdout) "total_kept": result.get('total', 0) - len(exited),
if r.returncode != 0 and r.stderr: }
print(f"⚠️ stderr: {r.stderr[:300]}") os.makedirs("/tmp/mofin_premarket", exist_ok=True)
# 从输出尾部解析统计 with open("/tmp/mofin_premarket/summary.json", "w") as f:
import re as _re json.dump(summary, f, ensure_ascii=False, indent=2)
m = _re.search(r"完成: (\d+)成功, (\d+)失败, (\d+)跳过", r.stdout)
if m: print(f"\n✅ 盘前重评完毕")
analysis_result = {"ok": int(m.group(1)), "fail": int(m.group(2)), "skip": int(m.group(3))}
except Exception as e:
print(f"⚠️ 12维分析步骤异常: {e}")
# Step 2: 自选退出
print("\n" + "=" * 50)
print("🔍 自选退出检查")
print("=" * 50)
from scripts.watchlist_auto_exit import main as auto_exit
exited = auto_exit(dry_run=False)
# Step 3: 写入摘要供开盘简报引用
summary = {
"premarket_at": __import__('datetime').datetime.now().isoformat(),
"reassess": result,
"llm_analysis_12d": analysis_result,
"auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited],
"total_kept": result.get('total', 0) - len(exited),
}
os.makedirs("/tmp/mofin_premarket", exist_ok=True)
with open("/tmp/mofin_premarket/summary.json", "w") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"\n✅ 盘前重评完毕")