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%

货币资金占总资产比重=货币资金/总资产

14. 应收款项占营业收入比重小于等于5%

应收款项占营业收入比重=(应收账款+应收票据)/营业收入

15. 优化流动比率大于2

优化流动比率:(流动资产-存货)/(流动负债-预收账款)(好企业较大,大于5,小于1的公司很差)

16. 现金负债总额比率大于等于50%

现金负债总额比率=经营活动产生的现金流量净额/全部负债

17. 分红占净利润比例大于等于30%

分红占净利润比例=分配给普通股股东及限制性股票持有者股利支付的现金/净利润

我们这样定义好公司:

1. 上述17项指标中,前4项必须满足,其余13须至少满足6项。

2. 最近的year_num年内,至少有一般年份满足条件1,而且最近1年必须满足条件1

我们找到2017年到2023年的好公司,然后以第二年51日前最后1个交易日的收盘价买入(signal=1),如果第二年该股票的基本面不满足要求了就以51日前的最后一个交易日的收盘价卖出(signal=-1),之所以选择51日是因为公司财报最晚的公告日是430日。

当空仓且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_codeend_date存入A15

下图是A16代码块筛选出的好公司和年份:

..

A38代码块:循环每支股票,计算出买卖点和买卖股数。

其中B41~B42计算后复权收盘价。

B43~B46:确定买入信号signal

B47:确定flagshares

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