소스 검색

feat(MAPatternStrategy): 增强均线形态交易策略功能

- 新增持仓记录保存至CSV文件的功能,便于后续分析
- 更新开仓函数以记录穿越均线的信息,提升策略透明度
maxfeng 1 개월 전
부모
커밋
b086e6cc55
4개의 변경된 파일1863개의 추가작업 그리고 5개의 파일을 삭제
  1. 94 5
      Lib/future/MAPatternStrategy_v002.py
  2. 1617 0
      Lib/future/MAPatternStrategy_v002_bak.py
  3. 1 0
      pyproject.toml
  4. 151 0
      uv.lock

+ 94 - 5
Lib/future/MAPatternStrategy_v002.py

@@ -206,7 +206,7 @@ def initialize(context):
     
     # 策略品种选择策略配置
     # 方案1:全品种策略 - 考虑所有配置的期货品种
-    g.strategy_focus_symbols = ['P']  # 空列表表示考虑所有品种
+    g.strategy_focus_symbols = []  # 空列表表示考虑所有品种
     
     # 方案2:精选品种策略 - 只交易流动性较好的特定品种(如需使用请取消下行注释)
     # g.strategy_focus_symbols = ['RM', 'CJ', 'CY', 'JD', 'L', 'LC', 'SF', 'SI']
@@ -644,7 +644,7 @@ def check_open_and_stop(context):
                     
                     if target_hands > 0:
                         success = open_position(context, dominant_future, target_hands, direction, single_hand_margin,
-                                              f'均线形态开仓')
+                                              f'均线形态开仓', crossed_ma_details=score_details)
                         if success:
                             log.info(f"  ✓✓ {symbol} 开仓成功,从候选列表移除")
                             candidates_to_remove.append(dominant_future)
@@ -1152,7 +1152,7 @@ def check_historical_ma_pattern_consistency(historical_data, direction, lookback
 
 ############################ 交易执行函数 ###################################
 
-def open_position(context, security, target_hands, direction, single_hand_margin, reason=''):
+def open_position(context, security, target_hands, direction, single_hand_margin, reason='', crossed_ma_details=None):
     """开仓"""
     try:
         # 记录交易前的可用资金
@@ -1203,6 +1203,12 @@ def open_position(context, security, target_hands, direction, single_hand_margin
             
             # 记录交易信息
             entry_trading_day = get_current_trading_day(context.current_dt)
+            
+            # 处理穿越均线信息
+            crossed_ma_lines = []
+            if crossed_ma_details:
+                crossed_ma_lines = [f"MA{detail['period']}" for detail in crossed_ma_details if detail.get('delta', 0) > 0]
+            
             g.trade_history[security] = {
                 'entry_price': order_price,
                 'target_hands': target_hands,
@@ -1210,7 +1216,8 @@ def open_position(context, security, target_hands, direction, single_hand_margin
                 'actual_margin': margin_change,
                 'direction': direction,
                 'entry_time': context.current_dt,
-                'entry_trading_day': entry_trading_day
+                'entry_trading_day': entry_trading_day,
+                'crossed_ma_lines': crossed_ma_lines  # 记录穿越的均线
             }
 
             ma_trailing_enabled = True
@@ -1226,8 +1233,10 @@ def open_position(context, security, target_hands, direction, single_hand_margin
 
             g.trade_history[security]['ma_trailing_enabled'] = ma_trailing_enabled
             
+            crossed_ma_str = ', '.join(crossed_ma_lines) if crossed_ma_lines else '无'
             log.info(f"开仓成功: {security} {direction} {order_amount}手 @{order_price:.2f}, "
-                    f"保证金: {margin_change:.0f}, 资金变化: {cash_change:.0f}, 原因: {reason}")
+                    f"保证金: {margin_change:.0f}, 资金变化: {cash_change:.0f}, 原因: {reason}, "
+                    f"穿越均线: {crossed_ma_str}")
             
             return True
             
@@ -1399,6 +1408,83 @@ def calculate_realtime_ma_values(security, ma_periods):
     ma_values = {f'ma{period}': sum(close_prices[-period:]) / period for period in ma_periods}
     return ma_values
 
+def save_today_new_positions_to_csv(context):
+    """将当天新增的持仓记录追加保存到CSV文件"""
+    log.info(f"保存当天新增的持仓记录到CSV文件")
+    if not g.trade_history:
+        return
+    
+    current_trading_day = normalize_trade_day_value(get_current_trading_day(context.current_dt))
+    
+    # 筛选当天新开仓的记录
+    today_new_positions = []
+    for security, trade_info in g.trade_history.items():
+        entry_trading_day = normalize_trade_day_value(trade_info.get('entry_trading_day'))
+        if entry_trading_day == current_trading_day:
+            underlying_symbol = security.split('.')[0][:-4]
+            crossed_ma_lines = trade_info.get('crossed_ma_lines', [])
+            # 使用分号分隔多条均线,避免CSV格式问题
+            crossed_ma_str = ';'.join(crossed_ma_lines) if crossed_ma_lines else ''
+            
+            today_new_positions.append({
+                'security': security,
+                'underlying_symbol': underlying_symbol,
+                'direction': trade_info['direction'],
+                'entry_price': trade_info['entry_price'],
+                'actual_hands': trade_info['actual_hands'],
+                'actual_margin': trade_info['actual_margin'],
+                'entry_time': trade_info['entry_time'].strftime('%H:%M:%S'),  # 只保留时间
+                'entry_trading_day': str(entry_trading_day),
+                'crossed_ma_lines': crossed_ma_str
+            })
+    
+    # 如果没有当天新增的记录,直接返回
+    if not today_new_positions:
+        log.debug("当天无新增持仓记录,跳过保存")
+        return
+    
+    try:
+        filename = 'trade_history.csv'
+        
+        # 尝试读取现有文件
+        existing_content = ''
+        file_exists = False
+        try:
+            existing_content_bytes = read_file(filename)
+            if existing_content_bytes:
+                existing_content = existing_content_bytes.decode('utf-8')
+                file_exists = True
+        except:
+            pass
+        
+        # 准备CSV内容
+        csv_lines = []
+        
+        # 如果文件不存在,添加表头
+        if not file_exists:
+            header = 'security,underlying_symbol,direction,entry_price,actual_hands,actual_margin,entry_time,entry_trading_day,crossed_ma_lines'
+            csv_lines.append(header)
+        
+        # 添加新记录
+        for pos in today_new_positions:
+            line = f"{pos['security']},{pos['underlying_symbol']},{pos['direction']},{pos['entry_price']:.2f},{pos['actual_hands']},{pos['actual_margin']:.2f},{pos['entry_time']},{pos['entry_trading_day']},{pos['crossed_ma_lines']}"
+            csv_lines.append(line)
+        
+        # 合并内容:现有内容 + 新内容
+        new_content = '\n'.join(csv_lines)
+        if file_exists:
+            final_content = existing_content.rstrip('\n') + '\n' + new_content
+        else:
+            final_content = new_content
+        
+        # 写入文件 - 将字符串编码为bytes以符合JoinQuant API要求
+        write_file(filename, final_content.encode('utf-8'))
+        
+        log.info(f"已追加{len(today_new_positions)}条当天新增持仓记录到文件: {filename}")
+        
+    except Exception as e:
+        log.warning(f"保存持仓记录到CSV文件时出错: {str(e)}")
+
 def sync_trade_history_with_positions(context):
     """同步g.trade_history与实际持仓,清理已平仓但记录未删除的持仓"""
     if not g.trade_history:
@@ -1431,6 +1517,9 @@ def after_market_close(context):
     """收盘后运行函数"""
     log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time())))
     
+    # 保存当天新增的持仓记录到CSV文件
+    save_today_new_positions_to_csv(context)
+    
     # 同步检查:清理g.trade_history中已平仓但记录未删除的持仓
     sync_trade_history_with_positions(context)
     

+ 1617 - 0
Lib/future/MAPatternStrategy_v002_bak.py

@@ -0,0 +1,1617 @@
+# 导入函数库
+from jqdata import *
+from jqdata import finance
+import pandas as pd
+import numpy as np
+import math
+from datetime import date, datetime, timedelta, time
+import re
+
+# 顺势交易策略 v001
+# 基于均线走势(前提条件)+ K线形态(开盘价差、当天价差)的期货交易策略
+#
+# 核心逻辑:
+# 1. 开盘时检查均线走势(MA30<=MA20<=MA10<=MA5为多头,反之为空头)
+# 2. 检查开盘价差是否符合方向要求(多头>=0.5%,空头<=-0.5%)
+# 3. 14:35和14:55检查当天价差(多头>0,空头<0),满足条件则开仓
+# 4. 应用固定止损和动态追踪止盈
+# 5. 自动换月移仓
+
+# 设置以便完整打印 DataFrame
+pd.set_option('display.max_rows', None)
+pd.set_option('display.max_columns', None)
+pd.set_option('display.width', None)
+pd.set_option('display.max_colwidth', 20)
+
+## 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 输出内容到日志
+    log.info('=' * 60)
+    log.info('均线形态交易策略 v001 初始化开始')
+    log.info('策略类型: 均线走势 + K线形态')
+    log.info('=' * 60)
+
+    ### 期货相关设定 ###
+    # 设定账户为金融账户
+    set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash, type='index_futures')])
+    # 期货类每笔交易时的手续费是: 买入时万分之0.23,卖出时万分之0.23,平今仓为万分之23
+    set_order_cost(OrderCost(open_commission=0.000023, close_commission=0.000023, close_today_commission=0.0023), type='index_futures')
+    
+    # 设置期货交易的滑点
+    set_slippage(StepRelatedSlippage(2))
+    
+    # 初始化全局变量
+    g.usage_percentage = 0.8  # 最大资金使用比例
+    g.max_margin_per_position = 30000  # 单个标的最大持仓保证金(元)
+    
+    # 均线策略参数
+    g.ma_periods = [5, 10, 20, 30]  # 均线周期
+    g.ma_historical_days = 60  # 获取历史数据天数(确保足够计算MA30)
+    g.ma_open_gap_threshold = 0.001  # 方案1开盘价差阈值(0.2%)
+    g.ma_pattern_lookback_days = 10  # 历史均线模式一致性检查的天数
+    g.ma_pattern_consistency_threshold = 0.8  # 历史均线模式一致性阈值(80%)
+    g.check_intraday_spread = False  # 是否检查日内价差(True: 检查, False: 跳过)
+    g.ma_proximity_min_threshold = 8  # MA5与MA10贴近计数和的最低阈值
+    g.ma_pattern_extreme_days_threshold = 4  # 极端趋势天数阈值
+    g.ma_distribution_lookback_days = 5  # MA5分布过滤回溯天数
+    g.ma_distribution_min_ratio = 0.4  # MA5分布满足比例阈值
+    g.enable_ma_distribution_filter = True  # 是否启用MA5分布过滤
+    g.ma_cross_threshold = 1  # 均线穿越数量阈值
+    g.enable_open_gap_filter = True  # 是否启用开盘价差过滤
+    
+    # 均线价差策略方案选择
+    g.ma_gap_strategy_mode = 3  # 策略模式选择(1: 原方案, 2: 新方案, 3: 方案3)
+    g.ma_open_gap_threshold2 = 0.001  # 方案2开盘价差阈值(0.2%)
+    g.ma_intraday_threshold_scheme2 = 0.005  # 方案2日内变化阈值(0.5%)
+    
+    # 止损止盈策略参数
+    g.fixed_stop_loss_rate = 0.01  # 固定止损比率(1%)
+    g.ma_offset_ratio_normal = 0.003  # 均线跟踪止盈常规偏移量(0.3%)
+    g.ma_offset_ratio_close = 0.01  # 均线跟踪止盈收盘前偏移量(1%)
+    g.days_for_adjustment = 4  # 持仓天数调整阈值
+    
+    # 输出策略参数
+    log.info("均线形态策略参数:")
+    log.info(f"  均线周期: {g.ma_periods}")
+    log.info(f"  策略模式: 方案{g.ma_gap_strategy_mode}")
+    log.info(f"  方案1开盘价差阈值: {g.ma_open_gap_threshold:.1%}")
+    log.info(f"  方案2开盘价差阈值: {g.ma_open_gap_threshold2:.1%}")
+    log.info(f"  方案2日内变化阈值: {g.ma_intraday_threshold_scheme2:.1%}")
+    log.info(f"  历史均线模式检查天数: {g.ma_pattern_lookback_days}天")
+    log.info(f"  历史均线模式一致性阈值: {g.ma_pattern_consistency_threshold:.1%}")
+    log.info(f"  极端趋势天数阈值: {g.ma_pattern_extreme_days_threshold}")
+    log.info(f"  均线贴近计数阈值: {g.ma_proximity_min_threshold}")
+    log.info(f"  MA5分布过滤天数: {g.ma_distribution_lookback_days}")
+    log.info(f"  MA5分布最低比例: {g.ma_distribution_min_ratio:.0%}")
+    log.info(f"  启用MA5分布过滤: {g.enable_ma_distribution_filter}")
+    log.info(f"  是否检查日内价差: {g.check_intraday_spread}")
+    log.info(f"  均线穿越阈值: {g.ma_cross_threshold}")
+    log.info(f"  是否启用开盘价差过滤: {g.enable_open_gap_filter}")
+    log.info(f"  固定止损: {g.fixed_stop_loss_rate:.1%}")
+    log.info(f"  均线跟踪止盈常规偏移: {g.ma_offset_ratio_normal:.1%}")
+    log.info(f"  均线跟踪止盈收盘前偏移: {g.ma_offset_ratio_close:.1%}")
+    log.info(f"  持仓天数调整阈值: {g.days_for_adjustment}天")
+    
+    # 期货品种完整配置字典
+    g.futures_config = {
+        # 贵金属
+        'AU': {'has_night_session': True, 'margin_rate': {'long': 0.21, 'short': 0.21}, 'multiplier': 1000, 'trading_start_time': '21:00'},
+        'AG': {'has_night_session': True, 'margin_rate': {'long': 0.22, 'short': 0.22}, 'multiplier': 15, 'trading_start_time': '21:00'},
+        
+        # 有色金属
+        'CU': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'AL': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'ZN': {'has_night_session': True, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'PB': {'has_night_session': True, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'NI': {'has_night_session': True, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 1, 'trading_start_time': '21:00'},
+        'SN': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 1, 'trading_start_time': '21:00'},
+        'SS': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        
+        # 黑色系
+        'RB': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'HC': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'I': {'has_night_session': True, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 100, 'trading_start_time': '21:00'},
+        'JM': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 100, 'trading_start_time': '21:00'},
+        'J': {'has_night_session': True, 'margin_rate': {'long': 0.25, 'short': 0.25}, 'multiplier': 60, 'trading_start_time': '21:00'},
+        
+        # 能源化工
+        'SP': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'FU': {'has_night_session': True, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'BU': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'RU': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'BR': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'SC': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 1000, 'trading_start_time': '21:00'},
+        'NR': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'LU': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'LC': {'has_night_session': False, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 1, 'trading_start_time': '09:00'},
+        
+        # 化工
+        'FG': {'has_night_session': True, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 20, 'trading_start_time': '21:00'},
+        'TA': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'MA': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'SA': {'has_night_session': True, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 20, 'trading_start_time': '21:00'},
+        'L': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'V': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'EG': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'PP': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'EB': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'PG': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 20, 'trading_start_time': '21:00'},
+        'PX': {'has_night_session': True, 'margin_rate': {'long': 0.1, 'short': 0.1}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        
+        # 农产品
+        'RM': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'OI': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'CF': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'SR': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'PF': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'C': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'CS': {'has_night_session': True, 'margin_rate': {'long': 0.11, 'short': 0.11}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'CY': {'has_night_session': True, 'margin_rate': {'long': 0.11, 'short': 0.11}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        'A': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'B': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'M': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'Y': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'P': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        
+        # 无夜盘品种
+        'IF': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 300, 'trading_start_time': '09:30'},
+        'IH': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 300, 'trading_start_time': '09:30'},
+        'IC': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 200, 'trading_start_time': '09:30'},
+        'IM': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 200, 'trading_start_time': '09:30'},
+        'AP': {'has_night_session': False, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 10, 'trading_start_time': '09:00'},
+        'CJ': {'has_night_session': False, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 5, 'trading_start_time': '09:00'},
+        'PK': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5, 'trading_start_time': '09:00'},
+        'JD': {'has_night_session': False, 'margin_rate': {'long': 0.11, 'short': 0.11}, 'multiplier': 10, 'trading_start_time': '09:00'},
+        'LH': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 16, 'trading_start_time': '09:00'},
+        'T': {'has_night_session': False, 'margin_rate': {'long': 0.03, 'short': 0.03}, 'multiplier': 1000000, 'trading_start_time': '09:30'},
+        'PS': {'has_night_session': False, 'margin_rate': {'long': 0.16, 'short': 0.16}, 'multiplier': 3, 'trading_start_time': '09:00'},
+        'UR': {'has_night_session': False, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 20, 'trading_start_time': '09:00'},
+        'MO': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 100, 'trading_start_time': '21:00'},
+        # 'LF': {'has_night_session': False, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 1, 'trading_start_time': '09:30'},
+        'HO': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 100, 'trading_start_time': '09:30'},
+        # 'LR': {'has_night_session': True, 'margin_rate': {'long': 0.21, 'short': 0.21}, 'multiplier': 20, 'trading_start_time': '21:00'},
+        'LG': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 90, 'trading_start_time': '21:00'},
+        # 'FB': {'has_night_session': True, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        # 'PM': {'has_night_session': True, 'margin_rate': {'long': 0.2, 'short': 0.2}, 'multiplier': 50, 'trading_start_time': '21:00'},
+        'EC': {'has_night_session': False, 'margin_rate': {'long': 0.23, 'short': 0.23}, 'multiplier': 50, 'trading_start_time': '09:00'},
+        # 'RR': {'has_night_session': True, 'margin_rate': {'long': 0.11, 'short': 0.11}, 'multiplier': 10, 'trading_start_time': '21:00'},
+        'OP': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 40, 'trading_start_time': '09:00'},
+        # 'IO': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 1, 'trading_start_time': '21:00'},
+        'BC': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '21:00'},
+        # 'WH': {'has_night_session': False, 'margin_rate': {'long': 0.2, 'short': 0.2}, 'multiplier': 20, 'trading_start_time': '09:00'},
+        'SH': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 30, 'trading_start_time': '21:00'},
+        # 'RI': {'has_night_session': False, 'margin_rate': {'long': 0.21, 'short': 0.21}, 'multiplier': 20, 'trading_start_time': '09:00'},
+        'TS': {'has_night_session': False, 'margin_rate': {'long': 0.015, 'short': 0.015}, 'multiplier': 2000000, 'trading_start_time': '09:30'},
+        # 'JR': {'has_night_session': False, 'margin_rate': {'long': 0.21, 'short': 0.21}, 'multiplier': 20, 'trading_start_time': '09:00'},
+        'AD': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10, 'trading_start_time': '09:00'},
+        # 'BB': {'has_night_session': False, 'margin_rate': {'long': 0.19, 'short': 0.19}, 'multiplier': 500, 'trading_start_time': '09:00'},
+        'PL': {'has_night_session': False, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 20, 'trading_start_time': '09:00'},
+        # 'RS': {'has_night_session': False, 'margin_rate': {'long': 0.26, 'short': 0.26}, 'multiplier': 10, 'trading_start_time': '09:00'},
+        'SI': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '09:00'},
+        # 'ZC': {'has_night_session': True, 'margin_rate': {'long': 0.56, 'short': 0.56}, 'multiplier': 100, 'trading_start_time': '21:00'},
+        'SM': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 5, 'trading_start_time': '09:00'},
+        'AO': {'has_night_session': True, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 20, 'trading_start_time': '21:00'},
+        'TL': {'has_night_session': False, 'margin_rate': {'long': 0.045, 'short': 0.045}, 'multiplier': 1000000, 'trading_start_time': '09:00'},
+        'SF': {'has_night_session': False, 'margin_rate': {'long': 0.14, 'short': 0.14}, 'multiplier': 5, 'trading_start_time': '09:00'},
+        # 'WR': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 10, 'trading_start_time': '09:00'},
+        'PR': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 15, 'trading_start_time': '21:00'},
+        'TF': {'has_night_session': False, 'margin_rate': {'long': 0.022, 'short': 0.022}, 'multiplier': 1000000, 'trading_start_time': '09:00'},
+        # 'VF': {'has_night_session': False, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 1, 'trading_start_time': '09:00'},
+        'BZ': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 30, 'trading_start_time': '09:00'},
+    }
+    
+    # 策略品种选择策略配置
+    # 方案1:全品种策略 - 考虑所有配置的期货品种
+    g.strategy_focus_symbols = []  # 空列表表示考虑所有品种
+    
+    # 方案2:精选品种策略 - 只交易流动性较好的特定品种(如需使用请取消下行注释)
+    # g.strategy_focus_symbols = ['RM', 'CJ', 'CY', 'JD', 'L', 'LC', 'SF', 'SI']
+    
+    log.info(f"品种选择策略: {'全品种策略(覆盖所有配置品种)' if not g.strategy_focus_symbols else '精选品种策略(' + str(len(g.strategy_focus_symbols)) + '个品种)'}")
+    
+    # 交易记录和数据存储
+    g.trade_history = {}  # 持仓记录 {symbol: {'entry_price': xxx, 'direction': xxx, ...}}
+    g.daily_ma_candidates = {}  # 通过均线和开盘价差检查的候选品种 {symbol: {'direction': 'long'/'short', 'open_price': xxx, ...}}
+    g.today_trades = []  # 当日交易记录
+    g.excluded_contracts = {}  # 每日排除的合约缓存 {dominant_future: {'reason': 'ma_trend'/'open_gap', 'trading_day': xxx}}
+    g.ma_checked_underlyings = {}  # 记录各品种在交易日的均线检查状态 {symbol: trading_day}
+    g.last_ma_trading_day = None  # 最近一次均线检查所属交易日
+    
+    # 夜盘禁止操作标志
+    g.night_session_blocked = False  # 标记是否禁止当晚操作
+    g.night_session_blocked_trading_day = None  # 记录被禁止的交易日
+    
+    # 定时任务设置
+    # 夜盘开始(21:05) - 均线和开盘价差检查
+    run_daily(check_ma_trend_and_open_gap, time='21:05:00', reference_security='IF1808.CCFX')
+    
+    # 日盘开始 - 均线和开盘价差检查
+    run_daily(check_ma_trend_and_open_gap, time='09:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_ma_trend_and_open_gap, time='09:35:00', reference_security='IF1808.CCFX')
+    
+    # 夜盘开仓和止损止盈检查
+    run_daily(check_open_and_stop, time='21:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='21:35:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='22:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='22:35:00', reference_security='IF1808.CCFX')
+    
+    # 日盘开仓和止损止盈检查
+    run_daily(check_open_and_stop, time='09:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='09:35:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='10:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='10:35:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='11:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='11:25:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='13:35:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='14:05:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='14:35:00', reference_security='IF1808.CCFX')
+    run_daily(check_open_and_stop, time='14:55:00', reference_security='IF1808.CCFX')
+    run_daily(check_ma_trailing_reactivation, time='14:55:00', reference_security='IF1808.CCFX')
+    
+    # 收盘后
+    run_daily(after_market_close, time='15:30:00', reference_security='IF1808.CCFX')
+    
+    log.info('=' * 60)
+
+############################ 主程序执行函数 ###################################
+
+def get_current_trading_day(current_dt):
+    """根据当前时间推断对应的期货交易日"""
+    current_date = current_dt.date()
+    current_time = current_dt.time()
+
+    trade_days = get_trade_days(end_date=current_date, count=1)
+    if trade_days and trade_days[0] == current_date:
+        trading_day = current_date
+    else:
+        next_days = get_trade_days(start_date=current_date, count=1)
+        trading_day = next_days[0] if next_days else current_date
+
+    if current_time >= time(20, 59):
+        next_trade_days = get_trade_days(start_date=trading_day, count=2)
+        if len(next_trade_days) >= 2:
+            return next_trade_days[1]
+        if len(next_trade_days) == 1:
+            return next_trade_days[0]
+    return trading_day
+
+
+def normalize_trade_day_value(value):
+    """将交易日对象统一转换为 datetime.date"""
+    if isinstance(value, date) and not isinstance(value, datetime):
+        return value
+    if isinstance(value, datetime):
+        return value.date()
+    if hasattr(value, 'to_pydatetime'):
+        return value.to_pydatetime().date()
+    try:
+        return pd.Timestamp(value).date()
+    except Exception:
+        return value
+
+
+def check_ma_trend_and_open_gap(context):
+    """阶段一:开盘时均线走势和开盘价差检查(一天一次)"""
+    log.info("=" * 60)
+    current_trading_day = get_current_trading_day(context.current_dt)
+    log.info(f"执行均线走势和开盘价差检查 - 时间: {context.current_dt}, 交易日: {current_trading_day}")
+    log.info("=" * 60)
+    
+    # 换月移仓检查(在所有部分之前)
+    position_auto_switch(context)
+    
+    # ==================== 第一部分:基础数据获取 ====================
+    
+    # 步骤1:交易日检查和缓存清理
+    if g.last_ma_trading_day != current_trading_day:
+        if g.excluded_contracts:
+            log.info(f"交易日切换至 {current_trading_day},清空上一交易日的排除缓存")
+        g.excluded_contracts = {}
+        g.ma_checked_underlyings = {}
+        g.last_ma_trading_day = current_trading_day
+
+    # 步骤2:获取当前时间和筛选可交易品种
+    current_time = str(context.current_dt.time())[:5]  # HH:MM格式
+    focus_symbols = g.strategy_focus_symbols if g.strategy_focus_symbols else list(g.futures_config.keys())
+    tradable_symbols = []
+    
+    # 根据当前时间确定可交易的时段
+    # 21:05 -> 仅接受21:00开盘的合约
+    # 09:05 -> 接受09:00或21:00开盘的合约
+    # 09:35 -> 接受所有时段(21:00, 09:00, 09:30)的合约
+    for symbol in focus_symbols:
+        trading_start_time = get_futures_config(symbol, 'trading_start_time', '09:05')
+        should_trade = False
+        
+        if current_time == '21:05':
+            should_trade = trading_start_time.startswith('21:00')
+        elif current_time == '09:05':
+            should_trade = trading_start_time.startswith('21:00') or trading_start_time.startswith('09:00')
+        elif current_time == '09:35':
+            should_trade = True
+        
+        if should_trade:
+            tradable_symbols.append(symbol)
+    
+    if not tradable_symbols:
+        log.info(f"当前时间 {current_time} 无品种开盘,跳过检查")
+        return
+    
+    log.info(f"当前时间 {current_time} 开盘品种: {tradable_symbols}")
+    
+    # 步骤3:对每个品种循环处理
+    for symbol in tradable_symbols:
+        # 步骤3.1:检查是否已处理过
+        if g.ma_checked_underlyings.get(symbol) == current_trading_day:
+            log.info(f"{symbol} 已在交易日 {current_trading_day} 完成均线检查,跳过本次执行")
+            continue
+
+        try:
+            g.ma_checked_underlyings[symbol] = current_trading_day
+            
+            # 步骤3.2:获取主力合约
+            dominant_future = get_dominant_future(symbol)
+            if not dominant_future:
+                log.info(f"{symbol} 未找到主力合约,跳过")
+                continue
+            
+            # 步骤3.3:检查排除缓存
+            if dominant_future in g.excluded_contracts:
+                excluded_info = g.excluded_contracts[dominant_future]
+                if excluded_info['trading_day'] == current_trading_day:
+                    continue
+                else:
+                    # 新的一天,从缓存中移除(会在after_market_close统一清理,这里也做兜底)
+                    del g.excluded_contracts[dominant_future]
+            
+            # 步骤3.4:检查是否已有持仓
+            if check_symbol_prefix_match(dominant_future, context, set(g.trade_history.keys())):
+                log.info(f"{symbol} 已有持仓,跳过")
+                continue
+            
+            # 步骤3.5:获取历史数据和前一交易日数据(合并优化)
+            # 获取历史数据(需要足够计算MA30)
+            historical_data = get_price(dominant_future, end_date=context.current_dt, 
+                                       frequency='1d', fields=['open', 'close', 'high', 'low'], 
+                                       count=g.ma_historical_days)
+            
+            if historical_data is None or len(historical_data) < max(g.ma_periods):
+                log.info(f"{symbol} 历史数据不足,跳过")
+                continue
+
+            # 获取前一交易日并在历史数据中匹配
+            previous_trade_days = get_trade_days(end_date=current_trading_day, count=2)
+            previous_trade_days = [normalize_trade_day_value(d) for d in previous_trade_days]
+            previous_trading_day = None
+            if len(previous_trade_days) >= 2:
+                previous_trading_day = previous_trade_days[-2]
+            elif len(previous_trade_days) == 1 and previous_trade_days[0] < current_trading_day:
+                previous_trading_day = previous_trade_days[0]
+
+            if previous_trading_day is None:
+                log.info(f"{symbol} 无法确定前一交易日,跳过")
+                continue
+
+            # 在历史数据中匹配前一交易日
+            historical_dates = historical_data.index.date
+            match_indices = np.where(historical_dates == previous_trading_day)[0]
+            if len(match_indices) == 0:
+                earlier_indices = np.where(historical_dates < previous_trading_day)[0]
+                if len(earlier_indices) == 0:
+                    log.info(f"{symbol} 历史数据缺少 {previous_trading_day} 之前的记录,跳过")
+                    continue
+                match_indices = [earlier_indices[-1]]
+
+            # 提取截至前一交易日的数据,并一次性提取所有需要的字段
+            data_upto_yesterday = historical_data.iloc[:match_indices[-1] + 1]
+            yesterday_data = data_upto_yesterday.iloc[-1]
+            yesterday_close = yesterday_data['close']
+            yesterday_open = yesterday_data['open']
+            
+            # 步骤3.6:获取当前价格数据
+            current_data = get_current_data()[dominant_future]
+            today_open = current_data.day_open
+            
+            # ==================== 第二部分:核心指标计算 ====================
+            
+            # 步骤4:计算均线相关指标(合并优化)
+            ma_values = calculate_ma_values(data_upto_yesterday, g.ma_periods)
+            ma_proximity_counts = calculate_ma_proximity_counts(data_upto_yesterday, g.ma_periods, g.ma_pattern_lookback_days)
+            
+            log.info(f"{symbol}({dominant_future}) 均线检查:")
+            log.info(f"  均线贴近统计: {ma_proximity_counts}")
+            
+            # 检查均线贴近计数
+            proximity_sum = ma_proximity_counts.get('MA5', 0) + ma_proximity_counts.get('MA10', 0)
+            if proximity_sum < g.ma_proximity_min_threshold:
+                log.info(f"  {symbol}({dominant_future}) ✗ 均线贴近计数不足,MA5+MA10={proximity_sum} < {g.ma_proximity_min_threshold},跳过")
+                add_to_excluded_contracts(dominant_future, 'ma_proximity', current_trading_day)
+                continue
+            
+            # 步骤5:计算极端趋势天数
+            extreme_above_count, extreme_below_count = calculate_extreme_trend_days(
+                data_upto_yesterday,
+                g.ma_periods,
+                g.ma_pattern_lookback_days
+            )
+            extreme_total = extreme_above_count + extreme_below_count
+            min_extreme = min(extreme_above_count, extreme_below_count)
+            filter_threshold = max(2, g.ma_pattern_extreme_days_threshold)
+            log.info(
+                f"  极端趋势天数统计: 收盘在所有均线上方 {extreme_above_count} 天, 收盘在所有均线下方 {extreme_below_count} 天, "
+                f"合计 {extreme_total} 天, min(A,B)={min_extreme} (过滤阈值: {filter_threshold})"
+            )
+            if extreme_above_count > 0 and extreme_below_count > 0 and min_extreme >= filter_threshold:
+                log.info(
+                    f"  {symbol}({dominant_future}) ✗ 极端趋势多空同时出现且 min(A,B)={min_extreme} ≥ {filter_threshold},跳过"
+                )
+                add_to_excluded_contracts(dominant_future, 'ma_extreme_trend', current_trading_day)
+                continue
+
+            # 步骤6:判断均线走势
+            direction = None
+            if check_ma_pattern(ma_values, 'long'):
+                direction = 'long'
+            elif check_ma_pattern(ma_values, 'short'):
+                direction = 'short'
+            else:
+                add_to_excluded_contracts(dominant_future, 'ma_trend', current_trading_day)
+                continue
+            
+            # 步骤7:检查MA5分布过滤
+            if g.enable_ma_distribution_filter:
+                distribution_passed, distribution_stats = check_ma5_distribution_filter(
+                    data_upto_yesterday,
+                    g.ma_distribution_lookback_days,
+                    direction,
+                    g.ma_distribution_min_ratio
+                )
+                log.info(
+                    f"  MA5分布过滤: 方向 {direction}, 有效天数 "
+                    f"{distribution_stats['valid_days']}/{distribution_stats['lookback_days']},"
+                    f"满足天数 {distribution_stats['qualified_days']}/{distribution_stats['required_days']}"
+                )
+                if not distribution_passed:
+                    insufficiency = distribution_stats['valid_days'] < distribution_stats['lookback_days']
+                    reason = "有效数据不足" if insufficiency else "满足天数不足"
+                    log.info(
+                        f"  {symbol}({dominant_future}) ✗ MA5分布过滤未通过({reason})"
+                    )
+                    add_to_excluded_contracts(dominant_future, 'ma5_distribution', current_trading_day)
+                    continue
+            
+            # 步骤8:检查历史均线模式一致性
+            consistency_passed, consistency_ratio = check_historical_ma_pattern_consistency(
+                historical_data, direction, g.ma_pattern_lookback_days, g.ma_pattern_consistency_threshold
+            )
+            
+            if not consistency_passed:
+                log.info(f"  {symbol}({dominant_future}) ✗ 历史均线模式一致性不足 "
+                        f"({consistency_ratio:.1%} < {g.ma_pattern_consistency_threshold:.1%}),跳过")
+                add_to_excluded_contracts(dominant_future, 'ma_consistency', current_trading_day)
+                continue
+            else:
+                log.info(f"  {symbol}({dominant_future}) ✓ 历史均线模式一致性检查通过 "
+                        f"({consistency_ratio:.1%} >= {g.ma_pattern_consistency_threshold:.1%})")
+            
+            # 步骤9:检查开盘价差(可配置开关)
+            if g.enable_open_gap_filter:
+                gap_check_passed = check_open_gap_filter(
+                    symbol=symbol,
+                    dominant_future=dominant_future,
+                    direction=direction,
+                    yesterday_close=yesterday_close,
+                    today_open=today_open,
+                    current_trading_day=current_trading_day
+                )
+                if not gap_check_passed:
+                    continue
+            else:
+                log.info("  已关闭开盘价差过滤(enable_open_gap_filter=False),跳过该检查")
+            
+            # 步骤10:将通过检查的品种加入候选列表
+            g.daily_ma_candidates[dominant_future] = {
+                'symbol': symbol,
+                'direction': direction,
+                'open_price': today_open,
+                'yesterday_close': yesterday_close,
+                'yesterday_open': yesterday_open,
+                'ma_values': ma_values
+            }
+            
+            log.info(f"  ✓✓ {symbol} 通过均线和开盘价差检查,加入候选列表")
+            
+        except Exception as e:
+            g.ma_checked_underlyings.pop(symbol, None)
+            log.warning(f"{symbol} 检查时出错: {str(e)}")
+            continue
+    
+    log.info(f"候选列表更新完成,当前候选品种: {list(g.daily_ma_candidates.keys())}")
+    log.info("=" * 60)
+
+def check_open_and_stop(context):
+    """统一的开仓和止损止盈检查函数"""
+    # 先检查换月移仓
+    log.info("=" * 60)
+    current_trading_day = get_current_trading_day(context.current_dt)
+    log.info(f"执行开仓和止损止盈检查 - 时间: {context.current_dt}, 交易日: {current_trading_day}")
+    log.info("=" * 60)
+    log.info(f"先检查换月:")
+    position_auto_switch(context)
+    
+    # 获取当前时间
+    current_time = str(context.current_dt.time())[:2]
+    
+    # 判断是否为夜盘时间
+    is_night_session = (current_time in ['21', '22', '23', '00', '01', '02'])
+    
+    # 检查是否禁止当晚操作
+    if is_night_session and g.night_session_blocked:
+        blocked_trading_day = normalize_trade_day_value(g.night_session_blocked_trading_day) if g.night_session_blocked_trading_day else None
+        current_trading_day_normalized = normalize_trade_day_value(current_trading_day)
+        if blocked_trading_day == current_trading_day_normalized:
+            log.info(f"当晚操作已被禁止(订单状态为'new',无夜盘),跳过所有操作")
+            return
+
+    # 得到当前未完成订单
+    orders = get_open_orders()
+    # 循环,撤销订单
+    if len(orders) == 0:
+        log.debug(f"无未完成订单")
+    else:
+        for _order in orders.values():
+            log.debug(f"order: {_order}")
+            cancel_order(_order)
+    
+    # 第一步:检查开仓条件
+    log.info(f"检查开仓条件:")
+    if g.daily_ma_candidates:
+        log.info("=" * 60)
+        log.info(f"执行开仓检查 - 时间: {context.current_dt}, 候选品种数量: {len(g.daily_ma_candidates)}")
+        
+        # 遍历候选品种
+        candidates_to_remove = []
+        
+        for dominant_future, candidate_info in g.daily_ma_candidates.items():
+            try:
+                symbol = candidate_info['symbol']
+                direction = candidate_info['direction']
+                open_price = candidate_info['open_price']
+                yesterday_close = candidate_info.get('yesterday_close')
+                yesterday_open = candidate_info.get('yesterday_open')
+                
+                # 检查是否已有持仓
+                if check_symbol_prefix_match(dominant_future, context, set(g.trade_history.keys())):
+                    log.info(f"{symbol} 已有持仓,从候选列表移除")
+                    candidates_to_remove.append(dominant_future)
+                    continue
+                
+                # 获取当前价格
+                current_data = get_current_data()[dominant_future]
+                current_price = current_data.last_price
+                
+                # 计算当天价差
+                intraday_diff = current_price - open_price
+                intraday_diff_ratio = intraday_diff / open_price
+                
+                log.info(f"{symbol}({dominant_future}) 开仓条件检查:")
+                log.info(f"  方向: {direction}, 开盘价: {open_price:.2f}, 当前价: {current_price:.2f}, "
+                        f"当天价差: {intraday_diff:.2f}, 变化比例: {intraday_diff_ratio:.2%}")
+                
+                # 判断是否满足开仓条件 - 仅检查均线穿越得分
+                should_open = True
+                log.info(f"  开仓条件简化:仅检查均线穿越得分,跳过策略1,2,3的判断")
+                
+                if should_open:
+                    ma_values = candidate_info.get('ma_values') or {}
+                    cross_score, score_details = calculate_ma_cross_score(open_price, current_price, ma_values, direction)
+
+                    # 根据当前时间调整所需的均线穿越得分阈值
+                    current_time_str = str(context.current_dt.time())[:5]  # HH:MM格式
+                    required_cross_score = g.ma_cross_threshold
+                    if current_time_str != '14:55':
+                        # 在14:55以外的时间,需要更高的得分阈值
+                        required_cross_score = g.ma_cross_threshold + 1
+
+                    log.info(f"  均线穿越得分: {cross_score}, 得分详情: {score_details}, 当前时间: {current_time_str}, 所需阈值: {required_cross_score}")
+
+                    # 检查得分是否满足条件
+                    score_passed = False
+                    if cross_score >= required_cross_score:
+                        # 如果得分达到阈值,检查特殊情况:只有1分且来自MA5
+                        if cross_score == 1 and required_cross_score == 1:
+                            # 检查这一分是否来自MA5
+                            from_ma5_only = len(score_details) == 1 and score_details[0]['period'] == 5
+                            if not from_ma5_only:
+                                score_passed = True
+                                log.info(f"  ✓ 均线穿越得分检查通过:1分且非来自MA5")
+                            else:
+                                log.info(f"  ✗ 均线穿越得分检查未通过:1分且仅来自MA5")
+                        else:
+                            score_passed = True
+                            log.info(f"  ✓ 均线穿越得分检查通过")
+
+                    if not score_passed:
+                        log.info(f"  ✗ 均线穿越得分不足或不符合条件({cross_score} < {required_cross_score} 或 1分来自MA5),跳过开仓")
+                        continue
+                    # 执行开仓
+                    log.info(f"  准备开仓: {symbol} {direction}")
+                    target_hands, single_hand_margin = calculate_target_hands(context, dominant_future, direction)
+                    
+                    if target_hands > 0:
+                        success = open_position(context, dominant_future, target_hands, direction, single_hand_margin,
+                                              f'均线形态开仓')
+                        if success:
+                            log.info(f"  ✓✓ {symbol} 开仓成功,从候选列表移除")
+                            candidates_to_remove.append(dominant_future)
+                        else:
+                            log.warning(f"  ✗ {symbol} 开仓失败")
+                    else:
+                        log.warning(f"  ✗ {symbol} 计算目标手数为0,跳过开仓")
+                        
+            except Exception as e:
+                log.warning(f"{dominant_future} 处理时出错: {str(e)}")
+                continue
+        
+        # 从候选列表中移除已开仓的品种
+        for future in candidates_to_remove:
+            if future in g.daily_ma_candidates:
+                del g.daily_ma_candidates[future]
+        
+        log.info(f"剩余候选品种: {list(g.daily_ma_candidates.keys())}")
+        log.info("=" * 60)
+    
+    # 第二步:检查止损止盈
+    log.info(f"检查止损止盈条件:")
+    subportfolio = context.subportfolios[0]
+    long_positions = list(subportfolio.long_positions.values())
+    short_positions = list(subportfolio.short_positions.values())
+    
+    closed_count = 0
+    skipped_count = 0
+    
+    for position in long_positions + short_positions:
+        security = position.security
+        underlying_symbol = security.split('.')[0][:-4]
+        
+        # 检查交易时间适配性
+        has_night_session = get_futures_config(underlying_symbol, 'has_night_session', False)
+        
+        # 如果是夜盘时间,但品种不支持夜盘交易,则跳过
+        if is_night_session and not has_night_session:
+            skipped_count += 1
+            continue
+        
+        # 执行止损止盈检查
+        if check_position_stop_loss_profit(context, position):
+            closed_count += 1
+    
+    if closed_count > 0:
+        log.info(f"执行了 {closed_count} 次止损止盈")
+    
+    if skipped_count > 0:
+        log.info(f"夜盘时间跳过 {skipped_count} 个日间品种的止损止盈检查")
+
+
+def check_ma_trailing_reactivation(context):
+    """检查是否需要恢复均线跟踪止盈"""
+    subportfolio = context.subportfolios[0]
+    positions = list(subportfolio.long_positions.values()) + list(subportfolio.short_positions.values())
+    
+    if not positions:
+        return
+    
+    reenabled_count = 0
+    current_data = get_current_data()
+    
+    for position in positions:
+        security = position.security
+        trade_info = g.trade_history.get(security)
+        
+        if not trade_info or trade_info.get('ma_trailing_enabled', True):
+            continue
+        
+        direction = trade_info['direction']
+        ma_values = calculate_realtime_ma_values(security, [5])
+        ma5_value = ma_values.get('ma5')
+        
+        if ma5_value is None or security not in current_data:
+            continue
+        
+        today_price = current_data[security].last_price
+        
+        if direction == 'long' and today_price > ma5_value:
+            trade_info['ma_trailing_enabled'] = True
+            reenabled_count += 1
+            log.info(f"恢复均线跟踪止盈: {security} {direction}, 当前价 {today_price:.2f} > MA5 {ma5_value:.2f}")
+        elif direction == 'short' and today_price < ma5_value:
+            trade_info['ma_trailing_enabled'] = True
+            reenabled_count += 1
+            log.info(f"恢复均线跟踪止盈: {security} {direction}, 当前价 {today_price:.2f} < MA5 {ma5_value:.2f}")
+    
+    if reenabled_count > 0:
+        log.info(f"恢复均线跟踪止盈持仓数量: {reenabled_count}")
+
+
+def check_position_stop_loss_profit(context, position):
+    """检查单个持仓的止损止盈"""
+    log.info(f"检查持仓: {position.security}")
+    security = position.security
+    
+    if security not in g.trade_history:
+        return False
+    
+    trade_info = g.trade_history[security]
+    direction = trade_info['direction']
+    entry_price = trade_info['entry_price']
+    entry_time = trade_info['entry_time']
+    entry_trading_day = trade_info.get('entry_trading_day')
+    if entry_trading_day is None:
+        entry_trading_day = get_current_trading_day(entry_time)
+        trade_info['entry_trading_day'] = entry_trading_day
+    if entry_trading_day is not None:
+        entry_trading_day = normalize_trade_day_value(entry_trading_day)
+    current_trading_day = normalize_trade_day_value(get_current_trading_day(context.current_dt))
+    current_price = position.price
+    
+    # 计算当前盈亏比率
+    if direction == 'long':
+        profit_rate = (current_price - entry_price) / entry_price
+    else:
+        profit_rate = (entry_price - current_price) / entry_price
+    
+    # 检查固定止损
+    log.info("=" * 60)
+    log.info(f"检查固定止损:")
+    log.info("=" * 60)
+    if profit_rate <= -g.fixed_stop_loss_rate:
+        log.info(f"{security} {direction} 触发固定止损 {g.fixed_stop_loss_rate:.3%}, 当前亏损率: {profit_rate:.3%}, "
+                f"成本价: {entry_price:.2f}, 当前价格: {current_price:.2f}")
+        close_position(context, security, direction)
+        return True
+    else:
+        log.debug(f"{security} {direction} 未触发固定止损 {g.fixed_stop_loss_rate:.3%}, 当前亏损率: {profit_rate:.3%}, "
+                f"成本价: {entry_price:.2f}, 当前价格: {current_price:.2f}")
+
+    if entry_trading_day is not None and entry_trading_day == current_trading_day:
+        log.info(f"{security} 建仓交易日内跳过动态止盈检查")
+        return False
+
+    # 检查是否启用均线跟踪止盈
+    log.info("=" * 60)
+    log.info(f"检查是否启用均线跟踪止盈:")
+    log.info("=" * 60)
+    if not trade_info.get('ma_trailing_enabled', True):
+        log.debug(f"{security} {direction} 未启用均线跟踪止盈")
+        return False
+
+    # 检查均线跟踪止盈
+    # 获取持仓天数
+    entry_date = entry_time.date()
+    current_date = context.current_dt.date()
+    all_trade_days = get_all_trade_days()
+    holding_days = sum((entry_date <= d <= current_date) for d in all_trade_days)
+    
+    # 计算变化率
+    today_price = get_current_data()[security].last_price
+    avg_daily_change_rate = calculate_average_daily_change_rate(security)
+    historical_data = attribute_history(security, 1, '1d', ['close'])
+    yesterday_close = historical_data['close'].iloc[-1]
+    today_change_rate = abs((today_price - yesterday_close) / yesterday_close)
+    
+    # 根据时间判断使用的偏移量
+    current_time = context.current_dt.time()
+    target_time = datetime.strptime('14:55:00', '%H:%M:%S').time()
+    if current_time > target_time:
+        offset_ratio = g.ma_offset_ratio_close
+        log.debug(f"当前时间是:{current_time},使用偏移量: {offset_ratio:.3%}")
+    else:
+        offset_ratio = g.ma_offset_ratio_normal
+        log.debug(f"当前时间是:{current_time},使用偏移量: {offset_ratio:.3%}")
+    # 选择止损均线
+    close_line = None
+    if today_change_rate >= 1.5 * avg_daily_change_rate:
+        close_line = 'ma5'  # 波动剧烈时用短周期
+    elif holding_days <= g.days_for_adjustment:
+        close_line = 'ma5'  # 持仓初期用短周期
+    else:
+        close_line = 'ma5' if today_change_rate >= 1.2 * avg_daily_change_rate else 'ma10'
+    
+    # 计算实时均线值
+    ma_values = calculate_realtime_ma_values(security, [5, 10])
+    ma_value = ma_values[close_line]
+    
+    # 应用偏移量
+    if direction == 'long':
+        adjusted_ma_value = ma_value * (1 - offset_ratio)
+    else:
+        adjusted_ma_value = ma_value * (1 + offset_ratio)
+    
+    # 判断是否触发均线止损
+    if (direction == 'long' and today_price < adjusted_ma_value) or \
+       (direction == 'short' and today_price > adjusted_ma_value):
+        log.info(f"触发均线跟踪止盈 {security} {direction}, 止损均线: {close_line}, "
+                f"均线值: {ma_value:.2f}, 调整后: {adjusted_ma_value:.2f}, "
+                f"当前价: {today_price:.2f}, 持仓天数: {holding_days}")
+        close_position(context, security, direction)
+        return True
+    else:
+        log.debug(f"未触发均线跟踪止盈 {security} {direction}, 止损均线: {close_line}, "
+                f"均线值: {ma_value:.2f}, 调整后: {adjusted_ma_value:.2f}, "
+                f"当前价: {today_price:.2f}, 持仓天数: {holding_days}")
+    
+    return False
+
+############################ 核心辅助函数 ###################################
+
+def calculate_ma_values(data, periods):
+    """计算均线值
+    
+    Args:
+        data: DataFrame,包含'close'列的历史数据(最后一行是最新的数据)
+        periods: list,均线周期列表,如[5, 10, 20, 30]
+    
+    Returns:
+        dict: {'MA5': value, 'MA10': value, 'MA20': value, 'MA30': value}
+        返回最后一行(最新日期)的各周期均线值
+    """
+    ma_values = {}
+    
+    for period in periods:
+        if len(data) >= period:
+            # 计算最后period天的均线值
+            ma_values[f'MA{period}'] = data['close'].iloc[-period:].mean()
+        else:
+            ma_values[f'MA{period}'] = None
+    
+    return ma_values
+
+
+def calculate_ma_cross_score(open_price, current_price, ma_values, direction):
+    """根据开盘价与当前价统计多周期均线穿越得分
+    返回: (总得分, 得分详情列表)
+    得分详情: [{'period': 5, 'delta': 1}, {'period': 10, 'delta': 1}, ...]
+    """
+    if not ma_values:
+        return 0, []
+    assert direction in ('long', 'short')
+    score = 0
+    score_details = []
+    for period in g.ma_periods:
+        key = f'MA{period}'
+        ma_value = ma_values.get(key)
+        if ma_value is None:
+            continue
+        cross_up = open_price < ma_value and current_price > ma_value
+        cross_down = open_price > ma_value and current_price < ma_value
+        if not (cross_up or cross_down):
+            continue
+        if direction == 'long':
+            delta = 1 if cross_up else -1
+        else:
+            delta = -1 if cross_up else 1
+        score += delta
+        score_details.append({'period': period, 'delta': delta})
+        log.debug(
+            f"  均线穿越[{key}] - 开盘 {open_price:.2f}, 当前 {current_price:.2f}, "
+            f"均线 {ma_value:.2f}, 方向 {direction}, 增量 {delta}, 当前得分 {score}"
+        )
+    return score, score_details
+
+
+def calculate_ma_proximity_counts(data, periods, lookback_days):
+    """统计近 lookback_days 天收盘价贴近各均线的次数"""
+    proximity_counts = {f'MA{period}': 0 for period in periods}
+
+    if len(data) < lookback_days:
+        return proximity_counts
+
+    closes = data['close'].iloc[-lookback_days:]
+    ma_series = {
+        period: data['close'].rolling(window=period).mean().iloc[-lookback_days:]
+        for period in periods
+    }
+
+    for idx, close_price in enumerate(closes):
+        min_diff = None
+        closest_period = None
+
+        for period in periods:
+            ma_value = ma_series[period].iloc[idx]
+            if pd.isna(ma_value):
+                continue
+            diff = abs(close_price - ma_value)
+            if min_diff is None or diff < min_diff:
+                min_diff = diff
+                closest_period = period
+
+        if closest_period is not None:
+            proximity_counts[f'MA{closest_period}'] += 1
+
+    return proximity_counts
+
+
+def calculate_extreme_trend_days(data, periods, lookback_days):
+    """统计过去 lookback_days 天收盘价相对所有均线的极端趋势天数"""
+    if len(data) < lookback_days:
+        return 0, 0
+
+    recent_closes = data['close'].iloc[-lookback_days:]
+    ma_series = {
+        period: data['close'].rolling(window=period).mean().iloc[-lookback_days:]
+        for period in periods
+    }
+
+    above_count = 0
+    below_count = 0
+
+    for idx, close_price in enumerate(recent_closes):
+        ma_values = []
+        valid = True
+
+        for period in periods:
+            ma_value = ma_series[period].iloc[idx]
+            if pd.isna(ma_value):
+                valid = False
+                break
+            ma_values.append(ma_value)
+
+        if not valid or not ma_values:
+            continue
+
+        if all(close_price > ma_value for ma_value in ma_values):
+            above_count += 1
+        elif all(close_price < ma_value for ma_value in ma_values):
+            below_count += 1
+
+    return above_count, below_count
+
+
+def check_ma5_distribution_filter(data, lookback_days, direction, min_ratio):
+    """检查近 lookback_days 天收盘价相对于MA5的分布情况"""
+    stats = {
+        'lookback_days': lookback_days,
+        'valid_days': 0,
+        'qualified_days': 0,
+        'required_days': max(0, math.ceil(lookback_days * min_ratio))
+    }
+
+    if lookback_days <= 0:
+        return True, stats
+
+    if len(data) < max(lookback_days, 5):
+        return False, stats
+
+    recent_closes = data['close'].iloc[-lookback_days:]
+    ma5_series = data['close'].rolling(window=5).mean().iloc[-lookback_days:]
+
+    for close_price, ma5_value in zip(recent_closes, ma5_series):
+        log.debug(f"close_price: {close_price}, ma5_value: {ma5_value}")
+        if pd.isna(ma5_value):
+            continue
+        stats['valid_days'] += 1
+        if direction == 'long' and close_price < ma5_value:
+            stats['qualified_days'] += 1
+        elif direction == 'short' and close_price > ma5_value:
+            stats['qualified_days'] += 1
+
+    if stats['valid_days'] < lookback_days:
+        return False, stats
+
+    return stats['qualified_days'] >= stats['required_days'], stats
+
+
+def check_open_gap_filter(symbol, dominant_future, direction, yesterday_close, today_open, current_trading_day):
+    """开盘价差过滤辅助函数
+
+    根据当前策略模式(`g.ma_gap_strategy_mode`)和对应阈值检查开盘价差是否符合方向要求。
+
+    Args:
+        symbol: 品种代码(如 'AO')
+        dominant_future: 主力合约代码(如 'AO2502.XSGE')
+        direction: 方向,'long' 或 'short'
+        yesterday_close: 前一交易日收盘价
+        today_open: 当日开盘价
+        current_trading_day: 当前期货交易日
+
+    Returns:
+        bool: True 表示通过开盘价差过滤,False 表示未通过(并已加入排除缓存)
+    """
+    open_gap_ratio = (today_open - yesterday_close) / yesterday_close
+    log.info(
+        f"  开盘价差检查: 昨收 {yesterday_close:.2f}, 今开 {today_open:.2f}, "
+        f"价差比例 {open_gap_ratio:.2%}"
+    )
+
+    gap_check_passed = False
+
+    if g.ma_gap_strategy_mode == 1:
+        # 方案1:多头检查上跳,空头检查下跳
+        if direction == 'long' and open_gap_ratio >= g.ma_open_gap_threshold:
+            log.info(
+                f"  {symbol}({dominant_future}) ✓ 方案1多头开盘价差检查通过 "
+                f"({open_gap_ratio:.2%} >= {g.ma_open_gap_threshold:.2%})"
+            )
+            gap_check_passed = True
+        elif direction == 'short' and open_gap_ratio <= -g.ma_open_gap_threshold:
+            log.info(
+                f"  {symbol}({dominant_future}) ✓ 方案1空头开盘价差检查通过 "
+                f"({open_gap_ratio:.2%} <= {-g.ma_open_gap_threshold:.2%})"
+            )
+            gap_check_passed = True
+    elif g.ma_gap_strategy_mode == 2 or g.ma_gap_strategy_mode == 3:
+        # 方案2和方案3:多头检查下跳,空头检查上跳
+        if direction == 'long' and open_gap_ratio <= -g.ma_open_gap_threshold2:
+            log.info(
+                f"  {symbol}({dominant_future}) ✓ 方案{g.ma_gap_strategy_mode}多头开盘价差检查通过 "
+                f"({open_gap_ratio:.2%} <= {-g.ma_open_gap_threshold2:.2%})"
+            )
+            gap_check_passed = True
+        elif direction == 'short' and open_gap_ratio >= g.ma_open_gap_threshold2:
+            log.info(
+                f"  {symbol}({dominant_future}) ✓ 方案{g.ma_gap_strategy_mode}空头开盘价差检查通过 "
+                f"({open_gap_ratio:.2%} >= {g.ma_open_gap_threshold2:.2%})"
+            )
+            gap_check_passed = True
+
+    if not gap_check_passed:
+        add_to_excluded_contracts(dominant_future, 'open_gap', current_trading_day)
+        return False
+
+    return True
+
+
+
+def check_ma_pattern(ma_values, direction):
+    """检查均线排列模式是否符合方向要求
+    
+    Args:
+        ma_values: dict,包含MA5, MA10, MA20, MA30的均线值
+        direction: str,'long'或'short'
+    
+    Returns:
+        bool: 是否符合均线排列要求
+    """
+    ma5 = ma_values['MA5']
+    ma10 = ma_values['MA10']
+    ma20 = ma_values['MA20']
+    ma30 = ma_values['MA30']
+    
+    if direction == 'long':
+        # 多头模式:MA30 <= MA20 <= MA10 <= MA5 或 MA30 <= MA20 <= MA5 <= MA10
+        # 或者:MA20 <= MA30 <= MA10 <= MA5 或 MA20 <= MA30 <= MA5 <= MA10
+        pattern1 = (ma30 <= ma20 <= ma10 <= ma5)
+        pattern2 = (ma30 <= ma20 <= ma5 <= ma10)
+        pattern3 = (ma20 <= ma30 <= ma10 <= ma5)
+        pattern4 = (ma20 <= ma30 <= ma5 <= ma10)
+        return pattern1 or pattern2 or pattern3 or pattern4
+    elif direction == 'short':
+        # 空头模式:MA10 <= MA5 <= MA20 <= MA30 或 MA5 <= MA10 <= MA20 <= MA30
+        # 或者:MA10 <= MA5 <= MA30 <= MA20 或 MA5 <= MA10 <= MA30 <= MA20
+        pattern1 = (ma10 <= ma5 <= ma20 <= ma30)
+        pattern2 = (ma5 <= ma10 <= ma20 <= ma30)
+        pattern3 = (ma10 <= ma5 <= ma30 <= ma20)
+        pattern4 = (ma5 <= ma10 <= ma30 <= ma20)
+        return pattern1 or pattern2 or pattern3 or pattern4
+    else:
+        return False
+
+def check_historical_ma_pattern_consistency(historical_data, direction, lookback_days, consistency_threshold):
+    """检查历史均线模式的一致性
+    
+    Args:
+        historical_data: DataFrame,包含足够天数的历史数据
+        direction: str,'long'或'short'
+        lookback_days: int,检查过去多少天
+        consistency_threshold: float,一致性阈值(0-1之间)
+    
+    Returns:
+        tuple: (bool, float) - (是否通过一致性检查, 实际一致性比例)
+    """
+    if len(historical_data) < max(g.ma_periods) + lookback_days:
+        # 历史数据不足
+        return False, 0.0
+    
+    match_count = 0
+    total_count = lookback_days
+    # log.debug(f"历史均线模式一致性检查: {direction}, 检查过去{lookback_days}天的数据")
+    # log.debug(f"历史数据: {historical_data}")
+    
+    # 检查过去lookback_days天的均线模式
+    for i in range(lookback_days):
+        # 获取倒数第(i+1)天的数据(i=0时是昨天,i=1时是前天,依此类推)
+        end_idx = -(i + 1)
+        # 获取这一天的具体日期
+        date = historical_data.index[end_idx].date()
+        # 获取到该天(包括该天)为止的所有数据
+        if i == 0:
+            data_slice = historical_data
+        else:
+            data_slice = historical_data.iloc[:-i]
+        
+        # 计算该天的均线值
+        # log.debug(f"对于倒数第{i+1}天,end_idx: {end_idx},日期: {date},计算均线值: {data_slice}")
+        ma_values = calculate_ma_values(data_slice, g.ma_periods)
+        # log.debug(f"end_idx: {end_idx},日期: {date},倒数第{i+1}天的均线值: {ma_values}")
+        
+        # 检查是否符合模式
+        if check_ma_pattern(ma_values, direction):
+            match_count += 1
+            # log.debug(f"日期: {date},对于倒数第{i+1}天,历史均线模式一致性检查: {direction} 符合模式")
+        # else:
+            # log.debug(f"日期: {date},对于倒数第{i+1}天,历史均线模式一致性检查: {direction} 不符合模式")
+    
+    consistency_ratio = match_count / total_count
+    passed = consistency_ratio >= consistency_threshold
+    
+    return passed, consistency_ratio
+
+############################ 交易执行函数 ###################################
+
+def open_position(context, security, target_hands, direction, single_hand_margin, reason=''):
+    """开仓"""
+    try:
+        # 记录交易前的可用资金
+        cash_before = context.portfolio.available_cash
+        
+        # 使用order_target按手数开仓
+        order = order_target(security, target_hands, side=direction)
+        log.debug(f"order: {order}")
+        
+        # 检查订单状态,如果为'new'说明当晚没有夜盘
+        if order is not None:
+            order_status = str(order.status).lower()
+            if order_status == 'new':
+                # 取消订单
+                cancel_order(order)
+                current_trading_day = get_current_trading_day(context.current_dt)
+                g.night_session_blocked = True
+                g.night_session_blocked_trading_day = current_trading_day
+                log.warning(f"订单状态为'new',说明{current_trading_day}当晚没有夜盘,已取消订单: {security} {direction} {target_hands}手,并禁止当晚所有操作")
+                return False
+        
+        if order is not None and order.filled > 0:
+            # 记录交易后的可用资金
+            cash_after = context.portfolio.available_cash
+            
+            # 计算实际资金变化
+            cash_change = cash_before - cash_after
+
+            # 计算保证金变化
+            margin_change = single_hand_margin * target_hands
+            
+            # 获取订单价格和数量
+            order_price = order.avg_cost if order.avg_cost else order.price
+            order_amount = order.filled
+            
+            # 记录当日交易
+            underlying_symbol = security.split('.')[0][:-4]
+            g.today_trades.append({
+                'security': security, # 合约代码
+                'underlying_symbol': underlying_symbol, # 标的代码
+                'direction': direction, # 方向
+                'order_amount': order_amount, # 订单数量
+                'order_price': order_price, # 订单价格
+                'cash_change': cash_change, # 资金变化
+                'margin_change': margin_change, # 保证金
+                'time': context.current_dt # 时间
+            })
+            
+            # 记录交易信息
+            entry_trading_day = get_current_trading_day(context.current_dt)
+            g.trade_history[security] = {
+                'entry_price': order_price,
+                'target_hands': target_hands,
+                'actual_hands': order_amount,
+                'actual_margin': margin_change,
+                'direction': direction,
+                'entry_time': context.current_dt,
+                'entry_trading_day': entry_trading_day
+            }
+
+            ma_trailing_enabled = True
+            ma_values_at_entry = calculate_realtime_ma_values(security, [5])
+            ma5_value = ma_values_at_entry.get('ma5')
+            if ma5_value is not None:
+                if direction == 'long' and order_price < ma5_value:
+                    ma_trailing_enabled = False
+                    log.info(f"禁用均线跟踪止盈: {security} {direction}, 开仓价 {order_price:.2f} < MA5 {ma5_value:.2f}")
+                elif direction == 'short' and order_price > ma5_value:
+                    ma_trailing_enabled = False
+                    log.info(f"禁用均线跟踪止盈: {security} {direction}, 开仓价 {order_price:.2f} > MA5 {ma5_value:.2f}")
+
+            g.trade_history[security]['ma_trailing_enabled'] = ma_trailing_enabled
+            
+            log.info(f"开仓成功: {security} {direction} {order_amount}手 @{order_price:.2f}, "
+                    f"保证金: {margin_change:.0f}, 资金变化: {cash_change:.0f}, 原因: {reason}")
+            
+            return True
+            
+    except Exception as e:
+        log.warning(f"开仓失败 {security}: {str(e)}")
+    
+    return False
+
+def close_position(context, security, direction):
+    """平仓"""
+    try:
+        # 使用order_target平仓到0手
+        order = order_target(security, 0, side=direction)
+        
+        if order is not None and order.filled > 0:
+            underlying_symbol = security.split('.')[0][:-4]
+            
+            # 记录当日交易(平仓)
+            g.today_trades.append({
+                'security': security,
+                'underlying_symbol': underlying_symbol,
+                'direction': direction,
+                'order_amount': -order.filled,
+                'order_price': order.avg_cost if order.avg_cost else order.price,
+                'cash_change': 0,
+                'time': context.current_dt
+            })
+            
+            log.info(f"平仓成功: {underlying_symbol} {direction} {order.filled}手")
+            
+            # 从交易历史中移除
+            if security in g.trade_history:
+                del g.trade_history[security]
+                log.debug(f"从交易历史中移除: {security}")
+        else:
+            log.info(f"平仓失败: {security} {direction} {order.filled}手")
+            return True
+            
+    except Exception as e:
+        log.warning(f"平仓失败 {security}: {str(e)}")
+    
+    return False
+
+############################ 辅助函数 ###################################
+
+def get_futures_config(underlying_symbol, config_key=None, default_value=None):
+    """获取期货品种配置信息的辅助函数"""
+    if underlying_symbol not in g.futures_config:
+        if config_key and default_value is not None:
+            return default_value
+        return {}
+    
+    if config_key is None:
+        return g.futures_config[underlying_symbol]
+    
+    return g.futures_config[underlying_symbol].get(config_key, default_value)
+
+def get_margin_rate(underlying_symbol, direction, default_rate=0.10):
+    """获取保证金比例的辅助函数"""
+    return g.futures_config.get(underlying_symbol, {}).get('margin_rate', {}).get(direction, default_rate)
+
+def get_multiplier(underlying_symbol, default_multiplier=10):
+    """获取合约乘数的辅助函数"""
+    return g.futures_config.get(underlying_symbol, {}).get('multiplier', default_multiplier)
+
+def add_to_excluded_contracts(dominant_future, reason, current_trading_day):
+    """将合约添加到排除缓存"""
+    g.excluded_contracts[dominant_future] = {
+        'reason': reason,
+        'trading_day': current_trading_day
+    }
+
+def has_reached_trading_start(current_dt, trading_start_time_str, has_night_session=False):
+    """判断当前是否已到达合约允许交易的起始时间"""
+    if not trading_start_time_str:
+        return True
+
+    try:
+        hour, minute = [int(part) for part in trading_start_time_str.split(':')[:2]]
+    except Exception:
+        return True
+
+    start_time = time(hour, minute)
+    current_time = current_dt.time()
+
+    if has_night_session:
+        if current_time >= start_time:
+            return True
+        if current_time < time(12, 0):
+            return True
+        if time(8, 30) <= current_time <= time(15, 30):
+            return True
+        return False
+
+    if current_time < start_time:
+        return False
+    if current_time >= time(20, 0):
+        return False
+    return True
+
+def calculate_target_hands(context, security, direction):
+    """计算目标开仓手数"""
+    current_price = get_current_data()[security].last_price
+    underlying_symbol = security.split('.')[0][:-4]
+    
+    # 使用保证金比例
+    margin_rate = get_margin_rate(underlying_symbol, direction)
+    multiplier = get_multiplier(underlying_symbol)
+    
+    # 计算单手保证金
+    single_hand_margin = current_price * multiplier * margin_rate
+    log.debug(f"计算单手保证金: {current_price:.2f} * {multiplier:.2f} * {margin_rate:.2f} = {single_hand_margin:.2f}")
+    
+    # 还要考虑可用资金限制
+    available_cash = context.portfolio.available_cash * g.usage_percentage
+    
+    # 根据单个标的最大持仓保证金限制计算开仓数量
+    max_margin = g.max_margin_per_position
+    
+    if single_hand_margin <= max_margin:
+        # 如果单手保证金不超过最大限制,计算最大可开仓手数
+        max_hands = int(max_margin / single_hand_margin)
+        max_hands_by_cash = int(available_cash / single_hand_margin)
+        
+        # 取两者较小值
+        actual_hands = min(max_hands, max_hands_by_cash)
+        
+        # 确保至少开1手
+        actual_hands = max(1, actual_hands)
+        
+        log.info(f"单手保证金: {single_hand_margin:.0f}, 目标开仓手数: {actual_hands}")
+        
+        return actual_hands, single_hand_margin
+    else:
+        # 如果单手保证金超过最大限制,默认开仓1手
+        actual_hands = 1
+        
+        log.info(f"单手保证金: {single_hand_margin:.0f} 超过最大限制: {max_margin}, 默认开仓1手")
+        
+        return actual_hands, single_hand_margin
+
+def check_symbol_prefix_match(symbol, context, hold_symbols):
+    """检查是否有相似的持仓品种"""
+    log.debug(f"检查持仓")
+    symbol_prefix = symbol[:-9]
+
+    long_positions = context.subportfolios[0].long_positions
+    short_positions = context.subportfolios[0].short_positions
+    log.debug(f"long_positions: {long_positions}, short_positions: {short_positions}")
+    
+    for hold_symbol in hold_symbols:
+        hold_symbol_prefix = hold_symbol[:-9] if len(hold_symbol) > 9 else hold_symbol
+        
+        if symbol_prefix == hold_symbol_prefix:
+            return True
+    return False
+
+def calculate_average_daily_change_rate(security, days=30):
+    """计算日均变化率"""
+    historical_data = attribute_history(security, days + 1, '1d', ['close'])
+    daily_change_rates = abs(historical_data['close'].pct_change()).iloc[1:]
+    return daily_change_rates.mean()
+
+def calculate_realtime_ma_values(security, ma_periods):
+    """计算包含当前价格的实时均线值"""
+    historical_data = attribute_history(security, max(ma_periods), '1d', ['close'])
+    today_price = get_current_data()[security].last_price
+    close_prices = historical_data['close'].tolist() + [today_price]
+    ma_values = {f'ma{period}': sum(close_prices[-period:]) / period for period in ma_periods}
+    return ma_values
+
+def sync_trade_history_with_positions(context):
+    """同步g.trade_history与实际持仓,清理已平仓但记录未删除的持仓"""
+    if not g.trade_history:
+        return
+    
+    subportfolio = context.subportfolios[0]
+    actual_positions = set(subportfolio.long_positions.keys()) | set(subportfolio.short_positions.keys())
+    
+    # 找出g.trade_history中有记录但实际已平仓的合约
+    stale_records = []
+    for security in g.trade_history.keys():
+        if security not in actual_positions:
+            stale_records.append(security)
+    
+    # 清理这些过期记录
+    if stale_records:
+        log.info("=" * 60)
+        log.info("发现持仓记录与实际持仓不同步,进行清理:")
+        for security in stale_records:
+            trade_info = g.trade_history[security]
+            underlying_symbol = security.split('.')[0][:-4]
+            log.info(f"  清理过期记录: {underlying_symbol}({security}) {trade_info['direction']}, "
+                    f"成本价: {trade_info['entry_price']:.2f}, "
+                    f"入场时间: {trade_info['entry_time']}")
+            del g.trade_history[security]
+        log.info(f"共清理 {len(stale_records)} 条过期持仓记录")
+        log.info("=" * 60)
+
+def after_market_close(context):
+    """收盘后运行函数"""
+    log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time())))
+    
+    # 同步检查:清理g.trade_history中已平仓但记录未删除的持仓
+    sync_trade_history_with_positions(context)
+    
+    # 清空候选列表(每天重新检查)
+    g.daily_ma_candidates = {}
+    
+    # 清空排除缓存(每天重新检查)
+    excluded_count = len(g.excluded_contracts)
+    if excluded_count > 0:
+        log.info(f"清空排除缓存,共 {excluded_count} 个合约")
+        g.excluded_contracts = {}
+    
+    # 重置夜盘禁止操作标志
+    if g.night_session_blocked:
+        log.info(f"重置夜盘禁止操作标志")
+        g.night_session_blocked = False
+        g.night_session_blocked_trading_day = None
+    
+    # 只有当天有交易时才打印统计信息
+    if g.today_trades:
+        print_daily_trading_summary(context)
+        
+        # 清空当日交易记录
+        g.today_trades = []
+    
+    log.info('##############################################################')
+
+def print_daily_trading_summary(context):
+    """打印当日交易汇总"""
+    if not g.today_trades:
+        return
+    
+    log.info("\n=== 当日交易汇总 ===")
+    total_margin = 0
+    
+    for trade in g.today_trades:
+        if trade['order_amount'] > 0:  # 开仓
+            log.info(f"开仓 {trade['underlying_symbol']} {trade['direction']} {trade['order_amount']}手 "
+                  f"价格:{trade['order_price']:.2f} 保证金:{trade['cash_change']:.0f}")
+            total_margin += trade['cash_change']
+        else:  # 平仓
+            log.info(f"平仓 {trade['underlying_symbol']} {trade['direction']} {abs(trade['order_amount'])}手 "
+                  f"价格:{trade['order_price']:.2f}")
+    
+    log.info(f"当日保证金占用: {total_margin:.0f}")
+    log.info("==================\n")
+
+########################## 自动移仓换月函数 #################################
+def position_auto_switch(context, pindex=0, switch_func=None, callback=None):
+    """期货自动移仓换月"""
+    import re
+    subportfolio = context.subportfolios[pindex]
+    symbols = set(subportfolio.long_positions.keys()) | set(subportfolio.short_positions.keys())
+    switch_result = []
+    for symbol in symbols:
+        match = re.match(r"(?P<underlying_symbol>[A-Z]{1,})", symbol)
+        if not match:
+            raise ValueError("未知期货标的: {}".format(symbol))
+        else:
+            underlying_symbol = match.groupdict()["underlying_symbol"]
+            trading_start = get_futures_config(underlying_symbol, 'trading_start_time', None)
+            has_night_session = get_futures_config(underlying_symbol, 'has_night_session', False)
+            # log.debug(f"移仓换月: {symbol}, 交易开始时间: {trading_start}, 夜盘: {has_night_session}")
+            if trading_start and not has_reached_trading_start(context.current_dt, trading_start, has_night_session):
+                # log.info("{} 当前时间 {} 未到达交易开始时间 {} (夜盘:{} ),跳过移仓".format(
+                #     symbol,
+                #     context.current_dt.strftime('%H:%M:%S'),
+                #     trading_start,
+                #     has_night_session
+                # ))
+                continue
+            dominant = get_dominant_future(underlying_symbol)
+            cur = get_current_data()
+            symbol_last_price = cur[symbol].last_price
+            dominant_last_price = cur[dominant].last_price
+            
+            if dominant > symbol:
+                for positions_ in (subportfolio.long_positions, subportfolio.short_positions):
+                    if symbol not in positions_.keys():
+                        continue
+                    else :
+                        p = positions_[symbol]
+
+                    if switch_func is not None:
+                        switch_func(context, pindex, p, dominant)
+                    else:
+                        amount = p.total_amount
+                        # 跌停不能开空和平多,涨停不能开多和平空
+                        if p.side == "long":
+                            symbol_low_limit = cur[symbol].low_limit
+                            dominant_high_limit = cur[dominant].high_limit
+                            if symbol_last_price <= symbol_low_limit:
+                                log.warning("标的{}跌停,无法平仓。移仓换月取消。".format(symbol))
+                                continue
+                            elif dominant_last_price >= dominant_high_limit:
+                                log.warning("标的{}涨停,无法开仓。移仓换月取消。".format(dominant))
+                                continue
+                            else:
+                                log.info("进行移仓换月: ({0},long) -> ({1},long)".format(symbol, dominant))
+                                order_old = order_target(symbol, 0, side='long')
+                                if order_old != None and order_old.filled > 0:
+                                    order_new = order_target(dominant, amount, side='long')
+                                    if order_new != None and order_new.filled > 0:
+                                        switch_result.append({"before": symbol, "after": dominant, "side": "long"})
+                                        # 换月成功,更新交易记录
+                                        if symbol in g.trade_history:
+                                            # 复制旧的交易记录作为基础
+                                            old_entry_price = g.trade_history[symbol]['entry_price']
+                                            g.trade_history[dominant] = g.trade_history[symbol].copy()
+
+                                            # 更新成本价为新合约的实际开仓价
+                                            new_entry_price = None
+                                            if order_new.avg_cost and order_new.avg_cost > 0:
+                                                new_entry_price = order_new.avg_cost
+                                                g.trade_history[dominant]['entry_price'] = order_new.avg_cost
+                                            elif order_new.price and order_new.price > 0:
+                                                new_entry_price = order_new.price
+                                                g.trade_history[dominant]['entry_price'] = order_new.price
+                                            else:
+                                                # 如果订单价格无效,使用当前价格作为成本价
+                                                new_entry_price = dominant_last_price
+                                                g.trade_history[dominant]['entry_price'] = dominant_last_price
+
+                                            # 更新入场时间
+                                            g.trade_history[dominant]['entry_time'] = context.current_dt
+                                            # 更新入场交易日
+                                            g.trade_history[dominant]['entry_trading_day'] = get_current_trading_day(context.current_dt)
+                                            # 删除旧合约的交易记录
+                                            del g.trade_history[symbol]
+
+                                            log.info(f"移仓换月成本价更新: {symbol} -> {dominant}, "
+                                                   f"旧成本价: {old_entry_price:.2f}, 新成本价: {new_entry_price:.2f}")
+                                    else:
+                                        log.warning("标的{}交易失败,无法开仓。移仓换月失败。".format(dominant))
+                        if p.side == "short":
+                            symbol_high_limit = cur[symbol].high_limit
+                            dominant_low_limit = cur[dominant].low_limit
+                            if symbol_last_price >= symbol_high_limit:
+                                log.warning("标的{}涨停,无法平仓。移仓换月取消。".format(symbol))
+                                continue
+                            elif dominant_last_price <= dominant_low_limit:
+                                log.warning("标的{}跌停,无法开仓。移仓换月取消。".format(dominant))
+                                continue
+                            else:
+                                log.info("进行移仓换月: ({0},short) -> ({1},short)".format(symbol, dominant))
+                                order_old = order_target(symbol, 0, side='short')
+                                if order_old != None and order_old.filled > 0:
+                                    order_new = order_target(dominant, amount, side='short')
+                                    if order_new != None and order_new.filled > 0:
+                                        switch_result.append({"before": symbol, "after": dominant, "side": "short"})
+                                        # 换月成功,更新交易记录
+                                        if symbol in g.trade_history:
+                                            # 复制旧的交易记录作为基础
+                                            old_entry_price = g.trade_history[symbol]['entry_price']
+                                            g.trade_history[dominant] = g.trade_history[symbol].copy()
+
+                                            # 更新成本价为新合约的实际开仓价
+                                            new_entry_price = None
+                                            if order_new.avg_cost and order_new.avg_cost > 0:
+                                                new_entry_price = order_new.avg_cost
+                                                g.trade_history[dominant]['entry_price'] = order_new.avg_cost
+                                            elif order_new.price and order_new.price > 0:
+                                                new_entry_price = order_new.price
+                                                g.trade_history[dominant]['entry_price'] = order_new.price
+                                            else:
+                                                # 如果订单价格无效,使用当前价格作为成本价
+                                                new_entry_price = dominant_last_price
+                                                g.trade_history[dominant]['entry_price'] = dominant_last_price
+
+                                            # 更新入场时间
+                                            g.trade_history[dominant]['entry_time'] = context.current_dt
+                                            # 更新入场交易日
+                                            g.trade_history[dominant]['entry_trading_day'] = get_current_trading_day(context.current_dt)
+                                            # 删除旧合约的交易记录
+                                            del g.trade_history[symbol]
+
+                                            log.info(f"移仓换月成本价更新: {symbol} -> {dominant}, "
+                                                   f"旧成本价: {old_entry_price:.2f}, 新成本价: {new_entry_price:.2f}")
+                                    else:
+                                        log.warning("标的{}交易失败,无法开仓。移仓换月失败。".format(dominant))
+                        if callback:
+                            callback(context, pindex, p, dominant)
+    return switch_result
+

+ 1 - 0
pyproject.toml

@@ -7,6 +7,7 @@ requires-python = ">=3.11"
 dependencies = [
     "beautifulsoup4>=4.14.2",
     "matplotlib>=3.10.3",
+    "nltk>=3.9.2",
     "pandas>=2.3.1",
     "requests>=2.32.5",
 ]

+ 151 - 0
uv.lock

@@ -101,6 +101,27 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
 ]
 
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
 [[package]]
 name = "contourpy"
 version = "1.3.2"
@@ -206,6 +227,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
 ]
 
+[[package]]
+name = "joblib"
+version = "1.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
+]
+
 [[package]]
 name = "jukuan"
 version = "0.1.0"
@@ -213,6 +243,7 @@ source = { virtual = "." }
 dependencies = [
     { name = "beautifulsoup4" },
     { name = "matplotlib" },
+    { name = "nltk" },
     { name = "pandas" },
     { name = "requests" },
 ]
@@ -221,6 +252,7 @@ dependencies = [
 requires-dist = [
     { name = "beautifulsoup4", specifier = ">=4.14.2" },
     { name = "matplotlib", specifier = ">=3.10.3" },
+    { name = "nltk", specifier = ">=3.9.2" },
     { name = "pandas", specifier = ">=2.3.1" },
     { name = "requests", specifier = ">=2.32.5" },
 ]
@@ -334,6 +366,21 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" },
 ]
 
+[[package]]
+name = "nltk"
+version = "3.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "joblib" },
+    { name = "regex" },
+    { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" },
+]
+
 [[package]]
 name = "numpy"
 version = "2.3.1"
@@ -556,6 +603,98 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
 ]
 
+[[package]]
+name = "regex"
+version = "2025.11.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" },
+    { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" },
+    { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" },
+    { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" },
+    { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" },
+    { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" },
+    { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" },
+    { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" },
+    { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" },
+    { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" },
+    { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" },
+    { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
+    { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
+    { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
+    { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
+    { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
+    { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
+    { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
+    { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
+    { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
+    { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
+    { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
+    { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
+    { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
+    { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
+    { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
+    { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
+    { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
+    { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
+    { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
+    { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
+    { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
+    { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
+    { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
+    { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
+]
+
 [[package]]
 name = "requests"
 version = "2.32.5"
@@ -589,6 +728,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
 ]
 
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.15.0"