DolphinDB-Based Solution for Quantitative Cryptocurrency Trading

1. Overview

In recent years, the global cryptocurrency market has grown rapidly. The proliferation of exchanges and cryptocurrency types, round-the-clock trading, massive trading volumes, and high price volatility have created unprecedented opportunities for quantitative cryptocurrency trading firms. At the same time, the complexity of the cryptocurrency market brings significant technical challenges. First, data sources are highly scattered and data structures are complex, leading to high costs for data ingestion and management. Second, high-frequency trading generates massive volumes of data, placing extremely high demands on database performance. Third, the separation between strategy research and live trading systems makes it difficult to directly apply research findings to live trading. Against this backdrop, building a high-performance, low-latency, and scalable integrated quantitative trading platform has become a core competitive advantage for quantitative cryptocurrency trading firms.

Tailored to the cryptocurrency market, DolphinDB delivers an end-to-end trading solution that combines high-performance distributed storage and computing, low-latency streaming processing, backtesting, and order matching. The solution covers the full workflow from data storage and processing to strategy development and live trading, helping trading firms accelerate strategy development and validation. Key features of the solution include:

  • Comprehensive solution for ingesting and storing market data from Binance and OKX, along with a monitoring and alerting mechanism that significantly reduces data maintenance costs.
  • Built-in data cleansing and processing functions that eliminate tedious preprocessing steps, lower the barrier to strategy development, and accelerate strategy implementation.
  • Batch and streaming factor computation, integrated with Python-based training models to generate factor signals in real time.
  • A live trading module with the same usage as the DolphinDB backtesting plugin, allowing high-performing strategies to be directly deployed in live trading.

In addition, the solution includes real-time risk control models for key risk metrics, enabling real-time monitoring and alerting of account risk to help manage risk effectively. It also extends to features such as strategy code management and user management.

Version requirements: We recommend that you use DolphinDB Server 3.00.4 to implement the solution.

1.1 Market Data Processing

In quantitative trading research, market data is the core for strategy development, factor construction, backtesting, simulated trading, and live trading. DolphinDB integrates a high-performance distributed storage engine with a streaming computation framework, enabling efficient storage and batch processing of massive data while also supporting real-time computation and persistence of streaming data. The processing workflow for cryptocurrency market data is divided into three parts: data ingestion, computation, and consumption.

Figure 1. Figure 1-1: Market Data Processing Workflow

Data ingestion: This solution integrates data sources from two major cryptocurrency exchanges—Binance and OKX. For different data types, it provides best-practice database and table designs, covering tick data, trading data, OHLC data, funding rate data, liquidation data, and more (see Chapter 2 for details). In addition, it supports customizing the cryptocurrency list to meet diverse data requirements. DolphinDB supports acquiring trading data through the following tools:

  • APIs: APIs for multiple programming languages, including Python, C++, Java, and Go.
  • Plugins: Data communication plugins such as httpClient and WebSocket.

To address issues such as network interruptions, exchange API errors, and database errors, this solution provides a comprehensive monitoring, alerting, and data backfill mechanism to prevent data loss. The specific methods include:

  • Writing real-time streaming data to stream tables on two servers simultaneously, with key-based deduplication to mitigate the impact of network disruptions.
  • Automatically retrying connections upon abnormal disconnections, caching data locally, and reloading them into the database once the connection is restored.
  • Periodically monitoring the status of stream tables and databases, and triggering alerts promptly when anomalies are detected.
  • Periodically validating the completeness of OHLC data for the previous trading day and re-fetching missing data when necessary.

Computation: DolphinDB provides a powerful computation framework that supports both batch and streaming processing, including real-time snapshot aggregation, batch and real-time factor computation, and real-time data storage into databases.

  • Streaming engines: DolphinDB provides multiple built-in streaming engines, such as the time-series engine (e.g., used to generate OHLC bars at different frequencies), the reactive state engine (e.g., used to cleanse data in real time), join engines (e.g., used to aggregate snapshot data), and the cross-sectional engine (e.g., used to rank cryptocurrency types).
  • High-performance batch processing: DolphinDB supports distributed batch processing and comes with multiple factor libraries and more than 2,000 optimized functions. Its powerful programming language enables efficient processing and analysis of large-scale datasets.

Consumption: Results computed by DolphinDB can be stored in DFS tables, in-memory tables, or stream tables. Consumers can subscribe to stream tables in real time to obtain the latest market data for simulated or live trading. Alternatively, computation results can be accessed in real time through APIs or pushed via message middleware.

1.2 Unified Stream and Batch Processing

In quantitative trading research, factor research is a critical part of strategy development. Factors are quantitative metrics that capture market characteristics and reflect price dynamics and risk structures, forming the foundation of model prediction and signal generation.

Traditional quantitative research platforms often adopt a dual-system architecture: using scripting languages like Python for strategy and factor development, and high-performance languages such as C++ for real-time computation in production. This approach not only incurs high development and maintenance costs, but also risks inconsistencies between batch and streaming results.

DolphinDB’s unified stream and batch processing framework eliminates this issue by enabling a single codebase that allows seamless migration of factor logic from development to real-time computation. This ensures consistent results between research and live trading while significantly reducing development costs and maintenance complexity.

Figure 2. Figure 1-2: Streaming–Batch Processing Architecture

Based on DolphinDB’s built-in 191 Alpha and WorldQuant 101 Alpha factor libraries, this solution supports both batch and streaming computations for these factors, along with a highly extensible factor storage architecture. User-defined factors can also be developed as needed.

On this foundation, machine learning models, such as LSTM, XGBoost, and Transformer, are trained on existing factors in a Python environment, with full support for data access, preprocessing, and model training. Trained models can be loaded into DolphinDB via the GpuLibTorch plugin for backtesting and simulated trading, enabling real-time trading signal generation. Leveraging these capabilities, this solution delivers an end-to-end pipeline from factor development and streaming-batch computation, to Python-based model training and real-time trading signal generation.

Figure 3. Figure 1-3: Architecture Combining Python Model Training with DolphinDB Backtesting and Simulation

1.3 Backtesting and Simulated Trading

Before being deployed in live trading, quantitative strategies are subjected to extensive backtesting and simulation to validate their effectiveness and feasibility. DolphinDB provides a comprehensive backtesting and simulation framework for cryptocurrency quantitative trading, featuring core components such as historical market data replay, real-time data ingestion, an order matching simulator, and a backtesting engine.

  • Historical data replay: In simulated trading, quantitative strategies usually process real-time data in an event-driven manner. To ensure consistent strategy logic across backtesting and simulated trading, DolphinDB provides a data replay mechanism that feeds historical market data into the backtesting engine in strict chronological order, reproducing real trading conditions.
    Figure 4. Figure 1-4: Cryptocurrency Backtesting Architecture
  • Real-time data ingestion: In simulated trading, the DolphinDB backtesting engine supports continuous ingestion of streaming data. During execution, results such as trade details, real-time positions, and account equity are written to stream tables in real time.
  • Backtesting plugin: The DolphinDB backtesting plugin includes a built-in order matching simulator optimized to reflect real-world matching logic, enabling more accurate evaluation and prediction of strategy performance in live trading. The plugin consists of four core components: user-defined strategy functions, strategy configuration and creation, market data replay, and execution of the backtesting engine. It supports snapshot and minute-level cryptocurrency market data, as well as multi-account management.
Figure 5. Figure 1-5: Backtesting Engine Architecture

To reduce strategy development costs, we focus on real-world quantitative trading requirements to provide an integrated backtesting and simulation solution. The solution unifies market data replay and processing, backtesting and simulation engine creation, strategy development and code management, strategy visualization and management, and user-level strategy permission control. This allows developers to focus solely on strategy logic, enabling both individual users and quantitative trading teams to conduct efficient research and development with minimal code and a low learning curve.

1.4 Live Trading

After thorough backtesting and simulation, strategy researchers aim to deploy high-performing strategies in live cryptocurrency markets to generate real returns. To support this transition, this solution integrates with exchange order and account APIs and provides a live trading module fully consistent with the simulation logic. This enables a true “single codebase” approach across backtesting, simulation, and live trading. This solution supports both Binance and OKX, offering capabilities such as order placement and cancellation, querying open orders, and retrieving trade details. In addition, it incorporates risk metrics to build real-time risk control models for timely monitoring and alerting of account conditions.

1.5 Real-Time Risk Control

The cryptocurrency market is highly volatile, offering significant return potential alongside substantial risk. In practice, many existing solutions compute risk metrics by directly consuming exchange-provided fields such as margin usage and unrealized profit and loss (P&L). Although simple, this approach has several limitations:

  • Risk data provided by exchanges often has low update frequency or high latency, making it difficult to promptly reflect risks caused by rapid market movements.
  • It does not support portfolio-level risk computation across multiple accounts.
  • It cannot be combined with simulated market data for scenario analysis and stress testing.

To address these issues, we provide a DolphinDB-based real-time risk control solution for cryptocurrency trading. By continuously ingesting exchange account data and periodically computing key metrics such as portfolio’s net asset value, position P&L, total asset value, and leverage ratio, this solution enables timely monitoring and alerting of account risk, helping investors better understand and manage their investment risks. Compared with traditional risk control approaches, this solution offers the following advantages:

  • Stronger real-time performance: Compared with exchange-provided leverage estimates, this solution captures market changes promptly and issues real-time risk alerts, ensuring superior timeliness.
  • Real-time data persistence: Account information and risk metrics are persisted in real time, enabling advanced analysis, scenario simulation, and stress testing.
  • High extensibility: You can flexibly modify and extend risk metric computation to meet diverse risk management requirements across different scenarios.
  • Isolation from business logic: The risk control module operates independently from trading logic and runs on read-only account data, ensuring the stability and continuity of the trading system.

2. Historical and Real-Time Data Ingestion

Unlike traditional financial assets, the cryptocurrency market operates with round-the-clock trading, high price volatility, and massive data volumes. These characteristics place much higher demands on database performance, including data storage, querying, and real-time processing. To address these challenges, we leverage DolphinDB’s high-performance storage and computing capabilities to provide a comprehensive solution for data ingestion and storage, data processing, as well as monitoring and data backfill.

This solution integrates two major exchanges (Binance and OKX) as data sources. It supports a wide range of cryptocurrency types and various market data to meet diverse requirements. For certain data types, different modes can be selected (USD-margined, coin-margined, or spot). In addition, this solution implements robust fault-tolerance mechanisms, including automatic reconnection retries, scheduled monitoring, and periodic data backfill, ensuring data completeness and reliability even in scenarios such as network failures or service restarts.

Below is a summary table of all provided market data ingestion scripts, covering both historical and real-time data. The complete data ingestion scripts are included in the Appendix. Exchange-related documentation can be found at: Binance Open Platform and OKX API Guide.

Table 1. Table 2-1: Market Data Ingestion Scripts
Data type Table Type/Name Script File Name Description
OHLC data
  • DFS table: dfs://CryptocurrencyKLine/minKLine
  • Stream table: Cryptocurrency_minKLineST
  • Historical data:
    • historyKLine.py
    • okxHistoryKLine.dos
  • Real-time data:
    • Binance_Future_KLine.py
    • Binance_Spot_KLine.py
    • okx_Future_KLine.py
    • okx_Spot_KLine.py
Supports ingesting historical and real-time data to generate OHLC bars at different frequencies.To add new frequencies or modify table names, see Sections 2.2 & 2.3.
Tick trade data
  • DFS table: dfs://CryptocurrencyTick/tickTrade
  • Stream table: Cryptocurrency_tickTradeST
  • Historical data:
    • historyTrades.py
    • okx_historyTrades.dos
  • Real-time data:
    • Binance_Spot_tickTrade.py
    • okx_Future_tickTrade.py
    • okx_Spot_tickTrade.py
  • OKX: Only the most recent 3 months of historical tick trade data are available.
  • Binance futures: Tick trade data is not provided.
Aggregated trade data
  • DFS table: dfs://CryptocurrencyTick/trade
  • Stream table: Cryptocurrency_aggTradeST
  • Historical data: historyAggtrades.py
  • Real-time data:
    • Binance_aggTrade.py
    • okx_aggTrade.py
OKX does not provide historical aggregated trade data.
Level 2 market data
  • DFS table: dfs://CryptocurrencyTick/depth
  • Stream table: Cryptocurrency_depthST
Real-time data:
  • Binance_depth.py
  • okx_depth.py
No historical data.
Snapshot market data
  • DFS table: dfs://CryptocurrencyTick/depthTradeMerge
  • Stream table: Cryptocurrency_depthTradeMerge
Real-time data: depthTradeMergeScript.dos Merged in real time from level 2 data and aggregated trade data, and persisted in real time.
400-level order book snapshot data
  • DFS table: dfs://CryptocurrencyOrderBook/OkexOrderbooks
  • Stream table: Cryptocurrency_Okex_OrderBook_400
Real-time data: okx_depth_400.py After retrieving both full and incremental snapshots, the data is processed by the orderbook snapshot engine and persisted in real time.
Funding rate data DFS table: dfs://CryptocurrencyDay/fundingRate
  • Hstorical data:
    • Binance_historyFundingRate.dos
    • okx_historyFundingRate.dos
  • Latest data: getLatestFundingRate.dos
  • OKX: Only the most recent 3 months of historical funding rates are available.
  • The latest funding rate is obtained by a scheduled task at 00:01, 08:01, and 16:01.
Index price/ mark price OHLC DFS table:
  • dfs://CryptocurrencyDay/indexPriceKLine
  • dfs://CryptocurrencyDay/markPriceKLine
Historical data: historyIndexMarkKLine.py Not pushed in real time and is retrieved on a scheduled basis, following the funding rate scheduling logic.
Metrics data DFS table: dfs://CryptocurrencyKLine/metrics Historical data: historyMetrics.py
  • An aggregated overview of open interest and long/short ratio within a given period.
  • OKX does not provide historical metrics data.
  • Not pushed in real time and is retrieved on a scheduled basis, following the funding rate scheduling logic.
Minute-level data for continuous contracts
  • DFS table: dfs://CryptocurrencyKLine/continousKLine
  • Stream table: Cryptocurrency_ContinuousKLineST
Real-time data:
  • Binance_continuousContractKLine.py
  • okx_continuousContractKLine.py
No historical data.
Liquidation data
  • DFS table: dfs://CryptocurrencyDay/liquidation
  • Stream table: Cryptocurrency_liquidation
Real-time data:
  • Binance_liquidation.py
  • okx_liquidation.py
  • No historical data.
  • Binance: Within any 1000 ms window, at most one last liquidation order is pushed as a snapshot.
  • OKX: Liquidation data represents at most one liquidation order per trading pair within any given second.
  • Therefore, the displayed liquidation data does not accurately represent the total liquidation volume.
Trading pair information
  • DFS table: dfs://CryptocurrencyDay/contractInfo
  • Stream table: contractInfo
Real-time data:
  • Binance_contractInfo.py
  • okx_contractInfo.py
No historical data.
Note:
  • Binance internally maintains multiple types of historical market data. For details, see Binance Data Collection.
  • Instructions for using the historical and real-time data ingestion scripts are provided in Sections 2.2 & 2.3.

2.1 Database and Table Schema

Considering the characteristics of cryptocurrency market data and its usage scenarios, we provide partitioning schemes based on both OLAP and TSDB engines to achieve optimal performance for data ingestion and querying. Taking nine major cryptocurrencies as examples, this section describes the database and table design by combining minute-level OHLC data, tick data, funding rate data, and other datasets. The design supports a certain level of extensibility for additional symbols and is reasonably generic. The database and table creation scripts are provided in the Appendix.

Table 2. Table 2-2: Database and Table Schema Description
Database Name Engine Partitioning Scheme Partition Column Sorting Column Data Type
CryptocurrencyTick TSDB Daily partition + HASH partition by symbol Trade time + symbol Exchange + symbol + trade time
  • Level 2 data
  • Snapshot data
  • Aggregated trade data
  • Tick trade data
CryptocurrencyOrderBook TSDB Hourly partition + VALUE partition by symbol Trade time + symbol Symbol + trade time High-frequency order book snapshot data (400 levels)
CryptocurrencyKLine OLAP Yearly partition Trade time None
  • Minute-level OHLC data
  • Index price and mark price OHLC data
  • OHLC data for continuous contracts
  • Metrics data
CryptocurrencyDay OLAP 5-year partition Trade time None
  • Daily OHLC data
  • Funding rate data (dimension table)
  • Liquidation data (dimension table)
  • Contract information (dimension table)
  • Precision data (dimension table)
Note:
  • For precise database and table design, you need to consider both the data volume of selected cryptocurrencies and their usage scenarios.
  • Low-frequency and infrequently queried data, such as funding rate data and liquidation data, are stored in dimension tables.

This solution integrates market data types from both Binance and OKX. For different data types, unified field naming conventions are adopted while preserving exchange-specific fields as much as possible. All timestamp fields use Beijing Time (UTC +8). A complete field description is provided in the Appendix.

2.2 Historical Data Ingestion

This solution provides a complete set of scripts for ingesting historical data, including OHLC data at different frequencies, aggregated trade data, tick trade data, funding rate data, and metrics data. You can specify time ranges, cryptocurrency list, and frequencies (for OHLC data). Historical market data is primarily used for backtesting and data monitoring. Therefore, we recommend that you do not customize database or table names. Otherwise, corresponding table names in scripts need to be modified. In addition, there are differences between the historical data provided by Binance and OKX. Detailed description can be found in the Description column of Table 2-1. The complete historical data ingestion scripts are provided in the Appendix.

2.2.1 Historical Data Ingestion for Binance

Overview

This solution uses Python to batch-download historical data from Binance and import it into DolphinDB. By accessing Binance’s official historical data warehouse, it supports batch processing across multiple trading pairs and date ranges, and includes error handling, logging, and data validation.

  • Data source: Access Binance’s official historical data warehouse to view all available assets and the corresponding start dates.
  • Data download: Use the requests library to download compressed CSV files, decompress them, and retain the raw data. After successful write, the original files are deleted.
  • Data write: Connect to DolphinDB and write the data into the target DFS tables.
  • Logging: Record write details, including download status, parsing status, and the number of rows written.
  • Workflow control: Support customizing the cryptocurrency list and start/end dates, and automatically iterating over the ingestion workflow.
Figure 6. Figure 2-1: Workflow for Ingesting Historical Binance Data

Core functions

  • download_file: Downloads the compressed file for a specified date to a given path, decompresses it, removes the .zip file, and returns the CSV file path.
  • parse_csv_to_dataframe: Reads the CSV file, checks for the presence of headers, converts fields to match the required data types for data write, and returns a standardized pandas.DataFrame.
  • import_to_db: Writes the transformed DataFrame into the specified DolphinDB table.
  • process_single_file: Chains the above three functions to process the historical data of a single cryptocurrency for a single day.
  • run: The main control function that iterates in batch over multiple cryptocurrencies and multiple dates, recording overall results.

Variables and usage instructions

Before running, ensure that the target database and tables exist in the DolphinDB server. Modify the following variables according to your requirements and execute the Python program. The Python script is provided in the Appendix.

Table 3. Table 2-3: Variable Description
Variable Location Description
DDB config.py Configures the DolphinDB connection, including host, port, username, and password
HIS_CONFIG config.py Directory for storing import logs
HIS_CONFIG config.py Directory for storing downloaded files
PROXY config.py Proxy address
url In download_file() URL of the file to download. Adjust as needed; /futures and /spot distinguish futures and spot addresses
symbols Configuration parameter List of symbols for which historical data is downloaded, e.g. ["BTCUSDT"]
start_date Configuration parameter Start time of the historical data to download
end_date Configuration parameter End time of the historical data to download
db_path Configuration parameter Target database path for data import
table_name Configuration parameter Name of the target DFS table
accountType Configuration parameter Contract type (varies by data type):
  • 'um': USD-Margined futures
  • 'cm': COIN-Margined futures
  • 'spot': spot
interval Configuration parameter OHLC frequency (OHLC data only): '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1mo'
klineType Configuration parameter OHLC type (index or mark price only):
  • 'indexPriceKlines': index price
  • 'markPriceKlines': mark price

The following example shows how to import Binance’s historical minute-level OHLC data using historyKline.py. You can reference this example and modify the parameters to import other types of market data.

  1. Create the required database and tables for the data type. We recommend that you do not modify table names. For the full script, refer to createDatabase.dos in the Appendix.
    dbName = "dfs://CryptocurrencyKLine"
    tbName = "minKLine"
    streamtbName = "Cryptocurrency_minKLineST"
    
    db = database(dbName, RANGE, 2010.01M + (0..20)*12)
    
    colNames = `eventTime`collectionTime`symbolSource`symbol`open`high`low`close`volume`numberOfTrades`quoteVolume`takerBuyBase`takerBuyQuote`volCcy
    colTypes = [TIMESTAMP, TIMESTAMP, SYMBOL, SYMBOL, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, INT, DOUBLE, DOUBLE, DOUBLE, DOUBLE]  
    
    createPartitionedTable(db, table(1:0, colNames, colTypes), tbName, `eventTime)
  2. Install required Python packages such as requests, zipfile, numpy, and dolphindb.
  3. Modify variables as needed:
    # config.py -- DDB and PROXY must be updated for your setup
    DDB = {"HOST": '192.xxx.xxx.xx', "PORT": 8848, "USER": 'admin', "PWD": '123456'}
    
    BINANCE_BASE_CONFIG = {
        "PROXY": 'http://127.0.0.1:7890/',
        "TIMEOUT": 5,
        "PROBE_COOLDOWN_SECS": 30,
        "READ_BATCH_SIZE": 20000,
        "LIVE_GET_TIMEOUT": 0.2
    }
    
    HIS_CONFIG = {"LOG_DIR": "./logs", "SAVE_DIR": "./data"}
    
    # Global settings
    symbols = ["BTCUSDT","ETHUSDT","ADAUSDT","ALGOUSDT","BNBUSDT","FETUSDT","GRTUSDT","LTCUSDT","XRPUSDT"]
    start_date = datetime(2025, 8, 11)
    end_date = datetime(2025, 8, 12)
    accountType = "um"  # Supported: um/cm/spot
    interval = "1m"     # Adjust for different OHLC frequencies
    db_path = "dfs://CryptocurrencyKLine"
    table_name = "minKLine"
  4. Execute the Python script to batch-import historical data. Import progress and results can be monitored via the log files at the custom LOG_DIR path.
    result = downloader.run(accountType, symbols, klineType, start_date, end_date, 
                            interval, db_path, table_name)

2.2.2 Historical Data Ingestion for OKX

Unlike Binance, OKX does not provide a historical data warehouse; historical data can only be accessed via the API. Additionally, due to OKX’s rate limits, concurrent requests across multiple cryptocurrencies are not feasible. Therefore, this solution uses the httpClient plugin to sequentially call the API by trading pair and date to fetch historical data.

Core functions

  • convertOKXSymbol: Symbol conversion function that converts OKX-formatted symbols (e.g., 'BTC-USDT-SWAP') to standard format (e.g., 'BTCUSDT').
  • getOKXHistoryKLineOne: Single API request that returns parsed data and the earliest timestamp.
  • insertKLines: Data insertion function that returns the number of inserted rows and a stop flag.
  • getHistoryKLine: Main function that retrieves historical data sequentially by trading pair and date, printing import status.

Variables and usage instructions

Before running, ensure the target database and tables exist in the DolphinDB server. Modify the following variables according to your requirements and execute the DOS script. The full script is provided in the Appendix.

Table 4. Table 2-4: Variable Description
Variable Location Description
dbName Configuration parameter Path to the target database
tbName Configuration parameter Name of the target DFS table
proxy_address Configuration parameter Proxy server address
codes Configuration parameter List of symbols; syntax differs by type:
  • Futures: e.g., ["BTC-USDT-SWAP"]
  • Spot: e.g., ["BTC-USDT"]
startDate Configuration parameter Start date of historical data
endDate Configuration parameter End date of historical data
bar Script for OHLC data: okx_historyKLine.dos OHLC frequency (OHLC data only): '1s', '1m', '3m', '5m', '15m', '30m', '1H', '2H', '4H', '6Hutc', '12Hutc', '1Dutc', '2Dutc', '3Dutc', '1Wutc', '1Mutc', '3Mutc'
klineType Script for OHLC data: okx_historyKLine.dos OHLC data type:
  • 'kline': OHLC data
  • 'mark-price': mark price
  • 'index': index price

The following example shows how to import OKX’s historical minute-level OHLC data using okx_historyKline.dos. You can modify the parameters to import other types of market data.

  1. Create the required database and tables. We recommend that you do not modify table names. For the full script, refer to createDatabase.dos in the Appendix.
  2. Install and load the httpClient plugin in DolphinDB:
    login("admin", "123456")         // Log in
    listRemotePlugins()              // List available plugins
    installPlugin("httpClient")      // Install the httpClient plugin
    loadPlugin("httpClient")         // Load the httpClient plugin
  3. Modify variables as needed:
    dbName = "dfs://CryptocurrencyKLine"
    tbName = "minKLine"
    proxy_address = 'http://127.0.0.1:7890'
    codes = ["BTC-USDT-SWAP","ETH-USDT-SWAP","ADA-USDT-SWAP","ALGO-USDT-SWAP","BNB-USDT-SWAP",
             "FIL-USDT-SWAP","GRT-USDT-SWAP","LTC-USDT-SWAP","XRP-USDT-SWAP"]
    startDate = 2025.10.01
    endDate = 2025.10.01
  4. Run the DOS script to batch-import historical OKX data:
    getHistoryKLine(startDate, endDate, codes, dbName, tbName, proxy_address, bar='1m', KLineType="kline")

The output displays the import status. You can also call submitJob to run the job in the background:

submitJob("getHistoryKLine", "Fetch OKX historical K-line data",
          getHistoryKLine, startDate, endDate, codes, dbName, tbName, proxy_address, '1m', "kline")

2.3 Real-Time Data Ingestion

This solution provides a complete set of scripts for ingesting real-time data, including OHLC data at different frequencies, minute-level data for continuous contracts, level 2 market data, aggregated trade data, tick trade data, liquidation data, and trading pair information. You can specify the cryptocurrency list. Real-time stream tables are used for backtesting, simulated trading, and data monitoring. Therefore, we recommend that you do not customize stream table names. Otherwise, corresponding table names in scripts need to be modified. In addition, there are differences between the real-time data provided by Binance and OKX. Detailed description can be found in the Description column of Table 2-1. If resources are sufficient, dual-channel real-time data ingestion is recommended to ensure data completeness.

2.3.1 Overview

This solution allows you to subscribe to real-time data for specified cryptocurrency trading pairs via WebSocket. It uses MultithreadedTableWriter (MTW) to batch-write data into DolphinDB’s persistent stream tables, and then persists the data into databases by subscribing to these stream tables. The system supports automatic reconnection, exception recovery, and data backfill. If a database exception occurs, it automatically switches to local file caching. With dual-channel ingestion enabled, the system can tolerate a network interruption on one channel and ensure zero data loss.

  • Data sources: Real-time data is subscribed via Python’s WebSocket library.
    • binance.websocket.um_futures.websocket_client: subscribes to Binance futures data;
    • binance.websocket.spot.websocket_stream: subscribes to Binance spot data;
    • okx.websocket.WsPublicAsync: subscribes to OKX data.
  • Ingestion side: The main process starts the WebSocket client, initializes the writer, and launches a daemon thread to monitor data reception.
  • Caching side: A single IOThread handles all write operations to avoid out-of-order writes.
  • Write side: Data is written to stream tables via MTW provided by DolphinDB API and then persisted into corresponding partitioned tables through subscriptions.
  • Fault tolerance: If the WebSocket client disconnects, the system continuously attempts reconnection. If writing fails, data is written to local JSON files. After the writer restarts, cached data is automatically read and backfilled into DolphinDB.
Figure 7. Figure 2-2 Real-Time Data Ingestion Workflow
Note:

The collectionTime field in stream tables represents the local timestamp when real-time data is collected and can be used to analyze subscription latency.

2.3.2 Core Components

  • _framework.py: General framework for data ingestion.
    • xxxBaseConfig: Base configuration class defining common settings such as DolphinDB connection, proxy configuration, real-time queues, and cache files. It also provides methods for workflow startup, timeout monitoring, etc.
    • IOThread: A common class that centrally manages the single write path for MTW writing, local persistence, and data backfill, executing serially to avoid disorder. It has following three states:
      • live: DolphinDB is healthy; data is taken from the real-time queue and written via MTW.
      • offline: DolphinDB is unavailable; real-time queue data is persisted locally as much as possible, then probed from the first local record after a cooldown period.
      • replay: Only local cache data is written to MTW; the real-time queue is not consumed. After the local cache is cleared, the system switches back to the live state.
  • <targetData>.py: Real-time data ingestion script.
    • get_create_table_script: Table creation script defining the schema of stream tables and DFS tables. The script is run when rebuilding the writer to prevent stream table invalidation caused by server shutdowns.
    • create_message_handler: Core handler that processes each subscribed message, parses fields, and converts them into the write format before pushing them into the queue for the writer thread. For OHLC data, only closed bars are processed.

2.3.3 State Transition Details

live state (normal operation)

Real-time data is fetched from the queue and written directly to DolphinDB via MTW, with write results monitored.

State transition condition

MTW write fails: live → offline

if self.mode == 'live':
    try:
        row = realtime_q.get(timeout=LIVE_GET_TIMEOUT) 
        self._insert_one(row)  # Call MTW for writing
    except Exception as e:
        # Processing sequence for write failure
        save_unwritten_to_local()          # 1. Save MTW's internal cache
        self._append_rows_to_local([row])  # 2. Save rows failed to be written
        self._save_queue_to_local()        # 3. Save rows in the queue
        writer = None          # Mark MTW as invalid
        self.mode = 'offline'  # Switch to offline mode
        self.next_probe_ts = time.time() + PROBE_COOLDOWN_SECS # Set the time for next probing

offline state (local cache mode)

To prevent data loss, the system automatically caches data locally during DolphinDB failures. After the cooldown period, it probes for recovery and automatically backfills cached data once the system is recovered.

State transition condition

Probing succeeds and the database connection is recovered: offline → replay

elif self.mode == 'offline':
    now = time.time()
    if now < self.next_probe_ts:
        # Within cooldown period: only persist data locally
        self._save_queue_to_local(max_n=50000)  # Batch persistence to avoid blocking
        time.sleep(0.1)
        continue
    # Cooldown period ended: attempt to probe liveness

Probe mechanism: The system attempts a test write by reading and writing the first line of the local cache file. Success indicates that DolphinDB has recovered.

def _probe_from_local_first_line(self) -> bool:
    # 1. Ensure an available MTW; rebuild it if missing
    if writer is None and not build_mtw():
        return False
    # 2. Read and test the first line from the local file
    with file_lock:
        with open(self.path, "r", encoding="utf-8") as f:
            line = f.readline()
            if not line or not line.endswith("\n"):
                return False
            try:
                row = json.loads(line)
                self._insert_one(row)  # Attempt to write a single record
                return True            # Success indicates recovery; then switch to live state
            except Exception:
                writer = None
                return False

replay state (data backfill mode)

In this state, the real-time queue is not consumed to preserve historical data ordering. The system reads the local cache file in batches and writes data sequentially into the database. Large files are processed in batches to avoid memory overflow.

State transition conditions

  • Local cache backfill completes successfully: replay → live
  • Failure occurs during replay: replay → offline
def _replay_all_local(self) -> bool:
        global writer
        total = 0
        try:
            with file_lock:
                src = open(self.path, "r", encoding="utf-8", newline="\n") 
                while True:
                    # Read in batches to avoid memory overflow
                    batch_lines = read_n_lines(READ_BATCH_SIZE)
                    if empty(batch_lines):
                        break
                    try:
                        # Batch write to the database
                        for line in batch_lines:
                            row = json.loads(line)
                            insert_one(row)
                        total += len(batch_lines)
                    except Exception as e:
                        # Roll back unwritten data and remaining records to local storage
                        return False
            # All records written successfully, clear the local file
            src.close()
            with file_lock, open(self.path, "w", encoding="utf-8"):
                pass
            print(f"[{time.strftime('%H:%M:%S')}] Backfill completed, {total} rows written, cache cleared")
            return True
        except Exception as e:
            print(f"[{time.strftime('%H:%M:%S')}] Backfill failed: {e} (cache retained, will retry)")
            writer = None
            return False

2.3.4 Variables and Usage Instructions

Before running, ensure that the target database and tables exist in the DolphinDB server. Modify the following variables according to your requirements and execute the Python script. The full script is provided in the Appendix.

Table 5. Table 2-5: Variables for Real-Time Data Import
Variable Location Description
DDB config.py DolphinDB connection settings: host, port, username, password
PROXY config.py Proxy address; does not apply to OKX real-time data
TIMEOUT config.py WebSocket timeout in seconds; default: 5s
PROBE_COOLDOWN_SECS config.py Probe interval; default: 30s
READ_BATCH_SIZE config.py Maximum number of lines read per batch during backfill; default: 20000
LIVE_GET_TIMEOUT config.py Blocking wait time in seconds when fetching data from the real-time queue in live mode; default: 0.2s
RECONNECT_TIME config.py WebSocket reconnection wait time (effective for OKX real-time data)
OKX_WS_URL config.py WebSocket server address (effective for OKX real-time data)
dbName Target data file (global config) Target database name for subscription-based writes
tbName Target data file (global config) Target table name for subscription-based writes
streamtbName Target data file (global config) Stream table name
BUFFER_FILE Target data file (global config) Local cache file path
symbols Target data file (global config) Symbols to import (Binance format), e.g. ["btcusdt"]
inst_ids Target data file (global config) Symbols to import (OKX format), e.g. futures ["BTC-USDT-SWAP"], spot ["BTC-USDT"]
script Function in target data file: get_create_table_script() Table schema; For details, refer to Section 2.1.

The following example describes how to ingest level 2 futures data using Binance_Future_KLine.py. You can modify the corresponding parameters to ingest other types of data.

  1. Create the required databases and tables for the target data types. We recommend that you do not change database or table names. For the full script, refer to createDatabase.dos provided in the Appendix.
  2. Create the required stream tables. To enable persistence, add the following parameter to the node configuration file:
    persistenceDir=/home/DolphinDB/Data/Persistence
  3. Install the required dependencies in the Python environment. Example:
    pip install dolphindb
    pip install binance-connector
    pip install binance-futures-connector
    pip install websockets
    pip install python-okx
    pip install okx
  4. Modify config.py and the configuration class BinanceBaseConfig in Binance_Future_KLine.py as needed. Example:
    // config.py -- modify DDB and PROXY as needed
    DDB={"HOST":'192.168.100.43',"PORT":8848,"USER":'admin',"PWD":'123456'}
    BINANCE_BASE_CONFIG={"PROXY":'http://127.0.0.1:7890/',
                         "TIMEOUT":5,
                         "PROBE_COOLDOWN_SECS":30,
                         "READ_BATCH_SIZE":20000,
                         "LIVE_GET_TIMEOUT":0.2
                         }
    //...
    // Binance_Future_KLine.py -- modify as needed
    class BinanceFutureKLineConfig(BinanceBaseConfig):
     """Binance minute-level data ingestion configuration"""
        tableName = "Cryptocurrency_minKLineST"
        BUFFER_FILE = "./Binance_fKLine_fail_buffer.jsonl"
        symbols = ["btcusdt","ethusdt","adausdt","algousdt",
                   "bnbusdt","fetusdt","grtusdt","ltcusdt","xrpusdt"]
  5. Create a WebSocket connection, subscribe to the target market data, and keep the main thread running.
    // Subscription function must be configured
    def start_client_and_subscribe(self):
        // Create WebSocket connection
        client = UMFuturesWebsocketClient(
            on_message=self.create_message_handler(),
            proxies={'http': self.proxy_address, 'https': self.proxy_address}
        )
        // Subscribe to target symbols
        for s in self.symbols:
                client.kline(symbol=s,interval="1m")
                time.sleep(0.2)      
        return client
    # Usage
    if __name__ == "__main__":
        config = BinanceFutureKLineConfig()
        client = config.start_all()  
        # Keep main thread alive
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            config.quick_exit()
Note:

To ingest real-time OHLC data at different frequencies, modify the subscription parameters (interval for Binance, channel for OKX ). For example, modify line 10 in the above code. Supported real-time OHLC frequencies are consistent with historical OHLC frequencies (see Section 2.2). For OKX, the real-time OHLC frequency requires the channel prefix, such as 'candle1m' and 'candle3m'; other frequencies follow the same pattern.

2.4 Monitoring and Daily Scheduled Backfill

Due to the complexity and uncertainty of cryptocurrency exchange networks, exception handling and monitoring are critical. Therefore, scheduled jobs are configured in DolphinDB for data integrity monitoring and daily batch processing, with alerts sent via communication channels such as WeCom, reducing the difficulty of data maintenance in quantitative trading systems.

2.4.1 Real-Time Data Monitoring

Core functions

  • Stream table health checks: Monitor subscription status of stream tables (e.g., level 2 data, trade details, OHLC data) to prevent ingestion interruptions.
  • Real-time data checks: Monitor whether new data arrives in each table within 30 minutes to detect issues in data sources.
  • Alerting: Send real-time alerts via WeCom bots to ensure timely response.

2.4.2 Daily Data Processing

Core functions

  • Data integrity checks: Verify the completeness of minute-level OHLC data for the previous trading day (e.g., number of symbols × 1440 minutes × 2 markets).
  • Automatic backfill: If data is missing, batch-fetch data via RESTful APIs for both futures and spot markets.
  • Batch factor computation: Compute MyTT technical metrics, Alpha factors, etc., based on complete data that has been cleansed.
  • Retry policy: Retry up to 10 times to mitigate transient network issues; if still failing after retries, send alerts via WeCom bots.

2.4.3 Usage Instructions

This section describes how to configure the system based on actual needs. For the full scripts, refer to checkData.dos and getCleanKLineAfterDay.dos provided in the Appendix.

  1. Determine the WeCom group to receive alerts and create a bot to obtain the Webhook URL.
  2. Update the webhook variable in the scripts with the actual bot URL.
  3. Verify that the monitored/processed database and table names match the actual environment.
  4. Process daily data (getCleanKLineAfterDay.dos):
    • Modify codes in getBinanceCleanData(), as well as future_inst_ids and spot_inst_ids in getOKXCleanData() based on selected assets.
    • To add more factors, update factor definitions in outputNamesMap() and verify the target tables for factor storage.
  5. Use scheduleJob to add scheduled tasks and adjust execution times as needed.

2.5 Funding Rate Data Ingestion

Funding rates are a key metric of perpetual futures market sentiment and price deviation. Their sign and magnitude directly reflect long-short balance, providing important quantitative signals for trend analysis. Based on the characteristics of funding rate data, scripts are provided to batch-import historical data and to periodically fetch the latest data via scheduled jobs in DolphinDB.

Historical Data

Both Binance and OKX provide historical funding rate data; however, OKX only provides data of the most recent three months. Below, we use Binance's historical funding rate data as an example. The full script is provided in the Appendix.

  • getBinanceFundingRate: Fetches data from the exchange and parses it into the database.
    def getBinanceFundingRate(param,proxy_address,dbName,tbName){
       //...
        config[`proxy] = proxy_address
        response = httpClient::httpGet(baseUrl,param,10000,,config)
        result = parseExpr(response.text).eval()
        tb = each(def(mutable d){
            d["symbol"] = string(d.symbol)
           //...
            return d
        },result).reorderColumns!(`symbol`symbolSource`fundingTime`fundingRate`markPrice)
         loadTable(dbName,tbName).tableInsert(tb)
    }
  • getFundingRate: Retrieves historical data for specified symbols and time ranges, with reconnection on failure.
    def getFundingRate(codes,startDate,endDate,proxy_address,dbName,tbName){
        //...
        for(code in codes){
            do{
                param = dict(STRING, ANY)
                param["symbol"] = code
                param["startTime"] = startTimeUTC 
                param["endTime"] = endTimeUTC
                param["limit"] = 1000
                // Import with retry on failure
                getBinanceFundingRate(param,proxy_address,dbName,tbName)
                cursor += long(8)*3600*1000*1000
                sleep(200)
            }while(cursor < endTimeUTC)
        }
    }

You only need to set the cryptocurrency list, start time, and target database/table names.

codes = ["btcusdt","ethusdt","adausdt"].upper()
proxy_address = "http://127.0.0.1:7890"
dbName, tbName= ["dfs://CryptocurrencyDay",`fundingRate]
getFundingRate(codes,2023.01.01,2025.10.05,proxy_address,dbName,tbName)

Real-time data

Since funding rates are updated every 8 hours, scheduled jobs are used to fetch them. Binance is used as an example below. The full script is provided in the Appendix.

  • getBinanceFundingRate: Calls the RESTful API to fetch funding rates, formats them into a vector using parseExpr and transpose, and writes data to the target partitioned table.
    def getBinanceFundingRate(param,proxy_address,dbName,tbName){
        //...
        config[`proxy] = proxy_address
        response = httpClient::httpGet(baseUrl,param,10000,,config)
        //...
        for(r in result){
            r = select string(symbol),"Binance-Futures", timestamp(long(fundingTime)+8*3600*1000), 
                double(fundingRate), double(markPrice) from r.transpose()
         loadTable(dbName,tbName).tableInsert(r)
        }
    }
  • job_getFundingRate: Iterates over specified symbols and constructs the UTC start time. Requests are spaced by 200 ms to avoid rate limits. Modify the symbol list (codes) as needed (line 2). Alerts are sent on failure.
    def job_getFundingRate(webhook,proxy_address,dbName,tbName){
        codes = ["btcusdt","ethusdt"].upper()
        // Use the timestamp from 2 minutes ago as the start time; schedule the job at :01
        startTimeUTC = convertTZ(now()-2*60*1000, "Asia/Shanghai", "UTC") 
        ts = long(timestamp(startTimeUTC))
        for(code in codes){
            param = dict(STRING, ANY)
            param["symbol"] = code
            param["startTime"] = ts 
            getBinanceFundingRate(param)
            sleep(200)
        }
        if(errCnt == codes.size()){
            msg = "Failed to fetch Binance fundingRate"
            sendWeChatMsg(msg,webhook)
    }

Configure scheduled jobs to fetch updated funding rates:

times = [08:01m, 16:01m, 00:01m]
proxy_address = 'http://127.0.0.1:7890'
dbName, tbName= ["dfs://CryptocurrencyDay",`fundingRate]
scheduleJob("fundingRateFetcher", "Fetch fundingRate",job_getFundingRate{proxy_address,dbName,tbName},times,
    2025.06.17, 2035.12.31,"D")

2.6 Market Data Visualization

After real-time market data ingestion, you can view live data in the DolphinDB data dashboard by importing the provided panel files. Dashboards support custom refresh intervals (e.g., 1 s). An example dashboard is shown below.

Figure 8. Figure 2-3 Market Data Dashboard Example

3. Unified Stream and Batch Processing for Factor Development

In quantitative trading, factor discovery is a critical step. Traditional approaches can be divided into manual discovery and algorithm-based discovery. Manual discovery relies heavily on researchers’ expertise and market insight, but it is inefficient, labor-intensive, and constrained by individual cognitive limits, making it difficult to fully capture the complex characteristics of the market. Algorithm-based discovery, on the other hand, leverages techniques such as machine learning to reduce manual costs. In particular, deep learning and neural networks, with their strong nonlinear fitting capabilities, can effectively capture complex nonlinear relationships among price-volume data, market sentiment, and macro or geopolitical factors in the cryptocurrency market, thereby generating factors that better reflect market fundamentals. Although deep learning models carry the risk of overfitting, this risk can be effectively controlled through proper model design, parameter tuning, and regularization, making them highly valuable for factor mining in cryptocurrency markets.

Training machine learning models for factor generation requires fast access to and processing of massive volumes of historical data. Traditional databases often suffer from low storage efficiency and slow query performance under such data scales. DolphinDB offers multiple advantages in quantitative factor discovery, significantly improving overall efficiency while aligning with practical requirements. Its built-in library of over 2,000 functions covering multiple domains and use cases provides fast and accurate data support for factor discovery, enabling machine learning models to be trained and optimized based on the latest market dynamics. In addition, DolphinDB’s streaming-batch framework allows you to efficiently perform batch factor computation while also supporting low-latency streaming computation, which can be applied in real time to cryptocurrency backtesting and simulated trading, closely approximating live trading conditions.

Accordingly, this chapter focuses on three aspects:

  • How to design an optimal storage scheme for minute-level factor data based on DolphinDB;
  • How to implement batch and streaming factor computation using DolphinDB’s built-in modules and plugins;
  • How machine learning models are applied in backtesting and simulation.

3.1 Minute-Level Factor Database and Table Design

Given the characteristics of minute-level factor data and practical usage scenarios, we recommend using the TSDB storage engine and adopting a narrow-table schema with time and factor columns as partition keys. This design supports continuous growth in the number of factors while ensuring high write and query performance. The specific database and table design is as follows; the full script is provided in the Appendix.

Table 6. Table 3-1: Minute-Level Factor Database Schema
Database Name Storage Engine Partition Scheme Partition Columns Sort Columns Partition Size
CryptocurrencyFactor TSDB Monthly partition + VALUE partition by factor name Time + factor name Market + asset + time About 115.36 MB per single factor for 100 assets
Note:

If the estimated number of combinations of market and asset exceeds 1,000, it is recommended to reduce the dimension of the sort columns during database creation by adding the following parameter: sortKeyMappingFunction=[hashBucket{,3}, hashBucket{,300}].

The column definitions of the minute-level factor table (factor_1m) are as follows.

Table 7. Table 3-2: Minute-Level Factor column Description
Column Name Data Type Description
datetime TIMESTAMP Time column
symbol SYMBOL Asset for factor computation
market SYMBOL Market, such as ‘Binance-Futures’, ‘Binance-Spot’, ‘OKX-Futures’, ‘OKX-Spot’
factorname SYMBOL Factor name
factorvalue DOUBLE Factor value

3.2 Factor Computation

Taking DolphinDB’s built-in 191 Alpha and WorldQuant 101 Alpha factor libraries as examples, these factors were originally designed for the stock market. Therefore, cross-sectional and financial-statement-based factors are not applicable to the cryptocurrency market, and time-series factors are not defined as state functions. In this tutorial, applicable time-series factors are converted into state functions and stored in the stateFactors.dos file. You can load this module via use or modify it to develop custom factors. The full script is stateFactors.dos provided in the Appendix.

Using the gtjaAlpha100 factor as an example, we demonstrate how to perform both batch and streaming computation for the same factor in DolphinDB. In stateFactors.dos, gtjaAlpha100 is defined as:

@state
def gtjaAlpha100(vol){
    return mstd(vol, 20)
}

Compared with its definition in gtja191Alpha.dos, the only difference is the addition of @state, which converts it from a regular function into a state function, enabling direct use in streaming computation.

3.2.1 Batch Factor Computation

Factors are computed based on minute-level OHLC data, so historical minute-level OHLC data must exist in the DolphinDB server.

Batch computation consists of three steps: retrieving historical data, generating factor computation expressions, and computing and writing results to the database. The full script is provided in the Appendix.

  1. Retrieve historical data from the database.
    // Use the factor module
    use stateFactors
    go
    all_data = select * from loadTable("dfs://CryptocurrencyKLine","minKLine")
    factor_value_tb = loadTable("dfs://CryptocurrencyFactor", "factor_1min")
  2. Generate factor computation expressions.
    cols_dict = {
        "vol": "volume"
    }
    def dict_replace_str(s, str_dict){
        for (i in str_dict.keys()){
            result = regexReplace(s, i, str_dict[i])
        }
        return result
    }
    use_func_call = exec name.split("::")[1]+syntax from defs() where name ilike "%stateFactors%"
    funcName = func_call[:regexFind(func_call, "[()]+")]
    deal_func_call = dict_replace_str(func_call, cols_dict)
    select_string = stringFormat(
        "select eventTime,symbol,symbolSource,\"%W\" as factorname,
         %W as factorvalue from all_data
         context by symbol,symbolSource csort eventTime",
        funcName, deal_func_call)

    Here, dict_replace_str and cols_dict are used to replace parameter names in function expressions. For example, the parameter vol corresponds to the column volume in the database, allowing you to compute factors without modifying original column names.

  3. Loop through all factors and append results to the database.
    for (func_call in use_func_call){// func_call=use_func_call[0]
        funcName = func_call[:regexFind(func_call, "[()]+")]
        deal_func_call = dict_replace_str(func_call, cols_dict)
        select_string = stringFormat("select eventTime,symbol,symbolSource,\"%W\" as factorname, %W as factorvalue from all_data context by symbol,symbolSource csort eventTime", funcName, deal_func_call)
        result = select * from parseExpr(select_string).eval()
        factor_value_tb.append!(result)
    }

3.2.2 Streaming Factor Computation

Factors are computed based on minute-level OHLC data, so a minute-level OHLC stream table must exist.

Streaming factor computation includes three steps: creating a factor stream table, building a narrow reactive state engine, and subscribing to upstream data streams. The full script is provided in the Appendix.

  1. Create a persistent stream table to store computed factor data.
    // Stream table used to store factor data
    output_stream = "min_factor_stream"
    // Output table definition and conversion function
    try{dropStreamTable(output_stream)}catch(ex){}
    temp = streamTable(1:0, ["datetime","symbol","market","factorname","factorvalue"],
            ["TIMESTAMP","SYMBOL","SYMBOL","SYMBOL","DOUBLE"])
    enableTableShareAndCachePurge(temp, output_stream, 10000000)
    def output_handler(msg, output_stream){
        if (count(msg) == 0){return}
        cols = msg.columnNames()[2 0 1 3 4]
        tb = objByName(output_stream)
        tb.append!(<select _$$cols from msg>.eval())
    }
    //Used as a placeholder for output table in the engine with no actual effect.
    try{dropStreamTable("output_tmp")}catch(ex){}
    share streamTable(1:0, ["symbol","market","datetime","factorname","factorvalue"],
            ["SYMBOL","SYMBOL","TIMESTAMP","SYMBOL","DOUBLE"]) as output_tmp

    Before building the reactive state engine that returns a table in narrow format, the output table and output handler must be created. The above code defines output_handler and two stream tables (output_stream and output_tmp). The output_handler function will serve as the outputHandler parameter for the engine. After setting this parameter, the results will no longer be written to the output table, but instead, output_handler will process the results. Therefore, the actual result writing happens at line 12 of the code. output_tmp is used as a placeholder, and after the engine is created, this stream table can be deleted.

  2. Build the narrow reactive state engine.
    // List of required factor function names
    func_table = select * from defs() where name ilike "%stateFactors%"
    factor_fuc_name = func_table["name"].split("::")[1]
    // List of factor metric computation meta-code
    factor = [<eventTime>]
    factor_fuc_syntax = parseExpr(func_table["name"] + func_table["syntax"])
    factor.appendTuple!(factor_fuc_syntax)
    engine_name = "cryto_cal_min_factor_stream"
    // Try to drop the existing stream engine, if any
    try { dropStreamEngine(engine_name) } catch (ex) {}
    // Create the narrow reactive state engine
    engine = createNarrowReactiveStateEngine(
        name=engine_name,
        keyColumn=["symbol", "symbolSource"], // Grouping columns
        metrics=factor, // Factor computation meta-code
        metricNames=factor_fuc_name, // Factor name
        dummyTable=table(1:0, ["eventTime", "collectionTime", "symbolSource", "symbol", "open", "high", "low", "close", "vol", "numberOfTrades", "quoteVolume", "takerBuyBase", "takerBuyQuote", "volCcy"], ["TIMESTAMP", "TIMESTAMP", "SYMBOL", "SYMBOL", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE", "INT", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE"]),
        outputHandler=output_handler{, output_stream},
        outputTable=objByName("output_tmp"),
        msgAsTable=true
    )
    try { dropStreamTable("output_tmp") } catch (ex) {}

    To add custom factors, you only need to append factor names and computation logic to factor_fuc_name and factor.

  3. Subscribe to upstream data streams.

    For example, subscribe to Cryptocurrency_minKLineST, the minute-level OHLC stream table.

    input_stream = 'Cryptocurrency_minKLineST'
    try{unsubscribeTable(,input_stream, "cal_min_factors")}catch(ex){}
    subscribeTable(,input_stream, "cal_min_factors", getPersistenceMeta(objByName(input_stream))["memoryOffset"], engine, msgAsTable=true, throttle=1, batchSize=10)

3.3 Model Training and Real-Time Signal Generation

After factor computation and storage, machine learning models are trained and applied to backtesting and simulation for generating trading signals. The workflow consists of four steps:

  1. factor data retrieval and preprocessing;
  2. factor model training in Python;
  3. factor model export;
  4. trading signal prediction within DolphinDB combined with the backtesting and simulation framework. The fourth step supports both historical and real-time signal generation. The overall workflow is illustrated in Figure 3-1.
Figure 9. Figure 3-1: Machine Learning–Based Backtesting and Simulation Workflow

3.3.1 Data Retrieval and Preprocessing

Historically computed factor data is stored in dfs://CryptocurrencyFactor, currently containing only 1-minute factors, with more frequencies to be added in the future. To simplify factor data retrieval and standardization in a Python client via the DolphinDB API, preprocessing is encapsulated in the Python class FactorDataloader. All preprocessing is executed on the DolphinDB server, and the Python client only receives the processed data.

FactorDataloader provides preprocessing support for LSTM, XGBoost, and Transformer models. Example:

from DataPre import FactorDataloader
ddb_connect = ddb.session("ip", port)
t = FactorDataloader(
    db_connect=ddb_connect,
    symbol="BTCUSDT",
    start_date="2024.03.01",
    end_date="2024.03.02",
    factor_name=["gtjaAlpha2"],
    market="Binance-Futures",
)
train_loader, test_loader = t.get_LSTM_dataloader(10, 0.2, 32)
train_loader, test_loader = t.get_xgboost_data(0.2)
train_loader, test_loader = t.get_transformer_data(1440, 0.2, 32)

3.3.2 Python Model Training

The DolphinDB’s GpuLibTorch plugin allows you to load and run the TorchScript model exported from Python, combining DolphinDB’s data processing capabilities with PyTorch’s deep learning functionality. You can run the following functions to install and load the GpuLibTorch plugin.

login("admin", "123456")
listRemotePlugins()
installPlugin("GpuLibTorch")
loadPlugin("GpuLibTorch")

The plugin does not support training models within DolphinDB, so training must be performed in a Python environment. A generic training and evaluation framework for three-class classification tasks is implemented in Models.py, supporting LSTM and Transformer models. The related functions are as follows:

# Functions
evaluate_classify_model(model, test_loader, device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
train_model(
    model,
    criterion,
    optimizer,
    num_epochs: int,
    train_loader,
    test_loader,
    device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
    per_epoch_show: int = 20,
)
# Usage
# Model training
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
trained_model = train_model(
    model=model,
    criterion=criterion,
    optimizer=optimizer,
    num_epochs=100,
    train_loader=train_loader,
    test_loader=test_loader,
)
# Model evaluation
accuracy, preds, targets = evaluate_classify_model(trained_model, test_loader)

3.3.3 Model Export

After training in Python, the model needs to be exported as a model.pth file for prediction in DolphinDB. Models exported using torch.jit.trace may cause an error when loading in DolphinDB due to hidden layer tensors and input tensors not being on the same device. Therefore, we recommend that you use torch.jit.script to export the model file.

torch.jit.script(model).save("model.pth")

3.3.4 Model Prediction and Backtesting/Simulation

Historical Computation Method

After obtaining the model, predictions can be made on upstream factor data using the GpuLibtorch plugin to generate trading signals for backtesting. This method requires pre-computed future signals and is suitable for backtesting only. For real-time simulation, the real-time computation method discussed later should be used.

table_name = "factor_predict_result"
try{
    undef(table_name, SHARED)
    print(table_name+" exists, stream table cleared")
} catch(ex) {
    print(table_name+" does not exist, environment cleaned")
}
share(table(100000: 0, `datetime`signal, [TIMESTAMP, INT]), table_name)
model = GpuLibTorch::load(model_file)
predict_result = GpuLibTorch::predict(model, data_input)

The above code creates a stream table named factor_predict_result to store the pre-computed signals for easy retrieval during backtesting. As shown in lines 9-10, you can load the trained model using GpuLibTorch and input pre-processed data (data_input) to get the model output (predict_result). The results are then inserted into the factor_predict_result stream table. The preprocessing of data_input is detailed in the DataPre.py script provided in the Appendix.

After obtaining the trading signal stream table factor_predict_result, it can be used in the event callback function for backtesting. The following example generates trading signals for BTCUSDT from 2025.03.01 to 2025.05.01 using an LSTM model, as shown in line 3. The callback function performs buy and sell operations based on the signal, as shown in lines 11 and 17. The full script is in the Appendix.

def onBar(mutable context, msg, indicator){
	//...
    singal_t = objByName("factor_predict_result")
    for(isymbol in msg.keys()){    
        source = msg[isymbol]["symbolSource"]
        lastPrice = msg[isymbol]["close"]
        tdt = msg[isymbol]["tradeTime"]
        signal = exec signal from singal_t where datetime=tdt
        signal = signal[0]
        if (signal==int(NULL)){break}
        if (signal==0){
            // Sell
            qty = Backtest::getPosition(context["engine"], isymbol, "futures").longPosition
            if (qty > 0){
                Backtest::submitOrder(context["engine"], (isymbol, source, context["tradeTime"], 0, lastPrice, 0, 1000000, qty, 3, 0, 0, context["tradeTime"].temporalAdd(1m)), "sell", 0, "futures")
            }
        } else if (signal==2){
            // Buy
            Backtest::submitOrder(context["engine"], (isymbol, source, context["tradeTime"], 5, lastPrice, 0, 1000000, 0.001, 1, 0, 0, context["tradeTime"].temporalAdd(1m)), "buy", 0, "futures")
        }
    }
}

After the backtest is completed, the daily realized profit and loss curve can be plotted as shown in Figure 3-2:

result = select * from day where accountType="futures"
plot((double(result["realizedPnl"])), result["tradeDate"])
Figure 10. Figure 3-2: Daily Realized Profit and Loss Curve of Backtesting Results

Real-time computation method

In addition to pre-computing trading signals for backtesting based on historical data, you can also simulate real-time trading by computing real-time factors and generating trading signals using a pre-trained model. This method is suitable for both historical market backtesting and simulated trading.

Below is the core code for a minute-level backtest demo that uses the gtjaAlpha94 and gtjaAlpha50 alpha factors for prediction. Since the factor computation functions in the built-in factor library of DolphinDB are not state functions, you need to use the factor computation functions that have been converted to state functions in the stateFactors module (see the previous section for module description).

  1. Pre-load the required model using a shared dictionary.
    // Initialize the model through a shared dictionary
    model_dir = "/home/customer/model_config/test_LSTM.pth"
    model = GpuLibTorch::load(model_dir)
    GpuLibTorch::setDevice(model, "CUDA")
    try{undef("test_ml_model", SHARED)}catch(ex){}
    test_ml_model = syncDict(STRING, ANY, "test_ml_model")
    go;
    test_ml_model["model"] = model

    This avoids reloading the model during subsequent computation. After loading the model, the GpuLibTorch::setDevice function must be called to specify the computing device for prediction. Otherwise, the default CPU will be used.

  2. Subscribe to signals in the initialize function to compute metrics.
    def gen_signal(factor1, factor2){
        model = objByName("test_ml_model")["model"]
        data_input = matrix(zscore(factor1).nullFill!(0), zscore(factor2)).nullFill!(0).float()
        data_input = tensor(array(ANY).append!(data_input))
        x = GpuLibTorch::predict(model, data_input)[0]
        signal = imax(1/(1+exp(x*-1)))
        return signal
    }
    def initialize(mutable context){     
        print("initialize load model")
        // Metric subscription
        indicator_dict = dict(STRING, ANY)
        indicator_dict["signal"] = <moving(gen_signal, (gtjaAlpha94(close.double(), volume.double()), gtjaAlpha50(high.double(), low.double())), 20)>
    
        Backtest::subscribeIndicator(context["engine"], "kline", indicator_dict, "futures")
    }

    By defining the meta-code in the initialize function, the backtesting engine can compute factor data in real time during backtesting and simulation. The corresponding model can be called in the signal computation function to generate signals. The event callback functions then obtain the trading signals through the indicator parameter. Here’s a usage example for the onBar callback function:

    def onBar(mutable context, msg, indicator){
        //...
        one_msg = msg[isymbol]
        one_indicator = indicator[isymbol]
        signal = one_indicator["signal"]
        tdt = one_msg["tradeTime"]
        if (signal == int(NULL)){continue}
        if (signal == 0){
            // Sell
            qty = Backtest::getPosition(context["engine"], isymbol, "futures").longPosition
            if (qty > 0){
                Backtest::submitOrder(context["engine"], (isymbol, source, context["tradeTime"], 0, lastPrice, 0, 1000000, qty, 3, 0, 0, context["tradeTime"].temporalAdd(1m)), "sell", 0, "futures")
            }
        }else if (signal == 2){
            // Buy
            Backtest::submitOrder(context["engine"], (isymbol, source, context["tradeTime"], 5, lastPrice, 0, 1000000, 0.001, 1, 0, 0, context["tradeTime"].temporalAdd(1m)), "buy", 0, "futures")
        }
    }

In the above onBar callback function, you can obtain the real-time computed results of the signal generation function, which are subscribed to in the initialize function, through the indicator parameter, and then open or close positions based on the corresponding signals. The full script is provided in the Appendix.

4. Strategy Backtesting and Simulated Trading

Before deploying a quantitative strategy to live trading, extensive backtesting and simulated trading are required to validate its effectiveness and feasibility. Mid- to high-frequency strategy backtesting and simulation for cryptocurrency trading face two major challenges. First, massive data storage and processing, especially for high-frequency snapshot data, which places extremely high demands on framework-level data processing and computational performance. Second, complex matching logic, where order execution must consider order price, traded volume, and market volatility, rather than simply using the latest price.

Leveraging its high-performance distributed architecture and low-latency streaming engine, DolphinDB integrates a backtesting engine with an order matching simulator to deliver an end-to-end backtesting and simulated trading solution. This solution supports market data replay and processing, strategy development and code management, result visualization, and strategy permission management. It supports both snapshot and minute-level cryptocurrency data, enabling individuals and quantitative teams to efficiently develop strategies with low code volume and a low learning curve. In addition to native DLang, strategies can also be written in Python and C++, offering high performance, low cost, and strong extensibility.

4.1 Cryptocurrency Backtesting Engine

In DolphinDB, backtesting consists of four components: user-defined strategy functions, strategy configuration and creation, market data replay, and execution of the backtesting engine to obtain results. The cryptocurrency backtesting engine is provided as a plugin, and its logical architecture is shown in Figure 4-1. The main workflow includes:

  1. The engine replays market data streams in chronological order and dispatches them to the order matching simulator and market data callback functions.
  2. Market data callback functions process strategy logic and submit orders.
  3. The engine performs risk control management.
  4. Orders that pass risk control are sent to the order matching simulator for execution.
  5. The engine tracks positions and funds in real time, and returns strategy returns and trade details after backtesting completes.

The backtesting engine supports both snapshot and minute-level cryptocurrency data, and allows a single engine to manage multiple spot and futures accounts as well as different contract types. Cryptocurrency methods support specifying account operations via the accountType parameter.

Figure 11. Figure 4-1: Architecture of the Mid- to High-Frequency Backtesting Engine

When using DolphinDB for cryptocurrency strategy backtesting, the market data passed into the engine must strictly conform to the required data schema, often requiring substantial data cleansing and transformation. However, since strategy researchers prioritize designing and optimizing strategy logic, i.e., writing market callback functions, this solution streamlines the entire workflow. It not only provides raw exchange data, but also delivers processed and replay-ready snapshot and minute-level market data. Therefore, you can focus only on custom strategy functions and strategy configuration to quickly complete backtesting.

4.2 Strategy Development and Code Management

Market data access, backtest execution functions, trade simulation functions, and code management for cryptocurrency backtesting are encapsulated in the CryptocurrencySolution module. Based on the cryptocurrency data environment built in Chapter 2, you can directly use this module to perform strategy backtesting. The sub-modules are described below:

Table 8. Table 4-1: Cryptocurrency Module Description
Sub-module Catagory Description
CryptocurrencySolution::utils Common functions Includes common functions such as engine deletion and management, funding rate retrieval, base information tables, OHLC data downsampling, backtest and simulation strategy retrieval, strategy log retrieval, and database/stream table status checks.
CryptocurrencySolution::setting Unified settings Includes unified strategy configuration, GitLab address configuration, module lists, and database/table metadata.
CryptocurrencySolution::runBacktest Backtesting Includes market data retrieval and processing functions, and backtest execution functions.
CryptocurrencySolution::simulatedTrading Simulation Includes market data retrieval and processing functions, and simulation start/stop function.
CryptocurrencySolution::manageScripts Code storage Includes strategy upload, retrieval, and deletion functions, backtest result storage and retrieval functions, and backtest/simulation code submission and storage function.

First, obtain the home directory using getHomeDir(), place the attached module folder in the corresponding path, and load it using use:

use CryptocurrencySolution::manageScripts
go

To meet strategy storage and management requirements, this solution integrates with GitLab. When submitting a strategy for backtesting or simulation, the system automatically uploads the strategy code to a specified Git repository, enabling unified management, version tracking, and rapid retrieval. For details, see Section 4.4.

If strategy code storage is not required, you can use the following modules for backtesting or simulation only:

// Backtesting
use CryptocurrencySolution::runBacktest
go
// Simulation
use CryptocurrencySolution::simulatedTrading
go

4.2.1 Writing Cryptocurrency Strategies

In this section, we introduce how to write cryptocurrency strategies. More detailed examples are provided in Chapter 5.

User-defined event callback functions

The backtesting engine adopts an event-driven mechanism and provides the following event functions. You can customize callbacks for strategy initialization, daily pre-market processing, snapshot and OHLC market data, and order and trade reports.

Table 9. Table 4-2: Strategy Callback Functions
Event Function Description
initialize(mutable context) Strategy initialization function: triggered once. The parameter context represents the logical context. In this function, you can use context to initialize global variables or subscribe to metric computation.
beforeTrading(mutable context) Callback executed during the daily session rollover; triggered once when the trading day switches.
onSnapshot(mutable context, msg) Snapshot market data callback.
onBar(mutable context, msg) Low- to mid-frequency market data callback.
onOrder(mutable context, orders) Order report callback, triggered whenever an order’s status changes.
onTrade(mutable context, trades) Trade execution callback, triggered when a trade occurs.
finalize(mutable context) Triggered once before strategy termination.
Note:
  • If GitLab-based strategy code management is required, the strategy must be declared using module, and the module name must match strategyName.
  • Required modules used in callbacks must be listed in getUsedModuleName() in CryptocurrencySolution::setting. Otherwise, strategy code will fail to be uploaded.
module testMinStrategy   //Declare the strategy module when enabling code management
def initialize(mutable context){	
}
def beforeTrading(mutable context){
}
def onBar(mutable context,msg, indicator){
}
def onSnapshot( mutable context,msg, indicator){
}
def onOrder( mutable context,orders){
}
def onTrade(mutable context,trades){
}
def finalize (mutable context){
}
strategyName = "testMinStrategy"

4.2.2 Cryptocurrency Strategy Configuration

The userConfig strategy configuration must include the strategy type, backtest start and end dates, market data type, initial capital, and the cryptocurrency asset to be backtested. The CryptocurrencySolution::setting module provides default strategy configuration, which can be obtained using the following function:

startDate = 2025.08.24
endDate = 2025.08.25
dataType = 3 // Minute-level market data; set to 1 for snapshot data
userConfig = CryptocurrencySolution::setting::getUnifiedConfig(startDate, endDate, dataType)

An example of the userConfig is shown below:

sym = ["BTCUSDT"]
userConfig = dict(STRING,ANY)
userConfig["startDate"] = 2025.08.24
userConfig["endDate"] = 2025.08.25
userConfig["strategyGroup"] = "cryptocurrency" 
cash = dict(STRING,DOUBLE)  
cash["spot"] = 1000000.
cash["futures"] = 1000000.
cash["option"] = 1000000.
userConfig["cash"] = cash
userConfig["dataType"] = 3    // 1: snapshot; 3: minute-level
userConfig["Universe"] = sym  // Symbol for backtesting (required)
p = dict(STRING,ANY)
p["Universe"] = userConfig["Universe"] + "_futures"
p["log"] = table(10000:0,[`tradeDate,`time,`info],[DATE,TIMESTAMP,STRING]) // log table for debugging
userConfig["context"] = p    // Pass the asset universe

To backtest historical market data, you can customize the start and end dates in userConfig. This module automatically loads the default strategy configuration and allows user-provided userConfig values to override the defaults. By default, Binance data is used, and the backtest period is the most recent 10 days.

userConfig = dict(STRING,ANY)
userConfig["startDate"] = 2025.08.24
userConfig["endDate"] = 2025.08.25
// Example: backtest an asset (used to define the asset universe)
p = dict(STRING,ANY)
p["Universe"] = ["XRPUSDT_futures"]    
userConfig["context"] = p
strategyType = 0
engine, straname_ =
    CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(
        strategyName, eventCallbacks, strategyType, userConfig
    )

To downsample OHLC data, you can use the built-in downsampling function and specify the barMinutes parameter:

strategyType = 0
engine, straname_ =
    CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(
        strategyName, eventCallbacks, strategyType, , barMinutes = 1
    )

To use market data from the OKX exchange, specify exchange = "OKX". Otherwise, it defaults to "Binance":

engine, straname_ =
    CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(
        strategyName, eventCallbacks, strategyType, exchange = "OKX"
    )

4.2.3 Strategy Backtest Execution and Result Retrieval

After setting the strategyName, eventCallbacks, and strategyType parameters, call runCryptoAndUploadToGit to execute the backtest.

strategyName = "testMinStrategy"
eventCallbacks = {
    "initialize": initialize,
    "beforeTrading": beforeTrading,
    "onBar": onBar,
    "onOrder": onOrder,
    "onTrade": onTrade,
    "finalize": finalize
}
// Execute backtest and upload strategy files to GitLab
strategyType = 0  
engine, straname_ =
    CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(
        strategyName, eventCallbacks, strategyType
    )
  • During backtest execution, the strategy files are automatically uploaded to the specified GitLab repository, and the corresponding commit ID is stored in the gitstrategy table of the database dfs://CryptocurrencyStrategy.
  • If strategy code management is not required, you can use runCryptoBacktest from the CryptocurrencySolution::runBacktest module. This function automatically loads default strategy configuration and allows you to override it with a custom userConfig. By default, it performs backtesting on Binance data for the most recent 10 days.
// userConfig = dict(STRING,ANY)
// userConfig["startDate"] = 2025.11.10
// userConfig["endDate"] = 2025.11.20
engine, straname_ =
    CryptocurrencySolution::runBacktest::runCryptoBacktest(
        strategyName, eventCallbacks, userConfig
    )

Retrieve backtest results

After the backtest completes, you can retrieve daily positions, daily equity, return summary, trade details, and strategy configurations using the following methods.

// Trade details
tradeDetails = Backtest::getTradeDetails(engine, "futures")
// Unfilled orders
tradeDetails[tradeDetails.orderStatus == -3]
// Current open (unfilled) orders
Backtest::getOpenOrders(engine, "futures")
// Daily positions
Backtest::getDailyPosition(engine, "futures")
// Overall backtest summary
Backtest::getReturnSummary(engine)
  • After the backtest completes, the strategy return summary and configuration are stored in dfs://CryptocurrencyStrategy/backtestStrategy for comparative analysis.
  • Visualization of backtest results is described in Section 4.3.

4.2.4 Submitting Simulated Trading

After real-time market data is ingested into stream tables, set strategyType = 1 to enable simulated trading. The strategy name will automatically be suffixed with SimulatedTrading, and the strategy code will be managed and stored.

You can use the overwrite parameter to control whether strategies with the same name are automatically deleted (defaults to false). By default, Binance data is used and simulation starts from the current day.

strategyType = 1
engine, straname_ =
    CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(
        strategyName, eventCallbacks, strategyType,
        userConfig = NULL, overwrite = false
    )

After simulation starts, trade details, real-time positions, and equity are written in real time to the corresponding simulation tables, for example:

  • strategyName + "_tradeDetails"
  • strategyName + "_position"

If strategy code management is not required, you can run simulated trading using the submitCryptoSimulatedTrading function in the CryptocurrencySolution::simulatedTrading module. This function automatically loads the default strategy configuration and allows you to override it with a custom userConfig. To run a simulation based on an existing backtest strategy, use the submitCryptoSimulatedTradingByStrategyName function, which submits the backtest strategy (strategyName_) in simulation mode.

// Directly submit simulated trading
engine, straname_ = submitCryptoSimulatedTrading(strategyName, eventCallbacks)
// Submit simulated trading based on an existing backtest strategy
engine, straname_ = submitCryptoSimulatedTradingByStrategyName(strategyName_, overwrite = false)
  • Simulation result dashboards are described in Section 4.3.
  • If you need to run simulated trading on downsampled OHLC data, use the time-series engine to process the original stream tables. For details, see klineMergeScript.dos in the Appendix.

4.2.5 Retrieving Strategy Logs

To simplify debugging, you can write logs inside strategy callback functions:

def beforeTrading(mutable context){
    // Write logs to the engine-maintained log table
    context.log.tableInsert(context["tradeDate"], now(), "beforeTrading")
}

Use getStrategyLog to retrieve strategy logs. If you define a custom userConfig, you must manually add log settings.

CryptocurrencySolution::utils::getStrategyLog(straname_)

Sample strategy logs:

4.2.6 Deleting Strategies

To avoid excessive engine creation, each user is limited to a maximum of 5 strategy engines. Unneeded engines can be removed using dropMyBacktestEngine. For simulated strategies, the corresponding result tables are also deleted. You can only delete strategies created by yourself.

use CryptocurrencySolution::utils
straname_ = 'admin_testMinStrategy'
dropMyBacktestEngine(straname_)

4.2.7 Strategy Code Management

To meet requirements for strategy code storage and versioning, we integrate backend modules with a GitLab repository. When a backtest or simulation is submitted, the backend automatically uploads the strategy event callback functions to a user-defined GitLab repository and records version information in the corresponding database tables. All GitLab-related strategy management functions are encapsulated in the CryptocurrencySolution::manageScripts module.

First, you must create a GitLab account and obtain a GitLab Personal Access Token for authentication. See Personal access tokens | GitLab Docs for details. When using strategy code management, you can either pass gitInfo or configure it globally in the CryptocurrencySolution::setting module. Example:

gitInfo = dict(STRING,ANY)
gitInfo[`gitLabSite] = "https://dolphindb.net"   // GitLab URL
gitInfo[`repoId] = 620                          // Project ID
gitInfo[`repoBranch] = "main"                   // Branch name
gitInfo[`privateToken] = "zQx-BzyJrHxd_sbnEFNR"  // Access token

Below are the main functions provided by this module, including uploading, retrieving, and deleting strategies.

Upload strategy files

def uploadOrupdateToGit(
    strategyName, strategyType, eventCallbacks,
    gitInfo = NULL, commitMsg = NULL, timeout = 10000
)

Parameter description

Parameter Type/Form Description Required
strategyName SYMBOL Strategy name Yes
strategyType INT Strategy type (0: backtest, 1: simulation) Yes
eventCallbacks DICT Event callback functions Yes
gitInfo DICT GitLab configuration; an example is displayed at the start of the section. No
commitMsg STRING Commit mesaage No
timeout INT Request timeout No

As described in Section 4.2, you can run backtests or simulations via runCryptoAndUploadToGit, which automatically uploads strategy code. Strategy versions (both new and updated) are recorded in dfs://CryptocurrencyStrategy/gitstrategy. You can retrieve specific versions using the stored commit IDs.

Retrieve strategy files

def getStrategyFromGit(
    strategyName, commitId = NULL, gitInfo = NULL, timeout = 10000
)

Parameter description

Parameter Type/Form Description Required
strategyName SYMBOL Strategy name Yes
commitId STRING ID of the commit that uploads the strategy No
gitInfo DICT GitLab configuration; an example is displayed at the start of the section. No
timeout INT Request timeout No

By default, this function retrieves the latest version of the strategy. To fetch a specific version, specify commitId based on the records in dfs://CryptocurrencyStrategy/gitstrategy.

Delete strategy files

def deleteStrategyFromGit(
    strategyName, gitInfo = NULL, timeout = 10000
)

Parameter description

Parameter Type/Form Description Required
strategyName SYMBOL Strategy name Yes
gitInfo DICT GitLab configuration No
timeout INT Request timeout No

This function deletes the corresponding strategy file from GitLab. Use with caution. You can only delete strategies uploaded by yourself.

Submit backtests or simulations with code upload

def runCryptoAndUploadToGit(
    strategyName, eventCallbacks, strategyType, userConfig = NULL,
    barMinutes = 1, overwrite = false, gitInfo = NULL,
    commitMsg = NULL, timeout = 10000
)

Parameter description

Parameter Type/Form Description Required
strategyName SYMBOL Strategy name Yes
eventCallbacks DICT Event callback functions Yes
strategyType INT Strategy type (0: backtest, 1: simulation) Yes
userConfig DICT Strategy configuration (Only start/end time can be modified; defaults are defined in setting) No
exchange STRING Exchange providing the source data (default: "Binance"). No
barMinutes INT Frequency in minutes, effective for OHLC data only. No
overwrite BOOL Whether to delete existing simulation strategies with the same name (default: false). No
gitInfo DICT GitLab configuration No
commitMsg STRING Commit mesaage No
timeout INT Request timeout No

This function wraps backtest/simulation submission and strategy code upload, using default strategy configuration. You only need to define strategyName, eventCallbacks, and strategyType to quickly deploy a cryptocurrency strategy. For details, see Section 4.2.3.

4.3 Strategy Result Visualization

To help you analyze backtesting and simulated trading results more intuitively, we provide separate dashboards for backtesting and simulation. These dashboards enable visual strategy management. You only need to import the dashboard files included in the Appendix—no additional configuration is required.

4.3.1 Backtesting Strategy Visualization

The backtesting dashboard displays all backtesting strategies and their results. It includes the following content:

  • Strategy list

    Shows the strategy names, engine status, error messages, and related information.

  • Backtesting results

    Select a strategy name and click Query to retrieve the backtesting results.

  • Submit strategy for simulation

    You can submit strategies that have been backtested for simulated trading. The simulation strategies will appear in the simulation dashboard.

  • Delete backtesting strategies

    You can delete unneeded backtesting strategies.

4.3.2 Simulated Trading Strategy Visualization

The simulation dashboard displays all simulated trading strategies and their results. Its structure is the same as the backtesting dashboard. The page refreshes once per second to display real-time strategy results.

  • Simulation strategy list

  • Real-time trades and positions

  • Delete simulation strategies

    You can delete unneeded simulation strategies.

  • Stop simulated trading

    Simulation strategies that are no longer needed can be stopped. After stopping, the strategy will appear in the backtesting dashboard.

4.4 Strategy Permission Management

To support strategy isolation within quantitative teams, we implement user-level permission isolation for both backtesting and simulation strategies. Each user can only view the backtesting and simulation results they created, and strategy code is stored in user-defined GitLab repositories.

To use the cryptocurrency data environment for backtesting or simulation, contact the admin to create a user account with the required read permissions on the relevant databases and tables. Then, you can log in as the created account and import the dashboard files to view backtesting and simulation strategies. A permission granting script is shown below:

// Executed by admin
login("admin","DolphinDB123456")
// Create a new user
createUser("user1","123456",,false)
// Grant database and table permissions
grant(`user1, DB_READ, "dfs://CryptocurrencyDay")
grant(`user1, DB_INSERT,"dfs://CryptocurrencyStrategy") 
grant(`user1, TABLE_READ, "P1-node1:Cryptocurrency_depthTradeMerge")
// Function view permissions
grant("user1", VIEW_EXEC, "CryptocurrencySolution::utils::getSimulatedTradingEngine")
grant("user1", VIEW_EXEC, "CryptocurrencySolution::utils::getAllBacktestEngine")
// Dashboard permissions
dashboard_grant_functionviews(`user1, NULL, false) 
// Grant permissions to group member bk
grant(`bk, VIEW_EXEC, objs="CryptocurrencySolution::utils::getSimulatedTradingEngine") 
grant(`bk, DB_INSERT, "dfs://CryptocurrencyStrategy")   // Permission on database storing backtesting results
grant(`bk, VIEW_OWNER)   // Strategy storage requires function view permission
Note:

If you want to share strategy backtesting results, use the dashboard sharing feature. Shared dashboards are view-only.

For centralized management, the following functions are restricted to the admin user:

  • Unsubscribe all minute-level or snapshot stream tables.
    CryptocurrencySolution::utils::unsubscribeTableByAdmin(tbName)
  • Retrieve return comparisons for all simulated strategies.
    CryptocurrencySolution::utils::getReturnComparision(startDate, endDate)
  • Rebuild the database for strategy IDs and backtesting results. (Write permissions must be re-granted to the specific user after rebuilding.
    CryptocurrencySolution::utils::createdatabase()

5. Live Trading

In a quantitative trading system, live trading is the final stage where strategies are put into real execution. Therefore, we integrated the trading and account APIs of Binance and OKX, and developed a live trading module based on DolphinDB’s httpClient plugin.

This module supports order placement, order cancellation, querying open orders, and retrieving trade details. It is consistent with the DolphinDB’s backtesting and simulation framework, allowing strategy backtesting, simulation, and live trading to share the same codebase. This significantly reduces the learning cost. Below is a description of the supported live trading functions. For details, see the Appendix.

Module Function Description Notes
submitOrder Places an order. Returns order IDs (orderId and newClientOrderId). All order details and error information are stored in a stream table.
cancelOrder Cancels an order. No return value. All cancellation information is stored in the same stream table.
getOpenOrders Gets open orders. Returns information on unfilled orders.
getTradeDetails Get trade details Returns the latest 500 order records for a given asset.

To record live trading information, the module maintains a persistent stream table, named by default as <username> + Cryptocurrency + '_BinanceTradeDetails' or<username> + Cryptocurrency + '_OKXTradeDetails'. This table stores real orders, network errors, exchange API errors, and other related information, making it easier for you to query orders and troubleshoot issues while avoiding frequent API calls that may be rejected. The detailed table schema is provided in the Appendix.

5.1 Prerequisites

Before live trading, you must obtain the API key and HMAC key for your personal or test account to ensure trading security.

Binance

  1. Register and log in to a Binance account.
  2. Go to API Management in the account settings to obtain the API key and HMAC key.
Figure 12. Figure 5-1: Obtain keys on Binance

If you do not want to trade with a personal account for now, you can use the Binance Spot Test Network.

  1. Log in to your Binance account and sign in using a GitHub account.
    Figure 13. Figure 5-2: Obtain keys on the Binance test site
  2. Click Generate HMAC-SHA-256 Key to generate the required keys.
    Figure 14. Figure 5-3: Generate keys on the Binance test site

OKX

  1. Register and log in to an OKX account.
  2. Go to the API section in the account settings and create an API key.
    Figure 15. Figure 5-4: Obtain API keys on OKX
  3. Fill in the IP whitelist and passphrase, and select key permissions (read, withdraw, trade) as needed.
    Figure 16. Figure 5-5: Creating an API key on OKX
    Note:
    • If you do not have a static IP address, it is not recommended to set an IP whitelist, as IP changes may cause order failures.
    • API keys without a bound IP address will be automatically deleted after 14 days of inactivity.
  4. After creation, click View to obtain the API key and secret. Please store them securely.

5.2 Implementation Steps

This section describes how to apply a simulated trading strategy to live trading. The full script is provided in the Appendix. For strategy development, see Section 4.2.1.

  1. Compile the OKX signature plugin (Linux). For compilation details, see the plugin document.
    1. Download the DolphinDBPlugin project based on your server version, and place the source code (hex2Base64-main.zip) in the root directory.
    2. Run the following commands to generate .so and .txt files in the output folder.
    3. Place the output/hex2Base64-main folder in the server/plugins directory.
    cd ../DolphinDBPlugin/hex2Base64-main
    export CMAKE_INSTALL_PREFIX="$(pwd)/output/$PluginName"
    # Default ABI1
    export CXXFLAGS="-D_GLIBCXX_USE_CXX11_ABI=0" # Specify ABI0 via environment variable
    CXX=g++-8 CC=gcc-8 bash -x ./build.sh # Modify compiler version as needed
  2. Place the CryptocurrencySolution module in the modules directory under the home directory (getHomeDir()), move files in CryptocurrencyTrading.zip to the CryptocurrencySolution directory, and modify the OKXTradingModule.dos file based on the plugin path.
    try{loadPlugin("/.../server/plugins/hex2Base64-main/PluginHex2Base64.txt")}catch(ex){print(ex)}
    go
  3. Load the modules as follows:
    use CryptocurrencySolution::simulatedTrading
    use CryptocurrencySolution::CryptocurrencyTrading
    go
  4. Retrieve and store asset precision information.
    tb = CryptocurrencySolution::CryptocurrencyTrading::getAssetPrecision()
    dbName = "dfs://CryptocurrencyDay"
    tbName = "precision"
    colNames = `symbol`symbolSource`contractType`tickSize`stepSize
    colTypes = [SYMBOL,STRING,STRING,DOUBLE,DOUBLE]
    if(!existsDatabase(dbName)){
        db = database(dbName,RANGE,2010.01M+(0..20)*60)
    }else{ db=database(dbName)}
    if(!existsTable(dbName,tbName)){
        createDimensionTable(db,table(1:0,colNames,colTypes),tbName) 
    }
    loadTable(dbName,tbName).append!(tb)
  5. Configure account credentials in userConfig["context"].
    p = dict(STRING,ANY)
    // Key information – fill in with actual keys
    keyInfo = dict(STRING,ANY)
    keyInfo["apiKey"] = ""
    keyInfo["secretKey"] = ""
     // OKX requires an additional passphrase
    // keyInfo["passphrase"] = "123456"
    p["keyInfo"] = keyInfo
    userConfig["context"] = p
  6. Example: placing orders in a minute-level strategy. Orders are placed in the onBar callback. In the submitOrder function, isSim indicates whether the order is placed in live trading or simulation mode, and it must match the applied API key.
    def onBar(mutable context, msg, indicator){
    	endTime = timestamp()
        for(istock in msg.keys()){
            source = msg[istock]["symbolSource"]
    		buyPrice=msg[istock]["close"]+10
    		sellPrice =msg[istock]["close"]-10
            if(context["number"]< 5){                
                submitOrder(context["keyInfo"],(istock,source,context["tradeTime"],5,buyPrice,0.0,0.0,0.01,1,0,1,endTime),"testOrders_buy",
                orderType=0, accountType ="futures",exchange = "Binance",paramDict=NULL,strategyName=context["strategyName"],isSim=true, proxy=NULL)
                context["number"] += 1
            }else{
                submitOrder(context["keyInfo"],(istock,source,context["tradeTime"],5,sellPrice,0.0,0.0,0.01,3,0,1,endTime),"testOrders_sell",
                orderType = 0, accountType = "futures",exchange = "Binance", paramDict = NULL,strategyName = context["strategyName"],isSim = true, proxy = NULL)
                con}
    def onOrder(mutable context,orders){}
    def onTrade(mutable context,trades){text["number"] -= 1
            }
       } 
    }
    def onOrder(mutable context,orders){}
    def onTrade(mutable context,trades){}
    Lines 8 and 12 show that the live order placement function is largely consistent with that of the backtesting plugin. In Backtest::submitOrder, the first engine parameter must be replaced with the account credential information keyInfo. Cryptocurrency exchanges support more order parameters. Extensible parameters are passed via paramDict. For details, see New Order | Binance Open Platform and POST / Place Order – OKX API Documentation.
  7. Submit the strategy using submitCryptoSimulatedTrading. Real-time market data will enter the engine and trigger the callbacks to execute live trading.
    engine,straname_ = CryptocurrencySolution::simulatedTrading::submitCryptoSimulatedTrading(strategyName, 
    eventCallbacks, userConfig, overwrite = true)
Note:

If you need to store live trading strategy code, use the CryptocurrencySolution::manageScripts module and add keyInfo in the unified configuration function under CryptocurrencySolution::setting, or pass keyInfo via a custom userConfig. Other usage is the same as backtesting and simulation. For details, see Chapter 4.

Live trading details can be obtained from the stream table (e.g., admin_testSubmitRealOrder_BinanceTradeDetails, shown in Figure 5-6). Successful order placements and cancellations are recorded in this table. If an order fails due to network issues or API errors, the error information is also stored, making it easy to review order status or diagnose failures.

Figure 17. Figure 5-6: Live Trading Stream Table

You can retrieve open orders and trade details using the following functions:

openOrders_s = getOpenOrders(keyInfo,"BTCUSDT_spot")
openOrders_f = getOpenOrders(keyInfo,"BTCUSDT_futures", accountType="futures")
tradeDetails_s = getTradeDetails(keyInfo,"BTCUSDT_spot", accountType="spot")
tradeDetails_f = getTradeDetails(keyInfo,"BTCUSDT_futures", accountType="futures")

6. Real-Time Risk Control

Risk management is critical for investors, especially in the highly volatile cryptocurrency market. Based on DolphinDB, we implement a real-time portfolio risk management solution that integrates exchange account data. By periodically polling and calculating metrics such as portfolio net value, unrealized P&L, total value, and leverage ratio, the solution helps investors better understand and manage risk. The solution supports both Binance and OKX. The main module functions are as follows:

  • Exchange data retrieval: Periodically retrieves real-time data such as wallet balances, futures positions, and market quotes from exchanges, and writes them into DolphinDB stream tables for subsequent computation.
  • Real-time risk computation: Periodically computes risk metrics for spot and futures accounts using DolphinDB, and then combines both to derive overall portfolio risk metrics.
  • Real-time data persistence: Persists account information and risk metrics to the DolphinDB database in real time for subsequent simulation and analysis.

6.1 Risk Metric Computation

This solution first computes risk metrics for spot and futures accounts separately, and then combines them to obtain overall portfolio risk metrics. The data involved includes spot and futures asset information, spot and futures price data, and futures position data. The formulas and variable names follow Binance’s definitions.

Spot account risk metrics

  • Free value (freeValue): The sum of each asset’s free quantity multiplied by its corresponding market price (midPrice).
  • Locked value (lockedValue): The sum of each asset’s locked quantity multiplied by its corresponding market price (midPrice).
  • Spot total value (spotTotalValue): The sum of free value and locked value.

Futures account risk metrics

  • Live unrealized P&L (liveUnrealizedProfit): The sum of potential profits across all futures positions based on the difference between the current market price and the break-even price.
  • Futures net value (futuresNetValue): The sum of the total futures account value and unrealized profit.
  • Total futures position value (principleAmt): The sum of each futures position multiplied by its corresponding market price (midPrice).
  • Futures leverage ratio (futuresLeverageRatio): The total futures position value divided by the total futures account balance.

In the leverage computation, the discountRatio parameter is introduced as a conservative adjustment factor to prevent excessive leverage. In this solution, the discount ratio is set to 0.95.

Overall risk metrics

  • Total value (totalValue): The sum of spot total value and futures net value.
  • Total leverage ratio (totalLeverageRatio): The ratio of (spot free value plus the total futures position value) to the total account value.

6.2 Risk Model Construction

This section describes how to build the real-time risk model and configure real-time alerts. Binance is used as an example. The full script is provided in the Appendix.

Create data tables

In DolphinDB, create in-memory tables to store exchange data. Use latestKeyedTable to create key-value in-memory tables for account information (asset and position data) and price data, and share them across all sessions on the current node using share. Key-value in-memory tables make it easy to store the latest data and can be directly used for risk metric computation. The following example shows how to create a spot position table:

// Spot position
colNames = `asset`free`locked`updateTime
colTypes = [SYMBOL, DOUBLE, DOUBLE, TIMESTAMP]
share latestKeyedTable(`asset, `updateTime, 1000:0, colNames, colTypes) as spotBalanceKT
go

For risk metrics, use streamTable to create stream tables, and enable enableTableShareAndPersistence to allow both sharing and persistence.

// Risk metrics
colNames = `freeValue`lockedValue`spotTotalValue`baseCurrency`free`locked`balance`crossWalletBalance`crossUnPnl`availableBalance`maxWithdrawAmount`updateTime`principleAmt`liveUnrealizedProfit`unrealizedProfit`futuresNetValue`futuresLeverageRatio`totalValue`totalLeverageRatio
colTypes = [DOUBLE,DOUBLE,DOUBLE,STRING,DOUBLE,DOUBLE,
            DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,TIMESTAMP,DOUBLE,
            DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE]
enableTableShareAndPersistence(
    table=streamTable(10000:0, colNames, colTypes), 
    tableName="portfolioRiskIndicatorST",
    cacheSize=100000,
    preCache=10000
)
go

Retrieve exchange data

Binance provides RESTful APIs for account information (assets and positions) and trading data. Using DolphinDB’s httpClient plugin, these data can be retrieved and written synchronously into DolphinDB tables.

  • Account information: Retrieved via background jobs that poll every minute and compute metrics. An example of fetching spot account information is shown below:
    def getBinanceSpotAccount(keyInfo,proxy=NULL){
        apiKey = keyInfo["apiKey"]
        secretKey = keyInfo["secretKey"]
        baseUrl = 'https://testnet.binance.vision/api/v3/account'
        config = dict(STRING, ANY)
        // ...
        headers = dict(STRING,STRING)
        headers["X-MBX-APIKEY"] = apiKey
        headers["Content-type"] = "application/x-www-form-urlencoded"
        param = dict(STRING,ANY)
        param["recvWindow"] = 5000
        target = objByName("spotBalanceKT")
        do{
            try{
                timestamp = getBinanceServerTime("spot")
                param["timestamp"] = timestamp 
                bodyString = signatureByHMAC(param,secretKey)
                url = baseUrl + "?" + bodyString 
                response = httpClient::httpGet(url,,10000,headers,config)
                if(response.responseCode!=200){
                    print("response error: " + response.text)
                }
                res = parseExpr(response.text).eval()
                updateTime = res.updateTime       
                tb = each(def(mutable d){
                    d["free"] = double(d["free"]);
                    d["locked"] = double(d["locked"]);
                    return d
                }, res.balances)      
                target.tableInsert(
                    tb.join!(take(updateTime.timestamp()+8*2600*1000,size(tb)) as updateTime)
                )
                success = true
            }catch(ex){
                failCnt += 1
                print("failCnt " + failCnt + ": " + ex)    
                if(failCnt == 3){
                    print("Failed to retrieve spot balance data")
                }
            }
        }while((not success) and failCnt < 3)   
        return 
    }

    You must configure account credentials via keyInfo. Account data is fetched using httpClient::httpGet, processed, and written into the spotBalanceKT table. All requests use a retry-on-failure mechanism.

  • Price data: Real-time cryptocurrency prices are obtained from market data stream tables. After computing midPrice, the data is written into the corresponding in-memory tables. For real-time data ingestion, see Chapter 2.
    spotPrice = select symbol, (high+low)/2 as midPrice, eventTime as updateTime 
        from Cryptocurrency_minKLineST
        where symbolSource = "Binance-Spot" 
        context by symbol, symbolSource
        order by eventTime
        limit -1
    objByName("spotPriceKT").tableInsert(spotPrice)

Compute risk metrics

After retrieving the required data, risk metrics are computed and written into the corresponding stream tables. A partial code example is shown below:

def computePortfolioRiskIndicator(){
    // Query spot market data and compute free and locked values
    spotmd = select asset, free, locked,
              sum(free*midPrice) as freeValue, 
              sum(locked*midPrice) as lockedValue
              from spotBalanceKT 
              left join spotPriceKT
              on spotBalanceKT.asset = left(spotPriceKT.symbol, 
                 strlen(spotPriceKT.symbol)-4)
              and spotPriceKT.symbol like '%USDT'
    // ...
    // Compute portfolio risk metrics
    portfolioRiskIndicator = select *,
        spotTotalValue + futuresNetValue as totalValue, 
        (freeValue + principleAmt) / (spotTotalValue + futuresNetValue)
        as totalLeverageRatio
        from spotRiskIndicator
        cross join futuresRiskIndicator
    // Write results to the stream table
    objByName("portfolioRiskIndicatorST").append!(portfolioRiskIndicator)
}

Submit background jobs

Finally, background jobs are submitted using submitJob, with the interval parameter set to 60000. Once submitted, the function polls account data and computes risk metrics every minute. In the code, spotKeyInfo and futuresKeyInfo correspond to your spot and futures account credentials. Key acquisition is described in Chapter 5.

def run_all(spotKeyInfo,futuresKeyInfo,interval=60000){    
    do{
        getBinanceSpotAccount(spotKeyInfo)
        getBinanceFutureBalance(futuresKeyInfo)
        getBinanceFuturePosition(futuresKeyInfo)
        getBinancePrice()
        computePortfolioRiskIndicator()
        sleep(interval)
    }while(true)
}
// ...
submitJob(
    "realTimeRiskTest",
    "test for cryptocurrency real time risk",
    run_all,
    spotKeyInfo,
    futuresKeyInfo
)

The data in the risk metric stream table (portfolioRiskIndicatorST) is shown in the figure below.

6.3 Risk Alert Notifications

This section explains how to implement real-time risk alerts via WeCom using the httpClient plugin, so you can be notified promptly. The implementation is similar to the real-time monitoring and alerting described in Chapter 2. The main alert function is shown below:

def sendWeChatMsg(msg, webhook){
    // JSON data
    sendcontent = dict(STRING, ANY)
    sendcontent["msgtype"] = "text"
    text = dict(STRING, STRING)
    text[`content] = msg 
    sendcontent["text"] = text
    header = dict(STRING,STRING)
    header["Content-Type"] = "application/json"
    sendNum = 0   // for early warning, send once
    for(i in 1..5){
        if(sendNum != 1){
            try {
                jsontext = toStdJson(sendcontent)
                response = httpClient::httpPost(webhook, jsontext, 10000, header)
                sendNum = 1
            }catch (ex) {
                info = "send message " + webhook + " failed " + ex[1];
                writeLogLevel(WARNING, info);
            }   
        }
    }
}
  1. Select the chat group to receive alerts, create a notification bot, and obtain the webhook URL.
  2. Check the risk metric stream table. For example, if freeValue exceeds 12,500, send an alert.
    def checkRiskValueFunc(){
        freeV = select * from objByName("portfolioRiskIndicatorST") 
                order by updateTime desc limit 1
        freeVal = freeV[`freeValue][0]
        if(freeVal > 12500){
            msg = "Risk indicator freeValue is: " + freeVal + 
                  ". Please take action to control account risk."
            sendWeChatMsg(msg)
        }
    }
  3. Schedule a task to check risk metrics every 10 minutes:
    scheduleJob("checkRiskValue","check risk info",checkRiskValueFunc,09:00m+(0..18)*30,2025.09.12,2035.12.31,"D")
Note:

Using DolphinDB’s httpClient plugin, alerts can also be sent via email or other channels. For details, refer to the official documentation on integrating external messaging with the httpClient plugin.

7. Demonstration for More Strategies

Through three strategy examples (funding rate arbitrage, active market making, and dynamic grid trading), this section demonstrates how to write event callback functions using built-in DolphinDB scripts. Based on the existing cryptocurrency backtesting and simulation solution, it explains in detail how to use the DolphinDB’s cryptocurrency backtesting engine and rapidly implement strategies.

First, install and load the Backtest and httpClient plugins:

login("admin", "123456")
listRemotePlugins()          // View plugin repository
installPlugin("Backtest")    // Install plugin
installPlugin("httpClient")
loadPlugin("Backtest")       // Load plugin
loadPlugin("httpClient")

Based on the data environment prepared in Chapter 2, call CryptocurrencySolution::manageScripts. If strategy code management is required, declare the strategy name using module:

use CryptocurrencySolution::manageScripts
go 
module cryptocurrencyStrategy   // Not required for backtest-only or simulation-only

7.1 Funding Rate Arbitrage Strategy

7.1.1 Strategy Overview

This section takes funding rate arbitrage as an example to show how to implement the strategy in the DolphinDB’s cryptocurrenct backtesting engine using minute-level market data. The arbitrage strategy profits from price differences across markets or assets. By buying and selling related assets at the same time, investors can lock in profits while hedging against price fluctuations.

Funding rate arbitrage is unique to the cryptocurrency market and originates from perpetual contract derivatives that are specific to cryptocurrency. The core idea is to go long or short perpetual contracts based on funding rate changes, while hedging with spot positions so that total portfolio value is insensitive to price movements (ignoring slippage and fees). The strategy logic is as follows:

  • Short the contract: When the funding rate is greater than 0.03%, the market is bullish and longs pay funding fees to shorts. In this case, short the perpetual contract and buy an equivalent amount of the asset in the spot market. When the funding rate turns from positive to negative and the market trend changes, promptly close the contract position and sell the spot holdings.
  • Long the contract: When the funding rate is less than −0.03%, the market is bearish and shorts pay funding fees to longs. In this case, go long on the perpetual contract and sell an equivalent amount of the asset in the spot market. When the funding rate turns from negative to positive and the market trend changes, promptly close the contract position and buy back the spot asset.
Note:

The following section implements only the short-side cryptocurrency perpetual contract strategy.

7.1.2 Strategy Implementation

The strategy initialization function initialize is triggered only once after the engine is created, allowing you to set strategy parameters. The parameter context represents the logical context. Line 4 calls the setUniverse method to define the asset universe and filter tradable assets. context["fundingRate"] on line 5 is used to retrieve funding rate data for the backtest period, while context["lastlastFunding"] on line 6 stores the previous funding rate for use in the strategy to determine exit conditions.

def initialize(mutable context){
    // Initialization
    print("initialize")
    // Backtest::setUniverse(context["engine"], context.Universe)
    context["fundingRate"] = Backtest::getConfig(context["engine"])[`fundingRate]
    context["lastlastFunding"] = dict(SYMBOL,ANY)  // Stores the previous funding rate, used to determine exit conditions
}

In the daily pre-market callback function beforeTrading, the trading date can be obtained via context["tradeDate"], as shown on line 4. In this example, the funding rate table for the current trading day is converted into the d dictionary and passed into the strategy through context. A nested dictionary structure is used, with the symbol and the settlementTime as keys. The funding rate data is obtained in the strategy initialization function.

def beforeTrading(mutable context){
    // Daily pre-market callback function
    // The current trading date can be obtained via context["tradeDate"]
    print("beforeTrading: " + context["tradeDate"])
    // Retrieve the funding rate table for the current day and store it as a dictionary keyed by symbol
    fundingRate = context["fundingRate"]
    d = dict(STRING,ANY)
    for (i in distinct(fundingRate.symbol)){
        temp = select * from fundingRate where symbol = i and date(settlementTime) = context["tradeDate"]
        replaceColumn!(temp, `settlementTime, datetime(exec settlementTime from temp))
        d[i] = dict(temp.settlementTime, temp.lastFundingRate, true)
    }
    context["dailyLastFundingPrice"] = d	
}

In the OHLC data callback function onBar, the msg parameter represents the latest minute-level market data passed in by the backtest engine, and the parameter indicator represents the subscribed provided by the engine. The code below demonstrates the logic for determining entry and exit conditions.

For each asset, line 11 selects the current funding rate, i.e., the value from the previous funding rate table. The strategy then checks whether the funding rate and the current position satisfy the entry or exit conditions, and places orders accordingly.
def onBar(mutable context, msg, indicator = NULL){
    // ...
    dailyFundingRate = context["dailyLastFundingPrice"]
    // Iterate over multiple assets
    for(i in msg.keys()){
        istock = split(i,"_")[0]
        istockFut = istock + "_futures"
        istockSpo = istock + "_spot"
        source = msg[i]["symbolSource"]
        closePrice = msg[i]["close"]
        // Current asset funding rate; select the corresponding funding rate based on the current time window
        if(second(context["tradeTime"]) >= 16:00:00){fundingRateTime = temporalAdd(datetime(context["tradeDate"]), 16, "h")}
        if(second(context["tradeTime"]) >= 08:00:00 and second(context["tradeTime"]) < 16:00:00){fundingRateTime = temporalAdd(datetime(context["tradeDate"]), 8, "h")}
        if(second(context["tradeTime"]) < 08:00:00){fundingRateTime = datetime(context["tradeDate"])}
        lastFundingPrice = dailyFundingRate[istockFut][fundingRateTime]
        // print(fundingRateTime)
        // Position status: spot and futures
        spotPos = Backtest::getPosition(context["engine"], istockSpo, "spot")
        futurePos = Backtest::getPosition(context["engine"], istockFut, "futures")
        // ...
    }
}

Entry conditions are determined based on the funding rate and current position size, and orders are placed via the Backtest::submitOrder method.

// When the funding rate is greater than 0.03%, short the perpetual contract and buy an equivalent amount of spot
// Here the maximum order quantity is set to 0.1. To allow short-to-open, set 0.1 to 0.
if(spotPos.longPosition[0] <= 0.1 and futurePos.shortPosition[0] < 0.1 and lastFundingPrice > 0.0003){
    // Spot
    if(i == istockSpo){                    
        Backtest::submitOrder(context["engine"], (i, source, context["tradeTime"], 5, closePrice,
                  lowerLimit, upperLimit, qty, 1, slippage, 1, expireTime ), "buyopen_spot", 0, "spot")        
    }
    // Contact
    if(i == istockFut){        
        Backtest::submitOrder(context["engine"], (i, source, context["tradeTime"], 5, closePrice,
                  lowerLimit, upperLimit, qty, 2, slippage, 1, expireTime ), "sellopen_contract", 0, "futures")        
    }
    context["lastlastFunding"][istockFut] = lastFundingPrice
}

Strategy execution:

strategyName = "cryptocurrencyStrategy"  // The code name must match the module name when storing
eventCallbacks = {
    "initialize": initialize,
    "beforeTrading": beforeTrading,
    "onBar": onBar
}
strategyType = 0  // Backtest — default is 10 days; start and end dates can be customized via userConfig
engine, straname_ = CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(strategyName, eventCallbacks, strategyType)

The userConfig parameter is set via the getUnifiedConfig() function in the CryptocurrencySolution::utils module by default. You can customize it as follows:

userConfig = dict(STRING,ANY)
userConfig["startDate"] = 2025.12.01
userConfig["endDate"] = 2025.12.05
engine,straname_= CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(strategyName, eventCallbacks,strategyType,userConfig)

The full strategy script is provided in the Appendix.

7.2 Dynamic Grid Trading Strategy

7.2.1 Strategy Overview

This section illustrates how to implement a grid trading strategy based on minute-level market data in the cryptocurrency backtesting engine. Traditional grid trading strategies revolve around a reference price. When the price drops, buy at the trigger points; when the price rises, sell at the trigger points. This method is only suitable for small-range fluctuations. It struggles to capture upward trends during sudden large swings and cannot effectively avoid sharp declines. By reducing purchases during a crash and buying again once the market stabilizes, or by pausing sales during an uptrend and resuming once the trend slows, overall returns of the grid trading strategy can be improved.

The dynamic grid strategy define a grid spacing alpha and a rebound spacing beta. Trades are executed when the asset price first triggers a grid line and then reaches the rebound price. The logic is as follows:

  • Construct grid strategy parameters: set the initial price as the first trade price of the strategy, grid spacing alpha as 2%, rebound spacing beta as 1%, and per-grid trade amount M as 100,000.
  • Opening logic: when the asset price hits the n-th grid line below the reference price, wait for the latest price to rebound by beta from the lowest price, then buy a quantity of n * M / latest price.
  • Closing logic: when the instrument price hits the n-th grid line above the reference price, wait for the latest price to fall by beta from the highest price, then sell a quantity of n * M / latest price.
  • The reference price is updated to the latest buy or sell price according to the opening or closing signal.

7.2.2 Strategy Implementation

The strategy initialization function initialize is triggered only once after creating the engine. You can set strategy parameters in contextDict. The parameters lowPrice, highPrice, baseBuyPrice, and baseSellPrice are used to update the current reference prices.

def initialize(mutable contextDict){
    print("initialize")
    // Initial price
    contextDict["initPrice"] = dict(SYMBOL, ANY)
    // Grid spacing (percentage)
    contextDict["alpha"] = 0.01
    // Rebound spacing (percentage)
    contextDict["beta"] = 0.005
    // Trade amount per grid
    contextDict["M"] = 100000
    contextDict["baseBuyPrice"] = dict(SYMBOL, ANY)
    contextDict["baseSellPrice"] = dict(SYMBOL, ANY)
    contextDict["lowPrice"] = dict(SYMBOL, ANY)
    contextDict["highPrice"] = dict(SYMBOL, ANY)
    contextDict["N"] = dict(SYMBOL, ANY)
    // Fee rate
    contextDict["feeRatio"] = 0.00015
    Backtest::setUniverse(contextDict["engine"], contextDict.Universe)
}

Dynamic grid strategies need to update grid lines based on OHLC prices. A user-defined function is implemented as follows:

def updateBaseBuyPrice(istock,lastPrice,basePrice,mutable baseBuyPrice,mutable baseSellPrice,mutable N,mutable highPrice,mutable lowPrice,alpha,n,mode=0){
	// Update grid lines and highest/lowest price based on the latest price and base price
	baseBuyPrice[istock] = basePrice*(1-alpha)
	baseSellPrice[istock] = basePrice*(1+alpha)
	N[istock] = n
	if(mode==0){
		// Initialization for buy/sell, etc.
		lowPrice[istock]=0.
		highPrice[istock]=10000.
	}else if(mode==1){
		// Price drop, update lower grid line
		lowPrice[istock]=lastPrice
		highPrice[istock]=10000.

	}else if(mode==2){
		// Price rise, update upper grid line
		lowPrice[istock]=0.
		highPrice[istock]=lastPrice
	}
}

The OHLC callback function onBar receives msg, the latest minute-level market data from the backtesting engine. The strategy updates grid lines based on the latest price and base price in contextDict. Trades are executed when the price hits the upper or lower grid lines.

def onBar(mutable contextDict, msg,indicator){
	//...
    initPrice = contextDict["initPrice"]
    baseBuyPrice = contextDict["baseBuyPrice"]
	baseSellPrice = contextDict["baseSellPrice"]
    highPrice = contextDict["highPrice"]
    lowPrice = contextDict["lowPrice"]
         
	for(isymbol in msg.keys()){
		istock = isymbol
		lastPrice = msg[isymbol]["close"]
        source = msg[isymbol]["symbolSource"]
		// Set initial price
		if(not istock in initPrice.keys()){
			initPrice[istock]=lastPrice
			updateBaseBuyPrice(istock,lastPrice,lastPrice, baseBuyPrice,
			baseSellPrice, N, highPrice, lowPrice,alpha,1,0)
		}
		init_price=initPrice[istock]
		if(lastPrice<=baseBuyPrice[istock]){
			// Price drop, update lower grid line
			n=floor(log(lastPrice\init_price)\log(1-alpha))+1	
			if(n>N[istock]){
				newBasePrice=init_price*pow((1-alpha),n)
				updateBaseBuyPrice(istock,lastPrice,newBasePrice,baseBuyPrice, baseSellPrice, 
				N, highPrice, lowPrice,alpha,n,1)
			}	
		}else if(lastPrice>baseSellPrice[istock]){
			// Price rise, update upper grid line
            //...
		}
		if(lowPrice[istock]>0. and lastPrice>lowPrice[istock]*(1+beta)){
			// Buy
			qty=decimal128(0.001,8)
			orderId = Backtest::submitOrder(contextDict["engine"], (istock, source, contextDict["tradeTime"],5, lastPrice,
			upperLimit, lowerLimit, qty,1, slippage, 1,expireTime ),"buy",0, "futures")	
			initPrice[istock] = lastPrice
			updateBaseBuyPrice(istock,lastPrice,lastPrice, baseBuyPrice, 
			baseSellPrice , N, highPrice, lowPrice,alpha,1,0)
		}else if(highPrice[istock]<10000. and lastPrice<highPrice[istock]*(1-beta)){
			// Sell ... (full code in attached script)
		}
		// Update lowest/highest price in real-time
		if(lowPrice[istock]>0){
			lowPrice[istock]=min([lowPrice[istock],lastPrice])
		}
		if(highPrice[istock]<10000.){
			highPrice[istock]=max([highPrice[istock],lastPrice])
		}
	}
    // Store updated prices
    contextDict["initPrice"]=initPrice
	contextDict["baseBuyPrice"]=baseBuyPrice
	contextDict["highPrice"]=highPrice
    //...
}

Strategy execution:

strategyName = "cryptocurrencyStrategy"  // Must match module name for code storage
eventCallbacks = {
	"initialize":initialize,
	"beforeTrading": beforeTrading,
	"onBar":onBar
}
strategyType = 0  // # Backtest – default 10 days; can customize start/end via userConfig
engine,straname_= CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(strategyName, eventCallbacks,strategyType)

The full strategy script is provided in the Appendix.

7.3 Active Market Making Strategy

7.3.1 Strategy Overview

Market-making strategies profit from the bid-ask spread and are a major approach in mid- to high-frequency trading. This section implements a classic Avellaneda-Stoikov (AS) market-making strategy on the BTC/USDT perpetual contract, demonstrating how to use DolphinDB to implement cryptocurrency strategies at snapshot frequency. The Avellaneda-Stoikov market-making model was proposed by Marco Avellaneda and Sasha Stoikov in their 2008 paper High-Frequency Trading in a Limit Order Book. The model studies how market makers set optimal quotes under inventory risk. Based on market price dynamics, the strategy considers price volatility, position, and risk preference. The AS model adjusts bid and ask quotes around the market mid-price to minimize inventory risk while maximizing profit.

The AS model derivation involves two steps. First, the market maker calculates the indifference price based on current inventory and risk preference. Second, by combining the quote’s distance from the market mid-price with market conditions and risk tolerance, the execution probability is estimated to determine the optimal quote. This approach allows market makers to provide liquidity while minimizing inventory risk and maximizing profit.

Indifference price formula:

  • : market mid-price
  • : market maker’s current inventory
  • : market maker’s risk aversion coefficient
  • : market price volatility
  • : normalized end time
  • : current time

This formula indicates that the larger the inventory or the higher the price volatility, the lower the market maker will set the mid-quote, reducing holding risk.

Spread formula:

  • : symmetric bid-ask spread
  • : same as the previous formula
  • : market liquidity

This formula shows that a higher risk aversion, higher market volatility, or better market liquidity leads to a larger optimal spread for the market maker, compensating for risk.

Quote formula:

7.3.2 Strategy Implementation

The strategy initialization function initialize is triggered once after the engine is created, allowing you to set strategy parameters. The context parameter represents the logical context, while userParam holds custom strategy parameters. In the AS strategy, we directly input the preset values of the previously mentioned for use in quote calculation.

def initialize(mutable context){
	print("initialize")
	Backtest::setUniverse(context["engine"], context.Universe)
	// Onsnapshot Parameters
    context["sigma"] = 0.025                                    // market volatility
    context["gamma"] = 0.1                                      // inventory risk aversion parameter
    context["k"] = 1.5                                          // order book liquidity parameter
    context["amount"] = 0.001                                    // order amount
    //...
	context["lastprice"] = NULL
	// 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])
}

In the snapshot callback function onSnapshot, the strategy logic is mainly divided into seven steps:

  1. Use orderInterval as the order frequency—for example, check, calculate, and place orders every second.
  2. Cancel existing orders before placing new ones.
  3. Calculate the current market mid-price from the best bid and ask.
  4. Retrieve the current long and short positions.
  5. Compute quotes based on the AS model. Note the decimal precision of the order price—for BTC/USDT, the minimum price increment is 0.1.
  6. Submit buy and sell orders.
  7. Manage inventory risk and close positions in a timely manner.
def onSnapshot(mutable context, msg, indicator){
	istock = context["istock"][0]
	if(context["lastprice"]<0){
		context["lastprice"] = msg[istock].lastPrice
	}
	// 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
	pos = Backtest::getPosition(context["engine"],istock,"futures")
	longPos = pos['longPosition']
	shortPos = pos['shortPosition']
	netPos = nullFill(pos['longPosition']-pos['longPosition'],0)
	if(count(netPos) == 0){
	    netPos = 0
	}
    // 5. Calculate order price
	gamma = context["gamma"]
	sigma = context["sigma"]
	k = context["k"]
	amount = context["amount"]
	endTime = timestamp(context["tradeDate"])
	timeToEnd = (endTime-t)\86400000
	reservePrice = midPrice - netPos * gamma * square(sigma) * timeToEnd
	spread = gamma * square(sigma) * timeToEnd + (2 / gamma) * log(1 + (gamma / k))
	buyPrice = reservePrice-0.5*spread
	sellPrice = reservePrice+0.5*spread
	buyPrice = round(buyPrice, 1)
	sellPrice = round(sellPrice, 1)
	// 6. Set order direction
	sellDirection = 3
	buyDirection = 4
	if(count(longPos) == 0 || longPos == 0){
	buyDirection = 1
	}
	if(count(shortPos) == 0 || shortPos == 0){
	sellDirection = 2
	}
	// 7. Submit buy orders
	Backtest::submitOrder(
	context["engine"], 
	(istock, 'Binance', context["tradeTime"], 5, buyPrice,0, 1000000, amount, buyDirection, 0, 0, endTime),
	"buy", 0, "futures")
	// 8. Submit sell orders
	Backtest::submitOrder(
	context["engine"], 
	 (istock, 'Binance', context["tradeTime"], 5, sellPrice, 0, 1000000, amount, sellDirection, 0, 0, endTime),
	"sell", 0, "futures")    
}

In the strategy termination callback function finalize, you need to implement order cancellation and post-trading log recording. An example is as follows:

def finalize(mutable context){
	tradeDate = context["tradeDate"]
	print("afterTrading: "+tradeDate)
	// Cancel all open orders
	Backtest::cancelOrder(context["engine"],context["istock"][0])
	// AfterTrading Log
	tb = context["log"]
	context["log"] = tb.append!(table(context["tradeDate"] as tradeDate,now() as time,"afterTrading" as info))
}

Strategy execution

strategyName = "cryptocurrencyStrategy"  //When saving the code, the name must match the module name
eventCallbacks = {
	"initialize":initialize,
	"beforeTrading": beforeTrading,
	"onSnapshot":onSnapshot,
    "finalize":finalize
}
strategyType = 0  // Backtest — default is 10 days, but you can customize start and end time via userConfig
engine,straname_= CryptocurrencySolution::manageScripts::runCryptoAndUploadToGit(strategyName, eventCallbacks,strategyType)

The full strategy script is provided in the Appendix.

8. Writing Strategies with Swordfish

The high-frequency strategy backtesting engine in DolphinDB is implemented in C++. To improve backtesting performance, event callback functions can be implemented in C++ using Swordfish, enabling tight integration between C++ quantitative programs and DolphinDB. The architecture for calling backtesting plugin or DolphinDB function library from C++ via Swordfish is shown in Figure 8‑1.

Figure 18. Figure 8-1: Architecture of Building Strategy Backtesting with Swordfish

8.1 C++ Strategy Implementation

Taking a grid strategy as an example, this section explains how to use the backtesting plugin in C++ Swordfish.

Load the plugin:

Swordfish provides the DolphinDBLib::execute method to run DolphinDB scripts. It returns a ConstantSP type, which is a pointer to the Constant class and can be cast to the actual type. This method can call the loadPlugin function to load the backtesting plugin, returning an array of function pointers for all plugin methods. The type is VectorSP, so the plugin can be loaded as follows:

VectorSP functions = DolphinDBLib::execute("loadPlugin('backtest')");

To simplify the use of the backtesting plugin in C++, a demo in the Appendix wraps the DolphinDBLib::execute method into a BacktestWrapper class. Example:

auto engine = BacktestWrapper::createBacktester(strategyName_, userConfig_, eventCallbacks, new Bool(false), securityReference);

Write the strategy:

When creating the backtesting engine, multiple strategy callback functions need to be defined, and a dictionary mapping string keys to function pointers must be passed to the eventCallbacks parameter of createBacktester. The SwordfishFunctionDef class is provided to convert C++ function pointers to Swordfish internal function pointers. Example of defining the initialize callback function for the backtesting engine:

ConstantSP initialize(vector<ConstantSP> &arguments) {
    cout << "initialize" << endl;
    DictionarySP contextDict = arguments[0];
    return new Void();
}

int main() {
    auto initializeFunc = new SwordfishFunctionDef(initialize, "initialize", 1, 1, false);
}

Constructor parameters of SwordfishFunctionDef:

  • func: const std::function<ConstantSP(ddb::vector<ConstantSP>&)>, a C++ function pointer type. It requires the input parameter to be of type ddb::vector<ConstantSP>& (a dynamic-length argument array) and returns ConstantSP.
  • name: const std::string, the custom Swordfish function name.
  • minParamNum: int, minimum number of parameters.
  • maxParamNum: int, maximum number of parameters.
  • hasReturnValue: bool, whether the function has a return value.
In the grid strategy, the main strategy logic is defined in the onBar callback function. The arguments array contains the logical context contextDict at index 0 and market data msg at index 1:
ConstantSP onBar(vector<ConstantSP> &arguments) {
    cout << "onBar" << endl;
    DictionarySP contextDict = arguments[0];
    DictionarySP msg = arguments[1];
    cout << "contextDict: " << endl << contextDict->getString() << endl;
    cout << "msg: " << endl << msg->getString() << endl;
    // ...
}

Retrive data and run backtesting:

After creating the backtesting engine, connect to DolphinDB remotely to read market data and write it to the engine. The CryptoBacktest class is provided to implement the full backtesting workflow, offering the same runCryptoBacktest method as before. Example:

SmartPointer<DDBConnection> conn = new DDBConnection("192.198.1.32", 8742, "admin", "123456");
CryptoBacktest backtest(conn);
// ...
SmartPointer<BacktestWrapper> engine = backtest.runCryptoBacktest(strategyName, userConfig, eventCallbacks);
// Get backtesting results
cout << engine->getTradeDetails()->getString() << endl;

The full script is provided in SwordfishBacktest.zip in the Appendix, where the callback functions for the grid strategy are defined in main.cpp.

8.2 Python Swordfish Strategy Implementation

Python Swordfish is a high-performance embedded computation engine and database for Python users, introduced by DolphinDB. Unlike DolphinDB scripts, Python Swordfish embeds the database directly into the Python process, avoiding network transmission and serialization overhead, achieving maximum performance. Python Swordfish is deeply integrated with the Python ecosystem, making it suitable for quant teams that rely on Python tools.

Taking a grid strategy as an example, this section demonstrates the basic usage of the backtesting plugin in Python Swordfish.

Import Python Swordfish libraries

import swordfish as sf
import swordfish.plugins.backtest as backtest
import swordfish.function as F

Strategy template

The Swordfish libraries provides the StrategyTemplate class as a strategy template. You can create strategies by inheriting from this class. When placing orders, the self.submit_order method of this template class can be used. For details, refer to the Python Swordfish documentation or the Appendix.

import swordfish.plugins.backtest as backtest

class MyStrategy(backtest.StrategyTemplate):
    def initialize(self, context):
        pass
    def on_bar(self, context, msg, indicator):
        pass

Write strategy

In a grid strategy, the main logic is defined in the onBar callback function. Strategy implementation is done in the MyGridStrategy class.

class MyGridStrategy(backtest.StrategyTemplate, backtest.CryptoOrderMixin):
      #...
      # Dynamically update grid lines
      def updateBaseBuyPrice(self, context, isymbol, lastPrice, basePrice, n, mode):
          context["baseBuyPrice"][isymbol]=basePrice*(1-context["alpha"])
          context["baseSellPrice"][isymbol]=basePrice*(1+context["alpha"])
          context["N"][isymbol]=n
          if mode == 0:
              context["lowPrice"][isymbol]=0.0
              context["highPrice"][isymbol]=1000000.0
          elif mode == 1:
              context["lowPrice"][isymbol]=lastPrice
              context["highPrice"][isymbol]=1000000.0
          elif mode == 2:
              context["lowPrice"][isymbol]=0.0
              context["highPrice"][isymbol]=lastPrice
              
      def on_bar(self, context, msg, indicator):
          # ...
          for isymbol in msg.keys():
            lastPrice = msg[isymbol]["close"]
            if context["lowPrice"][isymbol] > 0 and lastPrice > context["lowPrice"][isymbol] * (1 + context["beta"]):
                # Buy condition
                qty = F.int(context["M"] / lastPrice)
                self.submit_order((isymbol, source, context["tradeTime"], 5, lastPrice,
                    upperLimit, lowerLimit, qty, 1, slippage, 1, expireTime), "buy", 0, AccountType.FUTURES)
                context["initPrice"][isymbol] = lastPrice
                self.updateBaseBuyPrice(context, isymbol, lastPrice, lastPrice, 1, 0)
            elif context["highPrice"][isymbol] < 1000000 and lastPrice < context["highPrice"][isymbol]*(1 - context["beta"]):
                # Sell condition
                # Check the position, and if the position is less than 0, do not sell.
                qty = F.int(context["M"] / lastPrice)
                self.submit_order((isymbol, source, context["tradeTime"], 5, lastPrice,
                    upperLimit, lowerLimit, qty, 3, slippage, 1, expireTime), "sell", 0, AccountType.FUTURES)
                context["initPrice"][isymbol] = lastPrice
                self.updateBaseBuyPrice(context, isymbol, lastPrice, lastPrice, 1, 0)
            # Update min and max prices
            if context["lowPrice"][isymbol] > 0:
                context["lowPrice"][isymbol] = F.min(context["lowPrice"][isymbol], lastPrice)
            if context["highPrice"][isymbol] < 1000000:
                context["highPrice"][isymbol] = F.max(context["highPrice"][isymbol], lastPrice)

Strategy configuration

The following code shows the basic configuration for a cryptocurrency minute-level strategy.

config = backtest.CryptoConfig() # Crypto strategy configuration
config.start_date = sf.scalar("2025.09.01", type="DATE")
config.end_date = sf.scalar("2025.09.10", type="DATE")
config.asset_type = backtest.AssetType.CRYPTO
config.cash = { 
    AccountType.SPOT: 100000000.0,
    AccountType.FUTURES: 100000000.0,
    AccountType.OPTION: 100000000.0
}
config.commission = 0.00015
config.data_type = backtest.MarketType.MINUTE # Minute-level data
config.security_reference = sf.table(...) # Asset information
config.funding_rate = sf.table(...) # Funding rate information
backtester = backtest.Backtester(MyStrategy, config)

Input market data

Market data can be obtained via a DolphinDB connection or other data sources, and then fed into the engine using the append_data method.

conn = sf.connect(host="192.168.100.43", port=8848, user="admin", passwd="123456")
sql = """
  select ... from loadTable(..)
"""
kline_data = conn.sql(sql)
backtester.append_data(kline_data)

Print backtesting results

After the backtest is complete, the results can be obtained using the following methods.

res = backtester.accounts[AccountType.FUTURES]
print(res.trade_details.to_pandas()) # Trade details
print(res.get_daily_position().to_pandas()) # Daily positions
print(res.daily_total_portfolios.to_pandas()) # Daily portfolio metrics
print(res.return_summary.to_pandas()) # Return summary

9. Appendix

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8