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

FMZ 플랫폼의 외부 신호 수신에 대한 논의: 전략 내 내장 Http 서비스와 함께 신호 수신에 대한 완전한 솔루션

저자:FMZ~리디아, 창작: 2024-12-18 09:22:33, 업데이트: 2024-12-18 09:24:49

전문

이전 기사 에서FMZ 플랫폼의 외부 신호 수신에 대한 토론: 확장 API 대 전략 내장 HTTP 서비스, 우리는 프로그래밍 트레이딩을 위해 외부 신호를 수신하는 두 가지 다른 방법을 비교하고 세부 사항을 분석했습니다. FMZ 플랫폼 확장 API를 사용하여 외부 신호를 수신하는 솔루션은 플랫폼 전략 라이브러리에서 완전한 전략을 가지고 있습니다. 이 기사에서는 신호를 수신하기 위해 전략 내장 Http 서비스를 사용하는 완전한 솔루션을 구현합니다.

전략 실행

트레이딩 뷰 신호에 접근하기 위해 FMZ 확장 API를 사용하는 이전 전략을 따라 이전 메시지 형식, 메시지 처리 방법 등을 사용하여 전략에 간단한 수정 작업을 수행합니다.

전략에 내장된 서비스는 Http 또는 HTTPS를 사용할 수 있기 때문에 간단한 시범을 위해 우리는 Http 프로토콜을 사용하여 IP 백자 목록 검증을 추가하고 암호 검증을 추가합니다. 보안을 추가적으로 높여야 할 경우 전략에 내장된 서비스는 Https 서비스로 설계 될 수 있습니다.

//Signal structure
var Template = {
    Flag: "45M103Buy",     // Logo, can be specified at will
    Exchange: 1,           // Designated exchange trading pairs
    Currency: "BTC_USDT",  // Trading pairs
    ContractType: "spot",  // Contract type, swap, quarter, next_quarter, spot fill in spot
    Price: "{{close}}",    // Opening or closing price, -1 is the market price
    Action: "buy",         // Transaction type [buy: spot buy, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long]
    Amount: "1",           // Trading volume
}

var Success = "#5cb85c"    // Success color
var Danger = "#ff0000"     // Danger color
var Warning = "#f0ad4e"    // Warning color
var buffSignal = []

// Http service
function serverFunc(ctx, ipWhiteList, passPhrase) {
    var path = ctx.path()
    if (path == "/CommandRobot") {
        // Verify IP address
        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 
            }
        }

        // Verify password
        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)
    }
}

// Check signal message format
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("The minimum exchange number is 1 and is an integer.", Danger)
        return
    }
    if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
        Log("The trading volume cannot be less than 0 and must be a numeric type.", typeof(Signal.Amount), Danger)
        return
    }
    if (typeof(Signal.Price) != "number") {
        Log("Price must be a numeric value", Danger)
        return
    }
    if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
        Log("The instruction is to operate spot goods, and the Action is wrong, Action:", Signal.Action, Danger)
        return 
    }
    if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
        Log("The instruction is to operate futures, and the Action is wrong, Action:", Signal.Action, Danger)
        return 
    }
    return true
}

// Signal processing object
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("Create a task:", 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) {
            // If it is not spot, set up a contract
            e.SetContractType(task.ContractType)
        } else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
            isFutures = false 
        } else {
            task.error = "The ContractType in the instruction does not match the configured exchange object type"
            return 
        }
        
        var depth = e.GetDepth()
        if (!depth || !depth.Bids || !depth.Asks) {
            task.error = "Abnormal order book data"
            return 
        }
        
        if (depth.Bids.length == 0 && depth.Asks.length == 0) {
            task.error = "No orders on the market"
            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 = "Failed to get precision"
            return 
        }
        
        e.SetPrecision(self.pricePrecision, self.amountPrecision)
        
        // buy: spot purchase, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long
        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 = "Wrong transaction direction:" + 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 = "Wrong transaction direction:" + task.Action
                return 
            }
        }
        var id = tradeFunc(task.Price, task.Amount)
        if (!id) {
            task.error = "Order failed"
        }
        
        task.finished = true
    }
    
    return self
}

function main() {
    // Reset log information
    if (isResetLog) {
        LogReset(1)
    }

    Log("Transaction type [buy: spot buy, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long]", Danger)
    Log("Instruction templates:", 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}`)
    }

    // Creating an Http built-in service
    __Serve("http://0.0.0.0:" + port, serverFunc, ipWhiteList, passPhrase)

    // Initialize the code to execute
    if (initCode && initCode.length > 0) {
        try {
            Log("Execute the initialization code:", initCode)
            eval(initCode)
        } catch(error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
        }
    }    

    // Create a signal management object
    var manager = createManager()
    
    while (true) {
        try {
            // Detect interactive controls for testing
            var cmd = GetCommand()
            if (cmd) {
                // Send Http request, simulate test
                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": JSON.stringify(signal)})
                        Log("Test request response:", ret)
                    } else {
                        var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`, {"method": "POST", "body": JSON.stringify(signal)})
                        Log("Test request response:", ret)
                    }                    
                }
            }

            // Detect the message that the built-in Http service notifies the main thread after receiving the request, and writes it to the task queue of the manager object
            var msg = threading.mainThread().peekMessage(-1)
            if (msg) {
                Log("Receive message msg:", msg)
                var objSignal = JSON.parse(msg)
                if (DiffObject(Template, objSignal)) {
                    Log("Receive trading signal instructions:", objSignal)
                    buffSignal.push(objSignal)
                    
                    // Check trading volume, exchange ID
                    if (!CheckSignal(objSignal)) {
                        continue
                    }
                    
                    // Create a task
                    if (objSignal["Flag"] == "TestSignal") {
                        Log("Received test message:", JSON.stringify(objSignal))
                    } else {
                        manager.newTask(objSignal)
                    }                    
                } else {
                    Log("Command not recognized", signal)
                }
            } else {
                Sleep(1000 * SleepInterval)
            }

            // Processing tasks
            manager.process()
            
            // Status bar displays signal
            if (buffSignal.length > maxBuffSignalRowDisplay) {
                buffSignal.shift()
            }
            var buffSignalTbl = {
                "type" : "table",
                "title" : "Signal recording",
                "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

  • 포트 매개 변수: 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

따라서, 우리는 매개 변수를 추가합니다ipWhiteListIP 주소 백 목록에 없는 모든 요청은 무시됩니다.

        // Verify IP address
        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검증 암호를 설정하는 전략. 이 암호는 트레이딩 뷰에서 Webhook url 설정에서 구성됩니다. 검증 암호와 일치하지 않는 요청은 무시됩니다.

예를 들어, 우리는 이렇게 설정합니다.test123456.

        // Verify password
        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 스크립트를 실행할 수 있습니다. 하지만 만약 당신이 Trading View 플랫폼에서 신호를 전송하기 위해 PINE 스크립트를 실행하기를 원한다면, 당신은 우리가 논의한 솔루션만을 사용할 수 있습니다.

우리는 이 스크립트의 주문 배치 기능에 집중해야 합니다. 우리의 웹 요청의 메시지에 이 PINE 스크립트를 적응하기 위해, 우리는 거래 기능을 수정해야 합니다.comment이 기사 에서 나중에 언급 할 것 입니다.

WebhookURL 및 요청 본체 설정

WebhookUrl 및 요청 본체의 설정은 기본적으로 외부 신호에 액세스하기위한 이전 확장 API 방법과 동일합니다. 동일한 부분은이 기사에서 반복되지 않습니다. 이전 기사를 참조 할 수 있습니다.

웹후크 URL

img

이 PINE 스크립트를 트레이딩 뷰에서 시장의 차트에 추가한 후 (우리는 테스트를 위해 Binance의 ETH_USDT 영구계약 시장을 선택합니다), 스크립트가 작동하기 시작했다는 것을 볼 수 있습니다.

img

웹후크 URL 설정: 스트레이티 코드는 웹후크 URL을 자동으로 생성하도록 설계되었습니다. 우리는 전략 작업의 시작에서 로그에서 복사해야합니다.

img

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

트레이딩 뷰는 웹허크 URL가 HTTP 요청에 포트 80만을 사용할 수 있다고 규정하고 있습니다. 그래서 우리는 또한 전략에서 포트 매개 변수를 80로 설정합니다.

몸 메시지

img

다음에는 스크린 샷에서 보이는 것처럼 Settings 탭에서 요청 본체 메시지를 설정합니다.

{
    "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은 경고가 미래에 발생했을 때 {{전략.명령.평론}}에 채워지는 내용입니다. 전략에서 식별된 신호는 (아래의 스크린샷):

img

따라서 설정은 일관성 있어야 합니다. 여기서 우리는 longshort을 순서 함수로 설정하여, 긴 개척과 짧은 개척 신호를 나타냅니다.

PINE 스크립트는 각 주문에 대한 주문 양을 지정하지 않으므로, 트레이딩 뷰가 경고 메시지를 전송할 때, 기본 주문 양을 사용하여 {{전략.order.contracts}} 부분을 채우게 됩니다.

실시간 거래 테스트

img

img

트레이딩 뷰에서 실행되는 PINE 스크립트가 트레이딩 기능을 실행하면, 우리가 웹 룩 URL 알림을 설정했기 때문에, 트레이딩 뷰 플랫폼은 우리의 전략의 내장 Http 서비스에 POST 요청을 보낼 것입니다. 이 요청 쿼리는 비밀번호 매개 변수를 포함합니다passPhrase확인을 위해 수신된 실제 요청 기관은 다음과 같습니다.

img

그러면 우리의 전략은 이 메세지를 기반으로 해당 거래 거래를 실행합니다.

전략은 트레이딩 뷰에서 PINE 스크립트에 따라 OKX 시뮬레이션 환경에서 동기화 된 신호 거래를 수행한다는 것을 알 수 있습니다.

전략 주소

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

FMZ Quant에 대한 관심에 감사드립니다. 그리고 읽어 주셔서 감사합니다.


더 많은