[TOC]

이전 기사에서“FMZ 플랫폼에서 외부 신호 수신에 대한 토론: 확장 API 대 전략적 내장 HTTP 서비스”토론에서 우리는 프로그래밍 방식 거래를 위한 외부 신호를 수신하는 두 가지 방법을 비교하고 세부 사항을 분석했습니다. FMZ 플랫폼 확장 API를 사용하여 외부 신호를 수신하는 솔루션은 이미 플랫폼 전략 라이브러리에 완전한 전략이 있습니다. 이 문서에서는 전략 내장 Http 서비스를 사용하여 신호를 수신하는 완전한 솔루션을 구현합니다.
이전 전략인 FMZ 확장 API를 사용하여 Trading View 신호에 접근하는 방법에 따라, 이전 메시지 형식, 메시지 처리 방법 등을 사용하고 전략을 간단히 수정합니다.
정책에 내장된 서비스는 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)
}
}
}

외부 신호에 접근하기 위해 확장 API를 사용하는 전략과 비교하면 전략 변경은 크지 않으며 단지 추가된 것일 뿐입니다.serverFuncHttp 서비스 처리 기능은 FMZ 플랫폼에서 새롭게 추가된 다중 스레드 메시지 전달 방식을 사용합니다.postMessage/peekMessage나머지 코드는 거의 변경되지 않았습니다.
Trading View 웹훅의 요청은 다음 IP 주소에서만 전송됩니다.
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
그래서 우리는 전략에 매개변수를 추가합니다ipWhiteList, IP 허용 목록을 설정하는 데 사용됩니다. 이 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, 검증 비밀번호를 설정하는 데 사용됩니다. 이 비밀번호는 Trading View의 Webhook 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
}
}
Trading View 플랫폼의 PINE 스크립트를 외부 신호 트리거 소스로 사용하고 Trading View에서 공식적으로 릴리스한 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과 요청 본문의 설정은 기본적으로 외부 신호에 접근하기 위한 이전 확장 API 방법과 동일합니다. 이 문서에서는 동일한 부분을 반복하지 않습니다. 이전 문서를 참조할 수 있습니다.

우리가 이 PINE 스크립트를 Trading View의 시장 차트에 추가했을 때(우리는 테스트를 위해 Binance의 ETH_USDT 영구 계약 시장을 선택했습니다), 스크립트가 작동하기 시작했음을 볼 수 있습니다. 그런 다음 스크린샷에 표시된 대로 스크립트에 알람을 추가합니다.

웹훅 URL 설정: 정책 코드는 웹훅 URL을 자동으로 생성하도록 설계되었습니다. 정책 실행 시작 시 로그에서 복사하기만 하면 됩니다.

http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View에서는 Webhook URL이 HTTP 요청에 대해 포트 80만 사용할 수 있다고 규정하고 있기 때문에 전략에서도 포트 매개변수를 80으로 설정했습니다. 따라서 전략에서 생성된 Webhook URL의 링크 포트도 80임을 확인할 수 있습니다.

그런 다음 스크린샷에 표시된 대로 “설정” 탭에서 요청 본문 메시지를 설정합니다.
{
"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}}“에 채워지는 내용입니다. 전략에서 식별된 신호는 다음과 같습니다(아래 스크린샷):

그러므로 설정은 일관성이 있어야 합니다. 여기서는 주문 함수에 대해 “롱”과 “숏”을 설정하여 롱 또는 숏 포지션을 열기 위한 신호를 나타냅니다.
PINE 스크립트는 각 주문에 대한 주문 수량을 지정하지 않으므로 Trading View에서 알림 메시지를 보내면 기본 주문 수량을 사용하여 “{{strategy.order.contracts}}” 부분을 채웁니다.


Trading View에서 실행되는 PINE 스크립트가 거래 기능을 실행할 때, Webhook URL 알람을 설정했기 때문에 Trading View 플랫폼은 전략에 내장된 HTTP 서비스에 POST 요청을 보냅니다. 이 요청 쿼리에는 인증을 위한 비밀번호 매개변수가 포함되어 있습니다.passPhrase. 실제로 수신된 요청 본문은 다음과 유사합니다.

그러면 우리의 전략은 이 본문의 메시지를 기반으로 해당 거래 작업을 실행합니다.
Trading View의 PINE 스크립트에 따르면 해당 전략은 OKX 시뮬레이션 환경에서 동기화된 신호 거래를 수행하는 것을 볼 수 있습니다.
FMZ Quantitative에 관심을 가져주셔서 감사드리고, 읽어주셔서 감사합니다.