feat(scripts): 迁移轮动策略定时调度器

新增文件:
- scripts/daily_scheduler.py: 定时调度器,支持交易日判断、回测执行、OSS上传、钉钉推送
- scripts/run_rotation.py: 回测入口脚本,支持Flask API和本地数据源切换
- config/settings.py: 配置管理模块,支持钉钉多群配置

功能:
1. 每天15:30自动检查交易日
2. 交易日执行策略回测生成报告
3. 上传报告图片到OSS
4. 发送图片链接到钉钉群

修复:
- 添加oss2库SyntaxWarning过滤(Python 3.12兼容)
- 钉钉消息精简为标题+图片格式
This commit is contained in:
2026-05-18 00:57:59 +08:00
parent 79c6ab8620
commit 7b41bb8c6d
3 changed files with 522 additions and 0 deletions

115
scripts/run_rotation.py Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
轮动策略回测入口脚本
用法:
python scripts/run_rotation.py --config strategies/rotation/config.yaml
python scripts/run_rotation.py --config strategies/rotation/config.yaml --save-path results/report
"""
import sys
import argparse
from pathlib import Path
from datetime import datetime
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# 加载环境变量
from dotenv import load_dotenv
load_dotenv()
from strategies.rotation.strategy import RotationStrategy
def load_config(config_path: str) -> dict:
"""加载配置"""
import yaml
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def main():
parser = argparse.ArgumentParser(description='ETF轮动策略回测')
parser.add_argument(
'--config',
type=str,
default='strategies/rotation/config.yaml',
help='配置文件路径'
)
parser.add_argument(
'--save-path',
type=str,
default=None,
help='报告保存路径前缀'
)
parser.add_argument(
'--no-api',
action='store_true',
help='不使用Flask API使用本地数据源'
)
args = parser.parse_args()
start_time = datetime.now()
print("=" * 60)
print(" ETF轮动策略 回测系统")
print("=" * 60)
# 加载配置
print(f"\n加载配置: {args.config}")
config = load_config(args.config)
# 显示配置摘要
code_list = list(config.get('code_list', {}).keys())
print(f"候选标的: {len(code_list)}")
print(f"回测区间: {config.get('start_date', 'N/A')} ~ {config.get('end_date', 'N/A')}")
print(f"因子类型: {config.get('factor_type', 'momentum')}")
print(f"窗口天数: {config.get('n_days', 25)}")
print(f"选股数量: {config.get('select_num', 3)}")
print(f"调仓周期: {config.get('rebalance_days', 1)}")
print(f"交易成本: {config.get('trade_cost', 0.001):.2%}")
# 初始化策略
print("\n初始化策略...")
strategy = RotationStrategy.from_yaml(args.config)
# 设置保存路径
if args.save_path is None:
report_date = datetime.now().strftime('%Y%m%d')
args.save_path = f"results/report_{report_date}"
# 执行回测
print("\n" + "=" * 60)
print("开始回测...")
print("=" * 60)
# 使用Flask API或本地数据源
use_flask_api = not args.no_api
data = strategy.get_data(use_flask_api=use_flask_api)
result = strategy.run_backtest(data=data, save_path=args.save_path)
# 输出结果
if result.get('result') is not None:
final_nav = result['result']['策略净值'].iloc[-1]
total_return = (final_nav - 1) * 100
print("\n" + "=" * 60)
print("回测完成!")
print("=" * 60)
print(f"最终净值: {final_nav:.4f}")
print(f"累计收益: {total_return:.2f}%")
print(f"调仓次数: {len(result.get('rebalance_events', []))}")
print(f"报告保存: {args.save_path}_*.csv")
elapsed = datetime.now() - start_time
print(f"\n总耗时: {elapsed.total_seconds():.1f}")
return result
if __name__ == '__main__':
main()