[TOC]
Na comunidade da plataforma FMZ e na comunicação privada com os usuários, muitas vezes me fazem esta pergunta:
“Por que as estratégias escritas nos scripts My ou Pine podem controlar apenas uma conta e um produto?”
A essência desse problema está no posicionamento de design da própria linguagem. Minha linguagem e a linguagem Pine são linguagens de script altamente encapsuladas, e sua implementação subjacente é baseada em JavaScript. Para permitir que os usuários comecem rapidamente e se concentrem na lógica da estratégia, ambos fizeram muito encapsulamento e abstração no nível da linguagem, mas isso também sacrificou um certo grau de flexibilidade: por padrão, apenas um modelo de execução de estratégia de conta única e produto único é suportado.
Quando um usuário deseja executar várias contas em tempo real, isso só pode ser feito executando várias instâncias do Pine ou My Real. Essa abordagem é aceitável quando o número de contas é pequeno, mas se várias instâncias forem implantadas no mesmo host, um grande número de solicitações de API será gerado, e a troca pode até restringir o acesso devido à frequência excessiva de solicitações, trazendo riscos desnecessários em tempo real.
Então, existe uma maneira mais elegante de simplesmente executar um script do Pine ou do My Language para que outras contas possam copiar automaticamente seu comportamento de negociação?
A resposta é: Sim.
Este artigo orientará você na criação de um sistema de negociação de cópias entre contas e produtos do zero, compatível com as estratégias de linguagem My e Pine. Por meio da arquitetura Líder-Assinante, ele implementará uma estrutura de negociação síncrona multiconta eficiente, estável e escalável para resolver os vários pontos problemáticos que você encontra na implantação em tempo real.
O programa é escrito e projetado em JavaScript, e a arquitetura do programa usa o modelo Líder-Assinante.
Código fonte da estratégia:
/*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())
}
}
Padrões de Design Já projetamos diversas estratégias de copy trading na plataforma, usando um design orientado a processos. Este artigo é uma nova tentativa de design, usando um estilo orientado a objetos e o padrão de design do observador.
Solução de Monitoramento A essência do chamado copy trading é um tipo de comportamento de monitoramento, monitorando as ações do alvo e replicando-as quando novas ações são encontradas.
Este artigo implementa apenas uma solução: usar a API KEY para configurar objetos de câmbio e monitorar as posições da conta de destino. Na verdade, existem duas outras soluções que podem ser mais complicadas em termos de design:
Por meio da API estendida da FMZ Monitore as informações de log e barra de status da conta real de destino e execute operações e siga ordens com base nas alterações. A vantagem de usar essa solução é que ela pode reduzir efetivamente as solicitações de API.
Depende do envio da mensagem do disco real de destino Você pode ativar o envio de mensagens da conta real de destino no FMZ, e uma mensagem será enviada quando a conta real de destino tiver um pedido. A estratégia de copy trading pode receber essas mensagens e agir. Os benefícios de usar esta solução incluem: redução de solicitações de API e mudança de um mecanismo de pesquisa de solicitações para um mecanismo orientado a eventos.
Estratégia de negociação de cópias
Pode haver vários requisitos para estratégias de copy trading, e a estrutura da estratégia foi projetada para ser fácil de expandir.
Replicação de posição: As posições podem ser replicadas 1:1 ou podem ser dimensionadas de acordo com parâmetros de escala especificados.
Índice de Patrimônio Líquido A relação de patrimônio entre a conta com ordens e a conta que segue ordens pode ser usada automaticamente como um parâmetro de escala para ordens seguintes.
Sincronização de posição No uso real, pode haver vários motivos que fazem com que as posições do tomador de ordens e do seguidor de ordens sejam diferentes. O sistema pode ser projetado para detectar a diferença entre a conta de cópia e a conta de transporte de ordens ao seguir ordens e sincronizar posições automaticamente.
Tentar novamente o pedido Você pode especificar o número específico de tentativas para pedidos com falha na estratégia de copy trading.
Testes aleatórios de backtesting
No códigofunction randomTrade(symbol, amount)A função é usada para testes de abertura aleatória durante o backtesting para detectar o efeito de copy trading.

De acordo com o primeiro objeto de troca adicionado (o tomador de ordens), os objetos de troca adicionados posteriormente seguem as ordens (os seguidores).

No teste, três variedades de ordens foram colocadas aleatoriamente para verificar a demanda por negociação de cópias multivariedade:
randomTrade("BTC_USDT.swap", 0.001)
randomTrade("ETH_USDT.swap", 0.02)
randomTrade("SOL_USDT.swap", 0.1)
Você está convidado a deixar mensagens e discutir na biblioteca e comunidade do Inventor Quantitative (FMZ.COM). Você pode apresentar diversas demandas e ideias. O editor selecionará planos de produção de conteúdo mais valiosos, explicações e materiais didáticos com base nas mensagens.
Este artigo é apenas um ponto de partida. Ele usa estilo orientado a objetos e modo observador para projetar uma estrutura preliminar de estratégia de copy trading. Espero que possa servir de referência e inspiração para os leitores. Obrigado pela sua leitura e apoio.