44import json
55import logging
66import os
7+ import subprocess
8+ import sys
79import uuid
8- from datetime import datetime
10+ from datetime import date , datetime
911
1012from apscheduler .schedulers .background import BackgroundScheduler
1113from apscheduler .triggers .cron import CronTrigger
@@ -237,6 +239,97 @@ def run_memory_consolidation() -> None:
237239 logger .error ("记忆整理任务失败: %s" , e , exc_info = True , extra = {"category" : "dream" })
238240
239241
242+ # === Paper engine scheduling ===
243+
244+ _PROJECT_ROOT = os .path .dirname (STOCK_DATA_DIR )
245+
246+
247+ def _pre_market_paper () -> None :
248+ """盘前自动启动模拟盘引擎:检查交易日 → 停旧 → 起新"""
249+ _today = date .today ()
250+ from alphaclaude .engine .trading_calendar import is_trading_day
251+
252+ if not is_trading_day (_today ):
253+ logger .info ("今日非交易日,跳过模拟盘启动" , extra = {"category" : "paper" })
254+ return
255+
256+ # 防重复:同一天只启动一次
257+ _sentinel = os .path .join (STOCK_DATA_DIR , "state" , ".paper_last_start" )
258+ _today_str = _today .isoformat ()
259+ if os .path .exists (_sentinel ):
260+ try :
261+ if open (_sentinel ).read ().strip () == _today_str :
262+ logger .info ("今日模拟盘已启动过,跳过" , extra = {"category" : "paper" })
263+ return
264+ except OSError :
265+ pass
266+
267+ logger .info ("盘前模拟盘启动开始" , extra = {"category" : "paper" })
268+ try :
269+ # Step 1: 停止所有运行中的 paper 引擎
270+ _r1 = subprocess .run (
271+ [sys .executable , "-m" , "alphaclaude.engine.cli" ,
272+ "--stop-running" , "--mode" , "paper" ],
273+ capture_output = True , text = True , timeout = 30 ,
274+ cwd = _PROJECT_ROOT ,
275+ )
276+ _stdout = _r1 .stdout .strip () if _r1 .stdout else ""
277+ logger .info ("停旧模拟盘: %s" , _stdout or "无运行中进程" , extra = {"category" : "paper" })
278+
279+ # Step 2: 启动新的 paper daemon
280+ _r2 = subprocess .run (
281+ [sys .executable , "-m" , "alphaclaude.engine.cli" ,
282+ "--mode" , "paper" , "--daemon" ],
283+ capture_output = True , text = True , timeout = 30 ,
284+ cwd = _PROJECT_ROOT ,
285+ )
286+ _stdout2 = _r2 .stdout .strip () if _r2 .stdout else ""
287+ logger .info ("新模拟盘: %s" , _stdout2 , extra = {"category" : "paper" })
288+
289+ # 标记今日已启动
290+ os .makedirs (os .path .dirname (_sentinel ), exist_ok = True )
291+ with open (_sentinel , "w" ) as _f :
292+ _f .write (_today_str )
293+
294+ _send_to_all (f"[模拟盘] 今日模拟盘已启动 | { _today_str } " )
295+ except Exception as e :
296+ logger .error ("模拟盘启动失败: %s" , e , exc_info = True , extra = {"category" : "paper" })
297+
298+
299+ def _post_market_paper () -> None :
300+ """盘后自动停止模拟盘引擎"""
301+ _today = date .today ()
302+ if _today .weekday () >= 5 :
303+ return
304+
305+ logger .info ("盘后模拟盘停止" , extra = {"category" : "paper" })
306+ try :
307+ _r = subprocess .run (
308+ [sys .executable , "-m" , "alphaclaude.engine.cli" ,
309+ "--stop-running" , "--mode" , "paper" ],
310+ capture_output = True , text = True , timeout = 30 ,
311+ cwd = _PROJECT_ROOT ,
312+ )
313+ _stdout = _r .stdout .strip () if _r .stdout else ""
314+ logger .info ("停模拟盘: %s" , _stdout or "无运行中进程" , extra = {"category" : "paper" })
315+
316+ # 同时生成日报
317+ from alphaclaude .engine .cli import run_registry
318+ _runs = run_registry .list_runs ("paper" )
319+ if _runs :
320+ _last = _runs [0 ]
321+ _dr = subprocess .run (
322+ [sys .executable , "-m" , "alphaclaude.tools.daily_report" ,
323+ _last .run_id ],
324+ capture_output = True , text = True , timeout = 60 ,
325+ cwd = _PROJECT_ROOT ,
326+ )
327+ if _dr .stdout .strip ():
328+ _send_to_all (_dr .stdout .strip ())
329+ except Exception as e :
330+ logger .error ("模拟盘停止失败: %s" , e , exc_info = True , extra = {"category" : "paper" })
331+
332+
240333def start_scheduler (include_market_jobs : bool = True ):
241334 """Initialize scheduler and restore user-created tasks.
242335
@@ -259,10 +352,15 @@ def start_scheduler(include_market_jobs: bool = True):
259352 id = "dream_am" , name = "记忆整理(凌晨)" )
260353 _scheduler .add_job (run_memory_consolidation , CronTrigger (hour = 15 , minute = 17 ),
261354 id = "dream_pm" , name = "记忆整理(下午)" )
355+ # 模拟盘自动调度:盘前 8:30 启动,盘后 15:05 停止(交易日历兜底非交易日跳过)
356+ _scheduler .add_job (_pre_market_paper , CronTrigger (hour = 8 , minute = 30 , day_of_week = "mon-fri" ),
357+ id = "paper_start" , name = "模拟盘启动" )
358+ _scheduler .add_job (_post_market_paper , CronTrigger (hour = 15 , minute = 5 , day_of_week = "mon-fri" ),
359+ id = "paper_stop" , name = "模拟盘停止" )
262360 _scheduler .start ()
263361 if include_market_jobs :
264- msg = "定时任务已启动: 工作日 9:00/12:00/15:30 + 记忆整理 3:17/15:17"
362+ msg = "定时任务已启动: 模拟盘 8:30/15:05 + 工作日 9:00/12:00/15:30 + 记忆整理 3:17/15:17"
265363 else :
266- msg = "定时任务已启动: 记忆整理 3:17/15:17 + 自定义任务;内置行情分析由引擎负责"
364+ msg = "定时任务已启动: 模拟盘 8:30/15:05 + 记忆整理 3:17/15:17 + 自定义任务;内置行情分析由引擎负责"
267365 logger .info (msg , extra = {"category" : "startup" })
268366 restore_dynamic_tasks ()
0 commit comments