约翰·邓普顿(John Templeton)是邓普顿集团的创始人,是全球最受尊敬的投资人和最成功的基金经理之一,被福布斯称为“全球投资之父”。 邓普顿以“逆向投资”著称。其最著名的投资案例包括在二战期间借入1万美金购买104只低价股,并在随后几年翻了三倍。邓普顿在1999年互联网泡沫期间大举做空互联网股票。 其投资策略包含了如下列出的几个要点[1]:
- 在数据库中选择P/B 最低的40% 个股。
- P/E 小于过去5年平均水平。
- 过去5年盈利的增长均为正。
- EPS 的增长率高于行业平均。
- 营业利润率(OPM)高于过去5年平均水平。
- 长期负债与权益的比值小于行业平均水平。
- 总资产与总负债的比值高于行业平均水平。
- ROE高于行业平均。
我试图在策略中包含上述所有要点,但是在回测中发现同时满足这些要求的股票非常少,于是我就挑选了其中几个并在过去10年这样一个周期中进行了回测。通过过去10年的回测可以看出,策略的年化收益率在11.88%,超额收益率133.25%,最大回测为60.23%。可以看出相对来说收益还是可以接受的。
再来看一下最近3年策略的收益。我们看到从2020年开始的最近3年策略年化收益率是11.67%,还是相对比较稳定的。超额收益75.17%,沪深300在该时间段内的收益率是-14.30%。
附上完整的聚宽源代码,读者可以自行删除或者添加因子。
# 导入函数库
import jqdata
from jqlib.technical_analysis import *
from jqdata import *
import warnings
from datetime import date
import numpy as np
# 初始化函数
def initialize(context):
# 滑点高(不设置滑点的话用默认的0.00246)
set_slippage(FixedSlippage(0.02))
# 沪深300
set_benchmark('000300.XSHG')
# 用真实价格交易
set_option('use_real_price', True)
set_option("avoid_future_data", True)
# 过滤order中低于error级别的日志
log.set_level('order', 'error')
warnings.filterwarnings("ignore")
# 选股参数
g.stock_num = 10 # 持仓数
g.position = 1 # 仓位
# 手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0005, close_commission=0.0005, min_commission=5), type='stock')
# 设置交易时间
run_monthly(my_trade, monthday=-4, time='14:30', reference_security='000300.XSHG')
# 开盘时运行函数
def my_trade(context):
yesterday = context.previous_date
check_out_list = get_stock_list(context)
log.info('今日自选股:%s' % check_out_list)
adjust_position(context, check_out_list)
# 2-2 选股模块
def get_stock_list(context):
# type: (Context) -> list
curr_data = get_current_data()
yesterday = context.previous_date
year = yesterday.year - 1
df_stocknum = pd.DataFrame(columns=['当前符合条件股票数量'])
# 过滤次新股
by_date = yesterday - datetime.timedelta(days=365*5) # 五年
last_five_dates = [yesterday - datetime.timedelta(days=365), yesterday - datetime.timedelta(days=365*2),
yesterday - datetime.timedelta(days=365*3), yesterday - datetime.timedelta(days=365*4),
yesterday - datetime.timedelta(days=365*5)]
initial_list = get_all_securities(date=by_date).index.tolist()
# 过滤当日涨跌停,st,科创板,停牌
stock_list = filter_limit_stock(context, initial_list)
stock_list = filter_st_stock(stock_list)
stock_list = filter_kcb_stock(stock_list)
stock_list = filter_paused_stock(stock_list)
# 1. 选出PB 最低的40% 个股
q = query(
valuation.code,
valuation.pb_ratio
).filter(valuation.pb_ratio > 0,
valuation.code.in_(stock_list)
).order_by(
valuation.pb_ratio.asc()
)
df = get_fundamentals(q).dropna()
low_pb_ratio_list = list(df.code)[:int(0.4*len(list(df.code)))]
3. 过去五年盈利增长均为正数
df_list=[]
for date in last_five_dates:
df_year= get_fundamentals(query(
indicator.code, indicator.inc_net_profit_year_on_year
).filter(
indicator.code.in_(low_pb_ratio_list)
), date=date).set_index('code')
df_list.append(df_year)
df=pd.concat(df_list,axis=1)
df.columns=['year1', 'year2', 'year3', 'year4', 'year5']
#print(df)
inc_list = df[(df['year1'] > 0) & (df['year2'] > 0) & (df['year3'] > 0) & (df['year4'] > 0)
& (df['year5'] > 0)]
inc_list = inc_list.index.tolist()
# 2. P/E 小于过去五年的平均P/E. swap step 2 and 3 to save computing time
end_d = yesterday
start_d = end_d - datetime.timedelta(days=356*5) # 3 years
df_pe = get_valuation(inc_list, start_date=start_d,
end_date=end_d, fields=['code', 'pe_ratio'], count=None)
df_pe = df_pe.sort_values(by=['day'], ascending=False)
a = df_pe[:len(inc_list)][['code', 'pe_ratio']].set_index('code')
b = df_pe.groupby('code').mean()
a['mean_pe'] = b['pe_ratio']
a = a[a['pe_ratio'] > 0]
low_pe_list = a[a['pe_ratio'] < a['mean_pe']].index.tolist()
# 4. EPS 增长率大于行业平均水平, 这里用净利润增长率代替
eps_list = []
try: # some times it will give zjw keyError, so add this try except block
for stock_code in low_pe_list:
ind_code = get_industry(stock_code)[stock_code]['zjw']['industry_code']
good_list = get_industry_npy_mean(ind_code, yesterday)
if stock_code in good_list:
eps_list.append(stock_code)
except:
eps_list = low_pe_list
eps_list = low_pb_ratio_list
# 5. OPM 高于过去五年的平均OPM, (OPM 为营业利润率)
df_list=[]
for date in last_five_dates:
df_year= get_fundamentals(query(
indicator.code, indicator.operation_profit_to_total_revenue
).filter(
indicator.code.in_(eps_list)
),date=date).set_index('code')
df_list.append(df_year)
df=pd.concat(df_list,axis=1)
df.columns=['year1', 'year2', 'year3', 'year4', 'year5']
df['mean_opm'] = df.mean(axis=1)
opm_list = df[df['year1'] > df['mean_opm']]
opm_list = opm_list.index.tolist()
# 6. 长期负债率和权益的比值小于行业平均水平
non_current_liability_list = []
try:
for stock_code in opm_list:
ind_code = get_industry(stock_code)[stock_code]['zjw']['industry_code']
good_list = get_industry_non_current_liability_mean(ind_code, yesterday)
if stock_code in good_list:
non_current_liability_list.append(stock_code)
except:
non_current_liability_list = opm_list
# 7. 负债率低于行业平均水平
liability_list = []
try:
for stock_code in non_current_liability_list:
ind_code = get_industry(stock_code)[stock_code]['zjw']['industry_code']
good_list = get_industry_liability_mean(ind_code, yesterday)
if stock_code in good_list:
liability_list.append(stock_code)
except:
liability_list = non_current_liability_list
# 8. ROE 高于行业平均水平
roe_list = []
try:
for stock_code in liability_list:
ind_code = get_industry(stock_code)[stock_code]['zjw']['industry_code']
good_list = get_industry_roe_mean(ind_code, yesterday)
if stock_code in good_list:
roe_list.append(stock_code)
except:
log.info('Get roe_list error, use the previous list:%s' % liability_list)
roe_list = liability_list
return roe_list[:g.stock_num]
# 过滤涨跌停股票
def filter_limit_stock(context, stock_list):
current_data = get_current_data()
holdings = list(context.portfolio.positions)
return [stock for stock in stock_list if (stock in holdings) or
(current_data[stock].low_limit < current_data[stock].last_price and
current_data[stock].last_price < current_data[stock].high_limit)]
# 过滤停牌股票
def filter_paused_stock(stock_list):
current_data = get_current_data()
return [stock for stock in stock_list if not current_data[stock].paused]
# 过滤ST及其他具有退市标签的股票
def filter_st_stock(stock_list):
current_data = get_current_data()
return [stock for stock in stock_list if not (
current_data[stock].is_st or
'ST' in current_data[stock].name or
'*' in current_data[stock].name or
'退' in current_data[stock].name)]
# 过滤科创板
def filter_kcb_stock(stock_list):
return [stock for stock in stock_list if not stock.startswith('688')]
#过滤模块-创业板
def filter_cyb_stock(stock_list):
return [stock for stock in stock_list if stock[0:3] != '300']
def adjust_position(context, buy_stocks):
for stock in context.portfolio.positions:
if stock not in buy_stocks:
order_target(stock, 0)
position_count = len(context.portfolio.positions)
if g.stock_num > position_count:
value = context.portfolio.cash * g.position / (g.stock_num - position_count)
for stock in buy_stocks:
if stock not in context.portfolio.positions:
order_target_value(stock, value)
if len(context.portfolio.positions) == g.stock_num:
break
def get_industry_eps_mean(ind_code, date):
stock_list = get_industry_stocks(ind_code)
q = query(
indicator.code, indicator.eps
).filter(
indicator.code.in_(stock_list)
)
df = get_fundamentals(q, date=date).set_index('code')
mean_eps = df['eps'].mean()
filtered_list = df[df['eps'] > mean_eps].index.tolist()
return filtered_list
def get_industry_npy_mean(ind_code, date):
stock_list = get_industry_stocks(ind_code)
q = query(
indicator.code, indicator.inc_net_profit_year_on_year
).filter(
indicator.code.in_(stock_list)
)
df = get_fundamentals(q, date=date).set_index('code')
mean_npy = df['inc_net_profit_year_on_year '].mean()
filtered_list = df[df['inc_net_profit_year_on_year '] > mean_npy].index.tolist()
return filtered_list
def get_industry_roe_mean(ind_code, date):
stock_list = get_industry_stocks(ind_code)
q = query(
indicator.code, indicator.roe
).filter(
indicator.code.in_(stock_list)
)
df = get_fundamentals(q, date=date).set_index('code')
mean_roe = df['roe'].mean()
filtered_list = df[df['roe'] > mean_roe].index.tolist()
return filtered_list
def get_industry_non_current_liability_mean(ind_code, date):
stock_list = get_industry_stocks(ind_code)
q = query(
balance.code, balance.total_non_current_liability, balance.total_owner_equities
).filter(
balance.code.in_(stock_list)
)
df = get_fundamentals(q, date=date).set_index('code')
df['ratio'] = df['total_non_current_liability'] / df['total_owner_equities']
# df = df.sort_values(by = 'ratio',ascending=False)
mean_liability = df['ratio'].mean()
filtered_list = df[df['ratio'] < mean_liability].index.tolist()
return filtered_list
def get_industry_liability_mean(ind_code, date):
stock_list = get_industry_stocks(ind_code)
q = query(
balance.code, balance.total_liability, balance.total_assets
).filter(
balance.code.in_(stock_list)
)
df = get_fundamentals(q, date=date).set_index('code')
df['ratio'] = df['total_liability'] / df['total_assets'] # 资产负债率
# df = df.sort_values(by = 'ratio',ascending=False)
mean_liability = df['ratio'].mean()
filtered_list = df[df['ratio'] < mean_liability].index.tolist()
return filtered_list
参考文献:
[1] Chincarini, L. B. (2006). Quantitative equity portfolio management: An active approach to portfolio construction and management. McGraw-Hill.