Demand Scenarios of Walkthrough System
I was often asked this question in the FMZ platform community and in private communication with users:
"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. My language 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.
Strategy Design
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]] */classLeader { constructor(leaderCfg = { type: "exchange", apiClient: exchanges[0] }) { // Account managed with trading signals configurationthis.leaderCfg = leaderCfg // Cache the last position information for comparisonthis.lastPos = null// Record current position informationthis.currentPos = null// Record subscribersthis.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.// initializationlet ex = this.leaderCfg.apiClientlet currency = ex.GetCurrency() let arrCurrency = currency.split("_") if (arrCurrency.length !== 2) { thrownewError("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 initializationthis.initEquity = _C(ex.GetAccount).Equitythis.currentPos = _C(ex.GetPositions) } // Monitoring leader logicpoll() { // Get the exchange objectlet ex = this.leaderCfg.apiClient// Get the leader's position and account asset datalet pos = ex.GetPositions() if (!pos) { return } this.currentPos = pos // Call the judgment method to determine the position changeslet { hasChanged, diff } = this._hasChanged(pos) if (hasChanged) { Log("Leader position changes, current position:", pos) Log("Position changes:", diff) // Notify Subscribersthis.subscribers.forEach(subscriber => { subscriber.applyPosChanges(diff) }) } // Synchronous positionsthis.subscribers.forEach(subscriber => { subscriber.syncPositions(pos) }) } // Determine whether the position has changed_hasChanged(pos) { if (this.lastPos) { // Used to store the results of position differenceslet diff = { added: [], // Newly added positionsremoved: [], // Removed positionsupdated: [] // 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 objectlet lastPosMap = newMap(this.lastPos.map(p => [`${p.Symbol}|${p.Type}`, p])) let currentPosMap = newMap(pos.map(p => [`${p.Symbol}|${p.Type}`, p])) // Traverse the current positions and find new and updated positionsfor (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 changedlet 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 changelet hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0// If there is a change, update lastPosif (hasChanged) { this.lastPos = pos } return { hasChanged: hasChanged, diff: diff } } else { // If there is no last position record, update the record and do not synchronize positionsthis.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 registrationsubscribe(subscriber) { if (this.subscribers.indexOf(subscriber) === -1) { if (this.quoteCurrency !== subscriber.quoteCurrency) { thrownewError("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 signalslet ex = this.leaderCfg.apiClientlet equity = _C(ex.GetAccount).Equity subscriber.setEquityRatio(equity) } this.subscribers.push(subscriber) Log("Subscriber registration is successful, subscription configuration:", subscriber.getApiClientInfo()) } } // Unsubscribeunsubscribe(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 informationfetchLeaderUI() { // Information of the trade order issuerlet 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 informationlet 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 subscriberlet 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 } classSubscriber { constructor(subscriberCfg, followStrategy = { followMode: "position_ratio", ratio: 1, maxReTries: 3 }) { this.subscriberCfg = subscriberCfg this.followStrategy = followStrategy // initializationlet ex = this.subscriberCfg.apiClientlet currency = ex.GetCurrency() let arrCurrency = currency.split("_") if (arrCurrency.length !== 2) { thrownewError("Subscriber configuration error, must be two currency pairs") } this.baseCurrency = arrCurrency[0] this.quoteCurrency = arrCurrency[1] // Initial acquisition of position datathis.currentPos = _C(ex.GetPositions) } setEquityRatio(leaderEquity) { // {followMode: "equity_ratio"} Automatically follow orders based on account equity ratioif (this.followStrategy.followMode === "equity_ratio") { let ex = this.subscriberCfg.apiClientlet equity = _C(ex.GetAccount).Equitylet 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 subscribergetApiClientInfo() { let ex = this.subscriberCfg.apiClientlet idx = this.subscriberCfg.clientIdxif (ex) { return { exName: ex.GetName(), exLabel: ex.GetLabel(), exIdx: idx, followStrategy: this.followStrategy } } else { thrownewError("The subscriber is not bound to the API client") } } // Returns the transaction direction parameters according to the position type and position changesgetTradeSide(type, deltaAmount) { if (type == PD_LONG && deltaAmount > 0) { return"buy" } elseif (type == PD_LONG && deltaAmount < 0) { return"closebuy" } elseif (type == PD_SHORT && deltaAmount > 0) { return"sell" } elseif (type == PD_SHORT && deltaAmount < 0) { return"closesell" } returnnull } getSymbolPosAmount(symbol, type) { let ex = this.subscriberCfg.apiClientif (ex) { let pos = _C(ex.GetPositions, symbol) if (pos.length > 0) { // Traverse the positions and find the corresponding symbol and typefor (let i = 0; i < pos.length; i++) { if (pos[i].Symbol === symbol && pos[i].Type === type) { return pos[i].Amount } } } return0 } else { thrownewError("The subscriber is not bound to the API client") } } // Retry ordertryCreateOrder(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) } returnnull } // Synchronous position changesapplyPosChanges(diff) { let ex = this.subscriberCfg.apiClientif (ex) { ["added", "removed", "updated"].forEach(key => { diff[key].forEach(item => { let side = this.getTradeSide(item.type, item.deltaAmount) if (side) { // Calculate the walkthrough trading ratiolet ratio = this.followStrategy.ratiolet tradeAmount = Math.abs(item.deltaAmount) * ratio if (side == "closebuy" || side == "closesell") { // Get the number of positions to checklet 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 Idif (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 positionthis.currentPos = _C(ex.GetPositions) } else { thrownewError("The subscriber is not bound to the API client") } } // Synchronous positionssyncPositions(leaderPos) { let ex = this.subscriberCfg.apiClientthis.currentPos = _C(ex.GetPositions) // Used to store the results of position differenceslet diff = { added: [], // Newly added positionsremoved: [], // Removed positionsupdated: [] // Updated positions (amount or price changes) } let leaderPosMap = newMap(leaderPos.map(p => [`${p.Symbol}|${p.Type}`, p])) let currentPosMap = newMap(this.currentPos.map(p => [`${p.Symbol}|${p.Type}`, p])) // Traverse the current positions and find new and updated positionsfor (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 changelet hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0if (hasChanged) { // synchronousthis.applyPosChanges(diff) } } // Get subscriber UI informationfetchFollowerUI() { // Subscriber informationlet ex = this.subscriberCfg.apiClientlet equity = _C(ex.GetAccount).Equitylet 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 informationlet 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 changefunctionrandomTrade(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 positionslet 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 loopfunctionmain() { let leader = newLeader() let followStrategyArr = JSON.parse(strFollowStrategyArr) if (followStrategyArr.length > 0 && followStrategyArr.length !== exchanges.length - 1) { thrownewError("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 = nullif (followStrategyArr.length == 0) { subscriber = newSubscriber({ apiClient: exchanges[i], clientIdx: i }) } else { let followStrategy = followStrategyArr[i - 1] subscriber = newSubscriber({ apiClient: exchanges[i], clientIdx: i }, followStrategy) } leader.subscribe(subscriber) } // Start monitoringwhile (true) { leader.poll() Sleep(1000 * pollInterval) // Simulate random transactionsif (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
Monitor the log and status bar information of the target live trading, and operate and walkthrough orders according to the changes. The advantage of using this solution is that it can reduce API requests effectively.
Rely on the message push of the target live trading
You can turn on the message push of the target live trading on FMZ, and a message will be pushed when the target live trading has an order operation. The walkthrough trading strategy receives these messages and takes action. The advantages of using this solution are: reducing API requests and changing from a request polling mechanism to an event-driven mechanism.
- 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:
Positions can be replicated 1:1, or they can be scaled according to specified scaling parameters.
Equity ratio
The equity ratio of the account managed with trading signals and the walkthrough account can be automatically used as a scaling parameter for walkthrough trading.
- 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
Thefunction randomTrade(symbol, amount)
function in the code is used for random position opening test during backtesting to detect the walkthrough trading effect.
Strategy Backtesting and Verification

According to the first exchange object added (trade order issuer), the subsequent exchange objects added walkthrough order (walkthrough order follower).
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.
END
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.