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

张开发
2026/4/17 3:54:44 15 分钟阅读

分享文章

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

更多文章