当前位置: 首页 > news >正文

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

在前一篇文章《用Lisp写回测(数据篇)—— 如何“获得”股票数据》里,用Chez Scheme解析了 通达信的数据文件,理论上是可以获得K线数据了,但如果不想写成硬代码的“玩具”,那么多少 还是要做一些设计的。

比如,目前就有以下问题:

  1. 在回测项目里,K线应该有统一的模型 —— 无论数据来自哪里。

  2. 可以使用其它数据源吗?比如其它行情软件的数据文件或者网络下载的CSV格式数据等。

  3. 如何通过配置文件,指定运行时的数据源及其工作参数?比如通达信数据文件的磁盘路径。

解决了上面的问题,就是将“数据”变成了“模型”,将“原始文件”变成了“可用对象”, 也是将“只是读取”变成了“系统工程”。

好吧,其实:

数据与模型之间,隔着一层思想。

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做配置,

  1. 数据源:datasource是代表通达信的tdx数据源,

  2. 而通达信数据源所依赖的文件路径由: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写出一个股票回测系统。

http://www.jsqmd.com/news/653511/

相关文章:

  • 深度解析:OpenIPC固件在君正T31ZX平台烧录故障排查与修复指南
  • Unity URP 热更新兼容性:Shader 在 IL2CPP 打包下的注意事项
  • 如何监控集群 interconnect_ping与traceroute验证心跳通畅.txt
  • OpenAI惨遭反超,Anthropic狂吞70%新客户,Claude已开启「灵魂校准」
  • 别再只聊天了!用Python调用Gemini API,5分钟搞定图片识别和表格数据提取
  • 告别网络性能盲猜:手把手教你将iperf3交叉编译到ARM设备,实测WiFi/有线带宽
  • 【Ubuntu2404】Ubuntu24.04下Docker引擎的安装与配置全攻略
  • 装好Hermes只是第一步:四步调教,让AI“越用越聪明”
  • 红黑榜 | 以为吃了70碗水煮菜,其实换了个形式吃咸菜?
  • Unity中PICO手柄按键返回值的高级应用与实战解析
  • 黑群晖转白群晖DS920+数据迁移全记录(含避坑指南)
  • 太空算力:下一个万亿蓝海赛道
  • 【RAG】【vector_stores053】Milvus全文搜索向量存储示例分析
  • ICLR 2025 | HiPRAG:不是让 Agent RAG 搜得更多,而是让它学会什么时候不该搜
  • 数据结构之双端队列
  • 5大核心功能打造极致Markdown预览体验:Markdown Viewer全面解析
  • “程序包io.swagger.annotations不存在”终极解决方案:从原理到实战的万字深度剖析(2026年最全最新解决方案)
  • 2026年超长论文分章节降AI率的正确方法:多章节处理完整攻略
  • while(1);的top-down分析
  • 第3讲——并查集
  • 探店无数,平凉这口五仁月饼最难忘
  • AI Agents:正在爆发的“代理经济“时代
  • 从‘?’命令到调试高手:Lumerical FDTD脚本排错与数据验证实战指南
  • LLM服务SLO崩塌前的最后17分钟:如何通过流式token监控+语义一致性校验实现亚秒级异常预判
  • 工具技术集成开发环境IDE与轻量级编辑器的选择标准
  • 快递查询-物流查询-快递物流查询接口介绍
  • 2026年金融学论文降AI工具推荐:数据分析和金融模型部分如何降
  • C语言条件编译三种方式及第一种方式的格式、作用与示例
  • Unity URP 下 UI 特效开发指南 深入探索顶点色、Mask 交互与扭曲特效的实战技巧
  • 程序包javax.validation.constraints不存在