feat: 优中选优机制(老爸:阈值太低+存量清洗+直接删)

- 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
This commit is contained in:
hmo
2026-07-24 11:00:41 +08:00
parent 1cdef8da7e
commit 481acfb18f
4 changed files with 77 additions and 19 deletions
+16 -2
View File
@@ -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")
+34 -13
View File
@@ -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)
+25 -2
View File
@@ -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.01.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 = ""
+2 -2
View File
@@ -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不达标)