chore: capture deployed state

This commit is contained in:
知微
2026-07-20 17:30:43 +08:00
parent 4dcfee8140
commit 5d3b8e6fdd
14 changed files with 33422 additions and 2882 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+29640
View File
File diff suppressed because it is too large Load Diff
+304 -154
View File
@@ -35,10 +35,10 @@
{
"date": "2026-07-20",
"high": 15.75,
"low": 14.1,
"close": 14.21,
"volume": 37396292.0,
"amount": 55424.0
"low": 13.05,
"close": 13.41,
"volume": 41864386.0,
"amount": 56681.0
}
],
"688775": [
@@ -77,10 +77,10 @@
{
"date": "2026-07-20",
"high": 149.0,
"low": 135.76,
"close": 138.34,
"volume": 6157193.0,
"amount": 86656.0
"low": 129.5,
"close": 136.32,
"volume": 4713131.0,
"amount": 63136.0
}
],
"688475": [
@@ -118,11 +118,11 @@
},
{
"date": "2026-07-20",
"high": 27.74,
"high": 27.78,
"low": 26.4,
"close": 26.64,
"volume": 5999240.0,
"amount": 16223.0
"close": 27.73,
"volume": 6881480.0,
"amount": 18584.0
}
],
"688778": [
@@ -161,10 +161,10 @@
{
"date": "2026-07-20",
"high": 46.45,
"low": 44.09,
"close": 44.14,
"volume": 7684619.0,
"amount": 34599.0
"low": 42.05,
"close": 43.0,
"volume": 9254116.0,
"amount": 40066.0
}
],
"688501": [
@@ -203,10 +203,10 @@
{
"date": "2026-07-20",
"high": 17.8,
"low": 16.64,
"close": 16.79,
"volume": 1904231.0,
"amount": 3272.0
"low": 15.42,
"close": 15.91,
"volume": 2834080.0,
"amount": 4591.0
}
],
"688510": [
@@ -245,10 +245,10 @@
{
"date": "2026-07-20",
"high": 22.46,
"low": 20.63,
"close": 21.0,
"volume": 5596808.0,
"amount": 12011.0
"low": 20.25,
"close": 20.76,
"volume": 6770593.0,
"amount": 14184.0
}
],
"688513": [
@@ -287,10 +287,10 @@
{
"date": "2026-07-20",
"high": 59.9,
"low": 54.5,
"close": 54.63,
"volume": 4293163.0,
"amount": 24190.0
"low": 53.67,
"close": 57.6,
"volume": 3704960.0,
"amount": 20838.0
}
],
"688518": [
@@ -329,10 +329,10 @@
{
"date": "2026-07-20",
"high": 19.78,
"low": 18.31,
"close": 18.46,
"volume": 18674329.0,
"amount": 35390.0
"low": 16.81,
"close": 17.24,
"volume": 21448924.0,
"amount": 38124.0
}
],
"00700": [
@@ -372,9 +372,9 @@
"date": "2026-07-20",
"high": 488.8,
"low": 458.0,
"close": 461.6,
"volume": 36237657.0,
"amount": 16928705332.905
"close": 477.8,
"volume": 30822839.0,
"amount": 14678311361.3
}
],
"688533": [
@@ -413,10 +413,10 @@
{
"date": "2026-07-20",
"high": 21.74,
"low": 20.4,
"close": 20.59,
"volume": 1359591.0,
"amount": 2855.0
"low": 19.96,
"close": 20.22,
"volume": 992552.0,
"amount": 2026.0
}
],
"01088": [
@@ -454,11 +454,11 @@
},
{
"date": "2026-07-20",
"high": 42.5,
"high": 43.66,
"low": 41.54,
"close": 42.4,
"volume": 13003969.0,
"amount": 547347699.16
"close": 43.6,
"volume": 11166601.0,
"amount": 483013474.62
}
],
"688566": [
@@ -497,10 +497,10 @@
{
"date": "2026-07-20",
"high": 27.11,
"low": 25.0,
"close": 25.0,
"volume": 3344706.0,
"amount": 8599.0
"low": 24.81,
"close": 25.25,
"volume": 2923287.0,
"amount": 7436.0
}
],
"01211": [
@@ -538,11 +538,11 @@
},
{
"date": "2026-07-20",
"high": 91.45,
"high": 91.5,
"low": 87.25,
"close": 88.7,
"volume": 26427225.0,
"amount": 2340941950.5
"close": 90.15,
"volume": 18345094.0,
"amount": 1653609373.75
}
],
"688639": [
@@ -581,10 +581,10 @@
{
"date": "2026-07-20",
"high": 17.56,
"low": 16.1,
"close": 16.14,
"volume": 10030710.0,
"amount": 16760.0
"low": 15.56,
"close": 15.99,
"volume": 9039229.0,
"amount": 14474.0
}
],
"688582": [
@@ -623,10 +623,10 @@
{
"date": "2026-07-20",
"high": 43.8,
"low": 38.5,
"close": 38.61,
"volume": 10970620.0,
"amount": 44852.0
"low": 36.01,
"close": 36.95,
"volume": 9774815.0,
"amount": 37047.0
}
],
"01478": [
@@ -666,9 +666,9 @@
"date": "2026-07-20",
"high": 7.02,
"low": 6.52,
"close": 6.67,
"volume": 7833000.0,
"amount": 52372235.0
"close": 6.73,
"volume": 4247000.0,
"amount": 28512618.0
}
],
"688755": [
@@ -707,10 +707,10 @@
{
"date": "2026-07-20",
"high": 25.89,
"low": 23.8,
"close": 24.01,
"volume": 1862009.0,
"amount": 4574.0
"low": 22.97,
"close": 23.23,
"volume": 1436355.0,
"amount": 3390.0
}
],
"02202": [
@@ -750,9 +750,9 @@
"date": "2026-07-20",
"high": 2.55,
"low": 2.34,
"close": 2.48,
"volume": 40325958.0,
"amount": 98037795.66
"close": 2.44,
"volume": 13295500.0,
"amount": 32619014.9
}
],
"688692": [
@@ -791,10 +791,10 @@
{
"date": "2026-07-20",
"high": 231.68,
"low": 215.0,
"close": 215.4,
"volume": 1706538.0,
"amount": 38102.0
"low": 209.0,
"close": 216.1,
"volume": 1795104.0,
"amount": 38484.0
}
],
"300750": [
@@ -832,11 +832,11 @@
},
{
"date": "2026-07-20",
"high": 375.24,
"low": 360.0,
"close": 360.0,
"volume": 512453.0,
"amount": 1884671.0
"high": 377.6,
"low": 353.8,
"close": 376.43,
"volume": 447908.0,
"amount": 1663278.0
}
],
"688696": [
@@ -875,10 +875,10 @@
{
"date": "2026-07-20",
"high": 67.39,
"low": 62.37,
"close": 62.9,
"volume": 1185875.0,
"amount": 7647.0
"low": 61.5,
"close": 62.72,
"volume": 1078476.0,
"amount": 6756.0
}
],
"518880": [
@@ -916,11 +916,11 @@
},
{
"date": "2026-07-20",
"high": 8.326,
"high": 8.362,
"low": 8.256,
"close": 8.311,
"volume": 2844769.0,
"amount": 235805.0
"close": 8.318,
"volume": 2007208.0,
"amount": 167139.0
}
],
"688708": [
@@ -959,10 +959,10 @@
{
"date": "2026-07-20",
"high": 46.18,
"low": 43.51,
"close": 44.09,
"volume": 1442625.0,
"amount": 6448.0
"low": 42.31,
"close": 44.91,
"volume": 1781103.0,
"amount": 7823.0
}
],
"688779": [
@@ -1000,11 +1000,11 @@
},
{
"date": "2026-07-20",
"high": 7.03,
"high": 7.04,
"low": 6.67,
"close": 6.69,
"volume": 26278826.0,
"amount": 17957.0
"close": 6.9,
"volume": 37370857.0,
"amount": 25733.0
}
],
"300035": [
@@ -1043,10 +1043,10 @@
{
"date": "2026-07-20",
"high": 12.68,
"low": 12.05,
"close": 12.08,
"volume": 158047.0,
"amount": 19469.0
"low": 11.85,
"close": 12.09,
"volume": 186464.0,
"amount": 22594.0
}
],
"688789": [
@@ -1084,11 +1084,11 @@
},
{
"date": "2026-07-20",
"high": 47.73,
"high": 47.88,
"low": 45.19,
"close": 46.1,
"volume": 2179608.0,
"amount": 10118.0
"close": 47.71,
"volume": 1814330.0,
"amount": 8528.0
}
],
"600563": [
@@ -1146,9 +1146,9 @@
"date": "2026-07-20",
"high": 1072.0,
"low": 950.55,
"close": 979.46,
"volume": 561861.0,
"amount": 5648563.0
"close": 1004.0,
"volume": 453665.0,
"amount": 4609438.0
}
],
"601899": [
@@ -1186,11 +1186,11 @@
},
{
"date": "2026-07-20",
"high": 28.82,
"high": 29.45,
"low": 27.92,
"close": 28.36,
"volume": 2904169.0,
"amount": 824106.0
"close": 29.35,
"volume": 2757683.0,
"amount": 799203.0
}
],
"688981": [
@@ -1229,10 +1229,10 @@
{
"date": "2026-07-20",
"high": 155.51,
"low": 139.02,
"close": 139.88,
"volume": 102952043.0,
"amount": 1517166.0
"low": 138.11,
"close": 144.0,
"volume": 93942500.0,
"amount": 1346494.0
}
],
"688372": [
@@ -1271,10 +1271,10 @@
{
"date": "2026-07-20",
"high": 139.2,
"low": 120.66,
"close": 120.73,
"volume": 11689864.0,
"amount": 151702.0
"low": 115.0,
"close": 120.5,
"volume": 13508171.0,
"amount": 166596.0
}
],
"688411": [
@@ -1313,10 +1313,10 @@
{
"date": "2026-07-20",
"high": 193.48,
"low": 179.03,
"close": 181.68,
"volume": 4238973.0,
"amount": 78494.0
"low": 175.6,
"close": 181.35,
"volume": 4399767.0,
"amount": 80465.0
}
],
"688271": [
@@ -1354,11 +1354,11 @@
},
{
"date": "2026-07-20",
"high": 113.43,
"low": 109.86,
"close": 110.37,
"volume": 8809320.0,
"amount": 97913.0
"high": 119.85,
"low": 107.89,
"close": 118.0,
"volume": 13911975.0,
"amount": 158683.0
}
],
"688439": [
@@ -1397,10 +1397,10 @@
{
"date": "2026-07-20",
"high": 42.38,
"low": 39.58,
"close": 39.58,
"volume": 3189314.0,
"amount": 13157.0
"low": 37.05,
"close": 37.89,
"volume": 3734574.0,
"amount": 14394.0
}
],
"688588": [
@@ -1439,10 +1439,10 @@
{
"date": "2026-07-20",
"high": 9.19,
"low": 8.55,
"close": 8.6,
"volume": 4173682.0,
"amount": 3684.0
"low": 8.46,
"close": 8.63,
"volume": 4281284.0,
"amount": 3721.0
}
],
"688621": [
@@ -1481,10 +1481,10 @@
{
"date": "2026-07-20",
"high": 61.75,
"low": 56.73,
"close": 57.58,
"volume": 6154202.0,
"amount": 36271.0
"low": 54.72,
"close": 56.88,
"volume": 4638705.0,
"amount": 26217.0
}
],
"000850": [
@@ -1515,10 +1515,10 @@
{
"date": "2026-07-20",
"high": 4.24,
"low": 3.98,
"close": 4.08,
"volume": 159057.0,
"amount": 6535.0
"low": 3.94,
"close": 3.98,
"volume": 110497.0,
"amount": 4439.0
}
],
"301308": [
@@ -1617,10 +1617,10 @@
{
"date": "2026-07-20",
"high": 10.58,
"low": 10.3,
"close": 10.3,
"volume": 245805.0,
"amount": 25674.0
"low": 10.15,
"close": 10.35,
"volume": 193759.0,
"amount": 19951.0
}
],
"688087": [
@@ -1642,11 +1642,11 @@
},
{
"date": "2026-07-20",
"high": 38.23,
"high": 38.97,
"low": 35.99,
"close": 36.18,
"volume": 2780918.0,
"amount": 10256.0
"close": 38.57,
"volume": 2483595.0,
"amount": 9341.0
}
],
"688466": [
@@ -1669,10 +1669,10 @@
{
"date": "2026-07-20",
"high": 13.19,
"low": 12.44,
"close": 12.6,
"volume": 1788822.0,
"amount": 2286.0
"low": 11.94,
"close": 12.28,
"volume": 1944699.0,
"amount": 2412.0
}
],
"600111": [
@@ -1694,5 +1694,155 @@
"volume": 35790245.0,
"amount": 1105458.0
}
],
"688425": [
{
"date": "2026-07-20",
"high": 4.15,
"low": 4.08,
"close": 4.14,
"volume": 1496345.0,
"amount": 618.0
}
],
"688608": [
{
"date": "2026-07-20",
"high": 115.2,
"low": 110.0,
"close": 114.41,
"volume": 1143940.0,
"amount": 12834.0
}
],
"688800": [
{
"date": "2026-07-20",
"high": 65.11,
"low": 62.65,
"close": 63.51,
"volume": 1447347.0,
"amount": 9241.0
}
],
"688758": [
{
"date": "2026-07-20",
"high": 25.26,
"low": 22.95,
"close": 24.79,
"volume": 3826638.0,
"amount": 9338.0
}
],
"688289": [
{
"date": "2026-07-20",
"high": 15.64,
"low": 15.35,
"close": 15.52,
"volume": 755498.0,
"amount": 1172.0
}
],
"688726": [
{
"date": "2026-07-20",
"high": 39.6,
"low": 38.03,
"close": 38.48,
"volume": 1020332.0,
"amount": 3950.0
}
],
"688576": [
{
"date": "2026-07-20",
"high": 39.29,
"low": 38.18,
"close": 38.67,
"volume": 61912.0,
"amount": 239.0
}
],
"688122": [
{
"date": "2026-07-20",
"high": 49.28,
"low": 48.3,
"close": 48.95,
"volume": 1345698.0,
"amount": 6582.0
}
],
"605090": [
{
"date": "2026-07-20",
"high": 39.6,
"low": 37.7,
"close": 38.19,
"volume": 87773.0,
"amount": 33970.0
}
],
"603766": [
{
"date": "2026-07-20",
"high": 13.15,
"low": 12.91,
"close": 13.01,
"volume": 26823.0,
"amount": 3485.0
}
],
"688550": [
{
"date": "2026-07-20",
"high": 44.8,
"low": 43.0,
"close": 44.58,
"volume": 977623.0,
"amount": 4270.0
}
],
"688281": [
{
"date": "2026-07-20",
"high": 44.4,
"low": 43.0,
"close": 43.01,
"volume": 473467.0,
"amount": 2067.0
}
],
"688617": [
{
"date": "2026-07-20",
"high": 224.9,
"low": 201.31,
"close": 220.22,
"volume": 2634862.0,
"amount": 55058.0
}
],
"688606": [
{
"date": "2026-07-20",
"high": 45.44,
"low": 43.62,
"close": 44.26,
"volume": 544130.0,
"amount": 2421.0
}
],
"688314": [
{
"date": "2026-07-20",
"high": 35.91,
"low": 30.24,
"close": 31.14,
"volume": 2832553.0,
"amount": 9286.0
}
]
}
File diff suppressed because it is too large Load Diff
+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", 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()
#!/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()
+205 -205
View File
@@ -1,205 +1,205 @@
#!/usr/bin/env python3
"""market_scanner.py — 全市场异动扫描(替代小果扫描线)
每15分钟扫描:
1. 板块轮动检测(从已采集的 sector_snapshots 读)
2. 热门板块领涨股扫描(从腾讯API批量拉)
3. 资金流向异常检测(从已有 capital_flow_cache 读)
4. 输出候选股到 candidates 表
不依赖小果LLM,纯数据驱动。
"""
import sys, json, sqlite3, urllib.request, re, time
from pathlib import Path
from datetime import datetime
# XMPP 日志 hook
try:
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from xmpp_logger import log_xmpp
except ImportError:
def log_xmpp(*a, **kw): pass
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json")
UA = "Mozilla/5.0"
def get_conn():
return sqlite3.connect(str(DB_PATH))
def fetch_qq_batch(symbols):
"""腾讯批量行情"""
if not symbols:
return {}
url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
proxy = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy)
with opener.open(req, timeout=15) as r:
text = r.read().decode("gbk")
results = {}
for line in text.strip().split("\n"):
if "~" not in line:
continue
parts = line.split("~")
if len(parts) < 40:
continue
m = re.search(r'_(\w+)=', parts[0])
market = m.group(1) if m else ""
code = parts[2]
name = parts[1]
price = float(parts[3]) if parts[3] else 0
chg_pct = float(parts[32]) if parts[32] else 0
high = float(parts[33]) if parts[33] else 0
low = float(parts[34]) if parts[34] else 0
volume = int(parts[6]) if parts[6] else 0
amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0
if price > 0:
results[code] = {"code": code, "name": name, "price": price,
"change_pct": chg_pct, "high": high, "low": low,
"volume": volume, "amount": amount,
"market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"}
return results
except Exception as e:
print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr)
return {}
def scan_hot_sectors():
"""从DB读热门板块,返回板块名+领涨股"""
conn = get_conn()
latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0]
if not latest:
conn.close()
return []
sectors = conn.execute("""
SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count
FROM sector_snapshots WHERE snapshot_id=?
ORDER BY change_pct DESC LIMIT 15
""", (latest,)).fetchall()
conn.close()
return [{"name": s[0], "change": s[1], "lead_stock": s[2],
"lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0]
def scan_candidates():
"""主扫描流程"""
print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True)
# 1. 热门板块领涨股
hot = scan_hot_sectors()
print(f" 热门板块(涨幅>2%): {len(hot)}", flush=True)
candidates = {}
# 从热门板块拉领涨股
lead_codes = []
for s in hot:
if s.get("lead_code"):
lc = str(s["lead_code"]).strip()
if lc and lc not in candidates:
lead_codes.append(lc)
candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]}
print(f" 领涨股待查: {len(lead_codes)}", flush=True)
# 2. 腾讯API批量查行情
symbols = []
for c in lead_codes:
if len(c) == 6:
if c.startswith(("5", "6", "9")):
symbols.append(f"sh{c}")
else:
symbols.append(f"sz{c}")
else:
symbols.append(f"hk{c}")
prices = fetch_qq_batch(symbols)
print(f" 行情返回: {len(prices)}", flush=True)
# 3. 评估候选
new_candidates = []
for code, info in prices.items():
if code not in candidates:
continue
src = candidates[code]
price = info["price"]
chg = info["change_pct"]
name = info["name"]
vol = info["volume"]
amt = info["amount"]
# 条件:涨幅>3%,有量
if chg < 3.0:
continue
if vol <= 0:
continue
score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1))
entry_low = round(price * 0.95, 2)
entry_high = round(price, 2)
stop_loss = round(price * 0.92, 2)
take_profit = round(price * 1.15, 2)
candidate = {
"code": code,
"name": name,
"price": price,
"change_pct": chg,
"score": score,
"entry_low": entry_low,
"entry_high": entry_high,
"stop_loss": stop_loss,
"take_profit": take_profit,
"source": src["source"],
"sector": src.get("sector", ""),
"reason": f"热门板块{src['sector']}领涨+{chg:.1f}%"
}
new_candidates.append(candidate)
print(f"{code} {name}{price} (+{chg:.1f}%) 评分{score}", flush=True)
# 4. 写入candidates表
if new_candidates:
conn = get_conn()
for c in new_candidates:
conn.execute(
"INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, "
"entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))",
(c["code"], c["name"], c["price"], c["change_pct"], c["score"],
c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"],
c["source"], c["sector"], c["reason"])
)
conn.commit()
conn.close()
print(f" ✅ 写入{len(new_candidates)}只候选", flush=True)
else:
print(f" ⚪ 无新候选", flush=True)
# 5. 推送到Dad(只推高分)
high_score = [c for c in new_candidates if c["score"] >= 6]
if high_score:
lines = ["🔍 市场扫描发现潜在机会:"]
for c in high_score[:3]:
lines.append(
f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) "
f"评分{c['score']}/10 | {c['reason']}"
)
msg = "\n".join(lines)
# 推XMPP(通过知微 Bot HTTP 桥 :5805
import time as _time
t0 = _time.time()
try:
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)
print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000))
except Exception as e:
print(f" ⚠️ 推送失败: {e}", file=sys.stderr)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000))
if __name__ == "__main__":
scan_candidates()
#!/usr/bin/env python3
"""market_scanner.py — 全市场异动扫描(替代小果扫描线)
每15分钟扫描:
1. 板块轮动检测(从已采集的 sector_snapshots 读)
2. 热门板块领涨股扫描(从腾讯API批量拉)
3. 资金流向异常检测(从已有 capital_flow_cache 读)
4. 输出候选股到 candidates 表
不依赖小果LLM,纯数据驱动。
"""
import sys, json, sqlite3, urllib.request, re, time
from pathlib import Path
from datetime import datetime
# XMPP 日志 hook
try:
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from xmpp_logger import log_xmpp
except ImportError:
def log_xmpp(*a, **kw): pass
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json")
UA = "Mozilla/5.0"
def get_conn():
return sqlite3.connect(str(DB_PATH))
def fetch_qq_batch(symbols):
"""腾讯批量行情"""
if not symbols:
return {}
url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
proxy = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy)
with opener.open(req, timeout=15) as r:
text = r.read().decode("gbk")
results = {}
for line in text.strip().split("\n"):
if "~" not in line:
continue
parts = line.split("~")
if len(parts) < 40:
continue
m = re.search(r'_(\w+)=', parts[0])
market = m.group(1) if m else ""
code = parts[2]
name = parts[1]
price = float(parts[3]) if parts[3] else 0
chg_pct = float(parts[32]) if parts[32] else 0
high = float(parts[33]) if parts[33] else 0
low = float(parts[34]) if parts[34] else 0
volume = int(parts[6]) if parts[6] else 0
amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0
if price > 0:
results[code] = {"code": code, "name": name, "price": price,
"change_pct": chg_pct, "high": high, "low": low,
"volume": volume, "amount": amount,
"market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"}
return results
except Exception as e:
print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr)
return {}
def scan_hot_sectors():
"""从DB读热门板块,返回板块名+领涨股"""
conn = get_conn()
latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0]
if not latest:
conn.close()
return []
sectors = conn.execute("""
SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count
FROM sector_snapshots WHERE snapshot_id=?
ORDER BY change_pct DESC LIMIT 15
""", (latest,)).fetchall()
conn.close()
return [{"name": s[0], "change": s[1], "lead_stock": s[2],
"lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0]
def scan_candidates():
"""主扫描流程"""
print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True)
# 1. 热门板块领涨股
hot = scan_hot_sectors()
print(f" 热门板块(涨幅>2%): {len(hot)}", flush=True)
candidates = {}
# 从热门板块拉领涨股
lead_codes = []
for s in hot:
if s.get("lead_code"):
lc = str(s["lead_code"]).strip()
if lc and lc not in candidates:
lead_codes.append(lc)
candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]}
print(f" 领涨股待查: {len(lead_codes)}", flush=True)
# 2. 腾讯API批量查行情
symbols = []
for c in lead_codes:
if len(c) == 6:
if c.startswith(("5", "6", "9")):
symbols.append(f"sh{c}")
else:
symbols.append(f"sz{c}")
else:
symbols.append(f"hk{c}")
prices = fetch_qq_batch(symbols)
print(f" 行情返回: {len(prices)}", flush=True)
# 3. 评估候选
new_candidates = []
for code, info in prices.items():
if code not in candidates:
continue
src = candidates[code]
price = info["price"]
chg = info["change_pct"]
name = info["name"]
vol = info["volume"]
amt = info["amount"]
# 条件:涨幅>3%,有量
if chg < 3.0:
continue
if vol <= 0:
continue
score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1))
entry_low = round(price * 0.95, 2)
entry_high = round(price, 2)
stop_loss = round(price * 0.92, 2)
take_profit = round(price * 1.15, 2)
candidate = {
"code": code,
"name": name,
"price": price,
"change_pct": chg,
"score": score,
"entry_low": entry_low,
"entry_high": entry_high,
"stop_loss": stop_loss,
"take_profit": take_profit,
"source": src["source"],
"sector": src.get("sector", ""),
"reason": f"热门板块{src['sector']}领涨+{chg:.1f}%"
}
new_candidates.append(candidate)
print(f"{code} {name}{price} (+{chg:.1f}%) 评分{score}", flush=True)
# 4. 写入candidates表
if new_candidates:
conn = get_conn()
for c in new_candidates:
conn.execute(
"INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, "
"entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))",
(c["code"], c["name"], c["price"], c["change_pct"], c["score"],
c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"],
c["source"], c["sector"], c["reason"])
)
conn.commit()
conn.close()
print(f" ✅ 写入{len(new_candidates)}只候选", flush=True)
else:
print(f" ⚪ 无新候选", flush=True)
# 5. 推送到Dad(只推高分)
high_score = [c for c in new_candidates if c["score"] >= 6]
if high_score:
lines = ["🔍 市场扫描发现潜在机会:"]
for c in high_score[:3]:
lines.append(
f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) "
f"评分{c['score']}/10 | {c['reason']}"
)
msg = "\n".join(lines)
# 推XMPP(通过知微 Bot HTTP 桥 :5805
import time as _time
t0 = _time.time()
try:
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)
print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000))
except Exception as e:
print(f" ⚠️ 推送失败: {e}", file=sys.stderr)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000))
if __name__ == "__main__":
scan_candidates()
File diff suppressed because it is too large Load Diff
+59 -64
View File
@@ -1,64 +1,59 @@
#!/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 深度分析(每日强制,14只约8-10分钟
print("\n" + "=" * 50)
print("🧠 持仓12维LLM分析(每日强制刷新")
print("=" * 50)
import subprocess as _sp
analysis_result = {"ok": 0, "fail": 0, "skip": 0}
try:
r = _sp.run(
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py",
"--type", "holding", "--today"],
capture_output=True, text=True, timeout=3600)
print(r.stdout[-2000:] if len(r.stdout) > 2000 else r.stdout)
if r.returncode != 0 and r.stderr:
print(f"⚠️ stderr: {r.stderr[:300]}")
# 从输出尾部解析统计
import re as _re
m = _re.search(r"完成: (\d+)成功, (\d+)失败, (\d+)跳过", r.stdout)
if m:
analysis_result = {"ok": int(m.group(1)), "fail": int(m.group(2)), "skip": int(m.group(3))}
except Exception as e:
print(f"⚠️ 12维分析步骤异常: {e}")
# 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. 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✅ 盘前重评完毕")
File diff suppressed because it is too large Load Diff
+136 -135
View File
@@ -1,135 +1,136 @@
#!/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})"
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), 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()
@@ -13,7 +13,7 @@
import json, sys, os, re, urllib.request, sqlite3
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from datetime import datetime
from mo_data import read_decisions, read_portfolio, get_price
from mo_data import read_decisions, read_portfolio
DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json"
@@ -23,7 +23,25 @@ CRITICAL_DAYS = 21 # 超过21天→严重警告
DIVERGENCE_WARN = 30 # 偏离买入区>30%→警告
DIVERGENCE_CRIT = 50 # 偏离>50%→严重
# ── 使用 mo_data.get_price 统一获取价格 ──
def get_price(code):
"""从腾讯API获取当前价"""
try:
market = "sh" if code.startswith("6") else "sz" if code.startswith("0") or code.startswith("3") else ""
if code.startswith(("00", "30")) or code.startswith("68"):
market = "sh" if code.startswith("6") else "sz"
elif code.startswith(("01", "02", "03")):
market = "sz"
url = f"http://qt.gtimg.cn/q={market}{code}"
req = urllib.request.Request(url, headers={"User-Agent": "curl/7.81"})
with urllib.request.urlopen(req, timeout=5) as resp:
raw = resp.read().decode("gbk")
parts = raw.split("~")
if len(parts) > 3:
price = float(parts[3]) if parts[3] else 0
chg = float(parts[32]) if parts[32] else 0
return price, chg if price > 0 else (None, None)
except: pass
return None, None
def parse_buy_zone(current):
"""从策略current字段提取买入区间最低和最高"""
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# sync_profile_scripts.sh — 把 deploy/profile-scripts 全部硬链接到 profile scripts 目录
# 每次部署(scp/git)后必须跑:scp替换文件会破坏硬链接(inode变更),导致cron跑旧版本。
set -e
SRC="/home/hmo/MoFin/deploy/profile-scripts"
DST="/home/hmo/.hermes/profiles/position-analyst/scripts"
count=0
for f in "$SRC"/*.py; do
name=$(basename "$f")
ln -f "$f" "$DST/$name"
count=$((count+1))
done
echo "synced $count scripts (hardlink)"
+393 -327
View File
File diff suppressed because it is too large Load Diff