|
|
@@ -9,6 +9,13 @@ import re
|
|
|
|
|
|
MA_ADDITIONAL_HISTORY_DAYS = 30 # 计算均线时额外获取的交易日数量,用于支持更长周期
|
|
|
|
|
|
+# 均线聚合度阈值(基于MA5/MA10/MA20),用于动态调整单标的保证金上限
|
|
|
+MA_COMPACTION_THRESHOLDS = {
|
|
|
+ 'tight': 0.0045,
|
|
|
+ 'balanced': 0.0073,
|
|
|
+ 'loose': 0.0116
|
|
|
+}
|
|
|
+
|
|
|
# 顺势交易策略 v001
|
|
|
# 基于均线走势(前提条件)+ K线形态(开盘价差、当天价差)的期货交易策略
|
|
|
#
|
|
|
@@ -47,8 +54,11 @@ def initialize(context):
|
|
|
set_slippage(StepRelatedSlippage(2))
|
|
|
|
|
|
# 初始化全局变量
|
|
|
- g.usage_percentage = 0.8 # 最大资金使用比例
|
|
|
+ g.reserve_percentage = 0.2 # 预留资金比例
|
|
|
+ g.financial_reserve_amount = 180000 # 金融期货预留资金
|
|
|
g.max_margin_per_position = 30000 # 单个标的最大持仓保证金(元)
|
|
|
+ g.base_max_margin_per_position = g.max_margin_per_position # 记录基础保证金上限,便于动态调整
|
|
|
+ g.ma_compaction_thresholds = MA_COMPACTION_THRESHOLDS.copy()
|
|
|
|
|
|
# 均线策略参数
|
|
|
g.ma_periods = [5, 10, 20, 30, 60] # 均线周期(新增MA60)
|
|
|
@@ -99,6 +109,8 @@ def initialize(context):
|
|
|
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}天")
|
|
|
+ log.info(f" 预留资金比例: {g.reserve_percentage:.0%}")
|
|
|
+ log.info(f" 金融期货预留资金: {g.financial_reserve_amount:.0f}")
|
|
|
|
|
|
# 期货品种完整配置字典
|
|
|
g.futures_config = {
|
|
|
@@ -162,16 +174,16 @@ def initialize(context):
|
|
|
'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'},
|
|
|
+ 'IF': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 300, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
+ 'IH': {'has_night_session': False, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 300, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
+ 'IC': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 200, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
+ 'IM': {'has_night_session': False, 'margin_rate': {'long': 0.17, 'short': 0.17}, 'multiplier': 200, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
'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'},
|
|
|
+ 'T': {'has_night_session': False, 'margin_rate': {'long': 0.03, 'short': 0.03}, 'multiplier': 1000000, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
'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'},
|
|
|
@@ -189,7 +201,7 @@ def initialize(context):
|
|
|
# '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'},
|
|
|
+ 'TS': {'has_night_session': False, 'margin_rate': {'long': 0.015, 'short': 0.015}, 'multiplier': 2000000, 'trading_start_time': '09:30', 'is_financial': True},
|
|
|
# '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'},
|
|
|
@@ -199,18 +211,18 @@ def initialize(context):
|
|
|
# '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'},
|
|
|
+ 'TL': {'has_night_session': False, 'margin_rate': {'long': 0.045, 'short': 0.045}, 'multiplier': 1000000, 'trading_start_time': '09:00', 'is_financial': True},
|
|
|
'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'},
|
|
|
+ 'TF': {'has_night_session': False, 'margin_rate': {'long': 0.022, 'short': 0.022}, 'multiplier': 1000000, 'trading_start_time': '09:00', 'is_financial': True},
|
|
|
# '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 = [] # 空列表表示考虑所有品种
|
|
|
+ g.strategy_focus_symbols = ['SC'] # 空列表表示考虑所有品种
|
|
|
|
|
|
# 方案2:精选品种策略 - 只交易流动性较好的特定品种(如需使用请取消下行注释)
|
|
|
# g.strategy_focus_symbols = ['RM', 'CJ', 'CY', 'JD', 'L', 'LC', 'SF', 'SI']
|
|
|
@@ -425,9 +437,21 @@ def check_ma_trend_and_open_gap(context):
|
|
|
# 步骤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)
|
|
|
+ ma_compaction = calculate_ma_compaction_from_values(ma_values, periods=[5, 10, 20])
|
|
|
|
|
|
log.info(f"{symbol}({dominant_future}) 均线检查:")
|
|
|
log.info(f" 均线贴近统计: {ma_proximity_counts}")
|
|
|
+ if ma_compaction is not None:
|
|
|
+ log.info(f" 均线聚合度(MA5/MA10/MA20): {ma_compaction:.4f}")
|
|
|
+ if ma_compaction > g.ma_compaction_thresholds['loose']:
|
|
|
+ log.info(
|
|
|
+ f" {symbol}({dominant_future}) ✗ 均线聚合度 {ma_compaction:.4f} 超过阈值 "
|
|
|
+ f"{g.ma_compaction_thresholds['loose']:.4f},跳过"
|
|
|
+ )
|
|
|
+ add_to_excluded_contracts(dominant_future, 'ma_compaction', current_trading_day)
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ log.info(" 均线聚合度(MA5/MA10/MA20) 无法计算,使用默认值")
|
|
|
|
|
|
# 检查均线贴近计数
|
|
|
proximity_sum = ma_proximity_counts.get('MA5', 0) + ma_proximity_counts.get('MA10', 0)
|
|
|
@@ -524,7 +548,8 @@ def check_ma_trend_and_open_gap(context):
|
|
|
'open_price': today_open,
|
|
|
'yesterday_close': yesterday_close,
|
|
|
'yesterday_open': yesterday_open,
|
|
|
- 'ma_values': ma_values
|
|
|
+ 'ma_values': ma_values,
|
|
|
+ 'ma_compaction': ma_compaction
|
|
|
}
|
|
|
|
|
|
log.info(f" ✓✓ {symbol} 通过均线和开盘价差检查,加入候选列表")
|
|
|
@@ -587,6 +612,7 @@ def check_open_and_stop(context):
|
|
|
open_price = candidate_info['open_price']
|
|
|
yesterday_close = candidate_info.get('yesterday_close')
|
|
|
yesterday_open = candidate_info.get('yesterday_open')
|
|
|
+ ma_compaction = candidate_info.get('ma_compaction')
|
|
|
|
|
|
# 检查是否已有持仓
|
|
|
if check_symbol_prefix_match(dominant_future, context, set(g.trade_history.keys())):
|
|
|
@@ -605,6 +631,17 @@ def check_open_and_stop(context):
|
|
|
log.info(f"{symbol}({dominant_future}) 开仓条件检查:")
|
|
|
log.info(f" 方向: {direction}, 开盘价: {open_price:.2f}, 当前价: {current_price:.2f}, "
|
|
|
f"当天价差: {intraday_diff:.2f}, 变化比例: {intraday_diff_ratio:.2%}")
|
|
|
+
|
|
|
+ margin_limit = adjust_max_margin_per_position(ma_compaction)
|
|
|
+ if margin_limit is None:
|
|
|
+ compaction_str = f"{ma_compaction:.4f}" if ma_compaction is not None else "NA"
|
|
|
+ log.info(
|
|
|
+ f" ✗ 均线聚合度 {compaction_str} 超出允许范围(>{g.ma_compaction_thresholds['loose']:.4f}),跳过开仓"
|
|
|
+ )
|
|
|
+ candidates_to_remove.append(dominant_future)
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ log.info(f" 动态单标的保证金上限: {margin_limit:.0f}")
|
|
|
|
|
|
# 判断是否满足开仓条件 - 仅检查均线穿越得分
|
|
|
should_open = True
|
|
|
@@ -658,7 +695,11 @@ def check_open_and_stop(context):
|
|
|
# 执行开仓
|
|
|
log.info(f" 准备开仓: {symbol} {direction}")
|
|
|
target_hands, single_hand_margin, single_hand_exceeds_limit = calculate_target_hands(
|
|
|
- context, dominant_future, direction)
|
|
|
+ context,
|
|
|
+ dominant_future,
|
|
|
+ direction,
|
|
|
+ max_margin_override=margin_limit
|
|
|
+ )
|
|
|
|
|
|
if target_hands > 0:
|
|
|
success = open_position(
|
|
|
@@ -683,6 +724,8 @@ def check_open_and_stop(context):
|
|
|
except Exception as e:
|
|
|
log.warning(f"{dominant_future} 处理时出错: {str(e)}")
|
|
|
continue
|
|
|
+ finally:
|
|
|
+ adjust_max_margin_per_position(None)
|
|
|
|
|
|
# 从候选列表中移除已开仓的品种
|
|
|
for future in candidates_to_remove:
|
|
|
@@ -1352,6 +1395,32 @@ def get_multiplier(underlying_symbol, default_multiplier=10):
|
|
|
"""获取合约乘数的辅助函数"""
|
|
|
return g.futures_config.get(underlying_symbol, {}).get('multiplier', default_multiplier)
|
|
|
|
|
|
+def is_financial_underlying(underlying_symbol):
|
|
|
+ """判断标的是否属于金融期货"""
|
|
|
+ if not underlying_symbol:
|
|
|
+ return False
|
|
|
+ return bool(get_futures_config(underlying_symbol, 'is_financial', False))
|
|
|
+
|
|
|
+def has_financial_positions(context):
|
|
|
+ """检查当前持仓中是否包含金融期货"""
|
|
|
+ subportfolios = getattr(context, 'subportfolios', [])
|
|
|
+ if not subportfolios:
|
|
|
+ return False
|
|
|
+ subportfolio = subportfolios[0]
|
|
|
+ positions = list(getattr(subportfolio, 'long_positions', {}).values()) + \
|
|
|
+ list(getattr(subportfolio, 'short_positions', {}).values())
|
|
|
+ for position in positions:
|
|
|
+ security = getattr(position, 'security', '')
|
|
|
+ if not security:
|
|
|
+ continue
|
|
|
+ underlying = security.split('.')[0]
|
|
|
+ if not underlying:
|
|
|
+ continue
|
|
|
+ symbol = underlying[:-4] if len(underlying) > 4 else underlying
|
|
|
+ if is_financial_underlying(symbol):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
def add_to_excluded_contracts(dominant_future, reason, current_trading_day):
|
|
|
"""将合约添加到排除缓存"""
|
|
|
g.excluded_contracts[dominant_future] = {
|
|
|
@@ -1387,7 +1456,7 @@ def has_reached_trading_start(current_dt, trading_start_time_str, has_night_sess
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
-def calculate_target_hands(context, security, direction):
|
|
|
+def calculate_target_hands(context, security, direction, max_margin_override=None):
|
|
|
"""计算目标开仓手数
|
|
|
|
|
|
Returns:
|
|
|
@@ -1404,11 +1473,22 @@ def calculate_target_hands(context, security, direction):
|
|
|
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
|
|
|
+ # 还要考虑可用资金限制,先计算账户总资金(可用资金 + 已占用保证金)
|
|
|
+ total_value = context.portfolio.total_value
|
|
|
+ reserved_cash = total_value * g.reserve_percentage # 按比例预留的现金
|
|
|
+ available_cash = max(total_value - reserved_cash, 0)
|
|
|
+ # log.debug(f"账户总资金: {total_value:.0f}, 预留现金: {reserved_cash:.0f}, 可用资金: {available_cash:.0f}")
|
|
|
+
|
|
|
+ # 如果当前没有持有金融期货,则额外预留指定金额,避免被其他品种占用
|
|
|
+ if not has_financial_positions(context):
|
|
|
+ reserve_amount = getattr(g, 'financial_reserve_amount', 0)
|
|
|
+ log.debug(f"金融期货预留资金: {reserve_amount:.0f}")
|
|
|
+ if reserve_amount > 0:
|
|
|
+ available_cash = max(available_cash - reserve_amount, 0)
|
|
|
+ log.debug(f"金融期货预留资金扣除后可用资金: {available_cash:.0f}")
|
|
|
|
|
|
# 根据单个标的最大持仓保证金限制计算开仓数量
|
|
|
- max_margin = g.max_margin_per_position
|
|
|
+ max_margin = max_margin_override if max_margin_override is not None else g.max_margin_per_position
|
|
|
single_hand_exceeds_limit = single_hand_margin > max_margin
|
|
|
|
|
|
if not single_hand_exceeds_limit:
|
|
|
@@ -1492,6 +1572,52 @@ def calculate_recent_average_change(security, days=5):
|
|
|
return pct_changes.iloc[-days:].mean()
|
|
|
|
|
|
|
|
|
+def calculate_ma_compaction_from_values(ma_values, periods=(5, 10, 20)):
|
|
|
+ """基于给定的均线数值计算聚合度(标准差/均值)"""
|
|
|
+ if not ma_values:
|
|
|
+ return None
|
|
|
+ values = []
|
|
|
+ for period in periods:
|
|
|
+ key = f'MA{period}'
|
|
|
+ value = ma_values.get(key)
|
|
|
+ if value is None:
|
|
|
+ return None
|
|
|
+ values.append(value)
|
|
|
+ mean_val = np.mean(values)
|
|
|
+ if mean_val == 0:
|
|
|
+ return None
|
|
|
+ std_val = np.std(values, ddof=0)
|
|
|
+ return std_val / mean_val
|
|
|
+
|
|
|
+
|
|
|
+def determine_margin_limit_from_compaction(compaction_value):
|
|
|
+ """根据均线聚合度返回对应的最大持仓保证金上限"""
|
|
|
+ base_limit = getattr(g, 'base_max_margin_per_position', getattr(g, 'max_margin_per_position', 30000))
|
|
|
+ if compaction_value is None:
|
|
|
+ return base_limit
|
|
|
+ thresholds = getattr(g, 'ma_compaction_thresholds', MA_COMPACTION_THRESHOLDS)
|
|
|
+ tight_threshold = thresholds.get('tight', MA_COMPACTION_THRESHOLDS['tight'])
|
|
|
+ balanced_threshold = thresholds.get('balanced', MA_COMPACTION_THRESHOLDS['balanced'])
|
|
|
+ loose_threshold = thresholds.get('loose', MA_COMPACTION_THRESHOLDS['loose'])
|
|
|
+
|
|
|
+ if compaction_value <= tight_threshold:
|
|
|
+ return 40000
|
|
|
+ if compaction_value <= balanced_threshold:
|
|
|
+ return 30000
|
|
|
+ if compaction_value <= loose_threshold:
|
|
|
+ return 20000
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def adjust_max_margin_per_position(compaction_value):
|
|
|
+ """根据聚合度动态调整 g.max_margin_per_position,并返回新的上限"""
|
|
|
+ margin_limit = determine_margin_limit_from_compaction(compaction_value)
|
|
|
+ if margin_limit is None:
|
|
|
+ return None
|
|
|
+ g.max_margin_per_position = margin_limit
|
|
|
+ return margin_limit
|
|
|
+
|
|
|
+
|
|
|
def format_entry_details(entry_snapshot):
|
|
|
"""格式化记录到CSV的价格、均线及扩展信息"""
|
|
|
if not entry_snapshot:
|