资源加载中... loading...

回测系统

当您完成了一个量化交易策略的设计工作后,怎么才能知道您这个策略的逻辑、策略收益方向等基本情况?当然我们不能直接拿真金白银去交易市场上跑策略,我们可以用历史数据来测试您的策略。看看您的策略在历史数据中盈利如何。

回测系统模式

发明者量化交易平台将回测模式分为实盘级 Tick回测和模拟级 Tick回测。实盘级 Tick回测完全按照完整的历史数据回测;模拟级 Tick回测则根据真实K线数据生成tick数据来进行回测。两者都是根据真实历史数据回测的,但实盘级 Tick回测的数据更精准,结果更加可信。但是回测仅仅是策略在历史数据下的表现,历史数据并不能完全代表将来的行情,所以对待回测结果要理性、客观。

模拟级 Tick回测根据底层K线周期生成模拟的tick数据,每个底层K线周期上最多将生成12个回测时间点。而实盘级 Tick回测使用的是真实收集的逐秒tick数据,数据量很大,回测速度慢。因此不能回测特别长的时间范围,FMZ量化的回测机制可以使策略在一根K线上交易多次,避免了只能收盘价成交的情况,更加精准又兼顾了回测速度。

回测系统机制说明

  • 模拟级 Tick 模拟级 Tick回测是根据回测系统的底层K线数据,按照一定算法在给定的底层K线Bar的最高价、最低价、开盘价、收盘价的数值构成的价格框架内模拟出tick数据进行回测,作为回测时间序列上的实时tick数据,在策略程序调用接口时返回。具体可以参考:回测系统模拟级别机制说明

  • 实盘级 Tick 实盘级别回测是真实的tick级别数据在Bar的时间序列中。对于基于tick级别数据的策略来说,使用实盘级别回测更贴近真实。实盘级别回测tick是真实记录的数据,并非模拟生成。支持深度数据、市场成交记录数据回放,支持自定义深度,支持分笔数据。实盘级别回测数据最大支持50MB,在数据上限内不限制回测时间范围,如果需要尽可能增大回测时间范围,可以降低深度档位数值设置,不使用分笔数据以增加回测时间范围。调用GetDepthGetTrades函数获取回放行情数据。在时间轴上某个行情数据时刻,调用GetTickerGetTradesGetDepthGetRecords,不会多次推动时间在回测时间轴上移动(不会触发跳到下一个行情数据时刻)。对于以上某个函数重复调用,将推动回测时间在回测时间轴上移动(跳到下一个行情数据时刻)。回测时使用实盘级别回测不宜选择过早时间,可能过早时间段没有实盘级别数据。

实盘级Tick模拟级Tick模式,回测系统成交撮合机制:订单成交撮合是按照见价成交,全量成交进行。因此回测系统中无法测试出部分成交的场景。

回测系统支持多种编程语言

回测系统支持:JavaScriptTypeScriptPythonC++PINEMy语言Blockly可视化编写设计的策略进行回测测试。

JavaScriptC++ 策略回测是在浏览器端进行,JavaScriptC++ 语言的策略在实盘、回测运行时不用安装任何其它软件、库或模块。

Python 语言的策略回测是在托管者上进行,可以在FMZ量化的公共服务器上回测,也可以在用户自己的托管者上回测。实盘和回测都依赖托管者所在系统上安装的Python环境,如果需要使用一些库,需要自行安装,FMZ量化的公共服务器上只支持常用的 Python 库。

支持 JavaScript 语言的策略回测时在Chrome浏览器DevTools中调试,参考说明

回测系统支持的交易所

  • 加密货币 加密货币主流现货、期货交易所;支持交易所全品种数据。
  • 富途证券 港股、美股等市场。

回测系统参数调优

发明者量化交易平台回测系统参数调优功能是在回测时根据各个参数的调优选项设置参数组合,在「模拟回测」页面中策略参数部分,勾选策略参数右侧的调优选项即可出现调优设置。

  • 最小值:限定参数的起始值。
  • 最大值:限定参数递增变动后的最大值。
  • 步长:参数递增变动量。
  • 并发线程: 参数调优时,设置各个回测参数组合并发执行的线程数。该选项仅支持JavaScriptPINEMy语言的策略参数调优,不支持模板上的参数调优。

根据最小值最大值步长设置,生成参数组合。回测系统遍历这些参数组合进行回测(即每种参数组合都回测一遍)。策略参数只有类型为数字型(number)的参数才可以在回测系统中进行参数调优设置。

保存回测设置

策略编辑页面,「模拟回测」分页中(即:回测系统)可以设置回测配置、策略参数等选项进行策略回测。回测配置是用来设置回测时间范围、回测的交易所、交易滑点、手续费等条件;策略参数则是设置策略的参数选项。

当设置好这些参数配置时便可按照设定回测策略,那么如何保存这些设置好的配置信息呢? - 1、可以使用策略编辑页面的「保存回测设置」按钮将所有回测配置信息(包含回测设置、策略参数设置)以代码形式记录在策略源码中。 - 2、在策略编辑页面点击「保存策略」按钮保存策略时,平台会自动记录当前的回测设置、策略参数配置等信息。

回测系统如何载入回测配置呢? - 1、刷新策略编辑页面或者重新打开这个策略编辑页面时优先自动载入「保存回测设置」按钮所记录的回测配置信息。 - 2、如果当前策略代码中没有以注释形式backtest记录的回测配置信息(通过「保存回测设置」按钮保存在策略代码),回测系统自动配置回测设置为当前策略最后一次点击「保存策略」按钮时的回测配置信息。 - 3、如果在策略编辑页面中修改了策略代码开头部分以注释形式记录的回测配置信息,需要把当前更新后的回测配置信息同步到策略回测界面的选项,可以点击策略编辑区域backtest上方的「回测设置」按钮。

/*backtest
start: 2021-06-26 00:00:00
end: 2021-09-23 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/
'''backtest
start: 2021-06-26 00:00:00
end: 2021-09-23 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
'''
/*backtest
start: 2021-06-26 00:00:00
end: 2021-09-23 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

点击「保存回测设置」,JavaScript/Python/C++/My语言/PINE语言的策略保存回测设置到策略代码时,格式略有差别: My语言:

(*backtest
start: 2021-06-26 00:00:00
end: 2021-09-23 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*)

PINE语言:

/*backtest
start: 2021-06-26 00:00:00
end: 2021-09-23 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

自定义数据源

发明者量化交易平台的回测系统支持自定义数据源,回测系统使用GET方法请求自定义的URL(可公开可访问的网址)来获取外部数据源进行回测,附加的请求参数如下:

参数 意义 说明
symbol 品种名 现货行情数据例如:BTC_USDT,期货行情数据例如:BTC_USDT.swap,期货永续合约资金费率数据例如:BTC_USDT.funding,期货永续合约价格指数数据例如:BTC_USDT.index
eid 交易所 例如:OKX、Futures_OKX
round 数据精度 为true,表示由自定义数据源反馈的数据中定义具体精度。发明者量化交易平台回测系统向自定义数据源发送的请求固定为:round=true
period K线数据的周期(毫秒) 例如:60000为1分钟周期
depth 深度档数 1-20
trades 是否需要分笔数据 真(1)/假(0)
from 开始时间 unix时间戳
to 结束时间 unix时间戳
detail 请求数据的品种详细信息 为true,表示需要由自定义数据源提供。发明者量化交易平台回测系统向自定义数据源发送的请求固定为:detail=true
custom 可以忽略该参数

现货交易所、期货交易所对象的数据源设置为自定义数据源(feeder)时回测系统向自定义数据源服务发送请求的例子:

http://customserver:9090/data?custom=0&depth=20&detail=true&eid=Bitget&from=1351641600&period=86400000&round=true&symbol=BTC_USDT&to=1611244800&trades=1
http://customserver:9090/data?custom=0&depth=20&detail=true&eid=Futures_OKX&from=1351641600&period=86400000&round=true&symbol=BTC_USDT.swap&to=1611244800&trades=1

数据格式

返回的格式必须为以下两种格式其中之一(系统自动识别):

  • 模拟级Tick,以下是JSON数据范例:
  {
      "detail": {
          "eid": "Binance",
          "symbol": "BTC_USDT",
          "alias": "BTCUSDT",
          "baseCurrency": "BTC",
          "quoteCurrency": "USDT",
          "marginCurrency": "USDT",
          "basePrecision": 5,
          "quotePrecision": 2,
          "minQty": 0.00001,
          "maxQty": 9000,
          "minNotional": 5,
          "maxNotional": 9000000,
          "priceTick": 0.01,
          "volumeTick": 0.00001,
          "marginLevel": 10
      },
      "schema":["time", "open", "high", "low", "close", "vol"],
      "data":[
          [1564315200000, 9531300, 9531300, 9497060, 9497060, 787],
          [1564316100000, 9495160, 9495160, 9474260, 9489460, 338]
      ]
  }
  • 实盘级Tick,以下是JSON数据范例: Tick级回测的数据(包含盘口深度信息,深度格式为[价格, 量]的数组。可有多级深度,asks为价格升序,bids为价格倒序)。
  {
      "detail": {
          "eid": "Binance",
          "symbol": "BTC_USDT",
          "alias": "BTCUSDT",
          "baseCurrency": "BTC",
          "quoteCurrency": "USDT",
          "marginCurrency": "USDT",
          "basePrecision": 5,
          "quotePrecision": 2,
          "minQty": 0.00001,
          "maxQty": 9000,
          "minNotional": 5,
          "maxNotional": 9000000,
          "priceTick": 0.01,
          "volumeTick": 0.00001,
          "marginLevel": 10
      },
      "schema":["time", "asks", "bids", "trades", "close", "vol"],
      "data":[
          [1564315200000, [[9531300, 10]], [[9531300, 10]], [[1564315200000, 0, 9531300, 10]], 9497060, 787],
          [1564316100000, [[9531300, 10]], [[9531300, 10]], [[1564316100000, 0, 9531300, 10]], 9497060, 787]
      ]
  }
字段 说明
detail 请求数据的品种详细信息,包含计价币名称、交易币名称,精度,最小下单量等
schema 指定data数组中列的属性,区分大小写。仅限于 time, open, high, low, close, vol, asks, bids, trades
data 按照schema设置的列结构,记录的数据。

detail字段

字段 说明
eid 交易所Id,注意某个交易所现货与期货是不同的eid
symbol 交易品种代码
alias 当前交易品种代码对应的交易所中的symbol
baseCurrency 交易币种
quoteCurrency 计价币种
marginCurrency 保证金币种
basePrecision 交易币种精度
quotePrecision 计价币种精度
minQty 最小下单量
maxQty 最大下单量
minNotional 最小下单金额
maxNotional 最大下单金额
priceTick 价格一跳
volumeTick 下单量最小变动数值(下单量一跳)
marginLevel 期货杠杆值
contractType 对于永续合约设置为:swap,回测系统会继续发送资金费率、价格指数请求

特殊的列属性asksbidstrades

字段 说明 备注
asks / bids [[价格, 数量], …] 例如实盘级 Tick数据范例中的数据:[[9531300, 10]]
trades [[时间, 方向(0:买,1:卖), 价格, 数量], …] 例如实盘级 Tick数据范例中的数据:[[1564315200000, 0, 9531300, 10]]

期货交易所的永续合约回测时,自定义数据源还需要额外的资金费率数据、价格指数数据。只有当请求的行情数据返回时,返回的结构中detail字段包含"contractType": "swap"键值对,回测系统才会继续发送对于资金费率的请求。 当回测系统收到资金费率数据时,才会继续发送对于价格指数数据的请求。

资金费率数据结构如下:

{
    "detail": {
        "eid": "Futures_Binance",
        "symbol": "BTC_USDT.funding",
        "alias": "BTC_USDT.funding",
        "baseCurrency": "BTC",
        "quoteCurrency": "USDT",
        "marginCurrency": "",
        "basePrecision": 8,
        "quotePrecision": 8,
        "minQty": 1,
        "maxQty": 10000,
        "minNotional": 1,
        "maxNotional": 100000000,
        "priceTick": 1e-8,
        "volumeTick": 1e-8,
        "marginLevel": 10
    },
    "schema": [
        "time",
        "open",
        "high",
        "low",
        "close",
        "vol"
    ],
    "data": [
        [
            1584921600000,
            -16795,
            -16795,
            -16795,
            -16795,
            0
        ],
        [
            1584950400000,
            -16294,
            -16294,
            -16294,
            -16294,
            0
        ]
        // ...
    ]
}
  • 相邻的周期间隔8小时
  • 例如币安资金费率8小时更新一次,资金费率数据为什么为-16795? 因为和K线数据一样,为了避免网络传输过程中浮点数的精度丢失,数据采用整型;资金费率数据也可能为负值。

回测系统发出的资金费率数据请求,举例为:

http://customserver:9090/data?custom=0&depth=20&detail=true&eid=Futures_Binance&from=1351641600&period=86400000&round=true&symbol=BTC_USDT.funding&to=1611244800&trades=0

价格指数数据结构如下:


{
    "detail": {
        "eid": "Futures_Binance",
        "symbol": "BTC_USDT.index",
        "alias": "BTCUSDT",
        "baseCurrency": "BTC",
        "quoteCurrency": "USDT",
        "contractType": "index",
        "marginCurrency": "USDT",
        "basePrecision": 3,
        "quotePrecision": 1,
        "minQty": 0.001,
        "maxQty": 1000,
        "minNotional": 0,
        "maxNotional": 1.7976931348623157e+308,
        "priceTick": 0.1,
        "volumeTick": 0.001,
        "marginLevel": 10,
        "volumeMultiple": 1
    },
    "schema": [
        "time",
        "open",
        "high",
        "low",
        "close",
        "vol"
    ],
    "data": [
        [1584921600000, 58172, 59167, 56902, 58962, 0],
        [1584922500000, 58975, 59428, 58581, 59154, 0],
        // ...
    ]
}

回测系统发出的价格指数数据请求,举例为:

http://customserver:9090/data?custom=0&depth=20&detail=true&eid=Futures_Binance&from=1351641600&period=86400000&round=true&symbol=BTC_USDT.index&to=1611244800&trades=0

自定义数据源范例

指定数据源地址,例如:http://120.24.2.20:9090/data。自定义数据源服务程序使用Golang编写:

package main

import (
    "fmt"
    "net/http"
    "encoding/json"
)

func Handle (w http.ResponseWriter, r *http.Request) {
    // e.g. set on backtest DataSourse: http://xxx.xx.x.xx:9090/data

    // request: GET http://xxx.xx.x.xx:9090/data?custom=0&depth=20&detail=true&eid=OKX&from=1584921600&period=86400000&round=true&symbol=BTC_USDT&to=1611244800&trades=1
    //              http://xxx.xx.x.xx:9090/data?custom=0&depth=20&detail=true&eid=Futures_Binance&from=1599958800&period=3600000&round=true&symbol=BTC_USDT.swap&to=1611244800&trades=0
    fmt.Println("request:", r)

    // response
    defer func() {
        // response data
        /* e.g. data
        {
            "detail": {
                "eid": "Binance",
                "symbol": "BTC_USDT",
                "alias": "BTCUSDT",
                "baseCurrency": "BTC",
                "quoteCurrency": "USDT",
                "marginCurrency": "USDT",
                "basePrecision": 5,
                "quotePrecision": 2,
                "minQty": 0.00001,
                "maxQty": 9000,
                "minNotional": 5,
                "maxNotional": 9000000,
                "priceTick": 0.01,
                "volumeTick": 0.00001,
                "marginLevel": 10
            },
            "schema": [
                "time",
                "open",
                "high",
                "low",
                "close",
                "vol"
            ],
            "data": [
                [1610755200000, 3673743, 3795000, 3535780, 3599498, 8634843151],
                [1610841600000, 3599498, 3685250, 3385000, 3582861, 8015772738],
                [1610928000000, 3582499, 3746983, 3480000, 3663127, 7069811875],
                [1611014400000, 3662246, 3785000, 3584406, 3589149, 7961130777],
                [1611100800000, 3590194, 3641531, 3340000, 3546823, 8936842292],
                [1611187200000, 3546823, 3560000, 3007100, 3085013, 13500407666],
                [1611273600000, 3085199, 3382653, 2885000, 3294517, 14297168405],
                [1611360000000, 3295000, 3345600, 3139016, 3207800, 6459528768],
                [1611446400000, 3207800, 3307100, 3090000, 3225990, 5797803797],
                [1611532800000, 3225945, 3487500, 3191000, 3225420, 8849922692]
            ]
        }
        */
        
        // /* 模拟级 Tick
        ret := map[string]interface{}{
            "detail": map[string]interface{}{
                "eid": "Binance",
                "symbol": "BTC_USDT",
                "alias": "BTCUSDT",
                "baseCurrency": "BTC",
                "quoteCurrency": "USDT",
                "marginCurrency": "USDT",
                "basePrecision": 5,
                "quotePrecision": 2,
                "minQty": 0.00001,
                "maxQty": 9000,
                "minNotional": 5,
                "maxNotional": 9000000,
                "priceTick": 0.01,
                "volumeTick": 0.00001,
                "marginLevel": 10,
            },
            "schema": []string{"time","open","high","low","close","vol"},
            "data": []interface{}{
                []int64{1610755200000, 3673743, 3795000, 3535780, 3599498, 8634843151},  // 1610755200000 : 2021-01-16 08:00:00
                []int64{1610841600000, 3599498, 3685250, 3385000, 3582861, 8015772738},  // 1610841600000 : 2021-01-17 08:00:00
                []int64{1610928000000, 3582499, 3746983, 3480000, 3663127, 7069811875},
                []int64{1611014400000, 3662246, 3785000, 3584406, 3589149, 7961130777},
                []int64{1611100800000, 3590194, 3641531, 3340000, 3546823, 8936842292},
                []int64{1611187200000, 3546823, 3560000, 3007100, 3085013, 13500407666},
                []int64{1611273600000, 3085199, 3382653, 2885000, 3294517, 14297168405},
                []int64{1611360000000, 3295000, 3345600, 3139016, 3207800, 6459528768},
                []int64{1611446400000, 3207800, 3307100, 3090000, 3225990, 5797803797},
                []int64{1611532800000, 3225945, 3487500, 3191000, 3225420, 8849922692},
            },
        }
        // */

        /* 实盘级 Tick
        ret := map[string]interface{}{
            "detail": map[string]interface{}{
                "eid": "Binance",
                "symbol": "BTC_USDT",
                "alias": "BTCUSDT",
                "baseCurrency": "BTC",
                "quoteCurrency": "USDT",
                "marginCurrency": "USDT",
                "basePrecision": 5,
                "quotePrecision": 2,
                "minQty": 0.00001,
                "maxQty": 9000,
                "minNotional": 5,
                "maxNotional": 9000000,
                "priceTick": 0.01,
                "volumeTick": 0.00001,
                "marginLevel": 10,
            },
            "schema": []string{"time", "asks", "bids", "trades", "close", "vol"},
            "data": []interface{}{
                []interface{}{1610755200000, []interface{}{[]int64{9531300, 10}}, []interface{}{[]int64{9531300, 10}}, []interface{}{[]int64{1610755200000, 0, 9531300, 10}}, 9497060, 787},
                []interface{}{1610841600000, []interface{}{[]int64{9531300, 15}}, []interface{}{[]int64{9531300, 15}}, []interface{}{[]int64{1610841600000, 0, 9531300, 11}}, 9497061, 789},                
            },
        }
        */

        b, _ := json.Marshal(ret)
        w.Write(b)
    }()
}

func main () {
    fmt.Println("listen http://localhost:9090")
    http.HandleFunc("/data", Handle)
    http.ListenAndServe(":9090", nil)
}

测试策略,JavaScript范例:

/*backtest
start: 2021-01-16 08:00:00
end: 2021-01-22 00:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"OKX","currency":"BTC_USDT","feeder":"http://120.24.2.20:9090/data"}]
args: [["number",2]]
*/

function main() {
    var ticker = exchange.GetTicker()
    var records = exchange.GetRecords()
    Log(exchange.GetName(), exchange.GetCurrency())
    Log(ticker)
    Log(records)
}

本地回测引擎

发明者量化交易平台开源了JavaScript语言和Python语言的本地回测引擎,支持回测时设置底层K线周期。

回测页面快捷键

  • 策略编辑页面和策略回测页面切换的快捷键 使用Ctrl + ,键,切换回测页面和策略编辑页面,按住Ctrl键后,单按,键。

  • 策略保存的快捷键 使用Ctrl + s键,保存策略。

  • 启动回测的快捷键 使用Ctrl + b键,启动回测。

回测数据下载

  • 回测系统日志数据下载 打开具体策略,切换到「回测页面」进行策略回测。在回测结束后显示的策略「状态信息」一栏中,右上角有「下载表格」按钮。点击即可下载回测结束时状态栏数据的CSV格式文件。
  • 回测系统状态栏数据下载 打开具体策略,切换到「回测页面」进行策略回测。在回测结束后显示的策略「日志信息」一栏中,右上角有「下载表格」按钮。点击即可下载回测日志数据的CSV格式文件。

回测系统夏普算法

回测系统夏普算法源码:

function returnAnalyze(totalAssets, profits, ts, te, period, yearDays) {
    // force by days
    period = 86400000
    if (profits.length == 0) {
        return null
    }
    var freeProfit = 0.03 // 0.04
    var yearRange = yearDays * 86400000
    var totalReturns = profits[profits.length - 1][1] / totalAssets
    var annualizedReturns = (totalReturns * yearRange) / (te - ts)

    // MaxDrawDown
    var maxDrawdown = 0
    var maxAssets = totalAssets
    var maxAssetsTime = 0
    var maxDrawdownTime = 0
    var maxDrawdownStartTime = 0
    var winningRate = 0
    var winningResult = 0
    for (var i = 0; i < profits.length; i++) {
        if (i == 0) {
            if (profits[i][1] > 0) {
                winningResult++
            }
        } else {
            if (profits[i][1] > profits[i - 1][1]) {
                winningResult++
            }
        }
        if ((profits[i][1] + totalAssets) > maxAssets) {
            maxAssets = profits[i][1] + totalAssets
            maxAssetsTime = profits[i][0]
        }
        if (maxAssets > 0) {
            var drawDown = 1 - (profits[i][1] + totalAssets) / maxAssets
            if (drawDown > maxDrawdown) {
                maxDrawdown = drawDown
                maxDrawdownTime = profits[i][0]
                maxDrawdownStartTime = maxAssetsTime
            }
        }
    }
    if (profits.length > 0) {
        winningRate = winningResult / profits.length
    }
    // trim profits
    var i = 0
    var datas = []
    var sum = 0
    var preProfit = 0
    var perRatio = 0
    var rangeEnd = te
    if ((te - ts) % period > 0) {
        rangeEnd = (parseInt(te / period) + 1) * period
    }
    for (var n = ts; n < rangeEnd; n += period) {
        var dayProfit = 0.0
        var cut = n + period
        while (i < profits.length && profits[i][0] < cut) {
            dayProfit += (profits[i][1] - preProfit)
            preProfit = profits[i][1]
            i++
        }
        perRatio = ((dayProfit / totalAssets) * yearRange) / period
        sum += perRatio
        datas.push(perRatio)
    }

    var sharpeRatio = 0
    var volatility = 0
    if (datas.length > 0) {
        var avg = sum / datas.length;
        var std = 0;
        for (i = 0; i < datas.length; i++) {
            std += Math.pow(datas[i] - avg, 2);
        }
        volatility = Math.sqrt(std / datas.length);
        if (volatility !== 0) {
            sharpeRatio = (annualizedReturns - freeProfit) / volatility
        }
    }

    return {
        totalAssets: totalAssets,
        yearDays: yearDays,
        totalReturns: totalReturns,
        annualizedReturns: annualizedReturns,
        sharpeRatio: sharpeRatio,
        volatility: volatility,
        maxDrawdown: maxDrawdown,
        maxDrawdownTime: maxDrawdownTime,
        maxAssetsTime: maxAssetsTime,
        maxDrawdownStartTime: maxDrawdownStartTime,
        winningRate: winningRate
    }
}
策略编辑器 策略入口函数