From 481acfb18fd815c0ec1e3b65aac7f12ec841368e Mon Sep 17 00:00:00 2001 From: hmo Date: Fri, 24 Jul 2026 11:00:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E4=B8=AD=E9=80=89=E4=BC=98?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=EF=BC=88=E8=80=81=E7=88=B8=EF=BC=9A=E9=98=88?= =?UTF-8?q?=E5=80=BC=E5=A4=AA=E4=BD=8E+=E5=AD=98=E9=87=8F=E6=B8=85?= =?UTF-8?q?=E6=B4=97+=E7=9B=B4=E6=8E=A5=E5=88=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - enqueue_recommend加严: RR>=2.0+仓位必须明确%+买入区有效+不追高(防今早RR1.49/仓位观望/区缺失的垃圾digest) - promote: score>=7(原4=91%通过率)+ST排除+RR>=2.0 - watchlist_auto_exit: 新增死水3连退+RR<1.5退; 退出方式改为直接DELETE(非inactive) - 盯盘可执行门槛1.5→2.0 --- deploy/profile-scripts/promote_candidates.py | 18 ++++++- deploy/profile-scripts/watchlist_auto_exit.py | 47 ++++++++++++++----- mofin_db.py | 27 ++++++++++- server.py | 4 +- 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/deploy/profile-scripts/promote_candidates.py b/deploy/profile-scripts/promote_candidates.py index c99cebcf..cdb17daf 100644 --- a/deploy/profile-scripts/promote_candidates.py +++ b/deploy/profile-scripts/promote_candidates.py @@ -15,12 +15,13 @@ def main(): conn.row_factory = sqlite3.Row # 读未提拔候选(按评分降序) + # 2026-07-24 老爸"优中选优":score>=7 才可入候选评估(原 4 = 91%通过率等于没门槛) rows = conn.execute(""" SELECT c.code, c.name, c.score_final, c.entry_range, c.stop_loss, c.target FROM candidates c WHERE (c.promoted IS NULL OR c.promoted = 0) AND (c.dropped IS NULL OR c.dropped = 0) - AND c.score_final >= 4 + AND c.score_final >= 7 ORDER BY c.score_final DESC """).fetchall() @@ -58,17 +59,30 @@ def main(): continue # 验证实时价格:无有效价格的候选股不入自选(防假数据污染) + _price = 0.0 try: import subprocess, json as _jj _r = subprocess.run(["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/stock_quote.py", code], capture_output=True, text=True, timeout=10) _q = _jj.loads(_r.stdout) - if float(_q.get("price", 0)) <= 0: + _price = float(_q.get("price", 0)) + if _price <= 0: print(f" ⏭ {code} {name} 无实时价格,跳过") continue except Exception as _e: print(f" ⏭ {code} {name} 价格获取失败({_e}),跳过") continue + + # ── 优中选优闸(2026-07-24 老爸):ST排除 + RR>=2.0 ── + if "ST" in (name or "").upper(): + print(f" ⏭ {code} {name} ST股,不入自选") + continue + if el > 0 and eh > el and sl > 0 and tp > 0: + _mid = (el + eh) / 2 + _rr = (tp - _mid) / (_mid - sl) if (_mid - sl) > 0 else 0 + if _rr < 2.0: + print(f" ⏭ {code} {name} RR={_rr:.2f}<2.0,不入自选") + continue # 构建策略 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/deploy/profile-scripts/watchlist_auto_exit.py b/deploy/profile-scripts/watchlist_auto_exit.py index cd095238..fd281f25 100644 --- a/deploy/profile-scripts/watchlist_auto_exit.py +++ b/deploy/profile-scripts/watchlist_auto_exit.py @@ -38,14 +38,14 @@ def main(dry_run=False): for code, name, signal, el, eh, price, reassessed_at, pos_advice, fa in rows: signal_str = str(signal or "") rank = get_signal_rank(signal_str) - + # 退出条件判断 reasons = [] - + # 条件1: 信号=卖出 if "卖出" in signal_str: reasons.append(f"信号={signal_str}") - + # 条件2: 信号=观望/信号不充分 且 价格远离买入区 if "观望" in signal_str or "信号不充分" in signal_str: if price and el and eh and el > 0 and eh > 0: @@ -53,31 +53,52 @@ def main(dry_run=False): reasons.append(f"价{price}超买区上沿+{((price/eh)-1)*100:.0f}%") elif el > 0 and price < el * 0.85: # 低于买入区下沿15% reasons.append(f"价{price}低于买区下沿{(1-price/el)*100:.0f}%") - + # 条件3: 已清仓/零仓位且信号差 pos_str = str(pos_advice or "") if rank <= 1 and ("0%" in pos_str or "清仓" in pos_str or "不参与" in pos_str): reasons.append(f"仓位建议={pos_str}") - - # 如果有退出理由,执行退出 + + # ── 优中选优新增(2026-07-24 老爸)── + # 条件4: 死水闸——最近3次重评信号均为 观望/信号不充分/弱势持有 → 退出 + _weak = ("观望", "信号不充分", "弱势持有") + try: + _hist = conn.execute( + "SELECT timing_signal FROM strategy_history WHERE code=? " + "ORDER BY snapshotted_at DESC LIMIT 3", (code,)).fetchall() + if len(_hist) >= 3 and all((h[0] or "") in _weak for h in _hist): + reasons.append(f"死水3连({','.join((h[0] or '?') for h in _hist)})") + except Exception: + pass + + # 条件5: RR闸——RR<1.5 的平庸标的直接出(老爸:1.5边缘也是阿猫阿狗) + try: + _rr = conn.execute( + "SELECT rr_ratio FROM holding_strategies WHERE code=? AND status='active'", + (code,)).fetchone() + if _rr and _rr[0] is not None and 0 < _rr[0] < 1.5: + reasons.append(f"RR={_rr[0]}<1.5") + except Exception: + pass + + # 如果有退出理由,执行退出(2026-07-24 老爸:直接删,不降级) if reasons: reason_text = "; ".join(reasons) exited.append((code, name, signal_str, reason_text)) - + if not dry_run: - # 记录退出日志 + # 记录退出日志(审计留痕) conn.execute( "INSERT INTO watchlist_log (code, name, event, reason, old_signal, new_signal, price) " "VALUES (?,?,?,?,?,?,?)", - (code, name or "", "exit", reason_text, signal_str, "已退出", price) + (code, name or "", "exit", reason_text, signal_str, "已删除", price) ) - # 标记为inactive(软删除,保留历史) + # 直接删除(2026-07-24 老爸决策:不是降级回候选,不是inactive标记) conn.execute( - "UPDATE holding_strategies SET status='inactive', updated_at=datetime('now','localtime') " - "WHERE code=? AND status='active' AND decision_type='自选策略'", + "DELETE FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'", (code,) ) - print(f" 🔴 退出: {code} {name or ''} | {reason_text}") + print(f" 🔴 删除: {code} {name or ''} | {reason_text}") else: kept.append(code) diff --git a/mofin_db.py b/mofin_db.py index 201c4aa6..56ddc30d 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -1231,9 +1231,14 @@ def sync_recommend_tag(conn, code: str, timing_signal: str): def enqueue_recommend(conn, code: str): """新推荐入摘要队列(防逐只轰炸)。batch_reassess 跑完后 flush_rec_digest 统一发一条。 - 校验:必须 tag=current_recommend 且信号为动作级,否则拒绝入队。""" + 校验(2026-07-24 老爸"阿猫阿狗"事件后加严): + 1. tag=current_recommend 且信号为动作级 + 2. RR(中值)>=2.0(1.5边缘的平庸推荐一律拦下) + 3. position_advice 必须含明确仓位%("减仓或观望/不新建仓"不算推荐) + 4. 买入区必须有效(区—~—/0~0 不入) + 5. 买入信号时现价不得在区上沿 5% 以上(追高不买)""" try: - import json as _j + import json as _j, re as _re from datetime import datetime as _dt row = conn.execute( "SELECT name, timing_signal, tag, entry_low, entry_high, stop_loss, take_profit, " @@ -1245,6 +1250,24 @@ def enqueue_recommend(conn, code: str): if tag != 'current_recommend' or sig not in ("买入", "可买入", "可加仓", "卖出", "止盈"): print(f" [REC] {code} 非有效推荐(tag={tag},sig={sig}),不入队", flush=True) return False + # ── 买入类质量闸(卖出/止盈不受 RR/仓位限制——那是风控动作)── + if sig in ("买入", "可买入", "可加仓"): + if (rr or 0) < 2.0: + print(f" [REC] {code} RR={rr}<2.0 平庸推荐,不入队", flush=True) + return False + if not _re.search(r'\d+(?:\.\d+)?\s*%', pos or ''): + print(f" [REC] {code} 仓位非明确%({pos}),不入队", flush=True) + return False + if not (el and eh and el > 0 and eh > el): + print(f" [REC] {code} 买入区缺失/无效({el}~{eh}),不入队", flush=True) + return False + try: + _lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone() + if _lp and _lp[0] and _lp[0] > eh * 1.05: + print(f" [REC] {code} 现价{_lp[0]}超区上沿{eh}5%,追高不入队", flush=True) + return False + except Exception: + pass # 提取【最终新策略】段作为推荐依据摘要 fa_text = fa or "" strat = "" diff --git a/server.py b/server.py index dea4568c..1b130852 100644 --- a/server.py +++ b/server.py @@ -250,8 +250,8 @@ def get_watch(): sig_now = d.get('timing_signal') or '' if sig_now in _WEAK_SIGNALS: d['rec_exec'] = False # 弱信号永远排队 - elif rr >= 1.5 and _cum + pct <= _budget_pct + 1e-9: - d['rec_exec'] = True # 可执行 + elif rr >= 2.0 and _cum + pct <= _budget_pct + 1e-9: + d['rec_exec'] = True # 可执行(2026-07-24 老爸:门槛1.5→2.0,边缘推荐不算优) _cum += pct else: d['rec_exec'] = False # 排队(现金不足或RR不达标)