
Building A Multi-Account Walkthrough System That Supports MyLanguage and Pine Strategy Language Based on FMZ
Demand Scenarios of Walkthrough System "Why can strategies written in MyLanguage or Pine scripts always only control one account and one product?" The essence of this problem lies in the design positioning of the language itself. MyLanguage and Pine language are highly encapsulated scripting languages, and their underlying implementation is based on JavaScript. In order to allow users to quickly get started and focus on strategy logic, both have done a lot of encapsulation and abstraction at the language level, but this has also sacrificed a certain degree of flexibility: by default, only single-account, single-product strategy execution models are supported. When users want to run multiple accounts in live trading, they can only do so by running multiple Pine or MyLanguage live trading instances. This approach is acceptable when the number of accounts is small, but if multiple instances are deployed on the same docker, a large number of API requests will be generated, and the exchange may even restrict access due to excessive request frequency, bringing unnecessary live trading risks. So, is there a more elegant way to copy the trading behavior of other accounts automatically by just running a Pine or MyLanguage script? The answer is: Yes. This article will guide you through building a cross-account, cross-product walkthrough system from scratch, which is compatible with My and Pine language strategies. Through the Leader-Subscriber architecture, it will implement an efficient, stable, and scalable multi-account synchronous trading framework to solve the various disadvantages you encounter in live trading deployment. The program is designed and written in JavaScript, and the program architecture uses the Leader-Subscriber model. Strategy source code: /*backtest start: 2024-05-21 00:00:00 end: 2025-05-20 00:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_Binance","currency":"ETH_USDT"},{"eid":"Futures_Binance","currency":"ETH_USDT"},{"eid":"Futures_Binance","currency":"ETH_USDT","balance":10000}] args: [["isBacktest",true]] */ class Leader { constructor(leaderCfg = { type: "exchange", apiClient: exchanges[0] }) { // Account managed with trading signals configuration this.leaderCfg = leaderCfg // Cache the last position information for comparison this.lastPos = null // Record current position information this.currentPos = null // Record subscribers this.subscribers = [] // According to the leaderCfg configuration, determine which monitoring solution to use: 1. Monitor the account managed with trading signals directly. 2. Monitor the data of the live trading strategy of the account managed with trading signals through the FMZ extended API. 3. Walkthrough through the message push mechanism. The default solution is 1. // initialization let ex = this.leaderCfg.apiClient let currency = ex.GetCurrency() let arrCurrency = currency.split("_") if (arrCurrency.length !== 2) { throw new Error("The account managed with trading signals configuration is wrong, it must be two currency pairs") } this.baseCurrency = arrCurrency[0] this.quoteCurrency = arrCurrency[1] // Get the total equity of the account managed with trading signals at initialization this.initEquity = _C(ex.GetAccount).Equity this.currentPos = _C(ex.GetPositions) } // Monitoring leader logic poll() { // Get the exchange object let ex = this.leaderCfg.apiClient // Get the leader's position and account asset data let pos = ex.GetPositions() if (!pos) { return } this.currentPos = pos // Call the judgment method to determine the position changes let { hasChanged, diff } = this._hasChanged(pos) if (hasChanged) { Log("Leader position changes, current position:", pos) Log("Position changes:", diff) // Notify Subscribers this.subscribers.forEach(subscriber => { subscriber.applyPosChanges(diff) }) } // Synchronous positions this.subscribers.forEach(subscriber => { subscriber.syncPositions(pos) }) } // Determine whether the position has changed _hasChanged(pos) { if (this.lastPos) { // Used to store the results of position differences let diff = { added: [], // Newly added positions removed: [], // Removed positions updated: [] // Updated positions (amount or price changes) } // Convert the last position and the current position into a Map, with the key being `symbol + direction` and the value being the position object let lastPosMap = new Map(this.lastPos.map(p => [`${p.Symbol}|${p.Type}`, p])) let currentPosMap = new Map(pos.map(p => [`${p.Symbol}|${p.Type}`, p])) // Traverse the current positions and find new and updated positions for (let [key, current] of currentPosMap) { if (!lastPosMap.has(key)) { // If the key does not exist in the last position, it is a new position. diff.added.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount }) } else { // If it exists, check if the amount or price has changed let last = lastPosMap.get(key) if (current.Amount !== last.Amount) { diff.updated.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount - last.Amount }) } // Remove from lastPosMap, and what remains is the removed position lastPosMap.delete(key) } } // The remaining keys in lastPosMap are the removed positions. for (let [key, last] of lastPosMap) { diff.removed.push({ symbol: last.Symbol, type: last.Type, deltaAmount: -last.Amount }) } // Determine if there is a change let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0 // If there is a change, update lastPos if (hasChanged) { this.lastPos = pos } return { hasChanged: hasChanged, diff: diff } } else { // If there is no last position record, update the record and do not synchronize positions this.lastPos = pos return { hasChanged: false, diff: { added: [], removed: [], updated: [] } } /* Another solution: synchronize positions if (pos.length > 0) { let diff = { added: pos.map(p => ({symbol: p.Symbol, type: p.Type, deltaAmount: p.Amount})), removed: [], updated: [] } return {hasChanged: true, diff: diff} } else { return {hasChanged: false, diff: {added: [], removed: [], updated: []}} } */ } } // Subscriber registration subscribe(subscriber) { if (this.subscribers.indexOf(subscriber) === -1) { if (this.quoteCurrency !== subscriber.quoteCurrency) { throw new Error("Subscriber currency pair does not match, current leader currency pair: " + this.quoteCurrency + ", subscriber currency pair:" + subscriber.quoteCurrency) } if (subscriber.followStrategy.followMode === "equity_ratio") { // Set the ratio of account managed with trading signals let ex = this.leaderCfg.apiClient let equity = _C(ex.GetAccount).Equity subscriber.setEquityRatio(equity) } this.subscribers.push(subscriber) Log("Subscriber registration is successful, subscription configuration:", subscriber.getApiClientInfo()) } } // Unsubscribe unsubscribe(subscriber) { const index = this.subscribers.indexOf(subscriber) if (index !== -1) { this.subscribers.splice(index, 1) Log("Subscriber unregistration successful, subscription configuration:", subscriber.getApiClientInfo()) } else { Log("Subscriber unregistration failed, subscription configuration:", subscriber.getApiClientInfo()) } } // Get UI information fetchLeaderUI() { // Information of the trade order issuer let tblLeaderInfo = { "type": "table", "title": "Leader Info", "cols": ["order trade plan", "denominated currency", "number of walkthrough followers", "initial total equity"], "rows": [] } tblLeaderInfo.rows.push([this.leaderCfg.type, this.quoteCurrency, this.subscribers.length, this.initEquity]) // Construct the display information of the trade order issuer: position information let tblLeaderPos = { "type": "table", "title": "Leader pos", "cols": ["trading product", "direction", "amount", "price"], "rows": [] } this.currentPos.forEach(pos => { let row = [pos.Symbol, pos.Type == PD_LONG ? "long" : "short", pos.Amount, pos.Price] tblLeaderPos.rows.push(row) }) // Construct the display information of the subscriber let strFollowerMsg = "" this.subscribers.forEach(subscriber => { let arrTbl = subscriber.fetchFollowerUI() strFollowerMsg += "`" + JSON.stringify(arrTbl) + "`\n" }) return "`" + JSON.stringify([tblLeaderInfo, tblLeaderPos]) + "`\n" + strFollowerMsg } // Expand functions such as pausing walkthrough trading and removing subscriptions } class Subscriber { constructor(subscriberCfg, followStrategy = { followMode: "position_ratio", ratio: 1, maxReTries: 3 }) { this.subscriberCfg = subscriberCfg this.followStrategy = followStrategy // initialization let ex = this.subscriberCfg.apiClient let currency = ex.GetCurrency() let arrCurrency = currency.split("_") if (arrCurrency.length !== 2) { throw new Error("Subscriber configuration error, must be two currency pairs") } this.baseCurrency = arrCurrency[0] this.quoteCurrency = arrCurrency[1] // Initial acquisition of position data this.currentPos = _C(ex.GetPositions) } setEquityRatio(leaderEquity) { // {followMode: "equity_ratio"} Automatically follow orders based on account equity ratio if (this.followStrategy.followMode === "equity_ratio") { let ex = this.subscriberCfg.apiClient let equity = _C(ex.GetAccount).Equity let ratio = equity / leaderEquity this.followStrategy.ratio = ratio Log("Rights and interests of the trade order issuer:", leaderEquity, "Subscriber benefits:", equity) Log("Automatic setting, subscriber equity ratio:", ratio) } } // Get the API client information bound to the subscriber getApiClientInfo() { let ex = this.subscriberCfg.apiClient let idx = this.subscriberCfg.clientIdx if (ex) { return { exName: ex.GetName(), exLabel: ex.GetLabel(), exIdx: idx, followStrategy: this.followStrategy } } else { throw new Error("The subscriber is not bound to the API client") } } // Returns the transaction direction parameters according to the position type and position changes getTradeSide(type, deltaAmount) { if (type == PD_LONG && deltaAmount > 0) { return "buy" } else if (type == PD_LONG && deltaAmount < 0) { return "closebuy" } else if (type == PD_SHORT && deltaAmount > 0) { return "sell" } else if (type == PD_SHORT && deltaAmount < 0) { return "closesell" } return null } getSymbolPosAmount(symbol, type) { let ex = this.subscriberCfg.apiClient if (ex) { let pos = _C(ex.GetPositions, symbol) if (pos.length > 0) { // Traverse the positions and find the corresponding symbol and type for (let i = 0; i < pos.length; i++) { if (pos[i].Symbol === symbol && pos[i].Type === type) { return pos[i].Amount } } } return 0 } else { throw new Error("The subscriber is not bound to the API client") } } // Retry order tryCreateOrder(ex, symbol, side, price, amount, label, maxReTries) { for (let i = 0; i < Math.max(maxReTries, 1); i++) { let orderId = ex.CreateOrder(symbol, side, price, amount, label) if (orderId) { return orderId } Sleep(1000) } return null } // Synchronous position changes applyPosChanges(diff) { let ex = this.subscriberCfg.apiClient if (ex) { ["added", "removed", "updated"].forEach(key => { diff[key].forEach(item => { let side = this.getTradeSide(item.type, item.deltaAmount) if (side) { // Calculate the walkthrough trading ratio let ratio = this.followStrategy.ratio let tradeAmount = Math.abs(item.deltaAmount) * ratio if (side == "closebuy" || side == "closesell") { // Get the number of positions to check let posAmount = this.getSymbolPosAmount(item.symbol, item.type) tradeAmount = Math.min(posAmount, tradeAmount) } // Order Id // let orderId = ex.CreateOrder(item.symbol, side, -1, tradeAmount, ex.GetLabel()) let orderId = this.tryCreateOrder(ex, item.symbol, side, -1, tradeAmount, ex.GetLabel(), this.followStrategy.maxReTries) // Check the Order Id if (orderId) { Log("The subscriber successfully placed an order, order ID:", orderId, ", Order direction:", side, ", Order amount:", Math.abs(item.deltaAmount), ", walkthrough order ratio (times):", ratio) } else { Log("Subscriber order failed, order ID: ", orderId, ", order direction: ", side, ", order amount: ", Math.abs(item.deltaAmount), ", walkthrough order ratio (times): ", ratio) } } }) }) // Update current position this.currentPos = _C(ex.GetPositions) } else { throw new Error("The subscriber is not bound to the API client") } } // Synchronous positions syncPositions(leaderPos) { let ex = this.subscriberCfg.apiClient this.currentPos = _C(ex.GetPositions) // Used to store the results of position differences let diff = { added: [], // Newly added positions removed: [], // Removed positions updated: [] // Updated positions (amount or price changes) } let leaderPosMap = new Map(leaderPos.map(p => [`${p.Symbol}|${p.Type}`, p])) let currentPosMap = new Map(this.currentPos.map(p => [`${p.Symbol}|${p.Type}`, p])) // Traverse the current positions and find new and updated positions for (let [key, leader] of leaderPosMap) { if (!currentPosMap.has(key)) { diff.added.push({ symbol: leader.Symbol, type: leader.Type, deltaAmount: leader.Amount }) } else { let current = currentPosMap.get(key) if (leader.Amount !== current.Amount) { diff.updated.push({ symbol: leader.Symbol, type: leader.Type, deltaAmount: leader.Amount - current.Amount * this.followStrategy.ratio }) } currentPosMap.delete(key) } } for (let [key, current] of currentPosMap) { diff.removed.push({ symbol: current.Symbol, type: current.Type, deltaAmount: -current.Amount * this.followStrategy.ratio }) } // Determine if there is a change let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0 if (hasChanged) { // synchronous this.applyPosChanges(diff) } } // Get subscriber UI information fetchFollowerUI() { // Subscriber information let ex = this.subscriberCfg.apiClient let equity = _C(ex.GetAccount).Equity let exLabel = ex.GetLabel() let tblFollowerInfo = { "type": "table", "title": "Follower Info", "cols": ["exchange object index", "exchange object tag", "denominated currency", "walkthrough order mode", "walkthrough order ratio (times)", "maximum retry times", "total equity"], "rows": [] } tblFollowerInfo.rows.push([this.subscriberCfg.clientIdx, exLabel, this.quoteCurrency, this.followStrategy.followMode, this.followStrategy.ratio, this.followStrategy.maxReTries, equity]) // Subscriber position information let tblFollowerPos = { "type": "table", "title": "Follower pos", "cols": ["trading product", "direction", "amount", "price"], "rows": [] } let pos = this.currentPos pos.forEach(p => { let row = [p.Symbol, p.Type == PD_LONG ? "long" : "short", p.Amount, p.Price] tblFollowerPos.rows.push(row) }) return [tblFollowerInfo, tblFollowerPos] } } // Test function, simulate random opening, simulate leader position change function randomTrade(symbol, amount) { let randomNum = Math.random() if (randomNum < 0.0001) { Log("Simulate order managed with trading signals trading", "#FF0000") let ex = exchanges[0] let pos = _C(ex.GetPositions) if (pos.length > 0) { // Random close positions let randomPos = pos[Math.floor(Math.random() * pos.length)] let tradeAmount = Math.random() > 0.7 ? Math.abs(randomPos.Amount * 0.5) : Math.abs(randomPos.Amount) ex.CreateOrder(randomPos.Symbol, randomPos.Type === PD_LONG ? "closebuy" : "closesell", -1, tradeAmount, ex.GetLabel()) } else { let tradeAmount = Math.random() * amount let side = Math.random() > 0.5 ? "buy" : "sell" if (side === "buy") { ex.CreateOrder(symbol, side, -1, tradeAmount, ex.GetLabel()) } else { ex.CreateOrder(symbol, side, -1, tradeAmount, ex.GetLabel()) } } } } // Strategy main loop function main() { let leader = new Leader() let followStrategyArr = JSON.parse(strFollowStrategyArr) if (followStrategyArr.length > 0 && followStrategyArr.length !== exchanges.length - 1) { throw new Error("Walkthrough trading strategy configuration error, walkthrough trading strategy amount and exchange amount do not match") } for (let i = 1; i < exchanges.length; i++) { let subscriber = null if (followStrategyArr.length == 0) { subscriber = new Subscriber({ apiClient: exchanges[i], clientIdx: i }) } else { let followStrategy = followStrategyArr[i - 1] subscriber = new Subscriber({ apiClient: exchanges[i], clientIdx: i }, followStrategy) } leader.subscribe(subscriber) } // Start monitoring while (true) { leader.poll() Sleep(1000 * pollInterval) // Simulate random transactions if (IsVirtual() && isBacktest) { randomTrade("BTC_USDT.swap", 0.001) randomTrade("ETH_USDT.swap", 0.02) randomTrade("SOL_USDT.swap", 0.1) } LogStatus(_D(), "\n", leader.fetchLeaderUI()) } } Design pattern Previously, we have designed several walkthrough trading strategies on the platform, using process-oriented design. This article is a new design attempt, using object-oriented style and observer design pattern. Monitoring plan The essence of walkthrough trading is a monitoring behavior, monitoring the target's actions and replicating them when new actions are found. In this article, only one solution is implemented: configure the exchange object through API KEY, and monitor the position of the target account. In fact, there are two other solutions that can be used, which may be more complicated in design: Extended API through FMZ Rely on the message push of the target live trading Walkthrough trading strategy There may be multiple requirements for walkthrough trading strategies, and the strategy framework is designed to be as easy to expand as possible. Position replication: Equity ratio Position synchronization In actual use, there may be various reasons that cause the positions of the trade order issuer and the walkthrough trading follower to differ. You can design a system to detect the difference between the walkthrough trading account and the trade order issuer account when walkthrough orders, and synchronize the positions automatically. Order retry You can specify the specific number of failed order retries in the walkthrough trading strategy. Backtest random test The function randomTrade(symbol, amount) function in the code is used for random position opening test during backtesting to detect the walkthrough trading effect. In the test, three products are used to place orders randomly to verify the demand for multi-product order following: randomTrade("BTC_USDT.swap", 0.001) randomTrade("ETH_USDT.swap", 0.02) randomTrade("SOL_USDT.swap", 0.1) Strategy Sharing https://www.fmz.com/strategy/494950 Extension and Optimization Expand the monitoring scheme for data such as positions and account information. Increase control over subscribers, and add functions such as pausing and unsubscribing of walkthrough trading accounts. Dynamically update walkthrough trading strategy parameters. Increase and expand richer walkthrough trading data and information display. Welcome to leave messages and discuss in the FMZ Quant (FMZ.COM) Digest and Community. You can put forward various demands and ideas. The editor will select more valuable content production plan design, explanation, and teaching materials based on the messages. This article is just a starting point. It uses object-oriented style and observer mode to design a preliminary walkthrough trading strategy framework. I hope it can provide reference and inspiration for readers. Thank you for your reading and support.