用Lisp写回测(K线篇)—— 从“玩具”到工程

在前一篇文章《用Lisp写回测(数据篇)—— 如何“获得”股票数据》里,用Chez Scheme解析了 通达信的数据文件,理论上是可以获得K线数据了,但如果不想写成硬代码的“玩具”,那么多少 还是要做一些设计的。
比如,目前就有以下问题:
-
在回测项目里,K线应该有统一的模型 —— 无论数据来自哪里。
-
可以使用其它数据源吗?比如其它行情软件的数据文件或者网络下载的CSV格式数据等。
-
如何通过配置文件,指定运行时的数据源及其工作参数?比如通达信数据文件的磁盘路径。
解决了上面的问题,就是将“数据”变成了“模型”,将“原始文件”变成了“可用对象”, 也是将“只是读取”变成了“系统工程”。
好吧,其实:
数据与模型之间,隔着一层思想。
K线是什么 —— 某种对象实体吗?
从“面向对象”的角度来看,把一根K线封装成某种对象,似乎是再自然不过的事情。我们希望不同 数据源的结果,最终都能被统一地识别、操作和分析。
于是,K 线像一个“实体”,它有自己的属性,比如时间、开盘价、最高价、最低价、收盘价、 成交量 —— 但,这只是一种表象。
何况,那么多根K线,要创建多少对象? 更进一步说,我们需要的K线模型,是否必然是某种对象?
在代码中,“对象”意味着存在、边界、封装 ……
但在现实中,K线不是“实体”,它更像是一种 可被观察的现象 ,一种时间序列上的点。
设计什么样的K线模型?
其实,我们只是想从K线模型中,用统一的语义获得来自不同数据源的关于时间、价格及数量等信息。
而不想,为K线创建大量对象,那太费字节 —— 更准确地说,不想为K线创造一个“存在”的字节实体。
也不想,到处搬移K线 —— 这并非只是性能问题,而是希望流动的是K线的信息,而非字节。
那么,有一种模式,那就是 —— 迭代器 模型
(let ([it (query-kline market code type from to)])
(while (has-next? it)
(next! it)
(let ([open (get-open it)]
[close (get-close it)])
...
)))
我希望用 (query-kline market code …) 返回指定股票某种K线的一个迭代器。
使用 (has-next? it) 和 (next! it) 两个原语操作迭代器让K线“流动”起来。
而用 (get-open it) 、 (get-close it) 、 (get-time it) 等从迭代器读取当前K线信息。
如何用Chez Scheme实现通达信K线迭代器?
而“某某器”这个词,本来就有“物化”的概念。这里的所谓“物化”,其实就是“对象化”。
尽管不同于C++、Java这类面向对象的编程语言,Chez Scheme在语言层面并没有提供定义类、实例对象 的直接表达。但,
Scheme 可能是第一个正确实现“闭包”这一概念的编程语言。
我理解所谓“闭包”,就是 一个表达式的求值环境可以回溯到返回该表达式的求值环境 。
说人话就是,当一个函数执行时,没有在其自身变量域中的变量可以在返回它的函数的变量域中找到。 就像是,这个函数“封闭”着一个返回它,的函数,的环境。
也可以理解成,这是一个带有内部状态的函数,每次调用返回的值和内部状态有关 —— 带有状态的东西,不就是对象吗?
所以,可以用Chez Scheme实现一个通达信的迭代器 stock/db/tdx.ss :
;; 定义通达信K线迭代器
(define tdx-kline-iterator
(lambda (bv type)
(let ([offset 0] [blk 32])
;; 实现迭代器接口 has-next?
(define has-next?
(lambda ()
(< offset (bytevector-length bv))))
;; 实现迭代器接口 next!
(define next!
(lambda ()
(set! offset (+ offset blk))))
(define get-time
(lambda ()
;; 从bytevector中解析时间
...))
(define get-open
(lambda ()
;; 从bytevector中解析开盘价
...))
...;; 定义解析其它字段的函数
;; 返回路由函数,接受一个参数,返回指定的接口函数
(lambda (route)
(case route
[(has-next?) has-next?]
[(next!) next!]
[(get-time) get-time]
[(get-open) get-open]
...))
)))
;; 定义通达信K线查询函数
(define tdx-query-kline
(lambda (market code type from to)
...;; 构造文件路径
(with-input-from-file file
(let* ([port (current-input-port)]
;; 一次性读取全部数据
[bv (get-bytevector-all port)]
;; 过滤出所需时间段的数据
[bv1 (filter-by-time bv from to)])
;;构造并返回K线迭代器
(tdx-kline-iterator bv1 type)))
))
怎么使用这个K线迭代器? —— 完成K线模型的封装
上述的通达信K线迭代器,直接使用肯定是不方便的。
按照前面对K线迭代器模型的设计,还需要再进一步封装 —— 不仅是为了方便,而且也是为了支持多数据源。
可以在一个上层接口模块 stock/db.ss 中封装:
;; 封装迭代器 has-next?
(define has-next?
(lambda (it)
;; 调用it的路由函数,获得闭包函数 has-next?,再调用。
(apply (it 'has-next) '())))
;; 封装迭代器 next!
(define next!
(lambda (it)
;; 调用it的路由函数,获得闭包函数 next!,再调用。
(apply (it 'next!) '())))
(define get-open
(lambda (it)
(apply (it 'get-open) '())))
...
如此就实现了K线的迭代器模型。
所以,K线可以不必是“对象”,它的结构,不是为了“构造一切”,
而是为了让我的语义,能自如地在其中流转。
在 Lisp 中,这种语言表达的自由才刚刚开始。
轻量化的多数据源支持
前面提到过,支持多数据源的问题。
尽管,目前还不打算引入其它数据源,但做为一种考虑,在程序的构架上是可以设计的。
可以在迭代器模型封装的接口文件 stock/db.ss 中,这样实现:
;; 定义一个通用的K线查询器
(define query-kline)
;; 设置配置的函数
(define config-datasource
(lambda (cfg)
;; 从配置中读取数据源标识符
(let ([ds (props-tree-ref cfg 'datasource)])
(case ds
[(tdx)
;; 用配置初始化通达信模块,比如文件的基础路径
(tdx-init cfg)
;; 将通用查询器设为通达信的查询器
(set! query-kline tdx-query-kline)]
;; 其它数据源
[(...) ...]
[else (error 'config-datasource
(format "Unsupported datasource ~a" ds))])
)))
当然,这个框架只支持一次运行单一的数据源。但就用Lisp写回测这个项目来说,它足够实用且轻量化。
如何消除文件路径硬代码? —— Lisp程序的配置文件
如果说,硬代码是语言的枷锁,那么配置文件或运行时参数就是程序呼吸的空间。
就像我在《用Lisp构建Lisp项目——思想表达思想的极致》里,用Lisp写的 make.ss 来构建Lisp程序。
那么用Lisp写的 config.ss 来配置Lisp程序也是顺理成章的 —— 因为你能想像的任意复杂的配置, 都可以抽象成一棵“树”,而这棵“树”又能转成“二叉树”,从而被Lisp中嵌套的 list 数据结构所表达。
还是那句话:
你发明的任何DSL,本质上都是某种粗陋的Lisp。
仅就用Lisp写回测这个项目的当前进展来说。目前,还只用到下面的配置:
(define-config
:db (:ds tdx :path "~/.local/share/tdxcfv/drive_c/tc/vipdoc")
)
由冒号":"开始的标识符是 关键字 ,而其后的列表元素是 值 。
那么这个文件(代码)想描述的是 —— 为回测数据库 :db 做配置,
-
数据源 :datasource 是代表通达信的 tdx 数据源,
-
而通达信数据源所依赖的文件路径由 :path 设定。
这样一个既是代码,也是数据的文件,可以在程序运行时动态加载,并解析成下面这样一个列表:
(db ((datasource . tdx) (path . "~/.local/share/tdxcfv/drive_c/tc/vipdoc")))
对于这样一个列表,很容易写一个函数,接受一个从树根到节点的路径作为参数,比如’(db path), 以直接返回配置项的值。
结语
至此,伴随这篇文章,用Lisp写回测这个项目,终于从只有一,两个文件的“玩具”前进到有了 模块设计的工程。
写文章的过程,也是一个整理思路的过程。
很多的想法,很多的特性,都想马上用代码去表达,却往往抓不到重点。
比如,在决定写这篇文章前一刻,都还在想是否要记录一下,
当我发现Chez Scheme对整数位域有着强大的操作能力时,就想着把K线的时间统一成一个32位的整数, 用年12位、月4位、日5位、时5位、分6位来表示。
又由于Chez Scheme并没有定义位域结构的语法,我是怎么用宏写了一个定义位域结构语法的。
但这些想法,在开始定下这篇文章的标题后,就湮灭了 —— 我的思路开始围绕对推进用Lisp写回测这个项目 更紧迫也更有意义的工程化设计方面。
一边整理思路,一边编写代码,一边用文字记录。
可能会伴随我,直到真的用Lisp写出一个股票回测系统。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)