[TOC]
FMZ プラットフォーム コミュニティやユーザーとのプライベートなコミュニケーションの中で、次のような質問をよく受けます。
「My または Pine スクリプトで記述された戦略では、なぜ 1 つのアカウントと 1 つの製品しか制御できないのですか?」
この問題の本質は、言語自体の設計上の位置付けにあります。 My 言語と Pine 言語は高度にカプセル化されたスクリプト言語であり、その基礎となる実装は JavaScript に基づいています。ユーザーがすぐに使い始めて戦略ロジックに集中できるようにするために、どちらも言語レベルで多くのカプセル化と抽象化を行っていますが、これによってある程度の柔軟性が犠牲になっています。デフォルトでは、単一アカウント、単一製品の戦略実行モデルのみがサポートされています。
ユーザーが複数のアカウントをリアルタイムで実行したい場合、複数の Pine または My real インスタンスを実行することによってのみこれを実現できます。このアプローチは、アカウント数が少ない場合には受け入れられますが、複数のインスタンスを同じホストに展開すると、大量の API リクエストが生成され、過剰なリクエスト頻度のために取引所がアクセスを制限する場合もあり、不要なリアルタイム リスクをもたらします。
では、Pine または My language スクリプトを実行して、他のアカウントがその取引動作を自動的にコピーできるようにする、よりエレガントな方法はあるのでしょうか?
答えは「はい」です。
この記事では、My および Pine 言語戦略と互換性のあるクロスアカウント、クロスプロダクトのコピー取引システムをゼロから構築する方法について説明します。リーダー・サブスクライバーアーキテクチャを通じて、効率的で安定したスケーラブルなマルチアカウント同期取引フレームワークを実装し、リアルタイム展開で発生するさまざまな問題点を解決します。
プログラムは JavaScript で記述および設計されており、プログラム アーキテクチャではリーダー サブスクライバー モデルが使用されています。
戦略ソースコード:
/*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] }) {
// 带单账户配置
this.leaderCfg = leaderCfg
// 缓存上次持仓信息,用于对比
this.lastPos = null
// 记录当前持仓信息
this.currentPos = null
// 记录订阅者
this.subscribers = []
// 根据 leaderCfg 配置,确定使用哪种监控方案:1、直接监控带单账户。2、通过FMZ 扩展API监控带单策略实盘的数据。3、消息推送机制跟单。默认使用方案1。
// 初始化
let ex = this.leaderCfg.apiClient
let currency = ex.GetCurrency()
let arrCurrency = currency.split("_")
if (arrCurrency.length !== 2) {
throw new Error("带单账户配置错误,必须是两种币对")
}
this.baseCurrency = arrCurrency[0]
this.quoteCurrency = arrCurrency[1]
// 获取初始化时,带单账户的总权益
this.initEquity = _C(ex.GetAccount).Equity
this.currentPos = _C(ex.GetPositions)
}
// 监控leader的逻辑
poll() {
// 获取交易所对象
let ex = this.leaderCfg.apiClient
// 获取leader的持仓、账户资产数据
let pos = ex.GetPositions()
if (!pos) {
return
}
this.currentPos = pos
// 调用判断方法,判断持仓变化
let { hasChanged, diff } = this._hasChanged(pos)
if (hasChanged) {
Log("Leader持仓变化,当前持仓:", pos)
Log("持仓变动:", diff)
// 通知订阅者
this.subscribers.forEach(subscriber => {
subscriber.applyPosChanges(diff)
})
}
// 同步持仓
this.subscribers.forEach(subscriber => {
subscriber.syncPositions(pos)
})
}
// 判断持仓是否变化
_hasChanged(pos) {
if (this.lastPos) {
// 用于存储持仓差异的结果
let diff = {
added: [], // 新增的持仓
removed: [], // 移除的持仓
updated: [] // 更新的持仓(数量或价格变化)
}
// 将上次持仓和当前持仓转换为 Map,键为 `symbol + direction`,值为持仓对象
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]))
// 遍历当前持仓,找出新增和更新的持仓
for (let [key, current] of currentPosMap) {
if (!lastPosMap.has(key)) {
// 如果上次持仓中没有该 key,则为新增持仓
diff.added.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount })
} else {
// 如果存在,则检查数量或价格是否变化
let last = lastPosMap.get(key)
if (current.Amount !== last.Amount) {
diff.updated.push({ symbol: current.Symbol, type: current.Type, deltaAmount: current.Amount - last.Amount })
}
// 从 lastPosMap 中移除,剩下的就是被移除的持仓
lastPosMap.delete(key)
}
}
// 剩余在 lastPosMap 中的 key 是被移除的持仓
for (let [key, last] of lastPosMap) {
diff.removed.push({ symbol: last.Symbol, type: last.Type, deltaAmount: -last.Amount })
}
// 判断是否有变化
let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0
// 如果有变化,更新 lastPos
if (hasChanged) {
this.lastPos = pos
}
return { hasChanged: hasChanged, diff: diff }
} else {
// 如果没有上次持仓记录,更新记录,不做持仓同步
this.lastPos = pos
return { hasChanged: false, diff: { added: [], removed: [], updated: [] } }
/* 另一种方案:同步仓位
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: []}}
}
*/
}
}
// 订阅者注册
subscribe(subscriber) {
if (this.subscribers.indexOf(subscriber) === -1) {
if (this.quoteCurrency !== subscriber.quoteCurrency) {
throw new Error("订阅者币对不匹配,当前leader币对:" + this.quoteCurrency + ",订阅者币对:" + subscriber.quoteCurrency)
}
if (subscriber.followStrategy.followMode === "equity_ratio") {
// 设置跟单比例
let ex = this.leaderCfg.apiClient
let equity = _C(ex.GetAccount).Equity
subscriber.setEquityRatio(equity)
}
this.subscribers.push(subscriber)
Log("订阅者注册成功,订阅配置:", subscriber.getApiClientInfo())
}
}
// 取消订阅
unsubscribe(subscriber) {
const index = this.subscribers.indexOf(subscriber)
if (index !== -1) {
this.subscribers.splice(index, 1)
Log("订阅者取消注册成功,订阅配置:", subscriber.getApiClientInfo())
} else {
Log("订阅者取消注册失败,订阅配置:", subscriber.getApiClientInfo())
}
}
// 获取UI信息
fetchLeaderUI() {
// 带单者信息
let tblLeaderInfo = { "type": "table", "title": "Leader Info", "cols": ["带单方案", "计价币种", "跟单者数量", "初始总权益"], "rows": [] }
tblLeaderInfo.rows.push([this.leaderCfg.type, this.quoteCurrency, this.subscribers.length, this.initEquity])
// 构造带单者的显示信息:持仓信息
let tblLeaderPos = { "type": "table", "title": "Leader pos", "cols": ["交易品种", "方向", "数量", "价格"], "rows": [] }
this.currentPos.forEach(pos => {
let row = [pos.Symbol, pos.Type == PD_LONG ? "多" : "空", pos.Amount, pos.Price]
tblLeaderPos.rows.push(row)
})
// 构造订阅者的显示信息
let strFollowerMsg = ""
this.subscribers.forEach(subscriber => {
let arrTbl = subscriber.fetchFollowerUI()
strFollowerMsg += "`" + JSON.stringify(arrTbl) + "`\n"
})
return "`" + JSON.stringify([tblLeaderInfo, tblLeaderPos]) + "`\n" + strFollowerMsg
}
// 扩展暂停跟单、移除订阅等功能
}
class Subscriber {
constructor(subscriberCfg, followStrategy = { followMode: "position_ratio", ratio: 1, maxReTries: 3 }) {
this.subscriberCfg = subscriberCfg
this.followStrategy = followStrategy
// 初始化
let ex = this.subscriberCfg.apiClient
let currency = ex.GetCurrency()
let arrCurrency = currency.split("_")
if (arrCurrency.length !== 2) {
throw new Error("订阅者配置错误,必须是两种币对")
}
this.baseCurrency = arrCurrency[0]
this.quoteCurrency = arrCurrency[1]
// 初始获取持仓数据
this.currentPos = _C(ex.GetPositions)
}
setEquityRatio(leaderEquity) {
// {followMode: "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("带单者权益:", leaderEquity, "订阅者权益:", equity)
Log("自动设置,订阅者权益比例:", ratio)
}
}
// 获取订阅者绑定的API客户端信息
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("订阅者没有绑定API客户端")
}
}
// 根据持仓类型、仓位变化,返回交易方向参数
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) {
// 遍历持仓,查找对应的symbol和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("订阅者没有绑定API客户端")
}
}
// 下单重试
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
}
// 同步持仓变化
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) {
// 计算跟单比例
let ratio = this.followStrategy.ratio
let tradeAmount = Math.abs(item.deltaAmount) * ratio
if (side == "closebuy" || side == "closesell") {
// 获取持仓数量检查
let posAmount = this.getSymbolPosAmount(item.symbol, item.type)
tradeAmount = Math.min(posAmount, tradeAmount)
}
// 订单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)
// 检测订单Id
if (orderId) {
Log("订阅者下单成功,订单Id:", orderId, ",下单方向:", side, ",下单数量:", Math.abs(item.deltaAmount), ",跟单比例(倍):", ratio)
} else {
Log("订阅者下单失败,订单Id:", orderId, ",下单方向:", side, ",下单数量:", Math.abs(item.deltaAmount), ",跟单比例(倍):", ratio)
}
}
})
})
// 更新当前持仓
this.currentPos = _C(ex.GetPositions)
} else {
throw new Error("订阅者没有绑定API客户端")
}
}
// 同步持仓
syncPositions(leaderPos) {
let ex = this.subscriberCfg.apiClient
this.currentPos = _C(ex.GetPositions)
// 用于存储持仓差异的结果
let diff = {
added: [], // 新增的持仓
removed: [], // 移除的持仓
updated: [] // 更新的持仓(数量或价格变化)
}
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]))
// 遍历当前持仓,找出新增和更新的持仓
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 })
}
// 判断是否有变化
let hasChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.updated.length > 0
if (hasChanged) {
// 同步
this.applyPosChanges(diff)
}
}
// 获取订阅者UI信息
fetchFollowerUI() {
// 订阅者信息
let ex = this.subscriberCfg.apiClient
let equity = _C(ex.GetAccount).Equity
let exLabel = ex.GetLabel()
let tblFollowerInfo = { "type": "table", "title": "Follower Info", "cols": ["交易所对象索引", "交易所对象标签", "计价币种", "跟单模式", "跟单比例(倍)", "最大重试次数", "总权益"], "rows": [] }
tblFollowerInfo.rows.push([this.subscriberCfg.clientIdx, exLabel, this.quoteCurrency, this.followStrategy.followMode, this.followStrategy.ratio, this.followStrategy.maxReTries, equity])
// 订阅者持仓信息
let tblFollowerPos = { "type": "table", "title": "Follower pos", "cols": ["交易品种", "方向", "数量", "价格"], "rows": [] }
let pos = this.currentPos
pos.forEach(p => {
let row = [p.Symbol, p.Type == PD_LONG ? "多" : "空", p.Amount, p.Price]
tblFollowerPos.rows.push(row)
})
return [tblFollowerInfo, tblFollowerPos]
}
}
// 测试函数,模拟随机开仓,模拟leader仓位变动
function randomTrade(symbol, amount) {
let randomNum = Math.random()
if (randomNum < 0.0001) {
Log("模拟带单交易", "#FF0000")
let ex = exchanges[0]
let pos = _C(ex.GetPositions)
if (pos.length > 0) {
// 随机平仓
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())
}
}
}
}
// 策略主循环
function main() {
let leader = new Leader()
let followStrategyArr = JSON.parse(strFollowStrategyArr)
if (followStrategyArr.length > 0 && followStrategyArr.length !== exchanges.length - 1) {
throw new Error("跟单策略配置错误,跟单策略数量和交易所数量不匹配")
}
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)
}
// 启动监控
while (true) {
leader.poll()
Sleep(1000 * pollInterval)
// 模拟随机交易
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())
}
}
デザインパターン 私たちはこれまで、プロセス指向の設計を使用して、プラットフォーム上で複数のコピー取引戦略を設計してきました。この記事は、オブジェクト指向スタイルとオブザーバー デザイン パターンを使用した新しいデザインの試みです。
監視ソリューション いわゆるコピートレードの本質は、ターゲットの行動を監視し、新しい行動が見つかったときにそれを複製するという、一種の監視行動です。
この記事では、API KEY を使用して交換オブジェクトを構成し、ターゲット アカウントのポジションを監視するという 1 つのソリューションのみを実装します。実際、設計がより複雑になる可能性がある他の 2 つのソリューションがあります。
FMZの拡張APIを通じて 対象のリアルアカウントのログやステータスバーの情報を監視し、その変化に応じて操作や注文を実行します。このソリューションを使用する利点は、API リクエストを効果的に削減できることです。
ターゲットの実ディスクメッセージプッシュに依存する FMZ上で対象のリアルアカウントのメッセージプッシュをオンにしておくと、対象のリアルアカウントに注文があった際にメッセージがプッシュされます。コピー取引戦略はこれらのメッセージを受信してアクションを実行できます。このソリューションを使用する利点には、API リクエストの削減と、リクエスト ポーリング メカニズムからイベント駆動型メカニズムへの変更などがあります。
コピートレード戦略
コピートレード戦略には複数の要件がある場合があり、戦略フレームワークは簡単に拡張できるように設計されています。
ポジションの複製: 位置は 1:1 で複製することも、指定されたスケーリング パラメータに従ってスケーリングすることもできます。
自己資本比率 注文のあるアカウントと注文をフォローしているアカウント間のエクイティ比率は、フォローしている注文のスケーリング パラメータとして自動的に使用できます。
位置同期 実際の使用においては、注文を受ける側と注文に従う側の位置が異なる理由は様々考えられます。注文に従う際にコピー口座と注文実行口座の差異を検出し、自動的にポジションを同期するようにシステムを設計できます。
注文を再試行する コピー取引戦略では、失敗した注文の再試行回数を指定できます。
バックテスト ランダムテスト
コードではfunction randomTrade(symbol, amount)この機能は、コピー取引の効果を検出するためのバックテスト中にランダムオープニングテストに使用されます。

最初に追加された交換オブジェクト (注文受付者) に応じて、その後に追加された交換オブジェクトは注文に従います (フォロワー)。

テストでは、多品種コピー取引の需要を検証するために、3 種類の注文がランダムに出されました。
randomTrade("BTC_USDT.swap", 0.001)
randomTrade("ETH_USDT.swap", 0.02)
randomTrade("SOL_USDT.swap", 0.1)
Inventor Quantitative (FMZ.COM) ライブラリおよびコミュニティにメッセージを残したり、ディスカッションしたりすることを歓迎します。さまざまな要求やアイデアを出すことができます。編集者は、そのメッセージをもとに、より価値のあるコンテンツ制作プランの設計、解説、教材などを精査します。
この記事は単なる出発点に過ぎません。オブジェクト指向スタイルとオブザーバー モードを使用して、予備的なコピー取引戦略フレームワークを設計します。読者にとって参考になり、インスピレーションを与えることができれば幸いです。ご愛読、応援ありがとうございます。