[TOC]
지난 기사에서"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)
}
}
}
확장 API 방식을 사용하여 외부 신호에 액세스하는 정책에 비해 정책 변경은 크지 않습니다.serverFunc
Http 서비스 처리 기능은 FMZ 플랫폼의 새로운 멀티 스레드 메시지 전송 방식을 사용합니다.postMessage
/peekMessage
다른 코드는 거의 변경되지 않았습니다.
트레이딩 뷰의 웹후크로 인해 요청은 다음과 같은 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
이 글은 이 부분의 다른 부분에서 볼 수 있습니다.
Webhook Url와 Request Body의 설정은 이전 확장 API 방식과 거의 일치합니다.
우리가 이 PINE 스크립트를 트레이딩 뷰에서 어떤 시장에 추가한 다음 (우리는 이 시장을 테스트했습니다.
웹후크 URL 설정: 정책 코드는 웹후크 url를 자동으로 생성하도록 설계되어 있으며, 정책이 실행되는 로그에서 복사할 수 있습니다.
http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
트레이딩 뷰의 규정에 따르면 웹휴크 url는 HTTP 요청에 80 포트를 사용할 수 있습니다. 그래서 전략적으로 우리는 또한 포트 파라미터를 80로 설정했습니다. 그래서 당신은 정책에서 생성된 웹휴크 url의 링크 포트가 80이라고 볼 수 있습니다.
다음 스크린에 있는 "설정" 탭에서 요청된 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}}"에 대한 채용입니다.
따라서 일치하도록 설정해야 합니다. 여기 우리는 다음 단일 함수에 "long"와 "short"를 설정하여 더 많은, 빈 신호를 나타냅니다.
PINE 스크립트에서는 매번의 주문량이 얼마나 되는지 지정하지 않으므로, 트레이딩 뷰가 알람 메시지를 보낼 때, 기본 주문량 채우는 "{{strategy.order.contracts}}"를 사용합니다.
트레이딩 뷰에서 실행되는 PINE 스크립트에서 트레이딩 함수를 실행할 때, 우리는 웹후크 url 경보를 설정했기 때문에, 트레이딩 뷰 플랫폼은 우리의 정책에 내장된 HTTP 서비스에 POST 요청을 전송합니다. 이 요청 쿼리는 검증을 위한 암호 파라미터를 포함합니다.passPhrase
◎ 실제 요청체들은 다음과 같습니다:
그리고 우리의 전략은 이 body의 메시지에 따라 해당 거래 동작을 수행하는 것입니다.
볼 수 있듯이, 전략은 OKX 시뮬레이션 환경에서 PINE 스크립트에 따라 트레이딩 뷰에서 동기화 된 신호 거래를 수행합니다.
FMZ의 양성에 관심을 가졌고 읽어주셔서 감사합니다.