Pour les débutants dans la conception de stratégies, la stratégie de couverture est très bonne pour la pratique.
Tout d'abord, nous devons nous assurer que la stratégie à concevoir est une stratégie de couverture au comptant de crypto-monnaie. Nous concevons la couverture la plus simple. Nous vendons uniquement sur la plate-forme avec le prix le plus élevé entre les deux plateformes au comptant, et achetons sur la plate-forme avec le prix le plus bas pour gagner l'écart de prix. Lorsque la plate-forme avec le prix le plus élevé est rempli de symboles de devises de devises (parce que le prix est élevé, tous les symboles de devises sont vendus), ou lorsque la plate-forme avec le prix le plus bas est rempli de symboles de devises (parce que le prix est bas, les symboles de devises sont achetés par tous les actifs), il ne peut pas être couvert. À ce moment-là, vous ne pouvez attendre que le prix à inverser pour couvrir.
Pour le prix de l'ordre et le montant pendant la couverture, il y a des limites de précision dans chaque plate-forme, et il y a aussi une limite sur le montant minimum de l'ordre. En plus de la limite minimale, la stratégie doit également considérer le montant maximal de l'ordre pour une couverture. Si le montant de l'ordre est trop grand, le marché n'aura pas un volume d'ordre suffisant pour cela. Il est également nécessaire de considérer comment convertir le taux de change si les deux plateformes ont des devises de devises différentes. Les frais de manutention pendant la couverture et le glissement du preneur d'ordre sont tous des coûts de négociation. La couverture ne se produit pas toujours tant qu'il y a une différence de prix. Par conséquent, l'écart de prix de couverture a également une valeur de déclenchement. S'il est inférieur à un certain écart de prix, la couverture fera une perte.
Sur cette base, la stratégie doit être conçue avec plusieurs paramètres:
hedgeDiffPrice
; lorsque l'écart dépasse la valeur, une couverture est déclenchée.minHedgeAmount
, le montant minimum de l'ordre (montant du symbole) disponible pour une couverture.maxHedgeAmount
, le montant maximal de l'ordre (montant du symbole) disponible pour une couverture.pricePrecisionA
, la précision du prix de commande (chiffres décimaux) de la plateforme A.amountPrecisionA
, la précision du montant de la commande (chiffres décimaux) de la plateforme A.pricePrecisionB
, la précision du prix de commande (chiffres décimaux) de la plateforme B.amountPrecisionB
, la précision du montant de la commande (chiffres décimaux) de la plateforme B.rateA
, le taux de change de conversion du premier objet de change ajouté; la valeur par défaut est 1, ce qui indique qu'il ne doit pas être converti.rateB
, le taux de change de conversion du deuxième objet de change ajouté; la valeur par défaut est 1, ce qui indique qu'il ne doit pas être converti.La stratégie de couverture doit maintenir le montant du symbole de monnaie des deux comptes inchangé (c'est-à-dire ne pas détenir de positions directionnelles et maintenir la neutralité), il doit donc 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é un ordre, s'il n'y a pas d'ordre exécuté, 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 de la quantité de symboles de monnaie, nous devons trouver le prix avec une certaine quantité dans une certaine profondeur de données, donc nous avons besoin d'une fonction comme celle-ci 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 simultanément des ordres:
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 plus 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 currency spread
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("cannot balance")
} else {
// balanced ordering
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("error:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
return true
} else {
return true
}
}
Ces fonctions ont été conçues selon les exigences de la stratégie, et nous pouvons commencer à concevoir la fonction principale de la stratégie.
Sur FMZ, la stratégie est exécutée à partir dumain
Au début de l'annéemain
fonction, nous devons faire une certaine initialisation de la stratégie.
Nom de l'objet Exchange Pour beaucoup d'opérations dans la stratégie utilise des objets d'échange, tels que l'obtention de cotations de marché, la mise en place des commandes, et ainsi de suite, il serait donc gênant d'utiliser un nom plus long à chaque fois, mon petit tour est d'utiliser un nom court simple à la place, par exemple:
var exA = exchanges[0]
var exB = exchanges[1]
Ensuite, il sera plus confortable d'écrire le code plus tard.
Taux de change et précision
// settings of precision and exchange rate
if (rateA != 1) {
// set exchange rate A
exA.SetRate(rateA)
Log("Platform A sets exchange rate:", rateA, "#FF0000")
}
if (rateB != 1) {
// set exchange rate B
exB.SetRate(rateB)
Log("Platform B sets exchange rate:", rateB, "#FF0000")
}
exA.SetPrecision(pricePrecisionA, amountPrecisionA)
exB.SetPrecision(pricePrecisionB, amountPrecisionB)
Si l'un des paramètres du taux de change, à savoirrateA
etrateB
, est réglée sur 1 (la valeur par défaut est 1), c'est-à-direrateA != 1
ourateB != 1
les taux de change ne sont pas activés et ne peuvent être convertis.
Réinitialiser toute la date
Parfois, il est nécessaire de supprimer tous les journaux et vider les enregistrements de données lorsque la stratégie est lancée.isReset
, puis 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")
}
Récupérer les données du compte initial et mettre à jour les données du compte courant
Afin de juger du solde, la stratégie doit enregistrer en permanence l'état des actifs du compte initial en vue de la comparaison avec l'état actuel.nowAccs
est utilisé pour enregistrer les données des comptes courants.updateAccs
fonction que nous venons de concevoir pour obtenir les données de compte de la plateforme actuelle.initAccs
est utilisé pour enregistrer l'état initial du compte (données telles que le montant du symbole de monnaie de A et de B, le montant de la monnaie de cotation, etc.).initAccs
, utilisez d' abord le_G()
fonction de restauration (la fonction _G enregistrera les données de manière persistante et peut renvoyer les données enregistrées à nouveau; consultez la documentation de l'API pour plus de détails:lien).
Si vous ne pouvez pas interroger les données, utilisez les informations de compte courant pour attribuer et utiliser_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, et l'exécution répétée sans arrêt construit la boucle principale de stratégie.
Obtenez les offres du marché et jugez 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, vous pouvez voir que la fonction concurrenteexchange.Go
de la plateforme FMZ est utilisé pour créer des objets simultanésdepthARoutine
etdepthBRoutine
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 à la plateforme.
Alors, appelez lewait()
méthode de l'objetdepthARoutine
et objetdepthBRoutine
pour obtenir les données de profondeur.
Une fois les données de profondeur obtenues, il est nécessaire de vérifier les données de profondeur pour en juger la validité.continue
l'instruction est déclenchée pour réexécuter la boucle principale.
Utilisationprice spread
ouspread ratio
?
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.le spectacleouse cacherbasé sur un paramètre, afin que nous puissions faire un paramètre pour décider d'utiliserprice spread
, ouspread ratio
.
Le paramètrediffAsPercentage
Les deux autres paramètres, qui s'affichent ou se cachent en fonction du paramètre, sont définis comme suit:hedgeDiffPrice@!diffAsPercentage
; lorsquediffAsPercentage
est faux, il sera montré.hedgeDiffPercentage@diffAsPercentage
; lorsquediffAsPercentage
est vrai, il sera affiché.
Après la conception, nous avons vérifié lesdiffAsPercentage
Le taux d'intérêt de l'opérateur de couverture est le taux d'intérêt de l'opérateur de couverture.diffAsPercentage
Si le paramètre n'est pas vérifié, l'écart de prix est utilisé comme condition de déclenchement de la couverture.
Le juge Hedge Trigger
if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) { // A -> B market condition satisfied
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("triggerA->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks) // prompt message
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 condition satisfied
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("triggerB->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks) // prompt message
hedge(exA, exB, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
}
Il existe plusieurs conditions déclenchantes pour la couverture: 1.En premier lieu, respecter le spread de couverture; la couverture n'est possible que lorsque le spread du marché respecte le paramètre de spread fixé.
2.Le montant de la couverture du marché doit correspondre au montant minimum de couverture fixé dans les paramètres.Parce que le montant minimum des ordres des différentes plateformes est différent, le plus petit des deux doit être choisi.
3.Les actifs de la plateforme avec l'opération de vente sont suffisants pour vendre, et les actifs de la plateforme avec l'opération d'achat sont suffisants pour acheter. Lorsque ces conditions sont remplies, exécutez la fonction de couverture pour passer des ordres par couverture. Avant la fonction principale, nous déclarons une variableisTrade
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 de balance, et le régler à 0 déclenchera immédiatement l'opération de balance), puis annuler tous les ordres en attente.
Opération de mise en équilibre
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 de balance est exécutée périodiquement, mais si lalastKeepBalanceTS
Si la valeur de l'opération de couverture est réinitialisée à 0 après le déclenchement de l'opération de couverture, l'opération de solde est immédiatement déclenchée.
Informations de 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",
"currentA,Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n",
"currentB,Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n",
"initialA,Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n",
"initialB,Stocks:", initAccs[1].Stocks, "FrozenStocks:", initAccs[1].FrozenStocks, "Balance:", initAccs[1].Balance, "FrozenBalance", initAccs[1].FrozenBalance)
La barre d'état n'est pas conçue pour être particulièrement compliquée. Elle affiche l'heure actuelle, l'écart de prix de la plateforme A à la plateforme B ainsi que l'écart de prix de la plateforme B à A; elle affiche également l'écart cible de couverture actuel, les données d'actifs du compte de la plateforme A et les données d'actifs du compte de la plateforme B.
En ce qui concerne les paramètres, nous avons conçu le paramètre de conversion de la valeur du taux de change, 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.
Pour la fonction affectera deux aspects:
Par exemple, la paire de négociation actuelle estBTC_USDT
, l' unité de prix estUSDT
, et la devise de cotation disponible dans les actifs du compte est égalementUSDT
Si je veux convertir la valeur des actifs en CNY, définirexchange.SetRate(6.8)
dans le code pour convertir les données obtenues par toutes les fonctionsexchange
L'objectif est d'obtenir un taux de conversion en CNY.
Pour convertir dans quelle devise de devises, importerle taux de change de la devise de cotation courante à la devise de cotation cibledans leSetRate
function.
Stratégie complète:Stratégie de couverture au comptant de devises de cotation différentes (enseignement)