From 5b1412eab047c638f0521e73beaffb9e9f9d0d6c Mon Sep 17 00:00:00 2001 From: "WIN-ALB39P9B6BU\\Docker" Date: Wed, 24 Jun 2026 16:13:44 +0800 Subject: [PATCH] update --- core/scoring/models.py | 16 +- core/sfgrid/bus_events.py | 8 + core/sfgrid/pool_manager.py | 488 ++++++++++++++++++ core/ui/flet/app_v2.py | 42 +- .../{strategy.py => ref_backtest_strategy.py} | 0 5 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 core/sfgrid/pool_manager.py rename models/reference_backtest_logic/{strategy.py => ref_backtest_strategy.py} (100%) diff --git a/core/scoring/models.py b/core/scoring/models.py index a1346e5..53b8735 100644 --- a/core/scoring/models.py +++ b/core/scoring/models.py @@ -121,12 +121,26 @@ class ScoringResult(BaseModel): primary_key = CompositeKey('stock_code', 'trade_date') +# ============================================================ +# 8. PendingPoolAction — 待执行股票池操作(阶段一标记,阶段二执行) +# ============================================================ +class PendingPoolAction(BaseModel): + """pending_pool_actions 表 — T日标记的操作,待 T+1 执行""" + action_date = DateField() # T日日期(标记日期) + action_type = CharField(max_length=20) # 'eliminate' | 'liquidate' + stock_code = CharField(max_length=6) # 股票代码 + reason = TextField(null=True) # 淘汰原因描述 + + class Meta: + primary_key = CompositeKey('action_date', 'action_type', 'stock_code') + + # ============================================================ # 建表 # ============================================================ ALL_SCORING_TABLES = [ KlineStock, StockInfo, IndustryMapping, KlineIndex, - MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, + MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, PendingPoolAction, ] db.create_tables(ALL_SCORING_TABLES) diff --git a/core/sfgrid/bus_events.py b/core/sfgrid/bus_events.py index d17a7d2..079936f 100644 --- a/core/sfgrid/bus_events.py +++ b/core/sfgrid/bus_events.py @@ -5,3 +5,11 @@ EventTradeTargetDeleted = "trade_target_deleted" # 评分系统事件 EventScoringCompleted = "scoring_completed" # 评分完成, data: {'date', 'count'} EventSyncProgress = "sync_progress" # 同步进度, data: {'source', 'status', 'stats'} + +# 股票池管理器事件(阶段一) +# T日收盘标记完成, data: {action: 'eliminate'|'liquidate', stock_codes: list[str], count: int} +EventPoolMark = "pool_mark" + +# T+1日执行完成(阶段二用) +# data: {action: 'eliminate'|'liquidate'|'refill', stock_codes: list[str], count: int} +EventPoolActionExecute = "pool_action_execute" diff --git a/core/sfgrid/pool_manager.py b/core/sfgrid/pool_manager.py new file mode 100644 index 0000000..8b2b18e --- /dev/null +++ b/core/sfgrid/pool_manager.py @@ -0,0 +1,488 @@ +""" +pool_manager.py — 网格自动交易股票池管理器(阶段一:T日标记) +=================================================================== +后台 daemon 线程运行,每日定时: + 09:25 K线数据同步 + 09:30 执行评分 + 15:30 沉寂检测标记 + 周五15:30 额外执行周度淘汰标记 + +所有操作只记录到数据库,不执行真实交易(阶段二实现)。 +""" +from __future__ import annotations + +import threading +import time +from collections import defaultdict +from datetime import date, datetime, timedelta +import numpy as np +import pandas as pd + +from core.logger import LogLevel, PrintLog +from core.scoring.models import PendingPoolAction, ScoringResult +from core.sfgrid.model import SFGridTradeTarget +from core.sfgrid.bus_events import EventPoolMark +from core.eventbus import event_bus + + +# ============================================================ +# 配置 +# ============================================================ +TOP_N = 10 # 最大持仓数 +TOP_MODEL_N = 50 # 评分池 Top N +ELIM_WINDOW = 2 # 连续 N 周不在 Top50 则淘汰 +SLUMBER_DAYS = 10 # 沉寂触发天数(连续) +SLUMBER_TRIGGERS = 3 # 沉寂触发特征数(5个中触发几个) + +# ============================================================ +# 沉寂检测 — 直接复用 ref_backtest_strategy.py 的纯函数 +# ============================================================ + +def is_slumbering(df: pd.DataFrame, + lookback_60: int = 60, + lookback_20: int = 20, + min_triggers: int = SLUMBER_TRIGGERS) -> bool: + """ + 检测一只股是否陷入'沉寂'(资金离场后长期低位震荡)。 + 5 特征,>= min_triggers 触发则返回 True。 + + df 要求:包含 close/high/low/volume 列,index 为日期升序, + 至少 60 条记录。 + """ + if df is None or len(df) < lookback_60: + return False + + # 过滤停牌日期(volume=0 的行会导致 log_ret = NaN) + active = df[df["volume"] > 0] + if len(active) < lookback_60: + return False + + sub = active.tail(lookback_60) + close = sub["close"].values + high = sub["high"].values + low = sub["low"].values + vol = sub["volume"].values + + # 1. 波动率塌陷 + log_ret = np.log(close[1:] / close[:-1]) + if len(log_ret) < lookback_20: + return False + vol_20d = float(np.std(log_ret[-lookback_20:], ddof=1)) + vol_60d = float(np.std(log_ret, ddof=1)) + vol_collapse = (vol_60d > 0) and (vol_20d / vol_60d < 0.6) + + # 2. 振幅萎缩 + amp_20d = float(np.mean((high[-lookback_20:] - low[-lookback_20:]) / close[-lookback_20:]) * 100) + amp_shrink = amp_20d < 2.5 + + # 3. 成交量枯竭 + avg_vol_20 = float(np.mean(vol[-lookback_20:])) + avg_vol_60 = float(np.mean(vol)) + vol_dry = (avg_vol_60 > 0) and (avg_vol_20 / avg_vol_60 < 0.5) + + # 4. 价格弱势 + price_max_60 = float(np.max(close)) + price_weak = price_max_60 > 0 and (close[-1] / price_max_60) < 0.85 + + # 5. 反弹失败 + recent_high_30 = float(np.max(high[-30:])) + past_high_60 = float(np.max(high)) + rebound_fail = past_high_60 > 0 and (recent_high_30 / past_high_60) < 0.95 + + triggers = [vol_collapse, amp_shrink, vol_dry, price_weak, rebound_fail] + return sum(triggers) >= min_triggers + + +# ============================================================ +# 工具函数 +# ============================================================ + +def is_trading_day(td: date) -> bool: + """简单判断是否为交易日(周一~周五)""" + return td.weekday() < 5 # 0=周一, 4=周五 + + +def get_week_id(td: date) -> int: + """返回年内周序号(周一为起始)""" + return td.isocalendar()[1] + + +def seconds_to_target(target_hour: int, target_minute: int) -> float: + """计算从现在到目标时间(当天 target_hour:target_minute)的秒数。""" + now = datetime.now() + today_target = datetime(now.year, now.month, now.day, target_hour, target_minute, 0) + if now >= today_target: + # 今天已过,推到明天 + today_target += timedelta(days=1) + return (today_target - now).total_seconds() + + +# ============================================================ +# 股票池管理器 +# ============================================================ + +class PoolManager: + """ + 网格股票池自动管理器(阶段一:T日标记) + + 使用 threading.Timer 递归调度,实现每日 09:25 / 09:30 / 15:30 定时任务。 + 所有操作只写入数据库,不执行真实交易。 + """ + + def __init__(self): + self._thread: threading.Thread | None = None + self._stop_event = threading.Event() + + # 周度淘汰历史:key=股票代码,value=[(week_id, rank), ...] + # rank=0 表示在 Top50,rank=-1 表示不在 Top50 + self._top50_history: dict[str, list[tuple[int, int]]] = defaultdict(list) + + # 调试:手动触发时传入自定义日期(仅供测试用) + self._override_date: date | None = None + + # 加载历史排名数据(从 ScoringResult 重建) + self._rebuild_top50_history() + + # ---- 对外控制接口 ---- + + def start(self): + """启动后台管理线程(幂等)""" + if self._thread is not None and self._thread.is_alive(): + PrintLog(LogLevel.WARNING, '[PoolManager] 已启动,忽略重复调用') + return + self._stop_event.clear() + self._thread = threading.Thread(target=self._run_loop, daemon=True, name='PoolManager') + self._thread.start() + PrintLog(LogLevel.INFO, '[PoolManager] 已启动') + + def stop(self): + """停止后台管理线程""" + self._stop_event.set() + if self._thread is not None: + self._thread.join(timeout=5) + self._thread = None + PrintLog(LogLevel.INFO, '[PoolManager] 已停止') + + # ---- 每日定时任务 ---- + + def _run_loop(self): + """后台线程主循环:计算出距下次任务的时间,注册下一个 Timer""" + while not self._stop_event.is_set(): + now = datetime.now() + td = self._override_date or now.date() + + # 确定当天要执行的任务及距其的秒数 + delay, task_name = self._compute_next_delay(now, td) + + # 保证 Timer 不会太久(最多 24 小时),处理节假日顺延 + if delay <= 0: + delay = 60 # 异常时等 1 分钟重算 + + PrintLog(LogLevel.INFO, + f'[PoolManager] 计划任务 "{task_name}",{delay:.0f} 秒后执行') + + timer = threading.Timer(delay, self._execute_task, args=(task_name, td)) + timer.name = f'PoolManager-{task_name}' + timer.start() + + # 等待定时器完成或停止信号 + timer.join() + + if self._stop_event.is_set(): + break + + def _compute_next_delay(self, now: datetime, td: date) -> tuple[float, str]: + """ + 计算距下一个任务的时间和任务名称。 + 任务顺序:09:25 → 09:30 → 15:30 → (下一天 09:25) + """ + h, m = now.hour, now.minute + + if h < 9 or (h == 9 and m < 25): + # 现在在 09:25 之前 → 先执行 09:25 + return seconds_to_target(9, 25), '_sync_data' + elif h == 9 and 25 <= m < 30: + # 09:25~09:30 之间 → 立即执行 09:30 + return 0.0, '_run_scoring' + elif (h == 9 and m >= 30) or h < 15: + # 09:30 之后、15:30 之前 → 执行 15:30 + return seconds_to_target(15, 30), '_mark_slumber' + elif h >= 15: + # 15:30 之后 → 推到下一天 09:25 + delay = seconds_to_target(9, 25) + (td.weekday() < 4 and 1 or 3) * 86400 # 工作日+1,周末+3 + return delay, '_sync_data' + + # 默认兜底 + return seconds_to_target(9, 25), '_sync_data' + + def _execute_task(self, task_name: str, td: date): + """根据任务名执行对应任务""" + try: + if task_name == '_sync_data': + self._sync_data() + # 同步完成后自动调度评分 + self._run_scoring() + # 调度 15:30 + self._schedule_next(target_hour=15, target_minute=30, + task_name='_mark_slumber', td=td) + + elif task_name == '_run_scoring': + self._run_scoring() + # 评分完成后调度沉寂检测 + self._schedule_next(target_hour=15, target_minute=30, + task_name='_mark_slumber', td=td) + + elif task_name == '_mark_slumber': + self._mark_slumber(td) + # 如果是周五,额外调度淘汰标记 + if td.weekday() == 4: # 周五 + self._mark_weekly_elim(td) + # 调度下一天 09:25 + self._schedule_next(target_hour=9, target_minute=25, + task_name='_sync_data', td=td) + except Exception as e: + PrintLog(LogLevel.ERROR, f'[PoolManager] 任务 {task_name} 执行异常: {e}') + + def _schedule_next(self, target_hour: int, target_minute: int, + task_name: str, td: date): + """注册一个 Timer,在指定时间执行 task_name""" + # 计算 delay + delay = seconds_to_target(target_hour, target_minute) + # 如果 target 在过去(如周末顺延),加 1 天 + if delay <= 0: + delay += 86400 + + def wrapper(): + now = datetime.now() + # 重新计算真实日期 + exec_td = self._override_date or now.date() + self._execute_task(task_name, exec_td) + + timer = threading.Timer(delay, wrapper, name=f'PoolManager-sched-{task_name}') + timer.start() + + # ---- 任务实现 ---- + + def _sync_data(self): + """09:25 — 同步 K 线数据""" + PrintLog(LogLevel.INFO, '[PoolManager] 开始同步K线数据...') + try: + from core.scoring.sync.kline_sync import KlineStockSync + syncer = KlineStockSync() + syncer.run() + PrintLog(LogLevel.INFO, '[PoolManager] K线数据同步完成') + except Exception as e: + PrintLog(LogLevel.ERROR, f'[PoolManager] K线同步失败: {e}') + + def _run_scoring(self): + """09:30 — 执行评分""" + PrintLog(LogLevel.INFO, '[PoolManager] 开始执行评分...') + try: + from core.scoring.inference.scorer import GridSeekerPipeline + pipeline = GridSeekerPipeline() + td = self._override_date or date.today() + result = pipeline.run(trade_date=td) + if not result.empty: + pipeline.persist(result, trade_date=td) + PrintLog(LogLevel.INFO, f'[PoolManager] 评分完成,写入 {len(result)} 条结果') + # 触发 UI 刷新 + event_bus.publish(EventPoolMark, { + 'action': 'scoring', + 'stock_codes': [], + 'count': len(result), + }) + else: + PrintLog(LogLevel.WARNING, '[PoolManager] 评分无结果') + except Exception as e: + PrintLog(LogLevel.ERROR, f'[PoolManager] 评分失败: {e}') + + def _mark_slumber(self, td: date): + """15:30 — 沉寂检测,标记待卖出股""" + if not is_trading_day(td): + PrintLog(LogLevel.INFO, '[PoolManager] 非交易日,跳过沉寂检测') + return + + PrintLog(LogLevel.INFO, '[PoolManager] 开始沉寂检测...') + + # 获取所有 enabled=True 的持仓 + targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True) + if not targets: + PrintLog(LogLevel.INFO, '[PoolManager] 无持仓,跳过沉寂检测') + return + + marked: list[str] = [] + today_str = td.strftime('%Y-%m-%d') + + for tgt in targets: + try: + # 读取该股 K 线数据(从本地 SQLite KlineStock 表) + from core.scoring.models import KlineStock + klines = list(KlineStock + .select() + .where(KlineStock.stock_code == tgt.stock_code) + .order_by(KlineStock.trade_date) + .dicts()) + + if not klines or len(klines) < 60: + continue + + df = pd.DataFrame(klines) + if 'trade_date' in df.columns and not pd.api.types.is_datetime64_any_dtype(df['trade_date']): + df['trade_date'] = pd.to_datetime(df['trade_date']) + df = df.set_index('trade_date').sort_index() + + if is_slumbering(df): + self._write_pending(td, 'liquidate', tgt.stock_code, + reason=f'沉寂检测触发({today_str})') + marked.append(tgt.stock_code) + PrintLog(LogLevel.INFO, + f'[PoolManager] 沉寂标记: {tgt.stock_code}') + except Exception as e: + PrintLog(LogLevel.WARNING, + f'[PoolManager] 沉寂检测异常 {tgt.stock_code}: {e}') + + if marked: + from core.sfgrid.bus import event_bus + event_bus.publish(EventPoolMark, { + 'action': 'liquidate', + 'stock_codes': marked, + 'count': len(marked), + }) + PrintLog(LogLevel.INFO, + f'[PoolManager] 沉寂检测完成,标记 {len(marked)} 只股') + else: + PrintLog(LogLevel.INFO, '[PoolManager] 沉寂检测完成,无股触发') + + def _mark_weekly_elim(self, td: date): + """周五 15:30 — 周度评分淘汰标记(连续2周不在Top50则卖出)""" + if not is_trading_day(td): + return + + PrintLog(LogLevel.INFO, '[PoolManager] 开始周度淘汰检测...') + + # 获取最近 N 周的评分排名 + week_id = get_week_id(td) + today_str = td.strftime('%Y-%m-%d') + + # 查今日 Top50 + top50_codes: Set[str] = set() + rows = (ScoringResult + .select(ScoringResult.stock_code) + .where(ScoringResult.trade_date == td) + .where(ScoringResult.score_rank <= TOP_MODEL_N) + .dicts()) + for r in rows: + top50_codes.add(r['stock_code']) + + if not top50_codes: + PrintLog(LogLevel.WARNING, '[PoolManager] 今日无评分数据,无法进行淘汰检测') + return + + # 更新历史排名 + targets = SFGridTradeTarget.select().where(SFGridTradeTarget.enabled == True) + for tgt in targets: + code = tgt.stock_code + rank_in_top50 = 0 if code in top50_codes else -1 + self._top50_history[code].append((week_id, rank_in_top50)) + # 只保留近 8 周记录,防止内存膨胀 + if len(self._top50_history[code]) > 8: + self._top50_history[code] = self._top50_history[code][-8:] + + # 检测连续 N 周不在 Top50 的股 + marked: list[str] = [] + for tgt in targets: + code = tgt.stock_code + hist = self._top50_history.get(code, []) + if len(hist) < ELIM_WINDOW: + continue + recent = hist[-ELIM_WINDOW:] + if all(rank == -1 for _, rank in recent): + self._write_pending(td, 'eliminate', code, + reason=f'连续{ELIM_WINDOW}周不在Top50({today_str})') + marked.append(code) + PrintLog(LogLevel.INFO, + f'[PoolManager] 淘汰标记: {code},历史: {recent}') + + if marked: + from core.sfgrid.bus import event_bus + event_bus.publish(EventPoolMark, { + 'action': 'eliminate', + 'stock_codes': marked, + 'count': len(marked), + }) + PrintLog(LogLevel.INFO, + f'[PoolManager] 周度淘汰检测完成,标记 {len(marked)} 只股') + else: + PrintLog(LogLevel.INFO, '[PoolManager] 周度淘汰检测完成,无股触发') + + def _write_pending(self, td: date, action_type: str, stock_code: str, reason: str = ''): + """写入 pending_pool_actions 表(幂等)""" + try: + PendingPoolAction.insert( + action_date=td, + action_type=action_type, + stock_code=stock_code, + reason=reason, + ).on_conflict_replace().execute() + except Exception as e: + PrintLog(LogLevel.WARNING, + f'[PoolManager] 写入待处理操作失败: {e}') + + def _rebuild_top50_history(self): + """启动时从 ScoringResult 表重建 _top50_history(用于淘汰判断)""" + try: + rows = (ScoringResult + .select(ScoringResult.stock_code, ScoringResult.trade_date, + ScoringResult.score_rank) + .where(ScoringResult.score_rank <= TOP_MODEL_N) + .order_by(ScoringResult.trade_date) + .dicts()) + + week_groups: dict = defaultdict(list) + for r in rows: + td = r['trade_date'] + if isinstance(td, str): + td = datetime.strptime(td, '%Y-%m-%d').date() + week_id = get_week_id(td) + week_groups[(r['stock_code'], week_id)].append(r['score_rank']) + + # 取每周最新一条(排名最靠前的) + for (code, week_id), ranks in week_groups.items(): + best_rank = min(ranks) + rank_val = 0 if best_rank <= TOP_MODEL_N else -1 + self._top50_history[code].append((week_id, rank_val)) + + # 去重,每 week_id 只留一条 + for code in self._top50_history: + seen = set() + cleaned = [] + for w, r in self._top50_history[code]: + if w not in seen: + seen.add(w) + cleaned.append((w, r)) + self._top50_history[code] = cleaned + + PrintLog(LogLevel.INFO, + f'[PoolManager] 历史排名已重建,{len(self._top50_history)} 只股有历史数据') + except Exception as e: + PrintLog(LogLevel.ERROR, f'[PoolManager] 重建历史排名失败: {e}') + + # ---- 手动触发(供 UI 调试按钮调用)---- + + def trigger_sync(self): + """手动触发数据同步""" + threading.Thread(target=self._sync_data, daemon=True).start() + + def trigger_scoring(self): + """手动触发评分""" + threading.Thread(target=self._run_scoring, daemon=True).start() + + def trigger_slumber(self, td: date | None = None): + """手动触发沉寂检测""" + td = td or (self._override_date or date.today()) + threading.Thread(target=self._mark_slumber, args=(td,), daemon=True).start() + + def trigger_weekly_elim(self, td: date | None = None): + """手动触发周度淘汰检测""" + td = td or (self._override_date or date.today()) + threading.Thread(target=self._mark_weekly_elim, args=(td,), daemon=True).start() \ No newline at end of file diff --git a/core/ui/flet/app_v2.py b/core/ui/flet/app_v2.py index df18717..dd98071 100644 --- a/core/ui/flet/app_v2.py +++ b/core/ui/flet/app_v2.py @@ -17,7 +17,7 @@ from core.logger import LogLevel, PrintLog from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID from core.sfgrid.sfgrid_strategy import SFGridStrategy from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch -from core.sfgrid.bus_events import EventTradeTargetUpdate +from core.sfgrid.bus_events import EventTradeTargetUpdate, EventPoolMark # ── 工具函数 ── _ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤', @@ -78,6 +78,7 @@ class _DataStore: self.monitorPrice: float = 10.0 self.marketLog: dict[str, dict] = {} # stock_code → {name, price, time} self.listeningStocks: list = [] + self.poolLog: list[str] = [] # 股票池管理器日志(list,方便 append) # ── 初始化 ── @@ -903,6 +904,9 @@ class _DrawerPanel: ft.Container(ft.Divider(height=20), width=2), ft.ElevatedButton("同步数据", on_click=lambda e: self._run_sync(['kline', 'stocks', 'industry', 'market', 'sector']), height=32), ft.ElevatedButton("执行评分", on_click=lambda e: self._run_scoring(), height=32), + ft.Container(ft.Divider(height=20), width=2), + ft.ElevatedButton("沉寂检测", on_click=lambda e: self._data._pool_manager.trigger_slumber(), height=32, tooltip="手动触发沉寂检测(阶段一标记)"), + ft.ElevatedButton("淘汰检测", on_click=lambda e: self._data._pool_manager.trigger_weekly_elim(), height=32, tooltip="手动触发周度淘汰检测(阶段一标记)"), self._score_status, ], spacing=6, vertical_alignment=ft.CrossAxisAlignment.CENTER) return ft.Column([ @@ -1329,8 +1333,14 @@ class QmtApp: event_bus.subscribe(MarketDataUpdate, self._on_market_data) event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active) event_bus.subscribe(EventTradeTargetUpdate, lambda t: self._refresh_ui()) + event_bus.subscribe(EventPoolMark, self._on_pool_mark) threading.Thread(target=self._refresh_loop, daemon=True).start() + # 启动股票池管理器(阶段一:T日标记) + from core.sfgrid.pool_manager import PoolManager + self._data._pool_manager = PoolManager() + self._data._pool_manager.start() + def _show_error(self, msg: str): self.page.clean() self.page.add(ft.Container( @@ -1434,6 +1444,36 @@ class QmtApp: self._prices_loaded = True self._refresh_ui() + def _on_pool_mark(self, data: dict): + """响应股票池标记事件,在日志区显示""" + action = data.get('action', '') + count = data.get('count', 0) + codes = data.get('stock_codes', []) + if action == 'scoring': + msg = f'[池] 评分完成,共 {count} 只候选' + elif action == 'liquidate': + msg = f'[池] 沉寂检测完成,标记 {count} 只股: {", ".join(codes)}' + elif action == 'eliminate': + msg = f'[池] 周度淘汰检测完成,标记 {count} 只股: {", ".join(codes)}' + else: + msg = f'[池] 未知事件: {data}' + self._data.poolLog.append(msg) + # 追加到 UI 日志 + if hasattr(self, '_pool_log_text') and self._pool_log_text: + log = self._pool_log_text + log.value = '\n'.join(self._data.poolLog[-200:]) if self._data.poolLog else '' + log.update() + from core.logger import PrintLog, LogLevel + PrintLog(LogLevel.INFO, msg) + + def trigger_pool_slumber(self): + """供 UI 按钮手动触发沉寂检测""" + self._data._pool_manager.trigger_slumber() + + def trigger_pool_elim(self): + """供 UI 按钮手动触淘汰检测""" + self._data._pool_manager.trigger_weekly_elim() + def _on_market_active(self, is_active: bool): from core.logger import PrintLog, LogLevel PrintLog(LogLevel.INFO, f'[UI] 收到市场状态切换事件 is_active={is_active}') diff --git a/models/reference_backtest_logic/strategy.py b/models/reference_backtest_logic/ref_backtest_strategy.py similarity index 100% rename from models/reference_backtest_logic/strategy.py rename to models/reference_backtest_logic/ref_backtest_strategy.py