Explorar el Código

feat(MAPatternStrategy): 增强均线形态交易策略,新增MA60及开仓记录快照功能

- 新增MA60均线周期,更新均线历史数据获取逻辑以支持更长周期计算
- 引入开仓记录快照,记录开仓时的价格和均线信息,提升策略透明度
- 更新CSV保存功能,增加开仓详情字段,便于后续分析
maxfeng hace 1 mes
padre
commit
3ef7c84902
Se han modificado 1 ficheros con 100 adiciones y 12 borrados
  1. 100 12
      Lib/future/MAPatternStrategy_v002.py

+ 100 - 12
Lib/future/MAPatternStrategy_v002.py

@@ -7,6 +7,8 @@ import math
 from datetime import date, datetime, timedelta, time
 import re
 
+MA_ADDITIONAL_HISTORY_DAYS = 30  # 计算均线时额外获取的交易日数量,用于支持更长周期
+
 # 顺势交易策略 v001
 # 基于均线走势(前提条件)+ K线形态(开盘价差、当天价差)的期货交易策略
 #
@@ -49,8 +51,9 @@ def initialize(context):
     g.max_margin_per_position = 30000  # 单个标的最大持仓保证金(元)
     
     # 均线策略参数
-    g.ma_periods = [5, 10, 20, 30]  # 均线周期
-    g.ma_historical_days = 60  # 获取历史数据天数(确保足够计算MA30)
+    g.ma_periods = [5, 10, 20, 30, 60]  # 均线周期(新增MA60)
+    g.ma_cross_periods = [5, 10, 20, 30]  # 参与均线穿越判断的均线周期
+    g.ma_historical_days = 60 + MA_ADDITIONAL_HISTORY_DAYS  # 额外增加30天,确保MA60计算稳定
     g.ma_open_gap_threshold = 0.001  # 方案1开盘价差阈值(0.2%)
     g.ma_pattern_lookback_days = 10  # 历史均线模式一致性检查的天数
     g.ma_pattern_consistency_threshold = 0.8  # 历史均线模式一致性阈值(80%)
@@ -77,6 +80,7 @@ def initialize(context):
     # 输出策略参数
     log.info("均线形态策略参数:")
     log.info(f"  均线周期: {g.ma_periods}")
+    log.info(f"  均线穿越判定周期: {g.ma_cross_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%}")
@@ -608,6 +612,13 @@ def check_open_and_stop(context):
                 
                 if should_open:
                     ma_values = candidate_info.get('ma_values') or {}
+                    avg_5day_change = calculate_recent_average_change(dominant_future, days=5)
+                    entry_snapshot = {
+                        'yesterday_close': yesterday_close,
+                        'today_open': open_price,
+                        'ma_values': ma_values.copy() if ma_values else {},
+                        'avg_5day_change': avg_5day_change
+                    }
                     cross_score, score_details = calculate_ma_cross_score(open_price, current_price, ma_values, direction)
 
                     # 根据当前时间调整所需的均线穿越得分阈值
@@ -638,13 +649,19 @@ def check_open_and_stop(context):
                     if not score_passed:
                         log.info(f"  ✗ 均线穿越得分不足或不符合条件({cross_score} < {required_cross_score} 或 1分来自MA5),跳过开仓")
                         continue
+                    
+                    if current_time_str == '14:55':
+                        positive_cross_periods = {detail['period'] for detail in score_details if detail.get('delta', 0) > 0}
+                        if positive_cross_periods == {30}:
+                            log.info("  ✗ 尾盘仅穿越MA30,跳过开仓")
+                            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'均线形态开仓', crossed_ma_details=score_details)
+                                              f'均线形态开仓', crossed_ma_details=score_details, entry_snapshot=entry_snapshot)
                         if success:
                             log.info(f"  ✓✓ {symbol} 开仓成功,从候选列表移除")
                             candidates_to_remove.append(dominant_future)
@@ -881,7 +898,8 @@ def calculate_ma_cross_score(open_price, current_price, ma_values, direction):
     assert direction in ('long', 'short')
     score = 0
     score_details = []
-    for period in g.ma_periods:
+    periods = getattr(g, 'ma_cross_periods', g.ma_periods)
+    for period in periods:
         key = f'MA{period}'
         ma_value = ma_values.get(key)
         if ma_value is None:
@@ -1152,7 +1170,7 @@ def check_historical_ma_pattern_consistency(historical_data, direction, lookback
 
 ############################ 交易执行函数 ###################################
 
-def open_position(context, security, target_hands, direction, single_hand_margin, reason='', crossed_ma_details=None):
+def open_position(context, security, target_hands, direction, single_hand_margin, reason='', crossed_ma_details=None, entry_snapshot=None):
     """开仓"""
     try:
         # 记录交易前的可用资金
@@ -1209,6 +1227,14 @@ def open_position(context, security, target_hands, direction, single_hand_margin
             if crossed_ma_details:
                 crossed_ma_lines = [f"MA{detail['period']}" for detail in crossed_ma_details if detail.get('delta', 0) > 0]
             
+            snapshot_copy = None
+            if entry_snapshot:
+                snapshot_copy = {
+                    'yesterday_close': entry_snapshot.get('yesterday_close'),
+                    'today_open': entry_snapshot.get('today_open'),
+                    'ma_values': (entry_snapshot.get('ma_values') or {}).copy(),
+                    'avg_5day_change': entry_snapshot.get('avg_5day_change')
+                }
             g.trade_history[security] = {
                 'entry_price': order_price,
                 'target_hands': target_hands,
@@ -1217,7 +1243,8 @@ def open_position(context, security, target_hands, direction, single_hand_margin
                 'direction': direction,
                 'entry_time': context.current_dt,
                 'entry_trading_day': entry_trading_day,
-                'crossed_ma_lines': crossed_ma_lines  # 记录穿越的均线
+                'crossed_ma_lines': crossed_ma_lines,  # 记录穿越的均线
+                'entry_snapshot': snapshot_copy
             }
 
             ma_trailing_enabled = True
@@ -1402,12 +1429,71 @@ def calculate_average_daily_change_rate(security, days=30):
 
 def calculate_realtime_ma_values(security, ma_periods):
     """计算包含当前价格的实时均线值"""
-    historical_data = attribute_history(security, max(ma_periods), '1d', ['close'])
+    if not ma_periods:
+        return {}
+    max_period = max(ma_periods)
+    history_days = max_period + MA_ADDITIONAL_HISTORY_DAYS
+    historical_data = attribute_history(security, history_days, '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}
+    ma_values = {}
+    for period in ma_periods:
+        if len(close_prices) >= period:
+            ma_values[f'ma{period}'] = sum(close_prices[-period:]) / period
+        else:
+            ma_values[f'ma{period}'] = None
     return ma_values
 
+
+def calculate_recent_average_change(security, days=5):
+    """计算最近days天日间收盘涨幅的平均值"""
+    if days <= 0:
+        return None
+    try:
+        history = attribute_history(security, days + 1, '1d', ['close'])
+    except Exception as e:
+        log.warning(f"{security} 获取历史数据失败(均值涨幅计算): {str(e)}")
+        return None
+
+    closes = history['close']
+    if len(closes) < days + 1:
+        return None
+    pct_changes = closes.pct_change().dropna()
+    if len(pct_changes) < days:
+        return None
+    return pct_changes.iloc[-days:].mean()
+
+
+def format_entry_details(entry_snapshot):
+    """格式化记录到CSV的价格、均线及扩展信息"""
+    if not entry_snapshot:
+        return ''
+
+    def fmt(value):
+        return f"{value:.2f}" if value is not None else "NA"
+
+    ma_values = entry_snapshot.get('ma_values') or {}
+    snapshot_parts = [
+        f"prev_close:{fmt(entry_snapshot.get('yesterday_close'))}",
+        f"open:{fmt(entry_snapshot.get('today_open'))}"
+    ]
+
+    for period in [5, 10, 20, 30, 60]:
+        key_upper = f"MA{period}"
+        key_lower = key_upper.lower()
+        value = ma_values.get(key_upper)
+        if value is None:
+            value = ma_values.get(key_lower)
+        snapshot_parts.append(f"{key_upper}:{fmt(value)}")
+
+    avg_change = entry_snapshot.get('avg_5day_change')
+    if avg_change is not None:
+        snapshot_parts.append(f"AVG5:{avg_change:.4%}")
+    else:
+        snapshot_parts.append("AVG5:NA")
+
+    return '|'.join(snapshot_parts)
+
 def save_today_new_positions_to_csv(context):
     """将当天新增的持仓记录追加保存到CSV文件"""
     log.info(f"保存当天新增的持仓记录到CSV文件")
@@ -1425,7 +1511,8 @@ def save_today_new_positions_to_csv(context):
             crossed_ma_lines = trade_info.get('crossed_ma_lines', [])
             # 使用分号分隔多条均线,避免CSV格式问题
             crossed_ma_str = ';'.join(crossed_ma_lines) if crossed_ma_lines else ''
-            
+            details = format_entry_details(trade_info.get('entry_snapshot'))
+
             today_new_positions.append({
                 'security': security,
                 'underlying_symbol': underlying_symbol,
@@ -1435,7 +1522,8 @@ def save_today_new_positions_to_csv(context):
                 '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
+                'crossed_ma_lines': crossed_ma_str,
+                'details': details
             })
     
     # 如果没有当天新增的记录,直接返回
@@ -1462,12 +1550,12 @@ def save_today_new_positions_to_csv(context):
         
         # 如果文件不存在,添加表头
         if not file_exists:
-            header = 'security,underlying_symbol,direction,entry_price,actual_hands,actual_margin,entry_time,entry_trading_day,crossed_ma_lines'
+            header = 'security,underlying_symbol,direction,entry_price,actual_hands,actual_margin,entry_time,entry_trading_day,crossed_ma_lines,details'
             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']}"
+            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']},{pos['details']}"
             csv_lines.append(line)
         
         # 合并内容:现有内容 + 新内容