리소스 로딩... 로딩...

FMZ 플랫폼 외부 신호 수신에 대한 탐구: 전략 내장 HTTP 서비스 신호 수신의 전체 방안

저자:발명가들의 수량화 - 작은 꿈, 창작: 2024-12-17 11:44:07, 업데이트: 2024-12-17 16:08:11

[TOC]

img

전문

지난 기사에서"FMZ 플랫폼의 외부 신호 수신에 대한 탐구: 확장 API 대 전략 내장 HTTP 서비스"이 토론에서 우리는 두 가지 다른 외부 신호를 수신하는 방법을 비교하고 세부적으로 분석했습니다. FMZ 플랫폼을 사용하여 API를 확장하여 외부 신호를 수신하는 방법은 플랫폼 정책 라이브러리에서 이미 완전한 전략이 있습니다.

전략 실현

이전 FMZ 확장 API를 사용하여 트레이딩 뷰 신호에 액세스하는 전략을 모방하여 이전 메시지 형식, 메시지 처리 방식 등을 따라 간단한 변경을 수행했습니다.

정책 내장 서비스는 HTTP 또는 HTTPS로 사용할 수 있기 때문에 간단한 시범을 위해 우리는 HTTP 프로토콜을 사용하여 IP 화이트리스트 검증을 추가하고 암호 검증을 추가합니다. 보안을 추가할 필요가 있다면 정책 내장 서비스를 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)
        }        
    }
}

img

  • port 파라미터: HTTP 프로토콜을 사용하는 경우 트레이딩 뷰에서 80개의 포트를 설정할 수 있습니다.
  • serverIP 매개 변수: 서버의 공개 IP 주소를 채우십시오.
  • initCode 매개 변수: 데이터베이스 주소를 전환하는 데 사용할 수 있으며, 거래소 테스트 환경 테스트를 위해 사용됩니다.

확장 API 방식을 사용하여 외부 신호에 액세스하는 정책에 비해 정책 변경은 크지 않습니다.serverFuncHttp 서비스 처리 기능은 FMZ 플랫폼의 새로운 멀티 스레드 메시지 전송 방식을 사용합니다.postMessage/peekMessage다른 코드는 거의 변경되지 않았습니다.

IP 화이트리스트

트레이딩 뷰의 웹후크로 인해 요청은 다음과 같은 IP 주소에서만 발송됩니다.

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

그래서 우리는 전략에 한 개의 변수를 더했습니다.ipWhiteList이 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 
            }
        }

비밀번호 확인

정책에 변수를 추가합니다.passPhrase이 암호는 트레이딩 뷰에서 웹후크 url 설정에서 설정되어 있으며, 확인 암호가 일치하지 않는 요청은 무시됩니다.

예를 들어, 우리는 다음과 같이 설정했습니다.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 
            }
        }

외부 신호

트레이딩 뷰 플랫폼의 PINE 스크립트를 외부 신호 트리거 소스로 사용하여, 트레이딩 뷰의 공식 공개된 PINE 스크립트 중 임의로 하나를 선택하십시오.

//@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")

물론 FMZ 플랫폼에서 직접 PINE 스크립트를 실행하여 실제 거래를 수행할 수도 있지만, 트레이딩 뷰 플랫폼에서 PINE 스크립트를 실행하여 신호를 발송하기를 원한다면, 우리가 논의한 방법만 사용할 수 있습니다.

우리는 이 스크립트의 하위 함수에 관심을 가져야 합니다. 이 PINE 스크립트가 우리의 웹후크 요청의 메시지에 적합하도록 하기 위해 우리는 트랜잭션 함수의comment이 글은 이 부분의 다른 부분에서 볼 수 있습니다.

웹후크URL 및 요청체 설정

Webhook Url와 Request Body의 설정은 이전 확장 API 방식과 거의 일치합니다.

웹후크 URL

img

우리가 이 PINE 스크립트를 트레이딩 뷰에서 어떤 시장에 추가한 다음 (우리는 이 시장을 테스트했습니다.

img

웹후크 URL 설정: 정책 코드는 웹후크 url를 자동으로 생성하도록 설계되어 있으며, 정책이 실행되는 로그에서 복사할 수 있습니다.

img

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

트레이딩 뷰의 규정에 따르면 웹휴크 url는 HTTP 요청에 80 포트를 사용할 수 있습니다. 그래서 전략적으로 우리는 또한 포트 파라미터를 80로 설정했습니다. 그래서 당신은 정책에서 생성된 웹휴크 url의 링크 포트가 80이라고 볼 수 있습니다.

body 뉴스

img

다음 스크린에 있는 "설정" 탭에서 요청된 body 메시지를 설정합니다.

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

이 문서는 우리가 방금 말한 PINE 스크립트에서 아래와 같은 코드를 기억하시나요?

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

"MACrossLE"는 미래의 경보를 촉발할 때strategy.order.id}}"를 채우는 내용입니다.

이 중 "long"는 미래의 경보를 유발할 때 "{{strategy.order.comment}}"에 대한 채용입니다.

img

따라서 일치하도록 설정해야 합니다. 여기 우리는 다음 단일 함수에 "long"와 "short"를 설정하여 더 많은, 빈 신호를 나타냅니다.

PINE 스크립트에서는 매번의 주문량이 얼마나 되는지 지정하지 않으므로, 트레이딩 뷰가 알람 메시지를 보낼 때, 기본 주문량 채우는 "{{strategy.order.contracts}}"를 사용합니다.

실제 디스크 테스트

img

img

트레이딩 뷰에서 실행되는 PINE 스크립트에서 트레이딩 함수를 실행할 때, 우리는 웹후크 url 경보를 설정했기 때문에, 트레이딩 뷰 플랫폼은 우리의 정책에 내장된 HTTP 서비스에 POST 요청을 전송합니다. 이 요청 쿼리는 검증을 위한 암호 파라미터를 포함합니다.passPhrase◎ 실제 요청체들은 다음과 같습니다:

img

그리고 우리의 전략은 이 body의 메시지에 따라 해당 거래 동작을 수행하는 것입니다.

볼 수 있듯이, 전략은 OKX 시뮬레이션 환경에서 PINE 스크립트에 따라 트레이딩 뷰에서 동기화 된 신호 거래를 수행합니다.

전략적 주소

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

FMZ의 양성에 관심을 가졌고 읽어주셔서 감사합니다.


더 많은