feat: 股票分析加政策注入(A股CN/港美股CN+INTL) + 新建calc_cn_levels.py+fetch_policy.py

This commit is contained in:
2026-07-23 10:33:47 +08:00
parent 2b78639b9e
commit fca5bfc24d
4 changed files with 203 additions and 6 deletions
+32 -2
View File
@@ -121,6 +121,26 @@ def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float
), strategy ), strategy
def _read_policy(path):
"""读取政策文件,不存在则返回 None"""
p = Path(path)
return p.read_text(encoding="utf-8").strip() if p.exists() else None
def _build_header(args, strategy_label):
"""构建推送头部: 政策 + 策略"""
lines = []
# 政策注入
if args.policy:
pol = _read_policy(args.policy)
if pol:
lines.append(pol)
# 策略说明
if strategy_label and strategy_label != 'rsi2_revert':
lines.append(f"(策略: {strategy_label})")
return "\n".join(lines) if lines else ""
def format_cn_output(levels, change_pct, current_price, side, symbol, info, mode): def format_cn_output(levels, change_pct, current_price, side, symbol, info, mode):
"""A 股格式: ¥ 符号, 股息率""" """A 股格式: ¥ 符号, 股息率"""
name = info.get('name', symbol) name = info.get('name', symbol)
@@ -164,6 +184,9 @@ def main():
ap.add_argument('--strategy', default='rsi2_revert', ap.add_argument('--strategy', default='rsi2_revert',
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'], choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
help='策略 (default rsi2_revert, A 股推荐)') help='策略 (default rsi2_revert, A 股推荐)')
ap.add_argument('--policy',
default='/tmp/policy_cn.txt',
help='国内政策文件路径 (default /tmp/policy_cn.txt, 空则跳过)')
args = ap.parse_args() args = ap.parse_args()
# 过滤高股息 # 过滤高股息
@@ -229,9 +252,16 @@ def main():
print() print()
if output_lines: if output_lines:
header = f"📊 A 股日内做T点位 ({today})\n⚠️ 仅参考, 不交易\n" header_parts = []
pol = _read_policy(args.policy)
if pol:
header_parts.append(pol)
header_parts.append(f"📊 A 股日内做T点位 ({today})\n⚠️ 仅参考, 不交易")
if args.strategy != 'rsi2_revert':
header_parts.append(f"(策略: {args.strategy})")
header = "\n".join(header_parts)
print("\n=== QQ 推送内容 ===") print("\n=== QQ 推送内容 ===")
print(header + "\n---\n".join(output_lines)) print(header + "\n" + "\n---\n".join(output_lines))
else: else:
print("\n💤 全部场景否决, 无输出") print("\n💤 全部场景否决, 无输出")
+22 -2
View File
@@ -148,6 +148,11 @@ def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float
), strategy ), strategy
def _read_policy(path):
p = Path(path)
return p.read_text(encoding="utf-8").strip() if p.exists() else None
def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode): def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode):
"""格式化为 QQ 推送文本 (单条)""" """格式化为 QQ 推送文本 (单条)"""
if mode == 'strict': if mode == 'strict':
@@ -186,6 +191,10 @@ def main():
ap.add_argument('--strategy', default='rsi2_revert', ap.add_argument('--strategy', default='rsi2_revert',
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'], choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
help='策略 (default rsi2_revert)') help='策略 (default rsi2_revert)')
ap.add_argument('--policy-cn', default='/tmp/policy_cn.txt',
help='国内政策文件 (空/不存在则跳过)')
ap.add_argument('--policy-intl', default='/tmp/policy_intl.txt',
help='国际政策文件 (空/不存在则跳过)')
args = ap.parse_args() args = ap.parse_args()
if not os.path.exists(CANDIDATE_FILE): if not os.path.exists(CANDIDATE_FILE):
@@ -262,9 +271,20 @@ def main():
print() print()
if output_lines: if output_lines:
header = f"📊 港股日内做T点位 ({date})\n⚠️ 仅参考, 不交易\n" header_parts = []
pol_cn = _read_policy(args.policy_cn)
pol_intl = _read_policy(args.policy_intl)
if pol_cn:
header_parts.append(pol_cn)
if pol_intl:
header_parts.append(pol_intl)
header_parts.append(f"📊 港股日内做T点位 ({date})")
if args.strategy != 'rsi2_revert':
header_parts.append(f"(策略: {args.strategy})")
header_parts.append("⚠️ 仅参考, 不交易")
header = "\n".join(header_parts)
print("\n=== QQ 推送内容 ===") print("\n=== QQ 推送内容 ===")
print(header + "\n---\n".join(output_lines)) print(header + "\n" + "\n---\n".join(output_lines))
else: else:
print("\n💤 全部场景否决, 无输出") print("\n💤 全部场景否决, 无输出")
+22 -2
View File
@@ -136,6 +136,11 @@ def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float
), strategy ), strategy
def _read_policy(path):
p = Path(path)
return p.read_text(encoding="utf-8").strip() if p.exists() else None
def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode): def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode):
if mode == 'strict': if mode == 'strict':
return ( return (
@@ -173,6 +178,10 @@ def main():
ap.add_argument('--strategy', default='rsi2_revert', ap.add_argument('--strategy', default='rsi2_revert',
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'], choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
help='策略 (default rsi2_revert)') help='策略 (default rsi2_revert)')
ap.add_argument('--policy-cn', default='/tmp/policy_cn.txt',
help='国内政策文件 (空/不存在则跳过)')
ap.add_argument('--policy-intl', default='/tmp/policy_intl.txt',
help='国际政策文件 (空/不存在则跳过)')
args = ap.parse_args() args = ap.parse_args()
if not os.path.exists(CANDIDATE_FILE): if not os.path.exists(CANDIDATE_FILE):
@@ -246,9 +255,20 @@ def main():
print() print()
if output_lines: if output_lines:
header = f"📊 美股日内做T点位 ({date})\n⚠️ 仅参考, 不交易\n" header_parts = []
pol_cn = _read_policy(args.policy_cn)
pol_intl = _read_policy(args.policy_intl)
if pol_cn:
header_parts.append(pol_cn)
if pol_intl:
header_parts.append(pol_intl)
header_parts.append(f"📊 美股日内做T点位 ({date})")
if args.strategy != 'rsi2_revert':
header_parts.append(f"(策略: {args.strategy})")
header_parts.append("⚠️ 仅参考, 不交易")
header = "\n".join(header_parts)
print("\n=== QQ 推送内容 ===") print("\n=== QQ 推送内容 ===")
print(header + "\n---\n".join(output_lines)) print(header + "\n" + "\n---\n".join(output_lines))
else: else:
print("\n💤 全部场景否决, 无输出") print("\n💤 全部场景否决, 无输出")
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
fetch_policy.py - 读取 policy-news-monitor cron 最新 session,提取政策标题摘要
用法:
python3 fetch_policy.py --type cn # 国内政策 → /tmp/policy_cn.txt
python3 fetch_policy.py --type intl # 国际政策 → /tmp/policy_intl.txt
python3 fetch_policy.py --type both # 两者都抓
集成方式:
cron 先跑 policy-news-monitor 生成 session 文件,
本脚本读取最新 session 提取标题,供股票分析脚本注入。
"""
import argparse
import re
import sys
import os
from pathlib import Path
from datetime import datetime, date
REF_DIR = Path(__file__).parent.parent / "references"
_ref_dir = REF_DIR # 运行时覆盖
def latest_session(pattern):
"""返回匹配 pattern 的最新 session 文件路径"""
sessions = sorted(
_ref_dir.glob(f"session-*{pattern}*.md"),
key=lambda p: p.stat().st_mtime,
reverse=True
)
return sessions[0] if sessions else None
def extract_headlines(session_path, max_lines=8):
"""从 session 文件提取前 N 条政策新闻标题"""
if not session_path or not session_path.exists():
return None
text = session_path.read_text(encoding="utf-8")
lines = []
# 匹配形如 "· 央行宣布降准" 或 "1. 政策标题" 的行
for line in text.splitlines():
line = line.strip()
if not line:
continue
# 跳过代码块、标题、URL
if line.startswith("#") or line.startswith("```") or line.startswith("http"):
continue
# 提取列表项和政策标题
m = re.match(r"^[\d\.\\-\\◉]+\s*[\[【]?\s*([^\]\n]{8,60})", line)
if m:
title = m.group(1).strip()
if "免责" not in title and len(title) > 6:
lines.append(f" · {title}")
elif len(line) > 8 and len(line) < 80 and "" in line:
# "央行:降准" 格式
title = line.strip()
if "免责" not in title:
lines.append(f" · {title}")
if len(lines) >= max_lines:
break
return "\n".join(lines) if lines else None
def fetch_cn():
"""国内政策: 读最新 domestic session"""
session = latest_session("domestic")
if not session:
return None, "无 domestic session"
headlines = extract_headlines(session)
if not headlines:
return None, f"提取失败: {session.name}"
return headlines, session.name
def fetch_intl():
"""国际政策: 读最新 international session"""
session = latest_session("international")
if not session:
return None, "无 international session"
headlines = extract_headlines(session)
if not headlines:
return None, f"提取失败: {session.name}"
return headlines, session.name
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--type', default='both', choices=['cn', 'intl', 'both'])
ap.add_argument('--ref', default=None, help='references 目录')
args = ap.parse_args()
# 动态设置 ref dir(允许 cron 指定不同路径)
ref_path = Path(args.ref) if args.ref else REF_DIR
if ref_path.exists():
_ref_dir = ref_path
else:
_ref_dir = REF_DIR
date_str = date.today().isoformat()
done = []
if args.type in ('cn', 'both'):
headlines, info = fetch_cn()
if headlines:
Path("/tmp/policy_cn.txt").write_text(f"# 国内政策摘要 {date_str}(来源: {info}\n{headlines}\n")
print(f"✅ 国内政策 → /tmp/policy_cn.txt{info}")
done.append("cn")
else:
print(f"⚠️ 国内政策: {info}")
if args.type in ('intl', 'both'):
headlines, info = fetch_intl()
if headlines:
Path("/tmp/policy_intl.txt").write_text(f"# 国际政策摘要 {date_str}(来源: {info}\n{headlines}\n")
print(f"✅ 国际政策 → /tmp/policy_intl.txt{info}")
done.append("intl")
else:
print(f"⚠️ 国际政策: {info}")
if not done:
print("⚠️ 警告: 两个都没抓到,exit 1")
sys.exit(1)
if __name__ == '__main__':
main()