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:
8
.env
8
.env
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""从环境变量获取数据库配置"""
|
||||
|
||||
@@ -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:
|
||||
"""通知管理器 - 统一管理多种通知渠道"""
|
||||
|
||||
|
||||
@@ -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
160
轮动策略核心逻辑.md
Normal 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日价格进行线性回归
|
||||
- 得分 = 斜率 × R² × 10000
|
||||
- 同时考虑趋势强度和稳定性
|
||||
|
||||
### 调仓周期
|
||||
- **最低持有期**:1天(`rebalance_days = 1`)
|
||||
- **调仓阈值**:0%(`rebalance_threshold = 0.0`)
|
||||
- 新组合总得分 > 当前组合总得分时即触发调仓
|
||||
- **交易成本**:0.1%(双边,含佣金+滑点)
|
||||
|
||||
**实际运行**:每个交易日评估是否调仓,无强制锁定期
|
||||
|
||||
---
|
||||
|
||||
## 🧮 得分计算逻辑
|
||||
|
||||
### 核心公式
|
||||
```
|
||||
得分 = 斜率(slope) × R² × 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%阈值过滤 |
|
||||
| **基准指数** | 沪深300(000300.SH) |
|
||||
|
||||
---
|
||||
|
||||
## 💡 核心优势
|
||||
|
||||
1. **跨市场分散**:覆盖股票、商品、加密货币,降低单一市场风险
|
||||
2. **趋势+稳定性**:slope_r2因子同时捕捉趋势方向和可靠性
|
||||
3. **自动轮动**:量化评分,避免主观判断
|
||||
4. **溢价保护**:防止高价买入跨境ETF
|
||||
5. **灵活配置**:所有参数可通过YAML配置文件调整
|
||||
|
||||
---
|
||||
|
||||
*文档生成时间:2026-04-16*
|
||||
Reference in New Issue
Block a user