chore: capture deployed state
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)"
|
||||
Reference in New Issue
Block a user