
Pour les débutants en conception de stratégie, la stratégie de couverture est une très bonne stratégie de formation. Cet article met en œuvre une stratégie de couverture au comptant de devises numériques simple mais réelle, dans l’espoir que les débutants puissent acquérir une certaine expérience de conception.
Tout d’abord, il est clair que la stratégie à concevoir est une stratégie de couverture spot de monnaie numérique. Nous concevons la stratégie de couverture la plus simple, qui consiste à vendre à la bourse avec un prix plus élevé entre deux bourses au comptant et à acheter à la bourse avec un prix plus bas pour faire du profit. Prendre la différence. Lorsque tous les échanges avec des prix plus élevés sont libellés en pièces (parce que toutes les pièces avec des prix plus élevés sont vendues), et que tous les échanges avec des prix plus bas sont libellés en pièces (toutes les pièces avec des prix plus bas sont achetées), il est impossible de se couvrir. À ce stade, vous ne pouvez qu’attendre que le prix s’inverse et vous couvrir.
Lors de la couverture, la bourse impose des restrictions de précision sur le prix et la quantité de la commande, et il existe également une limite de quantité minimale de commande. En plus de la limite minimale, la stratégie doit également tenir compte du volume maximal d’ordres de couverture à un moment donné. Si le volume d’ordres est trop important, il n’y aura pas suffisamment d’ordres sur le marché. Vous devez également réfléchir à la manière de convertir en utilisant le taux de change si les deux bourses ont des devises libellées différentes. Lors de la couverture, les frais de traitement et le glissement des ordres sont tous deux des coûts de transaction. Il n’est pas possible de se couvrir tant qu’il existe une différence de prix. Par conséquent, il existe également une valeur de déclenchement pour couvrir la différence de prix. Lorsque la différence de prix est inférieure à à un certain niveau, la couverture entraînera une perte.
Sur la base de ces considérations, la stratégie doit concevoir plusieurs paramètres :
hedgeDiffPrice, lorsque la différence de prix dépasse cette valeur, l’opération de couverture est déclenchée.minHedgeAmount, la quantité minimale de commande (en pièces) qui peut être couverte.maxHedgeAmount, la quantité maximale de commande (nombre de pièces) pour une couverture.pricePrecisionA, la précision du prix de l’ordre (nombre de décimales) de la bourse A.amountPrecisionA, la précision de la quantité commandée (nombre de décimales) de l’échange A.pricePrecisionB, la précision du prix de l’ordre (nombre de décimales) de la bourse B.amountPrecisionB, la précision de la quantité commandée (nombre de décimales) de l’échange B.rateA, la conversion du taux de change du premier objet d’échange ajouté, la valeur par défaut est 1, aucune conversion.rateB, la conversion du taux de change du deuxième objet d’échange ajouté, la valeur par défaut est 1 et aucune conversion n’est effectuée.La stratégie de couverture doit maintenir le nombre de pièces dans les deux comptes inchangé (c’est-à-dire ne détenir aucune position directionnelle et maintenir la neutralité), il doit donc y avoir une logique d’équilibre dans la stratégie pour toujours vérifier l’équilibre. Lors de la vérification du solde, il est inévitable d’obtenir des données sur les actifs de deux bourses. Nous devons écrire une fonction pour l’utiliser.
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 une commande n’est pas exécutée après l’avoir passée, nous devons l’annuler à temps et ne pas la laisser en attente. Cette opération doit être gérée à la fois dans le module de solde et dans la logique de couverture, il est donc nécessaire de concevoir une fonction complète de retrait d’ordre.
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)
}
}
})
}
Lors de l’équilibrage du nombre de pièces, nous devons trouver le prix d’un certain nombre de pièces accumulées dans une certaine profondeur de données, nous avons donc besoin d’une telle fonction pour le gérer.
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
}
Ensuite, nous devons concevoir et écrire l’opération d’ordre de couverture spécifique, qui doit être conçue pour être un ordre simultané :
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()
}
Enfin, terminons la conception de la fonction d’équilibre, qui est un peu compliquée.
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
}
}
Maintenant que ces fonctions ont été conçues en fonction des exigences de la stratégie, nous pouvons commencer à concevoir la fonction principale de la stratégie.
Sur FMZ, la stratégie est demainLa fonction commence à s’exécuter. existermainAu début de la fonction, nous devons effectuer une initialisation de stratégie.
var exA = exchanges[0]
var exB = exchanges[1]
Cela rend l’écriture de code ultérieure très confortable.
// 精度,汇率设置
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 le paramètre de taux de changerateA、rateBCertains sont réglés sur 1 (la valeur par défaut est 1), c’est-à-direrateA != 1ourateB != 1Ne se déclenchera pas, donc aucune conversion de taux de change ne sera définie.

Parfois, lorsqu’une politique est démarrée, il est nécessaire de supprimer tous les journaux et d’effacer les données enregistrées. Vous pouvez concevoir un paramètre d’interface de stratégieisReset, puis concevez le code de réinitialisation dans la section d’initialisation de la stratégie, par exemple :
if (isReset) { // 当isReset为真时重置数据
_G(null)
LogReset(1)
LogProfitReset()
LogVacuum()
Log("重置所有数据", "#FF0000")
}
nowAccsCette variable est utilisée pour enregistrer les données du compte courant, en utilisant la fonction que nous venons de concevoir.updateAccsObtenez les données de compte de l’échange actuel.initAccsUtilisé pour enregistrer le statut initial du compte (données telles que le nombre de pièces et le nombre de pièces libellées de l’échange A et de l’échange B). pourinitAccsPremière utilisation_G()Récupération de fonction (_La fonction G enregistre les données de manière persistante et peut renvoyer à nouveau les données enregistrées. Pour plus de détails, consultez la documentation de l’API :Lien), si la requête échoue, utilisez les informations du compte actuel pour attribuer une valeur et utilisez_GEnregistrement de fonction.Par exemple, le code suivant :
var nowAccs = _C(updateAccs, exchanges)
var initAccs = _G("initAccs")
if (!initAccs) {
initAccs = nowAccs
_G("initAccs", initAccs)
}
Le code de la boucle principale est le processus de chaque cycle d’exécution de la logique de stratégie. L’exécution réciproque continue constitue la boucle principale de la stratégie. Jetons un œil au flux de chaque exécution du programme dans la boucle principale.
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
}
Ici vous pouvez voir les fonctions simultanées de la plateforme FMZ.exchange.Go, a créé l’appelGetDepth()Objet de concurrence de l’interfacedepthARoutine、depthBRoutine. Lorsque ces deux objets simultanés sont créés, appelezGetDepth()L’interface s’est également produite immédiatement et deux demandes d’obtention de données de profondeur ont été envoyées à l’échange.
Alors appelledepthARoutine、depthBRoutineObjetwait()Méthode pour obtenir des données de profondeur.
Après avoir obtenu les données de profondeur, il est nécessaire de vérifier les données de profondeur pour déterminer leur validité. Exécution du déclencheur pour les anomalies de donnéescontinueL’instruction réexécute la boucle principale.
价差值Paramètres ou差价比例paramètre? var targetDiffPrice = hedgeDiffPrice
if (diffAsPercentage) {
targetDiffPrice = (depthA.Bids[0].Price + depthB.Asks[0].Price + depthB.Bids[0].Price + depthA.Asks[0].Price) / 4 * hedgeDiffPercentage
}
Nous avons réalisé une telle conception en termes de paramètres. Les paramètres de FMZ peuvent être basés sur un certain paramètremontreroucacher, nous pouvons donc créer un paramètre pour décider d’utiliser ou non价格差,toujours差价比例。

Un paramètre a été ajouté aux paramètres de l’interface de stratégiediffAsPercentage. Deux autres paramètres qui sont affichés ou masqués en fonction de ce paramètre sont définis sur :
hedgeDiffPrice@!diffAsPercentage,quanddiffAsPercentageFalse affiche ce paramètre.
hedgeDiffPercentage@diffAsPercentage,quanddiffAsPercentageVrai pour afficher ce paramètre.
Après cette conception, nous avons vérifiédiffAsPercentageLes paramètres sont basés sur le ratio de différence de prix comme condition de déclenchement de couverture. DécocherdiffAsPercentageLe paramètre consiste à utiliser la différence de prix comme condition de déclenchement de couverture.
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
}
}
Il existe plusieurs conditions de déclenchement de couverture :
1. Tout d’abord, il faut satisfaire à la différence de prix de couverture. La couverture ne peut être effectuée que lorsque la différence de prix du marché répond aux paramètres de différence de prix définis.
2. Le volume de couverture sur le marché doit respecter le volume de couverture minimum défini dans les paramètres. Étant donné que les différentes bourses peuvent avoir des volumes de commande minimum différents, le plus petit des deux doit être retenu.
3. Il y a suffisamment d’actifs dans la bourse pour que les opérations de vente puissent être vendues, et il y a suffisamment d’actifs dans la bourse pour que les opérations d’achat puissent être achetées.
Lorsque ces conditions sont remplies, la fonction de couverture est exécutée pour passer un ordre de couverture. Avant la fonction principale, nous avons déclaré une variable à l’avanceisTradeUtilisé pour indiquer si la couverture se produit. Si la couverture est déclenchée, cette variable est définie surtrue. Et réinitialiser les variables globaleslastKeepBalanceTSDéfinissez lastKeepBalanceTS sur 0 (lastKeepBalanceTS est utilisé pour marquer l’horodatage de l’opération d’équilibrage la plus récente. Le définir sur 0 déclenchera l’opération d’équilibrage immédiatement), puis annulez toutes les commandes en attente.
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
}
}
}
Vous pouvez voir que la fonction d’équilibrage est exécutée régulièrement, mais si l’opération de couverture est déclenchée,lastKeepBalanceTSSi remis à 0, l’opération d’équilibrage sera déclenchée immédiatement. Une fois le solde atteint, le bénéfice sera calculé.
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 barre d’état n’est pas particulièrement complexe dans sa conception. Elle affiche l’heure actuelle, la différence de prix entre le marché A et le marché B, ainsi que la différence de prix entre le marché B et le marché A. Affiche le spread cible de couverture actuel. Affiche les données d’actifs du compte d’échange A et du compte d’échange B.
En termes de paramètres, nous avons conçu le paramètre de valeur du taux de conversion au début de la stratégiemainNous avons également conçu la conversion du taux de change pour le fonctionnement initial de la fonction. Il convient de noter queSetRateLa fonction de conversion du taux de change doit d’abord être exécutée.
Car cette fonction agit à deux niveaux :
BTC_USDT, les unités de prix sontUSDT, la devise disponible dans les actifs du compte est égalementUSDT. Si je veux le convertir en CNY, je le définis dans le codeexchange.SetRate(6.8)JusteexchangeLes données obtenues par toutes les fonctions sous cet objet d’échange sont converties en CNY.
Pourquoi la devise est-elle utilisée pour la conversion ?SetRatePassage de fonctionLe taux de change de la devise actuelle à la devise cible。La stratégie complète :Stratégies de couverture spot pour différentes devises (tutoriel)