#!/usr/bin/env python3 """ per_stock_reassess.py — 按个股触发重评 对每只传进来的 code 执行 reassess_with_context(),然后写入 DB holding_strategies 表(纯DB模式,已移除JSON依赖)。 """ import sys, json, os, re from datetime import datetime COOLDOWN_HOURS_TRADING = 1 # 交易时段冷却(1小时) COOLDOWN_HOURS_NONTRADING = 24 # 非交易时段冷却 def _in_cooldown(code): """检查个股是否在重评冷却期内""" try: import sqlite3 conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db") r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active' ORDER BY id DESC LIMIT 1", (code,)).fetchone() conn.close() if not r or not r[0]: return False # 从未重评,立即执行 last = datetime.fromisoformat(r[0]) now = datetime.now() # 交易时段 vs 非交易时段 if 9 <= now.hour < 15: hours = COOLDOWN_HOURS_TRADING else: hours = COOLDOWN_HOURS_NONTRADING diff = (now - last).total_seconds() / 3600 return diff < hours except: return False sys.path.insert(0, "/home/hmo/web-dashboard") sys.path.insert(0, "/home/hmo/MoFin") from strategy_lifecycle import reassess_with_context as reassess_strategy from mo_data import read_decisions, read_portfolio def _build_full_analysis(code, entry, result): """从重评结果构建完整九维分析文本""" if not result: return "" lines = [] name = entry.get("name", code) price = result.get("price") or entry.get("price", 0) tech = result.get("tech_snapshot") or entry.get("tech_snapshot", "") sector = result.get("sector_context") or entry.get("sector_context", "") signal = result.get("timing_signal") or entry.get("timing_signal", "") category = result.get("stock_category") or entry.get("stock_category", "") el = result.get("entry_low") or entry.get("entry_low", 0) eh = result.get("entry_high") or entry.get("entry_high", 0) sl = result.get("stop_loss") or entry.get("stop_loss", 0) tp = result.get("take_profit") or entry.get("take_profit", 0) rr = result.get("rr_ratio") or entry.get("rr_ratio", 0) act = result.get("action", "") # ── 从DB拉取大盘、基本面、资金流 ── macro_desc = "" pe_val = pb_val = "" try: import sqlite3 as _sq, json as _j _db = _sq.connect("/home/hmo/MoFin/data/mofin.db") # 大盘(从structure列读取) _m = _db.execute("SELECT structure, sector_mood FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone() if _m and _m[0]: _st = _j.loads(_m[0]) _ix = _st.get("indices", {}) _desc = _st.get("description", "") if _ix: _parts = [] for _name in ["上证指数", "深证成指", "创业板指", "科创50", "恒生指数"]: if _name in _ix: _d = _ix[_name] if isinstance(_d, dict): _p = _d.get("price", 0) _c = _d.get("change_pct", 0) _parts.append(f"{_name}({_p:.0f},{_c:+.1f}%)") elif isinstance(_d, (int, float)): _parts.append(f"{_name}({_d})") macro_desc = " ".join(_parts) elif _desc: macro_desc = _desc _mood = str(_m[1] or "") if _mood and not macro_desc: macro_desc = f"情绪={_mood}" elif _mood: macro_desc += f" 情绪={_mood}" if not macro_desc: # fallback: 直接用腾讯API拉大盘 try: _r2 = __import__('subprocess').run(["curl", "-s", "http://qt.gtimg.cn/q=sh000001,sz399001,sz399006,sh000688"], capture_output=True, timeout=10) _txt = _r2.stdout.decode("gbk", errors="ignore") _parts = [] for _line in _txt.strip().split("\n"): if "~" not in _line: continue _p = _line.split("~") if len(_p) < 4: continue _name2 = _p[1] _price2 = _p[3] _chg2 = _p[32] if len(_p) > 32 else "0" _parts.append(f"{_name2}({_price2},{_chg2}%)") if _parts: macro_desc = "腾讯实时 " + " ".join(_parts[:3]) except: pass # 基本面+实时价:直接从腾讯API拉(盘后也有收盘价) try: _pfx = "sh" if str(code).startswith(("6", "9")) else "sz" _r3 = __import__('subprocess').run(["curl", "-s", f"http://qt.gtimg.cn/q={_pfx}{code}"], capture_output=True, timeout=10) _txt3 = _r3.stdout.decode("gbk", errors="ignore") _p3 = _txt3.split("~") if len(_p3) > 45: _pe = _p3[39] if _p3[39] else "" _pb = _p3[40] if len(_p3) > 40 and _p3[40] else "" _mcap = _p3[44] if len(_p3) > 44 and _p3[44] else "" _price_now = float(_p3[3]) if _p3[3] else 0 _chg_now = float(_p3[32]) if len(_p3) > 32 and _p3[32] else 0 if _price_now > 0: price = _price_now # 覆盖策略中的price=0 if _pe: pe_val = f"PE={_pe}" if _pb: pb_val = f"PB={_pb}" if _mcap: mcap_val = f"市值{float(_mcap)/10000:.1f}亿" if float(_mcap) > 10000 else f"市值{_mcap}万" pe_val += f" {mcap_val}" if pe_val else mcap_val except: pass _db.close() except Exception as _e: pass # ── 从tech_snapshot提取MA和支撑阻力 ── import re ma5 = ma10 = ma20 = ma60 = "?" ma_match = re.search(r'MA5=([\d.]+).*?MA10=([\d.]+).*?MA20=([\d.]+).*?MA60=([\d.]+)', tech) if ma_match: ma5, ma10, ma20, ma60 = ma_match.groups() lines.append(f"【{name}({code} 九维全析)】") lines.append("") if macro_desc: lines.append(f"① 大盘环境(当日实时):{macro_desc}") else: lines.append(f"① 大盘环境(当日实时):数据待刷新") if pe_val or pb_val: lines.append(f"② 个股基本面(最新财报):{pe_val} {pb_val}") else: lines.append(f"② 个股基本面(最新财报):数据待补充") lines.append(f"③ 技术面(MA5/10/20/60日 支撑阻力近20日):MA5={ma5} MA10={ma10} MA20={ma20} MA60={ma60}") if el and eh and price > 0: pos = "在买入区内" if el <= price <= eh else (f"低于买入区{(1-price/el)*100:.0f}%" if price < el else f"高于买入区{(price/eh-1)*100:.0f}%") lines.append(f"④ 价格位置:{price} {pos} 区间{el}~{eh}") else: lines.append(f"④ 价格位置:数据待刷新") if sl and tp and rr: lines.append(f"⑤ 风报比:止损{sl} 止盈{tp} RR={rr:.1f}") # 支撑阻力 sr_m = re.search(r'强撑:([\d.]+).*?弱撑:([\d.]+).*?弱压:([\d.]+).*?强压:([\d.]+)', tech) if sr_m: lines.append(f"⑥ 支撑阻力:强撑{sr_m.group(1)}→弱撑{sr_m.group(2)}→弱压{sr_m.group(3)}→强压{sr_m.group(4)}") if sector: lines.append(f"⑦ 行业背景:{sector}") else: # 从stock_sectors表补行业 try: _s2 = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _sr = _s2.execute("SELECT sector_name FROM stock_sectors WHERE code=? LIMIT 1", (code,)).fetchone() if _sr and _sr[0]: lines.append(f"⑦ 行业背景:{_sr[0]}") _s2.close() except: pass # 消息面:从signal_news读最新信号 news_lines = [] try: _n_db = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _nr = _n_db.execute( "SELECT summary, overall_sentiment, created_at FROM signal_news " "WHERE (sector LIKE ? OR sector LIKE ?) AND overall_sentiment IN ('利好','利空') " "ORDER BY id DESC LIMIT 2", (f'%{code}%', f'%{name[:4]}%') ).fetchall() for _ns in _nr: _sent = _ns[1] _icon = '📈' if '利好' in str(_sent) else '📉' news_lines.append(f"{_icon} {_ns[0][:60]} ({str(_ns[2])[:10]})") _n_db.close() except: pass if category: lines.append(f"⑧ 分类评级:{category}") lines.append(f"⑨ 策略信号:{signal}") if news_lines: lines.append("") lines.extend(news_lines) if act: lines.append(f"\n策略详情:{act[:200]}") return "\n".join(lines) def main(): codes = [a for a in sys.argv[1:] if not a.startswith("-")] if not codes: print("[FULL] 无指定编码,跑全量 regenerate_all()") from strategy_lifecycle import regenerate_all regenerate_all(stdout=False) print("[FULL] 全量重评完成") return # 读现有 decisions raw = read_decisions() decisions_map = {d["code"]: d for d in raw.get("decisions", []) if d.get("code")} ok = 0 errors = 0 skipped = 0 for code in codes: # 冷却期检查 if _in_cooldown(code): print(f" ⏭ {code}: 冷却期内跳过") skipped += 1 continue entry = decisions_map.get(code) if not entry: # 不在 decisions 中的自选股 → 从 holding_strategies 构建entry import sqlite3 _db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') _db.row_factory = sqlite3.Row _wl = _db.execute("SELECT * FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'", (code,)).fetchone() _db.close() if _wl: entry = { "code": code, "name": _wl["name"], "price": _wl["price"] or 0, "cost": 0, "shares": 0, "entry_low": _wl["entry_low"] or 0, "entry_high": _wl["entry_high"] or 0, "stop_loss": _wl["stop_loss"] or 0, "take_profit": 0, "action": "", "type": "自选策略", "is_watchlist": True, "analysis": json.loads(_wl["analysis_json"]) if _wl["analysis_json"] else {} } print(f"[WL] {code} {_wl['name']}: 从自选表构建entry") if not entry: print(f"[SKIP] {code}: 不在 decisions 或 watchlist_stocks 中") errors += 1 continue try: # Always fetch live price for accurate reassessment price = 0 try: # 价格从 DB 读取(price_monitor 每2分钟更新,唯一价格入口) code_raw = entry.get("code", "") price = 0 import sqlite3 db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') db.row_factory = sqlite3.Row row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (code_raw,)).fetchone() if not row: row = db.execute("SELECT price FROM watchlist_stocks WHERE code=? AND is_active=1", (code_raw,)).fetchone() if not row: row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (code_raw,)).fetchone() if row: price = row['price'] or 0 db.close() if price > 0: print(f" 实时价: {price} (来自DB)") else: # fallback to DB portfolio data _pf_data = read_portfolio() for _h in _pf_data.get("holdings", []): if _h["code"] == code_raw: price = float(_h.get("price", 0)) break if price <= 0: price = entry.get("current_price") or entry.get("price") or 0 except Exception as e: print(f" 价格获取失败: {e}", file=sys.stderr) price = entry.get("current_price") or entry.get("price") or 0 # Price diff debounce: skip reassessment if price changed < 1% since last update last_price = entry.get("last_reassessed_price") or 0 if last_price > 0 and price > 0: diff_pct = abs(price - last_price) / last_price * 100 if diff_pct < 1.0: print(f" 价差仅{diff_pct:.2f}% (<1%),跳过重评(上次价={last_price},现价={price})") skipped += 1 continue # 打印参数调试 if entry is None: print(f" DEBUG: code={code} ENTRY=NONE 跳过") print(f" [SKIP] {code} 策略数据不存在") skipped += 1 continue entry_action = str(entry.get('action') or '') print(f" DEBUG: code={code} name={entry.get('name','')} price={price} cost={entry.get('cost')} shares={entry.get('shares')} action={entry_action[:30]} is_wl={entry.get('type','') in ('自选策略','watchlist')}", flush=True) result = reassess_strategy( code=code, name=entry.get("name", ""), price=price or 0, cost=entry.get("cost") or 0, shares=entry.get("shares") or 0, current_action=entry.get("action", ""), is_watchlist=entry.get("type", "") in ("自选策略", "watchlist"), ) if result and result.get("action"): # 持仓股止损不下移(移动止损规则):已有仓位的止损只上不下 is_held = (entry.get("cost") or 0) > 0 and (entry.get("shares") or 0) > 0 and \ entry.get("type", "") not in ("自选策略", "watchlist") old_stop = entry.get("stop_loss") or 0 new_stop = result.get("stop_loss") or 0 if is_held and old_stop > 0 and new_stop > 0 and new_stop < old_stop: print(f" 移动止损保护: {new_stop}→保持{old_stop} (持仓止损不下移)") result["stop_loss"] = old_stop # 同时更新 action 字符串中的止损值 act = result.get("action", "") if act: act = re.sub(r'止损[\d.]+', f'止损{old_stop}', act) result["action"] = act # ── 写入 DB holding_strategies 表(替代 decisions.json)── try: from mofin_db import get_conn, write_holding_strategy _conn = get_conn() _db_entry = { "code": code, "name": entry.get("name", ""), "price": price, "cost": entry.get("cost", 0), "shares": entry.get("shares", 0), "stop_loss": result.get("stop_loss", entry.get("stop_loss")), "take_profit": result.get("take_profit", entry.get("take_profit")), "entry_low": result.get("entry_low", entry.get("entry_low")), "entry_high": result.get("entry_high", entry.get("entry_high")), "currency": "HKD" if (len(str(code)) == 5 and str(code)[0] in '01') else "CNY", "strategy_type": "自选策略" if entry.get("type", "") in ("自选策略", "watchlist") else "持仓策略", "action": result.get("action", ""), "timing_signal": result.get("timing_signal", entry.get("timing_signal", "")), "rr_ratio": result.get("rr_ratio", entry.get("rr_ratio", 0)), "tech_snapshot": result.get("tech_snapshot", entry.get("tech_snapshot", "")), "stock_category": result.get("stock_category", entry.get("stock_category", "")), "sector_context": result.get("sector_context", entry.get("sector_context", "")), "status": result.get("status", "active"), "source": entry.get("source", "auto"), "reason": result.get("action_note", ""), "version": entry.get("version", 1), "full_analysis": _build_full_analysis(code, entry, result) if result else "", } write_holding_strategy(_conn, code, entry.get("name", ""), _db_entry) _conn.commit() _conn.close() # 验证写入 _fa_check = _db_entry.get("full_analysis", "") print(f" DEBUG: full_analysis长度={len(_fa_check)} 内容=[{_fa_check[:100]}]") # 直接用SQL写入full_analysis try: _fa_conn = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _fa_conn.execute("UPDATE holding_strategies SET full_analysis=? WHERE code=? AND status='active'", (_fa_check, code)) _fa_conn.commit() _fa_conn.close() print(f" ✅ full_analysis直接SQL写入成功") except Exception as _fa_e: print(f" ⚠️ 直接SQL写入失败: {_fa_e}") _v = __import__('sqlite3').connect(str(__import__('pathlib').Path("/home/hmo/MoFin/data/mofin.db"))) _fa = _v.execute("SELECT full_analysis FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() if _fa and _fa[0]: print(f" ✅ full_analysis已写入({len(_fa[0])}字)") else: print(f" ⚠️ full_analysis为空") _v.close() # LLM生成完整九维分析 _macro_desc = "" _pe_val = "" _pb_val = "" try: _mdb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _mr = _mdb.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone() if _mr and _mr[0]: _st = __import__('json').loads(_mr[0]) _macro_desc = _st.get("description", "") _mood = _mr[1] if len(_mr) > 1 else "" if _mood: _macro_desc += f" 情绪={_mood}" # 基本面从腾讯API _p = "sh" if str(code).startswith(("6","9")) else "sz" _cr = __import__('subprocess').run(["curl","-s",f"http://qt.gtimg.cn/q={_p}{code}"], capture_output=True, timeout=10) _ct = _cr.stdout.decode("gbk", errors="ignore").split("~") if len(_ct) > 39 and _ct[39]: _pe_val = f"PE={_ct[39]}" if len(_ct) > 44 and _ct[44]: _pb_val = f"PB≈{float(_ct[44])/10000:.1f}亿" _mdb.close() except: pass # 拉取资金流数据 _flow_note = "暂无资金流数据" try: _fdb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _fr = _fdb.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone() if _fr and _fr[0]: _fc = __import__('json').loads(_fr[0]) _s = _fc.get("stocks", {}).get(code, {}) if _s and _s.get("analysis"): _a = _s["analysis"] _flow_note = f"净流入{_a.get('net_flow',0):.0f}万 主力{_a.get('main_force',0):.0f}万 趋势{_a.get('trend','中性')}" _fdb.close() except: pass # 拉取近期消息面 _news_note = "暂无近期消息" try: _ndb = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _nr2 = _ndb.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", (code, f'%{entry.get("name","")[:4]}%') ).fetchall() if _nr2: _news_note = " | ".join([f"{r[2][:10]} {r[1]} {r[0][:40]}" for r in _nr2]) _ndb.close() except: pass _prompt = f"""你是一个资深股票分析师。请对股票{code}做一个完整的12维矩阵分析(3横×4纵:大盘/行业/个股 × 基本面/消息面/技术面/资金面)。 ⚠️ 重要:12个维度必须交叉对比,找出矛盾/共振点,给出综合判断。 当前数据(实时API,每条标注时间窗口,禁止使用模型训练数据): 大盘={_macro_desc or "震荡"}(当日实时) | PE/市值={_pe_val} {_pb_val}(最新财报) | 价格={price} 区间={entry.get("entry_low",0)}~{entry.get("entry_high",0)} 止损={entry.get("stop_loss",0)} 止盈={entry.get("take_profit",0)} RR={result.get("rr_ratio",entry.get("rr_ratio",0))} | 信号={result.get("timing_signal") or entry.get("timing_signal","")} | 行业={(result.get("sector_context") or entry.get("sector_context",""))[:50]}(当日实时) 策略={(result.get("action") or entry.get("action",""))[:200]} 技术={(result.get("tech_snapshot") or entry.get("tech_snapshot",""))[:200]}(MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势) 资金流={_flow_note}(近5日累计) 消息面={_news_note}(最近3条,自动标注抓取时间) 格式: 【交叉分析】哪些维度矛盾/共振,关键信号 ① 大盘×基本面 ② 大盘×消息面 ③ 大盘×技术面 ④ 大盘×资金面 ⑤ 行业×基本面 ⑥ 行业×消息面 ⑦ 行业×技术面 ⑧ 行业×资金面 ⑨ 个股×基本面 ⑩ 个股×消息面 ⑪ 个股×技术面 ⑫ 个股×资金面 最后必须输出: 【综合结论】(买入/关注/观望/卖出) 【操作建议】 【建议止损】 【建议止盈】""" try: _ur = __import__('urllib.request', fromlist=['Request']) _req = _ur.Request("http://127.0.0.1:8643/v1/chat/completions", data=__import__('json').dumps({"model":"deepseek-v4-flash","messages":[{"role":"user","content":_prompt}],"max_tokens":1024}).encode(), headers={"Content-Type":"application/json","Authorization":"Bearer hermes123"}) _resp = _ur.build_opener(_ur.ProxyHandler({})).open(_req, timeout=300) _llm_out = __import__('json').loads(_resp.read().decode())["choices"][0]["message"]["content"] _full_analysis_text = _llm_out print(f" ✅ LLM12维分析完成({len(_full_analysis_text)}字)", flush=True) except Exception as _e: print(f" ❌ LLM12维分析失败: {_e}", file=__import__('sys').stderr) _full_analysis_text = None # 保存到DB _fa_conn = __import__('sqlite3').connect("/home/hmo/MoFin/data/mofin.db") _fa_conn.execute("UPDATE holding_strategies SET full_analysis=?, reassessed_at=? WHERE code=? AND status='active'", (_full_analysis_text, __import__('datetime').datetime.now().isoformat(), code)) _fa_conn.commit() _fa_conn.close() print(f" ✅ 完整12维分析已保存({len(_full_analysis_text)}字)" if _full_analysis_text else f" ⚠️ 12维分析未完成,跳过保存") print(f" [DB] holding_strategies 已更新: {code}") # 从LLM输出提取信号 if _full_analysis_text and '【综合结论】' in _full_analysis_text: try: _sig_line = [l for l in _full_analysis_text.split('\n') if '综合结论' in l] if _sig_line: _sig = '买入' if '买入' in _sig_line[0] else '关注' if '关注' in _sig_line[0] else '观望' if '观望' in _sig_line[0] else '卖出' if '卖出' in _sig_line[0] else '' if _sig: __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute( "UPDATE holding_strategies SET timing_signal=? WHERE code=? AND status='active'", (_sig, code)).connection.commit() print(f" ✅ LLM信号={_sig} 已写入") # 买入信号→推XMPP if _sig == "买入": try: _nr2 = __import__('sqlite3').connect('/home/hmo/MoFin/data/mofin.db').execute( "SELECT name, price, entry_low, entry_high, stop_loss, take_profit, position_advice FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() if _nr2: _xm = f"📈 {_nr2[0] or code}({code}) 价{_nr2[1]}→12维买入信号!区间{_nr2[2]}~{_nr2[3]} 损{_nr2[4]} 盈{_nr2[5]} 仓位{_nr2[6] or '-'}" _xr = __import__('urllib.request').Request("http://127.0.0.1:5805/", data=__import__('json').dumps({"body": _xm, "to": "hmo@yoin.fun", "type": "chat"}).encode(), headers={"Content-Type": "application/json"}) __import__('urllib.request').urlopen(_xr, timeout=5) print(f" 📨 XMPP推送买入信号") except: pass except: pass # 冷却期已更新(reassessed_at写入) except Exception as _dbe: print(f" [DB FAIL] holding_strategies 写入失败: {_dbe}", file=sys.stderr) # 更新 decisions_map 中对应的条目 updated = entry.copy() # 币种标记:HK股保留HKD原始值,A股为CNY is_hk = len(str(code)) == 5 and str(code)[0] in '01' updated.update({ "action": result["action"], "stop_loss": result.get("stop_loss", entry.get("stop_loss")), "entry_low": result.get("entry_low", entry.get("entry_low")), "entry_high": result.get("entry_high", entry.get("entry_high")), "take_profit": result.get("take_profit"), "tech_snapshot": result.get("tech_snapshot", entry.get("tech_snapshot")), "timing_signal": result.get("timing_signal", entry.get("timing_signal")), "rr_ratio": result.get("rr_ratio", entry.get("rr_ratio", 0)), "status": result.get("status", "updated"), "price": price, "currency": "HKD" if is_hk else "CNY", }) # Save last reassessed price for debounce tracking updated["last_reassessed_price"] = price decisions_map[code] = updated # ——— 初始化多分支策略树 ——— try: sys.path.insert(0, '/home/hmo/MoFin') from strategy_tree import init_default_branches branches = init_default_branches( code, entry.get('name', ''), result.get('entry_low', 0), result.get('entry_high', 0), result.get('stop_loss', 0), result.get('take_profit', 0), ) st = updated.setdefault('strategy_tree', {}) st['branches'] = branches except Exception: pass print(f"[OK] {code} {entry.get('name','')}: {result['action'][:80]}") ok += 1 else: print(f"[SYNCED] {code}: 无变更") ok += 1 except Exception as e: print(f"[ERROR] {code}: {e}", file=sys.stderr) import traceback traceback.print_exc(file=sys.stderr) errors += 1 # 同步自选股更新回 watchlist_stocks 表(持仓策略已通过 write_holding_strategy 写入 DB) try: from datetime import datetime as _dt import sqlite3 _db2 = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') for _code in codes: _entry = decisions_map.get(_code) if _entry and _entry.get("is_watchlist"): _db2.execute(""" UPDATE watchlist_stocks SET entry_low=?, entry_high=?, stop_loss=?, price=?, analysis_json=json(?) WHERE code=? AND is_active=1 """, ( _entry.get("entry_low", 0), _entry.get("entry_high", 0), _entry.get("stop_loss", 0), _entry.get("price", 0), json.dumps({ "action": _entry.get("action",""), "take_profit": _entry.get("take_profit", 0), "stop_loss": _entry.get("stop_loss", 0), "tech_snapshot": _entry.get("tech_snapshot", ""), "rr": _entry.get("rr_ratio", 0), "reassessed_at": _dt.now().strftime("%Y-%m-%d") }, ensure_ascii=False), _code )) _db2.commit() _db2.close() if any(e.get("is_watchlist") for e in [decisions_map.get(c) for c in codes] if e): print("[SYNC] 自选股策略已同步回 watchlist_stocks 表") except Exception as e: print(f"[SYNC FAIL] watchlist_stocks 同步失败: {e}", file=sys.stderr) print(f"[DONE] {ok}成功 {skipped}跳过 {errors}失败") # ── 第二步:扫描自选股(watchlist),价格偏离买入区>20%触发重评 ── scan_watchlist_stocks() # ════════════════════════════════════════════════════════════════════ # 自选股扫描 # ════════════════════════════════════════════════════════════════════ def scan_watchlist_stocks(): """扫描自选股表 (watchlist_stocks),对价格偏离买入区 >20% 的股票自动重评。 偏离公式: max(|price - entry_low|, |price - entry_high|) / entry_low * 100 > 20 通过 technical_analysis.full_analysis() 获取最新支撑/阻力位, 更新 entry_low / entry_high / stop_loss / price / analysis_json。 每轮最多处理 3 只,超过时标记剩余数量待下次扫描。 """ import sqlite3, json from datetime import datetime from technical_analysis import full_analysis from mo_models import is_hk_stock DB = '/home/hmo/web-dashboard/data/mofin.db' db = sqlite3.connect(DB) db.row_factory = sqlite3.Row rows = db.execute( "SELECT * FROM watchlist_stocks WHERE is_active=1" ).fetchall() if not rows: print("[WL-SCAN] 自选股表为空,跳过") db.close() return # ── 筛选偏离 >20% 的股票 ── candidates = [] # (code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json) for r in rows: code = r["code"] name = r["name"] price = r["price"] or 0 entry_low = r["entry_low"] or 0 entry_high = r["entry_high"] or 0 stop_loss = r["stop_loss"] or 0 analysis_json = r["analysis_json"] if entry_low <= 0 or price <= 0: continue dev_low = abs(price - entry_low) dev_high = abs(price - entry_high) deviation = max(dev_low, dev_high) / entry_low * 100 if deviation > 20: candidates.append((code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json)) total_needed = len(candidates) print(f"[WL-SCAN] 自选股共{len(rows)}只,偏离>20%需重评: {total_needed}只") MAX_PER_RUN = 3 to_process = candidates[:MAX_PER_RUN] remaining = max(0, total_needed - MAX_PER_RUN) if remaining > 0: print(f"[WL-SCAN] 本轮限{MAX_PER_RUN}只,剩余{remaining}只待下次扫描") if not to_process: print("[WL-SCAN] 无需重评") db.close() return ok = 0 errors = 0 for code, name, price, old_low, old_high, old_stop, deviation, old_analysis_json in to_process: print(f"[WL-REASSESS] {code} {name}: 偏离{deviation:.1f}%,触发重评") try: ta = full_analysis(code) if not ta or "error" in ta: print(f" [WARN] TA失败: {ta}") errors += 1 continue sr = ta.get("support_resistance", {}) if "error" in sr: print(f" [WARN] 支撑/阻力计算失败: {sr}") errors += 1 continue new_price = ta.get("quote", {}).get("price", price) new_entry_low = round(sr.get("weak_support", old_low), 2) new_entry_high = round(sr.get("weak_resist", old_high), 2) new_stop_loss = round(sr.get("strong_support", old_stop), 2) new_take_profit = round(sr.get("strong_resist", 0), 2) # ── 更新 analysis_json + changelog ── old_analysis = json.loads(old_analysis_json) if old_analysis_json else {} changelog = old_analysis.get("changelog", []) changelog.append({ "action": "auto_reassess_watchlist", "reason": f"价格偏离买入区{deviation:.1f}%", "old_entry_low": old_low, "old_entry_high": old_high, "new_entry_low": new_entry_low, "new_entry_high": new_entry_high, "old_stop_loss": old_stop, "new_stop_loss": new_stop_loss, "take_profit": new_take_profit, "price": new_price, "deviation_pct": round(deviation, 1), "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"), }) new_analysis = { **old_analysis, "take_profit": new_take_profit, "tech_snapshot": { "support_resistance": sr, "candlestick": ta.get("candlestick", {}), "volume": ta.get("volume", {}), "analyzed_at": ta.get("analyzed_at", ""), }, "reassessed_at": datetime.now().strftime("%Y-%m-%d"), "changelog": changelog, } currency = "HKD" if is_hk_stock(code) else "CNY" db.execute(""" UPDATE watchlist_stocks SET entry_low=?, entry_high=?, stop_loss=?, price=?, currency=?, analysis_json=? WHERE code=? AND is_active=1 """, ( new_entry_low, new_entry_high, new_stop_loss, new_price, currency, json.dumps(new_analysis, ensure_ascii=False), code, )) db.commit() print(f" [OK] {code} {name}: 买入区{old_low}-{old_high} -> {new_entry_low}-{new_entry_high}, " f"止损{new_stop_loss}, 止盈{new_take_profit}") ok += 1 except Exception as e: import traceback print(f" [ERROR] {code}: {e}", file=sys.stderr) traceback.print_exc(file=sys.stderr) errors += 1 db.close() remaining_msg = f" (剩余{remaining}只)" if remaining else "" print(f"[WL-SCAN] DONE: {ok}成功 {errors}失败{remaining_msg}") if __name__ == "__main__": main()