13_卖出跨式策略50ETF.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # 卖出跨式策略-50ETF
  2. # 参考资料:
  3. # - 原始策略来源: https://www.joinquant.com/view/community/detail/73a7f044b73242b136c8c840ef7f1748
  4. # - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%8D%96%E5%87%BA%E8%B7%A8%E5%BC%8F%E7%AD%96%E7%95%A5-50ETF.ipynb
  5. # TODO: 添加卖出跨式策略50ETF相关代码
  6. import jqdata
  7. from jqdata import *
  8. import pandas as pd
  9. import numpy as np
  10. import datetime
  11. import matplotlib.pyplot as plt
  12. plt.rcParams['font.sans-serif']=['SimHei']
  13. plt.rcParams['axes.unicode_minus'] = False
  14. from datetime import datetime, timedelta
  15. def is_last_day_of_month(date_str):
  16. # 将字符串转换为日期对象
  17. date_obj = datetime.strptime(date_str, '%Y-%m-%d')
  18. # 获取下一个日期对象
  19. next_date_obj = date_obj + timedelta(days=1)
  20. # 判断是否为下个月的第一天,如果是,则当前日期为月末
  21. return date_obj.month != next_date_obj.month
  22. def get_last_day_of_month(date_str):
  23. # 将字符串转换为日期对象
  24. date_obj = datetime.strptime(date_str, '%Y-%m-%d')
  25. # 获取下个月的第一天日期对象
  26. next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
  27. # 从下个月的第一天减去一天,得到当前月的月末日期对象
  28. last_day_of_month = next_month_first_day - timedelta(days=1)
  29. # 返回月末日期的字符串形式
  30. return last_day_of_month.strftime('%Y-%m-%d')
  31. #获得虚值合约
  32. def getContract(start,end,listdate,contratType='CO'):
  33. q_contract_info = query(opt.OPT_CONTRACT_INFO.code,
  34. opt.OPT_CONTRACT_INFO.trading_code,
  35. opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
  36. opt.OPT_CONTRACT_INFO.exercise_price,
  37. opt.OPT_CONTRACT_INFO.last_trade_date,
  38. opt.OPT_CONTRACT_INFO.list_date
  39. ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType, #期权类型
  40. opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG', #上交所
  41. opt.OPT_CONTRACT_INFO.last_trade_date > start, #时间-到期月开始
  42. opt.OPT_CONTRACT_INFO.last_trade_date <= end, #时间-到期月结束
  43. opt.OPT_CONTRACT_INFO.list_date < listdate) #在交易前上市
  44. contract_info = opt.run_query(q_contract_info)
  45. etf_cls = get_price('510050.XSHG',listdate,listdate,fields=['close']).values[0][0]
  46. if contratType == 'CO':
  47. contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
  48. else:
  49. contract_info['price_spread'] = etf_cls - contract_info['exercise_price']
  50. if contract_info['price_spread'].max() > 0:
  51. contract_info = contract_info[contract_info['price_spread'] > 0] #选出虚值期权
  52. contract_info = contract_info.sort_values('exercise_price')
  53. else: #全是实值期权
  54. contract_info = contract_info.sort_values('exercise_price',ascending=False)
  55. return(contract_info)
  56. #处理合约切换标记
  57. def contractChange(holding_contract):
  58. data2 = pd.DataFrame(holding_contract)
  59. data2.columns = ['holding_contract']
  60. data2 = data2.reindex(columns=['holding_contract','close','last_close'])
  61. last_contract = holding_contract.iloc[0] #记录上个持仓
  62. for t in data2.index:
  63. if last_contract == data2.loc[t,'holding_contract']: #期权未换仓
  64. #收盘价
  65. q_price = query(opt.OPT_DAILY_PRICE.code,
  66. opt.OPT_DAILY_PRICE.date,
  67. opt.OPT_DAILY_PRICE.close,
  68. ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
  69. opt.OPT_DAILY_PRICE.date==t)
  70. price = opt.run_query(q_price)['close'][0]
  71. data2.loc[t,'close'] = price
  72. else:
  73. #收盘价,新
  74. q_price = query(opt.OPT_DAILY_PRICE.code,
  75. opt.OPT_DAILY_PRICE.date,
  76. opt.OPT_DAILY_PRICE.close,
  77. ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
  78. opt.OPT_DAILY_PRICE.date==t)
  79. price = opt.run_query(q_price)['close'][0]
  80. data2.loc[t,'close'] = price
  81. #收盘价,旧
  82. q_price = query(opt.OPT_DAILY_PRICE.code,
  83. opt.OPT_DAILY_PRICE.date,
  84. opt.OPT_DAILY_PRICE.close,
  85. ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
  86. opt.OPT_DAILY_PRICE.date==t)
  87. price = opt.run_query(q_price)['close'][0]
  88. data2.loc[t,'last_close'] = price
  89. last_contract = data2.loc[t,'holding_contract']
  90. return(data2)
  91. #平仓期权收益
  92. def optionProfit(data2,fee=5):
  93. #计算平仓期权的收益
  94. opt_ret2 = pd.Series(0,index=data2.index)
  95. pre_close2 = data2['close'].iloc[0]
  96. for t in data2.index[1:]:
  97. if data2.isna().loc[t,'last_close']: #未换仓,last为空
  98. opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
  99. else:
  100. opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - fee #手续费
  101. pre_close2 = data2.loc[t,'close']
  102. return(opt_ret2)
  103. #获得指定行权价以及到期日的合约
  104. def getContractForPrice(exercise_price,enddate,contratType='PO'):
  105. q_contract_info = query(opt.OPT_CONTRACT_INFO.code,
  106. opt.OPT_CONTRACT_INFO.trading_code,
  107. opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
  108. opt.OPT_CONTRACT_INFO.exercise_price,
  109. opt.OPT_CONTRACT_INFO.last_trade_date,
  110. opt.OPT_CONTRACT_INFO.list_date
  111. ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType, #期权类型
  112. opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG', #上交所
  113. opt.OPT_CONTRACT_INFO.last_trade_date == enddate, #到期月
  114. opt.OPT_CONTRACT_INFO.exercise_price == exercise_price) #指定行权价
  115. contract_info = opt.run_query(q_contract_info)
  116. return(contract_info)
  117. #获取交易时间和时间间隔(频率:月)
  118. #根据不同交易日分割月份
  119. #指定回测的起始时间
  120. starttime = '2020-01-01'
  121. endtime = '2024-03-01'
  122. trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
  123. trade_days.index = pd.to_datetime(trade_days.index)
  124. last_day = get_last_day_of_month(endtime)
  125. month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime(last_day)]
  126. month_split
  127. ##持仓情况
  128. holding_contract_CO = pd.Series(index=trade_days.index)
  129. holding_contract_PO = pd.Series(index=trade_days.index)
  130. #获取首个持仓认购合约
  131. contract_info_CO = getContract(month_split[0],month_split[1],trade_days.index[0],'CO')
  132. holding_contract_CO[trade_days.index[0]] = contract_info_CO['code'].iloc[0]
  133. #获取首个持仓认沽合约
  134. contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
  135. holding_contract_PO[trade_days.index[0]] = contract_info_PO['code'].iloc[0]
  136. newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
  137. newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
  138. # 循环访问每一个交易日,判断交易情况
  139. # 规则:同时卖出认购期权以及同一到期日的和同一行权价的认沽期权,待行权价低于现价的95%时
  140. # 平仓原期权合约,重新卖出认沽以及认购期权;到期前1天移仓换月至次月合约
  141. erro_date = []
  142. for t in trade_days.index[1:]:
  143. #到期前一天
  144. if t >= pd.to_datetime(get_trade_days(end_date=pd.to_datetime(newest_expire_date),count=2)[0]):
  145. #寻找month_idx
  146. for month_idx in range(len(month_split)):
  147. if month_split[month_idx] >= t:
  148. break
  149. if month_idx == len(month_split) -1:
  150. lasttradeday = t
  151. break
  152. else:
  153. lasttradeday = ''
  154. contract_info_CO = getContract(month_split[month_idx],month_split[month_idx+1],t,'CO')
  155. contract_info_PO = contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
  156. if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
  157. holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
  158. holding_contract_PO[t] = contract_info_PO['code'].iloc[0]
  159. newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
  160. newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
  161. else:
  162. #获取昨日50etf收盘价
  163. pre_cls = get_price('510050.XSHG',t,t,fields=['pre_close']).values[0][0]
  164. if pre_cls*0.95 >= newest_exercise_price: #原虚值变为实值,重新开仓略虚值期权
  165. #寻找month_idx
  166. for month_idx in range(len(month_split)):
  167. if month_split[month_idx] >= t:
  168. break
  169. contract_info_CO = getContract(month_split[month_idx-1],month_split[month_idx],t,'CO')
  170. contract_info_PO = contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
  171. if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
  172. holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
  173. holding_contract_PO[t] = contract_info_PO['code'].iloc[0]
  174. newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
  175. newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
  176. holding_contract_CO = holding_contract_CO.fillna(method='ffill')
  177. holding_contract_PO = holding_contract_PO.fillna(method='ffill')
  178. if lasttradeday != '':
  179. holding_contract_CO = holding_contract_CO[holding_contract_CO.index < lasttradeday]
  180. holding_contract_PO = holding_contract_PO[holding_contract_PO.index < lasttradeday]
  181. data_CO = contractChange(holding_contract_CO)
  182. data_PO = contractChange(holding_contract_PO)
  183. #计算期权的收益
  184. opt_ret_CO = optionProfit(data_CO)
  185. opt_ret_PO = optionProfit(data_PO)
  186. opt_ret = opt_ret_CO + opt_ret_PO
  187. opt_ret
  188. #计算现货收益
  189. etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
  190. etf_ret = 10000*etf_price.diff(1).fillna(0)
  191. etf_ret
  192. #计算净值
  193. init_asset2 = etf_price.iloc[0]*10000
  194. ass2 = init_asset2 + opt_ret.cumsum()
  195. pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
  196. pfl_nv2 = (1 + pfl_ret2).cumprod()
  197. pfl_nv2
  198. #绘制净值图
  199. plt.figure(figsize=(8, 5))
  200. plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
  201. plt.plot(pfl_nv2, label='卖出跨式策略净值')
  202. plt.legend(loc='upper left', fontsize='large')
  203. plt.xlabel('时间',size=12)
  204. plt.ylabel('净值',size=12)
  205. plt.show()