数据分析编程从 SQL 到 SPL:股票上涨分析

数据结构和样例数据:

..

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

1. 查找股价上穿中位数

T 日股价中位数是指从上市日收盘价到 T 日收盘价的中位数。

T 日股价上穿中位数是指 T 日收盘价大于 T 日股价中位数,T-1 日收盘价小于 T-1 日股价中位数,或者 T-1 日收盘价等于中位数但 T-2 日收盘价小于中位数,依此类推。

现在要查找 600036 这支股票满足股价上穿中位数的交易日期、收盘价和当日股价中位数。

以 Mysql 数据库为例写出 SQL:

with tbl1 as (
    select *
    from stock
    where sid='600036'
), 
tbl2 as (
    select t1.*, t2.close pclose, 
        count(*) over(partition by t1.sid,t1.tdate) cnt, 
	    row_number() over(partition by t1.sid,t1.tdate order by t2.close) r
    from tbl1 t1 join tbl1 t2
        on t1.sid=t2.sid  and t1.tdate>=t2.tdate 
), 
tbl3 as (
    select sid,tdate,max(close) close, avg(pclose) median 
    from tbl2
    where rn=floor((cnt+1)/2) or rn=floor((cnt+2)/2)
    group by sid,tdate
),
tbl4 as (
    select sid,tdate, close, median, 
    close>median and 
        lag(close) over(partition by sid order by tdate) 
        < lag(median) over(partition by sid order by tdate)  flag 
    from tbl3
    where median!=close 
)
    select tdate, close, median 
    from tbl4
    where flag=1;

Mysql 没有直接提供计算中位数的函数,只能先人为造出序号 r,在外层语句中用 r 作为条件查出中位数相关记录,SQL 非常复杂。

有些数据库(如 Oracle)提供了 median 函数,SQL 会稍简单,但也需要四层子查询配合窗口函数完成计算:

with tbl1 as (
    select t1.sid,t1.tdate,t1.close, 
        (select median(close)  
         from stock 
         where t1.sid=sid  and t1.tdate>=tdate 
        ) median
    from stock t1
    where t1.sid=600036
),
tbl2 as (
    select sid,tdate, close, median ,
        lag(close) over(partition by sid order by tdate) as pclose,
        lag(median) over(partition by sid order by tdate) as pmedian
    from tbl1
    where close!=median
)
    select tdate, close, median 
    from tbl2
    where close>median and pclose<pmedian;

要计算 T 日股价中位数,就要找到从上市日到 T 日收盘价的集合。无论是第一个 SQL 采用的连接计算,还是第二个 SQL 采用的子查询,都是比较绕的思路。

更自然的思路是:将这个股票的数据按交易日期排序,从第一条到 T 日(比如 2016 年 1 月 15 日)的 close 值组成的集合,就是 T 日对应的目标集合:

..

但是,SQL 的集合化不彻底,没有游离记录及其集合的表示方法,不能用原集合的成员构成新集合再进行计算,实现不了这种思路,只能用连接或者子查询来实现。

SPL 集合化更彻底,且支持有序集合,很容易实现这种思路:


A

1

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

2

=A1.derive(close[:0].median():median)

3

=A2.select(close!=median).select(close>median && close[-1]<median[-1])

A2:循环函数 derive 支持跨行引用,close[:0] 表示从 A1 第一行到当前行的 close 值构成的集合。这就是思路中的上市日到 T 日的全部收盘价,不需要使用连接或者子查询就可以得到。

而且,SPL 也提供了 median 函数,计算集合 close[:0] 的中位数很方便。

类似地,光标落在 A2 时,可以在右边面板中看到其计算结果:

..

A3 先过滤掉收盘价和中位数相等的记录,然后查找上穿中位数的记录,这里也使用了跨行引用,close[-1]<median[-1] 表示上一行的收盘价小于对应的中位数。

2. 查找指定股票的上升波段

上升波段的计算需求:

输入参数是交易日的个数 W;

一个波段是指连续的多个交易日组成的区间;

某交易日是“W 个交易日新高”,是指这个交易日收盘价比前 W-1 个交易日都高。

某交易日是一个波段中的新低,是这个交易日收盘价比波段内更早日期的收盘价都低。

某交易日在 W 日内必创新高,是指这个交易日后(包括当日)W 日内,会出现至少一个“W 个交易日新高”。

上升波段须满足以下条件:

波段中至少有一个交易日在 W 个交易日内必创新高;

波段的起点价格最低,终点价格最高;

波段最短不低于 W 个交易日。

现在给定 W=5,要找出 300469 这支股票所有上升波段对应的起止日期。

这个计算要找到新高、新低、起点、终点等多个记录位置。如果用 SQL 的话,要反复造出序号再用条件选出,即使能实现,代码也会非常繁琐,这里就不写了。

查找上升波段的思路:将这支股票的数据按照交易日期排序并增加列 high1 和 high2 作为新高和必创新高的标志。第一次循环计算,如果当前交易日收盘价大于前四个交易日,就将 high1 置为 1,否则置为 0。第二次循环计算,如果当前交易日之后的 W-1 个交易日内出现 high1 为 1 的记录,则将 high2 置为 1,否则为 0。计算结果:

..

结果中 high2 连续为 1,且长度 >=W 的区间,就是 W 日内必有新高的波段,比如红框的部分。接下来就在这些波段中找目标波段。

如果其中某个波段没有出现新低,就是目标波段了。

如果波段有新低,我们可以在第一个新低之前找第一条 high1 为 1 的记录位置序号 y。如果 y>=W,那么这个波段的前 y 条记录就是目标波段。

然后用第一个新低之后的所有数据组成一个新的波段,再按照同样的方法继续寻找目标波段,以此类推。

很明显,这样的过程需要采用递归函数计算,简单的 SQL 递归语句无法实现,借助存储过程也会非常困难,这里就放弃了。

SPL 有序号定位机制,也支持函数的递归调用,很容易实现这种思路:


A

B

C

1

=T("stktrade.csv")

>W=5


2

=A1.select(sid==300469 && tdate>=date(2016,01,04) && tdate<=date(2018,10,18)).sort(tdate)

3

=A2.derive(if(close[-W+1:-1].max()<close,1,0):high1).derive(high1[0:W-1].max():high2)

4

=A3.group@o(high2).select(high2==1 && ~.len()>=W)

=create(start,end)


5

for A4

=stockFunc(A5)

>B5.run(B4.insert(0,B5.~.tdate,B5.~.m(-1).tdate))

6




7

func stockFunc(stockSet)

=[]


8


=stockSet.pselect(close<close[:-1].min())

9


if B9

>if(B9>=W,(x=stockSet.to(B9),y=x.pselect@1(high1==1),if(y>=W,B8|=[stockSet.to(y)])))

10



>if(stockSet.len()-B9+1>=W,B8|=stockFunc(stockSet.to(B9,)))

11


else

>if(stockSet && stockSet.len()>=W,B8|=[stockSet])

12


return B8


A3: 利用循环函数中的跨行引用机制,简单用两个表达式就能按照上述思路计算出 W 日新高的标志 high1、 W 日内有新高的标志 high2。

A4 中的 group@o 是相邻记录分组,相当于按照原来的顺序,合并 high2 相同相邻记录。然后过滤出 high2 为 1,且长度 >=W 的分组,也就是 W 日内必有新高的波段。

A5 循环处理 A4 的这些波段,调用子程序 stockFunc 继续寻找上升波段,子程序的参数是 A5 当前成员。C5 把子程序返回的结果写到 B4 的结果集中。

SPL IDE 支持进入子程序单步执行,光标在调用子程序的格子 B5 时,点击菜单栏上的“单步进入”按钮即可:

..

在子程序 stockFunc 中,B8 利用跨行引用机制,查找 stockSet 集合中收盘价新低的位置。

C9 中 x、y 是临时变量。x 是新低以前的记录集合(包括新低),y 是 x 中第一条 high1 为 1 的记录位置。按照前述思路,如果 y>=W,那么 stockSet 中前 y 条记录就是符合要求的波段,加入结果 B7。

子程序中同样可以在预览窗口观察计算结果:

..

C10: 按照前述思路,用新低以后的数据组成新的波段,作为参数调用 stockFunc 继续查找目标波段。这是对 stockFunc 子程序的递归调用。

C11: 若没有新低且长度 >W,则整个集合是一个目标波段,加入结果。