avatar of 发明者量化-小小梦 发明者量化-小小梦
focar em Mensagem privada
4
focar em
1271
Seguidores

Projeto de sistema de simulação de negociação baseado na plataforma quantitativa FMZ

Criado em: 2025-04-30 16:59:01, atualizado em: 2025-04-30 17:53:06
comments   1
hits   666

[TOC]

Projeto de sistema de simulação de negociação baseado na plataforma quantitativa FMZ

Prefácio

Este artigo apresenta o design e a implementação do PaperTrader, um sistema de simulação de negociação baseado na plataforma quantitativa FMZ e orientado por condições reais de mercado. O sistema combina ordens por meio de condições de mercado detalhadas em tempo real, simula completamente processos de negociação, como colocação de ordens de estratégia, transações, alterações de ativos e processamento de taxas, oferece suporte a ordens de mercado/limite, congelamento de ativos e arquivamento de cancelamento, e é adequado para testes de estratégia e verificação de comportamento real antes de negociações reais. Este artigo explicará em detalhes seu conceito de design e implementação principal a partir das perspectivas de arquitetura do sistema, mecanismo de correspondência, compatibilidade de interface, etc., e fornecerá casos de uso de demonstração prática completa para ajudar estratégias quantitativas a construir uma “caixa de areia intermediária” segura e confiável antes de entrar online.

Análise e Design de Requisitos do PaperTrader

Pontos problemáticos da demanda:

  • A simulação de negociação de câmbio é caótica e irrealista.
  • Solicitar uma conta simulada em uma bolsa é trabalhoso, assim como obter fundos de teste.
  • Muitas bolsas não oferecem um ambiente de teste.

O problema da “área cinzenta” entre o backtesting e a negociação real

Por que você precisa de um sistema de negociação simulado?

Em todo o processo de desenvolvimento de estratégia quantitativa, geralmente passamos pelas etapas de “backtesting histórico → teste ambiental → negociação real”. No entanto, o backtesting histórico usa dados estatísticos e não consegue processar a eficácia das estratégias em condições reais de mercado. No entanto, a negociação real significa fuga de fundos, e a falta de um ambiente de teste intermediário se tornou um ponto problemático em nossa exploração. Para resolver esse problema, precisamos projetar um sistema de simulação de negociação leve - PaperTrader, que pode usar condições de mercado em tempo real (profundidade, preço de mercado) para simular todo o processo de negociação, incluindo colocação de ordens, ordens pendentes, transações, retiradas de ordens, alterações de ativos e deduções de comissão e, finalmente, concluir a verificação da estratégia próxima ao nível de negociação real.

Objetivos de design e principais recursos

    1. Movimentação do mercado em tempo real Use o FMZ para acessar cotações de câmbio reais, incluindo GetTicker(), GetDepth() e dezenas de outras interfaces.
    1. Simulação de colocação de pedidos e dedução Suporta ordens de limite/mercado, dedução separada de taxas de fabricante/tomador e usa o valor de entrada calculado real para ordens de compra de mercado.
    1. Gestão de ativos/posições/ordens Ele suporta o congelamento de ativos ao fazer pedidos e a devolução deles ao cancelar pedidos, além de suportar a manutenção de múltiplos ativos/posições/pedidos com base no nível do símbolo.
    1. Ciclo de pedido completo Os pedidos são gerenciados de forma clara, desde a criação, espera, execução até o cancelamento. Após a execução, eles são arquivados automaticamente no banco de dados local para dar suporte a consultas e análises futuras.
    1. Experiência do usuário A estratégia não precisa alterar nenhuma chamada de ordem e pode implementar negociações simuladas simplesmente substituindo o objeto de troca pelo PaperTrader.

Visão geral do design da biblioteca

O sistema consiste principalmente em três partes:

  • Classe PaperTrader A conta de simulação principal inclui manutenção de dados como ativos, ordens, posições, condições de mercado e configurações.

  • [mecanismo de correspondência simEngine]: Thread de fundo, verifica a ordem atual de acordo com a profundidade do mercado e executa operações

  • 【Arquivo de banco de dados】: Grave pedidos concluídos/cancelados no banco de dados local para análise e revisão posteriores

Design de motor correspondente

simEngine(data, lock) é o núcleo de todo o sistema de simulação. Ele combina os pedidos pendentes atuais em um loop de acordo com os dados reais de profundidade do mercado para fornecer resultados de simulação precisos para transações.

O processo principal inclui:

  • Obtenha ordens pendentes, posições, ativos e condições de mercado atuais;
  • Obter a profundidade de todos os símbolos usados ​​GetDepth;
  • Percorrer as ordens pendentes e selecionar a profundidade da operação (asks/bids) de acordo com a direção (compra/venda);
  • Determinar se a transação é concluída com base na conformidade do preço com as condições operacionais;
  • Se a transação for concluída, atualize e conte as informações do pedido, como AvgPrice / DealAmount;
  • Deduza taxas de manuseio com base no emissor/tomador.
  • Se todas as ordens forem executadas, a ordem será arquivada; caso contrário, permanecerá pendente.

Compatibilidade de informações de interface

O PaperTrader foi projetado para se alinhar o máximo possível com a interface de negociação real da plataforma FMZ, incluindo, mas não se limitando a:

Classificação interface descrever
Interface de pedido Buy(price, amount) / Sell(price, amount) / CreateOrder(symbol, side, price, amount) Operação de ordem
Interface de mercado GetTicker() / GetDepth() / GetRecords() / GetTrades() Solicite diretamente o preço real de mercado da troca
Interface de pedido GetOrders() / CancelOrder(id) / GetOrder(id) Para operações de pedidos
Interface de conta e posição GetAccount() / GetAssets() / GetPositions() Para operações de conta
Outras configurações de interface SetCurrency() / SetDirection() Outras configurações

Este design permite que a lógica da estratégia seja executada diretamente em um ambiente de negociação simulado sem modificações. Ao substituir a bolsa pelo PaperTrader com um clique, você pode migrar a estratégia para a “camada intermediária” entre o backtesting e a negociação real.

Código-fonte do design do PaperTrader

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
        
        // 数据同步锁
        this.data = threading.Dict()
        this.dataLock = threading.Lock()
        // 初始化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
        }

        // 记录交易所相关信息,用于传入撮合引擎
        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")

        // 检查amount
        if (amount <= 0) {
            this.e.Log(3, null, null, `invalid amount: ${amount}`)
            return null 
        }

        // 构造订单
        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 
        }

        // 检查资产/持仓,资产/持仓不足报错
        let needAssetName = ""
        let needAsset = 0
        if (this.exchangeType == "Futures") {
            // 检查资产、持仓
            // to do 
        } else if (this.exchangeType == "Spot") {
            // 检查资产
            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 
        }

        // 生成订单ID, UnixNano() 使用纳秒时间戳
        let orderId = this.generateOrderId(symbol, UnixNano())
        order["Id"] = orderId

        // 更新pending中的订单记录
        orders.push(order)
        this.data.set("orders", orders)
        
        // 输出日志记录
        if (this.exchangeType == "Futures") {
            this.e.SetDirection(side)
        }   
        this.e.Log(logType, price, amount, `orderId: ${orderId}`)

        // 更新资产
        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) {
            // 目标订单
            let targetOrder = orders[targetIdx]

            // 更新资产
            if (this.exchangeType == "Futures") {
                // 合约交易所资产更新
                // 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
                    }
                }

                // 更新 assets
                this.data.set("assets", assets)
            } else {
                this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
                return false 
            }

            // 更新撤销状态
            orders.splice(targetIdx, 1)
            targetOrder.Status = ORDER_STATE_CANCELED
            
            // 归档,写入数据库
            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 {
            // 撤单失败
            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) {
        // 查询历史订单
        // to do
    }

    SetMarginLevel(symbol) {
        // 设置杠杆值
        // 同步 this.marginLevel 和 this.data 中的 exchangeData["marginLevel"]
        // to do    
    }

    GetPositions(symbol) {
        // 查询持仓
        // 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] = {}
            }
            // 更新行情
            for (let symbol in dictTick) {
                dictTick[symbol] = e.GetDepth(symbol)
            }

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

                // 只处理pending订单
                if (o.Status != ORDER_STATE_PENDING) {
                    continue 
                }

                // 盘口无数据 
                let depth = dictTick[o.Symbol]
                if (!depth) {
                    e.Log(3, null, null, `Order canceled due to invalid order book data: ${JSON.stringify(o)}`)
                    continue 
                }

                // 根据订单方向,确定订单薄撮合方向
                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) {
                            // 现货市价单买单
                            let currentFilledQty = Math.min(levelAmount * levelPrice, remain)
                            remain -= currentFilledQty
                            filledValue += currentFilledQty
                            filledAmount += currentFilledQty / levelPrice
                        } else {
                            // 限价单,价格符合撮合;市价单,直接盘口撮合
                            let currentFilledAmount = Math.min(levelAmount, remain)
                            remain -= currentFilledAmount
                            filledValue += currentFilledAmount * levelPrice
                            filledAmount += currentFilledAmount
                        }
                        
                        // 初次判断,如果直接撮合,判定为 taker
                        if (typeof(o.isMaker) == "undefined") {
                            o.isMaker = false 
                        }
                    } else {
                        // 价格不符合撮合,初次判断,判定为 maker
                        if (typeof(o.isMaker) == "undefined") {
                            o.isMaker = true 
                        }
                        break
                    }

                    if (remain <= 0) {
                        // 订单成交完成
                        break 
                    }
                }

                // 订单有变动
                if (filledAmount > 0) {
                    // 更新订单变动
                    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
                    }

                    // 处理持仓更新
                    if (exchangeType == "Futures") {
                        // 期货,查找对应订单方向上的持仓,更新
                        // to do 

                        /*
                        if () {
                            // 查到对应持仓,更新
                        } else {
                            // 没有对应持仓,新建
                            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)
                        }
                        */ 
                    }

                    // 处理资产更新
                    if (exchangeType == "Futures") {
                        // 处理期货资产更新
                        // to do 
                    } else if (exchangeType == "Spot") {
                        // 处理现货资产更新
                        
                        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
                        
                        // 手续费扣除
                        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
                            }
                        }
                    }
                }

                // 检测remain更新订单状态
                if (remain <= 0) {
                    // 订单完成,更新订单状态,更新均价,更新完成量
                    o.Status = ORDER_STATE_CLOSED
                    
                    // 完成的订单归档,记录到数据库
                    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)
                }
            }

            // 更新当前挂单数据
            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) {
                // 固定为4
                uuid += '4'
            } else if (i === 19) {
                // 高2位固定为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)
        
        // 删除数据库 历史订单表
        DBExec(`DROP TABLE IF EXISTS ${this.historyOrdersTblName};`)
        
        // 重建 历史订单表
        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)
}

// 用真实行情打造高效 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)
}

Demonstração prática e casos de teste

Oferta Firme

O código acima pode ser salvo como a “Biblioteca de Modelos” da plataforma FMZ.mainA função é a função de teste:

Projeto de sistema de simulação de negociação baseado na plataforma quantitativa FMZ

Dessa forma, quando você estiver realmente negociando, você pode escrever uma string API KEY ao configurar o objeto de troca. Neste momento, operações como a colocação de ordens não acessarão de fato a interface de câmbio, mas usarão os ativos, ordens, posições e outros dados do sistema de simulação para simulação. Mas as condições de mercado são as condições reais de mercado da troca.

Direção de expansão e otimização

O valor dos sistemas de simulação no desenvolvimento de estratégias O PaperTrader fornece um ambiente de testes muito próximo do mercado real, permitindo que os desenvolvedores verifiquem o comportamento de execução, a lógica da ordem, o desempenho de correspondência e as alterações de capital da estratégia sem nenhum risco. É particularmente adequado para os seguintes cenários:

  • Teste simultâneo de depuração multiestratégia
  • Verifique rapidamente o desempenho das estratégias em diferentes condições de mercado
  • Evite ordens diretas em tempo real durante a depuração para evitar perdas
  • Substituir alguns métodos tradicionais de verificação de backtesting histórico

Diferença do backtesting puro

O backtesting tradicional é baseado em dados históricos e executado K por K, ignorando detalhes reais de transações, como pedidos pendentes, transações parciais, slippage correspondente e estrutura de taxas. O sistema de simulação:

  • Use cotações em tempo real (não dados históricos estáticos)
  • Simule o ciclo de vida real do pedido (criar → fazer pedido → corresponder → executar → cancelar)
  • Calcule com precisão taxas, slippage e preço médio de transação
  • Melhor em testar “comportamento estratégico” em vez de apenas “modelo estratégico”
  • Atuando como uma ponte entre a implantação em tempo real

Notas sobre o PaperTrader O PaperTrader acima é apenas um projeto preliminar (apenas revisão preliminar do código e testes foram feitos), e o objetivo é fornecer uma ideia de projeto e uma referência de solução. O PaperTrader também precisa ser testado para verificar se a lógica de correspondência, o sistema de ordens, o sistema de posições, o sistema de capital e outros designs são razoáveis. Devido a restrições de tempo, apenas a negociação à vista foi implementada de forma relativamente completa, e algumas funções de contratos futuros ainda estão em fase de conclusão.

Possíveis problemas potenciais:

  • Erro de cálculo de ponto flutuante.
  • Limites de processamento lógico.
  • O suporte para contratos de entrega é mais complicado
  • O desenho do mecanismo de liquidação é mais complicado

A próxima direção da evolução

Para aumentar ainda mais o valor da aplicação do PaperTrader, as seguintes direções podem ser consideradas para expansão na próxima etapa:

  • Melhorar o suporte para simulação de contratos (parte inacabada do código).
  • Oferece suporte à posição do contrato e à gestão de fundos de alavancagem (posição por posição, posição completa).
  • Introduzir cálculo de lucros e perdas flutuantes e mecanismo de liquidação forçada.

Por meio do PaperTrader, não só podemos fornecer um ambiente de teste mais seguro para estratégias, mas também promover ainda mais o vínculo fundamental das estratégias entre “modelos de pesquisa” e “produtividade real”.

Os leitores são bem-vindos para deixar mensagens, obrigado pela leitura.