「SQL实战」上市公司财务数据分析之“业绩预测交易策略”

前面的文章我给大家做了“Python抓取上市公司历年财务数据”的主题实战讲解,跟着我的原创文章实操完后,我们就成功将财务数据抓取存储到了MySQL数据库中。接下来你可能会开始有疑惑,“我拿到这些财务数据能做什么用呢?”。因此今天我们使用财务数据来做一个交易实战,通过我之前做过的一个量化交易策略“业绩预测交易策略”,来将财务数据快速用起来,真正对我们参与实盘交易提供帮助,实现套利。

本文主要用到的技术是SQL数据库语言,并将从三个方面依次展开介绍:1、业绩预告披露期;2、历史业绩统计;3、中报业绩增长率预测。如果还没有做财务数据抓取的朋友,可以先去看看我前面发的数据抓取文章,先完成基础数据的准备工作。

一、业绩预告披露期

说到业绩预测,老手应该都知道上市公司业绩披露的时间周期,包括年报、半年报,以及一季度报、三季度报,对应的正式披露的时间为每年1月1日-4月30日、7月1日-8月30日,以及每年4月1日-4月30日、10月1日-10月31日。上市公司在业绩报披露的时候,二级市场都会有对应的体现。

而我们通过历史规律会发现,往往在正式的业绩报出来之前,大部分上市企业会有业绩预告,而一般在业绩预告时‬二级市场‬就会进行炒作。如下图的某消费电子上市公司,在今年4月27日披露半年报预告后,开启的连续炒作情况:


而上市公司的业绩预告‬时间也是有规定的,年报预告在1月31日前,其他季度报预告在季度结束的15日前完成披露,也就是4月15日、7月15日、10月15日。

因此,我们一般可以在该截止时间的前一个月开始对上市公司的业绩进行预测,并结合公司的二级市场‬状态‬和业绩预告情况来制定交易策略。现在已经进入了6月份,上市公司的中报预告也开始逐渐陆续地披露了,想抓住这波业绩炒作的朋友们,我们抓紧开始本次交易策略实战吧!下面的业绩分析和增长率预测我将以归属母公司净利润为例来讲解,营业收入、每股收益等其他指标的计算方法是类似的。

二、历史业绩统计

第一步:创建季度数据存储的宽表

为了统计方便,我们将我们需要的数据转换导入到季度业绩表中,因此,先在ssgssj库中创建表code_season_profit,建表SQL如下:

CREATE TABLE `code_season_profit` (
  `type` int(4) NOT NULL COMMENT '类型,1代表利润(归属净利润)、2代表收入、3代表收益(每股收益)',
  `code` int(11) NOT NULL,
  `nyear` int(8) NOT NULL,
  `season1` float DEFAULT NULL,
  `season2` float DEFAULT NULL,
  `season3` float DEFAULT NULL,
  `season4` float DEFAULT NULL,
  `uptime` datetime DEFAULT NULL,
  PRIMARY KEY (`type`,`code`,`nyear`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

第二步:将业绩数据导入到季度业绩表

我们之前python抓取财务数据写入库后,利润数据存储在了profit_statement表中的Parent_owner_net_margin_affi字段中。并且报告期20210331代表一季度报,20210630代表半年报,20210690代表前三季度报,20211231代表年报,因此,我们通过如下SQL来插入数据到code_season_profit表中:

INSERT INTO code_season_profit
SELECT
  1 AS `type`,
  code,
  nyear,
  s1 AS season1,
  (CASE WHEN s1s2 is not null THEN s1s2 - s1 ELSE null END) AS season2,
  (CASE WHEN s1s2s3 is not null THEN s1s2s3 - s1s2 ELSE null END) AS season3,
  (CASE WHEN s1s2s3s4 is not null THEN s1s2s3s4 - s1s2s3 ELSE null END) AS season4,
  CURRENT_TIMESTAMP AS uptime
FROM (
  SELECT
    e.e_id AS code,
    substr(ps.report_date, 1, 4) AS nyear,
    MAX(CASE WHEN substr(ps.report_date, 5, 8) = '0331'
        THEN ps.Parent_owner_net_margin_affi END) as s1,
    MAX(CASE WHEN substr(ps.report_date, 5, 8) = '0630'
        THEN ps.Parent_owner_net_margin_affi END) as s1s2,
    MAX(CASE WHEN substr(ps.report_date, 5, 8) = '0930'
        THEN ps.Parent_owner_net_margin_affi END) as s1s2s3,
    MAX(CASE WHEN substr(ps.report_date, 5, 8) = '1231'
        THEN ps.Parent_owner_net_margin_affi END) as s1s2s3s4
  FROM ssgssj.profit_statement ps
  LEFT JOIN enterprise e ON e.id = ps.enterpriseID
  GROUP BY code, nyear
) season_data

其实现逻辑为:

1)先通过对利润表和公司简介表进行联表(LEFT JOIN)后,按公司股票代码e_id(别名code)和年份(报告期求子串提取而来,别名nyear)进行分组(GROUP BY),以将四个报告期的数据拆分成多列s1、s1s2、s1s2s3、s1s2s3s4;

2)对该子查询的结果进行计算(“后一报告期数据”减去“前一报告期数据”)处理得到每个季度的利润值;

3)最后使用INSERT INTO ... SELECT语法,将查询结果插入到表code_season_profit中。

该SQL语句除了基础的SELECT、INSERT使用外,还涉及到以下几点语法知识:

1、SQL子查询语法,即可以将SELECT查询结果作为临时表,再进行下一层SELECT查询,临时表必须设置一个别名(如上述SQL中设置的season_data),设置别名可用AS...,也可以不带AS关键字;

2、SQL的LEFT JOIN语法,JOIN的LEFT JOIN语法会以左侧表为主表,通过ON关键字后面的关联字段来进行匹配。若右侧表有一个记录或无记录匹配时,则会拼接成一条记录(无匹配记录时,拼接的记录值都为null);若有多条记录匹配,则会变成多条记录。

3.GROUP BY为分组聚合语法,后面指定分组字段,聚合结果会将分组后相同的记录合并成一条记录。

4.MAX函数为聚合函数,会将同一分组内的语句进行求最大值处理;

5.CASE为条件判断语句,CASE WHEN ... THEN ... ELSE ...END为一组条件判断,意思为如果符合...则赋值为...否则赋值为...。其中WHEN...THEN...可以连续出现,即支持多个条件的判断。最后的ELSE...可以没有,若没有则代表前面条件都不匹配时则赋值为null;

6.type为关键字,使用``扩起来,代表其为别名,而非关键字。

第三步:SQL统计今年一季度业绩为正的季度数据

我们将导入到code_season_profit表中的净利润数据进行统计分析,由于上市公司半年报都会跟同期业绩进行对比,得出增长幅度,因此我们先统计处今年(2022)和去年(2021)的季度数据情况。并筛选今年一季度净利润为正的公司,因为我们假设公司二季度业绩若继续保持一季度态势,那么一季度净利润为正的公司,其半年报净利润才更可能为正值。查询SQL如下:

SELECT
  p2022.code AS code,
	p2022.season1 AS season_cmp_2022,
  p2021.season_cmp AS season_cmp_2021,
  p2021.season_avg AS season_avg_2021,
  p2021.season_total AS season_total_2021,
  p2021.season_max AS season_max_2021
FROM sdata.code_season_profit p2022
LEFT JOIN (
  SELECT
    code,
    season1 AS season_cmp,
    (season1+season2+season3+season4) AS season_total,
    (season1+season2+season3+season4)/4 AS season_avg,
		GREATEST(season1,season2,season3,season4) AS season_max
	FROM code_season_profit
  WHERE `type`=1 AND nyear=2021
) p2021 ON p2021.code=p2022.code
WHERE p2022.`type`=1 AND p2022.nyear=2022 AND p2022.season1 > 0

以上SQL语句计算了去年季度数据的总体、平均、最大值情况,为进一步和今年一季度数据的比较作准备。

第四步:SQL统计今年一季度同比增长的企业

我们将上一步的SQL作为子查询,设置别名为season_year,再做进一步比较计算,查询SQL如下:

SELECT 
  code,
  (season_cmp_2022 - season_cmp_2021) AS season1_incr,
	(season_cmp_2022/1 - season_avg_2021) AS season1_avg_incr,
	(season_cmp_2022/1 - season_max_2021) AS season1_max_incr,
  (season_cmp_2022 - season_cmp_2021)/season_cmp_2021 AS season1_incr_rate,
	(season_cmp_2022/1 - season_avg_2021)/season_avg_2021 AS season1_avg_incr_rate,
	(season_cmp_2022/1 - season_max_2021)/season_max_2021 AS season1_max_incr_rate,
  (season_cmp_2022/1 - season_avg_2021 + next_season_space)/season_avg_2021 AS season1_expect_incr_rate
FROM (
  SELECT
    p2022.code AS code,
    p2022.season1 AS season_cmp_2022,
    p2021.season_cmp AS season_cmp_2021,
    p2021.season_avg AS season_avg_2021,
    p2021.season_total AS season_total_2021,
    p2021.season_max AS season_max_2021,
    p2021.season_avg_part-p2021.season_avg as next_season_space
  FROM sdata.code_season_profit p2022
  LEFT JOIN (
    SELECT
      code,
      season1 AS season_cmp,
      (season1+season2) AS season_total,
      (season1+season2)/2 AS season_avg,
      GREATEST(season1,season2) AS season_max,
      (season1)/2 AS season_avg_part
    FROM code_season_profit
    WHERE `type`=1 AND nyear=2021
  ) p2021 ON p2021.code=p2022.code
  WHERE p2022.`type`=1 AND p2022.nyear=2022 AND p2022.season1 > 0
) season_year
WHERE (season_cmp_2022 - season_cmp_2021)>0 AND (season_cmp_2022/1 - season_avg_2021) >0 

以上SQL语句对今年已知的季度净利润总和和去年同期总和进行对比,以及今年平均季度净利润和去年同期的平均对比,筛选出今年净利润均为增长的公司。并计算出:

1)已报告季度总净利润的增长量season1_incr;

2)平均净利润增长量season1_avg_incr(由于今年只出一个季度的业绩报告,季度平均业绩其实就等于一季度业绩,但为了SQL的通用型,我把它们进行了区分计算);

3)季度最大净利润增长量season1_max_incr;

4)已报告季度总净利润的增长率season1_incr_rate;

5)平均净利润增长率season1_avg_incr_rate;

6)季度最大净利润增长率season1_max_incr_rate;

7)下个报告期季度无盈利的情况下的平均净利润增长量next_season_space,我们将今年季度净利润平均-去年同期平均(含即将报告季度)+下一报告期无盈利情况的平均净利润增长量,可作为今年中报的净利润比去年同期的增长量预测的参考。【好好理解下~】

通过对今年已报告的季度数据和去年同期的季度数据进行对比计算,我们就能得出上市公司历史业绩的情况了。

三、中报业绩增长率预测

我们对公司做业绩预测的核心数据依据就是根据今年已报告季度的业绩和去年的报告的季度业绩进行对比计算,再通过假定下一报告期季度的业绩保持稳增长的趋势情况下,对业绩的同比增长率进行预测计算。

因此,我们对上市公司的中报净利润增长率进行预测计算,查询计算的SQL如下:

SELECT
  sr.code,
  e.ename as codename,
  (sr.season1_incr_rate+sr.season1_avg_incr_rate/4+sr.season1_max_incr_rate*4) AS factor,
  sr.season1_expect_incr_rate,
  sr.season1_incr_rate,
  sr.season1_avg_incr_rate,
  sr.season1_max_incr_rate
FROM (
  SELECT 
    code,
    (season_cmp_2022 - season_cmp_2021) AS season1_incr,
    (season_cmp_2022/1 - season_avg_2021) AS season1_avg_incr,
    (season_cmp_2022/1 - season_max_2021) AS season1_max_incr,
    (season_cmp_2022 - season_cmp_2021)/season_cmp_2021 AS season1_incr_rate,
    (season_cmp_2022/1 - season_avg_2021)/season_avg_2021 AS season1_avg_incr_rate,
    (season_cmp_2022/1 - season_max_2021)/season_max_2021 AS season1_max_incr_rate,
    (season_cmp_2022/1 - season_avg_2021 + next_season_space)/season_avg_2021 AS season1_expect_incr_rate
  FROM (
    SELECT
      p2022.code AS code,
      p2022.season1 AS season_cmp_2022,
      p2021.season_cmp AS season_cmp_2021,
      p2021.season_avg AS season_avg_2021,
      p2021.season_total AS season_total_2021,
      p2021.season_max AS season_max_2021,
      p2021.season_avg_part-p2021.season_avg as next_season_space
    FROM sdata.code_season_profit p2022
    LEFT JOIN (
      SELECT
        code,
        season1 AS season_cmp,
        (season1+season2) AS season_total,
        (season1+season2)/2 AS season_avg,
        GREATEST(season1,season2) AS season_max,
        (season1)/2 AS season_avg_part
      FROM code_season_profit
      WHERE `type`=1 AND nyear=2021
    ) p2021 ON p2021.code=p2022.code
    WHERE p2022.`type`=1 AND p2022.nyear=2022 AND p2022.season1 > 0
  ) season_year
  WHERE (season_cmp_2022 - season_cmp_2021)>0 AND (season_cmp_2022/1 - season_avg_2021) >0 
) sr
LEFT JOIN enterprise e ON e.e_id=cl.code
WHERE sr.season1_incr_rate>0 AND sr.season1_avg_incr_rate>0 AND sr.season1_max_incr_rate>0
ORDER BY season1_expect_incr_rate DESC

其中:

1)sr为上一步统计SQL的子查询别名,我们基于上一步统计SQL来进行的预测计算;

2)计算结果联表公司简介表(enterprise),将预测结果数据的公司名称也关联查出;

3)筛选已知季度总增长率、平均增长率、最大增长率均为正的上市公司,以排除今年已知季度业绩增长,或增长不稳定的上市公司,同时精减了候选公司的样本数量;

4)我们采用season1_expect_incr_rate作为预测结果的默认排序,另外,factor也可用来作为预测增长率的参考因子,该因子通过已知季度的增长率+平均增长率*1/4+最大增长率*4所得,用来对不同聚合指标采用的不同的权重加权而来。

查询计算出中报业绩增长率排名公司名单之后,我们就可以再结合行业板块、技术走势等相关信息来做进一步筛选和关注了,大大的提升了我们手动筛查的效率。

【注】SQL语句中的“/1”代表除以今年已报告季度数,以计算季度的平均业绩;“/2”代表除以要预测的报告季度数(中报则为2个季度),以计算去年前两个季度的平均季度业绩。这便于以后预测三季度报或年报时,调整这块的SQL计算算式即能实现快速复用。

四、结语

本节通过对财务数据的净利润进行统计分析,对下一报告期的对应去年同期数据和今年已报告期数据进行总量、平均量、最大量的对比计算,由此预测出中报业绩的同比增长率,并进行结果排序,为我们做进一步制定交易策略提供数据依据。

在实际的交易策略应用中,我们还需要对预测结果进一步关联排除消息面有风险的公司,以及已经出过中报预报的公司,技术上可以使用ElasticSearch索引已抓取的公告数据,再使用关键词对消息面数据进行统计查询,结果存储在数据库中。这样就能进一步提高我们的交易策略准确率。

端午节假期要到了,大家可以试试我这个预测方法,有任何疑问或意见建议,欢迎在评论区提出来。下次的文章,我会将我计算出的中报净利润预测结果数据分享出来,以及对应的买卖交易策略和个人建议。感兴趣的朋友请关注我的后续动态,祝大家端午节假期快乐!

举报
评论 0