数据分析编程从 SQL 到 SPL:股票指标计算

数据结构和样例数据:

..

其中 sid 是股票代码,tdate 是交易日期,close 是收盘价。

1. 计算每日收盘价的 12 日 EMA

某支股票上市第 1 天的 n 日 EMA 为当日收盘价。

n 日 EMA=(前一日的 n 日 EMA*(n-1)+ 当日收盘价 *2)/(n+1)。

n 为 12 时称为 12 日 EMA,简写为 e12。

现在要计算股票 600036 每个交易日的 12 日 EMA,

写出 SQL 语句:

with recursive tbl as (
    select tdate, close, row_number() over(order by tdate) rn
    from stktrade where sid='600036'),
tbl2 as (
    select tdate,close, cast(close as decimal(22,16)) e12, rn 
    from tbl where rn=1
    union all
    select t1.tdate, t1.close, (t1.close*2+t2.e12*11)/13, t1.rn
    from tbl t1 join tbl2 t2 on t1.rn-1=t2.rn
)
    select * from tbl2;

每日 e12 都要基于上一日的 e12 来计算,SQL 先使用窗口函数人为造出序号,再用递归方式计算,理解、编写以及调试的过程都很困难。

更符合自然思维的解题步骤是这样:将该股票的数据按日期排序,上一条记录就是上一个交易日的数据。增加一个计算列 e12,除第一条记录 e12 等于 close 外,循环用每条记录的收盘价和上一条记录的 e12 计算得出当前行的 e12,即可得到下面的表:

..

当所有记录都遍历过后,就得到期望的结果了。

可是,SQL 对过程式计算的支持力度太弱,不用存储过程很难实现这个思路。

SPL 有和常规程序语言一样的过程式计算能力,可以轻松实现上述思路:


A

1

=T("stock.csv").select(sid==600036).sort(tdate)

2

=A1.derive(if(#==1,close,(2*close+11*e12[-1])/13):e12)

A1 从 csv 文件中读入股票数据,过滤出代码为 600036 的股票数据,按照交易日期排序。

A2 就是实现上面讲过的思路,计算出 e12 列。这里 derive 是循环函数,直接针对集合实施循环处理,不必再写 for…next 这样的多条语句块。

SPL 在循环函数中支持跨行引用,表达式中的 e12[-1] 就是上一行的计算得到的 e12 值。# 是循环计算中当前行的次序号。

SPL 的 IDE 有很好的交互性,可以单步执行并随时在右边的面板中直观地查看到每一步的结果:

..

2. 计算 MACD 指标

MACD 是股票分析中常见的趋势指标,相关计算公式如下 :

快线 12 日 EMA=(前一日 EMA*11+ 今日收盘价 *2)/13

慢线 26 日 EMA=(前一日 EMA*25+ 今日收盘价 *2)/27

离差 DIFF = 今日 12 日 EMA- 今日 26 日 EMA

离差均线 DEA = (前一日 DEA*8+ 今日 DIFF*2)/10

MACD = 2*(DIFF-DEA)

现在要计算 600036 股票每个交易日的 MACD 指标。

写出 SQL:

with recursive tbl as(
    select tdate, close, row_number() over(order by tdate) rn
    from stktrade where sid='600036'
),
tbl2 as(
    select tdate,close, cast(close as decimal(22,16)) e12, 
        cast(close as decimal(22,16)) e26, rn 
    from  tbl where rn=1
    union all
    select t1.tdate, t1.close, (t1.close*2+t2.e12*11)/13, 
	    (t1.close*2+t2.e26*25)/27, t1.rn
    from tbl t1 join tbl2 t2 on t1.rn-1=t2.rn
),
tbl3 as (
    select tdate,close,e12, e26, e12-e26 diff,rn
    from tbl2
),
tbl4 as (
    select tdate,close,e12,e26, diff,diff dea,rn
    from tbl3 where rn=1
    union all
    select t3.tdate, t3.close, t3.e12, t3.e26, t3.diff, (t3.diff*2+t4.dea*8)/10, t3.rn
    from tbl3 t3 join tbl4 t4 on t3.rn-1=t4.rn
)
    select tdate,close,e12,e26,diff,dea,2*(diff-dea) macd
    from tbl4;

前面代码理解后,增加这些公式的计算并不难,但代码显得更繁琐了,而且 DEA 还要再次使用递归计算。

SPL 继续自然思路,只要在上个任务的 derive 函数中增加一些表达式并再多做一步 derive 就可以了:


A

1

=T("stock.csv").select(sid==600036).sort(tdate)

2

=A1.derive(if(#==1,close,(2*close+11*e12[-1])/13):e12,if(#==1,close,(2*close+25*e26[-1])/27):e26)

3

=A2.derive(e12-e26:diff, if(#==1,diff,(2*diff+8*dea[-1])/10):dea, 2*(diff-dea):macd)

代码复杂度几乎没有变化,还是很简单易懂。

3. 查找金叉与死叉

某交易日的前一日 DIFF 小于 DEA,后一日 DIFF 大于 DEA,则这个交易日称为金叉。

某交易日的前一日 DIFF 大于 DEA,后一日 DIFF 小于 DEA,则这个交易日称为死叉。

下图中 2 的位置是金叉,1 的位置是死叉:

..

现在要查出 600036 股票所有的金叉与死叉交易日。

SQL:

with recursive tbl as (
    select tdate, close, row_number() over(order by tdate) rn
    from stktrade
    where sid='600036'
),
tbl2 as (
    select tdate,close, cast(close as decimal(22,16)) e12, 
        cast(close as decimal(22,16)) e26, rn 
    from  tbl where rn=1
    union all
    select t1.tdate, t1.close, (t1.close*2+t2.e12*11)/13, 
        (t1.close*2+t2.e26*25)/27, t1.rn
    from tbl t1 join tbl2 t2 on t1.rn-1=t2.rn
),
tbl3 as (
    select tdate,close,e12, e26, e12-e26 diff,rn
    from tbl2
),
tbl4 as (
    select tdate,close,e12,e26, diff,diff dea,rn
    from tbl3 where rn=1
    union all
    select t3.tdate, t3.close, t3.e12, t3.e26, t3.diff, (t3.diff*2+t4.dea*8)/10, t3.rn
    from tbl3 t3 join tbl4 t4 on t3.rn-1=t4.rn
),
tbl5 as (
    select tdate,close,e12,e26,diff,dea,2*(diff-dea) macd,
        case when rn=1 then 0 
             when lag(diff) over(order by rn)<lag(dea) over(order by rn) and diff>dea then 1
             when lag(diff) over(order by rn)>lag(dea) over(order by rn) and diff<dea then -1
        else 0 end `cross`
    from tbl4
)
    select * from tbl5 where `cross`<>0;

又增加了一层子查询,还要增加窗口函数,代码变得更长、更繁琐。

上个任务的 SPL 代码已经得到 DIFF 和 DEA,现在只要再多一步即可计算出金叉死叉:


A

1

=T("stock.csv").select(sid==600036).sort(tdate)

2

=A1.derive(if(#==1,close,(2*close+11*e12[-1])/13):e12,if(#==1,close,(2*close+25*e26[-1])/27):e26)

3

=A2.derive(e12-e26:diff, if(#==1,diff,(2*diff+8*dea[-1])/10):dea, 2*(diff-dea):macd)

4

=A3.derive(if(diff[-1]<dea[-1]&&diff>dea:1,diff[-1]>dea[-1]&&diff<dea:-1;0):cross)

5

=A4.select@a(cross!=0)

A4 在 A3 的步骤之后再进行一次循环计算得到金叉死叉标志 cross,思路清晰。

A5 用 select@a 函数,得到所有符合 cross!=0 条件的结果,也就是所有的金叉和死叉。

4. 查找 MACD 顶背离

将股票数据按照交易日顺序划分成不同区间,如果后面区间的收盘价(或其他指标)最大值大于前面相邻区间最大值,称为收盘价(或其他指标)创新高。如果小于,则称为没有创新高。

MACD 顶背离是指相邻的两个金叉到死叉区间收盘价创新高,但 DIFF 没有创新高:

..

上图中箭头处,股票价格还在持续上升,但 macd 指标背离式走低,出现顶背离。

现在要查找股票 600036,所有 MACD 顶背离的两个区间中,第一个区间的最后一条记录。

SQL 实在过于复杂,不再尝试了。

SPL 上个任务已经计算出金叉和死叉的位置序号了。在此基础上,继续查找金叉到死叉的区间位置。然后再判断相邻两个区间的收盘价和 DIFF 是否创新高:


A

1

=T("stock.csv").select(sid==600036).sort(tdate)

2

=A1.derive(if(#==1,close,(2*close+11*e12[-1])/13):e12,if(#==1,close,(2*close+25*e26[-1])/27):e26)

3

=A2.derive(e12-e26:diff, if(#==1,diff,(2*diff+8*dea[-1])/10):dea, 2*(diff-dea):macd)

4

=A3.derive(if(diff[-1]<dea[-1]&&diff>dea:1,diff[-1]>dea[-1]&&diff<dea:-1;0):cross,0:topdev)

5

=A4.pselect@a(cross==1)

6

=A5.(A4.to(A5.~, A4.pselect(cross==-1,A5.~)))

7

>A6.run(if(#>1&&~[-1].max(close)<~.max(close)&&~[-1].max(diff)>~.max(diff),~.m(-1).topdev=1))


=A4.select(topdev==1)

A4 准备一个值为 0 的计算列 topdev,用于标记目标记录。

A5 查出所有金叉的位置序号,SPL 有丰富的位置运算能力,过滤时不仅可以返回满足条件的成员,还可以返回这些成员的位置。

A6:对 A5 中的金叉位置序号循环。A4.pselect(cross==-1,A5.~) 会得到该金叉之后的第一个死叉的位置。这样 A6 计算结果就是所有金叉到死叉区间:

..

双击结果中的成员,可以查看详细情况。比如第五个成员:

..

可以看到这是金叉(cross 为 1)到死叉(cross 为 -1)的区间。

A7:循环 A6,找出符合 MACD 线背离条件的相邻区间,并把其中第一个区间最后一条记录的 topdev 值赋值为 1。

A6 的成员是 A4 序表的排列,并没有产生新的序表,所以 A7 实际上是把 A4 序表目标记录的 topdev 赋值为 1。

A8 过滤 A4,得到期望的结果。