##时效
实际回测约10秒钟/交易日,对应一年的回测约2500秒,也即40分钟。
##待完善
现在的回测代码未考虑债券赎回的情况,比如:当晚发布公告,次日强制赎回,现在的代码是未考虑的,感兴趣的可以进行优化。比如:[核能转债(SH113026)赎回公告](https://xueqiu.com/S/SH113026/213330303)
##数据来源
从聚宽自带的三个表抓取双低数据:
- 一 是可转债每日收盘价CONBOND_DAILY_PRICE
- 二是可转债每日转股溢价=可转债收盘价-(100/转股价格)*正股收盘价),再除以(100/转股价格)*正股收盘价 得到溢价率,需要从CONBOND_CONVERT_PRICE_ADJUST抓取转股价,然后用Get_price得到股票价格,计算得到溢价率。
- 双低=转债价格+溢价率*100
## 代码借鉴
[《可转债双低轮动回测2.0 年化高达101.00% 》](https://www.joinquant.com/view/community/detail/a020e88dfa9da317d9e51c655f2d6618?type=1)
作者:行于非道
和
[《可转债双低策略》](https://www.joinquant.com/view/community/detail/6bb33a631f5bc1d6a0b2e4eae8f1bcc0?page=1#212412)
作者:周喆颋
**构建双低函数,最后输出df_final为按照双低值升序排列的转债。**
## 双低函数
```
# 可转债双低函数
def run_cvt_bond(date):
run_date=date.strftime("%Y-%m-%d")
# 获取指定日期可转债的收盘价
df1=bond.run_query(query(
bond.CONBOND_DAILY_PRICE.date,
bond.CONBOND_DAILY_PRICE.code,
bond.CONBOND_DAILY_PRICE.name,
bond.CONBOND_DAILY_PRICE.open,
bond.CONBOND_DAILY_PRICE.close
).filter(
bond.CONBOND_DAILY_PRICE.date==run_date,
).order_by(bond.CONBOND_DAILY_PRICE.code.asc()
))
# 可转债代码列表
code_list=list(df1['code'])
if df1.empty:
df_final = pd.DataFrame()
else:
# 获取可转载对应的股票代码
df2=bond.run_query(query(
bond.CONBOND_BASIC_INFO.code,
bond.CONBOND_BASIC_INFO.company_code,
bond.CONBOND_BASIC_INFO.convert_price
).filter(bond.CONBOND_BASIC_INFO.code.in_(code_list)))
# 获取转债对应的股票清单(存在多只转债对应一只股票的情形)
security_list=list(df2['company_code'])
# 获取转载对应股票的价格
df3=get_price(security_list, start_date=run_date, end_date=run_date, frequency='daily', fields=['close'], skip_paused=False, fq='pre', panel=False)
df3.rename(columns={'code':'company_code','close':'stk_close'}, inplace = True)
# 定义转股价格函数,获取可转债在某日期的转股价格
def cb_cv_price(cb_code,stock_date):
df=bond.run_query(query(bond.CONBOND_CONVERT_PRICE_ADJUST.code,
bond.CONBOND_CONVERT_PRICE_ADJUST.adjust_date,
bond.CONBOND_CONVERT_PRICE_ADJUST.new_convert_price,
).filter
(bond.CONBOND_CONVERT_PRICE_ADJUST.code==cb_code)
)
#可能依然是数据的问题,存在temp为空的情况
df_temp=df[df.adjust_date.map(lambda x:x.strftime("%Y-%m-%d")< =stock_date)]
#如果CONBOND_CONVERT_PRICE_ADJUST表无法得到指定日期的转股价,则从原始info表抓取
if not df_temp.empty:
cb_cv_price = df_temp.tail(1).reset_index(drop=True).loc[0, 'new_convert_price']
else:
cb_cv_price = float(df2[df2['code']==cb_code]['convert_price'])
return cb_cv_price
cv_price_lst=[]
# 获取转股价List
for code in code_list:
cv_price=cb_cv_price(code,run_date)
cv_price_lst.append(cv_price)
# 合并三张dataframe
df_mrg=pd.merge(df1, df2, how='left', on=['code'])
df_mrg=pd.merge(df_mrg,df3, how='left', on=['company_code'])
df_mrg=df_mrg.assign(cv_price=cv_price_lst)
# 计算双低值,并合并到现有df_mrg中
premium_ratio_lst=(df_mrg['close']-100/df_mrg['cv_price']*df_mrg['stk_close'])/ \
(100/df_mrg['cv_price']*df_mrg['stk_close'])*100
double_low_lst=df_mrg['close']+premium_ratio_lst
df_final=df_mrg.assign(premium_ratio=premium_ratio_lst,double_low=double_low_lst)
# 仅保留开盘价、收盘价都是正的可转债,部分转载在退市当天依然有数据
df_final=df_final[df_final['open']>0]
df_final=df_final[df_final['close']>0]
# 剔除双低为负数的转债
df_final=df_final[df_final['double_low']>0]
df_final=df_final.sort_values(by='double_low')
return df_final
```
## 实盘回测函数
```
from jqdata import bond
import datetime
import pandas as pd
import matplotlib.pyplot as plt
# 可转债交易部分
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
# 初始资金
initial_cash=100000
# 当前持仓记录
stock = pd.DataFrame(data=None, index=None, columns=['code', 'qty', 'cost_price', 'amount', 'price'])
# 交易记录
trade_log = pd.DataFrame(data=None, index=None, columns=['date', 'code', 'qty', 'price', 'amount', 'profit','cash'])
# 每日市值记录
stock_log = pd.DataFrame(data=None, index=None, columns=['date', 'stock_amount', 'cash' , 'profit'])
# 存在不少数据问题,暂时归集到这里
debug_log = pd.DataFrame(data=None, index=None, columns=['date', 'code', 'qty', 'price', 'amount', 'profit','cash'])
# 交易起止日期
begin = pd.to_datetime('20220101')
end = pd.to_datetime('20220418')
# 可用资金
cash = initial_cash
# 自然日天数
days = 0
print('开始日期:', begin.strftime("%Y-%m-%d"))
print('结束日期:', end.strftime("%Y-%m-%d"))
# 目标持仓转债数量
hold_num=10
# 初始赋值
run_date=begin
while run_date < = end:
print('正在计算:',run_date.strftime("%Y-%m-%d") , end = '' )
print('\r',end='')
days = days+1
# 节假日无可转债数据
# 获取前一天转载的双低清单
cvt_bond_list = run_cvt_bond(run_date)
hold_df = cvt_bond_list[0:9]
if cvt_bond_list.empty:
# 当日非交易日,日期加一天
run_date=run_date + datetime.timedelta(days=1)
# 跳出今日交易
continue
# 开盘卖出非目标可转债
for code in stock['code']:
if code not in list(hold_df['code']):
stock_code = code
cost_price = float(stock[stock['code']==code]['cost_price'])
stock_qty = float(stock[stock['code']==code]['qty'])
# 转债开盘价卖出
# 卖出价要在大的可转债表里面查询
sell_temp = cvt_bond_list[cvt_bond_list['code']==code]
if not sell_temp.empty:
sell_price = float(cvt_bond_list[cvt_bond_list['code']==code]['open'])
else:
sell_price = cost_price
debug_log.loc[len(debug_log)] =[run_date, stock_code, stock_qty * -1, sell_price,
amount * 1, profit ,cash]
amount = sell_price * stock_qty
profit = (sell_price - cost_price)* stock_qty
cash = cash + amount
# 删除已经卖出的债券
stock = stock.drop(stock[stock['code'] == code].index)
# 重置持仓索引
stock = stock.reset_index(drop=True)
trade_log.loc[len(trade_log)] = [run_date, stock_code, stock_qty * -1, sell_price,
amount * 1, profit ,cash]
# 拟加仓的可转债数量
buy_num=hold_num-len(stock)
# 拟购买转债的金额
if buy_num>0:
buy_cash= cash/buy_num
# print(buy_cash)
else:
buy_cash=0
# 买入目标可转债
for code in hold_df['code']:
if code not in list(stock['code']):
stock_code = code
# 转债开盘价买入
cost_price = float(hold_df[hold_df['code']==code]['open'])
stock_qty = round(buy_cash/cost_price/10,0)*10
sell_price = cost_price
amount = cost_price * stock_qty
profit = 0
cash = cash - amount
stock.loc[len(stock)]=[stock_code,stock_qty,cost_price,amount,sell_price]
# 添加交易记录
trade_log.loc[len(trade_log)] = [run_date, stock_code, stock_qty * 1, sell_price,
amount * (-1), profit ,cash]
# print(stock)
# 记录每日持仓市值和收益,方便画图
sum_amount = round(stock['amount'].sum(), 2)
stock_log.loc[len(stock_log)] = [run_date, sum_amount, cash , float(sum_amount + cash)]
run_date = run_date + datetime.timedelta(days=1)
# 交易循环结束
# 记录交易日志
trade_log.to_csv("trade_log.csv")
stock_log.to_csv("stock_log.csv")
debug_log.to_csv("debug_log.csv")
stock.to_csv("stock.csv")
print('==============代码运行结束===============')
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
# 计算最终持仓市值
last_amount = round(stock['amount'].sum(), 2)
profit = round(cash + last_amount, 2)
print('总天数:', days)
print('初始资产:', initial_cash)
print('最终资产:', profit)
print('收益率:%.2f%%' % ((profit - initial_cash) / initial_cash * 100))
yy = days / 365
year_profit = ((profit / initial_cash) ** (1 / yy) - 1) * 100
print('年化:%.2f%%' % year_profit)
#设置画布大小,默认画布序号为零
plt.figure(figsize=(16,8))
plt.plot(stock_log['date'], stock_log['profit'])
plt.show()
```
经回测,2022年1月1日至2022年4月19日,收益率为-14%。
感兴趣的可以进行进一步优化。
@binsente1987 点击策略研究-研究环境,新建Python3,把上面的代码复制进去,即可运行。但一创不支持可转债实盘,感兴趣的可以本地跑了,然后实盘手工操作。当然更简单的,去集思录上按照双低升序排列就有了。
我这里主要考虑回测用。
2022-04-20
补充一个可能的bug
可转债CONBOND_BASIC_INFO数据不全
运行如下代码
```
code_list=['111002','128100']
df2=bond.run_query(query(
bond.CONBOND_BASIC_INFO.code,
bond.CONBOND_BASIC_INFO.short_name,
bond.CONBOND_BASIC_INFO.issuer,
bond.CONBOND_BASIC_INFO.company_code,
).filter(bond.CONBOND_BASIC_INFO.code.in_(code_list)))
print(df2)
write_file('df2.csv',df2.to_csv())
```
得到的结果如下:
```
code short_name issuer company_code
0 111002 特纸转债 五洲特种纸业集团股份有限公司 None
1 128100 搜特转债 搜于特集团股份有限公司 002503.XSHE
```
有发现没??111002查不到这家转债对应公司的代码。
数据缺失。。。我发帖的时候都没问题,今天测试发现问题。
这就意味着我通过df1,去匹配df2,的时候股票代码缺失,造成后面的df_final是有问题的。
大家可以看看。
2022-04-20
@wangww 很神奇的事情,我现在跑,又有数据了。
code short_name issuer company_code
0 111002 特纸转债 五洲特种纸业集团股份有限公司 605007.XSHG
1 128100 搜特转债 搜于特集团股份有限公司 002503.XSHE
2022-04-21
@wangww 集思录上按照双低升序排列,这个收费的吧?
2022-04-21
2022-04-21 23:38:39
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
< ipython-input-3-9cf78fa75a8a> in < module>
5 initial_cash=100000
6 # 当前持仓记录
----> 7 stock = pd.DataFrame(data=None, index=None, columns=['code', 'qty', 'cost_price', 'amount', 'price'])
8 # 交易记录
9 trade_log = pd.DataFrame(data=None, index=None, columns=['date', 'code', 'qty', 'price', 'amount', 'profit','cash'])
NameError: name 'pd' is not defined
直接复制到 py3 研究,提示报错,什么原因呢?
2022-04-21
@美吉姆优秀毕业代表 抱歉,发现代码不齐整,在交易部分加
from jqdata import bond
import datetime
import pandas as pd
import matplotlib.pyplot as plt
正文代码已更新,可以再测试。
2022-04-22
函数回测校验:
```
run_date = pd.to_datetime('20220422')
cvt_bond_list = run_cvt_bond(run_date)
print(cvt_bond_list)
```
结果如下:
```
date code name ... cv_price premium_ratio double_low
361 2022-04-22 128100 搜特转债 ... 1.62 12.282759 112.782759
313 2022-04-22 128013 洪涛转债 ... 2.31 10.190000 120.380000
53 2022-04-22 113044 大秦转债 ... 7.18 12.049029 121.289029
39 2022-04-22 113011 光大转债 ... 3.55 15.310455 122.500455
7 2022-04-22 110053 苏银转债 ... 6.37 3.850561 125.960561
59 2022-04-22 113050 南银转债 ... 10.10 2.399098 126.089098
262 2022-04-22 127003 海印转债 ... 2.99 17.262267 127.465267
28 2022-04-22 110079 杭银转债 ... 12.99 4.804715 128.004715
48 2022-04-22 113033 利群转债 ... 7.01 16.568674 128.148674
383 2022-04-22 128130 景兴转债 ... 3.40 8.179947 129.086947
325 2022-04-22 128034 江银转债 ... 4.32 16.681340 129.581340
226 2022-04-22 123107 温氏转债 ... 17.62 5.212950 129.712950
356 2022-04-22 128087 孚日转债 ... 4.50 15.190000 130.380000
```
我们看下集思录最新的结果,发现两者的结果是一致的。

2022-04-23

请问图片缩到一起了,有办法强制设定大小吗?
2022-04-23
@美吉姆优秀毕业代表 请加一段代码
#设置画布大小,默认画布序号为零,figsize:指定figure的宽和高,单位为英寸
plt.figure(figsize=(16,8))
plt.plot(stock_log['date'], stock_log['profit'])
plt.show()
2022-04-24
请问是直接贴过去研究环境python3就可以运行了吗?我试了没有结果输出呢~
2022-04-24
@wangww 我也发现同样的问题,128012等的日行情数据也有问题,跟jq沟通了下,有兴趣加个微信交流交流吗375361308
2022-04-26
大佬,我用您这个代码回测双底,感觉有问题啊,看看是不是代码哪里有问题。
2022-04-27