feat(notify): 支持钉钉多群推送 & 添加轮动策略核心逻辑文档

- settings.py: 新增 get_all_dingtalk_configs() 自动扫描所有钉钉群配置
- notify.py: 新增 send_to_all_groups() 多群推送函数
- daily_scheduler.py: 报告和错误通知改用多群推送
- .env: 添加第二个钉钉群配置 (DINGTALK_WEBHOOK_2/SECRET_2)
- 轮动策略核心逻辑.md: 策略核心逻辑总结文档
This commit is contained in:
2026-04-23 22:57:23 +08:00
parent 3cca4d79c4
commit 4a500ca5bf
5 changed files with 230 additions and 8 deletions

8
.env
View File

@@ -3,10 +3,16 @@
# ==================== Tushare API (中国A股指数数据) ====================
TUSHARE_TOKEN=ae768b520150da8865a38f0d9c480578f695293588c3c684f00077a1
# 钉钉机器人配置
# 钉钉机器人配置 - 群1
DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=fb70c1561d8beba94b4f11568f4bb15e3ae07ccbdc8ac19676434a9d1cd17546
DINGTALK_SECRET=SEC1ae7cd2f1a6f9da3611af37da3e7d954c1e8533fc073c6c8cc5e5af3b6e5926b
# 钉钉机器人配置 - 群2
DINGTALK_WEBHOOK_2=https://oapi.dingtalk.com/robot/send?access_token=87c7abfcdd69b699c32da4e4f5981cd2ca6b0445474fc6ffb36f2ed0f6262fbb
DINGTALK_SECRET_2=SECf3d6b43f2f8a87ab91feffd052e71ec314fbf57a1842e483fe07af3c0a0e5aa6
# 数据库配置
DB_HOST=192.168.0.115
DB_PORT=5432

View File

@@ -27,13 +27,29 @@ DATA_CACHE_DIR.mkdir(exist_ok=True)
# ==================== 钉钉配置 ====================
def get_dingtalk_config() -> dict:
"""从环境变量获取钉钉配置"""
"""从环境变量获取钉钉配置默认群1"""
return {
"webhook": os.getenv("DINGTALK_WEBHOOK", ""),
"secret": os.getenv("DINGTALK_SECRET", ""),
}
def get_all_dingtalk_configs() -> list[dict]:
"""获取所有已配置的钉钉群配置列表"""
configs = []
# 群1主群
cfg1 = get_dingtalk_config()
if cfg1["webhook"]:
configs.append(cfg1)
# 群2 及后续扩展DINGTALK_WEBHOOK_2, _3, ...
for i in range(2, 10):
webhook = os.getenv(f"DINGTALK_WEBHOOK_{i}", "")
secret = os.getenv(f"DINGTALK_SECRET_{i}", "")
if webhook:
configs.append({"webhook": webhook, "secret": secret})
return configs
# ==================== 数据库配置 ====================
def get_db_config() -> dict:
"""从环境变量获取数据库配置"""

View File

@@ -12,7 +12,7 @@ import os
from loguru import logger
from typing import Optional
from config.settings import get_dingtalk_config
from config.settings import get_dingtalk_config, get_all_dingtalk_configs
from core.common.oss_utils import upload_image_to_oss
@@ -467,6 +467,45 @@ class DingTalkBot:
return self.send_image(image_path, title)
def send_to_all_groups(
send_func_name: str,
**kwargs,
) -> bool:
"""
向所有已配置的钉钉群发送消息
Args:
send_func_name: DingTalkBot 的发送方法名,如 'send_text', 'send_markdown', 'send_image_via_oss'
**kwargs: 传递给发送方法的参数
Returns:
bool: 是否全部发送成功
"""
configs = get_all_dingtalk_configs()
if not configs:
logger.warning("没有配置任何钉钉群,消息未发送")
return False
all_success = True
for i, cfg in enumerate(configs, 1):
bot = DingTalkBot(webhook=cfg["webhook"], secret=cfg["secret"])
method = getattr(bot, send_func_name, None)
if method is None:
logger.error(f"DingTalkBot 没有方法: {send_func_name}")
return False
try:
success = method(**kwargs)
if success:
logger.info(f"{i} 发送成功")
else:
logger.error(f"{i} 发送失败")
all_success = False
except Exception as e:
logger.error(f"{i} 发送异常: {e}")
all_success = False
return all_success
class NotificationManager:
"""通知管理器 - 统一管理多种通知渠道"""

View File

@@ -32,7 +32,7 @@ load_dotenv(project_root / ".env")
from loguru import logger
import schedule
import tushare as ts
from core.common.notify import DingTalkBot
from core.common.notify import DingTalkBot, send_to_all_groups
from core.common.oss_utils import upload_image_to_oss
# 配置日志
@@ -163,8 +163,9 @@ def send_report_to_dingtalk(chart_path: str, summary_text: str = "") -> bool:
today_str = datetime.now().strftime("%Y-%m-%d")
# 发送图文消息
success = bot.send_image_via_oss(
# 向所有群发送图文消息
success = send_to_all_groups(
"send_image_via_oss",
image_path=chart_path,
title=f"ETF轮动策略调仓日报 ({today_str})",
text=summary_text,
@@ -226,8 +227,8 @@ def daily_task(config_path: str = "config/strategies/rotation.yaml"):
if not result["success"]:
# 发送错误通知
bot = DingTalkBot()
bot.send_text(f"策略执行失败: {result.get('error', '未知错误')}")
# 发送错误通知到所有群
send_to_all_groups("send_text", content=f"策略执行失败: {result.get('error', '未知错误')}")
return
# 3. 发送报告

160
轮动策略核心逻辑.md Normal file
View File

@@ -0,0 +1,160 @@
# ETF轮动策略核心逻辑总结
## 📊 策略概览
基于**多因子动量**的跨市场ETF轮动策略通过量化评分自动选择表现最优的ETF组合进行投资。
---
## 🎯 候选池配置
### 覆盖市场
- **A股**17个指数
- 宽基沪深300(000300.SH)、中证500(000905.SH)、中证1000(000852.SH)、创业板指(399006.SZ)、上证红利(000015.SH)
- 金融:中证银行(399986.SZ)
- 消费:中证白酒(399997.SZ)
- 医药:中证医疗(399989.SZ)
- 科技:中证信息(000935.SH)
- 新能源:新能源车(399976.SZ)
- 周期资源:国证有色(399395.SZ)、中证煤炭(399998.SZ)、细分化工(399813.SZ)、中证能源(000937.SH)
- 其他:中证军工(399967.SZ)、中证农业(000949.SH)、国债指数(399702.SZ)
- **港股**1个恒生科技(HSTECH.HK)
- **美股**1个纳指100(NDX)
- **商品**1个黄金(AU.SHF)
- **加密货币**2个BTC、ETH
### 指数-ETF映射
每个指数对应一个可交易的ETF或直接交易例如
- `000300.SH`沪深300指数`510300.SH`华泰柏瑞沪深300ETF
- `NDX`纳指100`159501.SZ`嘉实纳指100ETF
- `BTC`(比特币) → 无ETF直接交易
**总计22个候选标的**
---
## ⏱️ 回看周期与调仓周期
### 回看周期
- **窗口长度**25个交易日
- **因子类型**`slope_r2`斜率×R²趋势得分
- 对过去25日价格进行线性回归
- 得分 = 斜率 ×× 10000
- 同时考虑趋势强度和稳定性
### 调仓周期
- **最低持有期**1天`rebalance_days = 1`
- **调仓阈值**0%`rebalance_threshold = 0.0`
- 新组合总得分 > 当前组合总得分时即触发调仓
- **交易成本**0.1%(双边,含佣金+滑点)
**实际运行**:每个交易日评估是否调仓,无强制锁定期
---
## 🧮 得分计算逻辑
### 核心公式
```
得分 = 斜率(slope) ×× 10000
```
### 计算步骤
1. **数据获取**获取每个标的过去25+日的收盘价
2. **价格归一化**:将价格序列除以起始价格(消除绝对价格影响)
3. **线性回归**:对归一化价格进行一元线性回归
- `x = [1, 2, 3, ..., 25]`
- `y = 归一化价格序列`
4. **提取指标**
- `slope`:回归斜率(代表趋势方向与强度)
- `R²`:拟合优度(代表趋势稳定性)
5. **计算得分**`score = 10000 × slope × R²`
### 得分含义
- **正值**:上升趋势,值越大趋势越强且越稳定
- **负值**:下降趋势
- **接近0**:无明显趋势或趋势不稳定
### 跨市场数据对齐
- **基准日历**以A股交易日为准
- **非A股标的**数据前向填充ffill到A股交易日
- **T+1规则**T日收盘计算信号仅使用T日及之前数据
---
## 📦 持仓数量与仓位配置
### 持仓数量
- **选中数量**5个ETF`select_num = 5`
- 每日从22个候选标的中选出得分最高的5个
### 仓位配置
- **等权分配**每个选中标的占总仓位的20%1/5
- **日收益率计算**`组合日收益 = mean(5个标的的日收益率)`
### 换仓逻辑
1. 每日计算所有标的的最新得分
2. 选出Top 5构成"目标组合"
3. 对比"目标组合"与"当前持仓组合"的总得分
4.`目标总得分 > 当前总得分`,则触发调仓
5. 调仓时卖出旧标的,等权买入新标的
---
## 🛡️ 溢价率控制跨境ETF
### 控制机制
- **A股ETF**:不启用(溢价通常 < 0.5%
- **港股ETF**阈值3%超过则排除
- **美股ETF**阈值2%超过则排除
- **商品ETF**不启用
### 处理模式
- **filter模式**溢价超阈值的标的直接从候选池排除
- **penalty模式**可选对高溢价标的得分乘以惩罚系数0.5
---
## 📅 运行时间安排
### 信号生成时间
- **运行时间**T+1日上午9:00北京时间
- **数据基准**基于T日前一交易日收盘数据
- **原因**美股和加密货币数据在T+1日凌晨才可用
### 数据源
- **A股**Tushare
- **港股/美股/商品**YFinance
- **加密货币**CCXT/OKX通过SSH隧道访问
---
## 📈 策略特点总结
| 维度 | 配置 |
|------|------|
| **策略类型** | 动量轮动趋势跟踪 |
| **候选池** | 22个标的A股/港股/美股/商品/加密货币 |
| **回看周期** | 25个交易日 |
| **因子类型** | 斜率×R²(趋势强度×稳定性 |
| **持仓数量** | 5个ETF |
| **仓位配置** | 等权各20% |
| **调仓频率** | 每日评估无锁定期 |
| **调仓条件** | 新组合得分 > 旧组合得分 |
| **交易成本** | 0.1%(双边) |
| **溢价控制** | 港股3%、美股2%阈值过滤 |
| **基准指数** | 沪深300000300.SH |
---
## 💡 核心优势
1. **跨市场分散**:覆盖股票、商品、加密货币,降低单一市场风险
2. **趋势+稳定性**slope_r2因子同时捕捉趋势方向和可靠性
3. **自动轮动**:量化评分,避免主观判断
4. **溢价保护**防止高价买入跨境ETF
5. **灵活配置**所有参数可通过YAML配置文件调整
---
*文档生成时间2026-04-16*