avatar of 发明者量化-小小梦 发明者量化-小小梦
Seguir Mensajes Privados
4
Seguir
1271
Seguidores

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Creado el: 2024-12-17 11:44:07, Actualizado el: 2024-12-17 16:08:11
comments   0
hits   663

[TOC]

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Prefacio

En el artículo anterior“Discusión sobre la recepción de señales externas en la plataforma FMZ: API extendida vs estrategia de servicio HTTP integrado”En la discusión, comparamos dos formas diferentes de recibir señales externas para el trading programático y analizamos los detalles. La solución de utilizar la API de extensión de la plataforma FMZ para recibir señales externas ya cuenta con una estrategia completa en la biblioteca de estrategias de la plataforma. En este artículo, implementaremos una solución completa de utilizar la estrategia incorporada en el servicio HTTP para recibir señales.

Implementación de la estrategia

Siguiendo la estrategia anterior de utilizar la API de extensión FMZ para acceder a las señales de Trading View, utilizamos el formato de mensaje anterior, el método de procesamiento de mensajes, etc. y realizamos modificaciones simples a la estrategia.

Debido a que los servicios integrados en la política pueden usar Http o HTTPS, para una demostración simple, usamos el protocolo Http, agregamos verificación de lista blanca de IP y agregamos verificación de contraseña. Si es necesario aumentar aún más la seguridad, el servicio integrado de políticas se puede diseñar como un servicio 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)
        }        
    }
}

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

  • Parámetro de puerto: si utiliza el protocolo HTTP, solo puede configurar el puerto 80 en Trading View.
  • Parámetro serverIP: Ingrese la dirección IP pública del servidor.
  • Parámetro initCode: se puede utilizar para cambiar la dirección base para realizar pruebas en el entorno de prueba de intercambio.

En comparación con la estrategia de usar la API extendida para acceder a señales externas, el cambio de estrategia no es grande, solo se agrega unaserverFuncLa función de procesamiento del servicio Http utiliza el método de paso de mensajes multiproceso recientemente agregado por la plataforma FMZ:postMessage/peekMessage, los demás códigos prácticamente no sufren modificaciones.

Lista blanca de IP

Dado que las solicitudes del webhook de Trading View solo se envían desde las siguientes direcciones IP:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Así que añadimos un parámetro a la estrategiaipWhiteList, se utiliza para configurar la lista blanca de direcciones IP. Se ignorarán todas las solicitudes que no estén en esta lista blanca de direcciones IP.

        // 校验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 contraseña

Añadir un parámetro a la estrategiapassPhrase, que se utiliza para configurar la contraseña de verificación. Esta contraseña se configura en la configuración de la URL del webhook en Trading View. Las solicitudes que no coincidan con la contraseña de verificación se ignorarán.

Por ejemplo, establecemos: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 
            }
        }

Señal externa

Utilice el script PINE de la plataforma Trading View como fuente de activación de señal externa y seleccione aleatoriamente uno de los scripts PINE publicados oficialmente por 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")

Por supuesto, también puedes ejecutar scripts PINE directamente en la plataforma FMZ para ejecutar transacciones reales, pero si quieres que la plataforma Trading View ejecute scripts PINE para enviar señales, solo puedes usar las soluciones que hemos comentado.

Necesitamos centrarnos en la función de pedido de este script. Para adaptar este script PINE al mensaje de nuestra solicitud de webhook, debemos modificar la función de pedido en la función de transacción.commentLo mencionaremos más adelante en el artículo.

Configuración de WebhookUrl y cuerpo de solicitud

La configuración de WebhookUrl y el cuerpo de la solicitud son básicamente los mismos que los del método API extendido anterior para acceder a señales externas. No se repetirán las mismas partes en este artículo. Puedes consultar el artículo anterior.

Webhook Url

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Cuando agregamos este script PINE a un gráfico de un mercado en Trading View (elegimos el mercado de contratos perpetuos ETH_USDT de Binance para nuestra prueba), podemos ver que el script ha comenzado a funcionar. Luego agregamos una alarma al script como se muestra en la captura de pantalla.

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Configuración de URL de webhook: El código de la política se ha diseñado para generar automáticamente la URL del webhook. Solo tenemos que copiarlo del registro al comienzo de la ejecución de la política.

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

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

Trading View estipula que la URL del webhook solo puede usar el puerto 80 para solicitudes HTTP, por lo que también establecemos el parámetro de puerto en 80 en la estrategia, para que pueda ver que el puerto de enlace de la URL del webhook generada por la estrategia también es 80.

Mensaje corporal

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Luego, configure el mensaje del cuerpo de la solicitud en la pestaña “Configuración” como se muestra en la captura de pantalla.

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

¿Recuerdas el código de pedido en el script PINE que acabo de mencionar? Tomemos como ejemplo la apertura de una posición larga en el código:

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

“MACrossLE” es el contenido que se completa para “{{strategy.order.id}}” cuando se activa la alarma en el futuro.

“long” es el contenido que se rellena en “{{strategy.order.comment}}” cuando se activa la alarma en el futuro. Las señales identificadas en la estrategia son (captura de pantalla a continuación):

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Por lo tanto, los ajustes deben ser consistentes. Aquí establecemos “largo” y “corto” para la función de orden, indicando las señales para abrir una posición larga o corta.

El script PINE no especifica la cantidad de pedido para cada pedido, por lo que cuando Trading View envía un mensaje de alerta, utiliza la cantidad de pedido predeterminada para completar la parte “{{strategy.order.contracts}}”.

Prueba real

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Cuando el script PINE que se ejecuta en Trading View ejecuta la función de trading, debido a que hemos configurado una alarma de URL de Webhook, la plataforma Trading View enviará una solicitud POST al servicio HTTP integrado en nuestra estrategia. Esta consulta de solicitud contiene parámetros de contraseña para la autenticación.passPhrase. El cuerpo de la solicitud real recibida es similar a esto:

Discusión sobre la recepción de señales externas de la plataforma FMZ: Una solución completa para la recepción de señales con servicio Http integrado en la estrategia

Luego, nuestra estrategia ejecuta las operaciones de transacción correspondientes en función del mensaje de este cuerpo.

Se puede observar que la estrategia realiza operaciones de señales sincronizadas en el entorno de simulación OKX de acuerdo con el script PINE en Trading View.

Dirección de la política

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

Gracias por su atención a FMZ Quantitative y gracias por leer.