Les stratégies de couverture sont de très bonnes stratégies de pratique pour les débutants dans la conception de stratégies.
Tout d'abord, il est clair que la stratégie à concevoir est une stratégie de couverture au comptant de crypto-monnaie. Nous concevons la stratégie de couverture la plus simple. Nous vendons sur l'échange avec le prix le plus élevé uniquement entre les deux échanges au comptant, et achetons sur l'échange avec le prix le plus bas pour prendre la différence. Lorsque les échanges avec des prix plus élevés sont tous des pièces dénommées (car les pièces à prix plus élevé sont vendues), et les échanges avec des prix plus bas sont tous des pièces (les pièces à prix plus bas sont achetées), il ne peut pas être couvert. À ce moment-là, nous ne pouvons qu'attendre que l'inversion du prix se couvre.
Lors de la couverture, le prix et la quantité de l'ordre sont limités par l'échange, et il existe également une limite sur la quantité minimale d'ordre. En plus de la limite minimale, la stratégie de couverture doit également prendre en compte le volume maximal d'ordre à la fois. Si le volume d'ordre est trop grand, il n'y aura pas assez de volume d'ordre. Il est également nécessaire de considérer comment convertir le taux de change si les deux pièces libellées en bourse sont différentes. Lors de la couverture, les frais de manutention et le glissement du preneur d'ordre sont tous des coûts de transaction, pas tant qu'il y a une différence de prix peut être couvert. Par conséquent, la différence de prix de couverture a également une valeur de déclenchement. Si elle est inférieure à une certaine différence de prix, la couverture perdra.
Sur la base de ces considérations, la stratégie doit être conçue avec plusieurs paramètres:
hedgeDiffPrice
, lorsque la différence dépasse cette valeur, l'opération de couverture est déclenchée.minHedgeAmount
, le montant minimum des ordres (pièces) pouvant être couverts.maxHedgeAmount
, le montant maximal de l'ordre (pièces) pour une couverture.pricePrecisionA
, la précision du prix des ordres (nombre de décimales) placés par la Bourse A.amountPrecisionA
, la précision du montant de l' ordre passé par la Bourse A (nombre de décimales).pricePrecisionB
, la précision du prix de l' ordre (nombre de décimales) placé par la Bourse B.amountPrecisionB
, la précision du montant de l' ordre passé par la Bourse B (nombre de décimales).rateA
, la conversion du taux de change du premier objet d'échange ajouté, par défaut 1, non convertie.rateB
, la conversion du taux de change du deuxième objet d'échange ajouté, par défaut 1, non convertie.La stratégie de couverture doit maintenir le nombre de pièces dans les deux comptes inchangé (c'est-à-dire ne pas détenir de positions dans n'importe quelle direction et maintenir la neutralité), donc il doit y avoir une logique d'équilibre dans la stratégie pour toujours détecter le solde.
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
}
Après avoir placé l'ordre, s'il n'y a pas d'ordre terminé, nous devons l'annuler à temps, et l'ordre ne peut pas être maintenu en attente.
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)
}
}
})
}
Lorsque l'équilibrage du nombre de pièces, nous devons trouver le prix accumulé à un certain nombre de pièces dans une certaine profondeur de données, donc nous avons 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 placer des ordres simultanés:
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.
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
// Calculate the currency difference
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("unable to balance")
} else {
// balance order
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("invalid price", price)
}
}
return false
} else if (!(initAccs.length == nowAccs.length && nowAccs.length == depths.length)) {
Log("errors:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
return true
} else {
return true
}
}
Après avoir conçu ces fonctions selon les exigences de la stratégie, commencez à concevoir la fonction principale de la stratégie.
Sur la plateforme FMZ, la stratégie est exécutée à partir dumain
Au début de l'annéemain
fonction, nous avons à faire un peu de travail d'initialisation de la stratégie.
Nom de l'objet Exchange Parce que beaucoup d'opérations dans la stratégie doivent utiliser les objets d'échange, tels que l'obtention de cotations de marché, la passation d'ordres, etc. Il serait donc encombrant d'utiliser un nom long à chaque fois, le conseil est d'utiliser un nom simple à la place, par exemple:
var exA = exchanges[0]
var exB = exchanges[1]
Cela facilite l'écriture de code plus tard.
Taux de change, conception liée à la précision
// precision, exchange rate settings
if (rateA != 1) {
// set exchange rate A
exA.SetRate(rateA)
Log("Exchange A sets the exchange rate:", rateA, "#FF0000")
}
if (rateB != 1) {
// set exchange rate B
exB.SetRate(rateB)
Log("Exchange B sets the exchange rate:", rateB, "#FF0000")
}
exA.SetPrecision(pricePrecisionA, amountPrecisionA)
exB.SetPrecision(pricePrecisionB, amountPrecisionB)
Si les paramètres du taux de changerateA
, rateB
sont définies sur 1 (par défaut 1), c'est-à-direrateA != 1
ourateB != 1
ne sera pas déclenchée, de sorte que la conversion du taux de change ne sera pas définie.
Réinitialisez toutes les données
Parfois, il est nécessaire de supprimer tous les journaux et effacer les données enregistrées lorsque la stratégie démarre.isReset
, et concevoir le code de réinitialisation dans la partie d'initialisation de la stratégie, par exemple:
if (isReset) { // When isReset is true, reset the data
_G(null)
LogReset(1)
LogProfitReset()
LogVacuum()
Log("reset all data", "#FF0000")
}
Restaurer les données du compte initial, mettre à jour les données du compte courant
Afin de juger du solde, la stratégie doit enregistrer en permanence les actifs du compte initial en vue de la comparaison avec le compte courant.nowAccs
est utilisé pour enregistrer les données de compte courant, en utilisant la fonction que nous venons de concevoirupdateAccs
pour obtenir les données de compte de l'échange actuel.initAccs
est utilisé pour enregistrer l'état initial du compte (nombre de pièces, nombre de pièces libellées, etc. sur les bourses A et B).initAccs
, utilisez_G()
fonction de restaurer d'abord (la fonction _G enregistrera les données de manière persistante, et peut retourner les données enregistrées à nouveau, voir la documentation API pour plus de détails: [lien](https://www.fmz.com/api#_gk-v)), si la requête ne fonctionne pas, utilisez les informations de compte courant pour attribuer la valeur et_G
fonction à enregistrer.
Par exemple, le code suivant:
var nowAccs = _C(updateAccs, exchanges)
var initAccs = _G("initAccs")
if (!initAccs) {
initAccs = nowAccs
_G("initAccs", initAccs)
}
Le code dans la boucle principale est le processus de chaque cycle d'exécution de la logique de stratégie, qui est exécuté encore et encore pour former la boucle principale de la stratégie.
Obtenir des données de marché et juger de leur validité
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, nous pouvons voir que la fonction concurrenteexchange.Go
de la plateforme FMZ est utilisé pour créer des objets simultanésdepthARoutine
, depthBRoutine
qui appellent leGetDepth()
Lorsque ces deux objets concomitants sont créés, leGetDepth()
L'interface est appelée immédiatement, et les deux demandes de données de profondeur sont envoyées à l'échange.
Alors appelle lewait()
méthode dedepthARoutine
, depthBRoutine
objets pour obtenir les 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 en déterminer la validité.continue
l'instruction est déclenchée pour réexécuter la boucle principale.
Utilisez lespread value
paramètre ou lespread ratio
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
}
En termes de paramètres, nous avons fait une telle conception: les paramètres de FMZ peuvent êtrele spectacleouse cacherbasé sur un paramètre, afin que nous puissions faire un paramètre pour décider d'utiliserprice spread
, ouspread ratio
.
Un paramètrediffAsPercentage
Les deux autres paramètres à afficher ou à masquer en fonction de ce paramètre sont:hedgeDiffPrice@!diffAsPercentage
, qui s' affiche lorsquediffAsPercentage
est fausse.hedgeDiffPercentage@diffAsPercentage
, qui s' affiche lorsquediffAsPercentage
C'est vrai.
Après cette conception, nous avons vérifié lesdiffAsPercentage
Paramètre, qui est la condition de déclenchement de la couverture basée sur le ratio de différence de prix.diffAsPercentage
paramètre vérifié, la couverture est déclenchée par la différence de prix.
Déterminer les conditions de déclenchement de la couverture
if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) { // A -> B market conditions are met
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("trigger A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks) // Tips
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 market conditions are met
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("trigger B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks) // Tips
hedge(exA, exB, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
}
Les conditions de déclenchement de la couverture sont les suivantes:
isTrade
Dans ce cas, si la couverture est déclenchée, la variable est réglée surtrue
. Et réinitialisez la variable globalelastKeepBalanceTS
à 0 (lastKeepBalanceTS est utilisé pour marquer l'horodatage de la dernière opération d'équilibrage, si elle est réglée à 0 l'opération d'équilibrage sera déclenchée immédiatement), puis annuler tous les ordres en attente.Opération d'équilibrage
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
}
}
}
On peut voir que la fonction d'équilibrage est exécutée périodiquement, mais si lalastKeepBalanceTS
si le taux de change est réinitialisé à 0 après le déclenchement de l'opération de couverture, l'opération d'équilibrage sera déclenchée immédiatement.
Informations sur la barre d'état
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",
"current A, Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n",
"current B, Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n",
"initial A, Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n",
"initial 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 de l'échange A à l'échange B et la différence de prix de l'échange B à l'échange A. Et elle affiche l'écart cible de couverture actuel, 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, et nous avons également conçu la conversion du taux de change dans le fonctionnement initial de lamain
Il convient de noter que laSetRate
La fonction de conversion des taux de change doit être exécutée en premier.
Parce que cette fonction affecte deux aspects:
BTC_USDT
, l' unité de prix estUSDT
, et la monnaie disponible en valeur nominale dans les actifs du compte est égalementUSDT
. Si je veux convertir la valeur en CNY, définirexchange.SetRate(6.8)
dans le code pour convertir les données obtenues par toutes les fonctionsexchange
l'objet de l'échange à CNY.
Pour convertir dans quelle devise, passezle taux de change de la monnaie libellée courante à la monnaie libellée cibleà laSetRate
function.Stratégie complète:La valeur de l'échange de titres est la valeur de l'échange de titres de titres.