avatar of 发明者量化-小小梦 发明者量化-小小梦
집중하다 사신
4
집중하다
1271
수행원

FMZ 양적 플랫폼 기반 실제 시장 중심 시뮬레이션 거래 시스템 설계

만든 날짜: 2025-04-30 16:59:01, 업데이트 날짜: 2025-04-30 17:53:06
comments   1
hits   661

[TOC]

FMZ 양적 플랫폼 기반 실제 시장 중심 시뮬레이션 거래 시스템 설계

머리말

이 글에서는 FMZ 양적 플랫폼을 기반으로 하고 실제 시장 상황에 따라 구동되는 시뮬레이션 거래 시스템인 PaperTrader의 설계와 구현을 소개합니다. 이 시스템은 실시간 심층 시장 상황을 통해 주문을 매칭하고, 전략 주문, 거래, 자산 변경 및 수수료 처리와 같은 거래 프로세스를 완벽하게 시뮬레이션하고, 시장/지정 주문, 자산 동결 및 취소 보관을 지원하며, 실제 거래 전 전략 테스트 및 실제 행동 검증에 적합합니다. 이 글에서는 시스템 아키텍처, 매칭 메커니즘, 인터페이스 호환성 등의 관점에서 설계 개념과 주요 구현을 자세히 설명하고, 정량적 전략이 온라인에 진출하기 전에 안전하고 신뢰할 수 있는 “중간 샌드박스”를 구축하는 데 도움이 되는 완전한 실용적 데모 사용 사례를 제공합니다.

PaperTrader 요구 사항 분석 및 설계

수요의 문제점:

  • 거래소 시뮬레이션 거래는 혼란스럽고 비현실적입니다.
  • 거래소에서 모의 ​​계좌를 신청하는 것은 번거롭고, 테스트 자금을 얻는 것도 번거롭습니다.
  • 많은 거래소는 테스트 환경을 제공하지 않습니다.

백테스팅과 실제 거래 사이의 “회색 영역” 문제

시뮬레이션 거래 시스템이 필요한 이유는 무엇입니까?

양적 전략 개발의 전체 과정에서 우리는 일반적으로 “과거 백테스팅 → 환경 테스트 → 실제 거래”의 단계를 거칩니다. 그러나 과거 백테스팅은 통계적 데이터를 사용하므로 실제 시장 상황에서 전략의 효과를 처리할 수 없습니다. 그러나 실제 거래는 자금 유출을 의미하며, 중간 테스트 환경이 부족하다는 점이 우리의 탐색에 있어 문제점으로 지적되었습니다. 이 문제를 해결하려면 실시간 시장 상황(깊이, 시장 가격)을 사용하여 주문, 보류 주문, 거래, 주문 인출, 자산 변경, 수수료 공제를 포함한 전체 거래 프로세스를 시뮬레이션하고 궁극적으로 실제 거래 수준에 가깝게 전략 검증을 완료할 수 있는 가벼운 시뮬레이션 거래 시스템인 PaperTrader를 설계해야 합니다.

디자인 목표 및 주요 기능

    1. 실시간 시장 주도 GetTicker(), GetDepth() 및 기타 수십 개의 인터페이스를 포함하여 FMZ를 사용하여 실제 거래소 시세에 액세스합니다.
    1. 시뮬레이션 주문 및 공제 제한/시장가 주문, 메이커/테이커 수수료 별도 공제를 지원하고, 시장가 매수 주문에 대해 실제로 계산된 입력 금액을 사용합니다.
    1. 자산/포지션/주문 관리 주문 시 자산을 동결하고, 주문 취소 시 자산을 반환하는 기능을 지원하며, 심볼 수준에 따라 여러 자산/포지션/주문을 유지할 수 있도록 지원합니다.
    1. 주문주기 완료 주문은 생성, 대기, 실행, 취소까지 명확하게 관리됩니다. 실행 후에는 향후 쿼리 및 분석을 지원하기 위해 로컬 데이터베이스에 자동으로 보관됩니다.
    1. 사용자 경험 이 전략에서는 주문 호출을 변경할 필요가 없으며, 간단히 교환 객체를 PaperTrader로 바꿔서 시뮬레이션 거래를 구현할 수 있습니다.

도서관 디자인 개요

이 시스템은 주로 세 부분으로 구성됩니다.

  • PaperTrader 클래스 핵심 시뮬레이션 계정에는 자산, 주문, 포지션, 시장 상황 및 구성과 같은 데이터 유지 관리가 포함됩니다.

  • [simEngine 매칭 엔진]: 백그라운드 스레드는 시장 깊이에 따라 현재 주문을 스캔하고 작업을 수행합니다.

  • 【데이터베이스 아카이브】: 완료/취소된 주문을 나중에 분석하고 검토할 수 있도록 로컬 데이터베이스에 기록합니다.

엔진 설계 매칭

simEngine(data, lock)은 전체 시뮬레이션 시스템의 핵심입니다. 실제 시장 심도 데이터에 따라 현재 보류 중인 주문을 루프로 매칭하여 거래에 대한 정확한 시뮬레이션 결과를 제공합니다.

주요 프로세스는 다음과 같습니다.

  • 현재 보류 중인 주문, 포지션, 자산 및 시장 상황을 알아보세요.
  • 사용된 모든 심볼의 깊이를 가져옵니다 GetDepth;
  • 보류 중인 주문을 탐색하고 방향(매수/매도)에 따라 작업 깊이(매수/매도)를 선택합니다.
  • 가격이 운영 조건을 충족하는지 여부에 따라 거래가 완료되었는지 판단합니다.
  • 거래가 완료되면 AvgPrice/DealAmount와 같은 주문 정보를 업데이트하고 계산합니다.
  • 메이커/테이커에 따라 처리 수수료를 공제합니다.
  • 모든 주문이 실행된 경우 주문은 보관됩니다. 그렇지 않으면 보류 상태로 유지됩니다.

인터페이스 정보 호환성

PaperTrader는 다음을 포함하되 이에 국한되지 않고 FMZ 플랫폼의 실제 거래 인터페이스와 최대한 일치하도록 설계되었습니다.

분류 인터페이스 설명하다
주문 인터페이스 Buy(price, amount) / Sell(price, amount) / CreateOrder(symbol, side, price, amount) 주문 작업
마켓 인터페이스 GetTicker() / GetDepth() / GetRecords() / GetTrades() 거래소의 실제 시장 가격을 직접 요청하세요
주문 인터페이스 GetOrders() / CancelOrder(id) / GetOrder(id) 주문 작업을 위해
계정 및 위치 인터페이스 GetAccount() / GetAssets() / GetPositions() 계정 운영을 위해
기타 설정 인터페이스 SetCurrency() / SetDirection() 기타 설정

이 설계를 사용하면 전략 논리를 수정하지 않고도 시뮬레이션된 거래 환경에서 직접 실행할 수 있습니다. 한 번의 클릭으로 거래소를 PaperTrader로 교체하면 전략을 백테스팅과 실제 거래 사이의 “중간 계층”으로 이전할 수 있습니다.

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

실제 시연 및 테스트 사례

확정 제안

위 코드는 FMZ 플랫폼의 “템플릿 라이브러리”로 저장할 수 있습니다.main함수는 테스트 함수입니다.

FMZ 양적 플랫폼 기반 실제 시장 중심 시뮬레이션 거래 시스템 설계

이렇게 하면 실제로 거래할 때 거래소 객체를 구성할 때 API 키 문자열을 작성할 수 있습니다. 이때, 주문하기 등의 작업은 실제로 거래소 인터페이스에 접근하지 않고, 시뮬레이션 시스템의 자산, 주문, 포지션 및 기타 데이터를 이용하여 시뮬레이션을 진행합니다. 하지만 시장 상황은 거래소의 실제 시장 상황입니다.

확장 및 최적화 방향

전략 개발에 있어 시뮬레이션 시스템의 가치 PaperTrader는 실제 시장과 매우 ​​유사한 테스트 환경을 제공하여 개발자가 어떠한 위험도 없이 전략의 실행 동작, 주문 논리, 매칭 성과 및 자본 변경을 검증할 수 있도록 합니다. 특히 다음 시나리오에 적합합니다.

  • 다중 전략 디버깅 동시 테스트
  • 다양한 시장 상황에서 전략의 성과를 빠르게 검증합니다.
  • 손실을 피하기 위해 디버깅 중 실시간 직접 주문을 피하십시오.
  • 일부 기존 역사적 백테스팅 검증 방법을 대체합니다.

순수 백테스팅과의 차이점

기존의 백테스팅은 과거 데이터를 기반으로 하며 보류 주문, 부분 거래, 매칭 슬리피지, 수수료 구조와 같은 실제 거래 세부 정보를 무시하고 K x K 방식으로 실행합니다. 시뮬레이션 시스템:

  • 실시간 견적을 사용하세요(정적인 과거 데이터가 아님)
  • 실제 주문 라이프사이클을 시뮬레이션합니다(생성 → 주문 → 매칭 → 실행 → 취소)
  • 수수료, 슬리피지, 평균 거래 가격을 정확하게 계산합니다.
  • “전략 모델”보다는 “전략 행동”을 테스트하는 데 더 능숙합니다.
  • 실시간 배포 간의 브리지 역할

PaperTrader에 대한 참고 사항 위의 PaperTrader는 단지 예비적인 설계일 뿐이며(단지 예비적인 코드 검토와 테스트만 수행됨), 설계 아이디어와 솔루션 참고 자료를 제공하는 것이 목표입니다. 또한 PaperTrader는 매칭 로직, 주문 시스템, 포지션 시스템, 자본 시스템 및 기타 설계가 합리적인지 확인하기 위해 테스트가 필요합니다. 시간적 제약으로 인해 현물 거래만 비교적 완전하게 구현되었으며, 선물 계약의 일부 기능은 아직 구현되지 않은 상태입니다.

잠재적으로 발생할 수 있는 문제:

  • 부동 소수점 계산 오류.
  • 논리적 처리 경계.
  • 배송 계약 지원은 더 복잡합니다
  • 청산 메커니즘의 설계는 더 복잡하다

다음 진화 방향

PaperTrader의 애플리케이션 가치를 더욱 향상시키기 위해 다음 단계의 확장을 위해 다음과 같은 방향을 고려할 수 있습니다.

  • 계약 시뮬레이션에 대한 지원을 개선합니다(코드에서 아직 완료되지 않은 부분).
  • 계약 포지션 및 레버리지 펀드 관리(포지션별, 전체 포지션)를 지원합니다.
  • 변동손익 계산 및 강제청산 메커니즘을 소개합니다.

PaperTrader를 통해 우리는 전략에 대한 보다 안전한 테스트 환경을 제공할 수 있을 뿐만 아니라, “연구 모델”에서 “실제 생산성”으로 이어지는 전략의 핵심적 연결 고리를 더욱 촉진할 수 있습니다.

독자 여러분께서 메시지를 남겨 주시면 감사하겠습니다.