[TOC]

Трейдеры, которые часто используют TradingView, знают, что TradingView может отправлять сообщения на другие платформы. Ранее стратегия push-уведомлений TradingView была опубликована в библиотеке документов. Содержание push-сообщения было жестко закодировано в URL-адресе запроса, что было несколько негибким. В этой статье мы по-другому перепроектируем стратегию исполнения сигналов TradingView.
Некоторые начинающие студенты могут прийти в замешательство, увидев название этой статьи и описание выше, но это неважно! Давайте сначала проясним сценарии и принципы спроса. Дам вам понять, о чем я говорю. Хорошо, давайте перейдем к делу.
Сценарий спроса: После всех этих разговоров, что эта штука должна делать? Проще говоря, у нас есть много индикаторов, стратегий, кодов и т. д., которые мы можем использовать на TradingView. Их можно запускать непосредственно на TradingView, и они могут рисовать линии, рассчитывать, отображать торговые сигналы и т. д. Кроме того, TradingView предоставляет данные о ценах в режиме реального времени и достаточный объем данных K-line для облегчения расчета различных индикаторов. Эти скриптовые коды на TradingView называются языком PINE. Единственное неудобство — это реальная торговля на TradingView. Хотя язык PINE уже поддерживается в FMZ, его также можно запускать в режиме реального времени. Однако есть и ярые поклонники TradingView, которые все еще надеются размещать заказы на основе сигналов, посылаемых графиками TradingView. Эту потребность также может удовлетворить FMZ. В этой статье объясняются конкретные детали этого решения.
Принцип:

Весь план состоит из четырех основных частей, которые вкратце таковы:
| серийный номер | основной корпус | описывать |
|---|---|---|
| 1 | TradingView (Торговый вид на картинке) | TradingView запускает скрипт PINE, который может отправлять сигналы и получать доступ к расширенному API-интерфейсу FMZ. |
| 2 | Платформа FMZ (на фото платформа (сайт) FMZ) | Управляйте реальным рынком, отправляйте интерактивные инструкции на страницу реального рынка, а также используйте расширенный интерфейс API, чтобы платформа FMZ отправляла интерактивные инструкции в программу стратегии реального рынка на кастодиане. |
| 3 | Реальная программа на хостинговом программном обеспечении (робот стратегии FMZ на картинке) | Фактическая программа, которая реализует стратегию исполнения сигнала TradingView |
| 4 | Обмен (обмен на картинке) | Биржа, настроенная на реальном рынке, биржа, на которую программа реального рынка на кастодиане напрямую отправляет запрос на размещение ордера |
Итак, если вы хотите играть таким образом, вам понадобятся следующие приготовления: 1. Скрипт, работающий на TradingView, отвечает за отправку запросов сигналов в расширенный интерфейс API FMZ. Учетная запись TradingView должна быть как минимум PRO-участником. 2. Разверните кастодиальную программу на FMZ, которая должна иметь доступ к интерфейсу биржи (например, к серверам в Сингапуре, Японии, Гонконге и т. д.). 3. Настройте API KEY биржи на FMZ для работы (размещения ордера) при отправке сигнала TradingView. 4. Вам необходимо иметь «стратегию исполнения сигналов TradingView», о которой в основном и пойдет речь в этой статье.
Предыдущая версия «Стратегии исполнения сигналов TradingView» не была гибкой, и сообщение можно было только жестко закодировать в URL-адресе запроса, отправленного TradingView. Если мы хотим, чтобы TradingView записывал некоторую переменную информацию в тело при отправке сообщения, то на данный момент мы ничего не можем сделать. Например, содержимое сообщения на TradingView выглядит так:

Затем TradingView можно настроить, как показано на рисунке, записав сообщение в теле запроса и отправив его в расширенный API-интерфейс FMZ. Так как же назвать этот расширенный API-интерфейс FMZ?
Среди ряда расширенных API-интерфейсов FMZ мы будем использоватьCommandRobotЭтот интерфейс обычно называется так:
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[186515,"ok12345"]
Этот запрос URLqueryВaccess_keyиsecret_keyЭто расширение платформы FMZ.API KEY, поэтому здесь мы устанавливаем его наxxxиyyyy. Как создать этот КЛЮЧ? На этой странице:https://www.fmz.com/m/account, просто создайте его, храните его надлежащим образом и никогда не разглашайте.

Возвращаемся к теме, продолжим.CommandRobotПроблема с интерфейсом. Если вам нужен доступCommandRobotИнтерфейс, в запросеmethodПросто установите его на:CommandRobot。CommandRobotФункция этого интерфейса заключается в отправке интерактивного сообщения на реальный диск определенного идентификатора через платформу FMZ, поэтому параметрыargsСодержание запроса — это реальный идентификатор и сообщение. Приведенный выше пример URL запроса — это отправка запроса на идентификатор186515Реальная программа, отправить сообщениеok12345。
Ранее этот метод использовался для запроса интерфейса CommandRobot API расширения FMZ. Сообщение могло быть только жестко закодировано, как в примере выше.ok12345. Если сообщение находится в теле запроса, необходим другой метод:
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[130350,+""]
Таким образом, запрос может быть отправлен через платформу FMZ, а содержимое тела запроса будет отправлено в виде интерактивного сообщения пользователю с идентификатором.130350Реальная сделка. Если сообщение на TradingView установлено следующим образом:{"close": {{close}}, "name": "aaa"}, то идентификатор будет130350Настоящий диск получит интерактивные инструкции:{"close": 39773.75, "name": "aaa"}
Для того чтобы «Стратегия исполнения сигналов TradingView» правильно поняла инструкцию, отправленную TradingView, при получении интерактивной инструкции, необходимо заранее согласовать формат сообщения:
{
Flag: "45M103Buy", // 标识,可随意指定
Exchange: 1, // 指定交易所交易对
Currency: "BTC_USDT", // 交易对
ContractType: "swap", // 合约类型,swap,quarter,next_quarter,现货填写spot
Price: "{{close}}", // 开仓或者平仓价格,-1为市价
Action: "buy", // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
Amount: "0", // 交易量
}
Стратегия разработана как многобиржевая архитектура, поэтому в этой стратегии можно настроить несколько объектов биржи, что означает возможность управления операциями с заказами нескольких различных счетов. Просто используйте Exchange в структуре сигнала, чтобы указать биржу, которая будет работать. Установка значения 1 означает, что этот сигнал будет работать на биржевом счете, соответствующем первому добавленному объекту обмена. Если будет использоваться спотовый контракт, установите ContractType на значение spot; для фьючерсов укажите конкретный контракт, например, укажите своп для бессрочных контрактов. Для рыночной цены ордера просто передайте -1. Настройки действий различны для фьючерсов, спотов, открытия и закрытия позиций и не могут быть установлены неправильно.
Далее вы можете разработать код стратегии. Полный код стратегии:
//信号结构
var Template = {
Flag: "45M103Buy", // 标识,可随意指定
Exchange: 1, // 指定交易所交易对
Currency: "BTC_USDT", // 交易对
ContractType: "swap", // 合约类型,swap,quarter,next_quarter,现货填写spot
Price: "{{close}}", // 开仓或者平仓价格,-1为市价
Action: "buy", // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
Amount: "0", // 交易量
}
var BaseUrl = "https://www.fmz.com/api/v1" // FMZ扩展API接口地址
var RobotId = _G() // 当前实盘ID
var Success = "#5cb85c" // 成功颜色
var Danger = "#ff0000" // 危险颜色
var Warning = "#f0ad4e" // 警告颜色
var buffSignal = []
// 校验信号消息格式
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 commandRobot(url, accessKey, secretKey, robotId, cmd) {
// https://www.fmz.com/api/v1?access_key=xxx&secret_key=xxx&method=CommandRobot&args=[xxx,+""]
url = url + '?access_key=' + accessKey + '&secret_key=' + secretKey + '&method=CommandRobot&args=[' + robotId + ',+""]'
var postData = {
method:'POST',
data:cmd
}
var headers = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36\nContent-Type: application/json"
var ret = HttpQuery(url, postData, "", headers)
Log("模拟TradingView的webhook请求,发送用于测试的POST请求:", url, "body:", cmd, "应答:", ret)
}
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
}
var manager = createManager()
function HandleCommand(signal) {
// 检测是否收到交互指令
if (signal) {
Log("收到交互指令:", signal) // 收到交互指令,打印交互指令
} else {
return // 没有收到时直接返回,不做处理
}
// 检测交互指令是否是测试指令,测试指令可以由当前策略交互控件发出来进行测试
if (signal.indexOf("TestSignal") != -1) {
signal = signal.replace("TestSignal:", "")
// 调用FMZ扩展API接口,模拟Trading View的webhook,交互按钮TestSignal发送的消息:{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"buy","Amount":"0"}
commandRobot(BaseUrl, FMZ_AccessKey, FMZ_SecretKey, RobotId, signal)
} else if (signal.indexOf("evalCode") != -1) {
var js = signal.split(':', 2)[1]
Log("执行调试代码:", js)
eval(js)
} else {
// 处理信号指令
objSignal = JSON.parse(signal)
if (DiffObject(Template, objSignal)) {
Log("接收到交易信号指令:", objSignal)
buffSignal.push(objSignal)
// 检查交易量、交易所编号
if (!CheckSignal(objSignal)) {
return
}
// 创建任务
manager.newTask(objSignal)
} else {
Log("指令无法识别", signal)
}
}
}
function main() {
Log("WebHook地址:", "https://www.fmz.com/api/v1?access_key=" + FMZ_AccessKey + "&secret_key=" + FMZ_SecretKey + "&method=CommandRobot&args=[" + RobotId + ',+""]', Danger)
Log("交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]", Danger)
Log("指令模板:", JSON.stringify(Template), Danger)
while (true) {
try {
// 处理交互
HandleCommand(GetCommand())
// 处理任务
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) + "`")
Sleep(1000 * SleepInterval)
} catch (error) {
Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
Sleep(1000 * 10)
}
}
}
Параметры стратегии и взаимодействия:

「Стратегия исполнения сигналов TradingView」Полный адрес стратегии: https://www.fmz.com/strategy/392048
Перед запуском стратегии необходимо настроить объект обмена и задать два параметра “AccessKey of FMZ platform” и “SecretKey of FMZ platform” в параметрах стратегии. Убедитесь, что вы не задали их неправильно. Запуск показывает:

Последовательно будут распечатаны следующие данные: адрес WebHook, который необходимо заполнить в TradingView, поддерживаемые инструкции по действию и формат сообщения. Важен адрес WebHook:
https://www.fmz.com/api/v1?access_key=22903bab96b26584dc5a22522984df42&secret_key=73f8ba01014023117cbd30cb9d849bfc&method=CommandRobot&args=[505628,+""]
Просто скопируйте и вставьте его в соответствующую позицию на TradingView.
Если вы хотите смоделировать отправку сигнала TradingView, вы можете нажать кнопку TestSignal во взаимодействии со стратегией:

Эта стратегия сама отправит запрос (имитируя отправку запроса сигнала TradingView), вызовет расширенный API-интерфейс FMZ и отправит сообщение самой стратегии:
{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"16000","Action":"buy","Amount":"1"}
Текущая стратегия получит еще одно интерактивное сообщение и выполнит:

И сделайте заказ.
Для использования тестирования TradingView вам понадобится учетная запись TradingView уровня Pro. Перед тестированием необходимо получить некоторые предварительные знания, которые необходимо кратко объяснить.
Возьмем в качестве примера простой скрипт PINE (я нашел его на TradingView и немного модифицировал).
//@version=5
strategy("Consecutive Up/Down Strategy", overlay=true)
consecutiveBarsUp = input(3)
consecutiveBarsDown = input(3)
price = close
ups = 0.0
ups := price > price[1] ? nz(ups[1]) + 1 : 0
dns = 0.0
dns := price < price[1] ? nz(dns[1]) + 1 : 0
if (not barstate.ishistory and ups >= consecutiveBarsUp and strategy.position_size <= 0)
action = strategy.position_size < 0 ? "closesell" : "long"
strategy.order("ConsUpLE", strategy.long, 1, comment=action)
if (not barstate.ishistory and dns >= consecutiveBarsDown and strategy.position_size >= 0)
action = strategy.position_size > 0 ? "closebuy" : "short"
strategy.order("ConsDnSE", strategy.short, 1, comment=action)
Ниже приведены заполнители. Например, я написал в поле «Сообщение» в будильнике:{{strategy.order.contracts}}, то при срабатывании ордера будет отправлено сообщение (на основе настроек сигнала тревоги, push-уведомления по электронной почте, запроса URL-адреса веб-перехватчика, всплывающего окна и т. д.), и сообщение будет включать количество выполненного ордера в этот раз время.
{{strategy.position_size}} - Возвращает значение того же ключевого слова в Pine, которое представляет собой размер текущей позиции.
{{strategy.order.action}} - Возвращает строку «купить» или «продать» для выполненного ордера.
{{strategy.order.contracts}} - Возвращает количество контрактов по выполненному заказу.
{{strategy.order.price}} - Возвращает цену, по которой был исполнен ордер.
{{strategy.order.id}} - Возвращает идентификатор выполненного ордера (строка, используемая в качестве первого аргумента в одном из вызовов функции, сгенерировавшей ордер: strategy.entry, strategy.exit или strategy.order).
{{strategy.order.comment}} - Возвращает комментарий выполненного заказа (строка, используемая в параметре комментария в одном из вызовов функций, сгенерировавших заказ: strategy.entry, strategy.exit или strategy.order). Если аннотация не указана, будет использовано значение strategy.order.id.
{{strategy.order.alert_message}} - Возвращает значение параметра alert_message, которое может быть использовано в Pine-коде стратегии при вызове одной из функций размещения ордеров: strategy.entry, strategy.exit или strategy.order. Эта функция поддерживается только в Pine v4.
{{strategy.market_position}} - Возвращает текущую позицию стратегии в виде строки: «длинная», «флэт» или «короткая».
{{strategy.market_position_size}} - Возвращает размер текущей позиции как абсолютное значение (т.е. неотрицательное число).
{{strategy.prev_market_position}} - Возвращает последнюю позицию стратегии в виде строки: «длинная», «флэт» или «короткая».
{{strategy.prev_market_position_size}} - Возвращает размер предыдущей позиции как абсолютное значение (т.е. неотрицательное число).
{
"Flag":"{{strategy.order.id}}",
"Exchange":1,
"Currency":"BTC_USDT",
"ContractType":"swap",
"Price":"-1",
"Action":"{{strategy.order.comment}}",
"Amount":"{{strategy.order.contracts}}"
}

Когда скрипт PINE на TradingView запускает торговое действие, отправляется запрос URL-адреса веб-перехватчика.


Реальная торговля FMZ исполнит этот сигнал.


Видео Xigua: https://www.ixigua.com/7172134169580372513?utm_source=xiguastudio Станция B: https://www.bilibili.com/video/BV1BY411d7c6/ Чжиху: https://www.zhihu.com/zvideo/1581722694294487040
Коды в статье приведены только для справки. Вы можете настроить и расширить их для фактического использования.