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

classPaperTrader {
    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 lockthis.data = threading.Dict()
        this.dataLock = threading.Lock()
        // Initialization of this.datathis.data.set("assets", assets)
        this.data.set("orders", [])
        this.data.set("positions", [])

        // exchangeDatalet exchangeData = {
            "exIdx": this.exIdx,
            "fee": this.fee
        }

        // exchange Typeif (this.name.includes("Futures_")) {
            this.exchangeType = "Futures"this.direction = "buy"this.marginLevel = 10this.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 enginethis.data.set("exchangeData", exchangeData)

        // databasethis.historyOrdersTblName = "HISTORY_ORDER"this.data.set("historyOrdersTblName", this.historyOrdersTblName)

        // initthis.init()
    }
    
    // exportSetCurrency(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]

        returnthis.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
        returnthis.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
        returnthis.e.SetDirection(direction)
    }

    GetTicker(...args) {
        returnthis.e.GetTicker(...args)
    }

    GetDepth(...args) {
        returnthis.e.GetDepth(...args)
    }

    GetTrades(...args) {
        returnthis.e.GetTrades(...args)
    }

    GetRecords(...args) {
        returnthis.e.GetRecords(...args)
    }

    GetMarkets() {
        returnthis.e.GetMarkets()
    }

    GetTickers() {
        returnthis.e.GetTickers()
    }

    GetFundings(...args) {
        if (this.exchangeType == "Spot") {
            this.e.Log(3, null, null, `not support`)
            return 
        }

        returnthis.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}
                    }
                } elseif (this.quoteCurrency == "USD") {
                    if (asset["Currency"] == this.baseCurrency) {
                        return {"Balance": 0, "FrozenBalance": 0, "Stocks": asset["Amount"], "FrozenStocks": asset["FrozenAmount"]}
                    }                
                }
            } elseif (this.exchangeType == "Spot") {
                if (asset["Currency"] == this.baseCurrency) {
                    // Stocks
                    acc["Stocks"] = asset["Amount"]
                    acc["FrozenStocks"] = asset["FrozenAmount"]
                } elseif (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}`)
                    returnnull 
                }

                for (let o of orders) {
                    if (o.Symbol == symbol) {
                        ret.push(o)
                    }
                }
                return ret 
            }
        } elseif (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}`)
                    returnnull 
                }

                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}`)
            returnnull 
        }
    }

    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}`)
            returnnull 
        }

        if (data && Array.isArray(data["values"]) && data["values"].length <= 0) {
            this.e.Log(3, null, null, `Order not found: ${orderId}`)
            returnnull 
        } elseif (data["values"].length != 1) {
            this.e.Log(3, null, null, `invalid data: ${data["values"]}`)
            returnnull 
        } else {
            let ret = this.parseJSON(data["values"][0])
            if (!ret) {
                this.e.Log(3, null, null, `invalid data: ${data["values"]}`)
                returnnull 
            }

            return ret 
        }
    }

    Buy(price, amount) {
        returnthis.trade("Buy", price, amount)
    }

    Sell(price, amount) {
        returnthis.trade("Sell", price, amount)
    }

    trade(tradeType, price, amount) {
        if (this.exchangeType == "Spot") {
            let side = ""if (tradeType == "Buy") {
                side = "buy"
            } elseif (tradeType == "Sell") {
                side = "sell"
            } else {
                this.e.Log(3, null, null, `invalid tradeType: ${tradeType}`)
                returnnull 
            }
            let symbol = this.currencyreturnthis.createOrder(symbol, side, price, amount)
        } elseif (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}`)
                returnnull 
            }

            let side = this.directionlet symbol = `${this.currency}.${this.contractType}`returnthis.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}`)
            returnnull 
        }
        if (this.exchangeType == "Spot") {
            if (side == "closebuy") {
                side = "sell"
            } elseif (side == "closesell") {
                side = "buy"
            }
        }
        returnthis.createOrder(symbol, side, price, amount)
    }

    createOrder(symbol, side, price, amount) {
        this.dataLock.acquire()
        let isError = falselet orders = this.data.get("orders")
        let positions = this.data.get("positions")
        let assets = this.data.get("assets")

        // Check amountif (amount <= 0) {
            this.e.Log(3, null, null, `invalid amount: ${amount}`)
            returnnull 
        }

        // Constructing orderslet 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 = nullswitch (side) {
            case"buy":
                order["Type"] = ORDER_TYPE_BUY
                order["Offset"] = ORDER_OFFSET_OPEN
                logType = LOG_TYPE_BUYbreakcase"sell":
                order["Type"] = ORDER_TYPE_SELL
                order["Offset"] = ORDER_OFFSET_OPEN
                logType = LOG_TYPE_SELLbreakcase"closebuy":
                order["Type"] = ORDER_TYPE_SELL
                order["Offset"] = ORDER_OFFSET_CLOSE
                logType = LOG_TYPE_SELLbreakcase"closesell":
                order["Type"] = ORDER_TYPE_BUY
                order["Offset"] = ORDER_OFFSET_CLOSE
                logType = LOG_TYPE_BUYbreakdefault:
                this.e.Log(3, null, null, `invalid direction: ${side}`)
                isError = true 
        }
        if (isError) {
            returnnull 
        }

        // Check assets/positions, report an error if assets/positions are insufficientlet needAssetName = ""let needAsset = 0if (this.exchangeType == "Futures") {
            // Check assets and positions// to do 
        } elseif (this.exchangeType == "Spot") {
            // Check assetslet arr = symbol.split(".")
            if (arr.length == 2) {
                this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
                returnnull 
            }
            let currency = arr[0]

            let arrCurrency = currency.split("_")
            if (arrCurrency.length != 2) {
                this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
                returnnull 
            }
            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 = falsefor (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)}`)
                returnnull 
            }
        } else {
            this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
            returnnull 
        }

        // Generate order ID, UnixNano() uses nanosecond timestamplet orderId = this.generateOrderId(symbol, UnixNano())
        order["Id"] = orderId

        // Update pending order records
        orders.push(order)
        this.data.set("orders", orders)
        
        // Output loggingif (this.exchangeType == "Futures") {
            this.e.SetDirection(side)
        }   
        this.e.Log(logType, price, amount, `orderId: ${orderId}`)

        // Update assetsfor (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 orderlet targetOrder = orders[targetIdx]

            // Update assetsif (this.exchangeType == "Futures") {
                // Contract exchange asset update// to do
            } elseif (this.exchangeType == "Spot") {
                let arrCurrency = targetOrder.Symbol.split("_")
                let baseCurrency = arrCurrency[0]
                let quoteCurrency = arrCurrency[1]

                let needAsset = 0let 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 assetsthis.data.set("assets", assets)
            } else {
                this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
                returnfalse 
            }

            // Update revocation status
            orders.splice(targetIdx, 1)
            targetOrder.Status = ORDER_STATE_CANCELED// Archive, write to databaselet 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 orderthis.e.Log(3, null, null, `Order not found: ${orderId}`)
            this.dataLock.release()
            returnfalse 
        }
        this.data.set("orders", orders)
        this.e.Log(LOG_TYPE_CANCEL, orderId)

        this.dataLock.release()
        returntrue 
    }

    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")
        */
    }


    // enginesimEngine(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 feelet exIdx = exchangeData["exIdx"]
            let fee = exchangeData["fee"]
            let e = exchanges[exIdx]

            // get exchangeType let exchangeType = exchangeData["exchangeType"]
            let marginLevel = 0if (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 tickersfor (let symbol in dictTick) {
                dictTick[symbol] = e.GetDepth(symbol)
            }

            // Matchmakinglet newPendingOrders = []
            for (let o of orders) {

                // Only pending orders are processedif (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 directionlet matchSide = o.Type == ORDER_TYPE_BUY ? depth.Asks : depth.Bidsif (!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.DealAmountlet filledValue = 0let filledAmount = 0for (let level of matchSide) {
                    let levelAmount = level.Amountlet levelPrice = level.Priceif ((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 orderlet currentFilledQty = Math.min(levelAmount * levelPrice, remain)
                            remain -= currentFilledQty
                            filledValue += currentFilledQty
                            filledAmount += currentFilledQty / levelPrice
                        } else {
                            // Limit order, the price is matched; market order, direct market matchinglet currentFilledAmount = Math.min(levelAmount, remain)
                            remain -= currentFilledAmount
                            filledValue += currentFilledAmount * levelPrice
                            filledAmount += currentFilledAmount
                        }
                        
                        // Initial judgment, if matched directly, it is judged as takerif (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 completedbreak 
                    }
                }

                // Changes in orderif (filledAmount > 0) {
                    // Update order changesif (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 updatesif (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 updatesif (exchangeType == "Futures") {
                        // Processing futures asset updates// to do 
                    } elseif (exchangeType == "Spot") {
                        // Handling spot asset updateslet 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 feeif (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)                                
                            } elseif (asset["Currency"] == plusAssetName) {
                                asset["Amount"] += plusAsset
                            }
                        }
                    }
                }

                // Check remain to update order statusif (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 databaselet 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)
        }
    }

    // otherisValidContractType(contractType) {
        // only support swap let contractTypes = ["swap"]
        if (contractTypes.includes(contractType)) {
            returntrue 
        } else {
            returnfalse 
        }
    }

    generateOrderId(symbol, ts) {
        let uuid = '', i, random
        for (i = 0; i < 36; i++) {
            if (i === 8 || i === 13 || i === 18 || i === 23) {
                uuid += '-'
            } elseif (i === 14) {
                // Fixed to 4
                uuid += '4'
            } elseif (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 = nulltry {
            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 tableDBExec(`DROP TABLE IF EXISTS ${this.historyOrdersTblName};`)
        
        // Rebuild the historical order tablelet 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) {
    returnnewPaperTrader(exIdx, realExchange, assets, fee)
}

// Use real ticker conditions to create efficient Paper Traderfunctionmain() {
    // create PaperTraderlet simulateAssets = [{"Currency": "USDT", "Amount": 10000, "FrozenAmount": 0}]
    let fee = {"taker": 0.001, "maker": 0.0005}
    paperTraderEx = $.CreatePaperTrader(0, exchange, simulateAssets, fee)
    Log(paperTraderEx)

    // test GetTickerLog("GetTicker:", paperTraderEx.GetTicker())

    // test GetOrdersLog("GetOrders:", paperTraderEx.GetOrders())

    // test Buy/Selllet orderId = paperTraderEx.Buy(-1, 0.1)
    Log("orderId:", orderId)

    // test GetOrderSleep(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.

From: Design of Real Ticker Driven Simulation Trading System Based on FMZ Quant Trading Platform

Leave a Reply

Your email address will not be published. Required fields are marked *