Files
MoFin/deploy/profile-scripts/market_insight.py
T
hmo 17305bed0b fix(pipelines): clear today's real cron errors + kill monitoring false alarms
Real errors fixed (all verified by manual run):
- price_monitor.py: shares None -> TypeError at L584 (now completes 3m7s,
  full 39-stock reassess + zone triggers + Dad push)
- market_insight.py: net_inflow None -> TypeError at L142 (now 0.3s, 5 insights)
- promote_candidates.py: add busy_timeout=30s (DB lock under concurrent writes)
- premarket_full_review.py: 12-dim analysis now detached background launch
  (was doomed by cron 120s script timeout no matter what)

Systemic:
- HERMES_CRON_SCRIPT_TIMEOUT=600 drop-in for both gateway services
  (fixes mofin_health SIGTERM, market_watch timeout, memory_guardian timeout)
- sync_profile_scripts.sh: re-hardlink deploy->profile scripts after every
  deploy (scp replaces files = new inode = broken hardlink = cron silently
  runs stale code; this caused promote to keep failing after my first fix)

Monitoring false-alarm fixes (the '花瓶' problem):
- mofin_health.py: legacy JSONs that migrated to DB (multi_tf_cache/
  macro_context/market/live_prices/price_history/macro_risk_state) no longer
  warn 'no readers'; marked as migrated
- NEW db_freshness section: real pipeline health from DB tables
  (mtf_cache 0.4h / macro_context_log 2h / market_snapshots 2h /
  live_prices 0.4h / price_events.json 0.4h — ALL HEALTHY)
- price_events freshness reads live JSON store (DB table is legacy)
- market.json placeholder created (13+ scripts have fallback paths)

Investigation notes: wiki-self-growth 03:04 key1 429 predates full key6
activation on default gateway; current 8642 verified on key6 and working.
Weekend 'Blocked' jobs verified fixed (vacuum_state_db passes).
2026-07-20 17:30:15 +08:00

204 lines
7.1 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
"""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()