目录
目录
如果一个投资标的,可以估值,那太好了。
可转债下有底,上不封顶。
这个底就可以估。
可转债本身是债,但向上走,又是正股的看涨期权。
“双低”策略
这里的双低,是指两个指标,一是价格,二是溢价率。
低价格
可转债一般100块1张,就好比是一个100块的借条,到期往本付息可能是103块。
都着价格的波动,这个借条可能低于100(市场低迷的时候),也可能高于130,甚至更高。
都买入的角度 ,当然价格越便宜越安全。
低溢价率
每张转债里内含几份股票是规定好的,这个张数N = 100/X,X是转股价。比如转股价是20, 那这100块就可以转成5股,或者说“约定好,未来以20块的价格卖入股票”。
而正股的价格每天都在波动,可能高于20,也可能低于20,这个值=Y
转股价值 = N*Y = 100/X * Y,就是若转为股票,这个借条值多少线。
溢价率 = 转债价格/转股价值 -1 。
这么理解:转股价值,有点像这个借条内含的价值,就是我可以转成股票,若转了,值多少钱,比如是108块。但现在这个借条实际标价是110,就是有溢价的, 110/108 -1。你说会不会溢价为负,有可以的,石油价格都可能为负,金融市场一切皆有可能。
溢价率当然越小越好,负的更好。就是说,如果你买了,如果今天就转股,你都是赚的。
策略设计:
1、选择距离可转债到期日期大于100天。
2、计算溢价率并标准化
3、价格排名与溢价率排期按5:5权重,得到综合排名。
4、最后选择前10名构建持仓。
实际上,这是“轮动”策略,就是说并没有说出现在“卖出”的强逻辑,而是“只持有”最划算的,或者评份最高的。
“轮动”是一个很好的思想。我只持有当下“最好的N支”。Qlib里的回测模型默认就是topK。
无论你是什么因子集,最终反正算出一个score,选择score高的集合就好。
数据准备:
可转债的价格是现成的。
需要每天的正股价。
这里有几种实现,把正股价作为转债的一个序列,生成到一个数据库里。
一是看qlib是否支持“跨库计算”,第一种肯定可以,但比较麻烦。第二种是今天调研之重点。
跨库计算没有找到计算的方式。
所以,把正股的收盘价接过下,然后concat,两个dataframe的index都设置为日期,然后使用pandas的concat,axis=1,join='inner'这样模向连接就可以了。
python qlib-main/scripts/dump_bin.py dump_all --csv_path ./cb_quotes --qlib_dir ./data/cb_data --include_fields open,close,high,low,volume,factor,change_price,stock_close --symbol_field_name ts_code --date_field_name trade_date
比传统的OHLCV,多了三例:
factor,change_price,stock_close, 复权因子,转股价,正股价。
回测
qlib的回测与传统量化框架如pyalgotrade和backtrader这种事件驱动不同。
传统框架是事件驱动,每天根据信号写自己的买入或卖出逻辑。
而qlib是给列表打分。
每天每个instrment都有一个分,前K个最高份的持仓(这里有一个逻辑,就是排序,但有可能所有都不取,比如动量策略,要求动量最大的N个,但动量必须为正!)。可以考虑一个变通的方法,就是把动量为负的直接过滤掉,不出现在pre_score里。
这是典型的”轮动“逻辑。
这个框架不太适合: 单instrument的择时。
数据加载及因子计算:
时间序列很多计算都直接可以使用表达式,比如MACD这种。
provider_uri = "./data/cb_data" # "./data/cn_data" # target_dir
qlib.init(provider_uri=provider_uri, region=REG_CN)
instruments = D.instruments(market='all')
fields = ['$close', '$close/(100/$change_price*$stock_close) -1']
#df = D.features(instruments, fields, start_time='2010-01-01', end_time='2018-12-31', freq='day')
#print(df)
fields = ["$close*0.5 + $close/(100/$change_price*$stock_close) -1", ]
names = ['score']
data_loader_config = {
"feature": (fields, names),
#"label": (labels, label_names)
}
data_loader = QlibDataLoader(config=data_loader_config)
df = data_loader.load(instruments='all', start_time='2010-01-01', end_time='2017-12-31')
print(df)
但我们偏偏要计算横截面的排名。
我们看一下当前支持的指标:
ChangeInstrument,
Rolling,
Ref,
Max,
Min,
Sum,
Mean,
Std,
Var,
Skew,
Kurt,
Med,
Mad,
Slope,
Rsquare,
Resi,
Rank,
Quantile,
Count,
EMA,
WMA,
Corr,
Cov,
Delta,
Abs,
Sign,
Log,
Power,
Add,
Sub,
Mul,
Div,
Greater,
Less,
And,
Or,
Not,
Gt,
Ge,
Lt,
Le,
Eq,
Ne,
Mask,
IdxMax,
IdxMin,
If,
Feature,
PFeature,
] + [TResample]
class Rank(Rolling):
#这里的排名是针对时间序列,比如Rank($close,20),过去20天,当前收盘价处于过去20天的分位。
instruments = D.instruments(market='all')
fields = ['$close', '$close/(100/$change_price*$stock_close) -1']
#df = D.features(instruments, fields, start_time='2010-01-01', end_time='2018-12-31', freq='day')
#print(df)
fields = ["$close","$close/(100/$change_price*$stock_close) -1"]
names = ['收盘价','溢价率']
data_loader_config = {
"feature": (fields, names),
#"label": (labels, label_names)
}
data_loader = QlibDataLoader(config=data_loader_config)
df = data_loader.load(instruments='all', start_time='2020-01-01', end_time='2020-01-10')
df = df['feature']
#print(df.columns)
df['rank_close'] = df.groupby(['datetime'],as_index=False)['收盘价'].rank(ascending=False)
df['rank_rate'] = df.groupby(['datetime'], as_index=False)['溢价率'].rank(ascending=False)
df['score'] = df['rank_rate'] * 0.5 + df['rank_close']*0.5
df = df[['score']]
print(df.head(50))
按天把每支转债的score计算出来的,就是价格排期,以及溢价率排名的综合排名。
这两年年化17%,还没有参数调优:
data_loader_config = {
"feature": (fields, names),
# "label": (labels, label_names)
}
start_date = '2021-01-01'
end_date = '2022-08-01'
data_loader = QlibDataLoader(config=data_loader_config)
df = data_loader.load(instruments='all', start_time=start_date, end_time=end_date)
df = df['feature']
# print(df.columns)
df['rank_close'] = df.groupby(['datetime'], as_index=False)['收盘价'].rank(ascending=False)
df['rank_rate'] = df.groupby(['datetime'], as_index=False)['溢价率'].rank(ascending=False)
df['score'] = df['rank_rate'] * 0.5 + df['rank_close'] * 0.5
df = df[['score']]
print(df.head(50))
CSI300_BENCH = "113013.SH"
STRATEGY_CONFIG = {
"topk": 10,
"n_drop": 1,
# pred_score, pd.Series
"signal": df,
}
strategy_obj = TopkDropoutStrategy(**STRATEGY_CONFIG)
report_normal, positions_normal = backtest_daily(
start_time=start_date, end_time=end_date, strategy=strategy_obj, benchmark=CSI300_BENCH
)
analysis = dict()
# default frequency will be daily (i.e. "day")
analysis["excess_return_without_cost"] = risk_analysis(report_normal["return"]) #- report_normal["bench"])
analysis["excess_return_with_cost"] = risk_analysis(
report_normal["return"] # - report_normal["bench"]
- report_normal["cost"]
)
analysis_df = pd.concat(analysis) # type: pd.DataFrame
pprint(analysis_df)