В предыдущей статьеДискуссия по внешнему приему сигнала платформы FMZ: расширенный API против стратегии встроенного HTTP-сервиса, мы сравнили два различных способа получения внешних сигналов для программируемой торговли и проанализировали детали. Решение использования расширенного API платформы FMZ для приема внешних сигналов имеет полную стратегию в библиотеке стратегии платформы. В этой статье мы реализуем полное решение использования стратегии встроенной службы Http для получения сигналов.
Следуя предыдущей стратегии использования расширения FMZ API для доступа к сигналам Trading View, мы используем предыдущий формат сообщения, метод обработки сообщений и т. Д. и вносим простые изменения в стратегию.
Поскольку встроенные сервисы в стратегии могут использовать Http или HTTPS, для простой демонстрации мы используем протокол Http, добавляем проверку IP-белого списка и добавляем проверку пароля.
//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)
}
}
}
По сравнению со стратегией использования расширенного API для доступа к внешним сигналам, стратегия не сильно меняется.serverFunc
Функция обработки HTTP-сервиса и использует метод передачи сообщений с несколькими потоками, недавно добавленный платформой FMZ:postMessage
/ peekMessage
Остальные коды почти не изменились.
Поскольку запросы от веб-хука Trading View
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
Поэтому мы добавляем параметрipWhiteList
Все запросы, не входящие в список IP-адресов, будут проигнорированы.
// 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
Этот пароль настроен в настройках URL-адреса Webhook в Trading View. Запросы, которые не соответствуют паролю проверки, будут проигнорированы.
Например, мы устанавливаем: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 платформы Trading View в качестве внешнего источника запуска сигнала и выберите один из скриптов PINE, случайным образом выпущенных 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")
Конечно, вы также можете запускать PINE-скрипты непосредственно на платформе FMZ для выполнения прямых торгов, но если вы хотите, чтобы платформа Trading View запускала PINE-скрипты для отправки сигналов, вы можете использовать только решения, которые мы обсудили.
Чтобы адаптировать этот скрипт PINE к сообщению в нашем запросе webhook, мы должны изменить торговую функциюcomment
, о котором мы поговорим позже в статье.
Настройки WebhookUrl и request body в основном такие же, как и в предыдущем расширенном методе API для доступа к внешним сигналам.
После того, как мы добавили этот сценарий PINE в график рынка (мы выбираем Binance
Настройки URL-адреса веб-хока: Код Stratey был разработан для автоматического создания URL-адреса веб-взлома. Нам нужно только скопировать его из журнала в начале операции стратегии.
http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View предусматривает, что URL-адрес Webhook может использовать только порт 80 для запросов HTTP, поэтому мы также устанавливаем параметр порта на 80 в стратегии, поэтому мы можем видеть, что порт ссылки URL-адреса Webhook, генерируемого стратегией, также 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")
Здесь мы устанавливаем
Скрипт PINE не указывает количество заказов для каждого заказа, поэтому, когда Trading View отправляет предупредительное сообщение, он использует количество заказов по умолчанию для заполнения части
Когда скрипт PINE, запущенный в Trading View, выполняет торговую функцию, потому что мы установили предупреждение URL-адреса Webhook, платформа Trading View отправит запрос POST в встроенный Http-сервис нашей стратегии.passPhrase
Фактически полученный запрашивающий орган похож на следующий:
Затем наша стратегия выполняет соответствующие торговые операции на основе сообщения в этом теле.
Можно видеть, что стратегия выполняет синхронизированную торговлю сигналами в среде моделирования OKX в соответствии со сценарием PINE на Trading View.
Спасибо за внимание к FMZ Quant, и спасибо за чтение.