|
|
@@ -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)
|
|
|
|
|
|
# 合并内容:现有内容 + 新内容
|