From 4791d3cf40da6e7f84de2cb7fca29c9405aea12d Mon Sep 17 00:00:00 2001 From: aszerW Date: Tue, 2 Jun 2026 01:16:34 +0800 Subject: [PATCH] refactor(scheduler): move daily_scheduler.py to rotation/ and add simple_rotation support - Move scripts/daily_scheduler.py -> rotation/daily_scheduler.py - Add run_simple_rotation() to execute simple_rotation.py via subprocess - Add --strategy flag (simple/legacy/all) for flexible strategy selection - Add --simple-config flag for custom simple rotation config path - Update Dockerfile and docker-compose.yml path references - Add configurable title to send_report_to_dingtalk() --- Dockerfile | 2 +- docker-compose.yml | 6 +- {scripts => rotation}/daily_scheduler.py | 162 ++++++++++++++++++----- 3 files changed, 133 insertions(+), 37 deletions(-) rename {scripts => rotation}/daily_scheduler.py (60%) diff --git a/Dockerfile b/Dockerfile index b69b913..e704a36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ EXPOSE 80 CMD ["python", "datasource/flask_server.py", "--host", "0.0.0.0"] # 运行定时任务调度器(如需使用Flask服务,取消上面注释并注释掉下面) -# CMD ["python", "scripts/daily_scheduler.py", "--time", "09:00"] \ No newline at end of file +# CMD ["python", "rotation/daily_scheduler.py", "--time", "09:00"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c8d6992..bf9d85f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,8 @@ services: # 挂载 results 目录(保存报告) - ./results:/app/results # 默认daemon模式运行,只需简单命令即可 - # command: ["python", "scripts/daily_scheduler.py"] + # command: ["python", "rotation/daily_scheduler.py"] # 如需立即执行一次并退出: - # command: ["python", "scripts/daily_scheduler.py", "--run-now"] + # command: ["python", "rotation/daily_scheduler.py", "--now"] # 如需执行一次后进入定时循环: - # command: ["python", "scripts/daily_scheduler.py", "--no-daemon"] + # command: ["python", "rotation/daily_scheduler.py", "--no-daemon"] diff --git a/scripts/daily_scheduler.py b/rotation/daily_scheduler.py similarity index 60% rename from scripts/daily_scheduler.py rename to rotation/daily_scheduler.py index a8c8583..98d2d70 100644 --- a/scripts/daily_scheduler.py +++ b/rotation/daily_scheduler.py @@ -9,9 +9,9 @@ ETF轮动策略定时调度器 4. 发送图片链接到钉钉群 用法: - python scripts/daily_scheduler.py --time 15:30 # 后台定时模式 - python scripts/daily_scheduler.py --now # 立即执行一次 - python scripts/daily_scheduler.py --no-daemon # 非后台模式 + python rotation/daily_scheduler.py --time 15:30 # 后台定时模式 + python rotation/daily_scheduler.py --now # 立即执行一次 + python rotation/daily_scheduler.py --no-daemon # 非后台模式 """ import warnings @@ -154,13 +154,68 @@ def run_strategy(config_path: str = "strategies/rotation/config.yaml") -> dict: return {"success": False, "error": str(e)} -def send_report_to_dingtalk(chart_path: str, summary_text: str = "") -> bool: +def run_simple_rotation(config_path: str = None) -> dict: + """ + 执行 simple_rotation.py 策略回测并生成报告 + + Args: + config_path: 配置文件路径,默认使用 rotation/config_simple.yaml + + Returns: + dict: 执行结果,包含报告路径等信息 + """ + logger.info("开始执行 Simple Rotation 策略回测...") + + try: + cmd = [ + sys.executable, + str(project_root / "rotation" / "simple_rotation.py") + ] + if config_path: + cmd.extend(["--config", config_path]) + + logger.info(f"执行命令: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=project_root, + timeout=900 + ) + + if result.returncode != 0: + logger.error(f"Simple Rotation 执行失败:\n{result.stderr}") + return {"success": False, "error": result.stderr} + + logger.info("Simple Rotation 执行成功") + logger.debug(result.stdout) + + # simple_rotation.py 生成的报告路径 + chart_path = project_root / "rotation" / "results" / "simple_rotation_report.png" + + return { + "success": True, + "stdout": result.stdout, + "chart_path": str(chart_path) if chart_path.exists() else None, + } + + except subprocess.TimeoutExpired: + logger.error("Simple Rotation 执行超时") + return {"success": False, "error": "timeout"} + except Exception as e: + logger.error(f"Simple Rotation 执行异常: {e}") + return {"success": False, "error": str(e)} + + +def send_report_to_dingtalk(chart_path: str, summary_text: str = "", title: str = None) -> bool: """ 上传报告到OSS并发送图片链接到钉钉 Args: chart_path: 图片文件路径 summary_text: 摘要文本 + title: 消息标题,默认自动生成 Returns: bool: 是否发送成功 @@ -173,7 +228,8 @@ def send_report_to_dingtalk(chart_path: str, summary_text: str = "") -> bool: try: today_str = datetime.now().strftime("%Y-%m-%d") - title = f"ETF轮动策略调仓日报 ({today_str})" + if title is None: + title = f"ETF轮动策略调仓日报 ({today_str})" # 使用原有的 send_to_all_groups 发送图片 # 该方法会自动:上传到OSS → 发送Markdown消息带图片链接 @@ -197,35 +253,49 @@ def send_report_to_dingtalk(chart_path: str, summary_text: str = "") -> bool: return False -def setup_schedule(target_time: str = "15:30", config_path: str = "strategies/rotation/config.yaml"): +def setup_schedule(target_time: str = "15:30", + config_path: str = "strategies/rotation/config.yaml", + strategy: str = "all", + simple_config: str = None): """ 设置定时任务 Args: target_time: 执行时间 (HH:MM) - config_path: 配置文件路径 + config_path: legacy策略配置文件路径 + strategy: 策略选择 - "simple" / "legacy" / "all" + simple_config: simple_rotation 配置文件路径 """ - logger.info(f"设置定时任务: 每天 {target_time} 执行") + logger.info(f"设置定时任务: 每天 {target_time} 执行 (策略: {strategy})") # 清除已有任务 schedule.clear() # 添加每日任务 - schedule.every().day.at(target_time).do(daily_task, config_path=config_path) + schedule.every().day.at(target_time).do( + daily_task, + config_path=config_path, + strategy=strategy, + simple_config=simple_config + ) logger.info("定时任务设置完成,等待执行...") -def daily_task(config_path: str = "strategies/rotation/config.yaml"): +def daily_task(config_path: str = "strategies/rotation/config.yaml", + strategy: str = "all", + simple_config: str = None): """ 每日任务主流程 Args: - config_path: 配置文件路径 + config_path: legacy策略配置文件路径 + strategy: 策略选择 - "simple" / "legacy" / "all"(两者都执行) + simple_config: simple_rotation 配置文件路径 """ today_str = datetime.now().strftime("%Y-%m-%d") logger.info("=" * 60) - logger.info(f"开始执行每日任务: {today_str}") + logger.info(f"开始执行每日任务: {today_str} 策略: {strategy}") logger.info("=" * 60) # 1. 检查是否为交易日 @@ -233,22 +303,35 @@ def daily_task(config_path: str = "strategies/rotation/config.yaml"): logger.info("今天不是交易日,跳过执行") return - # 2. 执行策略 - result = run_strategy(config_path) + # 2. 执行 Simple Rotation 策略 + if strategy in ("simple", "all"): + result = run_simple_rotation(simple_config) + if result["success"]: + if result.get("chart_path"): + send_report_to_dingtalk( + chart_path=result["chart_path"], + summary_text="", + title=f"Simple Rotation 调仓日报 ({today_str})" + ) + else: + logger.warning("Simple Rotation 未找到报告文件") + else: + logger.error(f"Simple Rotation 执行失败: {result.get('error', '未知错误')}") - if not result["success"]: - logger.error(f"策略执行失败: {result.get('error', '未知错误')}") - return - - # 3. 发送报告 - if result.get("chart_path"): - # 只发送标题和图片,不附带文字摘要 - send_report_to_dingtalk( - chart_path=result["chart_path"], - summary_text="" # 空字符串,只显示标题和图片 - ) - else: - logger.warning("未找到报告文件") + # 3. 执行 Legacy 策略 + if strategy in ("legacy", "all"): + result = run_strategy(config_path) + if result["success"]: + if result.get("chart_path"): + send_report_to_dingtalk( + chart_path=result["chart_path"], + summary_text="", + title=f"ETF轮动策略调仓日报 ({today_str})" + ) + else: + logger.warning("Legacy 策略未找到报告文件") + else: + logger.error(f"Legacy 策略执行失败: {result.get('error', '未知错误')}") logger.info("每日任务执行完成") @@ -276,7 +359,20 @@ def main(): '--config', type=str, default='strategies/rotation/config.yaml', - help='配置文件路径' + help='Legacy策略配置文件路径' + ) + parser.add_argument( + '--simple-config', + type=str, + default=None, + help='Simple Rotation 配置文件路径(默认 rotation/config_simple.yaml)' + ) + parser.add_argument( + '--strategy', + type=str, + choices=['simple', 'legacy', 'all'], + default='simple', + help='策略选择: simple=仅Simple Rotation, legacy=仅Legacy策略, all=两者都执行(默认 simple)' ) parser.add_argument( '--now', @@ -296,19 +392,19 @@ def main(): if args.now: # 立即执行一次并退出 - daily_task(args.config) + daily_task(args.config, args.strategy, args.simple_config) elif args.no_daemon: # 非后台模式:执行一次后进入定时循环 - setup_schedule(args.time, args.config) + setup_schedule(args.time, args.config, args.strategy, args.simple_config) logger.info("执行一次测试...") - daily_task(args.config) + daily_task(args.config, args.strategy, args.simple_config) logger.info("测试完成,启动定时任务循环(Ctrl+C 停止)...") run_scheduler_loop() else: # 默认:后台定时模式 - setup_schedule(args.time, args.config) + setup_schedule(args.time, args.config, args.strategy, args.simple_config) run_scheduler_loop() if __name__ == '__main__': - main() \ No newline at end of file + main()