用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写出一个股票回测系统。
