Type/to search
8
Follow
1359
Followers
Design of Real Ticker Driven Simulation Trading System Based on FMZ Quant Trading Platform
Original
Created 2025-05-06 10:44:33  Updated 2025-05-09 12:05:06
 0
 469

img

Preface

This article introduces the design and implementation of PaperTrader, a simulation trading system based on the FMZ Quant platform and driven by real ticker conditions. The system matches orders through real-time deep ticker conditions, fully simulates the trading process of strategy order placement, transaction, asset change and handling fee, supports market/limit orders, asset freezing and revocation archiving, and is suitable for strategy testing and real behavior verification before live trading. This article will explain its design concept and key implementation from the perspectives of system architecture, matching mechanism, interface compatibility, etc., and provide a complete practical demonstration case to help quantitative strategies build a safe and reliable "intermediate sandbox" before going online.

PaperTrader Demand Analysis and Design

Demand:

  • The exchange simulation trading is chaotic and unrealistic.
  • It is cumbersome to apply for a simulation account for the exchange simulation trading, and it is cumbersome to obtain test funds.
  • Many exchanges do not provide a test environment.

The "Gray Area" Problem Between Backtesting and Live Trading

Why do we need a simulated trading system?

In the whole process of quantitative strategy development, we usually go through the steps of "historical backtesting → environmental testing → live trading". However, historical backtesting uses statistical data and cannot handle the application effect of the strategy under the actual tickers. Live trading means capital flight, and the lack of an intermediate testing environment has become a disadvantage for our exploration.
In order to solve this problem, we need to design a lightweight simulated trading system - PaperTrader, which can use real-time ticker conditions (depth, market price) to simulate the complete trading process of placing orders, pending orders, transactions, order withdrawals, asset changes, and handling fee deductions, and finally complete strategy verification close to the live trading.

Design Goals and Key Features

    1. Real-time ticker driven
      Use FMZ to access real exchange ticker information, including GetTicker(), GetDepth() and dozens of other interfaces.
    1. Simulation order placement and fee deduction
      It supports limit/market orders, realize separate deduction of maker/taker fees, and use the actual calculated input amount for market buy orders.
    1. Asset/position/order management
      It supports freezing of assets when placing orders, returning assets when canceling orders, and supports maintenance of multiple assets/positions/orders based on symbol level.
    1. Complete order period
      Orders are clearly managed from creation, waiting, execution to cancellation. After execution, they are archived to the local database automatically to support future query and analysis.
    1. User experience
      The strategy does not need to change any order calls, and can achieve simulated trading simulation by simply replacing the exchange object with PaperTrader.

Overview of Class Library Design

The system mainly consists of three parts:

  • [PaperTrader class]:
    Core simulation account, including data maintenance such as assets, orders, positions, tickers and configurations

  • [simEngine matching engine]:
    Background thread, scans current orders according to ticker depth and performs operations

  • [Database archiving]: Write completed/cancelled orders to the local database for later analysis and review

Matching engine design:

simEngine(data, lock) is the core of the entire simulation system. It matches the current pending orders in a loop according to the actual ticker depth data to provide accurate simulation results for transactions.

The main processes include:

  • Get the current pending orders, positions, assets, and tickers;
  • Get the depth of all used symbols GetDepth;
  • Traverse the pending orders and select the operation depth (asks/bids) according to the direction (buy/sell);
  • Determine whether the transaction is completed based on whether the price meets the operation conditions;
  • If the transaction is completed, update and count the order information such as AvgPrice/DealAmount;
  • Deduct the handling fee according to the maker/taker;
  • If all transactions have been completed, the order will be archived; otherwise, it will be kept as pending;

Interface information compatible:

PaperTrader is designed to align with the real trading interface of the FMZ platform as much as possible, including but not limited to:

ClassificationInterfaceDescription
Order interfaceBuy(price, amount) / Sell(price, amount) / CreateOrder(symbol, side, price, amount)Order operation
Market interfaceGetTicker() / GetDepth() / GetRecords() / GetTrades()Request the real ticker price of the exchange directly
Order interfaceGetOrders() / CancelOrder(id) / GetOrder(id)For order operations
Account and position interfaceGetAccount() / GetAssets() / GetPositions()For account operations
Other settings interfaceSetCurrency() / SetDirection()Other settings

This design allows the strategy logic to run directly in the simulated trading environment without modification. By replacing exchange with PaperTrader with one click, the strategy can be migrated to the "middle layer" between backtesting and live trading.

PaperTrader Design Source Code

javascript
class PaperTrader { constructor(exIdx, realExchange, assets, fee) { this.exIdx = exIdx this.e = realExchange this.name = realExchange.GetName() + "_PaperTrader" this.currency = realExchange.GetCurrency() this.baseCurrency = this.currency.split("_")[0] this.quoteCurrency = this.currency.split("_")[1] this.period = realExchange.GetPeriod() this.fee = fee // Data synchronization lock this.data = threading.Dict() this.dataLock = threading.Lock() // Initialization of this.data this.data.set("assets", assets) this.data.set("orders", []) this.data.set("positions", []) // exchangeData let exchangeData = { "exIdx": this.exIdx, "fee": this.fee } // exchange Type if (this.name.includes("Futures_")) { this.exchangeType = "Futures" this.direction = "buy" this.marginLevel = 10 this.contractType = "swap" this.e.SetContractType(this.contractType) // set exchangeData exchangeData["exchangeType"] = this.exchangeType exchangeData["marginLevel"] = this.marginLevel } else { this.exchangeType = "Spot" // set exchangeData exchangeData["exchangeType"] = this.exchangeType } // Records information related to the exchange for transmission to the matching engine this.data.set("exchangeData", exchangeData) // database this.historyOrdersTblName = "HISTORY_ORDER" this.data.set("historyOrdersTblName", this.historyOrdersTblName) // init this.init() } // export SetCurrency(currency) { let arrCurrency = currency.split("_") if (arrCurrency.length != 2) { this.e.Log(3, null, null, `invalid currency: ${currency}`) return } this.currency = currency this.baseCurrency = arrCurrency[0] this.quoteCurrency = arrCurrency[1] return this.e.SetCurrency(currency) } SetContractType(contractType) { if (this.exchangeType == "Spot") { this.e.Log(3, null, null, `not support`) return } if (!this.isValidContractType(contractType)) { this.e.Log(3, null, null, `invalid contractType: ${contractType}`) return } this.contractType = contractType return this.e.SetContractType(contractType) } SetDirection(direction) { if (this.exchangeType == "Spot") { this.e.Log(3, null, null, `not support`) return } if (direction != "buy" && direction != "sell" && direction != "closebuy" && direction != "closesell") { this.e.Log(3, null, null, `invalid direction: ${direction}`) return } this.direction = direction return this.e.SetDirection(direction) } GetTicker(...args) { return this.e.GetTicker(...args) } GetDepth(...args) { return this.e.GetDepth(...args) } GetTrades(...args) { return this.e.GetTrades(...args) } GetRecords(...args) { return this.e.GetRecords(...args) } GetMarkets() { return this.e.GetMarkets() } GetTickers() { return this.e.GetTickers() } GetFundings(...args) { if (this.exchangeType == "Spot") { this.e.Log(3, null, null, `not support`) return } return this.e.GetFundings(...args) } GetAccount() { let assets = this.data.get("assets") let acc = {"Balance": 0, "FrozenBalance": 0, "Stocks": 0, "FrozenStocks": 0} for (let asset of assets) { if (this.exchangeType == "Futures") { if (this.quoteCurrency == "USDT" || this.quoteCurrency == "USDC") { if (asset["Currency"] == this.quoteCurrency) { return {"Balance": asset["Amount"], "FrozenBalance": asset["FrozenAmount"], "Stocks": 0, "FrozenStocks": 0} } } else if (this.quoteCurrency == "USD") { if (asset["Currency"] == this.baseCurrency) { return {"Balance": 0, "FrozenBalance": 0, "Stocks": asset["Amount"], "FrozenStocks": asset["FrozenAmount"]} } } } else if (this.exchangeType == "Spot") { if (asset["Currency"] == this.baseCurrency) { // Stocks acc["Stocks"] = asset["Amount"] acc["FrozenStocks"] = asset["FrozenAmount"] } else if (asset["Currency"] == this.quoteCurrency) { // Balance acc["Balance"] = asset["Amount"] acc["FrozenBalance"] = asset["FrozenAmount"] } } } return acc } GetAssets() { let assets = this.data.get("assets") return assets } GetOrders(symbol) { let ret = [] let orders = this.data.get("orders") if (this.exchangeType == "Spot") { if (typeof(symbol) == "undefined") { return orders } else { let arrCurrency = symbol.split("_") if (arrCurrency.length != 2) { this.e.Log(3, null, null, `invalid symbol: ${symbol}`) return null } for (let o of orders) { if (o.Symbol == symbol) { ret.push(o) } } return ret } } else if (this.exchangeType == "Futures") { if (typeof(symbol) == "undefined") { for (let o of orders) { if (o.Symbol.includes(`${this.quoteCurrency}.${this.contractType}`)) { ret.push(o) } } return ret } else { let arr = symbol.split(".") if (arr.length != 2) { this.e.Log(3, null, null, `invalid symbol: ${symbol}`) return null } let currency = arr[0] let contractType = arr[1] let arrCurrency = currency.split("_") if (arrCurrency.length != 2) { for (let o of orders) { if (o.Symbol.includes(`${arrCurrency[0]}.${contractType}`)) { ret.push(o) } } } else { for (let o of orders) { if (o.Symbol == symbol) { ret.push(o) } } } return ret } } else { this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`) return null } } GetOrder(orderId) { let data = DBExec(`SELECT ORDERDATA FROM ${this.historyOrdersTblName} WHERE ID = ?`, orderId) // {"columns":["ORDERDATA"],"values":[]} if (!data) { this.e.Log(3, null, null, `Order not found: ${orderId}`) return null } if (data && Array.isArray(data["values"]) && data["values"].length <= 0) { this.e.Log(3, null, null, `Order not found: ${orderId}`) return null } else if (data["values"].length != 1) { this.e.Log(3, null, null, `invalid data: ${data["values"]}`) return null } else { let ret = this.parseJSON(data["values"][0]) if (!ret) { this.e.Log(3, null, null, `invalid data: ${data["values"]}`) return null } return ret } } Buy(price, amount) { return this.trade("Buy", price, amount) } Sell(price, amount) { return this.trade("Sell", price, amount) } trade(tradeType, price, amount) { if (this.exchangeType == "Spot") { let side = "" if (tradeType == "Buy") { side = "buy" } else if (tradeType == "Sell") { side = "sell" } else { this.e.Log(3, null, null, `invalid tradeType: ${tradeType}`) return null } let symbol = this.currency return this.createOrder(symbol, side, price, amount) } else if (this.exchangeType == "Futures") { let compose = `${tradeType}_${this.direction}` if (compose != "Sell_closebuy" && compose != "Sell_sell" && compose != "Buy_buy" && compose != "Buy_closesell") { this.e.Log(3, null, null, `${tradeType}, invalid direction: ${this.direction}`) return null } let side = this.direction let symbol = `${this.currency}.${this.contractType}` return this.createOrder(symbol, side, price, amount) } else { this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`) return } } CreateOrder(symbol, side, price, amount) { if (side != "buy" && side != "sell" && side != "closebuy" && side != "closesell") { this.e.Log(3, null, null, `invalid direction: ${side}`) return null } if (this.exchangeType == "Spot") { if (side == "closebuy") { side = "sell" } else if (side == "closesell") { side = "buy" } } return this.createOrder(symbol, side, price, amount) } createOrder(symbol, side, price, amount) { this.dataLock.acquire() let isError = false let orders = this.data.get("orders") let positions = this.data.get("positions") let assets = this.data.get("assets") // Check amount if (amount <= 0) { this.e.Log(3, null, null, `invalid amount: ${amount}`) return null } // Constructing orders let order = { "Info": null, "Symbol": symbol, "Price": price, "Amount": amount, "DealAmount": 0, "AvgPrice": 0, "Status": ORDER_STATE_PENDING, "ContractType": symbol.split(".").length == 2 ? symbol.split(".")[1] : "" } let logType = null switch (side) { case "buy": order["Type"] = ORDER_TYPE_BUY order["Offset"] = ORDER_OFFSET_OPEN logType = LOG_TYPE_BUY break case "sell": order["Type"] = ORDER_TYPE_SELL order["Offset"] = ORDER_OFFSET_OPEN logType = LOG_TYPE_SELL break case "closebuy": order["Type"] = ORDER_TYPE_SELL order["Offset"] = ORDER_OFFSET_CLOSE logType = LOG_TYPE_SELL break case "closesell": order["Type"] = ORDER_TYPE_BUY order["Offset"] = ORDER_OFFSET_CLOSE logType = LOG_TYPE_BUY break default: this.e.Log(3, null, null, `invalid direction: ${side}`) isError = true } if (isError) { return null } // Check assets/positions, report an error if assets/positions are insufficient let needAssetName = "" let needAsset = 0 if (this.exchangeType == "Futures") { // Check assets and positions // to do } else if (this.exchangeType == "Spot") { // Check assets let arr = symbol.split(".") if (arr.length == 2) { this.e.Log(3, null, null, `invalid symbol: ${symbol}`) return null } let currency = arr[0] let arrCurrency = currency.split("_") if (arrCurrency.length != 2) { this.e.Log(3, null, null, `invalid symbol: ${symbol}`) return null } let baseCurrency = arrCurrency[0] let quoteCurrency = arrCurrency[1] needAssetName = side == "buy" ? quoteCurrency : baseCurrency if (side == "buy" && price <= 0) { // market order of buy, amount is quantity by quoteCurrency needAsset = amount } else { // limit order, amount is quantity by baseCurrency needAsset = side == "buy" ? price * amount : amount } let canPostOrder = false for (let asset of assets) { if (asset["Currency"] == needAssetName && asset["Amount"] >= needAsset) { canPostOrder = true } } if (!canPostOrder) { this.e.Log(3, null, null, `insufficient balance for ${needAssetName}, need: ${needAsset}, Account: ${JSON.stringify(assets)}`) return null } } else { this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`) return null } // Generate order ID, UnixNano() uses nanosecond timestamp let orderId = this.generateOrderId(symbol, UnixNano()) order["Id"] = orderId // Update pending order records orders.push(order) this.data.set("orders", orders) // Output logging if (this.exchangeType == "Futures") { this.e.SetDirection(side) } this.e.Log(logType, price, amount, `orderId: ${orderId}`) // Update assets for (let asset of assets) { if (asset["Currency"] == needAssetName) { asset["Amount"] -= needAsset asset["FrozenAmount"] += needAsset } } this.data.set("assets", assets) this.dataLock.release() return orderId } CancelOrder(orderId) { this.dataLock.acquire() let orders = this.data.get("orders") let assets = this.data.get("assets") let positions = this.data.get("positions") let targetIdx = orders.findIndex(item => item.Id == orderId) if (targetIdx != -1) { // Target order let targetOrder = orders[targetIdx] // Update assets if (this.exchangeType == "Futures") { // Contract exchange asset update // to do } else if (this.exchangeType == "Spot") { let arrCurrency = targetOrder.Symbol.split("_") let baseCurrency = arrCurrency[0] let quoteCurrency = arrCurrency[1] let needAsset = 0 let needAssetName = "" if (targetOrder.Type == ORDER_TYPE_BUY && targetOrder.Price <= 0) { needAssetName = quoteCurrency needAsset = targetOrder.Amount - targetOrder.DealAmount } else { needAssetName = targetOrder.Type == ORDER_TYPE_BUY ? quoteCurrency : baseCurrency needAsset = targetOrder.Type == ORDER_TYPE_BUY ? targetOrder.Price * (targetOrder.Amount - targetOrder.DealAmount) : (targetOrder.Amount - targetOrder.DealAmount) } for (let asset of assets) { if (asset["Currency"] == needAssetName) { asset["FrozenAmount"] -= needAsset asset["Amount"] += needAsset } } // Update assets this.data.set("assets", assets) } else { this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`) return false } // Update revocation status orders.splice(targetIdx, 1) targetOrder.Status = ORDER_STATE_CANCELED // Archive, write to database let strSql = [ `INSERT INTO ${this.historyOrdersTblName} (ID, ORDERDATA)`, `VALUES ('${targetOrder.Id}', '${JSON.stringify(targetOrder)}');` ].join("") let ret = DBExec(strSql) if (!ret) { e.Log(3, null, null, `Order matched successfully, but failed to archive to database: ${JSON.stringify(o)}`) } } else { // Failed to cancel the order this.e.Log(3, null, null, `Order not found: ${orderId}`) this.dataLock.release() return false } this.data.set("orders", orders) this.e.Log(LOG_TYPE_CANCEL, orderId) this.dataLock.release() return true } GetHistoryOrders(symbol, since, limit) { // Query historical orders // to do } SetMarginLevel(symbol) { // Set leverage value // Synchronize this.marginLevel and exchangeData["marginLevel"] in this.data // to do } GetPositions(symbol) { // Query positions // to do /* if (this.exchangeType == "Spot") { this.e.Log(3, null, null, `not support`) return } let pos = this.data.get("positions") */ } // engine simEngine(data, lock) { while (true) { lock.acquire() // get orders / positions / assets / exchangeData let orders = data.get("orders") let positions = data.get("positions") let assets = data.get("assets") let exchangeData = data.get("exchangeData") let historyOrdersTblName = data.get("historyOrdersTblName") // get exchange idx and fee let exIdx = exchangeData["exIdx"] let fee = exchangeData["fee"] let e = exchanges[exIdx] // get exchangeType let exchangeType = exchangeData["exchangeType"] let marginLevel = 0 if (exchangeType == "Futures") { marginLevel = exchangeData["marginLevel"] } // get Depth let dictTick = {} for (let order of orders) { dictTick[order.Symbol] = {} } for (let position of positions) { dictTick[position.Symbol] = {} } // Update tickers for (let symbol in dictTick) { dictTick[symbol] = e.GetDepth(symbol) } // Matchmaking let newPendingOrders = [] for (let o of orders) { // Only pending orders are processed if (o.Status != ORDER_STATE_PENDING) { continue } // No data in the market let depth = dictTick[o.Symbol] if (!depth) { e.Log(3, null, null, `Order canceled due to invalid order book data: ${JSON.stringify(o)}`) continue } // Determine the order book matching direction based on the order direction let matchSide = o.Type == ORDER_TYPE_BUY ? depth.Asks : depth.Bids if (!matchSide || matchSide.length == 0) { e.Log(3, null, null, `Order canceled due to invalid order book data: ${JSON.stringify(o)}`) continue } let remain = o.Amount - o.DealAmount let filledValue = 0 let filledAmount = 0 for (let level of matchSide) { let levelAmount = level.Amount let levelPrice = level.Price if ((o.Price > 0 && ((o.Type == ORDER_TYPE_BUY && o.Price >= levelPrice) || (o.Type == ORDER_TYPE_SELL && o.Price <= levelPrice))) || o.Price <= 0) { if (exchangeType == "Spot" && o.Type == ORDER_TYPE_BUY && o.Price <= 0) { // Spot market buy order let currentFilledQty = Math.min(levelAmount * levelPrice, remain) remain -= currentFilledQty filledValue += currentFilledQty filledAmount += currentFilledQty / levelPrice } else { // Limit order, the price is matched; market order, direct market matching let currentFilledAmount = Math.min(levelAmount, remain) remain -= currentFilledAmount filledValue += currentFilledAmount * levelPrice filledAmount += currentFilledAmount } // Initial judgment, if matched directly, it is judged as taker if (typeof(o.isMaker) == "undefined") { o.isMaker = false } } else { // The price does not meet the matching criteria, and is initially judged as a maker. if (typeof(o.isMaker) == "undefined") { o.isMaker = true } break } if (remain <= 0) { // Order completed break } } // Changes in order if (filledAmount > 0) { // Update order changes if (exchangeType == "Spot" && o.Type == ORDER_TYPE_BUY && o.Price <= 0) { if (o.AvgPrice == 0) { o.AvgPrice = filledValue / filledAmount o.DealAmount += filledValue } else { o.AvgPrice = (o.DealAmount + filledValue) / (filledAmount + o.DealAmount / o.AvgPrice) o.DealAmount += filledValue } } else { o.AvgPrice = (o.DealAmount * o.AvgPrice + filledValue) / (filledAmount + o.DealAmount) o.DealAmount += filledAmount } // Handling position updates if (exchangeType == "Futures") { // Futures, find the position in the corresponding order direction, update // to do /* if () { // Find the corresponding position and update it } else { // There is no corresponding position, create a new one let pos = { "Info": null, "Symbol": o.Symbol, "MarginLevel": marginLevel, "Amount": o.Amount, "FrozenAmount": 0, "Price": o.Price, "Profit": 0, "Type": o.Type == ORDER_TYPE_BUY ? PD_LONG : PD_SHORT, "ContractType": o.Symbol.split(".")[1], "Margin": o.Amount * o.Price / marginLevel // to do USDT/USD contract Multiplier } positions.push(pos) } */ } // Handling asset updates if (exchangeType == "Futures") { // Processing futures asset updates // to do } else if (exchangeType == "Spot") { // Handling spot asset updates let arrCurrency = o.Symbol.split("_") let baseCurrency = arrCurrency[0] let quoteCurrency = arrCurrency[1] let minusAssetName = o.Type == ORDER_TYPE_BUY ? quoteCurrency : baseCurrency let minusAsset = o.Type == ORDER_TYPE_BUY ? filledValue : filledAmount let plusAssetName = o.Type == ORDER_TYPE_BUY ? baseCurrency : quoteCurrency let plusAsset = o.Type == ORDER_TYPE_BUY ? filledAmount : filledValue // Deduction of handling fee if (o.isMaker) { plusAsset = (1 - fee["maker"]) * plusAsset } else { plusAsset = (1 - fee["taker"]) * plusAsset } for (let asset of assets) { if (asset["Currency"] == minusAssetName) { // asset["FrozenAmount"] -= minusAsset asset["FrozenAmount"] = Math.max(0, asset["FrozenAmount"] - minusAsset) } else if (asset["Currency"] == plusAssetName) { asset["Amount"] += plusAsset } } } } // Check remain to update order status if (remain <= 0) { // Order completed, update order status, update average price, update completion amount o.Status = ORDER_STATE_CLOSED // Completed orders are archived and recorded in the database let strSql = [ `INSERT INTO ${historyOrdersTblName} (ID, ORDERDATA)`, `VALUES ('${o.Id}', '${JSON.stringify(o)}');` ].join("") let ret = DBExec(strSql) if (!ret) { e.Log(3, null, null, `Order matched successfully, but failed to archive to database: ${JSON.stringify(o)}`) } } else { newPendingOrders.push(o) } } // Update current pending order data data.set("orders", newPendingOrders) data.set("assets", assets) lock.release() Sleep(1000) } } // other isValidContractType(contractType) { // only support swap let contractTypes = ["swap"] if (contractTypes.includes(contractType)) { return true } else { return false } } generateOrderId(symbol, ts) { let uuid = '', i, random for (i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) { uuid += '-' } else if (i === 14) { // Fixed to 4 uuid += '4' } else if (i === 19) { // The upper 2 bits are fixed to 10 random = (Math.random() * 16) | 0 uuid += ((random & 0x3) | 0x8).toString(16) } else { random = (Math.random() * 16) | 0 uuid += random.toString(16) } } return `${symbol},${uuid}-${ts}` } parseJSON(strData) { let ret = null try { ret = JSON.parse(strData) } catch (err) { Log("err.name:", err.name, ", err.stack:", err.stack, ", err.message:", err.message, ", strData:", strData) } return ret } init() { threading.Thread(this.simEngine, this.data, this.dataLock) // Delete the database, historical order table DBExec(`DROP TABLE IF EXISTS ${this.historyOrdersTblName};`) // Rebuild the historical order table let strSql = [ `CREATE TABLE IF NOT EXISTS ${this.historyOrdersTblName} (`, "ID VARCHAR(255) NOT NULL PRIMARY KEY,", "ORDERDATA TEXT NOT NULL", ")" ].join(""); DBExec(strSql) } } // extport $.CreatePaperTrader = function(exIdx, realExchange, assets, fee) { return new PaperTrader(exIdx, realExchange, assets, fee) } // Use real ticker conditions to create efficient Paper Trader function main() { // create PaperTrader let simulateAssets = [{"Currency": "USDT", "Amount": 10000, "FrozenAmount": 0}] let fee = {"taker": 0.001, "maker": 0.0005} paperTraderEx = $.CreatePaperTrader(0, exchange, simulateAssets, fee) Log(paperTraderEx) // test GetTicker Log("GetTicker:", paperTraderEx.GetTicker()) // test GetOrders Log("GetOrders:", paperTraderEx.GetOrders()) // test Buy/Sell let orderId = paperTraderEx.Buy(-1, 0.1) Log("orderId:", orderId) // test GetOrder Sleep(1000) Log(paperTraderEx.GetOrder(orderId)) Sleep(6000) }

Practical Demonstration and Test Cases

Live Trading

The above code can be saved as a "template library" of the FMZ platform. The main function in this template library is the test function:

img

In this way, we can write an API KEY string when configuring the exchange object. At this time, operations such as placing orders will not access the exchange interface, it will use the assets, orders, positions and other data of the simulation system for simulation. However, the ticker conditions are the real ticker conditions of the exchange.

Expansion and Optimization Direction

The value of simulation systems in strategy development
PaperTrader provides a testing environment that is highly close to the live trading, allowing developers to verify the execution behavior, order logic, matching performance and fund changes of strategies without risk. It is particularly suitable for the following scenarios:

  • Multi-strategy debugging and concurrent testing
  • Quickly verify the performance of strategies under different ticker conditions
  • Avoid losses caused by direct live trading orders during debugging
  • Replace some traditional historical backtesting verification methods

Difference from pure backtesting

Traditional backtesting is based on historical data and runs K by K, ignoring real trading details such as pending orders, partial transactions, matching slippage, and handling fee structure. While the simulation system:

  • Use real-time tickers (not static historical data)
  • Simulate the real order life period (new → pending order → matching → transaction → cancellation)
  • Calculate the handling fee, slippage, and average transaction price accurately
  • Better test "strategy behavior" rather than just "strategy model"
  • Serves as a bridge between live trading deployment

Notes on PaperTrader
The above PaperTrader is just a preliminary design (only preliminary code review and testing have been done), and the goal is to provide a design idea and solution reference. PaperTrader still needs to be tested to check whether the matching logic, order system, position system, capital system and other designs are reasonable. Due to time constraints, only a relatively complete implementation of spot trading has been made, and some functions of futures contracts are still in the to do state.

Possible potential problems:

  • Floating point calculation error.
  • Logical processing boundary.
  • It will be more complicated to support delivery contracts
  • It will be more complicated to design the liquidation mechanism

The next evolution direction

In order to further enhance the application value of PaperTrader, the following directions can be considered for expansion in the next stage:

  • Improve support for contract simulation (unfinished part of to do in the code).
  • Support contract position and leverage fund management (isolated position, crossed position).
  • Introduce floating profit and loss calculation and forced liquidation mechanism.

Through PaperTrader, we can not only provide a safer testing environment for strategies, but also further promote the key link of strategies from "research models" to "real productivity".

Readers are welcome to leave comments, thank you for your reading.

Comment
All comments (0)
No data
No data
  • 1
iPhone Download
Forums
PINE Language
© 2015 - ∞ INVENTOR PTE LTD (SG)