Conception d'une stratégie de couverture au comptant de crypto-monnaie (1)

Auteur:Je ne sais pas., Créé: 2022-08-16 10:30:56, mis à jour: 2023-09-19 21:46:16

img

Conception d'une stratégie de couverture au comptant de crypto-monnaie (1)

Les stratégies de couverture sont de très bonnes stratégies de pratique pour les débutants dans la conception de stratégies.

Concevoir certaines fonctions et paramètres d'interface de stratégie selon les exigences de la stratégie

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:

  • Différence de couverture:hedgeDiffPrice, lorsque la différence dépasse cette valeur, l'opération de couverture est déclenchée.
  • Montant minimum de la couverture:minHedgeAmount, le montant minimum des ordres (pièces) pouvant être couverts.
  • Montant maximal de la couverture:maxHedgeAmount, le montant maximal de l'ordre (pièces) pour une couverture.
  • Précision de prix de A:pricePrecisionA, la précision du prix des ordres (nombre de décimales) placés par la Bourse A.
  • Précision du montant de A:amountPrecisionA, la précision du montant de l' ordre passé par la Bourse A (nombre de décimales).
  • Précision du prix de B:pricePrecisionB, la précision du prix de l' ordre (nombre de décimales) placé par la Bourse B.
  • Précision du montant de B:amountPrecisionB, la précision du montant de l' ordre passé par la Bourse B (nombre de décimales).
  • Taux de change A:rateA, la conversion du taux de change du premier objet d'échange ajouté, par défaut 1, non convertie.
  • Taux de change B: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.

  • mise à jourAccs
    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.

  • annuler tout
    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.

  • Je vais vous donner le prix.
    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:

  • la haie
    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.

  • maintenir l'équilibre
    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.

Conception des principales fonctions de la stratégie

Sur la plateforme FMZ, la stratégie est exécutée à partir dumainAu début de l'annéemainfonction, 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, rateBsont définies sur 1 (par défaut 1), c'est-à-direrateA != 1ourateB != 1ne 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

    img

    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.nowAccsest utilisé pour enregistrer les données de compte courant, en utilisant la fonction que nous venons de concevoirupdateAccspour obtenir les données de compte de l'échange actuel.initAccsest 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_Gfonction à enregistrer.

    Par exemple, le code suivant:

      var nowAccs = _C(updateAccs, exchanges)
      var initAccs = _G("initAccs")
      if (!initAccs) {
          initAccs = nowAccs
          _G("initAccs", initAccs)
      }
    

Logique de négociation, boucle principale dans la fonction principale

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.Gode la plateforme FMZ est utilisé pour créer des objets simultanésdepthARoutine, depthBRoutinequi 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, depthBRoutineobjets 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é.continuel'instruction est déclenchée pour réexécuter la boucle principale.

  • Utilisez lespread valueparamètre ou lespread ratioParamè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.

    img

    Un paramètrediffAsPercentageLes deux autres paramètres à afficher ou à masquer en fonction de ce paramètre sont:hedgeDiffPrice@!diffAsPercentage, qui s' affiche lorsquediffAsPercentageest fausse.hedgeDiffPercentage@diffAsPercentage, qui s' affiche lorsquediffAsPercentageC'est vrai. Après cette conception, nous avons vérifié lesdiffAsPercentageParamètre, qui est la condition de déclenchement de la couverture basée sur le ratio de différence de prix.diffAsPercentageparamè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:

    1. Le montant de la couverture doit être calculé en fonction de l'indicateur de couverture.
    2. Le montant qui peut être couvert sur le marché doit répondre au montant minimum de couverture fixé dans les paramètres.
    3. Les actifs de l'échange de l'opération de vente sont suffisants pour vendre, et les actifs de l'échange de l'opération d'achat sont suffisants pour acheter. Lorsque ces conditions sont remplies, exécuter la fonction de couverture pour placer un ordre de couverture.isTradeDans 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 lalastKeepBalanceTSsi 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.

Traitement de paires de négociation de devises dénommées différentes

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 lamainIl convient de noter que laSetRateLa fonction de conversion des taux de change doit être exécutée en premier. Parce que cette fonction affecte deux aspects:

  • Conversion des prix dans toutes les données de marché, les données de commande et les données de position.
  • La conversion de la monnaie libellée dans les actifs du compte. Par exemple, la paire de négociation actuelle estBTC_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 fonctionsexchangel'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.


Relationnée

Plus de