[TOC]
Dans l’article précédent« Discussion sur la réception de signaux externes sur la plateforme FMZ : API étendue vs service HTTP intégré à la stratégie »Au cours de la discussion, nous avons comparé deux manières différentes de recevoir des signaux externes pour le trading programmatique et analysé les détails. La solution consistant à utiliser l’API d’extension de la plateforme FMZ pour recevoir des signaux externes dispose déjà d’une stratégie complète dans la bibliothèque de stratégies de la plateforme. Dans cet article, nous allons mettre en œuvre une solution complète consistant à utiliser le service HTTP intégré à la stratégie pour recevoir des signaux.
En suivant la stratégie précédente consistant à utiliser l’API d’extension FMZ pour accéder aux signaux Trading View, nous utilisons le format de message précédent, la méthode de traitement des messages, etc. et apportons des modifications simples à la stratégie.
Étant donné que les services intégrés à la politique peuvent utiliser HTTP ou HTTPS, pour une démonstration simple, nous utilisons le protocole HTTP, ajoutons une vérification de la liste blanche IP et ajoutons une vérification du mot de passe. S’il est nécessaire d’accroître davantage la sécurité, le service intégré à la politique peut être conçu comme un service 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)
}
}
}
Par rapport à la stratégie d’utilisation de l’API étendue pour accéder aux signaux externes, le changement de stratégie n’est pas important, il suffit d’ajouter unserverFunc
La fonction de traitement du service Http utilise la méthode de transmission de messages multithread nouvellement ajoutée par la plateforme FMZ :postMessage
/peekMessage
, le reste du code reste presque inchangé.
Étant donné que les requêtes du webhook de Trading View ne sont envoyées qu’à partir des adresses IP suivantes :
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
Nous ajoutons donc un paramètre à la stratégieipWhiteList
, utilisé pour définir la liste blanche IP. Toutes les demandes qui ne figurent pas dans cette liste blanche d’adresses IP seront ignorées.
// 校验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
}
}
Ajouter un paramètre à la stratégiepassPhrase
, utilisé pour définir le mot de passe de vérification. Ce mot de passe est configuré dans les paramètres d’URL du Webhook sur Trading View. Les demandes qui ne correspondent pas au mot de passe de vérification seront ignorées.
Par exemple, nous définissons :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
}
}
Utilisez le script PINE de la plateforme Trading View comme source de déclenchement du signal externe et sélectionnez au hasard l’un des scripts PINE officiellement publiés par 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")
Bien sûr, vous pouvez également exécuter des scripts PINE directement sur la plateforme FMZ pour exécuter des transactions réelles, mais si vous souhaitez que la plateforme Trading View exécute des scripts PINE pour envoyer des signaux, vous ne pouvez utiliser que les solutions dont nous avons parlé.
Nous devons nous concentrer sur la fonction de commande de ce script. Afin d’adapter ce script PINE au message de notre requête webhook, nous devons modifier la fonction de commande dans la fonction de transaction.comment
Nous en parlerons plus tard dans l’article.
Les paramètres de WebhookUrl et du corps de la requête sont fondamentalement les mêmes que ceux de la méthode API étendue précédente pour accéder aux signaux externes. Les mêmes parties ne seront pas répétées dans cet article. Vous pouvez vous référer à l’article précédent.
Lorsque nous ajoutons ce script PINE à un graphique d’un marché sur Trading View (nous avons choisi le marché des contrats perpétuels ETH_USDT de Binance pour notre test), nous pouvons voir que le script a commencé à fonctionner. Ensuite, nous ajoutons une alarme au script comme indiqué dans la capture d’écran.
Paramètres de l’URL du webhook : Le code de la politique a été conçu pour générer automatiquement l’URL du webhook. Il suffit de le copier à partir du journal au début de l’exécution de la politique.
http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View stipule que l’URL Webhook ne peut utiliser que le port 80 pour les requêtes HTTP, nous avons donc également défini le paramètre de port sur 80 dans la stratégie, vous pouvez donc voir que le port de lien de l’URL Webhook générée par la stratégie est également 80.
Définissez ensuite le corps du message de la demande dans l’onglet « Paramètres » comme indiqué dans la capture d’écran.
{
"Flag":"{{strategy.order.id}}",
"Exchange":1,
"Currency":"ETH_USDT",
"ContractType":"swap",
"Price":"-1",
"Action":"{{strategy.order.comment}}",
"Amount":"{{strategy.order.contracts}}"
}
Vous vous souvenez du code de commande dans le script PINE qui vient d’être mentionné ? Prenons comme exemple l’ouverture d’une position longue dans le code :
strategy.entry("MACrossLE", strategy.long, comment="long")
« MACrossLE » est le contenu renseigné pour « {{strategy.order.id}} » lorsque l’alarme est déclenchée dans le futur.
“long” est le contenu renseigné dans “{{strategy.order.comment}}” lorsque l’alarme est déclenchée dans le futur. Les signaux identifiés dans la stratégie sont (capture d’écran ci-dessous) :
Les paramètres doivent donc être cohérents. Ici, nous définissons « long » et « short » pour la fonction de commande, indiquant les signaux pour l’ouverture d’une position longue ou courte.
Le script PINE ne spécifie pas la quantité de commande pour chaque commande, donc lorsque Trading View envoie un message d’alerte, il utilise la quantité de commande par défaut pour remplir la partie « {{strategy.order.contracts}} ».
Lorsque le script PINE exécuté sur Trading View exécute la fonction de trading, étant donné que nous avons configuré une alarme URL Webhook, la plateforme Trading View enverra une requête POST au service HTTP intégré à notre stratégie. Cette requête contient des paramètres de mot de passe pour l’authentificationpassPhrase
. Le corps de la requête réellement reçue est similaire à ceci :
Ensuite, notre stratégie exécute les opérations de transaction correspondantes en fonction du message dans ce corps.
On peut voir que la stratégie effectue un trading de signaux synchronisés dans l’environnement de simulation OKX selon le script PINE sur Trading View.
Merci de votre attention à FMZ Quantitative et merci de votre lecture.