Best Practices for High-Frequency Backtesting of Market-Making Strategies in Cryptocurrency

In cryptocurrency high-frequency trading, backtesting efficiency and accuracy are critical for success. DolphinDB, with its high-performance time-series data processing and backtesting capabilities, provides an optimized solution for crypto trading strategies.

This document demonstrates high-frequency backtesting of the Avellaneda-Stoikov (AS) market-making strategy using Binance BTC/USDT perpetual contracts in DolphinDB. Through efficient processing of massive market data, precise strategy model derivation, and a full demonstration of the backtesting process, readers will master how to build a professional digital asset backtesting system with DolphinDB, execute high-frequency market-making strategies, and complete backtests within seconds — offering a powerful edge in the fast-changing crypto market.

All sample code in this tutorial is developed based on DolphinDB version 3.00.2, with a minimum requirement of Backtest plugin version 3.00.2.7, and is estimated to use about 2.4 GB of memory.

1. Overview

This tutorial demonstrates backtesting the Avellaneda-Stoikov (AS) market-making strategy for BTC/USDT perpetual contracts on Binance using DolphinDB.

1.1 BTC/USDT Perpetual Contract

BTC/USDT is a trading pair that represents the price relationship between Bitcoin (BTC) and Tether (USDT), which is commonly regarded as a benchmark for Bitcoin’s price. For example, as shown in the figure below (captured from Binance in December 2024), the BTC/USDT price is 96,191.03, meaning it costs 96,191.03 USDT to purchase 1 BTC.

Figure 1. Figure 1-1 BTC/USDT Perpetual Contract on Binance

A perpetual contract is a type of financial derivative based on digital assets, allowing traders to take leveraged long or short positions without holding the underlying asset. Perpetual contracts differ from traditional futures contracts with key features:

  • No expiration: Unlike traditional futures commodities, perpetual contracts have no delivery date, allowing traders to hold positions indefinitely.
  • Funding rate mechanism: Designed to align contract prices with spot prices, adjusting incentives between longs and shorts. If the funding rate is positive (longs pay shorts), this means the perpetual contract price is higher than the spot price, and long traders need to pay a fee to short traders, and vice versa. Position holders pay or receive the funding rate every 8 hours.
  • High leverage: Exchanges offer leverage ranging from 1x to up to 125x.

1.2 Avellaneda-Stoikov Strategy

Market makers provide liquidity by quoting both buy and sell prices, profiting from the bid-ask spread. This tutorial implements the Avellaneda-Stoikov (AS) market-making model for BTC/USDT perpetual contracts.

1.2.1 Introduction

The AS model, proposed by Marco Avellaneda and Sasha Stoikov in 2008 in High-frequency trading in a limit order book, studies how a market maker can optimally quote prices while managing inventory risk. The model adjusts quotes dynamically based on market volatility, inventory levels, and risk aversion to minimize risk and maximize profit.

1.2.2 AS Strategy Model Derivation

The derivation of the AS model consists of two main steps. First, the market maker calculates the indifference price based on current inventory and risk preferences. Second, the market maker estimates the probability of order execution by analyzing the distance between the quotes and the market mid-price, considering market conditions and risk tolerance, to determine the optimal quotes. In this way, the market maker can provide liquidity while minimizing inventory risk and maximizing profit.

Indifference Price Formula

  • s: market mid-price
  • q: current inventory
  • γ: risk aversion coefficient
  • σ: price volatility
  • T: normalized end time
  • t: current time

This formula shows that with higher inventory and greater volatility, the indifference price is adjusted downward to reduce risk.

Spread Formula

  • : symmetric bid-ask spread
  • γ, σ, T, t: same as above
  • k: market liquidity

This formula indicates that the optimal spread increases with higher risk aversion, greater volatility, and improved liquidity, compensating the market maker for additional risk.

Quote Formula

1.3 Cryptocurrency Backtesting

Cryptocurrency backtesting evaluates a trading strategy performance using historical market data. By simulating trading activity, key metrics such as return, maximum drawdown, and win rate can be calculated to estimate live trading potential. Given the 24/7 nature and high volatility of the cryptocurrency market, particular attention should be paid during backtesting to data quality, trading frequency, fees, slippage, and appropriate risk control measures to handle extreme market conditions and more accurately assess the strategy performance.

DolphinDB provides a simulated matching engine and backtest engine that allow custom strategies to be tested on historical data to evaluate their performance and robustness.

  • Simulated matching engine: uses market data and user orders as input, applies matching rules, and outputs execution results to the order detail table. Orders that are not filled immediately are retained for matching with future market data or user cancellation.
  • Backtest engine: executes the strategy and risk control management based on historical market data to evaluate strategy performance on historical data, including order generation, matching, capital management, and performance metrics such as return and trade details.

2. Market Data Acquisition

The core objective of strategy backtesting is to evaluate a strategy’s profitability and risk characteristics under historical market conditions, thereby estimating its potential performance in live trading.Therefore, acquiring accurate and relevant market data is a critical first step before implementing any trading strategy. Taking Binance as an example, the exchange provides comprehensive API services and detailed documentation for spot, margin, futures, and options trading on over 300 cryptocurrencies and fiat pairs. Market data can be retrieved from Binance via WebSocket.Correspondingly, DolphinDB supports market data acquisition through its WebSocket and HttpClient plugins, enabling users to ingest real-time or historical cryptocurrency trading data directly into DolphinDB. For detailed cryptocurrency data acquisition and storage solutions, see Chapter 2 Market Data Access, Integration and Storage in the DolphinDB Whitepaper: Cryptocurrency Solutions.

This tutorial uses Binance’s market snapshot data, specifically the aggregated trade data and the 500-level order book data for USDT-margined contracts. In addition, perpetual contracts require funding rate details to calculate the strategy’s return. This section primarily introduces the data required for strategy backtesting and explains how to obtain funding rate data.

The attached data.bin file contains USDT-margined contract data from September 9, 2024.

2.1 USDT-Margined Aggregated Trade Data

The data structure for USDT-margined aggregated trade data is as follows:

colNames = ["eventTime", "code", "id", "price", "quantity", "firstId", "lastId", 
            "tradeTime", "marketMaker", "currentTime"]
colTypes = [TIMESTAMP, SYMBOL, LONG, DOUBLE, DOUBLE, LONG, LONG,
            TIMESTAMP, BOOL, TIMESTAMP]
trade = table(1:0, colNames, colTypes)

Sample data is as shown below:

Figure 2. Figure 2-1 USDT-Margined Aggregated Trade Data

2.2 USDT-Margined 500-Level Order Book Data

The data structure for the 500-level order book data of USDT-margined contracts is as follows:

colNames = ["eventTime", "transactionTime", "code", "lastUpdateId", "bidPrice", 
              "bidQty", "askPrice", "askQty", "currentTime"]
colTypes = [TIMESTAMP, TIMESTAMP, SYMBOL, LONG, DOUBLE[], 
              DOUBLE[], DOUBLE[], DOUBLE[], TIMESTAMP]
orderbook = table(10000000:0, colNames, colTypes)

Sample data is as shown below:

Figure 3. Figure 2-2 USDT-Margined 500-Level Data

2.3 USDT-Margined Funding Rate Data

In addition to market data, funding rates should be considered when trading perpetual contracts as these rates impact a strategy’s capital cost and profitability. DolphinDB’s httpClient plugin supports retrieving real-time or historical funding rate data. The implementation is as follows:

def getBinanceFundingRate(param){
    baseUrl = 'https://fapi.binance.com/fapi/v1/fundingRate'
    config = dict(STRING, ANY)
    //Set to your proxy address
    config[`proxy] = "http://localhost:8899/" 
    response = httpClient::httpGet(baseUrl,param,10000,,config)
    result = fromStdJson(response.text)
    colNames = `symbol`fundingTime`fundingRate`markPrice
    colTypes = [SYMBOL,TIMESTAMP,DECIMAL128(8),DECIMAL128(8)]
    fundingRate = table(1:0, colNames, colTypes)
    for(r in result){//r = result[0]
        r = select string(symbol), timestamp(long(fundingTime)), 
                  decimal128(fundingRate,8), decimal128(markPrice,8) 
            from r.transpose()
        fundingRate.tableInsert(r)
    }
    return fundingRate
}
// params
startDate = 2024.09.09
param = dict(STRING, ANY)
param["symbol"] = 'BTCUSDT'
param["startTime"] = long(timestamp(startDate))
param["endTime"] = long(timestamp(startDate+1)) // Get one-day data
param["limit"] = 1000
fundingRate = select symbol, 
                  fundingTime as settlementTime, 
                  decimal128(fundingRate,8) as lastFundingRate 
              from getBinanceFundingRate(param)

The data structure is as follows:

colNames = ["symbol", "settlementTime", "lastFundingRate"]
colTypes = [SYMBOL, TIMESTAMP, DECIMAL128(8)]
fundingRate = table(100:0, colNames, colTypes)

Sample data is as shown below:

Figure 4. Figure 2-3 USDT-Margined Funding Rate Data

3. AS Strategy Backtesting in DolphinDB

The DolphinDB backtesting framework consists of three core components: market data replay, simulated order matching, and strategy development and execution. The following sections describe the detailed implementation steps of the AS strategy backtesting.

3.1 Loading Plugins

Before initiating a cryptocurrency backtest, ensure that your DolphinDB version is 3.00.2 or above.

// Prepare Plugins
installPlugin("MatchingEngineSimulator")
installPlugin("Backtest")
try{loadPlugin("Backtest")}catch(ex){print(ex)}
try{loadPlugin("MatchingEngineSimulator")}catch(ex){print(ex)}

3.2 Data Preprocessing

Raw market data needs to be cleaned and transformed before it can be used for backtesting. Preprocessing includes but is not limited to removing erroneous data, filling missing values, and aligning data frequency to ensure data quality and consistency. The backtest engine requires snapshot market data schema as follows:

colName = ["symbol", "symbolSource", "timestamp", "tradingDay", "lastPrice", 
          "upLimitPrice", "downLimitPrice", "totalBidQty", "totalOfferQty",
          "bidPrice", "bidQty", "offerPrice", "offerQty", 
          "highPrice", "lowPrice", "signal", "prevClosePrice",
          "settlementPrice", "prevSettlementPrice", "contractType"]
colType =  ["STRING", "STRING", "TIMESTAMP", "DATE", "DECIMAL128(8)", 
          "DECIMAL128(8)", "DECIMAL128(8)", "DECIMAL128(8)", "DECIMAL128(8)",
          "DECIMAL128(8)[]", "DECIMAL128(8)[]", "DECIMAL128(8)[]","DECIMAL128(8)[]",
          "DECIMAL128(8)", "DECIMAL128(8)", "DOUBLE[]", "DECIMAL128(8)",
          "DECIMAL128(8)", "DECIMAL128(8)", "INT"]
messageTable = table(10000000:0, colName, colType)

Since the aggregated trade and order book data obtained from Binance differ from this required format, transformation is necessary. DolphinDB’s built-in wj function can be used to join the aggregated trade data with the order book data, forming the structure required by the backtest engine. Missing fields are filled as appropriate.

def createSnapshotMartketData(orderbook, trade){
        // window join
        messageTable = wj(
              orderbook, trade, 0:0, 
              <[last(price) as lastPrice, 
                sum(bidQty) as totalBidQty, 
                sum(OfferQty) as totalOfferQty, 
                max(price) as highPrice, 
                min(price) as lowPrice]>, 
              `transactionTime, 
              `tradeTime)
        // fill the missing columns
        signal = take(array(DOUBLE[], 0).append!(0), count(messageTable))
        messageTable = select 
                          instrument as symbol, 
                          exchange as symbolSource, 
                          transactionTime as timestamp, 
                          date(transactionTime) as tradingDay, 
                          lastPrice, 
                          decimal128(0, 8) as upLimitPrice, 
                          decimal128(0, 8) as downLimitPrice, 
                          totalBidQty, totalOfferQty, 
                          bid_price as bidPrice, 
                          bid_qty as bidQty, 
                          ask_price as offerPrice, 
                          ask_qty as offerQty, 
                          highPrice, lowPrice, signal, 
                          decimal128(0, 8) as prevClosePrice, 
                          decimal128(0, 8) as settlementPrice, 
                          decimal128(0, 8) as prevSettlementPrice, 
                          2 as contractType 
                    from messageTable
        update messageTable set lastPrice = ffill(lastPrice)
        update messageTable set lastPrice = bfill(lastPrice)
        update messageTable set totalBidQty = nullFill(totalBidQty, 0), 
                                totalOfferQty = nullFill(totalBidQty, 0)
        return messageTable
}
messageTable = createSnapshotMartketData(orderbook, trade)

3.3 Configuring Backtest Engine

Configuring the backtest engine includes setting the time range, trading costs, and other parameters. These configurations define the basic conditions for backtesting and how trades are executed. Note:

  • dataType is set to 1 with input snapshot data.
  • The initial capital cash is defined as a dictionary, with the futures account assumed to hold 1,000,000.
  • The funding rate table obtained earlier is input at this step into the backtest engine.
startDate = 2024.09.09
endDate = 2024.09.10
// account
cash = dict(STRING,DOUBLE)
cash["spot"] = 1000000.0
cash["futures"] = 1000000.0
cash["option"] = 1000000.0
// Backtest Engine Configs
userConfig = dict(STRING,ANY)
userConfig["startDate"] = startDate
userConfig["endDate"] = endDate
userConfig["strategyGroup"] = "cryptocurrency"
userConfig["cash"] = cash
userConfig["msgAsTable"] = false 
userConfig["dataType"] = 1 //snapshot
userConfig["matchingMode"] = 3 
userConfig["fundingRate"] = fundingRate

3.4 Creating Contract Metadata Table

Basic information about each cryptocurrency contract (such as specifications, trading unit, and tick size) must be defined before the backtest begins. The setup of the contract metadata table contractReference can be referred to in the following example.

// Contract Information
contractReference = dict(STRING, ANY)
contractReference["symbol"] = "BTCUSDT"                      // symbol code: BTCUSDT
contractReference["contractType"] = 2                        // contract type: 2 - Perp
contractReference["optType"] = 1                             // option type(Unrelated): set default value, 1
contractReference["strikePrice"] = decimal128(0., 8)   		 // strike price(Unrelated): set default value default Null
contractReference["contractSize"] = decimal128(1. , 8)		 // contract multiplier
contractReference["marginRatio"] = decimal128(0.0065, 8)	 // margin ratio
contractReference["tradeUnit"] = decimal128(1., 8)			 // contract unit
contractReference["priceUnit"] = decimal128(0.1, 8)			 // order unit: 0.1 USDT
contractReference["priceTick"] = decimal128(0.1, 8)	         // minimum order unit: 0.1 USDT
contractReference["takerRate"] = decimal128(0.0005, 8)	     // taker rate
contractReference["makerRate"] = decimal128(0.0002, 8)		 // maker rate
contractReference["deliveryCommissionMode"] = 1       		 // Delivery Fee: 1 - makerRate (or takerRate) per contract
contractReference["fundingSettlementMode"] = 1        		 // Funding Rate Settlement: 1 - lastFundingRate per contract
contractReference["lastTradeTime"] = userParam["endTime"]    // last trading time
// Convert Dict to Table
contractReference = transpose(contractReference)

3.5 Defining Trading Strategy

After completing data preparation and configuration, the next step is to implement the trading strategy, including defining trading signals, entry and exit rules, and stop-loss/take-profit mechanisms.

The logic of this AS strategy is as follows: each time an order is placed, unfilled orders are canceled first. Then, the AS formula is used to calculate the quote, after which buy and sell orders are submitted. A risk management module is also added to close positions in a timely manner.

3.5.1 Initialization Function

The initialize function uses the context variable (a dictionary) to store custom parameters needed within the strategy. These parameters are predefined in the userParam variable and passed into the context. For the AS strategy, we directly input the preset values used in the pricing formula.

def initialize(mutable context, userParam){
    print("initialize")
	// Onsnapshot Parameters
	context[userParam.keys()] = userParam.values()
	// Daily Trade Summary
	context['dailyReport'] = table(1000:0,
		[`SecurityID,`tradeDate,`BuyVolume,`BuyAmount,`SellVolume,`SellAmount,
		`transactionCost,`closePrice,`rev],
		[SYMBOL,DATE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE])
	// Log
	context["log"] = table(10000:0,[`tradeDate,`time,`info],[DATE,TIMESTAMP,STRING])
}

3.5.2 Snapshot Callback Function

Since this example uses the snapshot data, the main strategy logic is implemented in the onSnapshot callback function. The strategy logic is divided as follows:

  1. Use orderInterval to determine the order frequency (e.g., once per second).
  2. Cancel any previous unfilled orders.
  3. Calculate the current market mid-price using the best bid and ask prices.
  4. Retrieve the current long and short positions.
  5. Use the AS model to calculate bid/ask prices, considering the price precision (e.g., 0.1 for BTCUSDT).
  6. Submit buy and sell orders.
  7. Implement inventory risk control and close positions if limits are breached.
def onSnapshot(mutable context,msg, indicator){
	istock = context["istock"]
	maxPos = context["maxPos"]
	InventoryTargetPercent = context["InventoryTargetPercent"]
	minSpread = context["minSpread"]
	// 1. Set frequency of each order
	t = msg[istock]["timestamp"]
	if(t < context["orderTime"]){
		return
	}
	context["orderTime"] = context["orderTime"] + context["orderInterval"]
	// 2. Cancel previous orders before submitting new one
	openOrders = Backtest::getOpenOrders(context["engine"],istock, , , "futures")
	if(count(openOrders)>0){
		Backtest::cancelOrder(context["engine"],istock)
	}
	// 3. Calculate Mid Price
	askPrice0 = msg[istock]["offerPrice"][0]
	bidPrice0 = msg[istock]["bidPrice"][0] 
	midPrice = (askPrice0+bidPrice0)/2
	// 4. Get Position
	cost = Backtest::getPosition(context["engine"],istock,"futures")
	pos = nullFill(cost['longPosition']-cost['shortPosition'], 0)
	cash = Backtest::getAvailableCash(context["engine"])
	lastPrice = msg[istock]['lastPrice']
	inventory = pos + cash \ lastPrice
    q_target = inventory * InventoryTargetPercent
	q = (pos - q_target) \ inventory
	// 5. Calculate order price
	gamma = context["gamma"]
	sigma = context["sigma"]
	k = context["k"]
	amount = context["amount"]
	endTime = context["endTime"]
	timeToEnd = (endTime-t)\86400000
	reservePrice = midPrice - q * gamma * square(sigma) * timeToEnd
	spread = gamma * square(sigma) * timeToEnd + (2 \ gamma) * log(1 + (gamma \ k))
	optimalBidPrice = reservePrice-0.5*spread
	optimalAskPrice = reservePrice+0.5*spread
	limitSpread = midPrice / 100 * minSpread
	maxLimitBidPrice = midPrice - limitSpread / 2
	minLimitAskPrice = midPrice + limitSpread / 2
    bidPrice = round(min(optimalBidPrice, maxLimitBidPrice), 1)
    askPrice = round(max(optimalAskPrice, minLimitAskPrice), 1)
	// 6. Submit buy orders
	Backtest::submitOrder(
		context["engine"], 
        // symbol, exchange, time, order type, price, 
        // stop-Loss Price, Take-Profit Price, Order Quantity, Buy/Sell Direction, 
        // Slippage, Order Validity, Order Expiration Time
		(istock, 'Binance', context["tradeTime"], 5, bidPrice,
			0, 1000000, amount, 1,
			0, 0, endTime),
        "buy",
			0,
        "futures")
	//Submit sell orders
    Backtest::submitOrder(
        context["engine"], 
        // symbol, exchange, time, order type, price, 
        // stop-Loss Price, Take-Profit Price, Order Quantity, Buy/Sell Direction, 
        // Slippage, Order Validity, Order Expiration Time
		 (istock, 'Binance', context["tradeTime"], 5, askPrice,
			0, 1000000, amount, 2, 
			0, 0, endTime),
        "sell",
		0,
        "futures")
	// 7. risk control
	// Stop quoting and close the position when the position on one side exceeds the upper limit
	cost = Backtest::getPosition(context["engine"],istock,"futures")
	pos = nullFill(cost['longPosition']-cost['shortPosition'], 0)
	if(pos >= maxPos){	
		// check for null values 
		if(any([askPrice0]<=0) or any(isNull([askPrice0]))){
			continue
		}
		Backtest::submitOrder(
			context["engine"], 
			(istock,context["tradeTime"], 5, askPrice0, pos, 3),
			"closePosition",
			0,
        	"futures")	
	}
	else if(pos<=-maxPos){
		bidPrice0=msg[istock]["bidPrice"][0]   
		Backtest::submitOrder(
			context["engine"], 
			(istock,context["tradeTime"], 3,bidPrice0, abs(pos), 4),
			"closePosition",
			0,
        	"futures")		
	}
}

3.5.3 Finalization Function

At the end of the strategy, the following operations are performed:

  • Cancel any remaining open orders.
  • Record post-trading logs.
  • Perform custom post-trade statistics (e.g., return and transaction costs), store them in context['dailyReport'], and access them via Backtest::getContextDict(long(engine)).
def finalized(mutable context){
	tradeDate = context["tradeDate"]
	print("afterTrading: "+tradeDate)
	// Cancel all open orders
	Backtest::cancelOrder(context["engine"],context["istock"])
	// AfterTrading Log
	tb = context["log"]
	context["log"] = tb.append!(table(context["tradeDate"] as tradeDate,now() as time,"afterTrading" as info))
	// AfterTrading Summary
	tradeDetails = Backtest::getTradeDetails(context["engine"])
	tradeValue = select sum(tradeQty) as qty,sum(tradePrice*tradeQty) as amt 
					from tradeDetails where orderStatus in [0, 1] group by label
	sellAmount = (exec amt from tradeValue where label = "sell")[0]
	buyAmount = (exec amt from tradeValue where label = "buy")[0]
	sellQty = (exec qty from tradeValue where label = "sell")[0]
	buyQty = (exec qty from tradeValue where label = "buy")[0]
	lastprice = context["lastprice"]
	makerRate = context["makerRate"]
	transactionCost = (buyAmount + sellAmount) * makerRate
	rev = (sellAmount - buyAmount) + (buyQty - sellQty) * lastprice
	dailyReport = context['dailyReport']
	tableInsert(dailyReport, (context["istock"], tradeDate, buyQty, buyAmount, 
					sellQty, sellAmount, transactionCost, lastprice, rev))
	context['dailyReport'] = dailyReport
}

3.6 Defining Strategy Parameters

We input serveral custome parameters to the onSnapshot function, which correspond to the AS model variables. These values can be computed from historical data or updated in real time during trading.

  • sigma: market volatility
  • gamma: risk aversion coefficient; the closer to 1, the more risk-averse
  • k: market liquidity parameter
  • amount: order size
  • orderInterval: strategy execution frequency (e.g., 5 minutes in milliseconds)

In this strategy, market volatility (sigma) is calculated from the previous day's data as 1.579. The risk aversion coefficient (gamma) is set to 0.1. The market liquidity parameter (k) is set to 0.1, based on the reference from Avellaneda & Stoikov (2008). Each order places 0.01 units, with a strategy frequency of once every 5 minutes. Users can compute, adjust, or optimize these parameters based on their own use case.

sigma = (select std(abs(deltas(price))) as volatility 
          from trade 
          where date(tradeTime) = startDate - 1 
          group by date(tradeTime)
        )[0]["volatility"]
// Onsnapshot Params
userParam = dict(STRING,ANY)
userParam["sigma"] = sigma									// market volatility
userParam["gamma"] = 0.1									// inventory risk aversion parameter
userParam["k"] = 1.5										// order book liquidity parameter
userParam["amount"] = 0.01									// order amount
userParam["endTime"] = endTime								// closing time
userParam["feeRatio"] = 0.0003								// fee ratio
userParam["istock"] = sym									// symbol
userParam["orderTime"] = timestamp(startDate)				// starting time
userParam["orderInterval"] = 1000							// stratygy frequency, ms
userParam["tradeDate"] = date(startDate)					// date
userParam['lastprice'] = last(messageTable["lastPrice"])	// last price
userParam["maxPos"] = 5										// inventory of upper limit
userParam["InventoryTargetPercent"] = 0.2					// target inventory percent 		
userParam["minSpread"] = 0.01	                            // minimum spread

3.7 Running Backtesting

After implementing the strategy, use Backtest::createBacktestEngine to create the engine. Then input data using Backtest::appendQuotationMsg, followed by a termination signal to mark the end of the backtest.

// 1. Create Backtest Engine
strategyName = "marketMakingStrategy"
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktestEngine(
										strategyName, 
										userConfig,
										contractReference,
										initialize{,userParam}, 
										beforeTrading,
										,
										onSnapshot,
										onOrder,
										onTrade,
										,
										finalized
									)
// 2. Start backtesting
timer Backtest::appendQuotationMsg(engine, messageTable)
go
endMsg = select top 1* from messageTable where timestamp = max(timestamp)
update endMsg set symbol = "END"
update endMsg set timestamp = endTime
Backtest::appendQuotationMsg(engine,endMsg)
go

3.8 Obtaining Backtesting Results

The backtest engine simulates order generation, matching, and execution based on the user-provided strategy, and returns trade results and relevant statistics to help evaluate the strategy’s effectiveness.

// Get Result
tradeDetails = Backtest::getTradeDetails(engine)
returnSummary = Backtest::getReturnSummary(long(engine))
context = Backtest::getContextDict(long(engine))
dailyReport = context["dailyReport"]
// Stop backtest
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
  • Backtest::getTradeDetails(engine) returns detailed trade records.
  • Backtest::getReturnSummary(engine) returns a summary of strategy returns.
  • Backtest::getContextDict(long(engine)) returns the content of the context.

As can be seen from dailyReport, 184 BTC were bought and 201 BTC sold, yielding a net loss of $27,082.04 and transaction fees totaling $6,489.60.

Figure 5. Figure 3-1 AS Strategy Backtest Results

3.9 Performance

This high-frequency backtest processes one day of BTC/USDT contract data with one quote per second. The backtest engine completed in 27 seconds.

Data Volume Trading Pair Time Elapsed (ms)
701,868 records BTC/USDT 27,606

4. Conclusion

This article presents a complete solution for high-frequency backtesting of cryptocurreny trading strategies in DolphinDB, with example data and strategy code included throughout. From market data acquisition to strategy execution, DolphinDB’s HttpClient and WebSocket plugins enable real-time access to exchange data. Coupled with its robust time-series processing capabilities and high-performance backtest engine, DolphinDB provides an efficient and reliable infrastructure for developing and evaluating cryptocurrency trading strategies.

In Section 3.5, we detailed the implementation of the snapshot callback function, demonstrating how users can define and customize trading logic. The modular design improves code maintainability and enhances the flexibility and scalability of strategy development. With fine-grained configurations and a structured simulation process, DolphinDB enables accurate replication of live trading conditions and objective evaluation of a strategy’s viability and profitability.

The methodology outlined here offers a beginner-friendly, reproducible, and performance-oriented approach for mid- and high-frequency backtesting, making it suitable for both academic research and professional quantitative trading in the cryptocurrency domain.

Disclaimer: Trading cryptocurrencies involves significant market risk. Before engaging in trading activities, please ensure you fully understand the applicable rules, risks, and platform-specific mechanisms.

Reference

Avellaneda, M., & Stoikov, S. (2008). High-frequency trading in a limit order book. Quantitative Finance, 8 (3), 217-224. Retrieved from https://doi.org/10.1080/14697680701381228