在这里插入图片描述

在前一篇文章《用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写出一个股票回测系统。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐