浏览代码

更新均线形态交易策略 v002,新增极端趋势过滤器以提高交易信号的准确性,并优化开仓逻辑。调整开盘价差阈值,支持三种策略模式,增强了策略的灵活性与风险控制。同时更新README文档,详细说明策略逻辑与参数配置,提升可操作性。

maxfeng 2 周之前
父节点
当前提交
d6c25caef3

+ 1 - 1
Lib/future/MAPatternStrategy_v001.py

@@ -157,7 +157,7 @@ def initialize(context):
     
     # 策略品种选择策略配置
     # 方案1:全品种策略 - 考虑所有配置的期货品种
-    g.strategy_focus_symbols = []  # 空列表表示考虑所有品种
+    g.strategy_focus_symbols = ['IC', 'LH']  # 空列表表示考虑所有品种
     
     # 方案2:精选品种策略 - 只交易流动性较好的特定品种(如需使用请取消下行注释)
     # g.strategy_focus_symbols = ['RM', 'CJ', 'CY', 'JD', 'L', 'LC', 'SF', 'SI']

+ 261 - 132
Lib/future/MAPatternStrategy_v002.py

@@ -50,15 +50,16 @@ def initialize(context):
     # 均线策略参数
     g.ma_periods = [5, 10, 20, 30]  # 均线周期
     g.ma_historical_days = 60  # 获取历史数据天数(确保足够计算MA30)
-    g.ma_open_gap_threshold = 0.002  # 方案1开盘价差阈值(0.2%)
+    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_gap_strategy_mode = 2  # 策略模式选择(1: 原方案, 2: 新方案)
-    g.ma_open_gap_threshold2 = 0.002  # 方案2开盘价差阈值(0.2%)
+    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%)
     
     # 止损止盈策略参数
@@ -76,6 +77,7 @@ def initialize(context):
     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"  是否检查日内价差: {g.check_intraday_spread}")
     log.info(f"  固定止损: {g.fixed_stop_loss_rate:.1%}")
@@ -180,27 +182,23 @@ def initialize(context):
     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')
     
-    # 盘中价差检查和开仓(14:35和14:55)
-    run_daily(check_intraday_price_diff, time='14:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_intraday_price_diff, time='14:55:00', reference_security='IF1808.CCFX')
-    
-    # 夜盘止损止盈检查
-    run_daily(check_stop_loss_profit, time='21:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='21:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='22:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='22:35:00', reference_security='IF1808.CCFX')
-    
-    # 日盘止损止盈检查
-    run_daily(check_stop_loss_profit, time='09:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='09:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='10:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='10:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='11:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='11:25:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='13:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='14:05:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='14:35:00', reference_security='IF1808.CCFX')
-    run_daily(check_stop_loss_profit, time='14:55: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(after_market_close, time='15:30:00', reference_security='IF1808.CCFX')
@@ -389,6 +387,28 @@ def check_ma_trend_and_open_gap(context):
                 }
                 continue
             
+            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},跳过"
+                )
+                g.excluded_contracts[dominant_future] = {
+                    'reason': 'ma_extreme_trend',
+                    'trading_day': current_trading_day
+                }
+                continue
+
             # 判断均线走势(使用新的灵活模式检查)
             direction = None
             if check_ma_pattern(ma_values, 'long'):
@@ -440,13 +460,13 @@ def check_ma_trend_and_open_gap(context):
                 elif direction == 'short' and open_gap_ratio <= -g.ma_open_gap_threshold:
                     log.info(f"  {symbol}({dominant_future}) ✓ 方案1空头开盘价差检查通过 ({open_gap_ratio:.2%} <= {-g.ma_open_gap_threshold:.2%})")
                     gap_check_passed = True
-            elif g.ma_gap_strategy_mode == 2:
-                # 方案2:多头检查下跳,空头检查上跳
+            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}) ✓ 方案2多头开盘价差检查通过 ({open_gap_ratio:.2%} <= {-g.ma_open_gap_threshold2:.2%})")
+                    log.info(f"  {symbol}({dominant_future}) ✓ 方案{g.ma_gap_strategy_mode}多头开盘价差检查通过 ({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}) ✓ 方案2空头开盘价差检查通过 ({open_gap_ratio:.2%} >= {g.ma_open_gap_threshold2:.2%})")
+                    log.info(f"  {symbol}({dominant_future}) ✓ 方案{g.ma_gap_strategy_mode}空头开盘价差检查通过 ({open_gap_ratio:.2%} >= {g.ma_open_gap_threshold2:.2%})")
                     gap_check_passed = True
             
             if not gap_check_passed:
@@ -458,12 +478,16 @@ def check_ma_trend_and_open_gap(context):
                 }
                 continue
             
+            # 获取前一日开盘价(用于方案3)
+            yesterday_open = yesterday_data['open']
+            
             # 将通过检查的品种加入候选列表
             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
             }
             
@@ -477,106 +501,8 @@ def check_ma_trend_and_open_gap(context):
     log.info(f"候选列表更新完成,当前候选品种: {list(g.daily_ma_candidates.keys())}")
     log.info("=" * 60)
 
-def check_intraday_price_diff(context):
-    """阶段二:盘中价差检查和开仓(14:35和14:55)"""
-    log.info("=" * 60)
-    log.info(f"执行当天价差检查和开仓逻辑 - 时间: {context.current_dt}")
-    log.info("=" * 60)
-    
-    # 先检查换月移仓
-    position_auto_switch(context)
-    
-    if not g.daily_ma_candidates:
-        log.info("当前无候选品种,跳过")
-        return
-    
-    log.info(f"候选品种数量: {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']
-            
-            # 再次检查是否已有持仓
-            if check_symbol_prefix_match(dominant_future, 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 = False
-            
-            if g.ma_gap_strategy_mode == 1:
-                # 方案1:根据参数决定是否检查日内价差
-                if not g.check_intraday_spread:
-                    # 跳过日内价差检查,直接允许开仓
-                    log.info(f"  方案1跳过日内价差检查(check_intraday_spread=False)")
-                    should_open = True
-                elif direction == 'long' and intraday_diff > 0:
-                    log.info(f"  ✓ 方案1多头当天价差检查通过 ({intraday_diff:.2f} > 0)")
-                    should_open = True
-                elif direction == 'short' and intraday_diff < 0:
-                    log.info(f"  ✓ 方案1空头当天价差检查通过 ({intraday_diff:.2f} < 0)")
-                    should_open = True
-                else:
-                    log.info(f"  ✗ 方案1当天价差不符合{direction}方向要求")
-            elif g.ma_gap_strategy_mode == 2:
-                # 方案2:强制检查日内变化,使用专用阈值
-                if direction == 'long' and intraday_diff_ratio >= g.ma_intraday_threshold_scheme2:
-                    log.info(f"  ✓ 方案2多头日内变化检查通过 ({intraday_diff_ratio:.2%} >= {g.ma_intraday_threshold_scheme2:.2%})")
-                    should_open = True
-                elif direction == 'short' and intraday_diff_ratio <= -g.ma_intraday_threshold_scheme2:
-                    log.info(f"  ✓ 方案2空头日内变化检查通过 ({intraday_diff_ratio:.2%} <= {-g.ma_intraday_threshold_scheme2:.2%})")
-                    should_open = True
-                else:
-                    log.info(f"  ✗ 方案2日内变化不符合{direction}方向要求(阈值: ±{g.ma_intraday_threshold_scheme2:.2%})")
-            
-            if should_open:
-                # 执行开仓
-                log.info(f"  准备开仓: {symbol} {direction}")
-                target_hands = calculate_target_hands(context, dominant_future, direction)
-                
-                if target_hands > 0:
-                    success = open_position(context, dominant_future, target_hands, direction, 
-                                          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)
-
-def check_stop_loss_profit(context):
-    """阶段三:止损止盈检查(所有时间点)"""
+def check_open_and_stop(context):
+    """统一的开仓和止损止盈检查函数"""
     # 先检查换月移仓
     position_auto_switch(context)
     
@@ -586,7 +512,120 @@ def check_stop_loss_profit(context):
     # 判断是否为夜盘时间
     is_night_session = (current_time in ['21', '22', '23', '00', '01', '02'])
     
-    # 遍历所有持仓进行止损止盈检查
+    # 第一步:检查开仓条件
+    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, 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 = False
+                
+                if g.ma_gap_strategy_mode == 1:
+                    # 方案1:根据参数决定是否检查日内价差
+                    if not g.check_intraday_spread:
+                        log.info(f"  方案1跳过日内价差检查(check_intraday_spread=False)")
+                        should_open = True
+                    elif direction == 'long' and intraday_diff > 0:
+                        log.info(f"  ✓ 方案1多头当天价差检查通过 ({intraday_diff:.2f} > 0)")
+                        should_open = True
+                    elif direction == 'short' and intraday_diff < 0:
+                        log.info(f"  ✓ 方案1空头当天价差检查通过 ({intraday_diff:.2f} < 0)")
+                        should_open = True
+                    else:
+                        log.info(f"  ✗ 方案1当天价差不符合{direction}方向要求")
+                        
+                elif g.ma_gap_strategy_mode == 2:
+                    # 方案2:强制检查日内变化,使用专用阈值
+                    if direction == 'long' and intraday_diff_ratio >= g.ma_intraday_threshold_scheme2:
+                        log.info(f"  ✓ 方案2多头日内变化检查通过 ({intraday_diff_ratio:.2%} >= {g.ma_intraday_threshold_scheme2:.2%})")
+                        should_open = True
+                    elif direction == 'short' and intraday_diff_ratio <= -g.ma_intraday_threshold_scheme2:
+                        log.info(f"  ✓ 方案2空头日内变化检查通过 ({intraday_diff_ratio:.2%} <= {-g.ma_intraday_threshold_scheme2:.2%})")
+                        should_open = True
+                    else:
+                        log.info(f"  ✗ 方案2日内变化不符合{direction}方向要求(阈值: ±{g.ma_intraday_threshold_scheme2:.2%})")
+                        
+                elif g.ma_gap_strategy_mode == 3:
+                    # 方案3:下跳后上涨(多头)或上跳后下跌(空头),并检查当前价格与前一日开盘收盘均值的关系
+                    if yesterday_open is not None and yesterday_close is not None:
+                        prev_day_avg = (yesterday_open + yesterday_close) / 2
+                        log.debug(f"  前一日开盘价: {yesterday_open:.2f}, 前一日收盘价: {yesterday_close:.2f}, 前一日开盘收盘均值: {prev_day_avg:.2f}")
+                        
+                        if direction == 'long':
+                            # 多头:当前价格 >= 前一日开盘收盘均值
+                            if current_price >= prev_day_avg:
+                                log.info(f"  ✓ 方案3多头入场条件通过: 当前价 {current_price:.2f} >= 前日均值 {prev_day_avg:.2f}")
+                                should_open = True
+                            else:
+                                log.info(f"  ✗ 方案3多头入场条件未通过: 当前价 {current_price:.2f} < 前日均值 {prev_day_avg:.2f}")
+                        elif direction == 'short':
+                            # 空头:当前价格 <= 前一日开盘收盘均值
+                            if current_price <= prev_day_avg:
+                                log.info(f"  ✓ 方案3空头入场条件通过: 当前价 {current_price:.2f} <= 前日均值 {prev_day_avg:.2f}")
+                                should_open = True
+                            else:
+                                log.info(f"  ✗ 方案3空头入场条件未通过: 当前价 {current_price:.2f} > 前日均值 {prev_day_avg:.2f}")
+                    else:
+                        log.info(f"  ✗ 方案3缺少前一日开盘或收盘价数据")
+                
+                if should_open:
+                    # 执行开仓
+                    log.info(f"  准备开仓: {symbol} {direction}")
+                    target_hands = calculate_target_hands(context, dominant_future, direction)
+                    
+                    if target_hands > 0:
+                        success = open_position(context, dominant_future, target_hands, direction, 
+                                              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)
+    
+    # 第二步:检查止损止盈
     subportfolio = context.subportfolios[0]
     long_positions = list(subportfolio.long_positions.values())
     short_positions = list(subportfolio.short_positions.values())
@@ -627,6 +666,13 @@ def check_position_stop_loss_profit(context, position):
     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
     
     # 计算当前盈亏比率
@@ -641,7 +687,11 @@ def check_position_stop_loss_profit(context, position):
                 f"成本价: {entry_price:.2f}, 当前价格: {current_price:.2f}")
         close_position(context, security, direction)
         return True
-    
+
+    if entry_trading_day is not None and entry_trading_day == current_trading_day:
+        log.info(f"{security} 建仓交易日内跳过动态止盈检查")
+        return False
+
     # 检查是否启用均线跟踪止盈
     if not trade_info.get('ma_trailing_enabled', True):
         return False
@@ -755,6 +805,42 @@ def calculate_ma_proximity_counts(data, periods, lookback_days):
     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_ma_pattern(ma_values, direction):
     """检查均线排列模式是否符合方向要求
     
@@ -858,13 +944,15 @@ def open_position(context, security, target_hands, direction, reason=''):
             })
             
             # 记录交易信息
+            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': cash_change,
                 'direction': direction,
-                'entry_time': context.current_dt
+                'entry_time': context.current_dt,
+                'entry_trading_day': entry_trading_day
             }
 
             ma_trailing_enabled = True
@@ -941,6 +1029,35 @@ def get_multiplier(underlying_symbol, default_multiplier=10):
     """获取合约乘数的辅助函数"""
     return g.futures_config.get(underlying_symbol, {}).get('multiplier', default_multiplier)
 
+
+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
@@ -1060,7 +1177,19 @@ def position_auto_switch(context, pindex=0, switch_func=None, callback=None):
         if not match:
             raise ValueError("未知期货标的: {}".format(symbol))
         else:
-            dominant = get_dominant_future(match.groupdict()["underlying_symbol"])
+            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

+ 237 - 0
Lib/future/MAPatternStrategy_v002_核心逻辑.md

@@ -0,0 +1,237 @@
+# 均线形态交易策略 v002 核心逻辑详解
+
+## 概述
+本策略基于均线走势(前提条件)+ K线形态(跳空、价格验证)的期货交易策略,通过两阶段筛选和执行机制进行交易。
+
+---
+
+## 开仓逻辑结构总览
+
+开仓验证包含两个独立阶段,每个阶段都有明确的检查项:
+
+### 第一阶段:均线走势和跳空检查
+**位置**:`check_ma_trend_and_open_gap`函数
+**目的**:筛选出符合均线趋势要求且满足跳空条件的品种
+**检查内容**:
+1. 均线贴近度
+2. 极端趋势过滤
+3. 均线排列模式
+4. 历史模式一致性
+5. **跳空方向检查**(必选)- 趋势跟随或逆势操作
+6. **跳空幅度检查**(可选)- 是否达到阈值
+
+### 第二阶段:最终价格验证和开仓
+**位置**:`check_open_and_stop`函数
+**目的**:对候选品种进行最终价格验证,确认开仓时机
+**检查内容**:
+- **策略1**:可选的日内价差检查
+- **策略2**:强制的日内变化阈值检查
+- **策略3**:价格回归到前日开盘收盘均值检查
+
+---
+
+## 详细逻辑说明
+
+开仓验证包含两个独立阶段,每个阶段都有明确的检查项:
+
+---
+
+### 第一阶段:均线走势和跳空检查(函数:check_ma_trend_and_open_gap)
+
+#### 执行时间点
+- **夜盘开盘**:21:05:00(仅检查21:00开盘的品种)
+- **日盘早盘**:09:05:00(检查21:00和09:00开盘的品种)
+- **日盘晚开**:09:35:00(检查所有品种,包括09:30开盘的)
+
+#### 筛选条件(必须全部满足)
+
+**1. 均线贴近度检查**
+   - 统计过去10天收盘价贴近各均线的次数
+   - MA5贴近次数 + MA10贴近次数 ≥ 8次(`g.ma_proximity_min_threshold = 8`)
+   - 贴近定义为:收盘价距离某条均线最近的均线
+
+**2. 极端趋势过滤**
+   - 统计过去10天收盘价在所有均线上方/下方的天数
+   - 过滤条件:min(上方天数, 下方天数) ≥ 4天(`g.ma_pattern_extreme_days_threshold = 4`)
+   - 如果多空极端趋势同时出现且都达到阈值,则跳过该品种
+
+**3. 均线排列模式检查**
+   - **多头模式**:满足以下任一模式
+     - MA30 ≤ MA20 ≤ MA10 ≤ MA5
+     - MA30 ≤ MA20 ≤ MA5 ≤ MA10
+   - **空头模式**:满足以下任一模式
+     - MA10 ≤ MA5 ≤ MA20 ≤ MA30
+     - MA5 ≤ MA10 ≤ MA20 ≤ MA30
+
+**4. 历史均线模式一致性检查**
+   - 检查过去10天的均线模式一致性
+   - 一致性比例 ≥ 80%(`g.ma_pattern_consistency_threshold = 0.8`)
+   - 即10天中至少有8天符合当前方向的均线排列
+
+**5. 跳空方向检查**(第一部分,必选)
+   - **策略1 - 趋势跟随**(`g.ma_gap_strategy_mode = 1`):
+     - 看涨趋势:必须向上跳空(开盘价 > 昨收价)
+     - 看跌趋势:必须向下跳空(开盘价 < 昨收价)
+   - **策略2 - 逆势操作**(`g.ma_gap_strategy_mode = 2`):
+     - 看涨趋势:必须向下跳空(开盘价 < 昨收价)
+     - 看跌趋势:必须向上跳空(开盘价 > 昨收价)
+   - **策略3 - 逆势操作**(`g.ma_gap_strategy_mode = 3`):
+     - 看涨趋势:必须向下跳空(开盘价 < 昨收价)
+     - 看跌趋势:必须向上跳空(开盘价 > 昨收价)
+
+**6. 跳空幅度检查**(第二部分,可选)
+   - **选项A**(`g.check_gap_magnitude = True`):检查跳空幅度与阈值比较
+     - 策略1:`|开盘价差比例| >= 0.2%`(`g.ma_open_gap_threshold = 0.002`)
+     - 策略2/3:`|开盘价差比例| >= 0.2%`(`g.ma_open_gap_threshold2 = 0.002`)
+   - **选项B**(`g.check_gap_magnitude = False`):不验证跳空幅度,只要方向正确即可
+
+#### 通过条件后的处理
+- 将品种加入候选列表 `g.daily_ma_candidates`
+- 记录方向、开盘价、昨收价、前一日开盘价、均线值等信息
+
+---
+
+### 第二阶段:最终价格验证和开仓执行(函数:check_open_and_stop)
+
+#### 执行时间点
+**夜盘**:21:05, 21:35, 22:05, 22:35
+**日盘**:09:05, 09:35, 10:05, 10:35, 11:05, 11:25, 13:35, 14:05, 14:35, 14:55
+
+#### 开仓前最后一道关卡:最终价格验证
+
+在每个时间点,对候选列表中的品种进行最终价格验证检查:
+
+**策略1 - 趋势跟随**(`g.ma_gap_strategy_mode = 1`):
+   - **选项A**(`g.check_intraday_spread = False`):
+     - 跳过日内价差检查,直接通过验证
+   - **选项B**(`g.check_intraday_spread = True`):
+     - 看涨趋势:当天价差 > 0(当前价 > 开盘价)
+     - 看跌趋势:当天价差 < 0(当前价 < 开盘价)
+
+**策略2 - 逆势操作+强制阈值**(`g.ma_gap_strategy_mode = 2`):
+   - 看涨趋势:当天变化比例 ≥ +0.5%(`g.ma_intraday_threshold_scheme2 = 0.005`)
+   - 看跌趋势:当天变化比例 ≤ -0.5%
+   - 说明:确保价格在下跳后有足够的反弹幅度
+
+**策略3 - 逆势操作+价格回归**(`g.ma_gap_strategy_mode = 3`):
+   - 看涨趋势:当前价格 ≥ (前一日开盘价 + 前一日收盘价) / 2
+   - 看跌趋势:当前价格 ≤ (前一日开盘价 + 前一日收盘价) / 2
+   - 说明:确保价格在下跳后回归到前一日开盘收盘均值附近
+
+#### 开仓执行流程
+1. 计算目标手数:
+   - 单手保证金 = 当前价 × 合约乘数 × 保证金比例
+   - 最大开仓手数 = min(最大保证金限制/单手保证金, 可用资金×80%/单手保证金)
+   - 单个标的最大持仓保证金限制:20,000元(`g.max_margin_per_position = 20000`)
+2. 执行开仓并记录交易信息
+3. 从候选列表中移除已开仓品种
+
+#### 止损止盈检查
+在完成开仓检查后,同时检查所有持仓的止损止盈条件(详见下方止损止盈逻辑部分)
+
+---
+
+## 止损逻辑
+
+### 执行时间点
+与开仓检查同步执行(第二阶段的所有时间点)
+
+### 止损条件
+1. **固定止损**
+   - 亏损比例 ≤ -1%(`g.fixed_stop_loss_rate = 0.01`)
+   - 计算公式:
+     - 多头:`(当前价 - 开仓价) / 开仓价`
+     - 空头:`(开仓价 - 当前价) / 开仓价`
+   - 触发后立即平仓,防止损失扩大
+
+2. **交易时间适配性检查**
+   - 夜盘时间只检查支持夜盘交易的品种
+   - 无夜盘品种在夜盘时间跳过止损检查
+
+---
+
+## 止盈逻辑
+
+### 执行时间点
+与开仓检查和止损检查同步执行(第二阶段的所有时间点)
+**建仓交易日内跳过动态止盈检查**
+
+### 止盈策略:均线跟踪止盈
+#### 启用条件
+- 持仓超过建仓交易日
+- 均线跟踪功能启用(`ma_trailing_enabled = True`)
+- 特殊情况:多头开仓价 < MA5时禁用均线跟踪
+
+#### 动态止盈参数选择
+1. **时间相关偏移量**:
+   - 14:55之后:偏移量 1%(`g.ma_offset_ratio_close = 0.01`)
+   - 其他时间:偏移量 0.3%(`g.ma_offset_ratio_normal = 0.003`)
+
+2. **均线选择逻辑**:
+   - 持仓天数 ≤ 4天(`g.days_for_adjustment = 4`):使用MA5
+   - 持仓天数 > 4天:
+     - 当日变化率 ≥ 1.2倍日均变化率:使用MA5
+     - 当日变化率 < 1.2倍日均变化率:使用MA10
+   - 波动剧烈时(当日变化率 ≥ 1.5倍日均变化率):强制使用MA5
+
+#### 止盈触发条件
+1. **计算调整后均线值**:
+   - 多头:调整后均线值 = 均线值 × (1 - 偏移量)
+   - 空头:调整后均线值 = 均线值 × (1 + 偏移量)
+
+2. **触发条件**:
+   - 多头:当前价 < 调整后均线值
+   - 空头:当前价 > 调整后均线值
+
+3. **平仓执行**:
+   - 触发条件后立即平仓
+   - 记录止盈原因:使用的均线、均线值、调整后值、当前价、持仓天数
+
+---
+
+## 其他重要机制
+
+### 自动换月移仓
+- 在每次策略执行前检查
+- 当主力合约发生变化时自动移仓
+- 考虑涨跌停板限制,避免极端情况下的移仓失败
+
+### 缓存机制
+- **排除缓存**:记录当日不符合条件的合约,避免重复检查
+- **均线检查缓存**:记录每个品种在交易日的均线检查状态
+- **候选列表**:存储通过第一阶段检查的候选品种
+
+### 风险控制参数
+- 最大资金使用比例:80%(`g.usage_percentage = 0.8`)
+- 单个标的最大持仓保证金:20,000元
+- 固定止损比例:1%
+- 均线贴近度最低要求:8次
+- 极端趋势过滤阈值:4天
+- 历史一致性要求:80%
+
+### 策略参数配置
+
+**基础参数**
+- 均线周期:[5, 10, 20, 30]
+- 历史数据天数:60天(确保足够计算MA30)
+- 历史均线模式检查天数:10天
+
+**三种策略模式**(`g.ma_gap_strategy_mode`)
+
+**策略1 - 趋势跟随**:
+- 跳空方向:与趋势一致(看涨上跳/看跌下跳)
+- 跳空幅度:可选检查(`g.check_gap_magnitude`)
+- 最终价格验证:日内价差可选检查(`g.check_intraday_spread`)
+- 适用场景:追随趋势方向的强势突破
+
+**策略2 - 逆势操作+强制阈值**:
+- 跳空方向:与趋势相反(看涨下跳/看跌上跳)
+- 跳空幅度:可选检查(`g.check_gap_magnitude`)
+- 最终价格验证:强制日内变化阈值≥0.5%(`g.ma_intraday_threshold_scheme2`)
+- 适用场景:捕捉回调后的强力反弹
+
+**策略3 - 逆势操作+价格回归**:
+- 跳空方向:与趋势相反(看涨下跳/看跌上跳)
+- 跳空幅度:可选检查(`g.check_gap_magnitude`)
+- 最终价格验证:价格回归到前日开盘收盘均值
+- 适用场景:捕捉回调后的均值回归行情

+ 75 - 1
Lib/future/README.md

@@ -6,8 +6,82 @@
 1. 期货交易里的`order_target_value`里的`value`建议用保证金的金额,而不是实际价格
 2. 期货交易里的`order_target_value`里的`side`建议用`long`或`short`,而不能为空
 
-## 沪深300期货蜘蛛网策略
+## MAPatternStrategy_v002 均线形态策略
+
+### 策略概览
+该策略围绕主力合约的多周期移动平均线(MA5、MA10、MA20、MA30)构建信号,首先判断趋势方向,再结合价差与日内表现决定是否开仓。策略采用`calculate_extreme_trend_days`作为极端波动过滤器,并配合固定止损和动态均线追踪止盈控制风险。默认仅交易`['IF', 'LH', 'AG', 'IC', 'B', 'EG']`等流动性较好的品种。
+
+### 趋势识别与开仓条件
+- **趋势方向(多头/空头)**:
+  - 多头排列:满足`MA30 ≤ MA20 ≤ MA10 ≤ MA5`或`MA30 ≤ MA20 ≤ MA5 ≤ MA10`。
+  - 空头排列:满足`MA10 ≤ MA5 ≤ MA20 ≤ MA30`或`MA5 ≤ MA10 ≤ MA20 ≤ MA30`。
+  - 判断逻辑由函数`check_ma_pattern`实现,是开仓前提。
+- **历史一致性检查**:
+  - 参数:`g.ma_pattern_lookback_days = 10`,`g.ma_pattern_consistency_threshold = 0.8`。
+  - 若过去10个交易日中有至少80%的天数符合当前趋势方向,则认为趋势稳定。
+- **极端趋势过滤器**:
+  - `calculate_extreme_trend_days`会统计过去10个交易日中,收盘价高于所有均线的天数A和低于所有均线的天数B。
+  - 当A和B都大于0且`min(A,B) ≥ max(2, g.ma_pattern_extreme_days_threshold)`(默认阈值4)时,视为多空急速转换,过滤该标的,不进入候选列表。
+- **开盘价差要求**:
+  - 策略模式由`g.ma_gap_strategy_mode`决定(默认2)。
+  - 方案1:多头需上跳≥`g.ma_open_gap_threshold`(默认0.002),空头需下跳≤`-g.ma_open_gap_threshold`。
+  - 方案2:多头需下跳≤`-g.ma_open_gap_threshold2`(默认0.002),空头需上跳≥`g.ma_open_gap_threshold2`。
+- **日内价差检查**:
+  - 方案1:若`g.check_intraday_spread = False`,则忽略日内价差;为True时,多头要求当日涨幅>0,空头要求跌幅<0。
+  - 方案2:必须满足日内相对变化绝对值≥`g.ma_intraday_threshold_scheme2`(默认0.005)。
+- **资金与手数**:
+  - 资金占用比例:`g.usage_percentage = 0.8`。
+  - 单品种保证金上限:`g.max_margin_per_position = 20000`。
+  - 手数依据保证金乘数和当前价格计算,确保至少1手。
+
+### 止损逻辑
+- **参数**:`g.fixed_stop_loss_rate = 0.01`。
+- **形式**:基于入场价的百分比亏损,当浮亏达到1%即触发止损,无需额外条件。
+- **触发流程**:每次定时任务执行`check_stop_loss_profit`时,计算当前价相对入场价的收益率(多头:`(现价-入场价)/入场价`,空头取反)。若收益率≤-1%,调用`close_position`平仓并记录日志。
+
+### 止盈逻辑
+- **均线追踪止盈**:根据持仓天数与波动程度选择参考均线,并使用偏移量形成动态出场线。
+  - 常规偏移:`g.ma_offset_ratio_normal = 0.003`。
+  - 收盘前偏移:`g.ma_offset_ratio_close = 0.01`。
+  - 进入交易日内不启用追踪止盈,以避免噪音。
+- **参考均线选择**:
+  - 若当日波动显著(涨跌幅≥1.5倍平均波动)或持仓未超过`g.days_for_adjustment = 4`天,则使用MA5。
+  - 否则根据波动大小在MA5与MA10之间切换。
+- **触发条件**:
+  - 多头:当前价 < 参考均线 × (1 - 偏移量)。
+  - 空头:当前价 > 参考均线 × (1 + 偏移量)。
+  - 满足条件即平仓,并在日志中记录触发原因与参数。
+
+### 关键参数一览
+| 参数名 | 默认值 | 含义 |
+| --- | --- | --- |
+| `g.ma_periods` | `[5, 10, 20, 30]` | 均线组合 |
+| `g.ma_pattern_lookback_days` | `10` | 历史趋势一致性统计天数 |
+| `g.ma_pattern_consistency_threshold` | `0.8` | 趋势一致性最小比例 |
+| `g.ma_pattern_extreme_days_threshold` | `4` | 极端趋势过滤阈值(min(A,B))|
+| `g.ma_gap_strategy_mode` | `2` | 开盘价差方案(1或2)|
+| `g.ma_open_gap_threshold` | `0.002` | 方案1开盘价差阈值 |
+| `g.ma_open_gap_threshold2` | `0.002` | 方案2开盘价差阈值 |
+| `g.ma_intraday_threshold_scheme2` | `0.005` | 方案2日内变化阈值 |
+| `g.fixed_stop_loss_rate` | `0.01` | 固定止损比例 |
+| `g.ma_offset_ratio_normal` | `0.003` | 常规追踪止盈偏移 |
+| `g.ma_offset_ratio_close` | `0.01` | 收盘前追踪止盈偏移 |
+| `g.days_for_adjustment` | `4` | 常规追踪止盈切换阈值 |
+| `g.usage_percentage` | `0.8` | 资金使用比例 |
+| `g.max_margin_per_position` | `20000` | 单品种保证金上限 |
+
+### 运行时序
+- 21:05、09:05、09:35:执行`check_ma_trend_and_open_gap`,依次检查趋势、极端过滤、开盘价差并更新候选列表。
+- 14:35、14:55:执行`check_intraday_price_diff`,根据日内表现决定是否开仓。
+- 多个时间点执行`check_stop_loss_profit`,同时负责止损、止盈与换月检查。
+
+### 日志与排错
+- 每次筛选都会输出趋势状态、均线贴近统计、极端趋势天数、开盘价差等详细日志。
+- 被过滤的合约会记录在`g.excluded_contracts`,原因包括`ma_extreme_trend`、`ma_trend`、`open_gap`等,便于复盘。
+
+---
 
+## 沪深300期货蜘蛛网策略
 ### 核心思路
 该策略基于期货主力合约的持仓数据进行交易,通过监控主力多空持仓变化来判断市场方向。策略重点关注机构持仓变动,通过多空持仓增量的对比来进行交易决策。
 

+ 34 - 0
Lib/future/Upgrade_log.md

@@ -0,0 +1,34 @@
+# MAPatternStrategy
+
+### 下跳涨回的策略
+
+#### Missing Open
+1. 20250926, EG2601, 上跳幅度太小只有0.19%
+2. 20250902, AU2512, 因为均线之前是纠缠的
+3. 20250911, IC2511, 下跳幅度太小只有0.1%
+
+#### Open
+1. 20250902, CJ2601, fail, 上涨趋势,但是低于MA20,上涨幅度不大在前一天长阴线的下半部分
+2. 20250909, SP2511, fail, 很难避免,可能是因为在支撑位附近有个小反弹
+3. 20250911, IF2509, fail, 上涨后期了
+4. 20250911, IC2509, fail, 开仓太晚,如果最晚是涨过前三天的高点就好了
+5. 20250924, CJ2601, fail, 下跌趋势,上跳跌回的收盘价都没有低过前一天开盘价和收盘价之间的高点
+6. 20251015, CU2511, fail, 上涨后期了,形态没毛病,可以开的早一点
+7. 20251016, CU2511, fail, 如果是长阳线后的下跳涨回得超过前一天收盘价;如果是长阴线后的上跳跌回得超过前一天收盘价
+
+#### Good
+1. 20250916, LH2511, big
+2. 20250923, AG2512, big
+3. 20250924, IF2510, medium, 可以更早点,高过前一天开盘价和收盘价之间的低点就可以
+4. 20250924, IC2510, medium, 可以更早点,高过前一天开盘价和收盘价之间的低点就可以
+5. 20250926, B2511, tiny, 可以更早点,高过前一天开盘价和收盘价之间的低点就可以
+6. 20250930, EG2601, medium
+
+#### Issue
+1. 20250917, EB2510, 为什么刚开仓就平仓?-solved
+2. 20250924, CJ2601, 为什么刚开仓就平仓?-solved
+3. 20250930, LH2511, 为什么要在夜盘平仓?-solved
+4. 20251010, ZN2511, 为什么刚开仓就平仓?-solved
+5. 20251013, ZN2511, 为什么刚开仓就平仓?-solved
+6. 20251013, IM2510, 为什么还能开仓?-solved
+7. 20250930, LH2511, 为什么会平仓-solved