After you have accomplished the design of a quantitative trading strategy, how can you know the basic situation of your strategy, such as the logic of the strategy and the direction of the strategy’s returns? Of course, we cannot use real money directly to run the strategy on the real trading market, but we can use historical data to test your strategy and know the profits of your strategy in the historical data.
FMZ Quant Trading Platform divides the backtest system into bot level and simulation level. The bot level is to backtest completely according to the complete historical data; while simulation level backtest generates tick
data according to the real K-line data at regular intervals for backtest. They are both based on the real historical data, but the bot level data is more accurate and the results are more credible. However, backtesting is just the performance of the strategy according to historical data. The historical data cannot fully represent the future market. The historical market may repeat, or it may also lead to the Black Swan. Therefore, the backtest results should be treated rationally and objectively.
The simulation level Tick generates the simulated tick data based on the underlying K-line period, each underlying K-line period will generate a maximum of 12 backtest time points; While the real market level Tick backtesting uses real collected second-by-second tick data, the volume of data is very large and the backtesting speed is slow, so it cannot be backtested for a very long period of time. The backtest mechanism of FMZ Quant allows the strategy to trade multiple times on a single K-line, avoiding the situation where the trading can only be executed at the closing price. It is more accurate while taking into account the speed of backtest.
Backtesting system mechanism description
Simulation Level Tick The simulation level Tick is based on the underlying K-line data of the backtest system, simulating tick data to backtest within the framework of the highest price, lowest price, opening price, and closing price values of a given underlying K-line bar according to a certain algorithm. As real-time tick data on the backtesting time series, it returns when the strategy program calls the interface. For details, please refer to: Backtesting System Simulation Level Mechanism Description.
Bot Level Tick
The bot level backtest is the actual tick level data in the Bar time series. For strategies based on tick level data, using real market level to backtest is closer to reality. In bot level backtest, tick data is real recorded data, not simulated one. It supports depth data, record data playback of market tradings, custom depth and each individual trading data. The maximum size of the real-market-level data backtest is up to a maximum of 50MB, with no limit on the backtest time range within the upper limit of the dataset. If you need to enlarge the backtest time range as much as possible, you can reduce the value of the depth gear setting and do not use each individual trading data to increase the backtest time range. Call GetDepth
, GetTrades
functions to obtain playback market data. At a moment of market data on the timeline, calling GetTicker
, GetTrades
, GetDepth
and GetRecords
will not push the time multiple times when the time moves on the backtest timeline (which will not trigger a jump to the next market data moment). Repeated calls to one of the above functions will push the backtest time to move on the backtest timeline (jump to the next market data moment). When the real market level is used for backtest, an earlier time is not recommended to choose. There may be no real-market-level data in the premature time period.
Bot-level Tick and Simulation-level Tick modes, the transaction matching mechanism of the backtest system: order transaction matching is carried out according to the price seen and the full volume is traded. Therefore, the scenario of partial transaction cannot be tested in the backtest system.
The backtesting system supports backtesting strategies written and designed by: JavaScript
, TypeScript
, Python
, C++
, PINE
, MyLanguage
, Blockly
visualization.
The backtest of JavaScript and C++ trading strategies is conducted in the browser, and the real market bot or WexApp emulated exchange real market (i.e. the WexApp emulated exchange of FMZ Quant Trading platform) runs without installing any other software, libraries or modules.
The backtest of Python is performed on the docker; it can be performed on the public server added by FMZ Quant Trading platform, and it can also be performed on user’s own docker. The real market operation and backtest both rely on the Python installed on the system where the docker is located. If some libraries are needed, they need to be installed manually (only common Python libraries are supported on FMZ Quant public servers).
It supports JavaScript strategy backtesting debugging in Chrome DevTools, please refer to.
The parameter optimization function of the backtest system of FMZ Quant Trading Platform is to set the parameter combinations according to every parameter optimization option during backtesting. In the strategy parameter section of the “Simulation Backtesting” page, check the Optimization option on the right side of the strategy parameter to display the optimization settings.
JavaScript
, PINE
, and My Language
, and does not support parameter optimization on templates.Parameter combinations are generated based on the minimum
, maximum
, and step size
settings. The backtesting system iterates through these parameter combinations for backtesting (i.e., backtesting each parameters combination once). Only strategy parameters of number type can be optimized in the backtesting system.
In the strategy editing page, in the pagination of “Backtest” (namely the backtest system), you can set options like backtest configurations and strategy parameters to backtest the strategy. Backtest settings refer to backtest time range, exchange platform, slippoint and service fee etc.; while strategy parameters are used to set parameter options for strategies. When these parameters are set, you can follow the set backtesting strategy, then how to save the set configuration information?
backtest
(saved in the strategy code via the “Save Backtest Settings” button), the backtest system automatically configures the backtest settings to the backtest configuration information when the “Save Strategy” button was last clicked for the current strategy.backtest
in the strategy editing area./*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"}]
*/
Click “Save backtest settings”, there are slight format differences on JavaScript
/Python
/C++
/MyLanguage
/PINE
languages when saving backtest settings to the strategy code:
MyLanguage:
(*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 Language:
/*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"}]
*/
The backtesting system of the FMZ Quant Trading Platform supports custom data sources, the backtesting system uses the GET
method to request a custom URL (publicly accessible URL) to obtain an external data source for backtest. The additional request parameters are as follows:
Parameter | Meaning | Explanation |
---|---|---|
symbol | Symbol Name | Spot market data, such as: BTC_USDT , futures market data, such as: BTC_USDT.swap , futures perpetual contract funding rate data, such as: BTC_USDT.funding , futures perpetual contract price index data, such as: BTC_USDT.index |
eid | Exchanges | such as OKX, Futures_OKX |
round | Data Accuracy | True means that the specific precision is defined in the data fed back by the custom data source. The request sent by the FMZ Quant Trading Platform Backtesting System to the custom data source is fixed as: round=true |
period | K-line Data Period (Milliseconds) | such as: 60000 is a 1-minute period |
depth | Depth Levels | 1-20 |
trades | Whether Need to Split Data | true(1) / false(0) |
from | Start Time | unix timestamp |
to | End Time | unix timestamp |
detail | Request data for symbol details | True means that it needs to be provided by a custom data source. The request sent by the FMZ Quant Trading Platform Backtesting System to the custom data source is fixed as: detail=true |
custom | – | This parameter can be ignored |
When the data source of the spot exchange and futures exchange objects is set to a custom data source (feeder), the backtesting system sends a request to the custom data source service:
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
The returned format must be one of the following two formats (which will be recognized by the system automatically):
Simulation level Tick, the following is an example of JSON 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":[
[1564315200000, 9531300, 9531300, 9497060, 9497060, 787],
[1564316100000, 9495160, 9495160, 9474260, 9489460, 338]
]
}
Bot level tick, the following is an example of JSON data:
Tick-level backtest data (contains information on the depth of the market, and the depth format is an array of [price, volume]
. It can have multiple levels of depth, asks
for price ascending order, bids
for price descending order).
{
"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]
]
}
Field | Description |
---|---|
detail | Detailed information on the requested data type, |
including the name of the denominated currency, the name of the trading currency, the precision, the minimum order quantity, etc. | | schema | It specifies the attributes of the columns in the data array, which is case sensitive and is only limited to time, open, high, low, close, vol, asks, bids, trades| | data | The column structure, recorded data according to the schema settings.|
detail field
Field | Description |
---|---|
eid | Exchange Id, please note that the spot and futures of a |
certain exchange have different eids. | |
symbol | Trading product code |
alias | The symbol in the exchange corresponding to the current |
trading product code | |
baseCurrency | Trading Currency |
quoteCurrency | Denominated Currency |
marginCurrency | Margin Currency |
basePrecision | Transaction Currency Accuracy |
quotePrecision | Pricing Currency Accuracy |
minQty | Minimum Order Quantity |
maxQty | Maximum Order Quantity |
minNotional | Minimum Order Amount |
maxNotional | Maximum Order Amount |
priceTick | Price Jump |
volumeTick | Minimum change value of order quantity (one jump in |
order quantity) | |
marginLevel | Futures Leverage Value |
contractType | For perpetual contracts set to: swap , the |
backtest system will continue to send funding rate and price index requests |
Special column attributes asks
, bids
, trades
:
Field | Description | Remarks |
---|---|---|
asks / bids | [[price, volume], …] | For example, the data in |
the Live Trading Level Tick
data example: [[9531300, 10]]
|
| trades | [[time,direction(0:buy,1:sell),price,volume], …] | For
example, the data in the Live Trading Level Tick
data example:
[[1564315200000, 0, 9531300, 10]]
|
When backtesting perpetual contracts on futures exchanges, custom
data sources also require additional funding rate data and price
index data. The backtesting system will continue to send requests
for funding rates only when the requested market data is returned
and the detail field in the returned structure contains the
"contractType": "swap"
key-value pair.
When the backtesting system receives funding rate data, it will continue to send requests for price index data.
The funding rate data structure is as follows:
{
"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
]
// ...
]
}
An example of a funding rate data request from the backtesting system is:
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
The price index data structure is as follows:
{
"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],
// ...
]
}
An example of a price index data request sent by the backtesting system is:
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
Specify the data source address, e.g., http://120.24.2.20:9090/data
. The custom data source service program is written using 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]
]
}
*/
// /* Simulation level 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},
},
}
// */
/* Bot level 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)
}
Test strategy, JavaScript
example:
/*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)
}
FMZ Quant Trading Platform has open-sourced for the JavaScript
language and the Python
language of the local backtest engine, supporting setting Underlying K-line Period during backtesting.
Shortcut key for switching between strategy “Editing” page and the “Backtesting” page
Use the key Ctrl +,
to switch back to “Backtest” page and “Edit Strategy” page. After the key Ctrl
, press the key ,
.
Shortcut key for saving strategy
Use the key Ctrl + s
to save strategies.
Shortcut for starting strategy backtest
Use the key Ctrl + b
to enable “Start Backtest”.
The source code of Sharpe algorithm in backtesting system:
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
}
}
Strategy Editor
Strategy Entry Functions