Files
MoFin/deploy/profile-scripts/promote_candidates.py
T
hmo 5de6bda868 fix: promote技术位锚定+accumulation入门4→5+watchlist_auto_exit排cron+归档孤儿market_scanner
- promote参数不再信扫描器: ta.full_analysis重定 区(弱撑~弱压)/损(强撑x0.985)/盈(强压)
- accumulation_scanner入门闸 score>=4→5(减少陪跑噪音)
- watchlist_auto_exit排入系统crontab(每日08:50,此前根本没调度)
- market_scanner.py是孤儿(跑的是market_screener),归档
2026-07-24 14:32:40 +08:00

193 lines
8.7 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
"""promote_candidates.py — 自动提拔候选股入自选
从 candidates 表读未提拔的候选,评估后自动加入 holding_strategies。
"""
import sys, json, sqlite3
from pathlib import Path
from datetime import datetime
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
def main():
conn = sqlite3.connect(str(DB_PATH), timeout=30)
conn.execute("PRAGMA busy_timeout=30000")
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 >= 7
ORDER BY c.score_final DESC
""").fetchall()
if not rows:
print("[PROMOTE] 无待提拔候选")
conn.close()
return
promoted = 0
for r in rows:
code = str(r[0])
name = r[1] or code
score = r[2] or 0
entry_range = r[3] or ""
sl = r[4] or 0
tp = r[5] or 0
# 解析 entry_range
el, eh = 0, 0
if "~" in entry_range:
parts = entry_range.split("~")
try:
el = float(parts[0])
eh = float(parts[1])
except: pass
# 查是否已在 holding_strategies
exists = conn.execute(
"SELECT id FROM holding_strategies WHERE code=? AND status='active'",
(code,)
).fetchone()
if exists:
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
print(f" ⏭ {code} {name} 已在自选中,标记promoted")
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)
_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
# 技术位锚定:不信扫描器拍的 entry/sl/tp,用 ta.full_analysis 的确定性技术位重定
try:
import sys as _s
if '/home/hmo/MoFin/deploy/profile-scripts' not in _s.path:
_s.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts')
import technical_analysis as _ta
_ta_r = _ta.full_analysis(code)
_sr = (_ta_r or {}).get("support_resistance", {}) or {}
_ws, _ss = _sr.get("weak_support"), _sr.get("strong_support")
_wr, _sr2 = _sr.get("weak_resist"), _sr.get("strong_resist")
if _ws and _wr and _price > 0:
el = round(_ws * 0.995, 2) # 区下沿贴弱撑
eh = round(min(_wr, _price * 1.05), 2) # 区上沿取弱压(且不超现价5%
sl = round((_ss or _ws) * 0.985, 2) # 止损=强撑下1.5%(无强撑用弱撑)
tp = round(_sr2 or _wr * 1.15, 2) # 止盈=强压(无强压则弱压+15%
except Exception as _te:
print(f" ⚠️ {code} 技术位锚定失败({_te}),用扫描器参数", flush=True)
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
else:
print(f" ⏭ {code} {name} 锚定后参数无效(区{el}~{eh}{sl}{tp}),跳过")
continue
# 构建策略(2026-07-24 老爸:提拔不直接给"买入"——先入观察,12维确认后再升)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
timing_signal = "关注"
price_est = (el + eh) / 2 if el > 0 and eh > 0 else 0
reason_text = []
if el > 0: reason_text.append(f"买{el}~{eh}")
if sl > 0: reason_text.append(f"损{sl}")
if tp > 0: reason_text.append(f"盈{tp}")
if sl > 0 and tp > 0 and price_est > 0:
rr = (tp - price_est) / (price_est - sl) if (price_est - sl) > 0 else 0
reason_text.append(f"RR{rr:.1f}")
reason_text.append(f"评分{score}")
action = " | ".join(reason_text) if reason_text else f"市场扫描发现(评分{score})"
# ── 容量闸:自选上限60只,超出时删综合分最弱的(RR低优先)──
MAX_WATCH = 60
wl_count = conn.execute(
"SELECT COUNT(*) FROM holding_strategies WHERE status='active' AND decision_type='自选策略'").fetchone()[0]
if wl_count >= MAX_WATCH:
weakest = conn.execute("""
SELECT code, name, COALESCE(rr_ratio,0) as rr FROM holding_strategies
WHERE status='active' AND decision_type='自选策略'
ORDER BY COALESCE(rr_ratio,0) ASC, updated_at ASC LIMIT 1""").fetchone()
if weakest and (weakest[2] or 0) < 2.0:
conn.execute(
"INSERT INTO watchlist_log (code, name, event, reason, old_signal, new_signal, price) "
"VALUES (?,?,?,?,?,?,?)",
(weakest[0], weakest[1] or "", "exit", f"容量{MAX_WATCH}淘汰为新标的{code}腾位", "", "已删除", 0))
conn.execute(
"DELETE FROM holding_strategies WHERE code=? AND status='active' AND decision_type='自选策略'",
(weakest[0],))
print(f" 🔴 容量淘汰: {weakest[0]} {weakest[1]} (RR={weakest[2]})", flush=True)
else:
print(f" ⏭ 自选已满{MAX_WATCH}且现有标的均RR>=2.0{code}暂缓提拔", flush=True)
continue
cur = conn.execute("""
INSERT OR IGNORE INTO holding_strategies
(code, name, price, entry_low, entry_high, stop_loss, take_profit,
timing_signal, action, decision_type, strategy_type, status,
rr_ratio, stock_category, created_at, updated_at,
sector_context, quality_check)
VALUES (?,?,?,?,?,?,?,?,?,'自选策略','scan',
'active',0,'关注',?,?,'', 'pending')
""", (code, name, 0, el, eh, sl, tp, timing_signal, action, now, now))
newly_added = cur.rowcount > 0
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
if newly_added:
promoted += 1
print(f" ✅ {code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True)
else:
print(f" ⏭ {code} {name} 已在自选策略中,标记promoted", flush=True)
# 触发全量重评(生成完整9维策略)——仅新插入的股票需要
if newly_added:
try:
import subprocess as _sp
r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code],
capture_output=True, text=True, timeout=480)
if r.returncode == 0:
print(f" 重评完成", flush=True)
else:
print(f" 重评失败: {r.stderr.strip()[:100]}", flush=True)
except Exception as e:
print(f" 重评异常: {e}", flush=True)
conn.commit()
print(f"\n[PROMOTE] 本次提拔{promoted}只", flush=True)
# 推XMPP
if promoted > 0:
try:
import urllib.request
msg = f"📈 自动提拔{promoted}只候选入自选"
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=5)
except Exception:
pass
conn.close()
if __name__ == "__main__":
main()