avatar of 发明者量化-小小梦 发明者量化-小小梦
focar em Mensagem privada
4
focar em
1271
Seguidores

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Criado em: 2024-12-17 11:44:07, atualizado em: 2024-12-17 16:08:11
comments   0
hits   663

[TOC]

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Prefácio

No artigo anterior“Discussão sobre recepção de sinal externo na plataforma FMZ: API estendida vs estratégia de serviço HTTP integrado”Na discussão, comparamos duas maneiras diferentes de receber sinais externos para negociação programática e analisamos os detalhes. A solução de usar a API de extensão da plataforma FMZ para receber sinais externos já tem uma estratégia completa na biblioteca de estratégia da plataforma. Neste artigo, implementaremos uma solução completa de usar o serviço Http integrado à estratégia para receber sinais.

Implementação da estratégia

Seguindo a estratégia anterior de usar a API de extensão FMZ para acessar os sinais do Trading View, usamos o formato de mensagem anterior, o método de processamento de mensagem, etc. e fazemos modificações simples na estratégia.

Como os serviços integrados na política podem usar HTTP ou HTTPS, para demonstração simples, usamos o protocolo HTTP, adicionamos verificação de lista de permissões de IP e verificação de senha. Se houver necessidade de aumentar ainda mais a segurança, o serviço integrado da política pode ser projetado como um serviço HTTPS.

//信号结构
var Template = {
    Flag: "45M103Buy",     // 标识,可随意指定
    Exchange: 1,           // 指定交易所交易对
    Currency: "BTC_USDT",  // 交易对
    ContractType: "spot",  // 合约类型,swap,quarter,next_quarter,现货填写spot
    Price: "{{close}}",    // 开仓或者平仓价格,-1为市价
    Action: "buy",         // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
    Amount: "1",           // 交易量
}

var Success = "#5cb85c"    // 成功颜色
var Danger = "#ff0000"     // 危险颜色
var Warning = "#f0ad4e"    // 警告颜色
var buffSignal = []

// Http服务
function serverFunc(ctx, ipWhiteList, passPhrase) {
    var path = ctx.path()
    if (path == "/CommandRobot") {
        // 校验IP地址
        var fromIP = ctx.remoteAddr().split(":")[0]        
        if (ipWhiteList && ipWhiteList.length > 0) {
            var ipList = ipWhiteList.split(",")
            if (!ipList.includes(fromIP)) {
                ctx.setStatus(500)
                ctx.write("IP address not in white list")
                Log("500 Error: IP address not in white list", "#FF0000")
                return 
            }
        }

        // 校验口令
        var pass = ctx.rawQuery().length > 0 ? ctx.query("passPhrase") : ""
        if (passPhrase && passPhrase.length > 0) {
            if (pass != passPhrase) {
                ctx.setStatus(500)
                ctx.write("Authentication failed")
                Log("500 Error: Authentication failed", "#FF0000")
                return 
            }
        }

        var body = JSON.parse(ctx.body())
        threading.mainThread().postMessage(JSON.stringify(body))
        ctx.write("OK")
        // 200
    } else {
        ctx.setStatus(404)
    }
}

// 校验信号消息格式
function DiffObject(object1, object2) {
    const keys1 = Object.keys(object1)
    const keys2 = Object.keys(object2)
    if (keys1.length !== keys2.length) {
        return false
    }
    for (let i = 0; i < keys1.length; i++) {
        if (keys1[i] !== keys2[i]) {
            return false
        }
    }
    return true
}

function CheckSignal(Signal) {
    Signal.Price = parseFloat(Signal.Price)
    Signal.Amount = parseFloat(Signal.Amount)
    if (Signal.Exchange <= 0 || !Number.isInteger(Signal.Exchange)) {
        Log("交易所最小编号为1,并且为整数", Danger)
        return
    }
    if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
        Log("交易量不能小于0,并且为数值类型", typeof(Signal.Amount), Danger)
        return
    }
    if (typeof(Signal.Price) != "number") {
        Log("价格必须是数值", Danger)
        return
    }
    if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
        Log("指令为操作现货,Action错误,Action:", Signal.Action, Danger)
        return 
    }
    if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
        Log("指令为操作期货,Action错误,Action:", Signal.Action, Danger)
        return 
    }
    return true
}

// 信号处理对象
function createManager() {
    var self = {}
    self.tasks = []
    
    self.process = function() {
        var processed = 0
        if (self.tasks.length > 0) {
            _.each(self.tasks, function(task) {
                if (!task.finished) {
                    processed++
                    self.pollTask(task)
                }
            })
            if (processed == 0) {
                self.tasks = []
            }
        }
    }
    
    self.newTask = function(signal) {
        // {"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"buy","Amount":"0"}
        var task = {}
        task.Flag = signal["Flag"]
        task.Exchange = signal["Exchange"]
        task.Currency = signal["Currency"]
        task.ContractType = signal["ContractType"]
        task.Price = signal["Price"]
        task.Action = signal["Action"]
        task.Amount = signal["Amount"]
        task.exchangeIdx = signal["Exchange"] - 1
        task.pricePrecision = null
        task.amountPrecision = null 
        task.error = null 
        task.exchangeLabel = exchanges[task.exchangeIdx].GetLabel()
        task.finished = false 
        
        Log("创建任务:", task)
        self.tasks.push(task)
    }
    
    self.getPrecision = function(n) {
        var precision = null 
        var arr = n.toString().split(".")
        if (arr.length == 1) {
            precision = 0
        } else if (arr.length == 2) {
            precision = arr[1].length
        } 
        return precision
    }
    
    self.pollTask = function(task) {
        var e = exchanges[task.exchangeIdx]
        var name = e.GetName()
        var isFutures = true
        e.SetCurrency(task.Currency)
        if (task.ContractType != "spot" && name.indexOf("Futures_") != -1) {
            // 非现货,则设置合约
            e.SetContractType(task.ContractType)
        } else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
            isFutures = false 
        } else {
            task.error = "指令中的ContractType与配置的交易所对象类型不匹配"
            return 
        }
        
        var depth = e.GetDepth()
        if (!depth || !depth.Bids || !depth.Asks) {
            task.error = "订单薄数据异常"
            return 
        }
        
        if (depth.Bids.length == 0 && depth.Asks.length == 0) {
            task.error = "盘口无订单"
            return 
        }
        
        _.each([depth.Bids, depth.Asks], function(arr) {
            _.each(arr, function(order) {
                var pricePrecision = self.getPrecision(order.Price)
                var amountPrecision = self.getPrecision(order.Amount)
                if (Number.isInteger(pricePrecision) && !Number.isInteger(self.pricePrecision)) {
                    self.pricePrecision = pricePrecision
                } else if (Number.isInteger(self.pricePrecision) && Number.isInteger(pricePrecision) && pricePrecision > self.pricePrecision) {
                    self.pricePrecision = pricePrecision
                }
                if (Number.isInteger(amountPrecision) && !Number.isInteger(self.amountPrecision)) {
                    self.amountPrecision = amountPrecision
                } else if (Number.isInteger(self.amountPrecision) && Number.isInteger(amountPrecision) && amountPrecision > self.amountPrecision) {
                    self.amountPrecision = amountPrecision
                }
            })
        })

        if (!Number.isInteger(self.pricePrecision) || !Number.isInteger(self.amountPrecision)) {
            task.err = "获取精度失败"
            return 
        }
        
        e.SetPrecision(self.pricePrecision, self.amountPrecision)
        
        // buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多
        var direction = null 
        var tradeFunc = null 
        if (isFutures) {
            switch (task.Action) {
                case "long": 
                    direction = "buy"
                    tradeFunc = e.Buy 
                    break
                case "short": 
                    direction = "sell"
                    tradeFunc = e.Sell
                    break
                case "closesell": 
                    direction = "closesell"
                    tradeFunc = e.Buy 
                    break
                case "closebuy": 
                    direction = "closebuy"
                    tradeFunc = e.Sell
                    break
            }
            if (!direction || !tradeFunc) {
                task.error = "交易方向错误:" + task.Action
                return 
            }
            e.SetDirection(direction)
        } else {
            if (task.Action == "buy") {
                tradeFunc = e.Buy 
            } else if (task.Action == "sell") {
                tradeFunc = e.Sell 
            } else {
                task.error = "交易方向错误:" + task.Action
                return 
            }
        }
        var id = tradeFunc(task.Price, task.Amount)
        if (!id) {
            task.error = "下单失败"
        }
        
        task.finished = true
    }
    
    return self
}

function main() {
    // 重置日志信息
    if (isResetLog) {
        LogReset(1)
    }

    Log("交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]", Danger)
    Log("指令模板:", JSON.stringify(Template), Danger)    
    if (!passPhrase || passPhrase.length == 0) {
        Log("webhook url:", `http://${serverIP}:${port}/CommandRobot`)
    } else {
        Log("webhook url:", `http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`)
    }

    // 创建Http内置服务
    __Serve("http://0.0.0.0:" + port, serverFunc, ipWhiteList, passPhrase)

    // 初始化执行的代码
    if (initCode && initCode.length > 0) {
        try {
            Log("执行初始化代码:", initCode)
            eval(initCode)
        } catch(error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
        }
    }    

    // 创建信号管理对象
    var manager = createManager()
    
    while (true) {
        try {
            // 检测交互控件,用于测试
            var cmd = GetCommand()
            if (cmd) {
                // 发送Http请求,模拟测试
                var arrCmd = cmd.split(":", 2)
                if (arrCmd[0] == "TestSignal") {
                    // {"Flag":"TestSignal","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"long","Amount":"1"}
                    var signal = cmd.replace("TestSignal:", "")
                    if (!passPhrase || passPhrase.length == 0) {
                        var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot`, {"method": "POST", "body": signal})
                        Log("测试请求的应答:", ret)
                    } else {
                        var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`, {"method": "POST", "body": signal})
                        Log("测试请求的应答:", ret)
                    }                    
                }
            }

            // 检测内置Http服务收到请求之后通知主线程的消息,写入manager对象的任务队列
            var msg = threading.mainThread().peekMessage(-1)
            if (msg) {
                Log("收到消息 msg:", msg)
                var objSignal = JSON.parse(msg)
                if (DiffObject(Template, objSignal)) {
                    Log("接收到交易信号指令:", objSignal)
                    buffSignal.push(objSignal)
                    
                    // 检查交易量、交易所编号
                    if (!CheckSignal(objSignal)) {
                        continue
                    }
                    
                    // 创建任务
                    if (objSignal["Flag"] == "TestSignal") {
                        Log("收到测试消息:", JSON.stringify(objSignal))
                    } else {
                        manager.newTask(objSignal)
                    }                    
                } else {
                    Log("指令无法识别", signal)
                }
            } else {
                Sleep(1000 * SleepInterval)
            }

            // 处理任务
            manager.process()
            
            // 状态栏显示信号
            if (buffSignal.length > maxBuffSignalRowDisplay) {
                buffSignal.shift()
            }
            var buffSignalTbl = {
                "type" : "table",
                "title" : "信号记录",
                "cols" : ["Flag", "Exchange", "Currency", "ContractType", "Price", "Action", "Amount"],
                "rows" : []
            }
            for (var i = buffSignal.length - 1 ; i >= 0 ; i--) {
                buffSignalTbl.rows.push([buffSignal[i].Flag, buffSignal[i].Exchange, buffSignal[i].Currency, buffSignal[i].ContractType, buffSignal[i].Price, buffSignal[i].Action, buffSignal[i].Amount])
            }

            LogStatus(_D(), "\n", "`" + JSON.stringify(buffSignalTbl) + "`")            
        } catch (error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
        }        
    }
}

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

  • parâmetro de porta: Se você usar o protocolo HTTP, só poderá definir a porta 80 no Trading View.
  • Parâmetro serverIP: Insira o endereço IP público do servidor.
  • Parâmetro initCode: pode ser usado para alternar o endereço base para testes no ambiente de teste do Exchange.

Comparado com a estratégia de usar a API estendida para acessar sinais externos, a mudança de estratégia não é grande, apenas adicionando umserverFuncA função de processamento do serviço Http usa o método de passagem de mensagens multithread recentemente adicionado pela plataforma FMZ:postMessage/peekMessage, o resto do código permanece quase inalterado.

Lista de permissões de IP

Como as solicitações do webhook do Trading View são enviadas apenas dos seguintes endereços IP:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Então adicionamos um parâmetro à estratégiaipWhiteList, usado para definir a lista de permissões de IP. Todas as solicitações que não estiverem nesta lista de permissões de endereço IP serão ignoradas.

        // 校验IP地址
        var fromIP = ctx.remoteAddr().split(":")[0]        
        if (ipWhiteList && ipWhiteList.length > 0) {
            var ipList = ipWhiteList.split(",")
            if (!ipList.includes(fromIP)) {
                ctx.setStatus(500)
                ctx.write("IP address not in white list")
                Log("500 Error: IP address not in white list", "#FF0000")
                return 
            }
        }

Verificar senha

Adicione um parâmetro à estratégiapassPhrase, usada para definir a senha de verificação. Essa senha é configurada nas configurações de URL do Webhook no Trading View. Solicitações que não correspondem à senha de verificação serão ignoradas.

Por exemplo, definimos:test123456

        // 校验口令
        var pass = ctx.rawQuery().length > 0 ? ctx.query("passPhrase") : ""
        if (passPhrase && passPhrase.length > 0) {
            if (pass != passPhrase) {
                ctx.setStatus(500)
                ctx.write("Authentication failed")
                Log("500 Error: Authentication failed", "#FF0000")
                return 
            }
        }

Sinal externo

Use o script PINE da plataforma Trading View como fonte de gatilho de sinal externo e selecione aleatoriamente um dos scripts PINE lançados oficialmente pela Trading View:

//@version=6
strategy("MovingAvg Cross", overlay=true)
length = input(9)
confirmBars = input(1)
price = close
ma = ta.sma(price, length)
bcond = price > ma
bcount = 0
bcount := bcond ? nz(bcount[1]) + 1 : 0
if (bcount == confirmBars)
	strategy.entry("MACrossLE", strategy.long, comment="long")
scond = price < ma
scount = 0
scount := scond ? nz(scount[1]) + 1 : 0
if (scount == confirmBars)
	strategy.entry("MACrossSE", strategy.short, comment="short")

Claro, você também pode executar scripts PINE diretamente na plataforma FMZ para executar transações reais, mas se quiser que a plataforma Trading View execute scripts PINE para enviar sinais, você só pode usar as soluções que discutimos.

Precisamos focar na função order deste script. Para adaptar este script PINE à mensagem em nossa solicitação de webhook, precisamos modificar a função order na função transaction.commentMencionaremos isso mais adiante no artigo.

Configurações do WebhookUrl e do corpo da solicitação

As configurações de WebhookUrl e do corpo da solicitação são basicamente as mesmas do método API estendido anterior para acessar sinais externos. As mesmas partes não serão repetidas neste artigo. Você pode consultar o artigo anterior.

Webhook Url

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Quando adicionamos este script PINE a um gráfico de um mercado no Trading View (escolhemos o mercado de contratos perpétuos ETH_USDT da Binance para nosso teste), podemos ver que o script começou a funcionar. Em seguida, adicionamos um alarme ao script, conforme mostrado na captura de tela.

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Configurações de URL do Webhook: O código da política foi projetado para gerar automaticamente a URL do webhook. Precisamos apenas copiá-lo do log no início da execução da política.

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456

O Trading View estipula que a URL do Webhook só pode usar a porta 80 para solicitações HTTP, então também definimos o parâmetro de porta como 80 na estratégia, para que você possa ver que a porta do link da URL do Webhook gerada pela estratégia também é 80.

Mensagem do corpo

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Em seguida, defina a mensagem do corpo da solicitação na aba “Configurações”, conforme mostrado na captura de tela.

{
    "Flag":"{{strategy.order.id}}",
    "Exchange":1,
    "Currency":"ETH_USDT",
    "ContractType":"swap",
    "Price":"-1",
    "Action":"{{strategy.order.comment}}",
    "Amount":"{{strategy.order.contracts}}"
}

Você se lembra do código do pedido no script PINE mencionado anteriormente? Vamos tomar como exemplo a abertura de uma posição longa no código:

strategy.entry("MACrossLE", strategy.long, comment="long")

“MACrossLE” é o conteúdo preenchido para “{{strategy.order.id}}” quando o alarme for acionado no futuro.

“long” é o conteúdo preenchido em “{{strategy.order.comment}}” quando o alarme for disparado no futuro. Os sinais identificados na estratégia são (captura de tela abaixo):

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Portanto, as configurações devem ser consistentes. Aqui definimos “longo” e “curto” para a função de ordem, indicando os sinais para abrir uma posição longa ou curta.

O script PINE não especifica a quantidade do pedido para cada pedido, então quando o Trading View envia uma mensagem de alerta, ele usa a quantidade do pedido padrão para preencher a parte “{{strategy.order.contracts}}”.

Teste real

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Quando o script PINE em execução no Trading View executa a função de negociação, porque configuramos um alarme de URL do Webhook, a plataforma Trading View enviará uma solicitação POST para o serviço HTTP integrado à nossa estratégia. Esta consulta de solicitação contém parâmetros de senha para autenticaçãopassPhrase. O corpo da solicitação real recebida é semelhante a este:

Discussão sobre recepção de sinal externo da plataforma FMZ: Uma solução completa para recepção de sinais com serviço HTTP integrado na estratégia

Em seguida, nossa estratégia executa as operações de transação correspondentes com base na mensagem neste corpo.

Pode-se observar que a estratégia realiza negociação de sinais sincronizados no ambiente de simulação OKX de acordo com o script PINE no Trading View.

Endereço da Política

https://www.fmz.com/strategy/475235

Obrigado pela sua atenção ao FMZ Quantitative e obrigado pela leitura.