- promote参数不再信扫描器: ta.full_analysis重定 区(弱撑~弱压)/损(强撑x0.985)/盈(强压) - accumulation_scanner入门闸 score>=4→5(减少陪跑噪音) - watchlist_auto_exit排入系统crontab(每日08:50,此前根本没调度) - market_scanner.py是孤儿(跑的是market_screener),归档
193 lines
8.7 KiB
Python
193 lines
8.7 KiB
Python
#!/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()
|