输入/搜索内容
入门
欢迎使用发明者量化交易平台
快速开始
密钥安全性
平台基础
账号与计费
实盘计费与充值
子账号
交易所
通用协议
本地凭据文件
交易所特殊说明
证券与期货
加密货币
托管者
策略库
实盘
编写策略
开发工具
回测系统
进阶专题
数据与研究
对外接口

用历史数据检验策略:回测系统用历史行情驱动策略代码,模拟撮合与账户,给出收益、回撤等结果。回测只反映策略在历史行情下的表现,不代表未来收益。

回测用平台的历史行情驱动策略代码:回测引擎维护一个虚拟时钟,为每个交易所对象维护一个模拟账户,策略调用的行情、下单、账户等函数都由引擎按历史数据应答。回测结果只反映策略在历史行情下的表现,历史行情不能代表未来,对回测结果要理性、客观地看待。

发起回测

  • 网页:打开策略编辑页面,切换到「模拟回测」分页,设置回测配置和策略参数后点击「开始回测」(快捷键见回测页面快捷键)。回测配置可以保存进策略源码,见回测配置与保存。
  • AI助手:通过MCP工具run_backtest发起回测、get_backtest读取结果,见AI接入。MCP发起的回测只使用模拟级Tick模式。
  • 本机:使用开源的本地回测引擎,见本地回测引擎。

回测配置项

配置项说明
时间范围回测的开始时间和结束时间。
K线周期策略调用GetRecords()默认得到的K线周期。
底层K线周期模拟级Tick模式下用来生成tick的K线周期。越小越接近真实行情,回测也越慢;策略K线由底层K线合成,不能小于底层K线周期。
模式模拟级Tick或实盘级Tick,见回测模式与撮合。
交易所、交易对每个交易所对象有各自的模拟账户;交易对写成BTC_USDT的形式。期货交易所需要在策略中先调用exchange.SetContractType()设置合约,才能获取行情、下单。
初始资金计价币(如USDT)和交易币(如BTC)的初始余额。币本位合约以交易币作保证金,需要设置交易币余额。
手续费挂单(maker)和吃单(taker)费率,单位为百分比,默认取该交易所市场的配置。限价单下单时立即成交按吃单费率计算,挂在盘口上之后才成交按挂单费率计算。
滑点单位为价格最小变动单位(一跳)的个数,加在模拟盘口买一价、卖一价的外侧,默认为0。
网络延迟单位为毫秒,策略每调用一次交易所接口,虚拟时钟前进相应时间,默认为200。
深度档位、每档数量GetDepth()返回的档位数(1~20)和模拟盘口每档的数量;实盘级Tick模式下深度档位是向数据源请求的真实深度档数。
K线最大条数第一次调用GetRecords()时返回的历史K线条数上限(100~5000,默认300)。
日志条数回测保留的运行日志、收益日志、图表数据的条数上限。
数据源默认使用平台的历史数据,也可以使用自定义数据源,见自定义数据源。

容错测试

回测页面另外提供「容错测试」:按一定概率(默认0.5)让交易所接口调用失败,并且每种接口的第一次调用一定失败,失败时记录错误日志FaultTolerant Test。用于检验策略对接口失败的处理,例如是否用_C()重试。

回测中的策略

  • IsVirtual()返回true,不应在回测中执行的逻辑可以据此跳过,见IsVirtual。
  • 时间是虚拟时间:Unix()、_D()等读取的是回测时钟,Sleep()推动时钟前进。时钟越过结束时间时,引擎抛出EOF异常结束回测,此时不会调用onexit()。
  • 回测中GetCommand()收不到交互命令,不支持onerror(),网络请求类功能受限。

回测分为模拟级Tick和实盘级Tick两种模式。两者都基于真实的历史数据:模拟级Tick由K线生成tick,实盘级Tick回放真实记录的tick,后者更精确,也更慢。

模拟级Tick

回测引擎在每根底层K线的开盘价、最高价、最低价、收盘价构成的价格框架内,沿 开盘→最低/最高→收盘 的路径生成2~14个模拟tick,K线的成交量分摊到这些tick上;策略调用行情接口时得到的是当前模拟tick的数据。因此每根底层K线上有多个回测时间点,策略可以在一根K线内多次交易,而不是只能按收盘价成交。底层K线周期越小,生成的tick越接近真实走势,回测也越慢。机制详见回测系统模拟级别机制说明、回测系统机制说明。

模拟盘口:卖一价 = 当前tick收盘价 + 一跳 + 滑点,买一价 = 收盘价 − 一跳 − 滑点(滑点以跳数计);GetDepth()返回按此间隔排列的若干档模拟深度,每档数量为配置中的「每档数量」。

实盘级Tick

使用平台真实记录的逐秒tick数据,包含盘口深度(档位可设置,最多20档),可以选择回放逐笔成交数据;GetDepth()、GetTrades()返回回放的真实数据。由于数据量大、回测速度慢,单次回测的数据上限为50MB,可回测的时间范围因此受限;需要更长的时间范围时,可以降低深度档位、不使用逐笔成交数据。较早的时间段可能没有实盘级数据,时间范围不宜选得过早。

在某个行情时刻,GetTicker()、GetDepth()、GetTrades()、GetRecords()各调用一次不会推动回测时间;再次调用其中同一个函数时,回测时间跳到下一个行情时刻。实盘级Tick模式下,策略循环中的Sleep()宜设得短一些(例如100毫秒)。

撮合规则

两种模式使用相同的撮合规则:

  • 按价格触及成交,并且一次全部成交,回测中不会出现部分成交。
  • 市价单在当前tick按卖一价/买一价成交;现货市价买单的数量是计价币金额。
  • 限价买单价格大于等于卖一价、限价卖单价格小于等于买一价时成交,挂单后的每个tick都会检查。下单时立即成交的,按市场价成交并收取吃单(taker)手续费;挂在盘口之后被价格触及而成交的,按委托价成交并收取挂单(maker)手续费。
  • 实盘级Tick模式下,挂在买一/卖一价位上的订单,要等排在它前面的挂单量被消耗后才成交。
  • 期货按 名义价值 ÷ 杠杆 冻结保证金;行情数据中包含资金费率时,永续合约按资金费率结算资金费。

数据粒度的影响

同一个策略在不同的数据粒度下(实盘级Tick、底层K线周期较小的模拟级Tick、底层K线周期较大的模拟级Tick等)回测,交易次数和盈亏都会不同。数据粒度大时回测快,但结果可能失真,回测时应尽量使用较小的数据粒度。可以用下面的策略在几种粒度下分别回测对比:

javascript
/*backtest start: 2025-04-01 08:00:00 end: 2025-04-18 00:00:00 period: 1m exchanges: [{"eid":"Binance","currency":"BTC_USDT","balance":1000000}] mode: 1 */ var delta = 50 var lotSize = 0.001 var lastPrice = null var direction = null function main() { while (true) { var ticker = _C(exchange.GetTicker) if (!lastPrice) { lastPrice = ticker.Last } var diff = ticker.Last - lastPrice if ((!direction || direction == "long") && diff >= delta) { // 价格上涨超过阈值 -> 做空 exchange.Sell(ticker.Last, lotSize) Log("Short @", ticker.Last) direction = "short" } else if ((!direction || direction == "short") && diff <= -delta) { // 价格下跌超过阈值 -> 做多 exchange.Buy(ticker.Last, lotSize) Log("Long @", ticker.Last) direction = "long" } // Tick 模式中尽量短,K线模式中没有影响 Sleep(100) } }

「模拟回测」分页中的回测配置(时间范围、交易所、手续费等)和策略参数可以随策略保存,再次打开策略时自动载入。

保存

  • 点击「保存回测设置」:把回测配置和策略参数以注释(backtest注释块)的形式写在策略源码开头。
  • 点击「保存策略」:平台同时记录当前的回测配置和策略参数。

载入

  • 打开或刷新策略编辑页面时,优先载入源码中backtest注释块记录的配置。
  • 源码中没有backtest注释块时,载入最后一次「保存策略」时记录的配置。
  • 在源码中手动修改了backtest注释块后,点击注释块上方的「回测设置」按钮,把修改同步到回测页面的选项中。

注释块格式

在该语言的块注释起始符后紧接着写backtest,之后每行一个键: 值:

javascript
/*backtest start: 2024-01-01 00:00:00 end: 2024-03-01 00:00:00 period: 1h basePeriod: 15m exchanges: [{"eid":"Binance","currency":"BTC_USDT","balance":10000,"stocks":0,"fee":[0.1,0.1]}] args: [["fast",5],["slow",20]] */

各语言的注释写法:JavaScript、TypeScript、Rust、PINE语言使用/*backtest ... */;Python使用'''backtest ... ''';My语言使用(*backtest ... *)。

键格式说明
start、endYYYY-MM-DD HH:mm:ss开始、结束时间,按浏览器所在时区解析。
period1m、1h、1d等,或秒数策略K线周期。
basePeriod同上底层K线周期,不写时与period相同;实盘级Tick模式下忽略。
mode1实盘级Tick模式;不写为模拟级Tick模式。
exchangesJSON数组每个元素对应一个交易所对象,字段见下表。
argsJSON数组策略参数,[["参数名", 值], ...];第三个元素为模板Id时设置该模板的参数:["参数名", 值, 模板Id]。

exchanges元素的字段,除eid、currency外都可以省略:

字段说明
eid交易所Id,例如Binance、Futures_OKX。
currency交易对,例如BTC_USDT。
balance、stocks计价币、交易币的初始余额。
fee[挂单费率, 吃单费率],单位为百分比。
feeMin每笔成交的最低手续费,只对部分市场生效。
depthDeep、depthAmount深度档位、模拟盘口每档的数量。
tradesMode实盘级Tick模式下是否回放逐笔成交:"0"回放,"1"不回放。
feeder自定义数据源地址,见自定义数据源。

「保存回测设置」还会写入一些以bt开头的键(例如btSlipPoint滑点、btNetDelay网络延迟、btFaultTolerant容错概率、btMaxBarLenK线最大条数),记录回测页面上其他选项的取值,不建议手工修改。本地回测引擎也读取同样的注释块,见本地回测引擎。

编程语言

回测系统支持以下语言编写的策略:JavaScript、TypeScript、Python、Rust、PINE语言、My语言、Blockly可视化、Workflow工作流。

  • JavaScript策略(TypeScript先编译为JavaScript)在浏览器中回测,回测引擎以WebAssembly形式运行,不需要安装任何软件。JavaScript策略回测时可以在Chrome浏览器的DevTools中调试,见参考说明。
  • Rust策略由平台服务器编译,编译结果在浏览器中回测;策略中通过frontmatter声明的第三方crate在编译时自动获取,本地不需要安装工具链。
  • Python策略在托管者上回测,可以使用平台的公共服务器,也可以使用自己的托管者。回测与实盘都依赖托管者所在系统的Python 3环境,需要的第三方库要自行安装;公共服务器只提供常用的库。
  • Workflow工作流策略回测时可以可视化查看各节点的执行状态和数据流转。

交易所

回测数据来自平台的历史数据,可以回测的交易所以回测页面中可选的为准(也可以通过MCP工具list_exchanges查看,backtest为true的交易所有历史数据)。

  • 加密货币:主流交易所的现货和期货,例如Binance与Futures_Binance、OKX与Futures_OKX、HTX与Futures_HTX、Bybit与Futures_Bybit、Bitget与Futures_Bitget、GateIO与Futures_GateIO,支持交易所的全部品种。
  • 富途证券(Futures_Futu):港股、美股等市场。回测只支持日线级别数据,currency设置为STOCK,在策略中用exchange.SetContractType()设置股票代码:
javascript
/*backtest start: 2024-05-01 00:00:00 end: 2025-02-17 00:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_Futu","currency":"STOCK","fee":[0.03,0.03]}] */ function main() { var info = exchange.SetContractType("TSLA.US") // 设置股票代码:特斯拉 Log("info:", info) // 合约信息:InstrumentID、PriceTick、LotTick、VolumeMultiple等 Log(exchange.GetTicker()) // 回测时间点的日线行情 }

参数调优在回测时按设置的范围生成多组参数,逐组回测。在「模拟回测」分页的策略参数部分,勾选参数右侧的调优选项后出现调优设置:

  • 最小值:参数的起始值。
  • 最大值:参数递增后的最大值。
  • 步长:每次递增的量。
  • 并发线程:参数调优时同时执行的回测数。该选项只支持JavaScript、PINE、My语言策略的参数调优,不支持模板上的参数调优。

回测系统按最小值、最大值、步长生成参数组合,对每种组合各回测一次。只有**数字型(number)**的策略参数可以设置调优。

回测结束后,回测页面显示收益曲线、统计指标、状态信息、日志信息和账户信息。

收益曲线

收益曲线由策略调用LogProfit()记录的收益值组成(LogProfit)。策略不调用LogProfit()时没有收益曲线,下面依赖收益序列的统计指标也无法计算,只能从账户信息中查看回测结束时的资产。MCP工具get_backtest返回的profit、max_drawdown同样来自LogProfit()。

统计指标

统计指标由收益序列profits(每个元素为[时间戳, 收益])和初始资产totalAssets按下面的算法计算:

指标含义
收益率(totalReturns)最后一个收益值 ÷ 初始资产。
年化收益(annualizedReturns)收益率 × 一年的时长(yearDays天)÷ 回测时长,按比例线性折算。
最大回撤(maxDrawdown)资产(初始资产 + 收益)相对此前最高点下跌的最大比例。maxDrawdownStartTime为该最高点的时间,maxDrawdownTime为回撤最深的时间。
胜率(winningRate)收益序列中高于前一个点的点所占的比例(第一个点与0比较)。统计的是收益记录,不是逐笔交易的胜率。
波动率(volatility)把回测时间按天切分,每天的收益额 ÷ 初始资产,再乘以yearDays折算为年化值(没有收益记录的日子按0计),取这些值的总体标准差。
夏普比率(sharpeRatio)(年化收益 − 无风险利率3%)÷ 波动率;波动率为0时为0。

yearDays是年化时使用的一年天数,由回测页面传入。注意波动率是把每日收益率直接乘以yearDays年化,而不是常见的乘以√yearDays,因此这里的夏普比率不宜与其他平台的数值直接比较。

算法源码:

javascript
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 } }

数据下载

  • 状态栏数据下载:回测结束后,在「状态信息」栏右上角点击「下载表格」,下载回测结束时状态栏数据的CSV文件。
  • 日志数据下载:在「日志信息」栏右上角点击「下载表格」,下载回测日志的CSV文件。

发明者量化交易平台的回测系统支持自定义数据源,回测时由平台的数据服务器使用GET方法请求自定义的URL获取数据,因此该URL必须能从公网访问。请求附加的参数如下:

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

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

url
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数据范例:
    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为价格倒序)。
    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", "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设置的列结构,记录的数据。

数值精度

请求中固定带round=true,返回的数值都写成按精度放大后的整数,以免传输过程中丢失浮点数精度:

  • 价格类数值(open、high、low、close,asks/bids和trades中的价格)= 实际值 × 10^quotePrecision。
  • 数量类数值(vol,asks/bids和trades中的数量)= 实际值 × 10^basePrecision。

例如上面的范例中quotePrecision为2,9531300表示价格95313.00;basePrecision为5,787表示数量0.00787。时间列(time及trades中的时间)是毫秒时间戳,不放大。

detail字段

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

特殊的列属性asks、bids、trades:

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

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

资金费率数据结构如下:

json
{ "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小时
  • 资金费率数据为什么是 -16795?
    与K线数据一样按精度放大为整数:该数据的quotePrecision为8,-16795表示资金费率-0.00016795。资金费率可以为负值。

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

url
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

价格指数数据结构如下:

json
{ "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] ] }

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

url
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://<服务器地址>:9090/data(<服务器地址>替换为该服务器的公网IP或域名)。自定义数据源服务程序使用Golang编写:

golang
package main import ( "fmt" "net/http" "encoding/json" ) func Handle (w http.ResponseWriter, r *http.Request) { // e.g. set on backtest DataSource: 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范例:

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://<服务器地址>: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的本地回测引擎,与云端回测使用相同的引擎核心和历史数据(从平台的数据服务器下载),可以在自己的电脑上快速回测JavaScript、Python策略:

Python

安装(需要Python 3和pip):

bash
pip install https://github.com/fmzquant/backtest_python/archive/master.zip pip install pandas matplotlib # 仅在使用 Join(True)、Show() 时需要

第一次使用时会从数据服务器下载对应系统的引擎文件,之后除了历史数据不再联网。策略文件就是普通的FMZ策略,加上开头的backtest配置注释(格式见回测配置与保存)和几行引擎调用代码:

python
'''backtest start: 2026-09-01 00:00:00 end: 2026-09-15 00:00:00 period: 1h basePeriod: 15m exchanges: [{"eid":"Binance","currency":"BTC_USDT","balance":10000,"stocks":0}] ''' import json from fmz import * task = VCtx(__doc__) # 按上方注释中的配置初始化回测引擎,exchange、Log、TA 等成为全局对象 # 以下为待测试的策略代码,可以从平台直接复制 def main(): Log(exchange.GetAccount()) while True: r = exchange.GetRecords() LogStatus(_D(), r[-1]["Close"]) Sleep(60 * 60 * 1000) try: main() except EOFError: # 虚拟时钟到达结束时间时,引擎抛出 EOFError pass result = json.loads(task.Join(False)) # 原始回测结果(JSON) print(result["LogsCount"], result["Elapsed"] / 1e6) # task.Show() # 或者显示收益图表(需要 matplotlib)

用python strategy.py运行。task.Join(False)返回原始回测结果的JSON,task.Join(True)返回收益数据的pandas表格,task.Show()画出收益曲线。

JavaScript

bash
npm install git+https://github.com/fmzquant/backtest_javascript.git
javascript
var fmz = require("fmz") var task = fmz.VCtx({ start: "2026-09-01 00:00:00", end: "2026-09-15 00:00:00", period: "1h", basePeriod: "15m", exchanges: [{eid: "Binance", currency: "BTC_USDT", balance: 10000, stocks: 0}] }) // 从这里开始 exchange、Log、TA 等成为全局对象,粘贴策略代码后调用 main() function main() { Log(exchange.GetAccount()) while (true) { var r = exchange.GetRecords() LogStatus(_D(), r[r.length - 1].Close) Sleep(60 * 60 * 1000) } } try { main() } catch (e) { // 虚拟时钟到达结束时间时,引擎抛出 "EOF" } var result = JSON.parse(task.Join()) // 与 Python 引擎 Join(False) 的结果相同 console.log(result.LogsCount)

与云端回测的区别

  • 只支持JavaScript和Python策略,不会自动加载模板:策略引用的模板代码需要粘贴到文件中,因此依赖交易类库的PINE、My语言策略不能在本地回测。
  • 策略参数不会自动注入,需要在代码中定义为全局变量。
  • 注释中的start、end按本机时区解析;网络延迟固定为200毫秒,不支持滑点和实盘级Tick模式。
  • 收益、回撤和收益曲线同样只在策略调用LogProfit()时才有。

AI回测

在AI助手中可以直接让它回测:AI助手通过MCP工具run_backtest发起云端回测,用get_backtest读取收益、回撤、错误日志等结果,见AI接入。也可以让AI助手在本机安装本地回测引擎,快速修改、回测,再用云端回测确认一次。

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