Design of Real Ticker Driven Simulation Trading System Based on FMZ Quant Trading Platform
Preface
This article introduces the design and implementation of PaperTrader, a simulation trading system based on the FMZ Quant platform and driven by real ticker conditions. The system matches orders through real-time deep ticker conditions, fully simulates the trading process of strategy order placement, transaction, asset change and handling fee, supports market/limit orders, asset freezing and revocation archiving, and is suitable for strategy testing and real behavior verification before live trading. This article will explain its design concept and key implementation from the perspectives of system architecture, matching mechanism, interface compatibility, etc., and provide a complete practical demonstration case to help quantitative strategies build a safe and reliable "intermediate sandbox" before going online.
PaperTrader Demand Analysis and Design
Demand:
- The exchange simulation trading is chaotic and unrealistic.
- It is cumbersome to apply for a simulation account for the exchange simulation trading, and it is cumbersome to obtain test funds.
- Many exchanges do not provide a test environment.
The "Gray Area" Problem Between Backtesting and Live Trading
Why do we need a simulated trading system?
In the whole process of quantitative strategy development, we usually go through the steps of "historical backtesting → environmental testing → live trading". However, historical backtesting uses statistical data and cannot handle the application effect of the strategy under the actual tickers. Live trading means capital flight, and the lack of an intermediate testing environment has become a disadvantage for our exploration.
In order to solve this problem, we need to design a lightweight simulated trading system - PaperTrader, which can use real-time ticker conditions (depth, market price) to simulate the complete trading process of placing orders, pending orders, transactions, order withdrawals, asset changes, and handling fee deductions, and finally complete strategy verification close to the live trading.
Design Goals and Key Features
- Real-time ticker driven
Use FMZ to access real exchange ticker information, including GetTicker(), GetDepth() and dozens of other interfaces. - Simulation order placement and fee deduction
It supports limit/market orders, realize separate deduction of maker/taker fees, and use the actual calculated input amount for market buy orders. - Asset/position/order management
It supports freezing of assets when placing orders, returning assets when canceling orders, and supports maintenance of multiple assets/positions/orders based on symbol level. - Complete order period
Orders are clearly managed from creation, waiting, execution to cancellation. After execution, they are archived to the local database automatically to support future query and analysis. - User experience
The strategy does not need to change any order calls, and can achieve simulated trading simulation by simply replacing the exchange object with PaperTrader.
Overview of Class Library Design
The system mainly consists of three parts:
[PaperTrader class]:
Core simulation account, including data maintenance such as assets, orders, positions, tickers and configurations[simEngine matching engine]:
Background thread, scans current orders according to ticker depth and performs operations[Database archiving]: Write completed/cancelled orders to the local database for later analysis and review
Matching engine design:
simEngine(data, lock) is the core of the entire simulation system. It matches the current pending orders in a loop according to the actual ticker depth data to provide accurate simulation results for transactions.
The main processes include:
- Get the current pending orders, positions, assets, and tickers;
- Get the depth of all used symbols GetDepth;
- Traverse the pending orders and select the operation depth (asks/bids) according to the direction (buy/sell);
- Determine whether the transaction is completed based on whether the price meets the operation conditions;
- If the transaction is completed, update and count the order information such as AvgPrice/DealAmount;
- Deduct the handling fee according to the maker/taker;
- If all transactions have been completed, the order will be archived; otherwise, it will be kept as pending;
Interface information compatible:
PaperTrader is designed to align with the real trading interface of the FMZ platform as much as possible, including but not limited to:
Classification | Interface | Description |
---|---|---|
Order interface | Buy(price, amount) / Sell(price, amount) / CreateOrder(symbol, side, price, amount) | Order operation |
Market interface | GetTicker() / GetDepth() / GetRecords() / GetTrades() | Request the real ticker price of the exchange directly |
Order interface | GetOrders() / CancelOrder(id) / GetOrder(id) | For order operations |
Account and position interface | GetAccount() / GetAssets() / GetPositions() | For account operations |
Other settings interface | SetCurrency() / SetDirection() | Other settings |
This design allows the strategy logic to run directly in the simulated trading environment without modification. By replacing exchange with PaperTrader with one click, the strategy can be migrated to the "middle layer" between backtesting and live trading.
PaperTrader Design Source Code
class PaperTrader {
constructor(exIdx, realExchange, assets, fee) {
this.exIdx = exIdx
this.e = realExchange
this.name = realExchange.GetName() + "_PaperTrader"
this.currency = realExchange.GetCurrency()
this.baseCurrency = this.currency.split("_")[0]
this.quoteCurrency = this.currency.split("_")[1]
this.period = realExchange.GetPeriod()
this.fee = fee
// Data synchronization lock
this.data = threading.Dict()
this.dataLock = threading.Lock()
// Initialization of this.data
this.data.set("assets", assets)
this.data.set("orders", [])
this.data.set("positions", [])
// exchangeData
let exchangeData = {
"exIdx": this.exIdx,
"fee": this.fee
}
// exchange Type
if (this.name.includes("Futures_")) {
this.exchangeType = "Futures"
this.direction = "buy"
this.marginLevel = 10
this.contractType = "swap"
this.e.SetContractType(this.contractType)
// set exchangeData
exchangeData["exchangeType"] = this.exchangeType
exchangeData["marginLevel"] = this.marginLevel
} else {
this.exchangeType = "Spot"
// set exchangeData
exchangeData["exchangeType"] = this.exchangeType
}
// Records information related to the exchange for transmission to the matching engine
this.data.set("exchangeData", exchangeData)
// database
this.historyOrdersTblName = "HISTORY_ORDER"
this.data.set("historyOrdersTblName", this.historyOrdersTblName)
// init
this.init()
}
// export
SetCurrency(currency) {
let arrCurrency = currency.split("_")
if (arrCurrency.length != 2) {
this.e.Log(3, null, null, `invalid currency: ${currency}`)
return
}
this.currency = currency
this.baseCurrency = arrCurrency[0]
this.quoteCurrency = arrCurrency[1]
return this.e.SetCurrency(currency)
}
SetContractType(contractType) {
if (this.exchangeType == "Spot") {
this.e.Log(3, null, null, `not support`)
return
}
if (!this.isValidContractType(contractType)) {
this.e.Log(3, null, null, `invalid contractType: ${contractType}`)
return
}
this.contractType = contractType
return this.e.SetContractType(contractType)
}
SetDirection(direction) {
if (this.exchangeType == "Spot") {
this.e.Log(3, null, null, `not support`)
return
}
if (direction != "buy" && direction != "sell" && direction != "closebuy" && direction != "closesell") {
this.e.Log(3, null, null, `invalid direction: ${direction}`)
return
}
this.direction = direction
return this.e.SetDirection(direction)
}
GetTicker(...args) {
return this.e.GetTicker(...args)
}
GetDepth(...args) {
return this.e.GetDepth(...args)
}
GetTrades(...args) {
return this.e.GetTrades(...args)
}
GetRecords(...args) {
return this.e.GetRecords(...args)
}
GetMarkets() {
return this.e.GetMarkets()
}
GetTickers() {
return this.e.GetTickers()
}
GetFundings(...args) {
if (this.exchangeType == "Spot") {
this.e.Log(3, null, null, `not support`)
return
}
return this.e.GetFundings(...args)
}
GetAccount() {
let assets = this.data.get("assets")
let acc = {"Balance": 0, "FrozenBalance": 0, "Stocks": 0, "FrozenStocks": 0}
for (let asset of assets) {
if (this.exchangeType == "Futures") {
if (this.quoteCurrency == "USDT" || this.quoteCurrency == "USDC") {
if (asset["Currency"] == this.quoteCurrency) {
return {"Balance": asset["Amount"], "FrozenBalance": asset["FrozenAmount"], "Stocks": 0, "FrozenStocks": 0}
}
} else if (this.quoteCurrency == "USD") {
if (asset["Currency"] == this.baseCurrency) {
return {"Balance": 0, "FrozenBalance": 0, "Stocks": asset["Amount"], "FrozenStocks": asset["FrozenAmount"]}
}
}
} else if (this.exchangeType == "Spot") {
if (asset["Currency"] == this.baseCurrency) {
// Stocks
acc["Stocks"] = asset["Amount"]
acc["FrozenStocks"] = asset["FrozenAmount"]
} else if (asset["Currency"] == this.quoteCurrency) {
// Balance
acc["Balance"] = asset["Amount"]
acc["FrozenBalance"] = asset["FrozenAmount"]
}
}
}
return acc
}
GetAssets() {
let assets = this.data.get("assets")
return assets
}
GetOrders(symbol) {
let ret = []
let orders = this.data.get("orders")
if (this.exchangeType == "Spot") {
if (typeof(symbol) == "undefined") {
return orders
} else {
let arrCurrency = symbol.split("_")
if (arrCurrency.length != 2) {
this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
return null
}
for (let o of orders) {
if (o.Symbol == symbol) {
ret.push(o)
}
}
return ret
}
} else if (this.exchangeType == "Futures") {
if (typeof(symbol) == "undefined") {
for (let o of orders) {
if (o.Symbol.includes(`${this.quoteCurrency}.${this.contractType}`)) {
ret.push(o)
}
}
return ret
} else {
let arr = symbol.split(".")
if (arr.length != 2) {
this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
return null
}
let currency = arr[0]
let contractType = arr[1]
let arrCurrency = currency.split("_")
if (arrCurrency.length != 2) {
for (let o of orders) {
if (o.Symbol.includes(`${arrCurrency[0]}.${contractType}`)) {
ret.push(o)
}
}
} else {
for (let o of orders) {
if (o.Symbol == symbol) {
ret.push(o)
}
}
}
return ret
}
} else {
this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
return null
}
}
GetOrder(orderId) {
let data = DBExec(`SELECT ORDERDATA FROM ${this.historyOrdersTblName} WHERE ID = ?`, orderId)
// {"columns":["ORDERDATA"],"values":[]}
if (!data) {
this.e.Log(3, null, null, `Order not found: ${orderId}`)
return null
}
if (data && Array.isArray(data["values"]) && data["values"].length <= 0) {
this.e.Log(3, null, null, `Order not found: ${orderId}`)
return null
} else if (data["values"].length != 1) {
this.e.Log(3, null, null, `invalid data: ${data["values"]}`)
return null
} else {
let ret = this.parseJSON(data["values"][0])
if (!ret) {
this.e.Log(3, null, null, `invalid data: ${data["values"]}`)
return null
}
return ret
}
}
Buy(price, amount) {
return this.trade("Buy", price, amount)
}
Sell(price, amount) {
return this.trade("Sell", price, amount)
}
trade(tradeType, price, amount) {
if (this.exchangeType == "Spot") {
let side = ""
if (tradeType == "Buy") {
side = "buy"
} else if (tradeType == "Sell") {
side = "sell"
} else {
this.e.Log(3, null, null, `invalid tradeType: ${tradeType}`)
return null
}
let symbol = this.currency
return this.createOrder(symbol, side, price, amount)
} else if (this.exchangeType == "Futures") {
let compose = `${tradeType}_${this.direction}`
if (compose != "Sell_closebuy" && compose != "Sell_sell" && compose != "Buy_buy" && compose != "Buy_closesell") {
this.e.Log(3, null, null, `${tradeType}, invalid direction: ${this.direction}`)
return null
}
let side = this.direction
let symbol = `${this.currency}.${this.contractType}`
return this.createOrder(symbol, side, price, amount)
} else {
this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
return
}
}
CreateOrder(symbol, side, price, amount) {
if (side != "buy" && side != "sell" && side != "closebuy" && side != "closesell") {
this.e.Log(3, null, null, `invalid direction: ${side}`)
return null
}
if (this.exchangeType == "Spot") {
if (side == "closebuy") {
side = "sell"
} else if (side == "closesell") {
side = "buy"
}
}
return this.createOrder(symbol, side, price, amount)
}
createOrder(symbol, side, price, amount) {
this.dataLock.acquire()
let isError = false
let orders = this.data.get("orders")
let positions = this.data.get("positions")
let assets = this.data.get("assets")
// Check amount
if (amount <= 0) {
this.e.Log(3, null, null, `invalid amount: ${amount}`)
return null
}
// Constructing orders
let order = {
"Info": null,
"Symbol": symbol,
"Price": price,
"Amount": amount,
"DealAmount": 0,
"AvgPrice": 0,
"Status": ORDER_STATE_PENDING,
"ContractType": symbol.split(".").length == 2 ? symbol.split(".")[1] : ""
}
let logType = null
switch (side) {
case "buy":
order["Type"] = ORDER_TYPE_BUY
order["Offset"] = ORDER_OFFSET_OPEN
logType = LOG_TYPE_BUY
break
case "sell":
order["Type"] = ORDER_TYPE_SELL
order["Offset"] = ORDER_OFFSET_OPEN
logType = LOG_TYPE_SELL
break
case "closebuy":
order["Type"] = ORDER_TYPE_SELL
order["Offset"] = ORDER_OFFSET_CLOSE
logType = LOG_TYPE_SELL
break
case "closesell":
order["Type"] = ORDER_TYPE_BUY
order["Offset"] = ORDER_OFFSET_CLOSE
logType = LOG_TYPE_BUY
break
default:
this.e.Log(3, null, null, `invalid direction: ${side}`)
isError = true
}
if (isError) {
return null
}
// Check assets/positions, report an error if assets/positions are insufficient
let needAssetName = ""
let needAsset = 0
if (this.exchangeType == "Futures") {
// Check assets and positions
// to do
} else if (this.exchangeType == "Spot") {
// Check assets
let arr = symbol.split(".")
if (arr.length == 2) {
this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
return null
}
let currency = arr[0]
let arrCurrency = currency.split("_")
if (arrCurrency.length != 2) {
this.e.Log(3, null, null, `invalid symbol: ${symbol}`)
return null
}
let baseCurrency = arrCurrency[0]
let quoteCurrency = arrCurrency[1]
needAssetName = side == "buy" ? quoteCurrency : baseCurrency
if (side == "buy" && price <= 0) {
// market order of buy, amount is quantity by quoteCurrency
needAsset = amount
} else {
// limit order, amount is quantity by baseCurrency
needAsset = side == "buy" ? price * amount : amount
}
let canPostOrder = false
for (let asset of assets) {
if (asset["Currency"] == needAssetName && asset["Amount"] >= needAsset) {
canPostOrder = true
}
}
if (!canPostOrder) {
this.e.Log(3, null, null, `insufficient balance for ${needAssetName}, need: ${needAsset}, Account: ${JSON.stringify(assets)}`)
return null
}
} else {
this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
return null
}
// Generate order ID, UnixNano() uses nanosecond timestamp
let orderId = this.generateOrderId(symbol, UnixNano())
order["Id"] = orderId
// Update pending order records
orders.push(order)
this.data.set("orders", orders)
// Output logging
if (this.exchangeType == "Futures") {
this.e.SetDirection(side)
}
this.e.Log(logType, price, amount, `orderId: ${orderId}`)
// Update assets
for (let asset of assets) {
if (asset["Currency"] == needAssetName) {
asset["Amount"] -= needAsset
asset["FrozenAmount"] += needAsset
}
}
this.data.set("assets", assets)
this.dataLock.release()
return orderId
}
CancelOrder(orderId) {
this.dataLock.acquire()
let orders = this.data.get("orders")
let assets = this.data.get("assets")
let positions = this.data.get("positions")
let targetIdx = orders.findIndex(item => item.Id == orderId)
if (targetIdx != -1) {
// Target order
let targetOrder = orders[targetIdx]
// Update assets
if (this.exchangeType == "Futures") {
// Contract exchange asset update
// to do
} else if (this.exchangeType == "Spot") {
let arrCurrency = targetOrder.Symbol.split("_")
let baseCurrency = arrCurrency[0]
let quoteCurrency = arrCurrency[1]
let needAsset = 0
let needAssetName = ""
if (targetOrder.Type == ORDER_TYPE_BUY && targetOrder.Price <= 0) {
needAssetName = quoteCurrency
needAsset = targetOrder.Amount - targetOrder.DealAmount
} else {
needAssetName = targetOrder.Type == ORDER_TYPE_BUY ? quoteCurrency : baseCurrency
needAsset = targetOrder.Type == ORDER_TYPE_BUY ? targetOrder.Price * (targetOrder.Amount - targetOrder.DealAmount) : (targetOrder.Amount - targetOrder.DealAmount)
}
for (let asset of assets) {
if (asset["Currency"] == needAssetName) {
asset["FrozenAmount"] -= needAsset
asset["Amount"] += needAsset
}
}
// Update assets
this.data.set("assets", assets)
} else {
this.e.Log(3, null, null, `invalid exchangeType: ${this.exchangeType}`)
return false
}
// Update revocation status
orders.splice(targetIdx, 1)
targetOrder.Status = ORDER_STATE_CANCELED
// Archive, write to database
let strSql = [
`INSERT INTO ${this.historyOrdersTblName} (ID, ORDERDATA)`,
`VALUES ('${targetOrder.Id}', '${JSON.stringify(targetOrder)}');`
].join("")
let ret = DBExec(strSql)
if (!ret) {
e.Log(3, null, null, `Order matched successfully, but failed to archive to database: ${JSON.stringify(o)}`)
}
} else {
// Failed to cancel the order
this.e.Log(3, null, null, `Order not found: ${orderId}`)
this.dataLock.release()
return false
}
this.data.set("orders", orders)
this.e.Log(LOG_TYPE_CANCEL, orderId)
this.dataLock.release()
return true
}
GetHistoryOrders(symbol, since, limit) {
// Query historical orders
// to do
}
SetMarginLevel(symbol) {
// Set leverage value
// Synchronize this.marginLevel and exchangeData["marginLevel"] in this.data
// to do
}
GetPositions(symbol) {
// Query positions
// to do
/*
if (this.exchangeType == "Spot") {
this.e.Log(3, null, null, `not support`)
return
}
let pos = this.data.get("positions")
*/
}
// engine
simEngine(data, lock) {
while (true) {
lock.acquire()
// get orders / positions / assets / exchangeData
let orders = data.get("orders")
let positions = data.get("positions")
let assets = data.get("assets")
let exchangeData = data.get("exchangeData")
let historyOrdersTblName = data.get("historyOrdersTblName")
// get exchange idx and fee
let exIdx = exchangeData["exIdx"]
let fee = exchangeData["fee"]
let e = exchanges[exIdx]
// get exchangeType
let exchangeType = exchangeData["exchangeType"]
let marginLevel = 0
if (exchangeType == "Futures") {
marginLevel = exchangeData["marginLevel"]
}
// get Depth
let dictTick = {}
for (let order of orders) {
dictTick[order.Symbol] = {}
}
for (let position of positions) {
dictTick[position.Symbol] = {}
}
// Update tickers
for (let symbol in dictTick) {
dictTick[symbol] = e.GetDepth(symbol)
}
// Matchmaking
let newPendingOrders = []
for (let o of orders) {
// Only pending orders are processed
if (o.Status != ORDER_STATE_PENDING) {
continue
}
// No data in the market
let depth = dictTick[o.Symbol]
if (!depth) {
e.Log(3, null, null, `Order canceled due to invalid order book data: ${JSON.stringify(o)}`)
continue
}
// Determine the order book matching direction based on the order direction
let matchSide = o.Type == ORDER_TYPE_BUY ? depth.Asks : depth.Bids
if (!matchSide || matchSide.length == 0) {
e.Log(3, null, null, `Order canceled due to invalid order book data: ${JSON.stringify(o)}`)
continue
}
let remain = o.Amount - o.DealAmount
let filledValue = 0
let filledAmount = 0
for (let level of matchSide) {
let levelAmount = level.Amount
let levelPrice = level.Price
if ((o.Price > 0 && ((o.Type == ORDER_TYPE_BUY && o.Price >= levelPrice) || (o.Type == ORDER_TYPE_SELL && o.Price <= levelPrice))) || o.Price <= 0) {
if (exchangeType == "Spot" && o.Type == ORDER_TYPE_BUY && o.Price <= 0) {
// Spot market buy order
let currentFilledQty = Math.min(levelAmount * levelPrice, remain)
remain -= currentFilledQty
filledValue += currentFilledQty
filledAmount += currentFilledQty / levelPrice
} else {
// Limit order, the price is matched; market order, direct market matching
let currentFilledAmount = Math.min(levelAmount, remain)
remain -= currentFilledAmount
filledValue += currentFilledAmount * levelPrice
filledAmount += currentFilledAmount
}
// Initial judgment, if matched directly, it is judged as taker
if (typeof(o.isMaker) == "undefined") {
o.isMaker = false
}
} else {
// The price does not meet the matching criteria, and is initially judged as a maker.
if (typeof(o.isMaker) == "undefined") {
o.isMaker = true
}
break
}
if (remain <= 0) {
// Order completed
break
}
}
// Changes in order
if (filledAmount > 0) {
// Update order changes
if (exchangeType == "Spot" && o.Type == ORDER_TYPE_BUY && o.Price <= 0) {
if (o.AvgPrice == 0) {
o.AvgPrice = filledValue / filledAmount
o.DealAmount += filledValue
} else {
o.AvgPrice = (o.DealAmount + filledValue) / (filledAmount + o.DealAmount / o.AvgPrice)
o.DealAmount += filledValue
}
} else {
o.AvgPrice = (o.DealAmount * o.AvgPrice + filledValue) / (filledAmount + o.DealAmount)
o.DealAmount += filledAmount
}
// Handling position updates
if (exchangeType == "Futures") {
// Futures, find the position in the corresponding order direction, update
// to do
/*
if () {
// Find the corresponding position and update it
} else {
// There is no corresponding position, create a new one
let pos = {
"Info": null,
"Symbol": o.Symbol,
"MarginLevel": marginLevel,
"Amount": o.Amount,
"FrozenAmount": 0,
"Price": o.Price,
"Profit": 0,
"Type": o.Type == ORDER_TYPE_BUY ? PD_LONG : PD_SHORT,
"ContractType": o.Symbol.split(".")[1],
"Margin": o.Amount * o.Price / marginLevel // to do USDT/USD contract Multiplier
}
positions.push(pos)
}
*/
}
// Handling asset updates
if (exchangeType == "Futures") {
// Processing futures asset updates
// to do
} else if (exchangeType == "Spot") {
// Handling spot asset updates
let arrCurrency = o.Symbol.split("_")
let baseCurrency = arrCurrency[0]
let quoteCurrency = arrCurrency[1]
let minusAssetName = o.Type == ORDER_TYPE_BUY ? quoteCurrency : baseCurrency
let minusAsset = o.Type == ORDER_TYPE_BUY ? filledValue : filledAmount
let plusAssetName = o.Type == ORDER_TYPE_BUY ? baseCurrency : quoteCurrency
let plusAsset = o.Type == ORDER_TYPE_BUY ? filledAmount : filledValue
// Deduction of handling fee
if (o.isMaker) {
plusAsset = (1 - fee["maker"]) * plusAsset
} else {
plusAsset = (1 - fee["taker"]) * plusAsset
}
for (let asset of assets) {
if (asset["Currency"] == minusAssetName) {
// asset["FrozenAmount"] -= minusAsset
asset["FrozenAmount"] = Math.max(0, asset["FrozenAmount"] - minusAsset)
} else if (asset["Currency"] == plusAssetName) {
asset["Amount"] += plusAsset
}
}
}
}
// Check remain to update order status
if (remain <= 0) {
// Order completed, update order status, update average price, update completion amount
o.Status = ORDER_STATE_CLOSED
// Completed orders are archived and recorded in the database
let strSql = [
`INSERT INTO ${historyOrdersTblName} (ID, ORDERDATA)`,
`VALUES ('${o.Id}', '${JSON.stringify(o)}');`
].join("")
let ret = DBExec(strSql)
if (!ret) {
e.Log(3, null, null, `Order matched successfully, but failed to archive to database: ${JSON.stringify(o)}`)
}
} else {
newPendingOrders.push(o)
}
}
// Update current pending order data
data.set("orders", newPendingOrders)
data.set("assets", assets)
lock.release()
Sleep(1000)
}
}
// other
isValidContractType(contractType) {
// only support swap
let contractTypes = ["swap"]
if (contractTypes.includes(contractType)) {
return true
} else {
return false
}
}
generateOrderId(symbol, ts) {
let uuid = '', i, random
for (i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) {
uuid += '-'
} else if (i === 14) {
// Fixed to 4
uuid += '4'
} else if (i === 19) {
// The upper 2 bits are fixed to 10
random = (Math.random() * 16) | 0
uuid += ((random & 0x3) | 0x8).toString(16)
} else {
random = (Math.random() * 16) | 0
uuid += random.toString(16)
}
}
return `${symbol},${uuid}-${ts}`
}
parseJSON(strData) {
let ret = null
try {
ret = JSON.parse(strData)
} catch (err) {
Log("err.name:", err.name, ", err.stack:", err.stack, ", err.message:", err.message, ", strData:", strData)
}
return ret
}
init() {
threading.Thread(this.simEngine, this.data, this.dataLock)
// Delete the database, historical order table
DBExec(`DROP TABLE IF EXISTS ${this.historyOrdersTblName};`)
// Rebuild the historical order table
let strSql = [
`CREATE TABLE IF NOT EXISTS ${this.historyOrdersTblName} (`,
"ID VARCHAR(255) NOT NULL PRIMARY KEY,",
"ORDERDATA TEXT NOT NULL",
")"
].join("");
DBExec(strSql)
}
}
// extport
$.CreatePaperTrader = function(exIdx, realExchange, assets, fee) {
return new PaperTrader(exIdx, realExchange, assets, fee)
}
// Use real ticker conditions to create efficient Paper Trader
function main() {
// create PaperTrader
let simulateAssets = [{"Currency": "USDT", "Amount": 10000, "FrozenAmount": 0}]
let fee = {"taker": 0.001, "maker": 0.0005}
paperTraderEx = $.CreatePaperTrader(0, exchange, simulateAssets, fee)
Log(paperTraderEx)
// test GetTicker
Log("GetTicker:", paperTraderEx.GetTicker())
// test GetOrders
Log("GetOrders:", paperTraderEx.GetOrders())
// test Buy/Sell
let orderId = paperTraderEx.Buy(-1, 0.1)
Log("orderId:", orderId)
// test GetOrder
Sleep(1000)
Log(paperTraderEx.GetOrder(orderId))
Sleep(6000)
}
Practical Demonstration and Test Cases
Live Trading
The above code can be saved as a "template library" of the FMZ platform. The main
function in this template library is the test function:
In this way, we can write an API KEY string when configuring the exchange object. At this time, operations such as placing orders will not access the exchange interface, it will use the assets, orders, positions and other data of the simulation system for simulation. However, the ticker conditions are the real ticker conditions of the exchange.
Expansion and Optimization Direction
The value of simulation systems in strategy development
PaperTrader provides a testing environment that is highly close to the live trading, allowing developers to verify the execution behavior, order logic, matching performance and fund changes of strategies without risk. It is particularly suitable for the following scenarios:
- Multi-strategy debugging and concurrent testing
- Quickly verify the performance of strategies under different ticker conditions
- Avoid losses caused by direct live trading orders during debugging
- Replace some traditional historical backtesting verification methods
Difference from pure backtesting
Traditional backtesting is based on historical data and runs K by K, ignoring real trading details such as pending orders, partial transactions, matching slippage, and handling fee structure. While the simulation system:
- Use real-time tickers (not static historical data)
- Simulate the real order life period (new → pending order → matching → transaction → cancellation)
- Calculate the handling fee, slippage, and average transaction price accurately
- Better test "strategy behavior" rather than just "strategy model"
- Serves as a bridge between live trading deployment
Notes on PaperTrader
The above PaperTrader is just a preliminary design (only preliminary code review and testing have been done), and the goal is to provide a design idea and solution reference. PaperTrader still needs to be tested to check whether the matching logic, order system, position system, capital system and other designs are reasonable. Due to time constraints, only a relatively complete implementation of spot trading has been made, and some functions of futures contracts are still in the to do state.
Possible potential problems:
- Floating point calculation error.
- Logical processing boundary.
- It will be more complicated to support delivery contracts
- It will be more complicated to design the liquidation mechanism
The next evolution direction
In order to further enhance the application value of PaperTrader, the following directions can be considered for expansion in the next stage:
- Improve support for contract simulation (unfinished part of to do in the code).
- Support contract position and leverage fund management (isolated position, crossed position).
- Introduce floating profit and loss calculation and forced liquidation mechanism.
Through PaperTrader, we can not only provide a safer testing environment for strategies, but also further promote the key link of strategies from "research models" to "real productivity".
Readers are welcome to leave comments, thank you for your reading.
From: Design of Real Ticker Driven Simulation Trading System Based on FMZ Quant Trading Platform