
Para los principiantes en el diseño de estrategias, la estrategia de cobertura es una muy buena estrategia de entrenamiento. Este artículo implementa una estrategia de cobertura al contado de moneda digital simple pero del mundo real, con la esperanza de que los principiantes puedan aprender algo de experiencia en diseño.
En primer lugar, es evidente que la estrategia que se va a diseñar es una estrategia de cobertura al contado de moneda digital. Diseñamos la estrategia de cobertura más sencilla, que consiste en vender en la bolsa con el precio más alto entre dos bolsas al contado y comprar en la bolsa con el precio más bajo. Para obtener ganancias, toma la diferencia. Cuando todos los intercambios con precios más altos están denominados en monedas (porque todas las monedas con precios más altos se venden), y todos los intercambios con precios más bajos están denominados en monedas (todas las monedas con precios más bajos se compran), es imposible realizar cobertura. En este momento, solo puedes esperar a que el precio se revierta y cubrirte.
Al realizar cobertura, el intercambio tiene restricciones de precisión sobre el precio y la cantidad de los pedidos, y también hay un límite de cantidad mínima de pedidos. Además del límite mínimo, la estrategia también debe considerar el volumen máximo de órdenes de cobertura en un momento dado. Si el volumen de órdenes es demasiado grande, no habrá suficientes órdenes en el mercado. También debes considerar cómo realizar la conversión utilizando el tipo de cambio si las dos casas de cambio tienen monedas denominadas en diferentes denominaciones. En la cobertura, las tarifas de gestión y el deslizamiento de las órdenes son ambos costos de transacción. No es posible cubrir mientras exista una diferencia de precio. Por lo tanto, también existe un valor de activación para cubrir la diferencia de precio. Cuando la diferencia de precio es menor que A partir de un cierto nivel, la cobertura resultará en una pérdida.
A partir de estas consideraciones, la estrategia debe diseñar varios parámetros:
hedgeDiffPrice, cuando la diferencia de precio supera este valor, se activa la operación de cobertura.minHedgeAmount, la cantidad mínima de pedido (en monedas) que se puede cubrir.maxHedgeAmount, la cantidad máxima de pedido (número de monedas) para una cobertura.pricePrecisionA, la precisión del precio de la orden (número de decimales) del intercambio A.amountPrecisionA, la precisión de la cantidad de pedido (número de decimales) del intercambio A.pricePrecisionB, la precisión del precio de la orden (número de decimales) del intercambio B.amountPrecisionB, la precisión de la cantidad de pedido (número de decimales) del intercambio B.rateA, la conversión del tipo de cambio del primer objeto de cambio agregado, el valor predeterminado es 1, sin conversión.rateB, la conversión del tipo de cambio del segundo objeto de cambio agregado, el valor predeterminado es 1 y no se realiza ninguna conversión.La estrategia de cobertura debe mantener sin cambios la cantidad de monedas en las dos cuentas (es decir, no mantener ninguna posición direccional y mantener la neutralidad), por lo que debe haber una lógica de equilibrio en la estrategia para verificar siempre el saldo. Al verificar el saldo, es inevitable obtener datos de activos de dos intercambios. Necesitamos escribir una función para usarlo.
function updateAccs(arrEx) {
var ret = []
for (var i = 0 ; i < arrEx.length ; i++) {
var acc = arrEx[i].GetAccount()
if (!acc) {
return null
}
ret.push(acc)
}
return ret
}
Si un pedido no se ejecuta después de realizarlo, debemos cancelarlo a tiempo y no dejarlo pendiente. Esta operación debe manejarse tanto en el módulo de balance como en la lógica de cobertura, por lo que es necesario diseñar una función completa de retiro de órdenes.
function cancelAll() {
_.each(exchanges, function(ex) {
while (true) {
var orders = _C(ex.GetOrders)
if (orders.length == 0) {
break
}
for (var i = 0 ; i < orders.length ; i++) {
ex.CancelOrder(orders[i].Id, orders[i])
Sleep(500)
}
}
})
}
Al equilibrar la cantidad de monedas, necesitamos encontrar el precio de una cierta cantidad de monedas acumuladas en una cierta profundidad de datos, por lo que necesitamos una función de este tipo para manejarlo.
function getDepthPrice(depth, side, amount) {
var arr = depth[side]
var sum = 0
var price = null
for (var i = 0 ; i < arr.length ; i++) {
var ele = arr[i]
sum += ele.Amount
if (sum >= amount) {
price = ele.Price
break
}
}
return price
}
Luego debemos diseñar y escribir la operación de orden de cobertura específica, que debe diseñarse para ser una orden concurrente:
function hedge(buyEx, sellEx, price, amount) {
var buyRoutine = buyEx.Go("Buy", price, amount)
var sellRoutine = sellEx.Go("Sell", price, amount)
Sleep(500)
buyRoutine.wait()
sellRoutine.wait()
}
Por último, completemos el diseño de la función de equilibrio, que es un poco complicado.
keepBalance
function keepBalance(initAccs, nowAccs, depths) {
var initSumStocks = 0
var nowSumStocks = 0
_.each(initAccs, function(acc) {
initSumStocks += acc.Stocks + acc.FrozenStocks
})
_.each(nowAccs, function(acc) {
nowSumStocks += acc.Stocks + acc.FrozenStocks
})
var diff = nowSumStocks - initSumStocks
// 计算币差
if (Math.abs(diff) > minHedgeAmount && initAccs.length == nowAccs.length && nowAccs.length == depths.length) {
var index = -1
var available = []
var side = diff > 0 ? "Bids" : "Asks"
for (var i = 0 ; i < nowAccs.length ; i++) {
var price = getDepthPrice(depths[i], side, Math.abs(diff))
if (side == "Bids" && nowAccs[i].Stocks > Math.abs(diff)) {
available.push(i)
} else if (price && nowAccs[i].Balance / price > Math.abs(diff)) {
available.push(i)
}
}
for (var i = 0 ; i < available.length ; i++) {
if (index == -1) {
index = available[i]
} else {
var priceIndex = getDepthPrice(depths[index], side, Math.abs(diff))
var priceI = getDepthPrice(depths[available[i]], side, Math.abs(diff))
if (side == "Bids" && priceIndex && priceI && priceI > priceIndex) {
index = available[i]
} else if (priceIndex && priceI && priceI < priceIndex) {
index = available[i]
}
}
}
if (index == -1) {
Log("无法平衡")
} else {
// 平衡下单
var price = getDepthPrice(depths[index], side, Math.abs(diff))
if (price) {
var tradeFunc = side == "Bids" ? exchanges[index].Sell : exchanges[index].Buy
tradeFunc(price, Math.abs(diff))
} else {
Log("价格无效", price)
}
}
return false
} else if (!(initAccs.length == nowAccs.length && nowAccs.length == depths.length)) {
Log("错误:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
return true
} else {
return true
}
}
Ahora que estas funciones se han diseñado de acuerdo con los requisitos de la estrategia, podemos comenzar a diseñar la función principal de la estrategia.
En FMZ la estrategia esmainLa función comienza a ejecutarse. existirmainAl comienzo de la función necesitamos realizar una inicialización de estrategia.
var exA = exchanges[0]
var exB = exchanges[1]
Esto hace que sea muy cómodo escribir código más tarde.
// 精度,汇率设置
if (rateA != 1) {
// 设置汇率A
exA.SetRate(rateA)
Log("交易所A设置汇率:", rateA, "#FF0000")
}
if (rateB != 1) {
// 设置汇率B
exB.SetRate(rateB)
Log("交易所B设置汇率:", rateB, "#FF0000")
}
exA.SetPrecision(pricePrecisionA, amountPrecisionA)
exB.SetPrecision(pricePrecisionB, amountPrecisionB)
Si el parámetro del tipo de cambiorateA、rateBAlgunos están configurados en 1 (el valor predeterminado es 1), es decirrateA != 1orateB != 1No se activará, por lo que no se establecerá ninguna conversión de tipo de cambio.

A veces, cuando se inicia una política, es necesario eliminar todos los registros y borrar los datos registrados. Puede diseñar un parámetro de interfaz de estrategiaisResety luego diseñar el código de reinicio en la sección de inicialización de la estrategia, por ejemplo:
if (isReset) { // 当isReset为真时重置数据
_G(null)
LogReset(1)
LogProfitReset()
LogVacuum()
Log("重置所有数据", "#FF0000")
}
nowAccsEsta variable se utiliza para registrar los datos de la cuenta corriente, mediante la función que acabamos de diseñar.updateAccsObtenga los datos de la cuenta del intercambio actual.initAccsSe utiliza para registrar el estado inicial de la cuenta (datos como el número de monedas y el número de monedas denominadas de la Bolsa A y la Bolsa B). parainitAccsEstreno_G()Recuperación de funciones (_La función G registrará datos de forma persistente y podrá devolver los datos registrados nuevamente. Para obtener más información, consulte la documentación de la API:Enlace), si la consulta falla, utilice la información de la cuenta actual para asignar un valor y utilizar_GRegistro de funciones.Por ejemplo, el siguiente código:
var nowAccs = _C(updateAccs, exchanges)
var initAccs = _G("initAccs")
if (!initAccs) {
initAccs = nowAccs
_G("initAccs", initAccs)
}
El código del bucle principal es el proceso de cada ronda de ejecución de la lógica de la estrategia. La ejecución recíproca continua constituye el bucle principal de la estrategia. Echemos un vistazo al flujo de cada ejecución del programa en el bucle principal.
var ts = new Date().getTime()
var depthARoutine = exA.Go("GetDepth")
var depthBRoutine = exB.Go("GetDepth")
var depthA = depthARoutine.wait()
var depthB = depthBRoutine.wait()
if (!depthA || !depthB || depthA.Asks.length == 0 || depthA.Bids.length == 0 || depthB.Asks.length == 0 || depthB.Bids.length == 0) {
Sleep(500)
continue
}
Aquí puedes ver las funciones concurrentes de la plataforma FMZ.exchange.Go, creó la llamadaGetDepth()Objeto de concurrencia de la interfazdepthARoutine、depthBRoutine. Cuando se crean estos dos objetos simultáneos, llameGetDepth()La interfaz también se produjo de inmediato y se enviaron dos solicitudes para obtener datos de profundidad al intercambio.
Entonces llamadepthARoutine、depthBRoutineObjetowait()Método para obtener datos de profundidad.
Después de obtener los datos de profundidad, es necesario verificarlos para determinar su validez. Ejecución de disparadores en caso de anomalías de datoscontinueLa declaración vuelve a ejecutar el bucle principal.
价差值Parámetros o差价比例¿parámetro? var targetDiffPrice = hedgeDiffPrice
if (diffAsPercentage) {
targetDiffPrice = (depthA.Bids[0].Price + depthB.Asks[0].Price + depthB.Bids[0].Price + depthA.Asks[0].Price) / 4 * hedgeDiffPercentage
}
Hicimos tal diseño en términos de parámetros. Los parámetros de FMZ pueden basarse en un parámetro determinadoespectáculooesconder, por lo que podemos crear un parámetro para decidir si usar o no价格差,aún差价比例。

Se ha añadido un parámetro a los parámetros de la interfaz de estrategia.diffAsPercentage. Otros dos parámetros que se muestran u ocultan en función de este parámetro se establecen en:
hedgeDiffPrice@!diffAsPercentage,cuandodiffAsPercentageFalso muestra este parámetro.
hedgeDiffPercentage@diffAsPercentage,cuandodiffAsPercentageVerdadero para mostrar este parámetro.
Después de este diseño, lo comprobamos.diffAsPercentageLos parámetros se basan en la relación de diferencia de precios como condición de activación de la cobertura. DesmarcardiffAsPercentageEl parámetro es utilizar la diferencia de precio como condición de activación de la cobertura.
if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) { // A -> B 盘口条件满足
var price = (depthA.Bids[0].Price + depthB.Asks[0].Price) / 2
var amount = Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount)
if (nowAccs[0].Stocks > minHedgeAmount && nowAccs[1].Balance / price > minHedgeAmount) {
amount = Math.min(amount, nowAccs[0].Stocks, nowAccs[1].Balance / price, maxHedgeAmount)
Log("触发A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks) // 提示信息
hedge(exB, exA, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
} else if (depthB.Bids[0].Price - depthA.Asks[0].Price > targetDiffPrice && Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount) >= minHedgeAmount) { // B -> A 盘口条件满足
var price = (depthB.Bids[0].Price + depthA.Asks[0].Price) / 2
var amount = Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount)
if (nowAccs[1].Stocks > minHedgeAmount && nowAccs[0].Balance / price > minHedgeAmount) {
amount = Math.min(amount, nowAccs[1].Stocks, nowAccs[0].Balance / price, maxHedgeAmount)
Log("触发B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks) // 提示信息
hedge(exA, exB, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
}
Existen varias condiciones desencadenantes de la cobertura:
1. En primer lugar, se debe satisfacer la diferencia de precio de cobertura. La cobertura solo se puede realizar cuando la diferencia de precio del mercado cumple con los parámetros de diferencia de precio establecidos.
2. El volumen de cobertura en el mercado debe cumplir con el volumen mínimo de cobertura establecido en los parámetros. Debido a que las distintas bolsas pueden tener distintos volúmenes mínimos de órdenes, se debe tomar el más pequeño de los dos.
3. Hay suficientes activos en el intercambio para realizar operaciones de venta, y hay suficientes activos en el intercambio para realizar operaciones de compra, para comprar.
Cuando se cumplen estas condiciones, se ejecuta la función de cobertura para colocar una orden de cobertura. Antes de la función principal declaramos una variable de antemanoisTradeSe utiliza para marcar si se produce una cobertura. Si se activa la cobertura, esta variable se establece entrue. Y restablecer las variables globaleslastKeepBalanceTSEstablezca lastKeepBalanceTS en 0 (lastKeepBalanceTS se utiliza para marcar la marca de tiempo de la operación de equilibrio más reciente. Establecerlo en 0 activará la operación de equilibrio inmediatamente) y luego cancele todos los pedidos pendientes.
if (ts - lastKeepBalanceTS > keepBalanceCyc * 1000) {
nowAccs = _C(updateAccs, exchanges)
var isBalance = keepBalance(initAccs, nowAccs, [depthA, depthB])
cancelAll()
if (isBalance) {
lastKeepBalanceTS = ts
if (isTrade) {
var nowBalance = _.reduce(nowAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
var initBalance = _.reduce(initAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
LogProfit(nowBalance - initBalance, nowBalance, initBalance, nowAccs)
isTrade = false
}
}
}
Se puede ver que la función de equilibrio se ejecuta regularmente, pero si se activa la operación de cobertura,lastKeepBalanceTSSi se restablece a 0, la operación de equilibrio se activará inmediatamente. Una vez que el saldo sea exitoso, se calculará la ganancia.
LogStatus(_D(), "A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, " B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, " targetDiffPrice:", targetDiffPrice, "\n",
"当前A,Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n",
"当前B,Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n",
"初始A,Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n",
"初始B,Stocks:", initAccs[1].Stocks, "FrozenStocks:", initAccs[1].FrozenStocks, "Balance:", initAccs[1].Balance, "FrozenBalance", initAccs[1].FrozenBalance)
La barra de estado no tiene un diseño particularmente complejo. Muestra la hora actual, la diferencia de precio entre la bolsa A y la bolsa B y la diferencia de precio entre la bolsa B y la bolsa A. Muestra el diferencial objetivo de cobertura actual. Muestra los datos de los activos de la cuenta de intercambio A y de la cuenta de intercambio B.
En términos de parámetros, diseñamos el parámetro de valor de la tasa de conversión al inicio de la estrategia.mainTambién diseñamos la conversión del tipo de cambio para la operación inicial de la función. Cabe señalar queSetRatePrimero debe ejecutarse la función de conversión del tipo de cambio.
Porque esta función afecta a dos niveles:
BTC_USDT, las unidades de precio sonUSDTLa moneda disponible en los activos de la cuenta esUSDT. Si quiero convertirlo a CNY, configúrelo en el códigoexchange.SetRate(6.8)JustoexchangeLos datos obtenidos por todas las funciones bajo este objeto de intercambio se convierten a CNY.
¿Por qué se utiliza la moneda para la conversión?SetRatePaso de funcionesEl tipo de cambio de la moneda actual a la moneda de destino。La estrategia completa:Estrategias de cobertura spot para diferentes divisas (tutorial)