Files
MoFin/scripts/per_stock_reassess.py
T

723 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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={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 中的自选股 → 从 DB watchlist_stocks 构建entry
import sqlite3
_db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
_db.row_factory = sqlite3.Row
_wl = _db.execute("SELECT * FROM watchlist_stocks WHERE code=? AND is_active=1", (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
# 打印参数调试
print(f" DEBUG: code={code} name={entry.get('name','')} price={price} cost={entry.get('cost')} shares={entry.get('shares')} action={entry.get('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}做一个完整的9维矩阵分析。
⚠️ 重要:9个维度必须交叉对比,找出矛盾/共振点,给出综合判断。
当前数据(实时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]}
资金流={_flow_note}
消息面={_news_note}
格式:
【交叉分析】哪些维度矛盾/共振,关键信号
① 大盘×基本面 ② 大盘×消息面 ③ 大盘×技术面 ④ 大盘×资金流
⑤ 行业×基本面 ⑥ 行业×消息面 ⑦ 行业×技术面
⑧ 个股×基本面 ⑨ 个股×消息面
最后必须输出:
【综合结论】(买入/关注/观望/卖出)
【操作建议】
【建议止损】
【建议止盈】"""
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" ✅ LLM九维分析完成({len(_full_analysis_text)}字)", flush=True)
except Exception as _e:
print(f" LLM调用失败: {_e},使用代码降级", file=__import__('sys').stderr)
_full_analysis_text = _build_full_analysis(code, entry, result)
# 保存到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" ✅ 完整九维分析已保存({len(_full_analysis_text)}字)")
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} 已写入")
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()