后端(重评管线):
- 新增 llm_client.py 共享客户端: REASSESS_MODEL=deepseek-v4-pro 单点,
gateway预检(fail-fast), 150s超时+1次重试, 永不抛异常
- batch_reassess/per_stock_reassess: curl/urllib -> call_llm,
prompt传入原策略全文+当前参数+最近3条变更, 输出 维持/修改判断+
修改点理由+最终新策略, max_tokens 4096
- mofin_db: 新增 strategy_history 表 + snapshot_strategy_history(),
write_holding_strategy 覆写前自动快照(保留20条/code)
- mofin_db: holding_strategies 补 tag 列迁移 + 写入保留
(tag缺席=保留旧值, 显式传''=允许清除), 修复推荐标签被静默丢弃
- mo_data.read_decisions: SELECT 补 tag
- stale_detector/promote_candidates: 子进程超时 240/60 -> 480s
前端:
- 移除 报告Tab -> mofin_health 全部流程/Cron 表加 最后十次 列
(modal列表->详情), /api/reports 支持 cron+script 多路匹配
(jobs.json name->id 解析 + 文件名/标题子串兜底)
- 移除 决策库Tab
- 盯盘Tab 重构: 全部持仓+自选, sort_group 分组(推荐/持仓/自选),
推荐行琥珀高亮+🔥badge+行内策略, 新增 操作策略 列查看
最近3次完整策略(/api/strategy_history/<code>, 表缺失时降级当前行)
- 提示词Tab: registry.py 数据路径改回 /home/hmo/MoFin/data/prompts
(红线: 数据只在规范数据根), 空态提示初始化命令
137 lines
5.3 KiB
Python
137 lines
5.3 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
|
|
|
|
# 读未提拔候选(按评分降序)
|
|
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
|
|
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
|
|
|
|
# 验证实时价格:无有效价格的候选股不入自选(防假数据污染)
|
|
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:
|
|
print(f" ⏭ {code} {name} 无实时价格,跳过")
|
|
continue
|
|
except Exception as _e:
|
|
print(f" ⏭ {code} {name} 价格获取失败({_e}),跳过")
|
|
continue
|
|
|
|
# 构建策略
|
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
timing_signal = "买入" if score >= 7 else "关注"
|
|
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})"
|
|
|
|
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()
|