chore: deployed pipeline fixes

This commit is contained in:
知微
2026-07-20 17:31:48 +08:00
parent 5d3b8e6fdd
commit c31736a38d
7 changed files with 2059 additions and 2215 deletions
+18 -73
View File
@@ -1,14 +1,9 @@
#!/usr/bin/env python3
"""batch_reassess.py — 批量补全12维(九维矩阵)LLM分析(逐只处理,间隔防限流)
"""batch_reassess.py — 批量补全九维分析(逐只处理,间隔防限流)
用法:
python3 batch_reassess.py # 所有缺分析/过期的 active 策略
python3 batch_reassess.py --type holding # 只处理持仓策略
python3 batch_reassess.py --type watchlist # 只处理自选策略
python3 batch_reassess.py --type holding --today # 持仓每日刷新(今早未评过的强制重评)
python3 batch_reassess.py --code XXXXXX # 单只
用法: python3 batch_reassess.py [--all] [--code XXXXXX]
流程:收集最新数据 → 调LLM(gateway)写12维分析+策略 → 保存到DB
流程:收集最新数据 → 调LLM(gateway)写维分析+策略 → 保存到DB
"""
import sys, json, subprocess, sqlite3, re, time
from datetime import datetime
@@ -16,10 +11,9 @@ from datetime import datetime
DB = "/home/hmo/MoFin/data/mofin.db"
GATEWAY = "http://127.0.0.1:8643/v1/chat/completions"
COOLDOWN_HOURS = 1
STALE_HOURS = 20 # 分析超过20小时视为过期,需要重评
def has_llm_analysis(code):
"""检查是否为LLM生成的12维分析(>500字)"""
"""检查是否为LLM生成的维分析(>500字)"""
conn = sqlite3.connect(DB)
r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
conn.close()
@@ -39,34 +33,6 @@ def in_cooldown(code):
except:
return False
def analysis_stale(code, force_today=False):
"""分析是否过期(>STALE_HOURS 或 force_today 时今早4点前未重评)"""
conn = sqlite3.connect(DB)
r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
conn.close()
if not r or not r[0]:
return True
try:
last = datetime.fromisoformat(r[0])
if force_today:
today4am = datetime.now().replace(hour=4, minute=0, second=0, microsecond=0)
return last < today4am
return (datetime.now() - last).total_seconds() / 3600 > STALE_HOURS
except:
return True
def get_portfolio():
"""从 portfolio_summary 读实时现金/总资产(不再硬编码)"""
try:
conn = sqlite3.connect(DB)
r = conn.execute("SELECT cash, total_assets FROM portfolio_summary WHERE id=1").fetchone()
conn.close()
if r and r[1]:
return int(r[0] or 0), int(r[1])
except Exception:
pass
return 0, 0
def collect_data(code):
"""收集最新数据"""
data = {"code": code}
@@ -89,14 +55,7 @@ def collect_data(code):
conn.close()
# 从腾讯API拉最新价和基本面
# 代码前缀:5位=港股(hk)6/9开头=沪(sh),其他=深(sz)
_c = str(code)
if len(_c) == 5:
prefix = "hk"
elif _c.startswith(("6", "9")):
prefix = "sh"
else:
prefix = "sz"
prefix = "sh" if str(code).startswith(("6","9")) else "sz"
try:
r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10)
parts = r.stdout.decode("gbk", errors="ignore").split("~")
@@ -122,9 +81,8 @@ def collect_data(code):
def build_prompt(data):
"""构建LLM prompt,要求输出完整策略"""
cash, total = get_portfolio() # 实时从 portfolio_summary 读
if not total:
cash, total = 241330, 929727 # 兜底(DB读不到时)
cash = 321271 # 可用现金(从DB读取)
total = 952879 # 总资产
# 拉取资金流数据
_flow_note = "暂无资金流数据"
@@ -313,19 +271,18 @@ def save_result(code, full_text, parsed):
conn.close()
def process_stock(code, force_today=False):
def process_stock(code):
"""处理单只股票"""
print(f"\n{'='*50}")
print(f"处理: {code}")
print(f"{'='*50}")
if in_cooldown(code):
print(f"冷却期内,跳过")
if has_llm_analysis(code):
print(f"已有LLM九维分析,跳过")
return False
# 有分析且未过期 → 跳过(除非 force_today 且今早未评)
if has_llm_analysis(code) and not analysis_stale(code, force_today):
print(f" ⏭ 已有12维分析且未过期,跳过")
if in_cooldown(code):
print(f" ⏭ 冷却期内,跳过")
return False
print(f" 收集数据...", flush=True)
@@ -372,41 +329,29 @@ def process_stock(code, force_today=False):
def main():
codes = []
force_today = "--today" in sys.argv
dtype = None
if "--type" in sys.argv:
idx = sys.argv.index("--type")
dtype = sys.argv[idx + 1] # holding | watchlist | all
if "--code" in sys.argv:
idx = sys.argv.index("--code")
codes = [sys.argv[idx+1]]
else:
# 按类型筛选 active 策略
type_map = {"holding": "持仓策略", "watchlist": "自选策略"}
# 所有自选策略
conn = sqlite3.connect(DB)
if dtype in type_map:
rows = conn.execute(
"SELECT code FROM holding_strategies WHERE status='active' AND decision_type=? ORDER BY code",
(type_map[dtype],)).fetchall()
else:
rows = conn.execute(
"SELECT code FROM holding_strategies WHERE status='active' ORDER BY decision_type, code").fetchall()
rows = conn.execute("SELECT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略' ORDER BY code").fetchall()
conn.close()
codes = [r[0] for r in rows]
print(f"待处理: {len(codes)} (type={dtype or 'all'}, force_today={force_today})")
print(f"待处理: {len(codes)}")
ok = 0
fail = 0
skip = 0
for i, code in enumerate(codes):
if has_llm_analysis(code) and not analysis_stale(code, force_today):
print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有12维分析且未过期")
if has_llm_analysis(code):
print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有LLM分析")
skip += 1
continue
print(f" [{i+1}/{len(codes)}] ", end="", flush=True)
if process_stock(code, force_today):
if process_stock(code):
ok += 1
else:
fail += 1
+1 -3
View File
@@ -17,9 +17,7 @@ DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
UA = "Mozilla/5.0"
def get_conn():
c = sqlite3.connect(str(DB_PATH), timeout=30)
c.execute("PRAGMA busy_timeout=30000")
return c
return sqlite3.connect(str(DB_PATH))
def log_candidate(conn, code, stage, passed, detail):
"""记录过滤日志"""
+203 -203
View File
@@ -1,203 +1,203 @@
#!/usr/bin/env python3
"""market_insight.py — 基于 market.json 数据生成基础洞察 + 潜力挖掘
输出:更新 data/market.json 中的 insights / potential_stocks 字段
策略:
1. 行业热点 vs 持仓匹配 → 相关影响
2. 资金流向异常 → 关注信号
3. 市场情绪 → 每日研判
4. 潜力挖掘 → 强势行业中寻找持仓相关标的
"""
import json
import sys
from datetime import datetime
from pathlib import Path
DATA_DIR = Path(__file__).parent.parent / "data"
# ── 持仓股 → 行业映射(从 stock_profiles 自动提取) ──
def load_holding_industry_map():
"""从 stock_profiles 和 portfolio 提取持仓→行业映射"""
try:
with open(DATA_DIR / "stock_profiles.json", "r", encoding="utf-8") as f:
profiles = json.load(f).get("profiles", [])
# 优先从DB读取持仓(Dad铁律:禁用JSON直读)
from mo_data import read_portfolio
portfolio = read_portfolio()
except FileNotFoundError:
return {}
# 构建 code→name 映射(从 portfolio
code_to_name = {}
for item in portfolio.get("holdings", []):
code_to_name[item.get("code", "")] = item.get("name", "")
# 构建行业→持仓列表
industry_holdings = {}
for p in profiles:
code = p.get("code", "")
name = p.get("name", "")
sector = p.get("sector", "")
if not sector or sector == "待补全":
continue
# 提取一级行业(取斜杠前第一个)
primary = sector.split("/")[0].split("")[0].strip()
if primary:
industry_holdings.setdefault(primary, []).append({
"code": code,
"name": name,
"sector": sector,
})
return industry_holdings
def generate():
# market_path 在DB和fallback两个分支后都会用到,所以提前定义
market_path = DATA_DIR / "market.json"
# 优先从 SQLite 读取市场数据
try:
from mofin_db import get_conn, query_latest_market
conn = get_conn()
market = query_latest_market(conn)
conn.close()
if market and market.get("sectors"):
sectors = market["sectors"]
top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "")
# 字段名适配
for s in sectors:
s["change"] = s.get("change_pct", 0)
for g in top_gainers:
g["change"] = g.get("change_pct", 0)
for l in top_losers:
l["change"] = l.get("change_pct", 0)
else:
raise Exception("no data")
except Exception:
market_path = DATA_DIR / "market.json"
with open(market_path, "r", encoding="utf-8") as f:
market = json.load(f)
sectors = market.get("sectors", [])
top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "")
industry_holdings = load_holding_industry_map()
insights = []
potentials = []
# ── 洞察1:市场情绪总览 ──
mood_cn = {"bullish": "偏强", "neutral": "中性", "bearish": "偏弱", "unknown": "未知"}
insights.append(
f"市场情绪{mood_cn.get(mood, '未知')},上涨占比{up_ratio}%"
)
# ── 洞察2:领涨行业 vs 持仓影响 ──
gainer_insights = []
for g in top_gainers[:3]:
name = g.get("name", "")
change = g.get("change", 0)
# 看持仓中是否有该行业
matched = []
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
matched.extend([h["name"] for h in holdings])
if matched:
gainer_insights.append(
f"{name}+{change}%, 关联持仓{'/'.join(matched[:3])}受益"
)
else:
gainer_insights.append(f"{name}+{change}%, 暂无持仓")
if gainer_insights:
insights.append("领涨板块: " + " | ".join(gainer_insights[:2]))
# ── 洞察3:领跌行业 vs 持仓风险 ──
loser_insights = []
for g in top_losers[:3]:
name = g.get("name", "")
change = g.get("change", 0)
matched = []
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
matched.extend([h["name"] for h in holdings])
if matched:
loser_insights.append(
f"{name}{change}%, {'/'.join(matched[:2])}需关注"
)
else:
loser_insights.append(f"{name}{change}%")
if loser_insights:
insights.append("风险板块: " + " | ".join(loser_insights[:3]))
# ── 洞察4:资金流向异动 ──
big_inflow = [s for s in sectors if (s.get("net_inflow") or 0) > 50]
big_outflow = [s for s in sectors if (s.get("net_inflow") or 0) < -50]
if big_inflow:
top = max(big_inflow, key=lambda s: s["net_inflow"])
insights.append(
f"资金流入最大: {top['name']} {top['net_inflow']}亿"
)
if big_outflow:
top = min(big_outflow, key=lambda s: s["net_inflow"])
insights.append(
f"资金流出最大: {top['name']} {top['net_inflow']}亿"
)
# ── 潜力股挖掘:从强势行业中找持仓或自选相关 ──
for g in top_gainers[:5]:
name = g.get("name", "")
change = g.get("change", 0)
if change < 2:
continue # 只关注涨>2%的
# 找该行业指数有没有关联持仓
lead_stock = g.get("lead_stock", "")
if lead_stock:
potentials.append({
"name": lead_stock,
"reason": f"{name}领涨股, 板块+{change}%",
})
# 看持仓中是否有该行业
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
for h in holdings:
potentials.append({
"name": h["name"],
"reason": f"所在行业{name}{change}%",
})
# 去重(最多5条)
seen = set()
unique_potentials = []
for p in potentials:
key = p["name"]
if key not in seen:
seen.add(key)
unique_potentials.append(p)
if len(unique_potentials) >= 5:
break
potentials = unique_potentials
# ── 写入 market.json ──
market["insights"] = insights
market["potential_stocks"] = potentials
market["insight_timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(market_path, "w", encoding="utf-8") as f:
json.dump(market, f, ensure_ascii=False, indent=2)
print(f"生成{len(insights)}条洞察 + {len(potentials)}条潜力挖掘")
if __name__ == "__main__":
generate()
#!/usr/bin/env python3
"""market_insight.py — 基于 market.json 数据生成基础洞察 + 潜力挖掘
输出:更新 data/market.json 中的 insights / potential_stocks 字段
策略:
1. 行业热点 vs 持仓匹配 → 相关影响
2. 资金流向异常 → 关注信号
3. 市场情绪 → 每日研判
4. 潜力挖掘 → 强势行业中寻找持仓相关标的
"""
import json
import sys
from datetime import datetime
from pathlib import Path
DATA_DIR = Path(__file__).parent.parent / "data"
# ── 持仓股 → 行业映射(从 stock_profiles 自动提取) ──
def load_holding_industry_map():
"""从 stock_profiles 和 portfolio 提取持仓→行业映射"""
try:
with open(DATA_DIR / "stock_profiles.json", "r", encoding="utf-8") as f:
profiles = json.load(f).get("profiles", [])
# 优先从DB读取持仓(Dad铁律:禁用JSON直读)
from mo_data import read_portfolio
portfolio = read_portfolio()
except FileNotFoundError:
return {}
# 构建 code→name 映射(从 portfolio
code_to_name = {}
for item in portfolio.get("holdings", []):
code_to_name[item.get("code", "")] = item.get("name", "")
# 构建行业→持仓列表
industry_holdings = {}
for p in profiles:
code = p.get("code", "")
name = p.get("name", "")
sector = p.get("sector", "")
if not sector or sector == "待补全":
continue
# 提取一级行业(取斜杠前第一个)
primary = sector.split("/")[0].split("")[0].strip()
if primary:
industry_holdings.setdefault(primary, []).append({
"code": code,
"name": name,
"sector": sector,
})
return industry_holdings
def generate():
# market_path 在DB和fallback两个分支后都会用到,所以提前定义
market_path = DATA_DIR / "market.json"
# 优先从 SQLite 读取市场数据
try:
from mofin_db import get_conn, query_latest_market
conn = get_conn()
market = query_latest_market(conn)
conn.close()
if market and market.get("sectors"):
sectors = market["sectors"]
top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "")
# 字段名适配
for s in sectors:
s["change"] = s.get("change_pct", 0)
for g in top_gainers:
g["change"] = g.get("change_pct", 0)
for l in top_losers:
l["change"] = l.get("change_pct", 0)
else:
raise Exception("no data")
except Exception:
market_path = DATA_DIR / "market.json"
with open(market_path, "r", encoding="utf-8") as f:
market = json.load(f)
sectors = market.get("sectors", [])
top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "")
industry_holdings = load_holding_industry_map()
insights = []
potentials = []
# ── 洞察1:市场情绪总览 ──
mood_cn = {"bullish": "偏强", "neutral": "中性", "bearish": "偏弱", "unknown": "未知"}
insights.append(
f"市场情绪{mood_cn.get(mood, '未知')},上涨占比{up_ratio}%"
)
# ── 洞察2:领涨行业 vs 持仓影响 ──
gainer_insights = []
for g in top_gainers[:3]:
name = g.get("name", "")
change = g.get("change", 0)
# 看持仓中是否有该行业
matched = []
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
matched.extend([h["name"] for h in holdings])
if matched:
gainer_insights.append(
f"{name}+{change}%, 关联持仓{'/'.join(matched[:3])}受益"
)
else:
gainer_insights.append(f"{name}+{change}%, 暂无持仓")
if gainer_insights:
insights.append("领涨板块: " + " | ".join(gainer_insights[:2]))
# ── 洞察3:领跌行业 vs 持仓风险 ──
loser_insights = []
for g in top_losers[:3]:
name = g.get("name", "")
change = g.get("change", 0)
matched = []
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
matched.extend([h["name"] for h in holdings])
if matched:
loser_insights.append(
f"{name}{change}%, {'/'.join(matched[:2])}需关注"
)
else:
loser_insights.append(f"{name}{change}%")
if loser_insights:
insights.append("风险板块: " + " | ".join(loser_insights[:3]))
# ── 洞察4:资金流向异动 ──
big_inflow = [s for s in sectors if s.get("net_inflow", 0) > 50]
big_outflow = [s for s in sectors if s.get("net_inflow", 0) < -50]
if big_inflow:
top = max(big_inflow, key=lambda s: s["net_inflow"])
insights.append(
f"资金流入最大: {top['name']} {top['net_inflow']}亿"
)
if big_outflow:
top = min(big_outflow, key=lambda s: s["net_inflow"])
insights.append(
f"资金流出最大: {top['name']} {top['net_inflow']}亿"
)
# ── 潜力股挖掘:从强势行业中找持仓或自选相关 ──
for g in top_gainers[:5]:
name = g.get("name", "")
change = g.get("change", 0)
if change < 2:
continue # 只关注涨>2%的
# 找该行业指数有没有关联持仓
lead_stock = g.get("lead_stock", "")
if lead_stock:
potentials.append({
"name": lead_stock,
"reason": f"{name}领涨股, 板块+{change}%",
})
# 看持仓中是否有该行业
for industry, holdings in industry_holdings.items():
if industry in name or name in industry:
for h in holdings:
potentials.append({
"name": h["name"],
"reason": f"所在行业{name}{change}%",
})
# 去重(最多5条)
seen = set()
unique_potentials = []
for p in potentials:
key = p["name"]
if key not in seen:
seen.add(key)
unique_potentials.append(p)
if len(unique_potentials) >= 5:
break
potentials = unique_potentials
# ── 写入 market.json ──
market["insights"] = insights
market["potential_stocks"] = potentials
market["insight_timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(market_path, "w", encoding="utf-8") as f:
json.dump(market, f, ensure_ascii=False, indent=2)
print(f"生成{len(insights)}条洞察 + {len(potentials)}条潜力挖掘")
if __name__ == "__main__":
generate()
File diff suppressed because it is too large Load Diff
+40 -59
View File
@@ -1,59 +1,40 @@
#!/usr/bin/env python3
"""premarket_full_review.py — 盘前全量重评
执行顺序:
1. regenerate_all() 全量技术参数重评(持仓+自选)
2. batch_reassess.py --type holding --today 持仓12维LLM分析(每日强制刷新)
3. watchlist_auto_exit() 自选退出检查
4. 输出摘要
调度:交易日 08:10(A股09:30开盘)
"""
import sys, os, json
sys.path.insert(0, '/home/hmo/MoFin')
# Step 1: 全量技术参数重评
print("=" * 50)
print("📊 盘前全量重评开始")
print("=" * 50)
from strategy_lifecycle import regenerate_all
result = regenerate_all(stdout=True)
print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
# Step 1.5: 持仓 12 维 LLM 深度分析——后台分离执行(12-40分钟,不能阻塞 cron 的 120s 超时)
print("\n" + "=" * 50)
print("🧠 持仓12维LLM分析(后台分离启动)")
print("=" * 50)
import subprocess as _sp
analysis_result = {"mode": "detached"}
try:
_log = open("/tmp/holdings_12d_daily.log", "a")
_sp.Popen(
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py",
"--type", "holding", "--today"],
stdout=_log, stderr=_log, start_new_session=True)
print(" ✅ 12维分析已后台启动,日志: /tmp/holdings_12d_daily.log(结果落DB,不阻塞盘前流程)")
except Exception as e:
print(f" ⚠️ 12维分析启动失败: {e}")
analysis_result = {"mode": "detached", "error": str(e)[:100]}
# Step 2: 自选退出
print("\n" + "=" * 50)
print("🔍 自选退出检查")
print("=" * 50)
from scripts.watchlist_auto_exit import main as auto_exit
exited = auto_exit(dry_run=False)
# Step 3: 写入摘要供开盘简报引用
summary = {
"premarket_at": __import__('datetime').datetime.now().isoformat(),
"reassess": result,
"llm_analysis_12d": analysis_result,
"auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited],
"total_kept": result.get('total', 0) - len(exited),
}
os.makedirs("/tmp/mofin_premarket", exist_ok=True)
with open("/tmp/mofin_premarket/summary.json", "w") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"\n✅ 盘前重评完毕")
#!/usr/bin/env python3
"""premarket_full_review.py — 盘前全量重评
执行顺序:
1. regenerate_all() 全量技术分析重评(持仓+自选)
2. watchlist_auto_exit() 自选退出检查
3. 输出摘要
调度:交易日 08:10(A股09:30开盘)
"""
import sys, os, json
sys.path.insert(0, '/home/hmo/MoFin')
# Step 1: 全量重评
print("=" * 50)
print("📊 盘前全量重评开始")
print("=" * 50)
from strategy_lifecycle import regenerate_all
result = regenerate_all(stdout=True)
print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
# Step 2: 自选退出
print("\n" + "=" * 50)
print("🔍 自选退出检查")
print("=" * 50)
from scripts.watchlist_auto_exit import main as auto_exit
exited = auto_exit(dry_run=False)
# Step 3: 写入摘要供开盘简报引用
summary = {
"premarket_at": __import__('datetime').datetime.now().isoformat(),
"reassess": result,
"auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited],
"total_kept": result.get('total', 0) - len(exited),
}
os.makedirs("/tmp/mofin_premarket", exist_ok=True)
with open("/tmp/mofin_premarket/summary.json", "w") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"\n✅ 盘前重评完毕")
File diff suppressed because it is too large Load Diff
+130 -136
View File
@@ -1,136 +1,130 @@
#!/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=60)
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()
#!/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))
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})"
conn.execute("""
INSERT 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))
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
promoted += 1
print(f"{code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True)
# 触发全量重评(生成完整9维策略)
try:
import subprocess as _sp
r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code],
capture_output=True, text=True, timeout=60)
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()