第 2 章 理解 K 线数据

2.1 数据下载

数据分析的第一步就是获取数据,为方便读者使用,我们将股票数据免费分享在百度网盘上,每个工作日更新。

下载链接:

https://pan.baidu.com/s/1fTuqDtIjvxzX22EcbeOvtA?pwd=cd5r

我们打开下载链接看一下,里面有很多文件,读者可以按需下载。

..

其中形式如 yyyy.trade.btx 的文件就是 yyyy 年全部 A 股的日线行情。stock.btx 则是所有股票的列表和基本信息,包括股票代码、名称、所属行业、上市日期、退市日期等。

其它文件,我们后续会根据课程内容逐步介绍。

2.2 K 线数据

量化交易最核心的数据,就是每日的股票交易数据,即日线行情,也称 K 线数据。

例如,我们下载 2025 年 A 股的日 K 线数据 2025.trade.btx。

..

细心的读者可能已经发现,下载文件的格式为 btx,而并非我们常见的 csv 或 Excel 文件。没错 btx 是 SPL 的特有二进制格式。

btx 称为集文件,是一种简单、开放的二进制文件格式,它不仅覆盖了 csv 文件的所有功能,而且性能要比 csv 文件快 4-5 倍。

SPL 可以支持 btx 文件的操作计算,也支持各种数据源与 btx 之间的相互转换。

例如我们将k_data.csv转换成btx格式:


A

1

=T("C:/Users/29636/Desktop/tmp/k_data.csv")

2

=T("C:/Users/29636/Desktop/tmp/k_data.btx":A1)

A1 用 T 函数读取 csv 文件

A2 将 A1 写入到k_data.btx中。T()函数除了可以用来读取数据,还可以用来写出数据,冒号后面的参数是要写出的内容。

当然反过来也可以:


A

1

=T("C:/Users/29636/Desktop/tmp/k_data.btx")

2

=T("C:/Users/29636/Desktop/tmp/k_data1.csv":A1)

A1 读取btx文件

A2 A1写入到k_data1.csv

下面重点来讲一下如何读取btx数据。

直接读取数据

当数据量不大时,可以直接用 T() 函数打开 btx 文件:


A

1

=T("D:/2025.trade.btx")

..

当数据中的字段比较多时,也可以将右边显示计算结果的区域拖拽到界面下方:

..

股票日 K 线的数据结构为:

字段名

字段类型

含义

code

Integer

股票代码

tdate

Date

交易日期

open

Double

开盘价

close

Double

收盘价

low

Double

最低价

high

Double

最高价

volume

Long

成交量

amount

Double

成交金额

pfactor

Double

当日价格复权因子

需要注意的是,股票代码在这里用了整数,而不是常规的字符串,这里 1 表示代码 为 000001 的股票,2 表示代码 000002…。这样表示并不会有歧义,而整数的运算性能会远高于字符串。

复权因子的意义在后面会讲到。其它字段,看一下说明都容易理解是什么意思,就不再详细解释了。

用游标读取数据

T函数会将整个文件的数据都读入内存,当数据量较大时,会占用很大空间,内存不够就可能溢出。

SPL提供了可以用游标流式读入文件的方式:


A

1

=T@c("D:/2025.trade.btx")

2

=A1.fetch(100)

A1 用 T@c() 将文件读成游标。

A2 取出前 100 条数据。

T函数里的 @c 又是个什么东西?

@c这种写法是 SPL 发明的,称为函数选项。理论上讲,T 和 T@c 是两个无关的不同函数,但这两个函数非常像,都是用来读取数据的,参数也一样,完成的功能虽然不完全相同,但也很类似。这种时候,我们更愿意把其中一个看成是另一个的变种,在起名时使用相似的名字,这样对于理解和记忆都会更方便一点。

但这两个函数总还是有不同,需要区分。在 SPL 中我们用相同的名字来命名这两个函数,而用函数名后 @后面的字符来区分,好象一个函数有了不同的模式:没有 @c 时,T 函数返回序表,有 @c 时返回游标。

SPL 中这种有选项的函数非常多,而且选项常常还不只一种。比如 T 函数还有个 @b 的选项,表示当文件内容无标题时自动添加标题。而且,这些选项还可能组合使用,并且书写没有次序,如 T@bc 和 T@cb 是一样的。

SPL 的函数选项能大大简化代码的编写。目前其它程序语言都没有这种语法,有些语言的函数也有选项概念,但一般是作为一个独立参数存在的。我们后面还会看到,在结构化数据处理时,参数会有很复杂的情况,把选项信息作为参数传递并不方便。

言归正传,A1 格中 T@c() 会返回一个游标对象,并不会真正的读取数据。

..

要想看到数据,需要用 fetch() 函数从游标中把数据取出来,如 A2 格中的代码,参数 100 表示取出前 100 条数据。这样既能查看数据又防止了内存溢出。

..

当然,只读前 100 条只是看一眼而已,并没什么实际用处。

我们通常会读取某支股票数据,也就是在游标读取数据过程中过滤:


A

1

=T@c("D:/2025.trade.btx")

2

=A1.select(code==600000)

3

=A2 .fetch()

A1 将文件读成游标。

A2 选出代码为 600000 的股票数据。

A3 取出 A2 格里的数据。

select为选出函数,表示筛选出 code 值等于 600000 的数据。注意在 SPL 里判断是否相等的运算符是”==”而不是”=”,“=”在 SPL 里表示赋值运算,这和有些程序语言的使用习惯不太相同,使用时要注意区分。

在游标上执行 select 函数后返回值仍然是个游标。因为游标对应的是大数据,选出后的结果仍然可能是大数据,内容会溢出,所以,游标上的 select 函数仍然返回一个游标。

A2格 select 函数返回的是一个游标,要想查看结果怎么办,还是用 fetch,这里我们知道一支股票的数据并不大,因此可以全部取出。当 fetch() 参数为空时,表示取出全部数据。

..

调用脚本读数

知道了如何用游标读数,下一步我们来写一个更通用的程序,可以读取任何多支股票任意时间段的 K 线数据。为了使用方便我们可以将一些通用的程序保存为脚本,需要时直接调用。

脚本是一个被保存成扩展名为.splx 的文件,它的功能和库函数类似,可以随时被调用。并且和函数一样,脚本也需要有输入参数和输出结果。

通常在编写一个脚本之前首先要明确它的功能,即定义好输入参数和输出结果是什么。

例如这里我们希望输入一支或多支股票的代码,以及要查询的时间范围,就可以读出对应的股票数据。

可以将脚本参数定义如下表:

cl

股票代码序列,如[600000,600001];可以为单值,如600000;也可以填空表示全读

start

开始日期,如2024-01-01

end

截止日期,如2024-12-31end为空时截止到当前日期

这里 cl 参数用到了一种新的数据类型序列,序列相当于 C 语言中的数组,用来表示一组数据。序列和序表是 SPL 中两种最基本的数据类型。

明确了输入参数和输出结果,我们来创建脚本。

(1)在界面新建一个文件

..

(2)使用程序菜单下的参数功能,会跳出一个对话框,添加参数如图。值 Value 那一列是指缺省值,调用这个脚本时如果不写参数,就会用这个缺省值。

.. ..

(3)在格子区编写脚本代码如下:


A

1

=end=ifn(end,now())

2

=to(year(start),year(end)).(file(~/".trade.btx")).select(~.exists())

3

=cl=if(ifa(cl), cl.sort(),cl)

4

=if(cl, A2.( ~.iselect@b(cl,code)), A2.(~.cursor@b()) )

5

=A4.merge(code,tdate).select(tdate>=start && tdate<=end)

6

return A5.fetch@x()

这段代码对于 SPL 的初学者来说,可能有点难,暂时读不懂也没关系,现在我们只要先学会如何用即可。

(4)脚本保存为 loadkday.splx,此脚本可返回一支或多支股票任意时间段的 K 线数据。

注意:运行此脚本之前,要将下载的所有yyyy.trade.btx 文件放到同一路径下。并且设置工具→选项→环境菜单中的主路经和寻址路径。主路经设置为yyyy.trade.btx 文件的存储路径,寻址路径设置为脚本 loadkday.splx 文件的存储路径。事实上,为了使用方便我们可以把所有的数据文件和通用脚本都放到主路径和寻址路径下。配置了主路经和寻址路径,在程序中就可以用相对路径来读取文件或脚本了。

..

脚本整理好后,就可以调用了。

例如,调用脚本读取一支股票的 k 线数据:


A


1

600000

股票代码

2

2024-01-01

开始日期

3

2024-12-31

截止日期

4

=call("loadkday.splx",A1,A2,A3)

调用脚本,返回600000股票2024年数据

5

=call("loadkday.splx",A1,A2)

end为空,截止到当前日期

call()函数用来调用脚本,返回结果集,第一个参数为要调用的脚本文件,后面参数依次为脚本的参数。

运行效果:

..

读取多支股票数据:


A


1

[300632,301115,603216]

股票代码序列

2

2024-01-01

开始日期

3

2024-01-10

截止日期

4

=call("loadkday.splx",A1,A2,A3)

调用脚本,A1序列中的股票

5

=call("loadkday.splx",A1,A2)

end为空,截止到当前日期

6

=call("loadkday.splx",,A2,A3)

cl为空,读取全部A

当要读取多支股票数据时,股票代码要写成一个序列,如 [300632,301115,603216]。

再例如,还可以读取某类股票,如读取所有银行股:


A


1

=T("stock.btx").select(industry=="J66货币金融服务").(code)

读取银行股票代码列表

2

2024-01-01

开始日期

3

2024-01-10

截止日期

4

=call("loadkday.splx",A1,A2,A3)

调用脚本,返回银行股数据

5

=call("loadkday.splx",A1,A2)

end为空,截止到当前日期

这里用到了 stock.btx,在下一小节中我们会具体讲解,这里只需知道 A1 返回的是所有银行股的代码序列 [1,1227……]

运行效果:

..

对于经常使用的脚本,也可以用 register 登记成一个函数来使用。

代码示例:


A


1

=register("Load","loadkday.splx")

将脚本登记为函数

2

=T("stock.btx").select(industry=="J66货币金融服务").(code)

读取银行股票代码列表

3

2024-01-01

开始日期

4

2024-01-10

截止日期

5

=Load(A2,A3,A4)

返回开始到截止日期的数据

6

=Load(A2,A3)

end为空,截止到当前日期

7

=Load(,A3,A4)

cl为空,读取全部股票

register()可以将脚本文件登记为函数,之后该函数便可以在代码中使用,如 A1 格中的代码表示将脚本文件 "loadkday.splx" 登记为函数 Load(),在后面的代码 A5、A6、A7 格中就可以通过函数名调用。

运行效果同上。

实际上在一个项目中通常会有很多个脚本,每次都要写一堆 call 或 register 有点麻烦,我们可以将一个项目中用到的所有脚本都登记到一个单独的脚本中,这样只要在每段程序开头执行这个脚本就可以了,避免了重复劳动。

这种脚本登记的工作后续还会再有,我们把这些事都写到一个统一的脚本 init.splx 中


A

1

>register("Load", "loadkday.splx")

>开头的单元格称为执行格,表示会执行单元格里的内容。后面遇到了我们还会再细讲,这里有个初步理解就行。

在使用的脚本前调它一下,这些函数就有了定义:


A


1

>call("init.splx")

将脚本登记为函数

2

=T("stock.btx").select(industry=="J66货币金融服务").(code)

读取银行股票代码列表

3

2024-01-01

开始日期

4

2024-01-10

截止日期

5

=Load(A2,A3,A4)

返回开始到截止日期的数据

6

=Load(A2,A3)

end为空,截止到当前日期

7

=Load(,A3,A4)

cl为空,读取全部股票

之后有更多需要登记的脚本时,只要补充 init.splx 的内容即可。

2.3 股票列表

在选股时,比较常用的数据还有股票代码列表。

文件名称:stock.btx。

提供股票基础信息,包括股票代码、名称、所属行业、上市日期、退市日期等。

数据示例:

..

数据结构:

字段名

字段类型

含义

code

Integer

股票代码

name

String

股票名

exchanges

String

中国三大交易所的简写

industry

String

所属行业

ipodate

Date

上市日期

outdate

Date

退市日期

例如我们需要查找所有银行股代码,可以根据行业信息来筛选,代码为:


A

1

=T("stock.btx").select(industry=="J66货币金融服务")

2

=A1.(code)

A1 筛选出所有行业信息等于 "J66 货币金融服务" 的数据,结果返回序表

..

A2 对每行数据取 code 值,结果返回序列。

..

A.()循环函数,表示对 A 的每一个成员计算括号里的表达式。循环函数也是 SPL 特有的发明,它的语法非常简洁高效,很多复杂的计算用循环函数很容易就能实现,这些后面用到了我们会具体讲。

再来一例,在股市中有一类股票叫做 ST 股票。

ST股票(Special Treatment Stock),即特别处理股票,是指因为经营不善等各种因素而发生了连续亏损的股票。上市公司股票被实施退市风险警示的,在公司股票简称前冠以“*ST”字样,被实施其他风险警示的,则冠以“ST”字样,俗称“戴帽”,这两类股票统称为 ST 股票。

通常我们要回避这些“地雷股”(ST 股票),可以用如下代码来清洗掉 ST 股票。


A

1

=T("stock.btx")

2

=A1.select(!like(name,"*ST*"))

A1 打开股票列表文件,如图可以看到数据中含有 ST 股票

..

A2 筛选出名称中没有“ST”的股票。

like()函数可以判断 name 中是否包含 ST,结果返回布尔值。运算符! 表示逻辑非运算,表示不是的意思,相当于 Excel 中的 NOT,取反值。!true = false,!false = true。对 like 的结果取反值就选出了非 ST 股票数据,实现了清洗目标。

如图,可以看到代码为 4 和 5 的 ST 股票被清洗掉了。

..

2.4 绘制图形

在股票投资领域,技术图形分析也是投资者不可或缺的重要手段。通过对股票价格和成交量等数据的图形化展示,技术图形能够为投资者提供丰富的信息,帮助他们做出更明智的投资决策。

SPL本身提供了绘制静态图形的功能,为了方便随时选取时间区间观察,我们需要动态效果的图形,这可以借助 echart 来完成。

1. 首先下载 stockchart.jar,放到集算器安装路径…\raqsoft\esProcDesk\lib 下,然后重启集算器。

stockchartjar

2. 类似地,我们写个通用脚本 draw.splx。

脚本代码:


A

B

1

=cols= x+","+y1+if(len(y2)>0,","+y2,"")


2

=data.new(${cols})


3

=if(year(data(1).field(x))==year(data.m(-1).field(x)),"MM-dd","YY-MM-dd")

4

=A2.run(~.field(x, string(~.field(x),A3)) )

5

=y1.split@c()

=y2.split@c()

6

=invoke(com.raqsoft.stockchart.LineChart.drawHtml,A4,[x,A5,B5])

7

=file(htmlfile:"utf-8").write(A6)

类似地,这段代码也不详细解释了,感兴趣的同学可以自己找 SPL 参考书理解。这里我们还是先学会如何调用它就可以了。

脚本参数:

data

序表,要绘制图形的列

x

字符串,x轴字段名,如"tdate"

y1

字符串,y1轴字段名,如"close""close,close_MA_5"

y2

字符串,y2轴字段名,如"volume",或"volume,volume_MA_5"y2也可以为空,为空时只有一个y

htmlfile

动态图文件路径

此脚本可生成回双轴或单轴折线动态图 html 文件。

将这个脚本也登记到 init.splx 中:


A

1

>register("Load", "loadkday.splx")

2

>register("Draw", "draw.splx")

3. 调用脚本,生成动态 html 文件。

例如绘制 600690 股票 2024 年 close 动态走势图。


A

1

>call("init.splx")

2

600000

3

2024-01-01

4

2024-12-31

5

=Load(A2,A3,A4)

6

=Draw(A5,"tdate","close",,"600000.html")

A1 执行初始化脚本,将脚本登记为函数

A3:A5 读取股票数据

A6会生成 600000.html 文件,用浏览器打开该文件即可看到动态图。

..