回测系统
用历史数据检验策略:回测系统用历史行情驱动策略代码,模拟撮合与账户,给出收益、回撤等结果。回测只反映策略在历史行情下的表现,不代表未来收益。
概述与发起回测
回测用平台的历史行情驱动策略代码:回测引擎维护一个虚拟时钟,为每个交易所对象维护一个模拟账户,策略调用的行情、下单、账户等函数都由引擎按历史数据应答。回测结果只反映策略在历史行情下的表现,历史行情不能代表未来,对回测结果要理性、客观地看待。
发起回测
- 网页:打开策略编辑页面,切换到「模拟回测」分页,设置回测配置和策略参数后点击「开始回测」(快捷键见
回测页面快捷键)。回测配置可以保存进策略源码,见回测配置与保存。 - 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、end | YYYY-MM-DD HH:mm:ss | 开始、结束时间,按浏览器所在时区解析。 |
| period | 1m、1h、1d等,或秒数 | 策略K线周期。 |
| basePeriod | 同上 | 底层K线周期,不写时与period相同;实盘级Tick模式下忽略。 |
| mode | 1 | 实盘级Tick模式;不写为模拟级Tick模式。 |
| exchanges | JSON数组 | 每个元素对应一个交易所对象,字段见下表。 |
| args | JSON数组 | 策略参数,[["参数名", 值], ...];第三个元素为模板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给出,见数据格式。 |
| period | K线数据的周期(毫秒) | 例如: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助手在本机安装本地回测引擎,快速修改、回测,再用云端回测确认一次。