|
|
@@ -1,1248 +1,2039 @@
|
|
|
# 深度实值买购和卖购组合的牛差策略
|
|
|
-# 参考文档: Lib/Options/README.md - 策略1
|
|
|
+# 策略说明:使用深度实值买购期权替代ETF持仓,结合卖购期权构建牛差组合
|
|
|
+# 参考资料:基于加百列分享中的50ETF期权备兑认购策略改进
|
|
|
|
|
|
+import jqdata
|
|
|
from jqdata import *
|
|
|
import pandas as pd
|
|
|
import numpy as np
|
|
|
+import datetime
|
|
|
import matplotlib.pyplot as plt
|
|
|
-import tqdm
|
|
|
-from datetime import datetime, timedelta
|
|
|
-import warnings
|
|
|
-warnings.filterwarnings('ignore')
|
|
|
+import os
|
|
|
+from enum import Enum
|
|
|
+
|
|
|
+plt.rcParams['font.sans-serif'] = ['SimHei']
|
|
|
+plt.rcParams['axes.unicode_minus'] = False
|
|
|
+
|
|
|
+class StrategyType(Enum):
|
|
|
+ """策略类型枚举"""
|
|
|
+ BULL_SPREAD = "bull_spread" # 牛差组合策略(深度实值买购+卖购)
|
|
|
|
|
|
class DeepITMBullSpreadStrategy:
|
|
|
"""深度实值买购和卖购组合的牛差策略"""
|
|
|
|
|
|
- def __init__(self, underlying='510300.XSHG', start_date='2024-01-01', end_date='2025-06-30'):
|
|
|
- self.underlying = underlying # 标的ETF
|
|
|
+ def __init__(self, underlying_symbol='510300.XSHG', start_date='2024-01-01', end_date='2024-12-31'):
|
|
|
+ """初始化策略参数"""
|
|
|
+ self.underlying_symbol = underlying_symbol
|
|
|
self.start_date = start_date
|
|
|
self.end_date = end_date
|
|
|
|
|
|
- # 策略参数设置
|
|
|
- self.params = {
|
|
|
- '一组张数': 30,
|
|
|
- '最小权利金': {'沪深300ETF': 0.03, '上证50ETF': 0.05},
|
|
|
- '最少开仓日期': 20, # 距离到期日天数
|
|
|
- '买购时间价值阈值': 0.0001,
|
|
|
- '卖购平仓权利金阈值': 0.0005,
|
|
|
- '合约到期移仓日期最大': 7, # 交易日
|
|
|
- '合约到期移仓日期最小': 2, # 交易日
|
|
|
- '加仓窗口阈值': {'沪深300ETF': 0.2, '上证50ETF': 0.1}
|
|
|
+ # 策略参数设置 - 对应README.md中的阈值设定
|
|
|
+ self.config = {
|
|
|
+ 'contract_size': 30, # 一组张数
|
|
|
+ 'min_premium': {'510300': 0.03, '510050': 0.05, '159915': 0.03}, # 最小权利金
|
|
|
+ 'min_days_to_expiry': 15, # 最少开仓日期(距离到期日)
|
|
|
+ 'call_time_value_threshold': 0.015, # 买购时间价值阈值(README中为0.015)
|
|
|
+ 'put_close_premium_threshold': 0.005, # 卖购平仓权利金阈值(README中为50单位,转换为0.005)
|
|
|
+ 'max_days_before_expiry': 7, # 合约到期移仓日期最大(交易日)
|
|
|
+ 'min_days_before_expiry': 2, # 合约到期移仓日期最小(交易日)
|
|
|
+ 'add_position_threshold': {'510300': 0.2, '510050': 0.1, '159915': 0.15}, # 加仓窗口阈值
|
|
|
+ 'max_add_positions': 2, # 加仓次数上限
|
|
|
+ 'max_profit_close_threshold': 0.83
|
|
|
}
|
|
|
|
|
|
- # 交易记录
|
|
|
- self.positions = [] # 持仓记录
|
|
|
- self.trades = [] # 交易记录
|
|
|
- self.daily_pnl = [] # 每日损益
|
|
|
+ # 持仓和交易记录
|
|
|
+ self.positions = []
|
|
|
+ self.trade_records = []
|
|
|
+
|
|
|
+ # CSV文件输出设置
|
|
|
+ self.transaction_csv_path = 'transaction.csv'
|
|
|
+ self.position_csv_path = 'position.csv'
|
|
|
+
|
|
|
+ # 验证合约数量
|
|
|
+ self._validate_contract_size()
|
|
|
+ self.daily_positions = [] # 每日持仓记录
|
|
|
|
|
|
- # 获取交易日历
|
|
|
- self.trade_days = get_trade_days(start_date, end_date)
|
|
|
+ # 账户管理器回调(用于通知账户管理器更新汇总)
|
|
|
+ self.account_manager_callback = None
|
|
|
|
|
|
- def get_etf_name(self):
|
|
|
- """根据标的代码获取ETF名称"""
|
|
|
- if self.underlying == '510300.XSHG':
|
|
|
- return '沪深300ETF'
|
|
|
- elif self.underlying == '510050.XSHG':
|
|
|
- return '上证50ETF'
|
|
|
+ # 获取交易日历和月度分割点
|
|
|
+ self.trade_days = pd.Series(index=jqdata.get_trade_days(start_date, end_date))
|
|
|
+ self.trade_days.index = pd.to_datetime(self.trade_days.index)
|
|
|
+ self.month_split = list(self.trade_days.resample('M', label='left').mean().index) + [pd.to_datetime(end_date)]
|
|
|
+
|
|
|
+ # 调试输出:显示月份分割点
|
|
|
+ print(f"Month split 计算结果 ({len(self.month_split)}个分割点):")
|
|
|
+ for i, split_date in enumerate(self.month_split):
|
|
|
+ print(f" 索引{i}: {split_date.strftime('%Y-%m-%d')}")
|
|
|
+ if i < len(self.month_split) - 1:
|
|
|
+ next_split = self.month_split[i + 1]
|
|
|
+ print(f" 月份{i}覆盖范围: {split_date.strftime('%Y-%m-%d')} 到 {next_split.strftime('%Y-%m-%d')}")
|
|
|
+
|
|
|
+ # 用于存储前一日ETF价格,计算当日涨幅
|
|
|
+ self.previous_etf_price = None
|
|
|
+
|
|
|
+ def _validate_contract_size(self):
|
|
|
+ """验证合约数量的有效性"""
|
|
|
+ contract_size = self.config.get('contract_size', 30)
|
|
|
+ if not isinstance(contract_size, (int, float)) or contract_size <= 0:
|
|
|
+ print(f"警告: 合约数量无效({contract_size}),重置为默认值30张")
|
|
|
+ self.config['contract_size'] = 30
|
|
|
+ elif contract_size > 200: # 设置一个合理的上限
|
|
|
+ print(f"警告: 合约数量过大({contract_size}),限制为200张")
|
|
|
+ self.config['contract_size'] = 200
|
|
|
else:
|
|
|
- return '未知ETF'
|
|
|
+ # 确保为整数
|
|
|
+ self.config['contract_size'] = int(contract_size)
|
|
|
+
|
|
|
+ def get_safe_contract_size(self):
|
|
|
+ """安全获取合约数量,确保返回有效值"""
|
|
|
+ contract_size = self.config.get('contract_size', 30)
|
|
|
+ if not isinstance(contract_size, (int, float)) or contract_size <= 0:
|
|
|
+ print(f"警告: 运行时检测到合约数量无效({contract_size}),使用默认值30张")
|
|
|
+ contract_size = 30
|
|
|
+ self.config['contract_size'] = 30
|
|
|
+ elif contract_size > 200:
|
|
|
+ print(f"警告: 运行时检测到合约数量过大({contract_size}),限制为200张")
|
|
|
+ contract_size = 200
|
|
|
+ self.config['contract_size'] = 200
|
|
|
+ return int(contract_size)
|
|
|
|
|
|
- def get_option_contracts(self, date, contract_type='CO', min_days_to_expire=20):
|
|
|
- """获取期权合约信息"""
|
|
|
- # 查询期权合约
|
|
|
- q = query(opt.OPT_CONTRACT_INFO.code,
|
|
|
+ def get_underlying_code(self):
|
|
|
+ """获取标的ETF的简化代码"""
|
|
|
+ if '510300' in self.underlying_symbol:
|
|
|
+ return '510300'
|
|
|
+ elif '510050' in self.underlying_symbol:
|
|
|
+ return '510050'
|
|
|
+ elif '159915' in self.underlying_symbol:
|
|
|
+ return '159915'
|
|
|
+ return '510300' # 默认
|
|
|
+
|
|
|
+ def get_option_contracts(self, trade_date, month_idx, contract_type='CO'):
|
|
|
+ """获取指定月份的期权合约信息"""
|
|
|
+ underlying_code = self.get_underlying_code()
|
|
|
+
|
|
|
+ # 基于自然月份计算期权筛选范围
|
|
|
+ query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+ current_year = query_date.year
|
|
|
+ current_month = query_date.month
|
|
|
+ print(f" 期权筛选调试: month_idx={month_idx}, query_date={query_date}, 当前年月={current_year}-{current_month:02d}")
|
|
|
+
|
|
|
+ # 计算目标月份
|
|
|
+ if month_idx == 0:
|
|
|
+ # 当月期权:到期日在当前月份
|
|
|
+ target_year = current_year
|
|
|
+ target_month = current_month
|
|
|
+ elif month_idx == 1:
|
|
|
+ # 下月期权:到期日在下个月份
|
|
|
+ if current_month == 12:
|
|
|
+ target_year = current_year + 1
|
|
|
+ target_month = 1
|
|
|
+ else:
|
|
|
+ target_year = current_year
|
|
|
+ target_month = current_month + 1
|
|
|
+ else:
|
|
|
+ # 其他月份:使用原来的month_split逻辑作为后备
|
|
|
+ start_date = self.month_split[month_idx].date() if hasattr(self.month_split[month_idx], 'date') else self.month_split[month_idx]
|
|
|
+ end_date = self.month_split[month_idx + 1].date() if hasattr(self.month_split[month_idx + 1], 'date') else self.month_split[month_idx + 1]
|
|
|
+ print(f" 使用month_split后备逻辑: 月份索引{month_idx}, 范围{start_date}到{end_date}")
|
|
|
+
|
|
|
+ if month_idx <= 1:
|
|
|
+ # 计算目标月份的完整范围
|
|
|
+ start_date = pd.to_datetime(f'{target_year}-{target_month:02d}-01').date()
|
|
|
+ if target_month == 12:
|
|
|
+ end_date = pd.to_datetime(f'{target_year + 1}-01-01').date() - pd.Timedelta(days=1)
|
|
|
+ else:
|
|
|
+ end_date = pd.to_datetime(f'{target_year}-{target_month + 1:02d}-01').date() - pd.Timedelta(days=1)
|
|
|
+
|
|
|
+ print(f" 月份索引{month_idx}({target_year}-{target_month:02d})期权筛选范围: {start_date} 到 {end_date}")
|
|
|
+
|
|
|
+ q_contract_info = query(
|
|
|
+ opt.OPT_CONTRACT_INFO.code,
|
|
|
opt.OPT_CONTRACT_INFO.trading_code,
|
|
|
opt.OPT_CONTRACT_INFO.name,
|
|
|
opt.OPT_CONTRACT_INFO.exercise_price,
|
|
|
opt.OPT_CONTRACT_INFO.last_trade_date,
|
|
|
opt.OPT_CONTRACT_INFO.list_date
|
|
|
).filter(
|
|
|
- opt.OPT_CONTRACT_INFO.underlying_symbol == self.underlying,
|
|
|
opt.OPT_CONTRACT_INFO.contract_type == contract_type,
|
|
|
- opt.OPT_CONTRACT_INFO.list_date <= date,
|
|
|
- opt.OPT_CONTRACT_INFO.last_trade_date > date
|
|
|
- )
|
|
|
-
|
|
|
- contracts = opt.run_query(q)
|
|
|
- print(f" 查询到{len(contracts)}个{contract_type}类型的期权合约")
|
|
|
-
|
|
|
- # 过滤距离到期日至少min_days_to_expire天的合约
|
|
|
- valid_contracts = []
|
|
|
- for _, contract in contracts.iterrows():
|
|
|
- days_to_expire = (pd.to_datetime(contract['last_trade_date']) - pd.to_datetime(date)).days
|
|
|
- if days_to_expire >= min_days_to_expire:
|
|
|
- valid_contracts.append(contract)
|
|
|
-
|
|
|
- print(f" 过滤后剩余{len(valid_contracts)}个距离到期日至少{min_days_to_expire}天的合约")
|
|
|
- return pd.DataFrame(valid_contracts) if valid_contracts else pd.DataFrame()
|
|
|
-
|
|
|
- def get_option_price(self, option_code, date):
|
|
|
- """获取期权价格"""
|
|
|
- try:
|
|
|
- q = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
- opt.OPT_DAILY_PRICE.code == option_code,
|
|
|
- opt.OPT_DAILY_PRICE.date == date
|
|
|
- )
|
|
|
- result = opt.run_query(q)
|
|
|
- return result['close'].iloc[0] if not result.empty else None
|
|
|
- except:
|
|
|
- return None
|
|
|
-
|
|
|
- def calculate_time_value(self, option_price, intrinsic_value):
|
|
|
- """计算时间价值"""
|
|
|
- return max(0, option_price - max(0, intrinsic_value))
|
|
|
+ opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',
|
|
|
+ opt.OPT_CONTRACT_INFO.last_trade_date >= start_date,
|
|
|
+ opt.OPT_CONTRACT_INFO.last_trade_date <= end_date,
|
|
|
+ opt.OPT_CONTRACT_INFO.list_date < query_date
|
|
|
+ )
|
|
|
+
|
|
|
+ contract_info = opt.run_query(q_contract_info)
|
|
|
+ contract_info = contract_info[contract_info['trading_code'].str[:6] == underlying_code]
|
|
|
+
|
|
|
+ # 调试输出:显示筛选到的期权到期日
|
|
|
+ if not contract_info.empty:
|
|
|
+ print(f" 筛选到{len(contract_info)}个期权,到期日范围:{contract_info['last_trade_date'].min()} 到 {contract_info['last_trade_date'].max()}")
|
|
|
+ else:
|
|
|
+ print(f" 未筛选到任何期权")
|
|
|
+
|
|
|
+ return contract_info
|
|
|
|
|
|
- def select_call_option_to_buy(self, date, etf_price):
|
|
|
- """选择深度实值买购期权"""
|
|
|
- contracts = self.get_option_contracts(date, 'CO')
|
|
|
- if contracts.empty:
|
|
|
- print(f" 买购选择失败: 未找到任何认购期权合约")
|
|
|
- return None
|
|
|
-
|
|
|
- print(f" 找到{len(contracts)}个认购期权合约")
|
|
|
-
|
|
|
- # 筛选深度实值期权(时间价值 <= 阈值)
|
|
|
- suitable_contracts = []
|
|
|
- checked_count = 0
|
|
|
- for _, contract in contracts.iterrows():
|
|
|
- option_price = self.get_option_price(contract['code'], date)
|
|
|
- checked_count += 1
|
|
|
- if option_price is None:
|
|
|
+ def get_monthly_option_candidates(self, trade_date, month_idx, silent=False):
|
|
|
+ """获取指定月份的所有认购期权候选信息
|
|
|
+ 返回: (contract_info, month_info) - contract_info为期权列表,month_info为月份信息
|
|
|
+ """
|
|
|
+ if not silent:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 获取月份索引 {month_idx} 的认购期权候选信息")
|
|
|
+
|
|
|
+ # 获取期权合约信息
|
|
|
+ contract_info = self.get_option_contracts(trade_date, month_idx, 'CO')
|
|
|
+
|
|
|
+ if contract_info.empty:
|
|
|
+ if not silent:
|
|
|
+ print(f" 月份索引 {month_idx} 无可用认购期权合约")
|
|
|
+ # 增加调试信息,显示查询的时间范围
|
|
|
+ if month_idx < len(self.month_split) - 1:
|
|
|
+ start_date = self.month_split[month_idx].date() if hasattr(self.month_split[month_idx], 'date') else self.month_split[month_idx]
|
|
|
+ end_date = self.month_split[month_idx + 1].date() if hasattr(self.month_split[month_idx + 1], 'date') else self.month_split[month_idx + 1]
|
|
|
+ print(f" 查询时间范围: {start_date} 到 {end_date}")
|
|
|
+ print(f" 查询日期: {trade_date}")
|
|
|
+ print(f" 标的代码: {self.get_underlying_code()}")
|
|
|
+ print(f" month_split总长度: {len(self.month_split)}")
|
|
|
+ return None, None
|
|
|
+
|
|
|
+ # 获取月份信息
|
|
|
+ underlying_code = self.get_underlying_code()
|
|
|
+ min_days_to_expiry = self.config['min_days_to_expiry']
|
|
|
+
|
|
|
+ # 为每个合约添加额外信息
|
|
|
+ query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+ candidates = []
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 查询到 {len(contract_info)} 个认购期权合约,开始获取价格信息:")
|
|
|
+
|
|
|
+ for idx, contract in contract_info.iterrows():
|
|
|
+ # 检查到期日
|
|
|
+ expiry_date = pd.to_datetime(contract['last_trade_date'])
|
|
|
+ trade_date_obj = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+ days_to_expiry = len(get_trade_days(trade_date_obj, expiry_date.date())) - 1
|
|
|
+
|
|
|
+ if days_to_expiry < min_days_to_expiry:
|
|
|
+ # if not silent:
|
|
|
+ # print(f" 行权价 {contract['exercise_price']:.3f}: 到期时间不足 ({days_to_expiry} < {min_days_to_expiry})")
|
|
|
continue
|
|
|
-
|
|
|
- intrinsic_value = max(0, etf_price - contract['exercise_price'])
|
|
|
- time_value = self.calculate_time_value(option_price, intrinsic_value)
|
|
|
-
|
|
|
- if time_value <= self.params['买购时间价值阈值']:
|
|
|
- suitable_contracts.append({
|
|
|
+
|
|
|
+ # 查询期权价格
|
|
|
+ try:
|
|
|
+ q_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == contract['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ price_result = opt.run_query(q_price)
|
|
|
+ if price_result.empty:
|
|
|
+ if not silent:
|
|
|
+ print(f" 行权价 {contract['exercise_price']:.3f}: 无价格数据")
|
|
|
+ continue
|
|
|
+
|
|
|
+ option_price = price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ candidate = {
|
|
|
'code': contract['code'],
|
|
|
'exercise_price': contract['exercise_price'],
|
|
|
- 'option_price': option_price,
|
|
|
+ 'price': option_price,
|
|
|
+ 'expiry_date': contract['last_trade_date'],
|
|
|
+ 'days_to_expiry': days_to_expiry,
|
|
|
+ 'contract_info': contract
|
|
|
+ }
|
|
|
+ candidates.append(candidate)
|
|
|
+
|
|
|
+ # if not silent:
|
|
|
+ # print(f" 行权价 {contract['exercise_price']:.3f}: 期权价格 {option_price:.4f}, 剩余天数 {days_to_expiry}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ if not silent:
|
|
|
+ print(f" 行权价 {contract['exercise_price']:.3f}: 价格查询失败 ({str(e)})")
|
|
|
+ continue
|
|
|
+
|
|
|
+ if not candidates:
|
|
|
+ if not silent:
|
|
|
+ print(f" 月份索引 {month_idx} 无符合基本条件的期权候选")
|
|
|
+ return None, None
|
|
|
+
|
|
|
+ month_info = {
|
|
|
+ 'month_idx': month_idx,
|
|
|
+ 'underlying_code': underlying_code,
|
|
|
+ 'min_days_to_expiry': min_days_to_expiry,
|
|
|
+ 'query_date': query_date
|
|
|
+ }
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 成功获取 {len(candidates)} 个有效期权候选")
|
|
|
+ # print(f" 月份索引 {month_idx} 的认购期权候选信息: {candidates}")
|
|
|
+
|
|
|
+ return candidates, month_info
|
|
|
+
|
|
|
+ def select_sell_call_from_candidates(self, candidates, etf_price, min_premium, silent=False):
|
|
|
+ """从候选期权中选择卖购期权(虚值期权)
|
|
|
+ 返回: (selected_call, reason) - selected_call为期权信息或None,reason为失败原因或None
|
|
|
+ """
|
|
|
+ # if not silent:
|
|
|
+ # print(f" 从 {len(candidates)} 个候选中筛选虚值卖购期权(行权价 > ETF价格 {etf_price:.4f}):")
|
|
|
+
|
|
|
+ # 筛选虚值期权(行权价 > ETF价格)
|
|
|
+ otm_candidates = [c for c in candidates if c['exercise_price'] > etf_price]
|
|
|
+
|
|
|
+ if not otm_candidates:
|
|
|
+ reason = f"无虚值期权:所有{len(candidates)}个期权的行权价都 <= ETF价格{etf_price:.4f}"
|
|
|
+ if not silent:
|
|
|
+ print(f" 筛选结果:无虚值期权(所有期权行权价都 <= ETF价格)")
|
|
|
+ for c in candidates:
|
|
|
+ option_type = "虚值" if c['exercise_price'] > etf_price else "实值"
|
|
|
+ print(f" 行权价: {c['exercise_price']:.3f} ({option_type})")
|
|
|
+ return None, reason
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" etf价格为{etf_price:.4f}时,筛选出 {len(otm_candidates)} 个虚值期权:")
|
|
|
+ for c in otm_candidates:
|
|
|
+ price_spread = c['exercise_price'] - etf_price
|
|
|
+ print(f" 行权价: {c['exercise_price']:.3f}, 价差: {price_spread:.4f}, 价格: {c['price']:.4f}")
|
|
|
+
|
|
|
+ # 按行权价排序,选择最接近ETF价格的虚值期权(行权价最小的)
|
|
|
+ otm_candidates.sort(key=lambda x: x['exercise_price'])
|
|
|
+ closest_otm = otm_candidates[0]
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 选择最接近的虚值期权:行权价 {closest_otm['exercise_price']:.3f}, 价格 {closest_otm['price']:.4f}")
|
|
|
+
|
|
|
+ # 检查权利金要求
|
|
|
+ if closest_otm['price'] >= min_premium:
|
|
|
+ if not silent:
|
|
|
+ print(f" ✓ 满足权利金要求 ({closest_otm['price']:.4f} >= {min_premium:.4f})")
|
|
|
+
|
|
|
+ # 构建返回结果
|
|
|
+ selected_call = {
|
|
|
+ 'code': closest_otm['code'],
|
|
|
+ 'exercise_price': closest_otm['exercise_price'],
|
|
|
+ 'price': closest_otm['price'],
|
|
|
+ 'expiry_date': closest_otm['expiry_date'],
|
|
|
+ 'price_spread': closest_otm['exercise_price'] - etf_price,
|
|
|
+ 'days_to_expiry': closest_otm['days_to_expiry']
|
|
|
+ }
|
|
|
+ return selected_call, None
|
|
|
+ else:
|
|
|
+ reason = f"期权权利金不足:{closest_otm['price']:.4f} < {min_premium:.4f}"
|
|
|
+ if not silent:
|
|
|
+ print(f" ✗ 权利金不足 ({closest_otm['price']:.4f} < {min_premium:.4f})")
|
|
|
+ return None, reason
|
|
|
+
|
|
|
+ def select_buy_call_from_candidates(self, candidates, etf_price, time_value_threshold, trade_date, silent=False):
|
|
|
+ """从候选期权中选择深度实值买购期权
|
|
|
+ 返回: (selected_call, reason) - selected_call为期权信息或None,reason为失败原因或None
|
|
|
+ """
|
|
|
+ if not silent:
|
|
|
+ print(f" 从 {len(candidates)} 个候选中筛选深度实值买购期权(行权价 < ETF价格 {etf_price:.4f}):")
|
|
|
+
|
|
|
+ # 筛选深度实值期权(行权价 < ETF价格)
|
|
|
+ itm_candidates = [c for c in candidates if c['exercise_price'] < etf_price]
|
|
|
+
|
|
|
+ if not itm_candidates:
|
|
|
+ reason = f"无深度实值期权:所有{len(candidates)}个期权的行权价都 >= ETF价格{etf_price:.4f}"
|
|
|
+ if not silent:
|
|
|
+ print(f" 筛选结果:无深度实值期权(所有期权行权价都 >= ETF价格)")
|
|
|
+ for c in candidates:
|
|
|
+ option_type = "实值" if c['exercise_price'] < etf_price else "虚值"
|
|
|
+ print(f" 行权价: {c['exercise_price']:.3f} ({option_type})")
|
|
|
+ return None, reason
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 筛选出 {len(itm_candidates)} 个深度实值期权:")
|
|
|
+ for c in itm_candidates:
|
|
|
+ print(f" 行权价: {c['exercise_price']:.3f}, 价格: {c['price']:.4f}")
|
|
|
+
|
|
|
+ # 按行权价与ETF价格的差异排序(升序,最接近的在前)
|
|
|
+ itm_candidates.sort(key=lambda x: abs(x['exercise_price'] - etf_price))
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 按距离ETF价格的差异排序,检查时间价值(阈值: {time_value_threshold:.4f}):")
|
|
|
+
|
|
|
+ # 检查时间价值
|
|
|
+ for candidate in itm_candidates:
|
|
|
+ intrinsic_value = etf_price - candidate['exercise_price']
|
|
|
+ time_value = round(max(0, candidate['price'] - intrinsic_value), 4)
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" 行权价 {candidate['exercise_price']:.3f}: 内在价值 {intrinsic_value:.4f}, 时间价值 {time_value:.4f}")
|
|
|
+
|
|
|
+ if time_value < time_value_threshold:
|
|
|
+ if not silent:
|
|
|
+ print(f" ✓ 满足时间价值要求 (< {time_value_threshold:.4f})")
|
|
|
+
|
|
|
+ # 构建返回结果
|
|
|
+ selected_call = {
|
|
|
+ 'code': candidate['code'],
|
|
|
+ 'exercise_price': candidate['exercise_price'],
|
|
|
+ 'price': candidate['price'],
|
|
|
'time_value': time_value,
|
|
|
- 'last_trade_date': contract['last_trade_date']
|
|
|
- })
|
|
|
-
|
|
|
- print(f" 检查了{checked_count}个合约,找到{len(suitable_contracts)}个符合时间价值条件的买购期权")
|
|
|
-
|
|
|
- # 选择时间价值最小的
|
|
|
- if suitable_contracts:
|
|
|
- return min(suitable_contracts, key=lambda x: x['time_value'])
|
|
|
- return None
|
|
|
+ 'expiry_date': candidate['expiry_date'],
|
|
|
+ 'price_diff': abs(candidate['exercise_price'] - etf_price),
|
|
|
+ 'days_to_expiry': candidate['days_to_expiry']
|
|
|
+ }
|
|
|
+ return selected_call, None
|
|
|
+ else:
|
|
|
+ if not silent:
|
|
|
+ print(f" ✗ 时间价值过高 ({time_value:.4f} >= {time_value_threshold:.4f})")
|
|
|
+
|
|
|
+ reason = f"所有{len(itm_candidates)}个深度实值期权的时间价值都过高(需<{time_value_threshold:.4f})"
|
|
|
+ if not silent:
|
|
|
+ print(f" 深度实值买购期权选择失败:所有期权时间价值都过高")
|
|
|
+ return None, reason
|
|
|
|
|
|
- def select_call_option_to_sell(self, date, etf_price):
|
|
|
- """选择平值卖购期权"""
|
|
|
- contracts = self.get_option_contracts(date, 'CO')
|
|
|
- if contracts.empty:
|
|
|
- print(f" 卖购选择失败: 未找到任何认购期权合约")
|
|
|
- return None
|
|
|
-
|
|
|
- # 找到最接近平值的期权
|
|
|
- etf_name = self.get_etf_name()
|
|
|
- min_premium = self.params['最小权利金'].get(etf_name, 0.03)
|
|
|
- print(f" 最小权利金要求: {min_premium}")
|
|
|
-
|
|
|
- suitable_contracts = []
|
|
|
- checked_count = 0
|
|
|
- for _, contract in contracts.iterrows():
|
|
|
- option_price = self.get_option_price(contract['code'], date)
|
|
|
- checked_count += 1
|
|
|
- if option_price is None or option_price < min_premium:
|
|
|
- continue
|
|
|
-
|
|
|
- price_diff = abs(contract['exercise_price'] - etf_price)
|
|
|
- suitable_contracts.append({
|
|
|
- 'code': contract['code'],
|
|
|
- 'exercise_price': contract['exercise_price'],
|
|
|
- 'option_price': option_price,
|
|
|
- 'price_diff': price_diff,
|
|
|
- 'last_trade_date': contract['last_trade_date']
|
|
|
- })
|
|
|
-
|
|
|
- print(f" 检查了{checked_count}个合约,找到{len(suitable_contracts)}个符合权利金条件的卖购期权")
|
|
|
-
|
|
|
- # 选择最接近平值且权利金较高的
|
|
|
- if suitable_contracts:
|
|
|
- # 先按价格差排序,再按权利金排序
|
|
|
- suitable_contracts.sort(key=lambda x: (x['price_diff'], -x['option_price']))
|
|
|
- return suitable_contracts[0]
|
|
|
- return None
|
|
|
+ def try_bull_spread_for_month(self, trade_date, etf_price, month_idx, is_current_month=True, silent=False):
|
|
|
+ """尝试指定月份的牛差策略
|
|
|
+ 返回: (buy_call, sell_call, reason) - 成功返回两个期权,失败返回None和原因
|
|
|
+ """
|
|
|
+ month_type = "当月" if is_current_month else "下月"
|
|
|
+ underlying_code = self.get_underlying_code()
|
|
|
+ base_premium = self.config['min_premium'][underlying_code]
|
|
|
+ min_premium = base_premium * 0.6 if is_current_month else base_premium
|
|
|
+ time_value_threshold = self.config['call_time_value_threshold']
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 尝试{month_type}牛差策略(月份索引:{month_idx})")
|
|
|
+ print(f" 权利金阈值: {min_premium:.4f},时间价值阈值: {time_value_threshold:.4f}")
|
|
|
+
|
|
|
+ # 1. 获取月份期权候选
|
|
|
+ candidates, month_info = self.get_monthly_option_candidates(trade_date, month_idx, silent)
|
|
|
+ if not candidates:
|
|
|
+ reason = f"{month_type}无可用期权候选"
|
|
|
+ if not silent:
|
|
|
+ print(f" {month_type}牛差策略失败:{reason}")
|
|
|
+ return None, None, reason
|
|
|
+
|
|
|
+ # 2. 选择卖购期权
|
|
|
+ sell_call, sell_reason = self.select_sell_call_from_candidates(candidates, etf_price, min_premium, silent)
|
|
|
+ if not sell_call:
|
|
|
+ reason = f"{month_type}卖购期权选择失败:{sell_reason}"
|
|
|
+ if not silent:
|
|
|
+ print(f" {month_type}牛差策略失败:{reason}")
|
|
|
+ return None, None, reason
|
|
|
+
|
|
|
+ # 3. 选择买购期权(必须与卖购期权到期日一致)
|
|
|
+ buy_call, buy_reason = self.select_buy_call_from_candidates(candidates, etf_price, time_value_threshold, trade_date, silent)
|
|
|
+ if not buy_call:
|
|
|
+ reason = f"{month_type}买购期权选择失败:{buy_reason}"
|
|
|
+ if not silent:
|
|
|
+ print(f" {month_type}牛差策略失败:{reason}")
|
|
|
+ return None, None, reason
|
|
|
+
|
|
|
+ # 4. 验证到期日是否一致
|
|
|
+ sell_expiry = pd.to_datetime(sell_call['expiry_date'])
|
|
|
+ buy_expiry = pd.to_datetime(buy_call['expiry_date'])
|
|
|
+ if buy_expiry != sell_expiry:
|
|
|
+ reason = f"买购和卖购期权到期日不一致:买购到期{buy_expiry.strftime('%Y-%m-%d')} vs 卖购到期{sell_expiry.strftime('%Y-%m-%d')}"
|
|
|
+ if not silent:
|
|
|
+ print(f" {month_type}牛差策略失败:{reason}")
|
|
|
+ return None, None, reason
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f" {month_type}牛差策略匹配成功:")
|
|
|
+ print(f" 卖购期权:行权价 {sell_call['exercise_price']:.3f}, 价格 {sell_call['price']:.4f}")
|
|
|
+ print(f" 买购期权:行权价 {buy_call['exercise_price']:.3f}, 价格 {buy_call['price']:.4f}")
|
|
|
+ print(f" 到期日:{sell_expiry.strftime('%Y-%m-%d')}")
|
|
|
+
|
|
|
+ return buy_call, sell_call, None
|
|
|
|
|
|
- def open_bull_spread_position(self, date, etf_price, quantity=None):
|
|
|
- """开仓牛差组合"""
|
|
|
- if quantity is None:
|
|
|
- quantity = self.params['一组张数']
|
|
|
-
|
|
|
- print(f"尝试开仓: 日期={date}, ETF价格={etf_price:.4f}")
|
|
|
|
|
|
- # 选择买购期权(深度实值)
|
|
|
- buy_call = self.select_call_option_to_buy(date, etf_price)
|
|
|
- if buy_call is None:
|
|
|
- print(f" 失败: 未找到合适的买购期权")
|
|
|
- return None
|
|
|
-
|
|
|
- print(f" 找到买购期权: 代码={buy_call['code']}, 行权价={buy_call['exercise_price']:.4f}, 权利金={buy_call['option_price']:.4f}")
|
|
|
-
|
|
|
- # 选择卖购期权(平值)
|
|
|
- sell_call = self.select_call_option_to_sell(date, etf_price)
|
|
|
- if sell_call is None:
|
|
|
- print(f" 失败: 未找到合适的卖购期权")
|
|
|
- return None
|
|
|
-
|
|
|
- print(f" 找到卖购期权: 代码={sell_call['code']}, 行权价={sell_call['exercise_price']:.4f}, 权利金={sell_call['option_price']:.4f}")
|
|
|
-
|
|
|
- # 确保买购行权价 < 卖购行权价
|
|
|
- if buy_call['exercise_price'] >= sell_call['exercise_price']:
|
|
|
- print(f" 失败: 买购行权价({buy_call['exercise_price']:.4f}) >= 卖购行权价({sell_call['exercise_price']:.4f})")
|
|
|
- return None
|
|
|
-
|
|
|
- # 计算单张最大盈利和最小盈利
|
|
|
- max_profit_per_contract = (sell_call['exercise_price'] - buy_call['exercise_price']
|
|
|
- - buy_call['option_price'] + sell_call['option_price'])
|
|
|
- min_profit_per_contract = sell_call['option_price']
|
|
|
+
|
|
|
+ def calculate_bull_spread_profit(self, buy_call, sell_call, contract_size=None):
|
|
|
+ """计算牛差组合的盈利情况"""
|
|
|
+ if contract_size is None:
|
|
|
+ contract_size = self.get_safe_contract_size()
|
|
|
+
|
|
|
+ # 单张最大盈利 = (卖购行权价 - 买购行权价 - 买购权利金 + 卖购权利金) * 10000
|
|
|
+ max_profit_per_contract = (
|
|
|
+ sell_call['exercise_price'] - buy_call['exercise_price']
|
|
|
+ - buy_call['price'] + sell_call['price']
|
|
|
+ ) * 10000
|
|
|
+
|
|
|
+ # 最小盈利(卖购权利金)
|
|
|
+ min_profit_per_contract = sell_call['price'] * 10000
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'max_profit_per_contract': max_profit_per_contract,
|
|
|
+ 'min_profit_per_contract': min_profit_per_contract,
|
|
|
+ 'total_max_profit': max_profit_per_contract * contract_size,
|
|
|
+ 'total_min_profit': min_profit_per_contract * contract_size
|
|
|
+ }
|
|
|
|
|
|
+ def _create_bull_spread_position(self, trade_date, etf_price, buy_call, sell_call, position_type='main', silent=False, save_to_csv=True):
|
|
|
+ """直接使用已选择的期权信息创建牛差仓位,避免重复期权选择"""
|
|
|
+ # 输出期权选择成功的信息
|
|
|
+ if not silent:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 卖购期权选择成功,sell_call: {sell_call}")
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 检查买购期权选择,buy_call: {buy_call}")
|
|
|
+
|
|
|
+ # 安全获取合约数量
|
|
|
+ contract_size = self.get_safe_contract_size()
|
|
|
+
|
|
|
+ # 计算盈利信息
|
|
|
+ profit_info = self.calculate_bull_spread_profit(buy_call, sell_call, contract_size)
|
|
|
+
|
|
|
+ # 创建仓位记录
|
|
|
position = {
|
|
|
- 'open_date': date,
|
|
|
+ 'open_date': trade_date,
|
|
|
'etf_price': etf_price,
|
|
|
- 'buy_call_code': buy_call['code'],
|
|
|
- 'buy_call_strike': buy_call['exercise_price'],
|
|
|
- 'buy_call_price': buy_call['option_price'],
|
|
|
- 'sell_call_code': sell_call['code'],
|
|
|
- 'sell_call_strike': sell_call['exercise_price'],
|
|
|
- 'sell_call_price': sell_call['option_price'],
|
|
|
- 'quantity': quantity,
|
|
|
- 'max_profit_per_contract': max_profit_per_contract,
|
|
|
- 'min_profit_per_contract': min_profit_per_contract,
|
|
|
- 'max_profit_total': max_profit_per_contract * quantity * 10000,
|
|
|
- 'min_profit_total': min_profit_per_contract * quantity * 10000,
|
|
|
- 'expire_date': sell_call['last_trade_date'],
|
|
|
+ 'buy_call': buy_call,
|
|
|
+ 'sell_call': sell_call,
|
|
|
+ 'contract_size': contract_size,
|
|
|
+ 'profit_info': profit_info,
|
|
|
+ 'position_type': position_type,
|
|
|
'status': 'open',
|
|
|
- 'is_additional': False # 是否为加仓
|
|
|
+ 'strategy_type': StrategyType.BULL_SPREAD, # 标记策略类型
|
|
|
+ 'add_position_trigger_price': etf_price - self.config['add_position_threshold'][self.get_underlying_code()] if position_type == 'main' else None
|
|
|
}
|
|
|
|
|
|
self.positions.append(position)
|
|
|
|
|
|
- # 记录交易
|
|
|
- self.trades.append({
|
|
|
- 'date': date,
|
|
|
- 'action': 'open_bull_spread',
|
|
|
- 'details': position
|
|
|
- })
|
|
|
-
|
|
|
- print(f" 成功开仓: 数量={quantity}张, 最大盈利={max_profit_per_contract:.4f}元/张")
|
|
|
+ # 记录交易(内存)
|
|
|
+ trade_record = {
|
|
|
+ '交易日期': trade_date,
|
|
|
+ '交易类型': '开仓',
|
|
|
+ '仓位类型': position_type,
|
|
|
+ '策略类型': StrategyType.BULL_SPREAD.value, # 新增字段
|
|
|
+ 'ETF标的': self.underlying_symbol,
|
|
|
+ '买购期权价格': buy_call['price'],
|
|
|
+ '买购期权行权价': buy_call['exercise_price'],
|
|
|
+ '买购期权到期日': buy_call['expiry_date'],
|
|
|
+ '卖购期权价格': sell_call['price'],
|
|
|
+ '卖购期权行权价': sell_call['exercise_price'],
|
|
|
+ '卖购期权到期日': sell_call['expiry_date'],
|
|
|
+ '合约数量': contract_size,
|
|
|
+ 'ETF价格': etf_price,
|
|
|
+ '单张最大盈利': profit_info['max_profit_per_contract'],
|
|
|
+ '单张最小盈利': profit_info['min_profit_per_contract'],
|
|
|
+ '总最大盈利': profit_info['total_max_profit'],
|
|
|
+ '总最小盈利': profit_info['total_min_profit']
|
|
|
+ }
|
|
|
+ self.trade_records.append(trade_record)
|
|
|
+
|
|
|
+ # 保存交易记录到CSV(如果需要)
|
|
|
+ if save_to_csv:
|
|
|
+ self.save_transaction_to_csv(trade_record)
|
|
|
|
|
|
return position
|
|
|
|
|
|
- def should_close_position(self, position, date, etf_price):
|
|
|
- """判断是否应该平仓"""
|
|
|
- # 1. 检查是否接近到期
|
|
|
- days_to_expire = (pd.to_datetime(position['expire_date']) - pd.to_datetime(date)).days
|
|
|
- if days_to_expire <= self.params['合约到期移仓日期最大']:
|
|
|
- return True, "approaching_expiry"
|
|
|
-
|
|
|
- # 2. 检查ETF大涨情况(接近最大盈利)
|
|
|
- current_max_profit = max(0, min(etf_price - position['buy_call_strike'],
|
|
|
- position['sell_call_strike'] - position['buy_call_strike']))
|
|
|
- if current_max_profit >= position['max_profit_per_contract'] * 0.9:
|
|
|
- return True, "max_profit_reached"
|
|
|
+ def try_bull_spread_strategy(self, trade_date, etf_price, month_idx, position_type='main', silent=False, save_to_csv=True):
|
|
|
+ """尝试牛差策略:深度实值买购+卖购期权"""
|
|
|
+
|
|
|
+ if not silent:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 开始尝试牛差策略")
|
|
|
+
|
|
|
+ # 1. 先尝试当月牛差策略
|
|
|
+ buy_call, sell_call, reason = self.try_bull_spread_for_month(trade_date, etf_price, month_idx, is_current_month=True, silent=silent)
|
|
|
+
|
|
|
+ if buy_call and sell_call:
|
|
|
+ if not silent:
|
|
|
+ print(f" 当月牛差策略匹配成功,执行开仓")
|
|
|
+ return self._create_bull_spread_position(trade_date, etf_price, buy_call, sell_call, position_type, silent, save_to_csv), None
|
|
|
+
|
|
|
+ # 2. 当月失败,尝试下月牛差策略
|
|
|
+ # 计算下个月的月份索引
|
|
|
+ trade_date_obj = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+ current_year = trade_date_obj.year
|
|
|
+ current_month = trade_date_obj.month
|
|
|
+
|
|
|
+ # 计算下个月
|
|
|
+ if current_month == 12:
|
|
|
+ next_year = current_year + 1
|
|
|
+ next_month = 1
|
|
|
+ else:
|
|
|
+ next_year = current_year
|
|
|
+ next_month = current_month + 1
|
|
|
+
|
|
|
+ # 下个月第一天和最后一天
|
|
|
+ next_month_start = pd.to_datetime(f'{next_year}-{next_month:02d}-01').date()
|
|
|
+ if next_month == 12:
|
|
|
+ next_month_end = pd.to_datetime(f'{next_year + 1}-01-01').date() - pd.Timedelta(days=1)
|
|
|
+ else:
|
|
|
+ next_month_end = pd.to_datetime(f'{next_year}-{next_month + 1:02d}-01').date() - pd.Timedelta(days=1)
|
|
|
+
|
|
|
+ # 查找对应的月份索引
|
|
|
+ next_month_idx = None
|
|
|
+ for i, month_date in enumerate(self.month_split[:-1]):
|
|
|
+ month_start = month_date.date() if hasattr(month_date, 'date') else month_date
|
|
|
+ month_end = self.month_split[i + 1].date() if hasattr(self.month_split[i + 1], 'date') else self.month_split[i + 1]
|
|
|
+
|
|
|
+ # 检查下月范围是否与month_split中的某个月份重叠
|
|
|
+ if (next_month_start <= month_end and next_month_end >= month_start):
|
|
|
+ next_month_idx = i
|
|
|
+ break
|
|
|
+
|
|
|
+ if next_month_idx is None:
|
|
|
+ if not silent:
|
|
|
+ print(f" 无法找到下月({next_year}-{next_month:02d})对应的月份索引")
|
|
|
+ return None, f"牛差策略失败: 当月原因({reason}),且无下月期权可用"
|
|
|
+
|
|
|
+ buy_call_next, sell_call_next, reason_next = self.try_bull_spread_for_month(trade_date, etf_price, next_month_idx, is_current_month=False, silent=silent)
|
|
|
+
|
|
|
+ if buy_call_next and sell_call_next:
|
|
|
+ if not silent:
|
|
|
+ print(f" 下月牛差策略匹配成功,执行开仓")
|
|
|
+ return self._create_bull_spread_position(trade_date, etf_price, buy_call_next, sell_call_next, position_type, silent, save_to_csv), None
|
|
|
+
|
|
|
+ # 两个月份都失败
|
|
|
+ combined_reason = f"当月失败({reason}),下月失败({reason_next})"
|
|
|
+ return None, f"牛差策略失败: {combined_reason}"
|
|
|
+
|
|
|
+ def open_position(self, trade_date, etf_price, position_type='main', silent=False, save_to_csv=True):
|
|
|
+ """开仓方法 - 牛差策略(深度实值买购+卖购)"""
|
|
|
+ # 确定月份索引
|
|
|
+ month_idx = 0
|
|
|
+ for i, month_date in enumerate(self.month_split[:-1]):
|
|
|
+ if trade_date >= month_date:
|
|
|
+ month_idx = i
|
|
|
+
|
|
|
+ # 尝试牛差策略(深度实值买购+卖购)
|
|
|
+ result, reason = self.try_bull_spread_strategy(trade_date, etf_price, month_idx, position_type, silent, save_to_csv)
|
|
|
+ if result is not None:
|
|
|
+ return result
|
|
|
+
|
|
|
+ # 牛差策略失败,输出详细原因
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 牛差策略开仓失败,原因:{reason}")
|
|
|
+ return None
|
|
|
|
|
|
- # 3. 检查ETF下跌情况(卖购权利金剩余低于阈值)
|
|
|
- sell_call_price = self.get_option_price(position['sell_call_code'], date)
|
|
|
- if sell_call_price is not None and sell_call_price <= self.params['卖购平仓权利金阈值']:
|
|
|
- return True, "etf_declined"
|
|
|
+ def should_close_position(self, position, current_date, etf_price):
|
|
|
+ """判断是否应该平仓 - 支持两种策略类型"""
|
|
|
+ if position['status'] != 'open':
|
|
|
+ return False, None
|
|
|
+
|
|
|
+ # 获取策略类型,兼容旧版本数据
|
|
|
+ strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
|
|
|
+
|
|
|
+ # 检查合约到期时间
|
|
|
+ expiry_date = pd.to_datetime(position['sell_call']['expiry_date'])
|
|
|
+ # 确保日期格式正确
|
|
|
+ current_date_obj = current_date.date() if hasattr(current_date, 'date') else current_date
|
|
|
+ expiry_date_obj = expiry_date.date() if hasattr(expiry_date, 'date') else expiry_date
|
|
|
+ days_to_expiry = len(get_trade_days(current_date_obj, expiry_date_obj)) - 1
|
|
|
+
|
|
|
+ # 到期日临近(对所有策略类型都适用)
|
|
|
+ if days_to_expiry <= self.config['max_days_before_expiry']:
|
|
|
+ return True, '过期时间平仓'
|
|
|
+
|
|
|
+ # 获取卖购行权价
|
|
|
+ sell_call_strike = position['sell_call']['exercise_price']
|
|
|
+
|
|
|
+ # 牛差策略的平仓逻辑:根据ETF价格与卖购行权价关系选择条件
|
|
|
+ if etf_price >= sell_call_strike:
|
|
|
+ # ETF价格大于等于卖购行权价时,检查最大盈利平仓条件
|
|
|
+ return self._check_max_profit_close_condition(position, current_date, etf_price, sell_call_strike)
|
|
|
+ else:
|
|
|
+ # ETF价格小于卖购行权价时,检查卖购权利金剩余平仓条件
|
|
|
+ return self._check_sell_call_close_condition(position, current_date, etf_price, sell_call_strike)
|
|
|
|
|
|
return False, None
|
|
|
+
|
|
|
|
|
|
- def close_position(self, position, date, reason):
|
|
|
- """平仓操作"""
|
|
|
- buy_call_price = self.get_option_price(position['buy_call_code'], date)
|
|
|
- sell_call_price = self.get_option_price(position['sell_call_code'], date)
|
|
|
-
|
|
|
- if buy_call_price is None or sell_call_price is None:
|
|
|
- return None
|
|
|
-
|
|
|
- # 计算平仓收益
|
|
|
- buy_call_pnl = (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
|
|
|
- sell_call_pnl = (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
|
|
|
- total_pnl = buy_call_pnl + sell_call_pnl
|
|
|
-
|
|
|
- # 更新持仓状态
|
|
|
- position['status'] = 'closed'
|
|
|
- position['close_date'] = date
|
|
|
- position['close_reason'] = reason
|
|
|
- position['close_buy_call_price'] = buy_call_price
|
|
|
- position['close_sell_call_price'] = sell_call_price
|
|
|
- position['realized_pnl'] = total_pnl
|
|
|
+
|
|
|
+ def _check_max_profit_close_condition(self, position, current_date, etf_price, sell_call_strike):
|
|
|
+ """检查最大盈利平仓条件(牛差策略专用)"""
|
|
|
+ try:
|
|
|
+ query_date = current_date.date() if hasattr(current_date, 'date') else current_date
|
|
|
+
|
|
|
+ # 获取买购期权价格
|
|
|
+ q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == position['buy_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ buy_price_result = opt.run_query(q_buy_price)
|
|
|
+
|
|
|
+ # 获取卖购期权价格
|
|
|
+ q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ sell_price_result = opt.run_query(q_sell_price)
|
|
|
+
|
|
|
+ if not buy_price_result.empty and not sell_price_result.empty:
|
|
|
+ current_buy_call_price = buy_price_result['close'].iloc[0]
|
|
|
+ current_sell_call_price = sell_price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ # 计算当前盈利
|
|
|
+ buy_call_pnl = (current_buy_call_price - position['buy_call']['price']) * 10000
|
|
|
+ sell_call_pnl = (position['sell_call']['price'] - current_sell_call_price) * 10000
|
|
|
+ current_pnl_per_contract = buy_call_pnl + sell_call_pnl
|
|
|
+
|
|
|
+ # 获取最大盈利
|
|
|
+ max_profit_per_contract = position['profit_info']['max_profit_per_contract']
|
|
|
+
|
|
|
+ # 判断是否达到平仓阈值
|
|
|
+ if max_profit_per_contract > 0 and current_pnl_per_contract >= max_profit_per_contract * self.config['max_profit_close_threshold']:
|
|
|
+ print(f"{current_date.strftime('%Y-%m-%d')} 触发最大盈利平仓: 当前盈利: {current_pnl_per_contract:.2f}, 最大盈利: {max_profit_per_contract:.2f}")
|
|
|
+ return True, '最大盈利平仓'
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"{current_date.strftime('%Y-%m-%d')} 检查最大盈利平仓条件时出错: {e}")
|
|
|
+
|
|
|
+ return False, None
|
|
|
+
|
|
|
+ def _check_sell_call_close_condition(self, position, current_date, etf_price, sell_call_strike):
|
|
|
+ """检查卖购权利金剩余平仓条件(两种策略共用)"""
|
|
|
+ try:
|
|
|
+ query_date = current_date.date() if hasattr(current_date, 'date') else current_date
|
|
|
+ q_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ price_result = opt.run_query(q_price)
|
|
|
+ if not price_result.empty:
|
|
|
+ current_sell_call_price = price_result['close'].iloc[0]
|
|
|
+ if current_sell_call_price < self.config['put_close_premium_threshold']:
|
|
|
+ print(f"{current_date.strftime('%Y-%m-%d')} 触发卖购权利金平仓: 卖购期权价格{current_sell_call_price:.4f}")
|
|
|
+ return True, '卖购权利金平仓'
|
|
|
+ except Exception as e:
|
|
|
+ print(f"{current_date.strftime('%Y-%m-%d')} 检查卖购权利金平仓条件时出错: {e}")
|
|
|
+
|
|
|
+ return False, None
|
|
|
+
|
|
|
|
|
|
- # 记录交易
|
|
|
- self.trades.append({
|
|
|
- 'date': date,
|
|
|
- 'action': 'close_bull_spread',
|
|
|
- 'reason': reason,
|
|
|
- 'pnl': total_pnl,
|
|
|
- 'details': position
|
|
|
- })
|
|
|
+
|
|
|
|
|
|
- return total_pnl
|
|
|
|
|
|
- def should_add_position(self, etf_price):
|
|
|
+ def close_position(self, position, current_date, etf_price, reason):
|
|
|
+ """平仓操作 - 支持两种策略类型"""
|
|
|
+ try:
|
|
|
+ # 获取策略类型,兼容旧版本数据
|
|
|
+ strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
|
|
|
+
|
|
|
+ query_date = current_date.date() if hasattr(current_date, 'date') else current_date
|
|
|
+
|
|
|
+ # 获取卖购期权当前价格(两种策略都需要)
|
|
|
+ q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ sell_price_result = opt.run_query(q_sell_price)
|
|
|
+ if sell_price_result.empty:
|
|
|
+ raise Exception(f"无法获取卖购期权{position['sell_call']['code']}在{query_date}的价格数据")
|
|
|
+ sell_call_close_price = sell_price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ # 计算卖购期权盈亏
|
|
|
+ sell_call_pnl = (position['sell_call']['price'] - sell_call_close_price) * position['contract_size'] * 10000
|
|
|
+
|
|
|
+ # 获取买购期权当前价格
|
|
|
+ q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == position['buy_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ buy_price_result = opt.run_query(q_buy_price)
|
|
|
+ if buy_price_result.empty:
|
|
|
+ raise Exception(f"无法获取买购期权{position['buy_call']['code']}在{query_date}的价格数据")
|
|
|
+ buy_call_close_price = buy_price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ buy_call_pnl = (buy_call_close_price - position['buy_call']['price']) * position['contract_size'] * 10000
|
|
|
+ total_pnl = buy_call_pnl + sell_call_pnl - 10 # 两种策略都扣除相同手续费
|
|
|
+
|
|
|
+ # 牛差策略平仓详情输出
|
|
|
+ print(f"{current_date.strftime('%Y-%m-%d')} 牛差策略平仓详情:")
|
|
|
+ print(f" 深度实值买购:开仓价格{position['buy_call']['price']:.4f} -> 平仓价格{buy_call_close_price:.4f}")
|
|
|
+ print(f" 深度实值买购盈亏:({buy_call_close_price:.4f} - {position['buy_call']['price']:.4f}) × {position['contract_size']} × 10000 = {buy_call_pnl:.2f}元")
|
|
|
+ print(f" 卖购期权:开仓价格{position['sell_call']['price']:.4f} -> 平仓价格{sell_call_close_price:.4f}")
|
|
|
+ print(f" 卖购期权盈亏:({position['sell_call']['price']:.4f} - {sell_call_close_price:.4f}) × {position['contract_size']} × 10000 = {sell_call_pnl:.2f}元")
|
|
|
+ print(f" 手续费:-10元")
|
|
|
+ print(f" 组合总盈亏:{buy_call_pnl:.2f} + {sell_call_pnl:.2f} - 10 = {total_pnl:.2f}元")
|
|
|
+
|
|
|
+ # 更新仓位状态
|
|
|
+ position['status'] = 'closed'
|
|
|
+ position['close_date'] = current_date
|
|
|
+ position['close_etf_price'] = etf_price
|
|
|
+ position['close_reason'] = reason
|
|
|
+ position['buy_call_close_price'] = buy_call_close_price
|
|
|
+ position['sell_call_close_price'] = sell_call_close_price
|
|
|
+ position['pnl'] = total_pnl
|
|
|
+
|
|
|
+ # 记录交易(内存)- 两种策略都有买购期权信息
|
|
|
+ trade_record = {
|
|
|
+ '交易日期': current_date,
|
|
|
+ '交易类型': '平仓',
|
|
|
+ '仓位类型': position['position_type'],
|
|
|
+ '策略类型': strategy_type.value, # 策略类型字段
|
|
|
+ 'ETF标的': self.underlying_symbol,
|
|
|
+ '买购期权价格': position['buy_call']['price'], # 两种策略都有买购期权
|
|
|
+ '买购期权行权价': position['buy_call']['exercise_price'],
|
|
|
+ '买购期权到期日': position['buy_call']['expiry_date'],
|
|
|
+ '卖购期权价格': position['sell_call']['price'],
|
|
|
+ '卖购期权行权价': position['sell_call']['exercise_price'],
|
|
|
+ '卖购期权到期日': position['sell_call']['expiry_date'],
|
|
|
+ '合约数量': position['contract_size'],
|
|
|
+ 'ETF价格': etf_price,
|
|
|
+ '买购期权收盘价': buy_call_close_price,
|
|
|
+ '卖购期权收盘价': sell_call_close_price,
|
|
|
+ '开仓日期': position['open_date'],
|
|
|
+ '开仓ETF价格': position['etf_price'],
|
|
|
+ '买购期权盈亏': buy_call_pnl,
|
|
|
+ '卖购期权盈亏': sell_call_pnl,
|
|
|
+ '总盈亏': total_pnl,
|
|
|
+ '平仓原因': reason,
|
|
|
+ '单张最大盈利': '', # 平仓时不需要,保持字段一致性
|
|
|
+ '单张最小盈利': '', # 平仓时不需要,保持字段一致性
|
|
|
+ '总最大盈利': '', # 平仓时不需要,保持字段一致性
|
|
|
+ '总最小盈利': '' # 平仓时不需要,保持字段一致性
|
|
|
+ }
|
|
|
+ self.trade_records.append(trade_record)
|
|
|
+
|
|
|
+ # 保存交易记录到CSV
|
|
|
+ self.save_transaction_to_csv(trade_record)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"平仓时出错: {e}")
|
|
|
+ position['status'] = 'error'
|
|
|
+
|
|
|
+ def should_add_position(self, current_date, etf_price):
|
|
|
"""判断是否应该加仓"""
|
|
|
- # 检查是否有开仓的主仓位
|
|
|
- main_positions = [p for p in self.positions if p['status'] == 'open' and not p['is_additional']]
|
|
|
+ # 检查是否有主仓位
|
|
|
+ main_positions = [p for p in self.positions if p['position_type'] == 'main' and p['status'] == 'open']
|
|
|
if not main_positions:
|
|
|
return False
|
|
|
|
|
|
- # 获取最近的主仓位
|
|
|
- latest_main = max(main_positions, key=lambda x: x['open_date'])
|
|
|
-
|
|
|
- # 检查价格下跌幅度
|
|
|
- etf_name = self.get_etf_name()
|
|
|
- threshold = self.params['加仓窗口阈值'].get(etf_name, 0.2)
|
|
|
+ # 获取最新主仓位
|
|
|
+ latest_main_position = main_positions[-1]
|
|
|
|
|
|
- if latest_main['etf_price'] - etf_price >= threshold:
|
|
|
- # 检查是否已经在这个价格水平加过仓
|
|
|
- existing_additional = [p for p in self.positions
|
|
|
- if p['status'] == 'open' and p['is_additional']
|
|
|
- and abs(p['etf_price'] - etf_price) < threshold * 0.5]
|
|
|
- if not existing_additional:
|
|
|
+ # 检查加仓次数是否超限
|
|
|
+ add_positions = [p for p in self.positions if p['position_type'] == 'add' and p['status'] == 'open']
|
|
|
+ if len(add_positions) >= self.config['max_add_positions']:
|
|
|
+ return False
|
|
|
+
|
|
|
+ # 检查是否触发加仓条件
|
|
|
+ trigger_price = latest_main_position['add_position_trigger_price']
|
|
|
+
|
|
|
+ if trigger_price and etf_price <= trigger_price:
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
- def run_backtest(self):
|
|
|
- """运行回测"""
|
|
|
- print(f"开始回测: {self.start_date} 到 {self.end_date}")
|
|
|
- print(f"标的: {self.underlying} ({self.get_etf_name()})")
|
|
|
-
|
|
|
- # 获取ETF价格数据
|
|
|
- etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
|
|
|
-
|
|
|
- # 初始化
|
|
|
- portfolio_value = []
|
|
|
- etf_benchmark = []
|
|
|
-
|
|
|
- with tqdm.tqdm(self.trade_days, desc="回测进度") as pbar:
|
|
|
- for date in pbar:
|
|
|
- if date not in etf_prices.index:
|
|
|
- continue
|
|
|
-
|
|
|
- etf_price = etf_prices[date]
|
|
|
-
|
|
|
- # 检查现有持仓是否需要平仓
|
|
|
- open_positions = [p for p in self.positions if p['status'] == 'open']
|
|
|
- for position in open_positions:
|
|
|
- should_close, reason = self.should_close_position(position, date, etf_price)
|
|
|
- if should_close:
|
|
|
- self.close_position(position, date, reason)
|
|
|
-
|
|
|
- # 检查是否需要开新仓
|
|
|
- open_positions = [p for p in self.positions if p['status'] == 'open']
|
|
|
- if not open_positions: # 没有持仓时开仓
|
|
|
- self.open_bull_spread_position(date, etf_price)
|
|
|
- elif self.should_add_position(etf_price): # 加仓
|
|
|
- additional_pos = self.open_bull_spread_position(date, etf_price)
|
|
|
- if additional_pos:
|
|
|
- additional_pos['is_additional'] = True
|
|
|
-
|
|
|
- # 计算当日组合价值
|
|
|
- daily_pnl = self.calculate_daily_pnl(date)
|
|
|
- portfolio_value.append(daily_pnl)
|
|
|
-
|
|
|
- # ETF基准收益
|
|
|
- if len(etf_benchmark) == 0:
|
|
|
- etf_benchmark.append(0)
|
|
|
+ def save_transaction_to_csv(self, transaction_data):
|
|
|
+ """保存交易记录到CSV文件"""
|
|
|
+ try:
|
|
|
+ # 数据验证 - 确保必需字段存在
|
|
|
+ required_fields = ['交易日期', '交易类型', '仓位类型', 'ETF标的', '合约数量', 'ETF价格']
|
|
|
+ for field in required_fields:
|
|
|
+ if field not in transaction_data or transaction_data[field] is None:
|
|
|
+ print(f"警告: 交易记录缺少必需字段 {field},跳过保存")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 数据验证 - 确保数据类型正确
|
|
|
+ if not isinstance(transaction_data.get('合约数量'), (int, float)) or transaction_data.get('合约数量') <= 0:
|
|
|
+ print(f"警告: 合约数量无效 {transaction_data.get('合约数量')},跳过保存")
|
|
|
+ return
|
|
|
+
|
|
|
+ if not isinstance(transaction_data.get('ETF价格'), (int, float)) or transaction_data.get('ETF价格') <= 0:
|
|
|
+ print(f"警告: ETF价格无效 {transaction_data.get('ETF价格')},跳过保存")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 复制数据并标准化格式
|
|
|
+ data_copy = transaction_data.copy()
|
|
|
+
|
|
|
+ # 定义完整的字段顺序,确保开仓和平仓记录字段一致
|
|
|
+ standard_fields = [
|
|
|
+ '交易日期', '交易类型', '仓位类型', '策略类型', 'ETF标的', # 新增策略类型字段
|
|
|
+ '买购期权价格', '买购期权行权价', '买购期权到期日',
|
|
|
+ '卖购期权价格', '卖购期权行权价', '卖购期权到期日',
|
|
|
+ '合约数量', 'ETF价格',
|
|
|
+ '单张最大盈利', '单张最小盈利', '总最大盈利', '总最小盈利',
|
|
|
+ '买购期权收盘价', '卖购期权收盘价', '开仓日期', '开仓ETF价格',
|
|
|
+ '买购期权盈亏', '卖购期权盈亏', '总盈亏', '平仓原因'
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 确保所有字段都存在,缺失的用空字符串填充
|
|
|
+ standardized_data = {}
|
|
|
+ for field in standard_fields:
|
|
|
+ standardized_data[field] = data_copy.get(field, '')
|
|
|
+
|
|
|
+ # 转换日期字段为YYYY-MM-DD格式
|
|
|
+ date_fields = ['交易日期', '买购期权到期日', '卖购期权到期日', '开仓日期']
|
|
|
+ for field in date_fields:
|
|
|
+ if standardized_data[field] and standardized_data[field] != '':
|
|
|
+ try:
|
|
|
+ if hasattr(standardized_data[field], 'strftime'):
|
|
|
+ standardized_data[field] = standardized_data[field].strftime('%Y-%m-%d')
|
|
|
+ elif hasattr(standardized_data[field], 'date'):
|
|
|
+ standardized_data[field] = standardized_data[field].date().strftime('%Y-%m-%d')
|
|
|
+ else:
|
|
|
+ # 如果是字符串,尝试解析后转换
|
|
|
+ date_obj = pd.to_datetime(standardized_data[field])
|
|
|
+ standardized_data[field] = date_obj.strftime('%Y-%m-%d')
|
|
|
+ except:
|
|
|
+ print(f"警告: 无法转换日期字段 {field}: {standardized_data[field]}")
|
|
|
+ pass # 保持原值
|
|
|
+
|
|
|
+ # 检查文件是否存在
|
|
|
+ file_exists = os.path.exists(self.transaction_csv_path)
|
|
|
+
|
|
|
+ # 转换为DataFrame,保持字段顺序
|
|
|
+ df = pd.DataFrame([standardized_data], columns=standard_fields)
|
|
|
+
|
|
|
+ # 保存到CSV
|
|
|
+ if file_exists:
|
|
|
+ df.to_csv(self.transaction_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
|
|
|
+ else:
|
|
|
+ df.to_csv(self.transaction_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"保存交易记录到CSV时出错: {e}")
|
|
|
+ print(f"问题数据: {transaction_data}")
|
|
|
+
|
|
|
+ def save_daily_position_to_csv(self, position_data):
|
|
|
+ """保存每日持仓记录到CSV文件"""
|
|
|
+ try:
|
|
|
+ # 复制数据并标准化日期格式
|
|
|
+ data_copy = position_data.copy()
|
|
|
+
|
|
|
+ # 转换日期字段为YYYY-MM-DD格式(使用中文字段名)
|
|
|
+ if '交易日期' in data_copy and data_copy['交易日期'] is not None:
|
|
|
+ if hasattr(data_copy['交易日期'], 'strftime'):
|
|
|
+ data_copy['交易日期'] = data_copy['交易日期'].strftime('%Y-%m-%d')
|
|
|
+ elif hasattr(data_copy['交易日期'], 'date'):
|
|
|
+ data_copy['交易日期'] = data_copy['交易日期'].date().strftime('%Y-%m-%d')
|
|
|
else:
|
|
|
- etf_return = (etf_price / etf_prices[self.trade_days[0]] - 1) * 100000 # 假设初始投资10万
|
|
|
- etf_benchmark.append(etf_return)
|
|
|
-
|
|
|
- # 生成回测报告
|
|
|
- self.generate_detailed_report()
|
|
|
-
|
|
|
- return portfolio_value, etf_benchmark
|
|
|
-
|
|
|
- def calculate_daily_pnl(self, date):
|
|
|
- """计算每日损益"""
|
|
|
- total_pnl = 0
|
|
|
-
|
|
|
- for position in self.positions:
|
|
|
- if position['status'] == 'closed':
|
|
|
- if 'realized_pnl' in position:
|
|
|
- total_pnl += position['realized_pnl']
|
|
|
- elif position['status'] == 'open':
|
|
|
- # 计算未实现损益
|
|
|
- buy_call_price = self.get_option_price(position['buy_call_code'], date)
|
|
|
- sell_call_price = self.get_option_price(position['sell_call_code'], date)
|
|
|
-
|
|
|
- if buy_call_price is not None and sell_call_price is not None:
|
|
|
- buy_call_pnl = (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
|
|
|
- sell_call_pnl = (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
|
|
|
- total_pnl += buy_call_pnl + sell_call_pnl
|
|
|
-
|
|
|
- return total_pnl
|
|
|
-
|
|
|
- def export_data_to_csv(self, filename_prefix="bull_spread_data"):
|
|
|
- """导出数据到CSV文件,用于线下分析"""
|
|
|
-
|
|
|
- # 1. 导出持仓数据
|
|
|
- positions_data = []
|
|
|
- for pos in self.positions:
|
|
|
- positions_data.append({
|
|
|
- 'open_date': pos['open_date'],
|
|
|
- 'etf_price': pos['etf_price'],
|
|
|
- 'buy_call_code': pos['buy_call_code'],
|
|
|
- 'buy_call_strike': pos['buy_call_strike'],
|
|
|
- 'buy_call_price': pos['buy_call_price'],
|
|
|
- 'sell_call_code': pos['sell_call_code'],
|
|
|
- 'sell_call_strike': pos['sell_call_strike'],
|
|
|
- 'sell_call_price': pos['sell_call_price'],
|
|
|
- 'quantity': pos['quantity'],
|
|
|
- 'max_profit_total': pos['max_profit_total'],
|
|
|
- 'expire_date': pos['expire_date'],
|
|
|
- 'status': pos['status'],
|
|
|
- 'is_additional': pos['is_additional'],
|
|
|
- 'close_date': pos.get('close_date', ''),
|
|
|
- 'close_reason': pos.get('close_reason', ''),
|
|
|
- 'realized_pnl': pos.get('realized_pnl', 0)
|
|
|
- })
|
|
|
-
|
|
|
- positions_df = pd.DataFrame(positions_data)
|
|
|
- positions_df.to_csv(f"{filename_prefix}_positions.csv", index=False, encoding='utf-8-sig')
|
|
|
-
|
|
|
- # 2. 导出交易记录
|
|
|
- trades_data = []
|
|
|
- for trade in self.trades:
|
|
|
- trades_data.append({
|
|
|
- 'date': trade['date'],
|
|
|
- 'action': trade['action'],
|
|
|
- 'pnl': trade.get('pnl', 0),
|
|
|
- 'reason': trade.get('reason', ''),
|
|
|
- 'details': str(trade.get('details', ''))
|
|
|
- })
|
|
|
-
|
|
|
- trades_df = pd.DataFrame(trades_data)
|
|
|
- trades_df.to_csv(f"{filename_prefix}_trades.csv", index=False, encoding='utf-8-sig')
|
|
|
-
|
|
|
- # 3. 导出每日损益数据
|
|
|
- daily_data = []
|
|
|
- etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
|
|
|
-
|
|
|
- for date in self.trade_days:
|
|
|
- if date not in etf_prices.index:
|
|
|
- continue
|
|
|
-
|
|
|
- etf_price = etf_prices[date]
|
|
|
-
|
|
|
- # 计算买购和卖购分别的损益
|
|
|
- buy_call_pnl = 0
|
|
|
- sell_call_pnl = 0
|
|
|
- total_positions = 0
|
|
|
-
|
|
|
- for position in self.positions:
|
|
|
- if position['open_date'] <= date and (position['status'] == 'open' or position.get('close_date', date) >= date):
|
|
|
- total_positions += 1
|
|
|
-
|
|
|
- if position['status'] == 'closed' and position.get('close_date') == date:
|
|
|
- # 已平仓的实现损益
|
|
|
- buy_call_pnl += position.get('realized_pnl', 0) / 2 # 简化分配
|
|
|
- sell_call_pnl += position.get('realized_pnl', 0) / 2
|
|
|
- elif position['status'] == 'open':
|
|
|
- # 未实现损益
|
|
|
- buy_call_price = self.get_option_price(position['buy_call_code'], date)
|
|
|
- sell_call_price = self.get_option_price(position['sell_call_code'], date)
|
|
|
-
|
|
|
- if buy_call_price is not None:
|
|
|
- buy_call_pnl += (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
|
|
|
-
|
|
|
- if sell_call_price is not None:
|
|
|
- sell_call_pnl += (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
|
|
|
-
|
|
|
- # ETF基准收益
|
|
|
- etf_return = (etf_price / etf_prices.iloc[0] - 1) * 100000 if len(etf_prices) > 0 else 0
|
|
|
-
|
|
|
- daily_data.append({
|
|
|
- 'date': date,
|
|
|
- 'etf_price': etf_price,
|
|
|
- 'etf_return': etf_return,
|
|
|
- 'buy_call_pnl': buy_call_pnl,
|
|
|
- 'sell_call_pnl': sell_call_pnl,
|
|
|
- 'total_pnl': buy_call_pnl + sell_call_pnl,
|
|
|
- 'total_positions': total_positions
|
|
|
- })
|
|
|
-
|
|
|
- daily_df = pd.DataFrame(daily_data)
|
|
|
- daily_df.to_csv(f"{filename_prefix}_daily.csv", index=False, encoding='utf-8-sig')
|
|
|
-
|
|
|
- print(f"数据已导出到以下文件:")
|
|
|
- print(f"- {filename_prefix}_positions.csv (持仓数据)")
|
|
|
- print(f"- {filename_prefix}_trades.csv (交易记录)")
|
|
|
- print(f"- {filename_prefix}_daily.csv (每日损益)")
|
|
|
-
|
|
|
- return positions_df, trades_df, daily_df
|
|
|
-
|
|
|
- def generate_detailed_report(self):
|
|
|
- """生成详细的分析报告,分别分析买购和卖购收益"""
|
|
|
- print("\n" + "="*80)
|
|
|
- print("深度实值牛差策略详细回测报告")
|
|
|
- print("="*80)
|
|
|
-
|
|
|
- # 获取ETF价格数据
|
|
|
- etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
|
|
|
-
|
|
|
- # 计算分项收益
|
|
|
- buy_call_returns = []
|
|
|
- sell_call_returns = []
|
|
|
- combined_returns = []
|
|
|
- etf_returns = []
|
|
|
- dates = []
|
|
|
-
|
|
|
- initial_etf_price = etf_prices.iloc[0] if len(etf_prices) > 0 else 1
|
|
|
-
|
|
|
- for date in self.trade_days:
|
|
|
- if date not in etf_prices.index:
|
|
|
- continue
|
|
|
-
|
|
|
- etf_price = etf_prices[date]
|
|
|
- dates.append(date)
|
|
|
-
|
|
|
- # 计算买购和卖购分别的损益
|
|
|
- buy_call_pnl = 0
|
|
|
- sell_call_pnl = 0
|
|
|
-
|
|
|
- for position in self.positions:
|
|
|
- if position['open_date'] <= date:
|
|
|
- if position['status'] == 'closed':
|
|
|
- if 'realized_pnl' in position:
|
|
|
- # 简化处理:假设买购和卖购各承担一半损益
|
|
|
- buy_call_pnl += position['realized_pnl'] / 2
|
|
|
- sell_call_pnl += position['realized_pnl'] / 2
|
|
|
- elif position['status'] == 'open':
|
|
|
- # 计算未实现损益
|
|
|
- buy_call_price = self.get_option_price(position['buy_call_code'], date)
|
|
|
- sell_call_price = self.get_option_price(position['sell_call_code'], date)
|
|
|
-
|
|
|
- if buy_call_price is not None:
|
|
|
- buy_call_pnl += (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
|
|
|
-
|
|
|
- if sell_call_price is not None:
|
|
|
- sell_call_pnl += (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
|
|
|
-
|
|
|
- buy_call_returns.append(buy_call_pnl)
|
|
|
- sell_call_returns.append(sell_call_pnl)
|
|
|
- combined_returns.append(buy_call_pnl + sell_call_pnl)
|
|
|
-
|
|
|
- # ETF基准收益
|
|
|
- etf_return = (etf_price / initial_etf_price - 1) * 100000 # 假设10万本金
|
|
|
- etf_returns.append(etf_return)
|
|
|
-
|
|
|
- # 基本统计
|
|
|
- total_trades = len([t for t in self.trades if t['action'] == 'close_bull_spread'])
|
|
|
- winning_trades = len([t for t in self.trades if t['action'] == 'close_bull_spread' and t['pnl'] > 0])
|
|
|
-
|
|
|
- if total_trades > 0:
|
|
|
- win_rate = winning_trades / total_trades * 100
|
|
|
- total_pnl = sum([t['pnl'] for t in self.trades if t['action'] == 'close_bull_spread'])
|
|
|
- avg_pnl = total_pnl / total_trades
|
|
|
- else:
|
|
|
- win_rate = 0
|
|
|
- total_pnl = 0
|
|
|
- avg_pnl = 0
|
|
|
-
|
|
|
- print(f"交易次数: {total_trades}")
|
|
|
- print(f"胜率: {win_rate:.2f}%")
|
|
|
- print(f"总收益: {total_pnl:.2f}元")
|
|
|
- print(f"平均每笔收益: {avg_pnl:.2f}元")
|
|
|
-
|
|
|
- # 分项收益统计和对比分析
|
|
|
- if len(buy_call_returns) > 0:
|
|
|
- final_buy_call = buy_call_returns[-1]
|
|
|
- final_sell_call = sell_call_returns[-1]
|
|
|
- final_combined = combined_returns[-1]
|
|
|
- final_etf = etf_returns[-1]
|
|
|
-
|
|
|
- print(f"\n=== 分项收益分析 ===")
|
|
|
- print(f"买购期权收益: {final_buy_call:.2f}元")
|
|
|
- print(f"卖购期权收益: {final_sell_call:.2f}元")
|
|
|
- print(f"组合总收益: {final_combined:.2f}元")
|
|
|
- print(f"ETF基准收益: {final_etf:.2f}元")
|
|
|
-
|
|
|
- print(f"\n=== 与ETF基准对比分析 ===")
|
|
|
- print(f"1. 牛差策略 vs ETF: {final_combined:.2f} vs {final_etf:.2f} = {final_combined - final_etf:+.2f}元")
|
|
|
- print(f"2. 买购策略 vs ETF: {final_buy_call:.2f} vs {final_etf:.2f} = {final_buy_call - final_etf:+.2f}元")
|
|
|
- print(f"3. 卖购策略 vs ETF: {final_sell_call:.2f} vs {final_etf:.2f} = {final_sell_call - final_etf:+.2f}元")
|
|
|
-
|
|
|
- # 持仓统计
|
|
|
- open_positions = len([p for p in self.positions if p['status'] == 'open'])
|
|
|
- closed_positions = len([p for p in self.positions if p['status'] == 'closed'])
|
|
|
- print(f"\n=== 持仓统计 ===")
|
|
|
- print(f"当前持仓: {open_positions}个")
|
|
|
- print(f"已平仓位: {closed_positions}个")
|
|
|
-
|
|
|
- # 绘制分项收益曲线
|
|
|
- if len(dates) > 0:
|
|
|
- self.plot_detailed_performance(dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns)
|
|
|
- self.plot_strategy_vs_etf_comparison(dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns)
|
|
|
-
|
|
|
- return {
|
|
|
- 'dates': dates,
|
|
|
- 'buy_call_returns': buy_call_returns,
|
|
|
- 'sell_call_returns': sell_call_returns,
|
|
|
- 'combined_returns': combined_returns,
|
|
|
- 'etf_returns': etf_returns
|
|
|
+ # 如果是字符串,尝试解析后转换
|
|
|
+ try:
|
|
|
+ date_obj = pd.to_datetime(data_copy['交易日期'])
|
|
|
+ data_copy['交易日期'] = date_obj.strftime('%Y-%m-%d')
|
|
|
+ except:
|
|
|
+ pass # 保持原值
|
|
|
+
|
|
|
+ # 处理持仓详情字段 - 将列表转换为字符串
|
|
|
+ if '持仓详情' in data_copy and isinstance(data_copy['持仓详情'], list):
|
|
|
+ if data_copy['持仓详情']: # 如果列表不为空
|
|
|
+ detail_strings = []
|
|
|
+ for detail in data_copy['持仓详情']:
|
|
|
+ detail_str = f"{detail['期权类别']}:{detail['持仓标的代码']}@{detail['行权价格']:.2f}×{detail['合约数量']}(盈亏{detail['盈亏金额']:.2f})"
|
|
|
+ detail_strings.append(detail_str)
|
|
|
+ data_copy['持仓详情'] = '; '.join(detail_strings)
|
|
|
+ else:
|
|
|
+ data_copy['持仓详情'] = '无持仓'
|
|
|
+
|
|
|
+ # 添加到每日持仓列表
|
|
|
+ self.daily_positions.append(data_copy)
|
|
|
+
|
|
|
+ # 创建DataFrame,排除列表类型的复杂字段
|
|
|
+ csv_data = {k: v for k, v in data_copy.items() if not isinstance(v, list)}
|
|
|
+ df = pd.DataFrame([csv_data])
|
|
|
+
|
|
|
+ # 检查文件是否存在
|
|
|
+ file_exists = os.path.exists(self.position_csv_path)
|
|
|
+
|
|
|
+ # 保存到CSV
|
|
|
+ if file_exists:
|
|
|
+ df.to_csv(self.position_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
|
|
|
+ else:
|
|
|
+ df.to_csv(self.position_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"保存持仓记录到CSV时出错: {e}")
|
|
|
+ print(f"问题数据: {position_data}")
|
|
|
+
|
|
|
+ def record_daily_positions(self, trade_date, etf_price):
|
|
|
+ """记录每日持仓状况"""
|
|
|
+ # 获取当前所有开仓状态的持仓
|
|
|
+ open_positions = [p for p in self.positions if p['status'] == 'open']
|
|
|
+
|
|
|
+ if not open_positions:
|
|
|
+ # 如果没有持仓,记录一条空仓记录
|
|
|
+ position_record = {
|
|
|
+ '交易日期': trade_date,
|
|
|
+ 'ETF价格': etf_price,
|
|
|
+ '总仓位数': 0,
|
|
|
+ '主仓位数': 0,
|
|
|
+ '加仓仓位数': 0,
|
|
|
+ '总合约数': 0,
|
|
|
+ '总组合盈亏': 0,
|
|
|
+ '总卖购盈亏': 0,
|
|
|
+ '总买购盈亏': 0,
|
|
|
+ '持仓详情': []
|
|
|
+ }
|
|
|
+ self.save_daily_position_to_csv(position_record)
|
|
|
+ return
|
|
|
+
|
|
|
+ # 统计持仓信息
|
|
|
+ main_positions = [p for p in open_positions if p['position_type'] == 'main']
|
|
|
+ add_positions = [p for p in open_positions if p['position_type'] == 'add']
|
|
|
+ total_contracts = sum(p['contract_size'] for p in open_positions)
|
|
|
+
|
|
|
+ # 记录详细持仓信息
|
|
|
+ position_details = []
|
|
|
+ total_combo_pnl = 0
|
|
|
+ total_sell_call_pnl = 0
|
|
|
+ total_buy_call_pnl = 0
|
|
|
+
|
|
|
+ for i, pos in enumerate(open_positions):
|
|
|
+ # 获取策略类型,兼容旧版本数据
|
|
|
+ strategy_type = pos.get('strategy_type', StrategyType.BULL_SPREAD)
|
|
|
+
|
|
|
+ # 获取当前期权价值(尽量获取,失败则使用开仓价格)
|
|
|
+ try:
|
|
|
+ query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+
|
|
|
+ # 获取卖购期权当前价格(两种策略都需要)
|
|
|
+ q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == pos['sell_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ sell_price_result = opt.run_query(q_sell_price)
|
|
|
+ if sell_price_result.empty:
|
|
|
+ raise Exception("无法获取卖购期权价格数据")
|
|
|
+ sell_call_current_price = sell_price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ # 计算卖购期权盈亏
|
|
|
+ sell_call_pnl = (pos['sell_call']['price'] - sell_call_current_price) * pos['contract_size'] * 10000
|
|
|
+
|
|
|
+ # 两种策略都有买购期权,需要计算买购期权盈亏
|
|
|
+ q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
|
|
|
+ opt.OPT_DAILY_PRICE.code == pos['buy_call']['code'],
|
|
|
+ opt.OPT_DAILY_PRICE.date == query_date
|
|
|
+ )
|
|
|
+ buy_price_result = opt.run_query(q_buy_price)
|
|
|
+ if buy_price_result.empty:
|
|
|
+ raise Exception("无法获取买购期权价格数据")
|
|
|
+ buy_call_current_price = buy_price_result['close'].iloc[0]
|
|
|
+
|
|
|
+ buy_call_pnl = (buy_call_current_price - pos['buy_call']['price']) * pos['contract_size'] * 10000
|
|
|
+ combo_pnl = buy_call_pnl + sell_call_pnl
|
|
|
+
|
|
|
+ except:
|
|
|
+ # 两种策略都有买购期权,异常处理相同
|
|
|
+ buy_call_current_price = pos['buy_call']['price']
|
|
|
+ sell_call_current_price = pos['sell_call']['price']
|
|
|
+ buy_call_pnl = 0
|
|
|
+ sell_call_pnl = 0
|
|
|
+ combo_pnl = 0
|
|
|
+
|
|
|
+ # 累计总盈亏
|
|
|
+ total_combo_pnl += combo_pnl
|
|
|
+ total_sell_call_pnl += sell_call_pnl
|
|
|
+ total_buy_call_pnl += buy_call_pnl
|
|
|
+
|
|
|
+ # 牛差策略:记录深度实值买购和卖购期权
|
|
|
+ buy_call_detail = {
|
|
|
+ '持仓标的代码': pos['buy_call']['code'],
|
|
|
+ '期权类别': '买购(牛差)',
|
|
|
+ '合约数量': pos['contract_size'],
|
|
|
+ '行权价格': pos['buy_call']['exercise_price'],
|
|
|
+ '成本价格': pos['buy_call']['price'],
|
|
|
+ '当前价格': buy_call_current_price,
|
|
|
+ '盈亏金额': buy_call_pnl,
|
|
|
+ '仓位类型': pos['position_type'],
|
|
|
+ '策略类型': '牛差组合',
|
|
|
+ '开仓日期': pos['open_date']
|
|
|
+ }
|
|
|
+
|
|
|
+ sell_call_detail = {
|
|
|
+ '持仓标的代码': pos['sell_call']['code'],
|
|
|
+ '期权类别': '卖购(牛差)',
|
|
|
+ '合约数量': pos['contract_size'],
|
|
|
+ '行权价格': pos['sell_call']['exercise_price'],
|
|
|
+ '成本价格': pos['sell_call']['price'],
|
|
|
+ '当前价格': sell_call_current_price,
|
|
|
+ '盈亏金额': sell_call_pnl,
|
|
|
+ '仓位类型': pos['position_type'],
|
|
|
+ '策略类型': '牛差组合',
|
|
|
+ '开仓日期': pos['open_date']
|
|
|
+ }
|
|
|
+
|
|
|
+ position_details.extend([buy_call_detail, sell_call_detail])
|
|
|
+
|
|
|
+ # 记录每日持仓汇总
|
|
|
+ position_record = {
|
|
|
+ '交易日期': trade_date,
|
|
|
+ 'ETF价格': etf_price,
|
|
|
+ '总仓位数': len(open_positions),
|
|
|
+ '主仓位数': len(main_positions),
|
|
|
+ '加仓仓位数': len(add_positions),
|
|
|
+ '总合约数': total_contracts,
|
|
|
+ '总组合盈亏': total_combo_pnl,
|
|
|
+ '总卖购盈亏': total_sell_call_pnl,
|
|
|
+ '总买购盈亏': total_buy_call_pnl,
|
|
|
+ '持仓详情': position_details
|
|
|
}
|
|
|
+
|
|
|
+ self.save_daily_position_to_csv(position_record)
|
|
|
+
|
|
|
+ # 注释掉单独的账户管理器回调,改为在多标的管理器中统一处理
|
|
|
+ # 通知账户管理器更新每日汇总
|
|
|
+ # if self.account_manager_callback:
|
|
|
+ # try:
|
|
|
+ # self.account_manager_callback(trade_date)
|
|
|
+ # except Exception as e:
|
|
|
+ # print(f"{trade_date.strftime('%Y-%m-%d')} 更新账户汇总回调出错: {e}")
|
|
|
+
|
|
|
+ def run_strategy(self):
|
|
|
+ """运行策略主逻辑"""
|
|
|
+ print("开始运行深度实值买购和卖购组合的牛差策略...")
|
|
|
+
|
|
|
+ for i, trade_date in enumerate(self.trade_days.index):
|
|
|
+ # 获取ETF价格
|
|
|
+ try:
|
|
|
+ price_data = get_price(self.underlying_symbol, trade_date, trade_date, fields=['close'])['close']
|
|
|
+ # print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格: {price_data.iloc[0]:.4f}")
|
|
|
+ if price_data.empty:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格失败,因为price_data为空")
|
|
|
+ continue
|
|
|
+ etf_price = price_data.iloc[0]
|
|
|
+ except:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格失败,因为获取{self.underlying_symbol}价格失败")
|
|
|
+ continue
|
|
|
|
|
|
- def plot_detailed_performance(self, dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns):
|
|
|
- """绘制详细的策略表现图,分别显示买购和卖购收益"""
|
|
|
- # 设置中文字体
|
|
|
- plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
|
|
|
- plt.rcParams['axes.unicode_minus'] = False
|
|
|
-
|
|
|
- plt.figure(figsize=(15, 12))
|
|
|
-
|
|
|
- # 第一个子图:分项收益对比
|
|
|
- plt.subplot(3, 1, 1)
|
|
|
- plt.plot(dates, buy_call_returns, label='买购期权收益', linewidth=2, color='blue')
|
|
|
- plt.plot(dates, sell_call_returns, label='卖购期权收益', linewidth=2, color='red')
|
|
|
- plt.plot(dates, combined_returns, label='组合总收益', linewidth=2, color='green')
|
|
|
- plt.plot(dates, etf_returns, label=f'{self.get_etf_name()}基准', linewidth=2, color='orange')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.title('策略分项收益对比')
|
|
|
- plt.ylabel('收益(元)')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第二个子图:累计收益率
|
|
|
- plt.subplot(3, 1, 2)
|
|
|
- if len(combined_returns) > 0 and len(etf_returns) > 0:
|
|
|
- combined_cumret = [(x / 100000) * 100 for x in combined_returns] # 转换为百分比
|
|
|
- etf_cumret = [(x / 100000) * 100 for x in etf_returns]
|
|
|
-
|
|
|
- plt.plot(dates, combined_cumret, label='策略累计收益率', linewidth=2, color='green')
|
|
|
- plt.plot(dates, etf_cumret, label='ETF累计收益率', linewidth=2, color='orange')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.title('累计收益率对比')
|
|
|
- plt.ylabel('收益率(%)')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第三个子图:持仓数量变化
|
|
|
- plt.subplot(3, 1, 3)
|
|
|
- position_counts = []
|
|
|
- for date in dates:
|
|
|
- count = len([p for p in self.positions
|
|
|
- if p['open_date'] <= date and
|
|
|
- (p['status'] == 'open' or p.get('close_date', date) >= date)])
|
|
|
- position_counts.append(count)
|
|
|
-
|
|
|
- plt.plot(dates, position_counts, label='持仓数量', linewidth=2, color='purple')
|
|
|
- plt.title('持仓数量变化')
|
|
|
- plt.ylabel('持仓数量')
|
|
|
- plt.xlabel('日期')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- plt.tight_layout()
|
|
|
- plt.show()
|
|
|
-
|
|
|
- def plot_strategy_vs_etf_comparison(self, dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns):
|
|
|
- """绘制三个策略分别与ETF的对比图"""
|
|
|
- # 设置中文字体
|
|
|
- plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
|
|
|
- plt.rcParams['axes.unicode_minus'] = False
|
|
|
-
|
|
|
- plt.figure(figsize=(15, 12))
|
|
|
-
|
|
|
- # 第一个子图:牛差策略 vs ETF
|
|
|
- plt.subplot(3, 1, 1)
|
|
|
- plt.plot(dates, combined_returns, label='牛差策略收益', linewidth=2, color='green')
|
|
|
- plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.title('牛差策略 vs ETF基准收益对比')
|
|
|
- plt.ylabel('收益(元)')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第二个子图:买购策略 vs ETF
|
|
|
- plt.subplot(3, 1, 2)
|
|
|
- plt.plot(dates, buy_call_returns, label='买购期权收益', linewidth=2, color='blue')
|
|
|
- plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.title('买购期权 vs ETF基准收益对比')
|
|
|
- plt.ylabel('收益(元)')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第三个子图:卖购策略 vs ETF
|
|
|
- plt.subplot(3, 1, 3)
|
|
|
- plt.plot(dates, sell_call_returns, label='卖购期权收益', linewidth=2, color='red')
|
|
|
- plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.title('卖购期权 vs ETF基准收益对比')
|
|
|
- plt.ylabel('收益(元)')
|
|
|
- plt.xlabel('日期')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- plt.tight_layout()
|
|
|
- plt.show()
|
|
|
-
|
|
|
- def plot_performance(self, portfolio_value, etf_benchmark):
|
|
|
- """绘制策略表现图"""
|
|
|
- plt.figure(figsize=(12, 8))
|
|
|
-
|
|
|
- dates = self.trade_days[:len(portfolio_value)]
|
|
|
-
|
|
|
- plt.subplot(2, 1, 1)
|
|
|
- plt.plot(dates, portfolio_value, label='牛差策略', linewidth=2)
|
|
|
- plt.plot(dates, etf_benchmark, label=f'{self.get_etf_name()}基准', linewidth=2)
|
|
|
- plt.title('策略收益对比')
|
|
|
- plt.ylabel('收益(元)')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- plt.subplot(2, 1, 2)
|
|
|
- # 绘制持仓数量变化
|
|
|
- position_counts = []
|
|
|
- for date in dates:
|
|
|
- count = len([p for p in self.positions
|
|
|
- if p['open_date'] <= date and
|
|
|
- (p['status'] == 'open' or p.get('close_date', date) >= date)])
|
|
|
- position_counts.append(count)
|
|
|
-
|
|
|
- plt.plot(dates, position_counts, label='持仓数量', linewidth=2, color='orange')
|
|
|
- plt.title('持仓数量变化')
|
|
|
- plt.ylabel('持仓数量')
|
|
|
- plt.xlabel('日期')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
+ # 记录每日持仓状况
|
|
|
+ self.record_daily_positions(trade_date, etf_price)
|
|
|
+
|
|
|
+ # 标记是否有交易发生
|
|
|
+ has_trading = False
|
|
|
+
|
|
|
+ # 检查是否需要平仓
|
|
|
+ if len(self.positions) > 0:
|
|
|
+ for position in self.positions:
|
|
|
+ should_close, reason = self.should_close_position(position, trade_date, etf_price)
|
|
|
+ # print(f"{trade_date.strftime('%Y-%m-%d')} 检查是否需要平仓: {should_close}, 平仓原因: {reason}")
|
|
|
+ if should_close:
|
|
|
+ self.close_position(position, trade_date, etf_price, reason)
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 平仓: {reason}, ETF价格: {etf_price:.4f}")
|
|
|
+ has_trading = True
|
|
|
+
|
|
|
+ # 检查是否需要开新仓(首次开仓或平仓后重新开仓)
|
|
|
+ open_positions = [p for p in self.positions if p['status'] == 'open']
|
|
|
+ if not open_positions:
|
|
|
+ new_position = self.open_position(trade_date, etf_price, 'main') # 使用新的统一开仓方法
|
|
|
+ if new_position:
|
|
|
+ max_profit = new_position['profit_info']['total_max_profit']
|
|
|
+ contract_size = new_position['contract_size']
|
|
|
+
|
|
|
+ # 牛差策略输出
|
|
|
+ strategy_desc = f"牛差组合: 深度实值买购{new_position['buy_call']['exercise_price']:.2f}@{new_position['buy_call']['price']:.4f}, 卖购{new_position['sell_call']['exercise_price']:.2f}@{new_position['sell_call']['price']:.4f}"
|
|
|
+ profit_desc = f"最大牛差收益: {max_profit:.2f}元"
|
|
|
+
|
|
|
+ # 获取资金信息(如果有的话)
|
|
|
+ if hasattr(self, 'config') and 'allocated_capital' in self.config:
|
|
|
+ allocated_capital = self.config.get('allocated_capital', 0)
|
|
|
+ estimated_margin = contract_size * 1000 # 每张约1000元保证金(粗略估算)
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 开仓主仓位: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}, 可用资金: {allocated_capital:.0f}元, 预估保证金: {estimated_margin:.0f}元")
|
|
|
+ else:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 开仓主仓位: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
|
|
|
+ has_trading = True
|
|
|
+
|
|
|
+ # 检查是否需要加仓
|
|
|
+ elif self.should_add_position(trade_date, etf_price):
|
|
|
+ add_position = self.open_position(trade_date, etf_price, 'add', silent=False) # 使用新的统一开仓方法
|
|
|
+ if add_position:
|
|
|
+ max_profit = add_position['profit_info']['total_max_profit']
|
|
|
+ contract_size = add_position['contract_size']
|
|
|
+
|
|
|
+ # 牛差策略输出
|
|
|
+ strategy_desc = f"牛差组合: 深度实值买购{add_position['buy_call']['exercise_price']:.2f}@{add_position['buy_call']['price']:.4f}, 卖购{add_position['sell_call']['exercise_price']:.2f}@{add_position['sell_call']['price']:.4f}"
|
|
|
+ profit_desc = f"最大牛差收益: {max_profit:.2f}元"
|
|
|
+
|
|
|
+ # 获取资金信息(如果有的话)
|
|
|
+ if hasattr(self, 'config') and 'allocated_capital' in self.config:
|
|
|
+ allocated_capital = self.config.get('allocated_capital', 0)
|
|
|
+ estimated_margin = contract_size * 1000 # 每张约1000元保证金(粗略估算)
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 加仓: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}, 可用资金: {allocated_capital:.0f}元, 预估保证金: {estimated_margin:.0f}元")
|
|
|
+ else:
|
|
|
+ print(f"{trade_date.strftime('%Y-%m-%d')} 加仓: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
|
|
|
+ has_trading = True
|
|
|
+
|
|
|
+ # 更新前一日ETF价格,用于下一天计算涨幅
|
|
|
+ self.previous_etf_price = etf_price
|
|
|
+
|
|
|
+ print("策略运行完成!")
|
|
|
+
|
|
|
+ def get_performance_summary(self):
|
|
|
+ """获取策略表现总结"""
|
|
|
+ if not self.trade_records:
|
|
|
+ return "没有交易记录"
|
|
|
+
|
|
|
+ closed_positions = [p for p in self.positions if p['status'] == 'closed']
|
|
|
+ if not closed_positions:
|
|
|
+ return "没有已平仓的交易"
|
|
|
+
|
|
|
+ total_pnl = sum(p['pnl'] for p in closed_positions)
|
|
|
+ winning_trades = len([p for p in closed_positions if p['pnl'] > 0])
|
|
|
+ total_trades = len(closed_positions)
|
|
|
+ win_rate = winning_trades / total_trades if total_trades > 0 else 0
|
|
|
+
|
|
|
+ summary = f"""
|
|
|
+策略表现总结:
|
|
|
+=============
|
|
|
+总交易次数: {total_trades}
|
|
|
+获利交易: {winning_trades}
|
|
|
+胜率: {win_rate:.2%}
|
|
|
+总盈亏: {total_pnl:.2f}元
|
|
|
+平均每笔盈亏: {total_pnl/total_trades:.2f}元
|
|
|
+ """
|
|
|
+ return summary
|
|
|
+
|
|
|
+ def plot_results(self):
|
|
|
+ """绘制策略结果"""
|
|
|
+ if not self.daily_positions:
|
|
|
+ print("没有持仓数据可以绘制")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 使用每日持仓记录绘制盈亏曲线
|
|
|
+ position_df = pd.DataFrame(self.daily_positions)
|
|
|
+
|
|
|
+ # 过滤有持仓的记录(排除空仓记录,但保留盈亏为0的记录用于显示完整曲线)
|
|
|
+ position_data = position_df.copy()
|
|
|
+
|
|
|
+ if position_data.empty:
|
|
|
+ print("没有持仓数据可以绘制")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 确保交易日期是datetime格式
|
|
|
+ position_data['交易日期'] = pd.to_datetime(position_data['交易日期'])
|
|
|
+ position_data = position_data.sort_values('交易日期')
|
|
|
+
|
|
|
+ # 确保盈亏字段为数值类型
|
|
|
+ position_data['总组合盈亏'] = pd.to_numeric(position_data['总组合盈亏'], errors='coerce').fillna(0)
|
|
|
+ position_data['总卖购盈亏'] = pd.to_numeric(position_data['总卖购盈亏'], errors='coerce').fillna(0)
|
|
|
+
|
|
|
+ # 绘图
|
|
|
+ fig, ax = plt.subplots(1, 1, figsize=(12, 8))
|
|
|
+
|
|
|
+ # 绘制两条盈亏曲线
|
|
|
+ ax.plot(position_data['交易日期'], position_data['总组合盈亏'],
|
|
|
+ label='每日组合浮动盈亏(买购+卖购)', color='blue', linewidth=2)
|
|
|
+ ax.plot(position_data['交易日期'], position_data['总卖购盈亏'],
|
|
|
+ label='每日卖购浮动盈亏', color='red', linewidth=2, linestyle='--')
|
|
|
+
|
|
|
+ # 添加零线
|
|
|
+ ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
|
|
|
+
|
|
|
+ ax.set_title(f'{self.get_underlying_code()}策略每日浮动盈亏对比', fontsize=14)
|
|
|
+ ax.set_ylabel('浮动盈亏 (元)', fontsize=12)
|
|
|
+ ax.set_xlabel('日期', fontsize=12)
|
|
|
+ ax.legend(fontsize=10)
|
|
|
+ ax.grid(True, alpha=0.3)
|
|
|
+
|
|
|
+ # 格式化日期显示
|
|
|
+ import matplotlib.dates as mdates
|
|
|
+ ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
|
|
+ ax.xaxis.set_major_locator(mdates.MonthLocator())
|
|
|
+ plt.xticks(rotation=45)
|
|
|
+
|
|
|
+ # 添加数据标注
|
|
|
+ if len(position_data) > 0:
|
|
|
+ final_combo_pnl = position_data['总组合盈亏'].iloc[-1]
|
|
|
+ final_sell_pnl = position_data['总卖购盈亏'].iloc[-1]
|
|
|
+ max_combo_pnl = position_data['总组合盈亏'].max()
|
|
|
+ min_combo_pnl = position_data['总组合盈亏'].min()
|
|
|
+
|
|
|
+ ax.text(0.02, 0.98,
|
|
|
+ f'当前组合浮盈: {final_combo_pnl:.2f}元\n'
|
|
|
+ f'当前卖购浮盈: {final_sell_pnl:.2f}元\n'
|
|
|
+ f'最大组合浮盈: {max_combo_pnl:.2f}元\n'
|
|
|
+ f'最大组合浮亏: {min_combo_pnl:.2f}元',
|
|
|
+ transform=ax.transAxes, fontsize=10, verticalalignment='top',
|
|
|
+ bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
|
|
|
|
|
|
plt.tight_layout()
|
|
|
plt.show()
|
|
|
|
|
|
- def print_position_details(self):
|
|
|
- """打印持仓详情"""
|
|
|
- print("\n" + "="*80)
|
|
|
- print("持仓详情")
|
|
|
- print("="*80)
|
|
|
-
|
|
|
- for i, pos in enumerate(self.positions):
|
|
|
- print(f"\n持仓 {i+1}:")
|
|
|
- print(f" 开仓日期: {pos['open_date']}")
|
|
|
- print(f" ETF价格: {pos['etf_price']:.4f}")
|
|
|
- print(f" 买购: {pos['buy_call_strike']:.2f} @ {pos['buy_call_price']:.4f}")
|
|
|
- print(f" 卖购: {pos['sell_call_strike']:.2f} @ {pos['sell_call_price']:.4f}")
|
|
|
- print(f" 数量: {pos['quantity']}张")
|
|
|
- print(f" 单张最大盈利: {pos['max_profit_per_contract']:.4f}")
|
|
|
- print(f" 总最大盈利: {pos['max_profit_total']:.2f}元")
|
|
|
- print(f" 状态: {pos['status']}")
|
|
|
- print(f" 是否加仓: {'是' if pos['is_additional'] else '否'}")
|
|
|
-
|
|
|
- if pos['status'] == 'closed':
|
|
|
- print(f" 平仓日期: {pos['close_date']}")
|
|
|
- print(f" 平仓原因: {pos['close_reason']}")
|
|
|
- print(f" 实现损益: {pos['realized_pnl']:.2f}元")
|
|
|
-
|
|
|
-
|
|
|
-class OptionsAnalyzer:
|
|
|
- """期权分析工具类"""
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def analyze_options(*options):
|
|
|
+# 策略配置类
|
|
|
+class StrategyConfig:
|
|
|
+ """策略配置管理"""
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ # ETF标的配置字典
|
|
|
+ self.etf_symbols = {
|
|
|
+ '50ETF': '510050.XSHG' # 上证50ETF
|
|
|
+ # '300ETF': '510300.XSHG', # 沪深300ETF
|
|
|
+ # '创业板ETF': '159915.XSHE' # 创业板ETF
|
|
|
+ }
|
|
|
+
|
|
|
+ # 时间范围配置
|
|
|
+ self.time_config = {
|
|
|
+ 'start_date': '2024-06-01',
|
|
|
+ 'end_date': '2024-08-31'
|
|
|
+ }
|
|
|
+
|
|
|
+ # 资金配置
|
|
|
+ self.capital_config = {
|
|
|
+ 'total_capital': 1000000, # 总资金额度(100万)
|
|
|
+ 'capital_allocation': { # 不同标的资金分配比例
|
|
|
+ '50ETF': 0.3, # 50ETF 30%
|
|
|
+ '300ETF': 0.5, # 300ETF 50%
|
|
|
+ '创业板ETF': 0.2 # 创业板ETF 20%
|
|
|
+ },
|
|
|
+ 'capital_usage_limit': 0.8, # 资金使用上限80%
|
|
|
+ 'bull_spread_margin_discount': 30, # 牛差组合策略保证金优惠(每单位组合加收30元)
|
|
|
+ 'contract_unit': 10000, # 合约单位(1张期权代表10000份标的)
|
|
|
+ 'margin_params': { # 保证金计算参数
|
|
|
+ 'volatility_factor': 0.12, # 波动率因子12%
|
|
|
+ 'min_margin_factor': 0.07 # 最小保证金因子7%
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ def get_allocated_capital(self, etf_code):
|
|
|
+ """获取指定ETF的分配资金"""
|
|
|
+ total_usable = self.capital_config['total_capital'] * self.capital_config['capital_usage_limit']
|
|
|
+
|
|
|
+ # 获取实际存在的ETF列表
|
|
|
+ available_etfs = list(self.etf_symbols.keys())
|
|
|
+
|
|
|
+ # 计算实际存在ETF的原始比例总和
|
|
|
+ original_allocation = self.capital_config['capital_allocation']
|
|
|
+ available_ratio_sum = sum(original_allocation.get(etf, 0) for etf in available_etfs)
|
|
|
+
|
|
|
+ if available_ratio_sum == 0:
|
|
|
+ print(f"警告: 无可用ETF或比例配置错误,{etf_code}分配资金为0")
|
|
|
+ return 0
|
|
|
+
|
|
|
+ # 获取当前ETF的原始比例
|
|
|
+ original_ratio = original_allocation.get(etf_code, 0)
|
|
|
+
|
|
|
+ if original_ratio == 0:
|
|
|
+ print(f"警告: {etf_code}未在capital_allocation中配置,分配资金为0")
|
|
|
+ return 0
|
|
|
+
|
|
|
+ # 重新调整比例:当前ETF比例 / 实际存在ETF的比例总和
|
|
|
+ adjusted_ratio = original_ratio / available_ratio_sum
|
|
|
+
|
|
|
+ allocated_capital = total_usable * adjusted_ratio
|
|
|
+
|
|
|
+ print(f"资金分配调整 - {etf_code}: 原始比例{original_ratio:.1%}, 调整后比例{adjusted_ratio:.1%}, 分配资金{allocated_capital:,.0f}元")
|
|
|
+
|
|
|
+ return allocated_capital
|
|
|
+
|
|
|
+ def calculate_option_margin(self, option_type, settlement_price, underlying_price, strike_price):
|
|
|
"""
|
|
|
- 统一的期权分析方法
|
|
|
- 参数: *options: 一个或多个期权,每个期权格式为 (direction, option_type, premium, strike_price, quantity)
|
|
|
+ 计算单张期权的保证金
|
|
|
+ :param option_type: 'call' 或 'put'
|
|
|
+ :param settlement_price: 合约前结算价
|
|
|
+ :param underlying_price: 标的证券前收盘价
|
|
|
+ :param strike_price: 行权价
|
|
|
+ :return: 单张期权保证金
|
|
|
"""
|
|
|
- if not options:
|
|
|
- raise ValueError("请至少提供一个期权")
|
|
|
-
|
|
|
- # 解析期权数据
|
|
|
- option_list = []
|
|
|
- all_strikes = []
|
|
|
-
|
|
|
- for i, opt in enumerate(options):
|
|
|
- if len(opt) != 5:
|
|
|
- raise ValueError(f"期权{i+1}格式错误,应为(direction, option_type, premium, strike_price, quantity)")
|
|
|
-
|
|
|
- direction, option_type, premium, strike_price, quantity = opt
|
|
|
- option_list.append({
|
|
|
- 'direction': direction,
|
|
|
- 'option_type': option_type,
|
|
|
- 'premium': premium,
|
|
|
- 'strike_price': strike_price,
|
|
|
- 'quantity': quantity
|
|
|
- })
|
|
|
- all_strikes.append(strike_price)
|
|
|
-
|
|
|
- # 确定价格分析区间
|
|
|
- min_strike = min(all_strikes)
|
|
|
- max_strike = max(all_strikes)
|
|
|
- price_min = min_strike * 0.7
|
|
|
- price_max = max_strike * 1.3
|
|
|
-
|
|
|
- # 生成价格序列
|
|
|
- gap = (price_max - price_min) / 1000
|
|
|
- prices = np.arange(price_min, price_max + gap, gap)
|
|
|
-
|
|
|
- # 计算每个期权的收益
|
|
|
- results = {'price': prices}
|
|
|
-
|
|
|
- for i, opt in enumerate(option_list):
|
|
|
- profits = []
|
|
|
- for price in prices:
|
|
|
- profit = OptionsAnalyzer._calculate_profit(opt, price)
|
|
|
- profits.append(profit)
|
|
|
- results[f'opt{i+1}'] = profits
|
|
|
-
|
|
|
- # 计算组合收益
|
|
|
- if len(option_list) > 1:
|
|
|
- combined_profits = []
|
|
|
- for j in range(len(prices)):
|
|
|
- total = sum(results[f'opt{i+1}'][j] for i in range(len(option_list)))
|
|
|
- combined_profits.append(total)
|
|
|
- results['combined'] = combined_profits
|
|
|
-
|
|
|
- # 绘制图表
|
|
|
- OptionsAnalyzer._plot_results(results, option_list, prices)
|
|
|
-
|
|
|
- # 打印分析报告
|
|
|
- OptionsAnalyzer._print_report(results, option_list, prices)
|
|
|
-
|
|
|
- return pd.DataFrame(results)
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _calculate_profit(option, price):
|
|
|
- """计算单个期权在特定价格下的收益"""
|
|
|
- direction = option['direction']
|
|
|
- option_type = option['option_type']
|
|
|
- premium = option['premium']
|
|
|
- strike_price = option['strike_price']
|
|
|
- quantity = option['quantity']
|
|
|
-
|
|
|
- if direction == 'buy' and option_type == 'call':
|
|
|
- # 买入认购
|
|
|
- if price > strike_price:
|
|
|
- return (price - strike_price - premium) * quantity
|
|
|
- else:
|
|
|
- return -premium * quantity
|
|
|
-
|
|
|
- elif direction == 'sell' and option_type == 'call':
|
|
|
- # 卖出认购
|
|
|
- if price > strike_price:
|
|
|
- return -(price - strike_price - premium) * quantity
|
|
|
- else:
|
|
|
- return premium * quantity
|
|
|
-
|
|
|
- elif direction == 'buy' and option_type == 'put':
|
|
|
- # 买入认沽
|
|
|
- if price < strike_price:
|
|
|
- return (strike_price - price - premium) * quantity
|
|
|
- else:
|
|
|
- return -premium * quantity
|
|
|
-
|
|
|
- elif direction == 'sell' and option_type == 'put':
|
|
|
- # 卖出认沽
|
|
|
- if price < strike_price:
|
|
|
- return -(strike_price - price - premium) * quantity
|
|
|
- else:
|
|
|
- return premium * quantity
|
|
|
-
|
|
|
- return 0
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _plot_results(results, option_list, prices):
|
|
|
- """绘制分析图表"""
|
|
|
- # 设置中文字体
|
|
|
- plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
|
|
|
- plt.rcParams['axes.unicode_minus'] = False
|
|
|
-
|
|
|
- plt.figure(figsize=(14, 10))
|
|
|
-
|
|
|
- colors = ['blue', 'green', 'orange', 'purple', 'brown']
|
|
|
-
|
|
|
- # 绘制单个期权曲线
|
|
|
- for i in range(len(option_list)):
|
|
|
- opt = option_list[i]
|
|
|
- opt_name = f'opt{i+1}'
|
|
|
- strategy_name = f"{opt['direction'].upper()} {opt['option_type'].upper()}"
|
|
|
- color = colors[i % len(colors)]
|
|
|
-
|
|
|
- plt.plot(prices, results[opt_name], '--', color=color, linewidth=2, alpha=0.7,
|
|
|
- label=f'{opt_name}: {strategy_name} (行权价:{opt["strike_price"]})')
|
|
|
-
|
|
|
- # 绘制组合曲线
|
|
|
- if 'combined' in results:
|
|
|
- plt.plot(prices, results['combined'], 'r-', linewidth=3, label='组合收益')
|
|
|
-
|
|
|
- # 添加零线和行权价线
|
|
|
- plt.axhline(0, color='gray', linestyle='-', alpha=0.5)
|
|
|
- for opt in option_list:
|
|
|
- plt.axvline(opt['strike_price'], color='gray', linestyle='--', alpha=0.3)
|
|
|
-
|
|
|
- # 找到并标注关键点
|
|
|
- if 'combined' in results:
|
|
|
- OptionsAnalyzer._mark_key_points(results['combined'], prices, '组合')
|
|
|
- elif len(option_list) == 1:
|
|
|
- OptionsAnalyzer._mark_key_points(results['opt1'], prices, '期权')
|
|
|
-
|
|
|
- plt.xlabel('标的资产价格', fontsize=12)
|
|
|
- plt.ylabel('收益/损失', fontsize=12)
|
|
|
-
|
|
|
- if len(option_list) == 1:
|
|
|
- opt = option_list[0]
|
|
|
- title = f'{opt["direction"].upper()} {opt["option_type"].upper()} 期权分析'
|
|
|
+ contract_unit = self.capital_config['contract_unit']
|
|
|
+ volatility_factor = self.capital_config['margin_params']['volatility_factor']
|
|
|
+ min_margin_factor = self.capital_config['margin_params']['min_margin_factor']
|
|
|
+
|
|
|
+ if option_type == 'call':
|
|
|
+ # 认购期权虚值 = max(行权价 - 标的价格, 0)
|
|
|
+ out_of_money = max(strike_price - underlying_price, 0)
|
|
|
+ margin = (settlement_price + max(
|
|
|
+ volatility_factor * underlying_price - out_of_money,
|
|
|
+ min_margin_factor * underlying_price
|
|
|
+ )) * contract_unit
|
|
|
+ elif option_type == 'put':
|
|
|
+ # 认沽期权虚值 = max(标的价格 - 行权价, 0)
|
|
|
+ out_of_money = max(underlying_price - strike_price, 0)
|
|
|
+ margin = min(
|
|
|
+ settlement_price + max(
|
|
|
+ volatility_factor * underlying_price - out_of_money,
|
|
|
+ min_margin_factor * strike_price
|
|
|
+ ),
|
|
|
+ strike_price
|
|
|
+ ) * contract_unit
|
|
|
else:
|
|
|
- title = f'期权组合分析 ({len(option_list)}个期权)'
|
|
|
-
|
|
|
- plt.title(title, fontsize=14, weight='bold')
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
- plt.legend()
|
|
|
- plt.tight_layout()
|
|
|
- plt.show()
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _mark_key_points(profits, prices, label_prefix):
|
|
|
- """标注关键点:盈亏平衡点、最大收益/损失边界点"""
|
|
|
- # 标注盈亏平衡点
|
|
|
- for i in range(len(profits) - 1):
|
|
|
- if profits[i] * profits[i + 1] <= 0: # 符号改变
|
|
|
- # 线性插值找到精确平衡点
|
|
|
- p1, profit1 = prices[i], profits[i]
|
|
|
- p2, profit2 = prices[i + 1], profits[i + 1]
|
|
|
- if profit2 != profit1:
|
|
|
- breakeven_price = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
|
|
|
- plt.plot(breakeven_price, 0, 'ro', markersize=10)
|
|
|
- plt.annotate(f'平衡点: {breakeven_price:.3f}',
|
|
|
- xy=(breakeven_price, 0),
|
|
|
- xytext=(breakeven_price + (prices.max() - prices.min()) * 0.05, max(profits) * 0.1),
|
|
|
- arrowprops=dict(arrowstyle='->', color='red'),
|
|
|
- fontsize=11, color='red', weight='bold')
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _print_report(results, option_list, prices):
|
|
|
- """打印分析报告"""
|
|
|
- print("=" * 60)
|
|
|
- print("期权分析报告")
|
|
|
- print("=" * 60)
|
|
|
-
|
|
|
- # 期权基本信息
|
|
|
- for i, opt in enumerate(option_list):
|
|
|
- print(f"期权{i+1}: {opt['direction'].upper()} {opt['option_type'].upper()}")
|
|
|
- print(f" 行权价: {opt['strike_price']}")
|
|
|
- print(f" 权利金: {opt['premium']}")
|
|
|
- print(f" 数量: {opt['quantity']}手")
|
|
|
-
|
|
|
- # 分析关键指标
|
|
|
- if 'combined' in results:
|
|
|
- profits = results['combined']
|
|
|
- print(f"\n【组合分析】")
|
|
|
+ raise ValueError("option_type must be 'call' or 'put'")
|
|
|
+
|
|
|
+ return margin
|
|
|
+
|
|
|
+ def calculate_bull_spread_margin(self, buy_call_info, sell_call_info, underlying_price):
|
|
|
+ """
|
|
|
+ 计算牛差组合的保证金
|
|
|
+ :param buy_call_info: 买入认购期权信息
|
|
|
+ :param sell_call_info: 卖出认购期权信息
|
|
|
+ :param underlying_price: 标的价格
|
|
|
+ :return: 牛差组合保证金
|
|
|
+ """
|
|
|
+ # 计算行权价差
|
|
|
+ strike_diff = sell_call_info['exercise_price'] - buy_call_info['exercise_price']
|
|
|
+ contract_unit = self.capital_config['contract_unit']
|
|
|
+ margin_discount = self.capital_config['bull_spread_margin_discount']
|
|
|
+
|
|
|
+ # 判断组合类型
|
|
|
+ if strike_diff > 0:
|
|
|
+ # 传统牛差组合(买购行权价 < 卖购行权价)
|
|
|
+ # 保证金 = 行权价差 * 合约单位 + 保证金优惠
|
|
|
+ bull_spread_margin = strike_diff * contract_unit + margin_discount
|
|
|
else:
|
|
|
- profits = results['opt1']
|
|
|
- print(f"\n【单期权分析】")
|
|
|
-
|
|
|
- max_profit = max(profits)
|
|
|
- min_profit = min(profits)
|
|
|
- max_idx = profits.tolist().index(max_profit) if hasattr(profits, 'tolist') else profits.index(max_profit)
|
|
|
- min_idx = profits.tolist().index(min_profit) if hasattr(profits, 'tolist') else profits.index(min_profit)
|
|
|
-
|
|
|
- print(f"最大收益: {max_profit:.4f} (标的价格: {prices[max_idx]:.4f})")
|
|
|
- print(f"最大损失: {min_profit:.4f} (标的价格: {prices[min_idx]:.4f})")
|
|
|
- print(f"一单最大收益: {max_profit * 10000:.2f}元")
|
|
|
- print(f"一单最大亏损: {abs(min_profit) * 10000:.2f}元")
|
|
|
-
|
|
|
- # 找盈亏平衡点
|
|
|
- breakeven_points = []
|
|
|
- for i in range(len(profits) - 1):
|
|
|
- if profits[i] * profits[i + 1] <= 0:
|
|
|
- p1, profit1 = prices[i], profits[i]
|
|
|
- p2, profit2 = prices[i + 1], profits[i + 1]
|
|
|
- if profit2 != profit1:
|
|
|
- bp = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
|
|
|
- breakeven_points.append(bp)
|
|
|
-
|
|
|
- if breakeven_points:
|
|
|
- print(f"盈亏平衡点: {[f'{bp:.4f}' for bp in breakeven_points]}")
|
|
|
+ # 其他情况的保证金计算
|
|
|
+ # 使用净权利金作为保证金基础
|
|
|
+ net_premium = buy_call_info['price'] - sell_call_info['price']
|
|
|
+ if net_premium > 0:
|
|
|
+ # 需要支付净权利金
|
|
|
+ bull_spread_margin = abs(net_premium) * contract_unit + margin_discount
|
|
|
+ else:
|
|
|
+ # 收取净权利金,使用估算保证金
|
|
|
+ estimated_margin = max(sell_call_info['price'] * contract_unit * 0.1, 1000) # 最少1000元保证金
|
|
|
+ bull_spread_margin = estimated_margin
|
|
|
+
|
|
|
+ return bull_spread_margin
|
|
|
+
|
|
|
+ def calculate_contract_size(self, etf_code, etf_price, buy_call_info=None, sell_call_info=None):
|
|
|
+ """
|
|
|
+ 计算可开仓的合约数量
|
|
|
+ :param etf_code: ETF代码
|
|
|
+ :param etf_price: ETF价格
|
|
|
+ :param buy_call_info: 买入认购期权信息(可选,用于牛差组合计算)
|
|
|
+ :param sell_call_info: 卖出认购期权信息(可选,用于牛差组合计算)
|
|
|
+ """
|
|
|
+ allocated_capital = self.get_allocated_capital(etf_code)
|
|
|
+
|
|
|
+ if buy_call_info and sell_call_info:
|
|
|
+ # 牛差组合保证金计算
|
|
|
+ margin_per_contract = self.calculate_bull_spread_margin(buy_call_info, sell_call_info, etf_price)
|
|
|
else:
|
|
|
- print("无盈亏平衡点")
|
|
|
-
|
|
|
- print("=" * 60)
|
|
|
-
|
|
|
-
|
|
|
-def test_opening_logic():
|
|
|
- """专门测试开仓逻辑"""
|
|
|
- print("="*60)
|
|
|
- print("测试开仓逻辑")
|
|
|
- print("="*60)
|
|
|
-
|
|
|
- # 创建策略实例
|
|
|
- strategy = DeepITMBullSpreadStrategy(
|
|
|
- underlying='510300.XSHG', # 300ETF
|
|
|
- start_date='2024-01-01',
|
|
|
- end_date='2025-06-30'
|
|
|
- )
|
|
|
-
|
|
|
- # 获取ETF价格数据
|
|
|
- try:
|
|
|
- etf_prices = get_price(strategy.underlying, strategy.start_date, strategy.end_date, fields=['close'])['close']
|
|
|
- print(f"成功获取ETF价格数据,共{len(etf_prices)}个交易日")
|
|
|
-
|
|
|
- # 测试前几个交易日的开仓逻辑
|
|
|
- test_dates = strategy.trade_days[:10] # 只测试前10个交易日
|
|
|
-
|
|
|
- for i, date in enumerate(test_dates):
|
|
|
- if date not in etf_prices.index:
|
|
|
- continue
|
|
|
-
|
|
|
- etf_price = etf_prices[date]
|
|
|
- print(f"\n第{i+1}个交易日测试: {date}, ETF价格: {etf_price:.4f}")
|
|
|
-
|
|
|
- # 尝试开仓
|
|
|
- position = strategy.open_bull_spread_position(date, etf_price)
|
|
|
+ # 单一期权保证金估算(使用卖出认购期权)
|
|
|
+ estimated_settlement_price = etf_price * 0.02 # 估算权利金为标的价格的2%
|
|
|
+ estimated_strike_price = etf_price * 1.05 # 估算行权价为标的价格的105%
|
|
|
+ margin_per_contract = self.calculate_option_margin('call', estimated_settlement_price, etf_price, estimated_strike_price)
|
|
|
+
|
|
|
+ # 边界条件检查
|
|
|
+ if allocated_capital <= 0:
|
|
|
+ print(f"警告: {etf_code} 分配资金无效({allocated_capital}),返回默认合约数量30张")
|
|
|
+ return 30
|
|
|
+
|
|
|
+ if margin_per_contract <= 0:
|
|
|
+ print(f"警告: {etf_code} 保证金计算结果无效({margin_per_contract}),返回默认合约数量30张")
|
|
|
+ return 30
|
|
|
+
|
|
|
+ max_contracts = int(allocated_capital / margin_per_contract)
|
|
|
+
|
|
|
+ # 确保结果为正数
|
|
|
+ if max_contracts <= 0:
|
|
|
+ print(f"警告: {etf_code} 合约数量计算结果无效({max_contracts}),返回默认合约数量30张")
|
|
|
+ return 30
|
|
|
+
|
|
|
+ return min(max_contracts, 100) # 限制最大100张
|
|
|
|
|
|
- if position:
|
|
|
- print(f"✓ 成功开仓!")
|
|
|
- print(f" 持仓总数: {len(strategy.positions)}")
|
|
|
- break
|
|
|
+# 多标的策略管理器
|
|
|
+class MultiUnderlyingBullSpreadManager:
|
|
|
+ """多标的牛差策略管理器"""
|
|
|
+
|
|
|
+ def __init__(self, config: StrategyConfig):
|
|
|
+ self.config = config
|
|
|
+ self.strategies = {}
|
|
|
+ self.daily_account_records = [] # 每日账户资金记录
|
|
|
+ self.account_csv_path = 'account_summary.csv'
|
|
|
+ self.cumulative_realized_pnl = 0 # 累积已实现盈亏
|
|
|
+ self.previous_date_summary = None # 前一天的账户汇总记录
|
|
|
+ self.initialize_strategies()
|
|
|
+
|
|
|
+ def initialize_strategies(self):
|
|
|
+ """初始化各个标的的策略"""
|
|
|
+ for etf_code, symbol in self.config.etf_symbols.items():
|
|
|
+ allocated_capital = self.config.get_allocated_capital(etf_code)
|
|
|
+ if allocated_capital > 0:
|
|
|
+ strategy = DeepITMBullSpreadStrategy(
|
|
|
+ underlying_symbol=symbol,
|
|
|
+ start_date=self.config.time_config['start_date'],
|
|
|
+ end_date=self.config.time_config['end_date']
|
|
|
+ )
|
|
|
+
|
|
|
+ # 动态调整策略参数
|
|
|
+ strategy.config['allocated_capital'] = allocated_capital
|
|
|
+ strategy.config['etf_code'] = etf_code
|
|
|
+
|
|
|
+ # 设置账户管理器回调
|
|
|
+ strategy.account_manager_callback = self.record_daily_account_summary
|
|
|
+
|
|
|
+ self.strategies[etf_code] = strategy
|
|
|
+
|
|
|
+ print(f"初始化{etf_code}策略,分配资金: {allocated_capital:,.0f}元")
|
|
|
+
|
|
|
+ def calculate_dynamic_contract_size(self, strategy, etf_price, buy_call_info=None, sell_call_info=None):
|
|
|
+ """动态计算合约数量"""
|
|
|
+ etf_code = strategy.config['etf_code']
|
|
|
+ return self.config.calculate_contract_size(etf_code, etf_price, buy_call_info, sell_call_info)
|
|
|
+
|
|
|
+ def calculate_used_capital(self, strategy):
|
|
|
+ """计算策略已使用的资金 - 支持两种策略类型"""
|
|
|
+ used_capital = 0
|
|
|
+ open_positions = [p for p in strategy.positions if p['status'] == 'open']
|
|
|
+
|
|
|
+ for position in open_positions:
|
|
|
+ # 获取策略类型,兼容旧版本数据
|
|
|
+ strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
|
|
|
+
|
|
|
+ # 两种策略都有买购期权,资金使用计算相同
|
|
|
+ # 资金使用 = (买购权利金 - 卖购权利金) * 合约数量 * 10000
|
|
|
+ net_premium = position['buy_call']['price'] - position['sell_call']['price']
|
|
|
+ position_capital = net_premium * position['contract_size'] * 10000
|
|
|
+
|
|
|
+ used_capital += position_capital
|
|
|
+
|
|
|
+ return used_capital
|
|
|
+
|
|
|
+ def record_daily_account_summary(self, trade_date):
|
|
|
+ """记录每日账户资金汇总"""
|
|
|
+ initial_capital = self.config.capital_config['total_capital']
|
|
|
+ total_used_capital = 0
|
|
|
+ total_floating_pnl = 0
|
|
|
+ total_current_market_value = 0 # 所有策略的当前市值总和
|
|
|
+
|
|
|
+ strategy_details = {}
|
|
|
+
|
|
|
+ # 计算当天的已实现盈亏(通过检查平仓交易)
|
|
|
+ today_realized_pnl = 0
|
|
|
+ for etf_code, strategy in self.strategies.items():
|
|
|
+ # 检查当天是否有平仓交易
|
|
|
+ for trade_record in strategy.trade_records:
|
|
|
+ if (trade_record.get('交易类型') == '平仓' and
|
|
|
+ trade_record.get('交易日期') and
|
|
|
+ trade_record.get('总盈亏') is not None):
|
|
|
+
|
|
|
+ trade_date_record = trade_record.get('交易日期')
|
|
|
+ if hasattr(trade_date_record, 'date'):
|
|
|
+ trade_date_record = trade_date_record.date()
|
|
|
+ elif isinstance(trade_date_record, str):
|
|
|
+ try:
|
|
|
+ trade_date_record = pd.to_datetime(trade_date_record).date()
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+
|
|
|
+ target_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+
|
|
|
+ if trade_date_record == target_date:
|
|
|
+ today_realized_pnl += float(trade_record.get('总盈亏', 0))
|
|
|
+
|
|
|
+ # 更新累积已实现盈亏
|
|
|
+ self.cumulative_realized_pnl += today_realized_pnl
|
|
|
+
|
|
|
+ # 汇总各策略的资金使用和浮动盈亏
|
|
|
+ total_bull_spread_positions = 0
|
|
|
+
|
|
|
+ for etf_code, strategy in self.strategies.items():
|
|
|
+ used_capital = self.calculate_used_capital(strategy)
|
|
|
+ open_positions = [p for p in strategy.positions if p['status'] == 'open']
|
|
|
+ open_positions_count = len(open_positions)
|
|
|
+
|
|
|
+ # 统计牛差策略仓位数量
|
|
|
+ bull_spread_count = len([p for p in open_positions if p.get('strategy_type', StrategyType.BULL_SPREAD) == StrategyType.BULL_SPREAD])
|
|
|
+
|
|
|
+ total_bull_spread_positions += bull_spread_count
|
|
|
+
|
|
|
+ # 计算组合当前市值和浮动盈亏
|
|
|
+ current_market_value = 0
|
|
|
+ floating_pnl = 0
|
|
|
+
|
|
|
+ if strategy.daily_positions:
|
|
|
+ # 查找指定日期的持仓记录
|
|
|
+ target_record = None
|
|
|
+ for position_record in strategy.daily_positions:
|
|
|
+ record_date = position_record.get('交易日期')
|
|
|
+ if hasattr(record_date, 'date'):
|
|
|
+ record_date = record_date.date()
|
|
|
+ elif isinstance(record_date, str):
|
|
|
+ try:
|
|
|
+ record_date = pd.to_datetime(record_date).date()
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+
|
|
|
+ target_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
|
|
|
+
|
|
|
+ if record_date == target_date:
|
|
|
+ target_record = position_record
|
|
|
+ break
|
|
|
+
|
|
|
+ # 如果找到了对应日期的记录,计算当前市值和浮动盈亏
|
|
|
+ if target_record:
|
|
|
+ floating_pnl = target_record.get('总组合盈亏', 0)
|
|
|
+ if isinstance(floating_pnl, str):
|
|
|
+ try:
|
|
|
+ floating_pnl = float(floating_pnl)
|
|
|
+ except:
|
|
|
+ floating_pnl = 0
|
|
|
+
|
|
|
+ # 计算组合当前市值 = 初始投入成本 + 浮动盈亏
|
|
|
+ current_market_value = used_capital + floating_pnl
|
|
|
+
|
|
|
+ total_used_capital += used_capital
|
|
|
+ total_floating_pnl += floating_pnl
|
|
|
+ total_current_market_value += current_market_value
|
|
|
+
|
|
|
+ strategy_details[etf_code] = {
|
|
|
+ '分配资金': strategy.config.get('allocated_capital', 0),
|
|
|
+ '初始投入': used_capital,
|
|
|
+ '当前市值': current_market_value,
|
|
|
+ '浮动盈亏': floating_pnl,
|
|
|
+ '持仓数量': open_positions_count,
|
|
|
+ '牛差策略数量': bull_spread_count
|
|
|
+ }
|
|
|
+
|
|
|
+ # 修正后的计算逻辑:
|
|
|
+ # 当前总资金 = 初始资金 + 累积已实现盈亏
|
|
|
+ current_total_capital = initial_capital + self.cumulative_realized_pnl
|
|
|
+ # 剩余现金 = 当前总资金 - 投入资金
|
|
|
+ remaining_cash = current_total_capital - total_used_capital
|
|
|
+ # 账户总价值 = 剩余现金 + 组合当前市值
|
|
|
+ total_account_value = remaining_cash + total_current_market_value
|
|
|
+
|
|
|
+ # 记录账户汇总
|
|
|
+ account_record = {
|
|
|
+ '交易日期': trade_date,
|
|
|
+ '总资金': current_total_capital, # 使用当前总资金而非初始资金
|
|
|
+ '初始投入总额': total_used_capital,
|
|
|
+ '剩余现金': remaining_cash,
|
|
|
+ '组合当前市值': total_current_market_value,
|
|
|
+ '总浮动盈亏': total_floating_pnl,
|
|
|
+ '账户总价值': total_account_value,
|
|
|
+ '累积已实现盈亏': self.cumulative_realized_pnl, # 新增字段
|
|
|
+ '当日已实现盈亏': today_realized_pnl, # 新增字段
|
|
|
+ '资金使用率': total_used_capital / current_total_capital if current_total_capital > 0 else 0,
|
|
|
+ '收益率': (total_account_value - initial_capital) / initial_capital if initial_capital > 0 else 0, # 修正收益率计算
|
|
|
+ '牛差策略总数': total_bull_spread_positions, # 新增字段
|
|
|
+
|
|
|
+ '策略详情': strategy_details
|
|
|
+ }
|
|
|
+
|
|
|
+ self.daily_account_records.append(account_record)
|
|
|
+ self.save_account_summary_to_csv(account_record)
|
|
|
+
|
|
|
+ # 更新前一天的记录
|
|
|
+ self.previous_date_summary = account_record
|
|
|
+
|
|
|
+ # 输出整体账户汇总(只在有活动时输出)
|
|
|
+ # if total_used_capital > 0 or total_floating_pnl != 0:
|
|
|
+ # print(f"{trade_date.strftime('%Y-%m-%d')} 【账户汇总】总资金{total_capital:.0f}元|已投入{total_used_capital:.0f}元|剩余现金{remaining_cash:.0f}元|组合市值{total_current_market_value:.0f}元|账户总值{total_account_value:.0f}元|总收益{total_floating_pnl:.0f}元|收益率{(total_floating_pnl/total_capital)*100:.2f}%")
|
|
|
+
|
|
|
+ def save_account_summary_to_csv(self, account_data):
|
|
|
+ """保存账户汇总到CSV文件"""
|
|
|
+ try:
|
|
|
+ # 复制数据并处理日期格式
|
|
|
+ data_copy = account_data.copy()
|
|
|
+
|
|
|
+ # 转换日期字段
|
|
|
+ if '交易日期' in data_copy and data_copy['交易日期'] is not None:
|
|
|
+ if hasattr(data_copy['交易日期'], 'strftime'):
|
|
|
+ data_copy['交易日期'] = data_copy['交易日期'].strftime('%Y-%m-%d')
|
|
|
+ elif hasattr(data_copy['交易日期'], 'date'):
|
|
|
+ data_copy['交易日期'] = data_copy['交易日期'].date().strftime('%Y-%m-%d')
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ date_obj = pd.to_datetime(data_copy['交易日期'])
|
|
|
+ data_copy['交易日期'] = date_obj.strftime('%Y-%m-%d')
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # 处理策略详情字段
|
|
|
+ if '策略详情' in data_copy and isinstance(data_copy['策略详情'], dict):
|
|
|
+ strategy_strings = []
|
|
|
+ for etf_code, details in data_copy['策略详情'].items():
|
|
|
+ bull_count = details.get('牛差策略数量', 0)
|
|
|
+ strategy_str = f"{etf_code}(分配{details['分配资金']:.0f}|投入{details['初始投入']:.0f}|市值{details['当前市值']:.0f}|浮盈{details['浮动盈亏']:.0f}|持仓{details['持仓数量']}|牛差{bull_count})"
|
|
|
+ strategy_strings.append(strategy_str)
|
|
|
+ data_copy['策略详情'] = '; '.join(strategy_strings)
|
|
|
+
|
|
|
+ # 创建DataFrame,排除复杂字段
|
|
|
+ csv_data = {k: v for k, v in data_copy.items() if not isinstance(v, dict)}
|
|
|
+ df = pd.DataFrame([csv_data])
|
|
|
+
|
|
|
+ # 检查文件是否存在
|
|
|
+ file_exists = os.path.exists(self.account_csv_path)
|
|
|
+
|
|
|
+ # 保存到CSV
|
|
|
+ if file_exists:
|
|
|
+ df.to_csv(self.account_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
|
|
|
else:
|
|
|
- print(f"✗ 开仓失败")
|
|
|
-
|
|
|
- # 打印最终结果
|
|
|
- print(f"\n测试结果:")
|
|
|
- print(f"总持仓数: {len(strategy.positions)}")
|
|
|
- print(f"总交易数: {len(strategy.trades)}")
|
|
|
-
|
|
|
- if strategy.positions:
|
|
|
- print(f"第一个持仓详情:")
|
|
|
- pos = strategy.positions[0]
|
|
|
- for key, value in pos.items():
|
|
|
- print(f" {key}: {value}")
|
|
|
-
|
|
|
- return strategy
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- print(f"测试过程中出现错误: {e}")
|
|
|
- import traceback
|
|
|
- traceback.print_exc()
|
|
|
- return None
|
|
|
-
|
|
|
-
|
|
|
-def test_strategy():
|
|
|
- """测试策略"""
|
|
|
- print("开始测试深度实值牛差策略...")
|
|
|
-
|
|
|
- # 创建策略实例
|
|
|
- strategy = DeepITMBullSpreadStrategy(
|
|
|
- underlying='510300.XSHG', # 300ETF
|
|
|
- start_date='2024-01-01',
|
|
|
- end_date='2025-06-30'
|
|
|
- )
|
|
|
-
|
|
|
- # 运行回测
|
|
|
+ df.to_csv(self.account_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"保存账户汇总到CSV时出错: {e}")
|
|
|
+ print(f"问题数据: {account_data}")
|
|
|
+
|
|
|
+ def plot_account_summary(self):
|
|
|
+ """绘制整体账户资金曲线"""
|
|
|
+ if not self.daily_account_records:
|
|
|
+ print("没有账户数据可以绘制")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 转换为DataFrame
|
|
|
+ account_df = pd.DataFrame(self.daily_account_records)
|
|
|
+
|
|
|
+ if account_df.empty:
|
|
|
+ print("没有账户数据可以绘制")
|
|
|
+ return
|
|
|
+
|
|
|
+ # print(f"账户数据记录数量: {len(account_df)}")
|
|
|
+ # print(f"账户数据列: {account_df.columns.tolist()}")
|
|
|
+ # print(f"前几行数据:\n{account_df.head()}")
|
|
|
+
|
|
|
+ # 确保日期格式正确
|
|
|
+ account_df['交易日期'] = pd.to_datetime(account_df['交易日期'], errors='coerce')
|
|
|
+ account_df = account_df.dropna(subset=['交易日期']).sort_values('交易日期')
|
|
|
+
|
|
|
+ if account_df.empty:
|
|
|
+ print("日期转换后没有有效数据")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 确保数值字段为数值类型,处理可能的字符串或其他类型
|
|
|
+ numeric_columns = ['总资金', '初始投入总额', '剩余现金', '组合当前市值', '总浮动盈亏', '账户总价值', '资金使用率', '收益率', '牛差策略总数']
|
|
|
+
|
|
|
+ for col in numeric_columns:
|
|
|
+ if col in account_df.columns:
|
|
|
+ # 先转换为字符串,移除可能的非数字字符
|
|
|
+ account_df[col] = account_df[col].astype(str).str.replace('[^\d.-]', '', regex=True)
|
|
|
+ # 再转换为数值类型
|
|
|
+ account_df[col] = pd.to_numeric(account_df[col], errors='coerce').fillna(0)
|
|
|
+ print(f"列 {col} 数据范围: {account_df[col].min()} 到 {account_df[col].max()}")
|
|
|
+
|
|
|
+ # 检查是否有有效的数值数据
|
|
|
+ if account_df[['总资金', '账户总价值']].isna().all().all():
|
|
|
+ print("所有数值数据都无效,无法绘图")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 绘图
|
|
|
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 上图:账户总价值曲线
|
|
|
+ # 转换为numpy数组以确保数据类型
|
|
|
+ dates = account_df['交易日期'].values
|
|
|
+ total_values = account_df['账户总价值'].values.astype(float)
|
|
|
+ initial_capitals = account_df['总资金'].values.astype(float)
|
|
|
+
|
|
|
+ ax1.plot(dates, total_values,
|
|
|
+ label='账户总价值', color='green', linewidth=2)
|
|
|
+ ax1.plot(dates, initial_capitals,
|
|
|
+ label='初始资金', color='gray', linewidth=1, linestyle='--')
|
|
|
+
|
|
|
+ # 填充区域(只有在数据有效时)
|
|
|
+ if len(total_values) > 0 and len(initial_capitals) > 0:
|
|
|
+ profit_mask = total_values >= initial_capitals
|
|
|
+ loss_mask = total_values < initial_capitals
|
|
|
+
|
|
|
+ if profit_mask.any():
|
|
|
+ ax1.fill_between(dates, initial_capitals, total_values,
|
|
|
+ where=profit_mask,
|
|
|
+ color='green', alpha=0.3, label='盈利区域')
|
|
|
+ if loss_mask.any():
|
|
|
+ ax1.fill_between(dates, initial_capitals, total_values,
|
|
|
+ where=loss_mask,
|
|
|
+ color='red', alpha=0.3, label='亏损区域')
|
|
|
+
|
|
|
+ ax1.set_title('整体账户资金曲线', fontsize=14)
|
|
|
+ ax1.set_ylabel('资金 (元)', fontsize=12)
|
|
|
+ ax1.legend(fontsize=10)
|
|
|
+ ax1.grid(True, alpha=0.3)
|
|
|
+
|
|
|
+ # 下图:资金构成
|
|
|
+ remaining_cash = account_df['剩余现金'].values.astype(float)
|
|
|
+ current_market_value = account_df['组合当前市值'].values.astype(float)
|
|
|
+
|
|
|
+ ax2.plot(dates, remaining_cash,
|
|
|
+ label='剩余现金', color='blue', linewidth=2)
|
|
|
+ ax2.plot(dates, current_market_value,
|
|
|
+ label='组合当前市值', color='orange', linewidth=2, linestyle='--')
|
|
|
+
|
|
|
+ ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3)
|
|
|
+ ax2.set_title('资金构成分析', fontsize=14)
|
|
|
+ ax2.set_ylabel('金额 (元)', fontsize=12)
|
|
|
+ ax2.set_xlabel('日期', fontsize=12)
|
|
|
+ ax2.legend(fontsize=10)
|
|
|
+ ax2.grid(True, alpha=0.3)
|
|
|
+
|
|
|
+ # 格式化日期显示
|
|
|
+ import matplotlib.dates as mdates
|
|
|
+ for ax in [ax1, ax2]:
|
|
|
+ ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
|
|
+ ax.xaxis.set_major_locator(mdates.MonthLocator())
|
|
|
+
|
|
|
+ plt.xticks(rotation=45)
|
|
|
+
|
|
|
+ # 添加数据标注
|
|
|
+ if len(total_values) > 0:
|
|
|
+ final_value = float(total_values[-1])
|
|
|
+ initial_capital = float(initial_capitals[0])
|
|
|
+ total_return = final_value - initial_capital
|
|
|
+ return_rate = total_return / initial_capital if initial_capital > 0 else 0
|
|
|
+ max_value = float(np.max(total_values))
|
|
|
+ min_value = float(np.min(total_values))
|
|
|
+
|
|
|
+ # 获取最新的策略类型统计
|
|
|
+ final_bull_count = account_df['牛差策略总数'].iloc[-1] if '牛差策略总数' in account_df.columns else 0
|
|
|
+
|
|
|
+ ax1.text(0.02, 0.98,
|
|
|
+ f'当前账户价值: {final_value:,.0f}元\n'
|
|
|
+ f'总收益: {total_return:,.0f}元\n'
|
|
|
+ f'收益率: {return_rate:.2%}\n'
|
|
|
+ f'最高价值: {max_value:,.0f}元\n'
|
|
|
+ f'最低价值: {min_value:,.0f}元\n'
|
|
|
+ f'当前持仓:牛差{final_bull_count}个',
|
|
|
+ transform=ax1.transAxes, fontsize=10, verticalalignment='top',
|
|
|
+ bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
|
|
|
+
|
|
|
+ plt.tight_layout()
|
|
|
+ plt.show()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"绘图过程中出错: {e}")
|
|
|
+ print(f"账户数据样本:\n{account_df[['交易日期', '总资金', '账户总价值', '剩余现金', '组合当前市值', '总浮动盈亏']].head()}")
|
|
|
+ # 尝试简化版本的绘图
|
|
|
+ try:
|
|
|
+ fig2, ax = plt.subplots(1, 1, figsize=(10, 6))
|
|
|
+ ax.plot(range(len(account_df)), account_df['账户总价值'].astype(float),
|
|
|
+ label='账户总价值', color='green', linewidth=2)
|
|
|
+ ax.set_title('简化版账户资金曲线')
|
|
|
+ ax.set_ylabel('资金 (元)')
|
|
|
+ ax.set_xlabel('交易日序号')
|
|
|
+ ax.legend()
|
|
|
+ ax.grid(True)
|
|
|
+ plt.show()
|
|
|
+ except Exception as e2:
|
|
|
+ print(f"简化绘图也失败: {e2}")
|
|
|
+
|
|
|
+ def run_all_strategies(self):
|
|
|
+ """运行所有策略"""
|
|
|
+ print("="*60)
|
|
|
+ print("开始运行多标的深度实值买购和卖购组合的牛差策略")
|
|
|
+ print("="*60)
|
|
|
+
|
|
|
+ # 获取统一的交易日历(取第一个策略的交易日历作为基准)
|
|
|
+ if not self.strategies:
|
|
|
+ print("没有可运行的策略")
|
|
|
+ return {}
|
|
|
+
|
|
|
+ first_strategy = list(self.strategies.values())[0]
|
|
|
+ trade_days = first_strategy.trade_days.index
|
|
|
+
|
|
|
+ # 为每个策略设置动态合约数量计算方法
|
|
|
+ for etf_code, strategy in self.strategies.items():
|
|
|
+ original_open_method = strategy.open_position # 使用新的统一开仓方法
|
|
|
+
|
|
|
+ def make_dynamic_open_position(strat, orig_method):
|
|
|
+ def dynamic_open_position(trade_date, etf_price, position_type='main', silent=False, save_to_csv=True):
|
|
|
+ # 首先用原方法获取期权信息,但不保存到CSV
|
|
|
+ original_contract_size = strat.config['contract_size']
|
|
|
+ strat.config['contract_size'] = 1 # 临时设置为1张以获取期权信息
|
|
|
+
|
|
|
+ temp_result = orig_method(trade_date, etf_price, position_type, silent=True, save_to_csv=False)
|
|
|
+
|
|
|
+ if temp_result:
|
|
|
+ # 根据策略类型获取期权信息进行动态计算
|
|
|
+ strategy_type = temp_result.get('strategy_type', StrategyType.BULL_SPREAD)
|
|
|
+ sell_call_info = temp_result['sell_call']
|
|
|
+ buy_call_info = temp_result['buy_call'] # 两种策略都有买购期权
|
|
|
+
|
|
|
+ # 两种策略都使用买购和卖购期权信息
|
|
|
+ dynamic_size = self.calculate_dynamic_contract_size(strat, etf_price, buy_call_info, sell_call_info)
|
|
|
+
|
|
|
+ # 验证动态合约数量,确保为正数
|
|
|
+ if dynamic_size <= 0:
|
|
|
+ print(f" {strat.config['etf_code']}: 动态合约数量计算结果无效({dynamic_size}),使用默认值30张")
|
|
|
+ dynamic_size = 30 # 使用默认值
|
|
|
+
|
|
|
+ # 更新合约数量并重新开仓
|
|
|
+ strat.config['contract_size'] = dynamic_size
|
|
|
+ strat._validate_contract_size() # 验证合约数量
|
|
|
+
|
|
|
+ # 移除临时仓位和交易记录
|
|
|
+ if strat.positions and strat.positions[-1] == temp_result:
|
|
|
+ strat.positions.pop()
|
|
|
+ if strat.trade_records and len(strat.trade_records) > 0:
|
|
|
+ # 检查最后一条记录是否是刚刚添加的临时记录
|
|
|
+ last_record = strat.trade_records[-1]
|
|
|
+ if (last_record.get('交易类型') == '开仓' and
|
|
|
+ not last_record.get('平仓原因') and
|
|
|
+ last_record.get('合约数量') == 1):
|
|
|
+ strat.trade_records.pop()
|
|
|
+
|
|
|
+ # 重新开仓,这次保存到CSV
|
|
|
+ result = orig_method(trade_date, etf_price, position_type, silent, save_to_csv)
|
|
|
+ else:
|
|
|
+ # 第一次调用失败,恢复原始合约数量设置,用传入的silent参数重新调用显示失败详情
|
|
|
+ strat.config['contract_size'] = original_contract_size
|
|
|
+ result = orig_method(trade_date, etf_price, position_type, silent, save_to_csv)
|
|
|
+ return result
|
|
|
+
|
|
|
+ # 恢复原始设置
|
|
|
+ strat.config['contract_size'] = original_contract_size
|
|
|
+ return result
|
|
|
+ return dynamic_open_position
|
|
|
+
|
|
|
+ strategy.open_position = make_dynamic_open_position(strategy, original_open_method) # 更新方法名
|
|
|
+
|
|
|
+ # 按日期统一运行所有策略
|
|
|
+ print(f"开始按日期统一运行策略,共{len(trade_days)}个交易日")
|
|
|
+
|
|
|
+ for i, trade_date in enumerate(trade_days):
|
|
|
+ # print(f"\n处理交易日 {trade_date.strftime('%Y-%m-%d')} ({i+1}/{len(trade_days)})")
|
|
|
+
|
|
|
+ # 为每个策略处理当天的交易
|
|
|
+ daily_has_activity = False
|
|
|
+
|
|
|
+ for etf_code, strategy in self.strategies.items():
|
|
|
+ try:
|
|
|
+ # 获取ETF价格
|
|
|
+ price_data = get_price(strategy.underlying_symbol, trade_date, trade_date, fields=['close'])['close']
|
|
|
+ if price_data.empty:
|
|
|
+ print(f" {etf_code}: 获取ETF价格失败")
|
|
|
+ continue
|
|
|
+ etf_price = price_data.iloc[0]
|
|
|
+
|
|
|
+ # 记录每日持仓状况(但不触发账户汇总回调)
|
|
|
+ strategy.record_daily_positions(trade_date, etf_price)
|
|
|
+
|
|
|
+ # 检查是否需要平仓
|
|
|
+ if len(strategy.positions) > 0:
|
|
|
+ for position in strategy.positions:
|
|
|
+ should_close, reason = strategy.should_close_position(position, trade_date, etf_price)
|
|
|
+ if should_close:
|
|
|
+ strategy.close_position(position, trade_date, etf_price, reason)
|
|
|
+ print(f" {etf_code}: 平仓 {reason}, ETF价格: {etf_price:.4f}")
|
|
|
+ daily_has_activity = True
|
|
|
+
|
|
|
+ # 检查是否需要开新仓(首次开仓或平仓后重新开仓)
|
|
|
+ open_positions = [p for p in strategy.positions if p['status'] == 'open']
|
|
|
+ if not open_positions:
|
|
|
+ new_position = strategy.open_position(trade_date, etf_price, 'main', silent=False) # 使用新的统一开仓方法
|
|
|
+ if new_position:
|
|
|
+ max_profit = new_position['profit_info']['total_max_profit']
|
|
|
+ contract_size = new_position['contract_size']
|
|
|
+
|
|
|
+ # 牛差策略输出
|
|
|
+ strategy_desc = f"牛差组合策略"
|
|
|
+ profit_desc = f"最大牛差收益: {max_profit:.2f}元"
|
|
|
+
|
|
|
+ print(f" {etf_code}: 开仓主仓位({strategy_desc}),ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
|
|
|
+ daily_has_activity = True
|
|
|
+ else:
|
|
|
+ print(f" {etf_code}: 开仓失败,无法找到合适的期权组合,ETF价格: {etf_price:.4f}")
|
|
|
+
|
|
|
+ # 检查是否需要加仓
|
|
|
+ elif strategy.should_add_position(trade_date, etf_price):
|
|
|
+ add_position = strategy.open_position(trade_date, etf_price, 'add', silent=False) # 使用新的统一开仓方法
|
|
|
+ if add_position:
|
|
|
+ max_profit = add_position['profit_info']['total_max_profit']
|
|
|
+ contract_size = add_position['contract_size']
|
|
|
+
|
|
|
+ # 牛差策略输出
|
|
|
+ strategy_desc = f"牛差组合策略"
|
|
|
+ profit_desc = f"最大牛差收益: {max_profit:.2f}元"
|
|
|
+
|
|
|
+ print(f" {etf_code}: 加仓({strategy_desc}),ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
|
|
|
+ daily_has_activity = True
|
|
|
+
|
|
|
+ # 更新策略的前一日ETF价格
|
|
|
+ strategy.previous_etf_price = etf_price
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f" {etf_code}: 处理失败 - {e}")
|
|
|
+
|
|
|
+ # 统一记录当天的账户汇总(无论是否有交易活动)
|
|
|
+ try:
|
|
|
+ self.record_daily_account_summary(trade_date)
|
|
|
+ if daily_has_activity:
|
|
|
+ print(f" 账户汇总已更新")
|
|
|
+ except Exception as e:
|
|
|
+ print(f" 记录账户汇总失败: {e}")
|
|
|
+
|
|
|
+ # 收集所有策略的结果
|
|
|
+ results = {}
|
|
|
+ for etf_code, strategy in self.strategies.items():
|
|
|
+ try:
|
|
|
+ results[etf_code] = {
|
|
|
+ 'strategy': strategy,
|
|
|
+ 'summary': strategy.get_performance_summary(),
|
|
|
+ 'allocated_capital': strategy.config['allocated_capital']
|
|
|
+ }
|
|
|
+ print(f"{etf_code}策略运行完成")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"{etf_code}策略结果收集出错: {e}")
|
|
|
+ results[etf_code] = {'error': str(e)}
|
|
|
+
|
|
|
+ print("\n所有策略运行完成!")
|
|
|
+ return results
|
|
|
+
|
|
|
+ def generate_overall_report(self, results):
|
|
|
+ """生成总体报告"""
|
|
|
+ print("\n" + "="*60)
|
|
|
+ print("多标的牛差策略总体报告")
|
|
|
+ print("="*60)
|
|
|
+
|
|
|
+ total_pnl = 0
|
|
|
+ total_trades = 0
|
|
|
+ total_winning_trades = 0
|
|
|
+
|
|
|
+ for etf_code, result in results.items():
|
|
|
+ if 'error' in result:
|
|
|
+ print(f"\n{etf_code}: 策略执行出错 - {result['error']}")
|
|
|
+ continue
|
|
|
+
|
|
|
+ strategy = result['strategy']
|
|
|
+ allocated_capital = result['allocated_capital']
|
|
|
+
|
|
|
+ print(f"\n{etf_code} 策略结果:")
|
|
|
+ print(f"分配资金: {allocated_capital:,.0f}元")
|
|
|
+ print(result['summary'])
|
|
|
+
|
|
|
+ # 统计总体数据
|
|
|
+ closed_positions = [p for p in strategy.positions if p['status'] == 'closed']
|
|
|
+ if closed_positions:
|
|
|
+ strategy_pnl = sum(p['pnl'] for p in closed_positions)
|
|
|
+ strategy_trades = len(closed_positions)
|
|
|
+ strategy_winning = len([p for p in closed_positions if p['pnl'] > 0])
|
|
|
+
|
|
|
+ total_pnl += strategy_pnl
|
|
|
+ total_trades += strategy_trades
|
|
|
+ total_winning_trades += strategy_winning
|
|
|
+
|
|
|
+ # 总体统计
|
|
|
+ if total_trades > 0:
|
|
|
+ overall_win_rate = total_winning_trades / total_trades
|
|
|
+ print(f"\n总体策略表现:")
|
|
|
+ print(f"总交易次数: {total_trades}")
|
|
|
+ print(f"总获利交易: {total_winning_trades}")
|
|
|
+ print(f"总体胜率: {overall_win_rate:.2%}")
|
|
|
+ print(f"总盈亏: {total_pnl:.2f}元")
|
|
|
+ print(f"平均每笔盈亏: {total_pnl/total_trades:.2f}元")
|
|
|
+ print(f"总资金收益率: {total_pnl/self.config.capital_config['total_capital']:.2%}")
|
|
|
+
|
|
|
+# 使用示例
|
|
|
+def run_deep_itm_bull_spread_example():
|
|
|
+ """运行深度实值买购和卖购组合牛差策略示例"""
|
|
|
+
|
|
|
+ # 创建策略配置
|
|
|
+ config = StrategyConfig()
|
|
|
+
|
|
|
+ # 可以自定义配置
|
|
|
+ config.time_config = {
|
|
|
+ 'start_date': '2025-06-15',
|
|
|
+ 'end_date': '2025-08-15'
|
|
|
+ }
|
|
|
+
|
|
|
+ config.capital_config.update({
|
|
|
+ 'total_capital': 1000000, # 100万总资金
|
|
|
+ 'capital_allocation': {
|
|
|
+ '50ETF': 0.5, # 50ETF 50%
|
|
|
+ '300ETF': 0.3, # 300ETF 30%
|
|
|
+ '创业板ETF': 0.2 # 创业板ETF 20%
|
|
|
+ },
|
|
|
+ 'capital_usage_limit': 0.8, # 使用80%资金
|
|
|
+ 'bull_spread_margin_discount': 30, # 牛差组合策略保证金优惠(每单位组合加收30元)
|
|
|
+ 'contract_unit': 10000, # 合约单位
|
|
|
+ 'margin_params': { # 保证金计算参数(按交易所规定)
|
|
|
+ 'volatility_factor': 0.12, # 波动率因子12%
|
|
|
+ 'min_margin_factor': 0.07 # 最小保证金因子7%
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ # 创建多标的策略管理器
|
|
|
+ manager = MultiUnderlyingBullSpreadManager(config)
|
|
|
+
|
|
|
+ # 运行所有策略
|
|
|
+ results = manager.run_all_strategies()
|
|
|
+
|
|
|
+ # 生成报告
|
|
|
+ manager.generate_overall_report(results)
|
|
|
+
|
|
|
+ # 绘制整体账户资金曲线
|
|
|
try:
|
|
|
- print("正在运行回测...")
|
|
|
- strategy.run_backtest()
|
|
|
-
|
|
|
- print("正在生成详细报告...")
|
|
|
- # 生成详细报告(包含买购和卖购分项分析)
|
|
|
- detailed_results = strategy.generate_detailed_report()
|
|
|
-
|
|
|
- print("正在导出数据...")
|
|
|
- # 导出数据到CSV
|
|
|
- positions_df, trades_df, daily_df = strategy.export_data_to_csv("bull_spread_300etf")
|
|
|
-
|
|
|
- print("正在打印持仓详情...")
|
|
|
- # 打印详细持仓信息
|
|
|
- strategy.print_position_details()
|
|
|
-
|
|
|
- return strategy, detailed_results
|
|
|
-
|
|
|
+ print(f"\n绘制整体账户资金曲线...")
|
|
|
+ manager.plot_account_summary()
|
|
|
except Exception as e:
|
|
|
- print(f"回测过程中出现错误: {e}")
|
|
|
- import traceback
|
|
|
- traceback.print_exc()
|
|
|
- return None, None
|
|
|
-
|
|
|
-
|
|
|
-def analyze_bull_spread_example():
|
|
|
- """分析牛差策略示例"""
|
|
|
- print("\n" + "="*60)
|
|
|
- print("牛差策略期权组合分析示例")
|
|
|
- print("="*60)
|
|
|
-
|
|
|
- # 假设当前300ETF价格为4.0
|
|
|
- underlying_price = 4.0
|
|
|
-
|
|
|
- # 根据策略逻辑选择期权
|
|
|
- buy_strike = underlying_price * 0.90 # 深度实值买购
|
|
|
- sell_strike = underlying_price * 1.00 # 平值卖购
|
|
|
-
|
|
|
- # 估算权利金(简化)
|
|
|
- buy_premium = 0.44 # 深度实值期权,主要是内在价值
|
|
|
- sell_premium = 0.20 # 平值期权,主要是时间价值
|
|
|
-
|
|
|
- print(f"当前标的价格: {underlying_price}")
|
|
|
- print(f"买购期权: 行权价 {buy_strike:.2f}, 权利金 {buy_premium:.4f}")
|
|
|
- print(f"卖购期权: 行权价 {sell_strike:.2f}, 权利金 {sell_premium:.4f}")
|
|
|
-
|
|
|
- # 使用期权分析工具
|
|
|
- analyzer = OptionsAnalyzer()
|
|
|
- result = analyzer.analyze_options(
|
|
|
- ('buy', 'call', buy_premium, buy_strike, 1),
|
|
|
- ('sell', 'call', sell_premium, sell_strike, 1)
|
|
|
- )
|
|
|
-
|
|
|
- return result
|
|
|
-
|
|
|
-
|
|
|
-def compare_with_etf_holding():
|
|
|
- """三个策略分别与ETF持有收益对比"""
|
|
|
- print("\n" + "="*80)
|
|
|
- print("三个策略分别与ETF持有收益对比")
|
|
|
- print("="*80)
|
|
|
-
|
|
|
- initial_price = 4.0
|
|
|
- initial_investment = 100000
|
|
|
-
|
|
|
- # 牛差策略参数
|
|
|
- buy_strike = initial_price * 0.90
|
|
|
- sell_strike = initial_price * 1.00
|
|
|
- buy_premium = 0.44
|
|
|
- sell_premium = 0.20
|
|
|
-
|
|
|
- # 计算可开仓张数
|
|
|
- net_premium = buy_premium - sell_premium
|
|
|
- contracts = int(initial_investment / (net_premium * 10000))
|
|
|
-
|
|
|
- print(f"初始投资: {initial_investment:,.0f}元")
|
|
|
- print(f"初始ETF价格: {initial_price:.2f}")
|
|
|
- print(f"牛差组合: 买购{buy_strike:.2f}@{buy_premium:.4f}, 卖购{sell_strike:.2f}@{sell_premium:.4f}")
|
|
|
- print(f"可开仓张数: {contracts}张")
|
|
|
-
|
|
|
- # 模拟不同价格变化下的收益
|
|
|
- price_changes = np.arange(-20, 21, 5)
|
|
|
- results = []
|
|
|
-
|
|
|
- for change in price_changes:
|
|
|
- new_price = initial_price * (1 + change / 100)
|
|
|
-
|
|
|
- # ETF持有收益
|
|
|
- etf_return = (new_price - initial_price) / initial_price * initial_investment
|
|
|
-
|
|
|
- # 分别计算三个策略的收益
|
|
|
- buy_call_value = max(0, new_price - buy_strike)
|
|
|
- sell_call_value = max(0, new_price - sell_strike)
|
|
|
-
|
|
|
- # 1. 买购期权收益
|
|
|
- buy_call_pnl = (buy_call_value - buy_premium) * contracts * 10000
|
|
|
-
|
|
|
- # 2. 卖购期权收益
|
|
|
- sell_call_pnl = (sell_premium - sell_call_value) * contracts * 10000
|
|
|
-
|
|
|
- # 3. 牛差组合收益
|
|
|
- bull_spread_return = buy_call_pnl + sell_call_pnl
|
|
|
-
|
|
|
- results.append({
|
|
|
- 'price_change': change,
|
|
|
- 'new_price': new_price,
|
|
|
- 'etf_return': etf_return,
|
|
|
- 'buy_call_return': buy_call_pnl,
|
|
|
- 'sell_call_return': sell_call_pnl,
|
|
|
- 'bull_spread_return': bull_spread_return,
|
|
|
- 'bull_vs_etf': bull_spread_return - etf_return,
|
|
|
- 'buy_vs_etf': buy_call_pnl - etf_return,
|
|
|
- 'sell_vs_etf': sell_call_pnl - etf_return
|
|
|
- })
|
|
|
-
|
|
|
- # 创建对比表格
|
|
|
- df = pd.DataFrame(results)
|
|
|
-
|
|
|
- print(f"\n=== 详细收益对比表 ===")
|
|
|
- print("价格变化(%) | 新价格 | ETF收益 | 买购收益 | 卖购收益 | 牛差收益")
|
|
|
- print("-" * 75)
|
|
|
-
|
|
|
- for _, row in df.iterrows():
|
|
|
- print(f"{row['price_change']:>8.0f}% | {row['new_price']:>6.2f} | "
|
|
|
- f"{row['etf_return']:>7.0f} | {row['buy_call_return']:>8.0f} | "
|
|
|
- f"{row['sell_call_return']:>8.0f} | {row['bull_spread_return']:>8.0f}")
|
|
|
-
|
|
|
- print(f"\n=== 与ETF基准的差异对比 ===")
|
|
|
- print("价格变化(%) | 牛差-ETF | 买购-ETF | 卖购-ETF")
|
|
|
- print("-" * 50)
|
|
|
-
|
|
|
- for _, row in df.iterrows():
|
|
|
- print(f"{row['price_change']:>8.0f}% | {row['bull_vs_etf']:>8.0f} | "
|
|
|
- f"{row['buy_vs_etf']:>8.0f} | {row['sell_vs_etf']:>8.0f}")
|
|
|
-
|
|
|
- # 绘制三个对比图
|
|
|
- # 设置中文字体
|
|
|
- plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
|
|
|
- plt.rcParams['axes.unicode_minus'] = False
|
|
|
-
|
|
|
- plt.figure(figsize=(15, 12))
|
|
|
-
|
|
|
- # 第一个子图:牛差策略 vs ETF
|
|
|
- plt.subplot(3, 1, 1)
|
|
|
- plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
|
|
|
- plt.plot(df['price_change'], df['bull_spread_return'], 's-', linewidth=2, label='牛差策略', color='green')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.xlabel('价格变化 (%)')
|
|
|
- plt.ylabel('收益 (元)')
|
|
|
- plt.title('牛差策略 vs ETF持有收益对比')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第二个子图:买购策略 vs ETF
|
|
|
- plt.subplot(3, 1, 2)
|
|
|
- plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
|
|
|
- plt.plot(df['price_change'], df['buy_call_return'], '^-', linewidth=2, label='买购期权', color='blue')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.xlabel('价格变化 (%)')
|
|
|
- plt.ylabel('收益 (元)')
|
|
|
- plt.title('买购期权 vs ETF持有收益对比')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- # 第三个子图:卖购策略 vs ETF
|
|
|
- plt.subplot(3, 1, 3)
|
|
|
- plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
|
|
|
- plt.plot(df['price_change'], df['sell_call_return'], 'v-', linewidth=2, label='卖购期权', color='red')
|
|
|
- plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
|
|
|
- plt.xlabel('价格变化 (%)')
|
|
|
- plt.ylabel('收益 (元)')
|
|
|
- plt.title('卖购期权 vs ETF持有收益对比')
|
|
|
- plt.legend()
|
|
|
- plt.grid(True, alpha=0.3)
|
|
|
-
|
|
|
- plt.tight_layout()
|
|
|
- plt.show()
|
|
|
-
|
|
|
- return df
|
|
|
-
|
|
|
-
|
|
|
-if __name__ == "__main__":
|
|
|
- print("深度实值牛差策略综合测试工具")
|
|
|
- print("="*50)
|
|
|
-
|
|
|
- # 0. 首先测试开仓逻辑
|
|
|
- print("\n0. 测试开仓逻辑:")
|
|
|
- test_strategy_instance = test_opening_logic()
|
|
|
-
|
|
|
- if test_strategy_instance and len(test_strategy_instance.positions) > 0:
|
|
|
- print("✓ 开仓逻辑测试通过,继续完整回测")
|
|
|
-
|
|
|
- # 1. 运行策略回测
|
|
|
- print("\n1. 运行策略回测:")
|
|
|
- strategy, detailed_results = test_strategy()
|
|
|
-
|
|
|
- # 2. 分析期权组合
|
|
|
- print("\n2. 期权组合分析:")
|
|
|
- option_analysis = analyze_bull_spread_example()
|
|
|
-
|
|
|
- # 3. 与ETF持有对比
|
|
|
- print("\n3. 与ETF持有收益对比:")
|
|
|
- comparison_results = compare_with_etf_holding()
|
|
|
-
|
|
|
- print(f"\n测试完成!数据已导出到CSV文件,可用于进一步分析。")
|
|
|
- else:
|
|
|
- print("✗ 开仓逻辑测试失败,请检查期权数据和策略参数")
|
|
|
- print("建议检查以下几点:")
|
|
|
- print("1. 期权合约数据是否可用")
|
|
|
- print("2. 策略参数设置是否合理")
|
|
|
- print("3. 时间价值阈值是否过于严格")
|
|
|
+ print(f"绘制整体账户资金曲线时出错: {e}")
|
|
|
+
|
|
|
+ # 绘制各策略结果(可选)
|
|
|
+ for etf_code, result in results.items():
|
|
|
+ if 'strategy' in result:
|
|
|
+ try:
|
|
|
+ print(f"\n绘制{etf_code}策略结果...")
|
|
|
+ result['strategy'].plot_results()
|
|
|
+ except Exception as e:
|
|
|
+ print(f"绘制{etf_code}图表时出错: {e}")
|