Type/to search
8
Follow
1369
Followers
基于FMZ量化平台的真实行情驱动仿真交易系统设计
Discussions
Created 2025-04-30 16:59:01  Updated 2025-04-30 17:53:06
 1
 1351

img

前言

本文介绍了一个基于FMZ量化平台、以真实行情驱动的仿真交易系统 —— PaperTrader 的设计与实现。系统通过实时深度行情进行订单撮合,完整模拟策略下单、成交、资产变化及手续费处理等交易流程,支持市价/限价单、资产冻结与撤销归档,适用于策略测试与实盘前的真实行为验证。本文将从系统架构、撮合机制、接口兼容性等角度详解其设计理念与关键实现,并提供完整的实战演示用例,助力量化策略在上线前构建安全可靠的“中间沙盒”。

PaperTrader需求分析与设计

需求痛点:

  • 交易所模拟盘,行情混乱,不真实。
  • 交易所模拟盘仿真账户申请繁琐,测试资金获取繁琐。
  • 较多交易所未提供测试环境。

回测与实盘之间的“灰色地带”问题

为什么需要仿真交易系统?

在量化策略开发的全过程中,我们通常经历“历史回测 → 环境测试 → 实盘交易”这样的步骤。然而,历史回测应用的是统计数据,无法处理策略在实际行情下的应用效果。而实盘交易就意味着资金飞行,缺乏中间测试环境就成了我们探索的痛点。
为了解决这个问题,我们需求设计一个轻量级仿真交易系统——PaperTrader,它能够使用实时行情(深度、市价)条件,模拟下单、挂单、成交、撤单、资产变化、手续费扣除等完整交易流程,最终完成接近实盘级别的策略验证。

设计目标与关键特性

    1. 实时行情驱动
      使用 FMZ 接入的真实交易所行情,包括 GetTicker() 、 GetDepth() 等几十种接口。
    1. 仿真下单与扣费
      支持限价/市价下单,实现 maker/taker 手续费分别扣除,对于市价买单采用实际合算输入额。
    1. 资产/仓位/订单管理
      支持挂单时资产冻结,撤单时返还,支持基于 symbol 级别的多个资产/仓位/订单 维护。
    1. 完整订单周期
      订单从新建、等待、成交到撤单,均有清晰状态管理,成交後自动归档到本地数据库,支持以后查询和分析。
    1. 使用体验
      策略无需更改任何下单调用,直接将 exchange对象替换为 PaperTrader 就可以实现仿真交易模拟。

类库设计概览

系统主要由三部分构成:

  • 【PaperTrader 类】:
    核心仿真账户,内部包括资产、订单、仓位、行情和配置等数据维护

  • 【simEngine 撮合引擎】:
    背景线程,按照行情深度扫描当前订单,执行操作

  • 【数据库归档】:
    将已成交/撤单订单写入本地数据库,便于以后分析和复盘

撮合引擎设计

simEngine(data, lock) 是整个仿真系统的核心,它按照实际行情深度数据,对当前挂单进行循环撮合,为成交提供精确仿真结果。

主要流程包括:

  • 获取当前挂单、仓位、资产、行情;
  • 获取所有被用到的 symbol 的深度 GetDepth;
  • 遍历挂单,根据方向(buy/sell)选择操作 depth (asks/bids);
  • 根据价格是否符合操作条件,判定是否成交;
  • 如成交,更新并统计 AvgPrice / DealAmount 等订单信息;
  • 根据 maker/taker 分别扣除手续费;
  • 如已全部成交,订单归档;否则保留为 pending;

接口信息兼容

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()其它设置

这样设计可以让策略逻辑在无需修改的情况下,直接运行在仿真交易环境中。通过一键替换 exchange 为 PaperTrader,就可以将策略迁移到回测 + 实盘之间的“中间层”。

PaperTrader设计源码

javascript
class PaperTrader { constructor(exIdx, realExchange, assets, fee) { this.exIdx = exIdx this.e = realExchange this.name = realExchange.GetName() + "_PaperTrader" this.currency = realExchange.GetCurrency() this.baseCurrency = this.currency.split("_")[0] this.quoteCurrency = this.currency.split("_")[1] this.period = realExchange.GetPeriod() this.fee = fee // 数据同步锁 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函数就是测试函数:

img

这样实盘的时候可以在配置交易所对象的时候随便写一个API KEY字符串,这个时候下单等操作就不会真的去访问交易所接口,而是会使用这个仿真系统的资产、订单、持仓等数据进行模拟。但是行情却是交易所的真实行情。

扩展与优化方向

仿真系统在策略开发中的价值
PaperTrader 提供了一个高度贴近实盘的测试环境,让开发者能够在无风险的前提下,验证策略的执行行为、下单逻辑、撮合表现和资金变化。它尤其适用于以下场景:

  • 多策略调试并发测试
  • 快速验证策略在不同行情下的表现
  • 调试过程中避免直接实盘下单造成损失
  • 替代部分传统历史回测的验证手段

与纯回测的区别

传统回测基于历史数据逐 K 运行,忽略了订单挂单、部分成交、撮合滑点、手续费结构等真实交易细节。而仿真系统:

  • 使用实时行情(非静态历史数据)
  • 模拟真实订单生命周期(新建 → 挂单 → 撮合 → 成交 → 撤销)
  • 精确计算手续费、滑点、成交均价
  • 能更好地测试“策略行为”而不仅是“策略模型”
  • 与实盘部署之间的桥梁作用

关于PaperTrader的说明
以上 PaperTrader 还只是一个初步设计(只做了初步的代码审核、测试),目标是给与一种设计思路、方案参考。PaperTrader还需要经过测试检查撮合逻辑、订单系统、持仓系统、资金系统等设计是否合理。由于时间紧迫仅仅对于现货交易做了比较完善的实现,期货合约部分功能还处于to do状态。

可能的潜在问题:

  • 浮点数计算误差。
  • 逻辑处理边界。
  • 对于交割合约支持会比较复杂
  • 对于爆仓机制设计会比较复杂

下一步的演进方向

为进一步提升 PaperTrader 的应用价值,下一阶段可以考虑拓展以下方向:

  • 完善对于合约仿真的支持(代码中 to do 未完成部分)。
  • 支持合约仓位与杠杆资金管理(逐仓、全仓)。
  • 引入浮动盈亏计算与强平机制。

通过 PaperTrader,我们不仅能为策略提供更安全的测试环境,也进一步推动了策略从「研究模型」走向「真实生产力」的关键一环。

欢迎读者留言,感谢您的阅读。

Comment
All comments (1)

    牛逼啊

    a year ago
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)