SPL 量化系列实践:基本面选股
基本面选股条件:
1. 毛利润率不低于30%;
毛利润率=(营业收入-营业成本)/营业收入
2. 净利润率不低于15%;
净利润率=净利润/营业总收入
3. 优化净资产收益率大于等于20%;
优化净资产收益率=经营活动现金流量净额/净资产
4. 优化资产负债率小于50%
优化资产负债率:(负债总额-预收账款)/资产总额
5. 净利润持续增长,每年增长率不低于10%;
净利润增长率=(净利润-前一年净利润)/前一年净利润
6. 净资产持续增长,每年增长率不低于10%;
净资产增长率=(净资产-前一年净资产)/前一年净资产
7. 经营现金流持续增长,每年增长率不低于10%;
经营现金流增长率=(经营活动产生的现金流量净额-前一年经营活动产生的现金流量净额)/ 前一年经营活动产生的现金流量净额
8. 费用率(管理费用和销售费用占毛利润比重)低于40%;
费用率=(管理费用+销售费用)/毛利润
9. 营业利润占利润总额的比重高于70%;
营业利润占利润总额的比重=营业利润/利润总额
10. 销售净利润率大于等于20%;
销售净利润率=净利润/营业收入
11. 营业收入含金量大于1
营业收入含金量=销售商品、提供劳务收到的现金/营业收入
12. 净利润含金量大于1
净利润含金量=经营活动产生的现金流量净额/净利润
13. 货币资金占总资产比重大于等于30%
货币资金占总资产比重=货币资金/总资产
应收款项占营业收入比重=(应收账款+应收票据)/营业收入
15. 优化流动比率大于2
优化流动比率:(流动资产-存货)/(流动负债-预收账款)(好企业较大,大于5,小于1的公司很差)
16. 现金负债总额比率大于等于50%
现金负债总额比率=经营活动产生的现金流量净额/全部负债
17. 分红占净利润比例大于等于30%
分红占净利润比例=分配给普通股股东及限制性股票持有者股利支付的现金/净利润
我们这样定义好公司:
1. 上述17项指标中,前4项必须满足,其余13须至少满足6项。
2. 最近的year_num年内,至少有一般年份满足条件1,而且最近1年必须满足条件1。
我们找到2017年到2023年的好公司,然后以第二年5月1日前最后1个交易日的收盘价买入(signal=1),如果第二年该股票的基本面不满足要求了就以5月1日前的最后一个交易日的收盘价卖出(signal=-1),之所以选择5月1日是因为公司财报最晚的公告日是4月30日。
当空仓且signal=1时,买入(flag=1),买入的股数是200000元能买到的最大股数,如果有仓位且盈利超过inrate或者亏损超过lorate或者signal=-1就以当天的收盘价卖出(flag=-1),卖出股数就是之前买入的股数。
SPL代码
A | B | C | D | |
1 | =to(2018,2024) | |||
2 | =year_num=6 | |||
3 | =money=200000 | |||
4 | =inrate=0.5 | |||
5 | =lorate=0.4 | |||
6 | =file("IncomeStatement/income_statement.csv").cursor@tc() | |||
7 | =A6.select(end_date%10000==1231&&total_revenue&&total_cogs&&ts_code.split(".")(2)!="BJ").fetch@x() | |||
8 | =A7.group(ts_code,end_date).(~.maxp(f_ann_date)).sort(ts_code,end_date) | |||
9 | =file("BalanceSheet/balance_sheet.csv").cursor@tc() | |||
10 | =A9.select(end_date%10000==1231&&ts_code.split(".")(2)!="BJ").fetch@x() | |||
11 | =A10.group(ts_code,end_date).(~.maxp(f_ann_date)).sort(ts_code,end_date) | |||
12 | =file("CashFlowStatement/cash_flow.csv").cursor@tc() | |||
13 | =A12.select(end_date%10000==1231&&ts_code.split(".")(2)!="BJ").fetch@x() | |||
14 | =A13.group(ts_code,end_date).(~.maxp(f_ann_date)).sort(ts_code,end_date) | |||
15 | =create(ts_code,end_date) | |||
16 | for A1 | =A16-1 | ||
17 | =B16-year_num | |||
18 | =A8.select((ed=end_date\10000,ed>=B17&&ed<=B16)) | |||
19 | =B18.group@o(ts_code).select(~.len()==year_num+1) | |||
20 | =A11.select((ed=end_date\10000,ed>=B17&&ed<=B16)) | |||
21 | =B20.group@o(ts_code).select(~.len()==year_num+1) | |||
22 | =A14.select((ed=end_date\10000,ed>=B17&&ed<=B16)) | |||
23 | =B22.group@o(ts_code).select(~.len()==year_num+1) | |||
24 | =join@m(B19:income,ts_code;B21:balance,ts_code;B23:cash,ts_code) | |||
25 | for B24 | =B25.income | ||
26 | =B25.balance | |||
27 | =B25.cash | |||
28 | =C25.new(ts_code, end_date, (rvn=if(revenue,revenue,total_revenue),cst=if(oper_cost,oper_cost,total_cogs),gross=rvn-cst,gross/rvn):gross_margin, n_income/total_revenue:nprofit_margin, (ca=C27(#),ba=C26(#),ca.n_cashflow_act/ba.total_hldr_eqy_inc_min_int):opt_roe, ca.n_cashflow_act/ba.total_liab:money_liab_ratio, n_income/n_income[-1]-1:nprofit_rise, (pre_ba=if(#==1,,C26(#-1)),ba.total_hldr_eqy_inc_min_int/pre_ba.total_hldr_eqy_inc_min_int-1):nassets_rise, (pre_ca=if(#==1,,C27(#-1)),ca.n_cashflow_act/pre_ca.n_cashflow_act-1):ncash_rise, (admin_exp+sell_exp)/gross:exp_ratio, operate_profit/total_profit:op_profit_ratio, n_income/rvn:sale_nincome_ratio, ca.c_fr_sale_sg/rvn:rvn_gold, ca.n_cashflow_act/n_income:nprofit_gold, ba.money_cap/ba.total_assets:money_ratio, (ba.accounts_receiv+ba.notes_receiv)/rvn:rcv_ratio, (ba.total_liab-ba.adv_receipts)/ba.total_assets:opt_liab_ratio, (ba.total_cur_assets-ba.inventories)/(ba.total_cur_liab-ba.adv_receipts):opt_current_ratio, ca.c_pay_dist_dpcp_int_exp/n_income:div_ratio ).to(2,) |
|||
29 | =C28.fname().to(3,) | |||
30 | [>=0.3,>=0.15,>=0.2,<0.5,>=0.1,>=0.1,>=0.1,<0.4,>0.7,>=0.2,>1,>1,>=0.3,<0.05,>2,>=0.5,>=0.3] | |||
31 | =C29.(~/C30(#)).concat@c() | |||
32 | =C28.((cond=[${C31}],cond.to(4).id()==[true]&&cond.to(5,).count(~)>=C30.len()\2)) | |||
33 | =C32.count(~) | |||
34 | if C33>=year_num\2&&C32.m(-1) | |||
35 | =rcd=C28.m(-1),[rcd.ts_code,rcd.end_date] | |||
36 | =A15.record(D35) | |||
37 | =A15.group(ts_code) | |||
38 | for A37 | =filename@n(A38.ts_code)/".csv" | ||
39 | =A38.(end_date) | |||
40 | =file("daily/"/B38).import@tc() | |||
41 | =B40.derive(if(#>1, close/ pre_close *factor[-1], close/pre_close):factor) | |||
42 | =hfq_fst=B41(1),B41.derive(round(factor/hfq_fst.factor*hfq_fst.close,2): hfq_close,0:signal) | |||
43 | =B39.((s=(~\10000+1)*10000,e=(~\10000+1)*10000,[s+430,e+10430])).select(~(2)>B40(1).trade_date) | |||
44 | =B43.((s=~(1),e=~(2),[B40.pselect@zb(if(trade_date<=s,0,1):0),B40.pselect@bz1(if(trade_date<=e,0,1):0)])) | |||
45 | =s=B44.(~(1)).select(~&&B42(~).trade_date\100==B43(#)(1)\100),B42.calc(s,signal=1) | |||
46 | =edate=B42.m(-1).trade_date,e=B44(B43.pselect@a(~[1](1)\10000!=~(2)\10000&&~(2)<=edate)).(~(2)),B42.calc(e,signal=-1) | |||
47 | =ps=0,bprice=0,bshare=0,B42.derive(if(ps==0&&signal==1,(ps=1,bprice=hfq_close,1),if(ps!=0&&(signal==-1||hfq_close/bprice>1+inrate||hfq_close/bprice<1-lorate),(ps=0,-1),0)):flag,if(flag==1,bshare=floor(money\close,-2),if(flag==-1,bshare,0)):shares) | |||
48 | =B47.select(flag!=0) | |||
49 | =@|B48 | |||
50 | =B49.sort(trade_date) |
A2~A5:策略参数,分别是:需要观察的历史年报数、买入时的最大钱数、卖出时的收益阈值、卖出时的亏损阈值。
A6~A14:读取并过滤财报数据,选择最后公告的财报,利润表、资产负债表和现金流量表。
利润表:
资产负债表:
现金流量表:
A15:存储筛选到好公司和年份。
A16代码块:按好公司标准筛选2017年到2023年中每一年的好公司,结果存入A15的序表中。
其中B18~B23筛选出指定年份之前his_fin_num+1个年份(因为要算净利润增长率等所以要多一个年份)的3张报表数据。
B24:按年份和ts_code关联3张报表。
C28:计算需要的财务指标。
C32:看某支股票是否满足某一年是好公司的条件。
C34:选出好公司,将ts_code和end_date存入A15。
下图是A16代码块筛选出的好公司和年份:
A38代码块:循环每支股票,计算出买卖点和买卖股数。
其中B41~B42计算后复权收盘价。
B43~B46:确定买入信号signal,
B47:确定flag和shares。
A50:结果按trade_date排序。
有了A50的数据,再调用回测接口即可得到回测收益率图和回测指标。
SPL提供了一些高效算法,可以大幅提高计算效率,比如B19中的group@o(),如果数据有序,用@o()选项可以避免哈希的复杂过程,只要比较相邻的数据即可,使分组效率更高;还有B44中的pselect@b(),@b选项可以在数据有序时,用二分法迅速找到所需数据。当然还有其他的高效算法,详细的可以阅读《性能优化》相关内容。
回测收益率图:
红色圆点是买入点,绿色方点是卖出点。(因为买入点往往是其他股票的卖出点,所以会被覆盖掉,只显示了绿色方框)。
回测指标:
indicators | value |
累计收益率(cumulative rate of return) | 48.18% |
年化收益率(annualized rate of return) | 7.02% |
年化波动率(annual volatility) | 12.6% |
夏普比率(sharpe ratio) | 0.32 |
最大回撤(maximum drawdown) | 12.05% |
投入现金(cash invested) | 3692818.22 |
总资产(total assets) | 5472175.22 |
仓位占比(stock holding ratio) | 84.43% |
盈利次数(profit times) | 28 |
亏损次数(losses times) | 21 |
股票的文件有么?
我附上了部分财报数据和部分股票的行情数据,可以下载实验一下,因为数据量小,财报数据筛选条件要放宽松一些,否则找不到符合要求的股票。如果想实验全量数据,请到 Tushare 官网(https://tushare.pro/document/2)
下载全量的财报数据和行情数据。