"Version C++ de la stratégie de couverture des contrats à terme d'OKEX" qui vous emmène à travers une stratégie quantitative hardcore

Auteur:La bonté, Créé: 2019-08-29 16:05:07, Mis à jour: 2023-11-07 20:53:27

img

En parlant de stratégies de couverture, il existe différents types, combinaisons diverses et idées diverses sur différents marchés. Nous explorons les idées de conception et les concepts de la stratégie de couverture à partir de la couverture intertemporelle la plus classique. Aujourd'hui, le marché de la crypto-monnaie est beaucoup plus actif qu'au début, et il existe également de nombreux échanges de contrats à terme qui offrent de nombreuses opportunités de couverture d'arbitrage.

Principe de stratégie

Pourquoi la stratégie est un peu hardcore parce que la stratégie est écrite en c++ et la lecture de la stratégie est un peu plus difficile. Mais cela n'empêche pas les lecteurs d'apprendre l'essence de cette stratégie conception et des idées. La logique de la stratégie est relativement simple, la longueur du code est modérée, seulement 500 lignes. En termes d'acquisition de données de marché, contrairement aux autres stratégies qui utilisent l'interface rest. Cette stratégie utilise l'interface websocket pour accepter les devises du marché des changes.

En termes de conception, la structure de la stratégie est raisonnable, le degré de couplage du code est très faible et il est pratique de l'étendre ou de l'optimiser. La logique est claire, et une telle conception n'est pas seulement facile à comprendre. En tant que matériel pédagogique, l'apprentissage de la conception de cette stratégie est également un bon exemple. Le principe de cette stratégie est relativement simple, c'est-à-dire, le spread du contrat à terme et le contrat récent sont-ils positifs ou négatifs? le principe de base est conforme à la couverture intertemporelle des contrats à terme sur matières premières.

  • Diffusion positive, vente de contrats à terme courts, achat de contrats récents longs.
  • Diffusion négative, achat de contrats à terme longs, vente de contrats récents courts.

Après avoir compris les principes de base, le reste est la façon dont la stratégie déclenche la position d'ouverture de la couverture, comment fermer la position, comment ajouter des positions, la méthode de contrôle de la position totale et d'autres détails de traitement de la stratégie.

La stratégie de couverture concerne principalement la fluctuation de la différence de prix de l'objet (le Spread) et sa régression.

Cela entraîne une incertitude quant aux profits et pertes de couverture, mais le risque est encore beaucoup plus faible que la tendance unilatérale. pour les différentes optimisations de la stratégie intertemporelle, nous pouvons choisir de commencer par le niveau de contrôle de la position et la condition de déclenchement d'ouverture et de fermeture. par exemple, nous pouvons utiliser le classique indicateur de bande de Bollinger pour déterminer la fluctuation des prix. En raison de la conception raisonnable et du faible degré de couplage, cette stratégie peut être facilement modifiée en indice de Bollinger stratégie de couverture intertemporelle

Analyse du code de stratégie

En regardant le code tout au long, vous pouvez conclure que le code est grossièrement divisé en quatre parties.

  1. Enumérer les définitions de valeur, définir certaines valeurs d'état et utiliser pour marquer les états. Certaines fonctions fonctionnelles qui ne sont pas liées à la stratégie, telles que les fonctions d'encodage d'url, les fonctions de conversion de temps, etc., n'ont aucune relation avec la logique de stratégie, uniquement pour le traitement des données.

  2. Classe de générateur de données en ligne K: la stratégie est guidée par les données en ligne K générées par l'objet de classe générateur.

  3. Classe de couverture: les objets de cette classe peuvent exécuter une logique de négociation spécifique, des opérations de couverture et traiter les détails de la stratégie.

  4. La fonction principale de la stratégie, qui est la fonction main. La fonction principale est la fonction d'entrée de la stratégie. La boucle principale est exécutée à l'intérieur de cette fonction. En outre, cette fonction effectue également une opération importante, c'est-à-dire accéder à l'interface websocket de l'échange et obtenir les données de marché de tick brutes poussées en tant que générateur de données K-line.

Grâce à la compréhension globale du code de stratégie, nous pouvons progressivement apprendre les différents aspects de la stratégie, puis étudier la conception, les idées et les compétences de la stratégie.

  • Définition des valeurs d'énumération, autres fonctions de fonction
  1. type énuméréStateDéclaration
enum State {                    // Enum type defines some states
    STATE_NA,                   // Abnormal state
    STATE_IDLE,                 // idle
    STATE_HOLD_LONG,            // holding long positions
    STATE_HOLD_SHORT,           // holding short positions
};

Parce que certaines fonctions dans le code renvoient un état, ces états sont définis dans le type d'énumérationState.

En voyant çaSTATE_NAapparaît dans le code est anormal, etSTATE_IDLEest inactif, c'est-à-dire que l'état de l'opération peut être couvert.STATE_HOLD_LONGest l'état dans lequel la position de couverture positive est détenue.STATE_HOLD_SHORTest l'état dans lequel la position de couverture négative est détenue.

  1. La substitution de chaîne, non appelée dans cette stratégie, est une fonction d'utilité alternative, traitant principalement des chaînes.
string replace(string s, const string from, const string& to)
  1. Une fonction de conversion en caractères hexadécimauxtoHex
inline unsigned char toHex(unsigned char x)
  1. Gérer les fonctions encodées par url
std::string urlencode(const std::string& str)
  1. Une fonction de conversion de l'heure qui convertit l'heure au format de chaîne en horodatage.
uint64_t _Time(string &s)
  • Classe de générateur de données de ligne K
class BarFeeder { // K line data generator class
    public:
        BarFeeder(int period) : _period(period) { // constructor with argument "period" period, initialized in initialization list
            _rs.Valid = true; // Initialize the "Valid" property of the K-line data in the constructor body.
        }

        void feed(double price, chart *c=nullptr, int chartIdx=0) { // input data, "nullptr" null pointer type, "chartIdx" index default parameter is 0
            uint64_t epoch = uint64_t(Unix() / _period) * _period * 1000; // The second-level timestamp removes the incomplete time period (incomplete _period seconds) and is converted to a millisecond timestamp.
            bool newBar = false; // mark the tag variable of the new K line Bar
            if (_rs.size() == 0 || _rs[_rs.size()-1].Time < epoch) { // if the K line data is 0 in length. Or the last bar's timestamp is less than epoch (the last bar of the K line is more than the current most recent cycle timestamp)
                record r; // declare a K line bar structure
                r.Time = epoch; // construct the K line bar of the current cycle
                r.Open = r.High = r.Low = r.close = price; // Initialize the property
                _rs.push_back(r); // K line bar is pressed into the K line data structure
                if (_rs.size() > 2000) { // if the K-line data structure length exceeds 2000, the oldest data is removed.
                    _rs.erase(_rs.begin());
                }
                newBar = true; // tag
            } else { // In other cases, it is not the case of a new bar.
                record &r = _rs[_rs.size() - 1]; // Reference the data of the last bar in the data.
                r.High = max(r.High, price); // The highest price update operation for the referenced data.
                r.Low = min(r.Low, price); // The lowest price update operation for the referenced data.
                r.close = price; // Update the closing price of the referenced data.
            }
    
            auto bar = _rs[_rs.size()-1]; // Take the last column data and assign it to the bar variable
            json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.close}; // construct a json type data
            if (c != nullptr) { // The chart object pointer is not equal to the null pointer, do the following.
               if (newBar) { // judge if the new Bar appears
                    c->add(chartIdx, point); // call the chart object member function add to insert data into the chart object (new k line bar)
                    c->reset(1000); // retain only 1000 bar of data
                } else {
                    c->add(chartIdx, point, -1); // Otherwise update (not new bar), this point (update this bar).
                }
            }
        }
        records & get() { // member function, method for getting K line data.
            Return _rs; // Returns the object's private variable _rs . (ie generated K-line data)
        }
    private:
        int _period;
        records _rs;
};

Cette classe est principalement responsable du traitement des données de tick acquises dans une ligne K de différence pour conduire la logique de couverture de la stratégie.

Certains lecteurs peuvent avoir des questions, pourquoi utiliser des données de tick? Pourquoi construire un générateur de données de ligne K comme ceci? N'est-il pas bon d'utiliser directement des données de ligne K? Ce genre de question a été émis en trois éclats. Lorsque j'ai écrit quelques stratégies de couverture, j'ai également fait un tollé. J'ai trouvé la réponse lorsque j'ai écrit la stratégie de couverture Bollinger. Puisque les données de ligne K pour un seul contrat sont les statistiques de variation de prix pour ce contrat sur une certaine période de temps.

Les données de la ligne K de la différence entre les deux contrats sont les statistiques de variation des prix de la différence dans une certaine période. Par conséquent, il n'est pas possible de simplement prendre les données de la ligne K de chacun des deux contrats pour la soustraction et de calculer la différence de chaque donnée sur chaque barre de ligne K. L'erreur la plus évidente est, par exemple, le prix le plus élevé et le prix le plus bas de deux contrats, pas nécessairement en même temps.

Par conséquent, nous avons besoin d'utiliser des données de tick en temps réel pour calculer la différence en temps réel et calculer la variation de prix dans une certaine période en temps réel (c'est-à-dire le prix le plus élevé, le plus bas, l'ouverture et la fermeture sur la colonne K-line).

  • Classes de couverture
class Hedge { // Hedging class, the main logic of the strategy.
  public:
    Hedge() { // constructor
        ...
    };
    
    State getState(string &symbolA, depth &depthA, string &symbolB, depth &depthB) { // Get state, parameters: contract A name, contract A depth data, contract B name, contract B depth data
        
        ...
    }
    bool Loop(string &symbolA, depth &depthA, string &symbolB, depth &depthB, string extra="") { // Opening and closing position main logic
        
        ...
    }

  private:
    vector<double> _addArr; // Hedging adding position list
    string _state_desc[4] = {"NA", "IDLE", "LONG", "SHORT"}; // Status value Description
    int _countOpen = 0; // number of opening positions
    int _countcover = 0; // number of closing positions
    int _lastcache = 0; //
    int _hedgecount = 0; // number of hedging
    int _loopcount = 0; // loop count (cycle count)
    double _holdPrice = 0; // holding position price
    BarFeeder _feederA = BarFeeder(DPeriod); // A contract Quote K line generator
    BarFeeder _feederB = BarFeeder(DPeriod); // B contract Quote K line generator
    State _st = STATE_NA; // Hedging type Object Hedging position status
    string _cfgStr; // chart configuration string
    double _holdAmount = 0; // holding position amount
    bool _iscover = false; // the tag of whether to close the position
    bool _needcheckOrder = true; // Set whether to check the order
    chart _c = chart(""); // chart object and initialize
};

Parce que le code est relativement long, certaines parties sont omises, ce qui montre principalement la structure de cette classe de couverture, la fonction constructeur Hedge est omise, principalement dans le but de l'initialisation de l'objet.

- Je suis désolé.

Cette fonction traite principalement de l'inspection des ordres, de l'annulation des ordres, de la détection des positions, de l'équilibrage des positions, etc. Parce que dans le processus des opérations de couverture, il est impossible d'éviter une seule étape (c'est-à-dire qu'un contrat est exécuté, un autre n'est pas), si l'examen est effectué dans la logique de placement des ordres, puis le traitement de l'opération de réexpédition des ordres ou l'opération de fermeture des positions, la logique de stratégie sera chaotique.

Donc, lors de la conception de cette partie, j'ai pris une autre idée. si l'opération de couverture est déclenchée, tant que l'ordre est placé une fois, indépendamment du fait qu'il y ait une couverture à une seule jambe, la valeur par défaut est que la couverture est réussie, et puis le solde de position est détecté dans legetStateLa fonction et la logique de traitement du solde seront traitées indépendamment.

Boucle

La logique de négociation de la stratégie est résumée dans cette fonction, dans laquellegetStateL'objet générateur de données K-line est utilisé pour générer les données K-line de la différence (l'écart), et le jugement d'ouverture, de fermeture et d'addition de la logique de position est effectué.

  • Principale fonction de la stratégie
void main() {

    ...
    
    string realSymbolA = exchange.SetcontractType(symbolA)["instrument"]; // Get the A contract (this_week / next_week / quarter ), the real contract ID corresponding to the week, next week, and quarter of the OKEX futures contract.
    string realSymbolB = exchange.SetcontractType(symbolB)["instrument"]; // ...
    
    string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump()) ; // jSON encoding, url encoding for the parameters to be passed on the ws interface
    Log("try connect to websocket"); // Print the information of the connection WS interface.
    auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs); // call the FMZ API "Dial" function to acess the WS interface of OKEX Futures
    Log("connect to websocket sucess");
    
    depth depthA, depthB; // Declare two variables of the depth data structure to store the depth data of the A contract and the B contract
    auto filldepth = [](json &data, depth &d) { // construct the code for the depth data with the json data returned by the interface.
        d.Valid = true;
        d.Asks.clear();
        d.Asks.push_back({atof(string(data["asks"][0][0]).c_str()), atof(string(data["asks"][0][1]).c_str( ))});
        d.Bids.clear();
        d.Bids.push_back({atof(string(data["bids"][0][0]).c_str()), atof(string(data["bids"][0][1]).c_str( ))});
    };
    string timeA; // time string A
    string timeB; // time string B
    while (true) {
        auto buf = ws.read(); // Read the data pushed by the WS interface
        
        ...
        
}

Après le démarrage de la stratégie, elle est exécutée à partir de la fonction principale. Lors de l'initialisation de la fonction principale, la stratégie s'abonne au tick market de l'interface websocket. La tâche principale de la fonction principale est de construire une boucle principale qui reçoit continuellement les tick quotes poussées par l'interface websocket de l'échange, puis appelle la fonction membre de l'objet de la classe de couverture: fonction de boucle.

Un point à noter est que le marché des ticks mentionné ci-dessus est en fait l'interface de données de profondeur mince des commandes d'abonnement, qui est les données du carnet de commandes pour chaque fichier. Cependant, la stratégie n'utilise que le premier fichier de données, en fait, elle est presque la même que les données du marché des ticks.

Examinons de plus près comment la stratégie souscrit aux données de l'interface websocket et comment elle est configurée.

string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump());    
Log("try connect to websocket");                                                                                                            
auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs);     
Log("connect to websocket sucess");

Tout d'abord, le codage url du paramètre json du message d'abonnement passé par l'interface souscrite, c'est-à-dire la valeur dupayloadEnsuite, une étape importante est d'appeler la fonction d'interface API de la plateforme FMZ QuantDialLa fonctionDialici nous faisons quelques paramètres, laissez le contrôle de connexion websocket objet ws à créer ont une reconnexion automatique de déconnexion (le message d'abonnement utilise toujours la valeurqschaîne de lapayloadparamètre), pour atteindre cette fonction, vous devez ajouter la configuration dans la chaîne de paramètres de laDial function.

Le début de laDialle paramètre de fonction est le suivant:

wss://real.okex.com:10442/ws/v3

c'est l'adresse de l'interface de la socket web à laquelle il faut accéder, et qui est séparée par you.

compress=gzip_raw&mode=recv&reconnect=true&payload="+qssont tous des paramètres de configuration.

Nom du paramètre Définition
compresser compresser est le mode de compression, l'interface du websocket OKEX utilise gzip_raw de cette façon, donc il est réglé sur gzip_raw
le mode Le mode est le mode, en option double, envoyer et récupérer trois types. Le double est bidirectionnel, envoyer des données compressées et recevoir des données compressées. Envoyer est d'envoyer des données compressées. Recv reçoit les données compressées et les décomprime localement.
reconnecter Reconnect est réglé sur reconnect, reconnect=true pour activer la reconnexion, aucun défaut n'est reconnecté.
charge utile La charge utile est un message d'abonnement qui doit être envoyé lorsque ws est reconnecté.

Après ce réglage, même si la connexion websocket est déconnectée, le système sous-jacent de la plate-forme de trading FMZ Quant du système docker se reconnectera automatiquement et obtiendra les dernières données du marché à temps.

Saisissez toutes les fluctuations de prix et capturez rapidement la bonne couverture.

  • Contrôle de la position

Le contrôle des positions est contrôlé à l'aide d'un ratio de positions de couverture similaire à la série Fibonaci.

for (int i = 0; i < AddMax + 1; i++) { // construct a data structure that controls the number of scalping, similar to the ratio of the Bofinac sequence to the number of hedges.
     if (_addArr.size() < 2) { // The first two added positions are changed as: double the number of hedges
         _addArr.push_back((i+1)*OpenAmount);
     }
     _addArr.push_back(_addArr[_addArr.size()-1] + _addArr[_addArr.size()-2]); // The last two adding positions are added together, and the current position quantity is calculated and stored in the "_addArr" data structure.
}

On voit que le nombre de positions supplémentaires ajoutées à chaque fois est la somme des deux dernières positions.

Un tel contrôle de position peut réaliser la plus grande différence, l'augmentation relative de la couverture d'arbitrage et la dispersion de la position, afin de saisir la petite position de la petite fluctuation de prix, et la grande position de fluctuation de prix est augmentée de manière appropriée.

  • position de clôture: stop loss et profit

Différence fixe de stop loss et de prise de profit.

Lorsque la différence de position atteint la position take profit et la position stop loss, les positions take profit et stop loss sont exécutées.

  • La conception de l'entrée et de la sortie du marché

La période du paramètreNPeriodLe contrôle fournit un certain contrôle dynamique sur la position d'ouverture et de fermeture de la stratégie.

  • Tableau de stratégie

La stratégie génère automatiquement un graphique de lignes K pour marquer les informations de transaction pertinentes.

Vous pouvez voir que dans le constructeur de la classe de couverture, nous utilisons la chaîne de configuration de graphique écrite_cfgStrpour configurer l'objet graphique_c, _cest la composante privée de la classe de couverture.chartl'objet construit par la fonction d'interface API de la plateforme FMZ Quant est appelé.

_cfgStr = R"EOF(
[{
"extension": { "layout": "single", "col": 6, "height": "500px"},
"rangeSelector": {"enabled": false},
"tooltip": {"xDateformat": "%Y-%m-%d %H:%M:%S, %A"},
"plotOptions": {"candlestick": {"color": "#d75442", "upcolor": "#6ba583"}},
"chart":{"type":"line"},
"title":{"text":"Spread Long"},
"xAxis":{"title":{"text":"Date"}},
"series":[
    {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
    {"type":"flags","data":[], "onSeries": "dataseriesA"}
    ]
},
{
"extension": { "layout": "single", "col": 6, "height": "500px"},
"rangeSelector": {"enabled": false},
"tooltip": {"xDateformat": "%Y-%m-%d %H:%M:%S, %A"},
"plotOptions": {"candlestick": {"color": "#d75442", "upcolor": "#6ba583"}},
"chart":{"type":"line"},
"title":{"text":"Spread Short"},
"xAxis":{"title":{"text":"Date"}},
"series":[
    {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
    {"type":"flags","data":[], "onSeries": "dataseriesA"}
    ]
}
]
)EOF";
_c.update(_cfgStr);                 // Update chart objects with chart configuration
_c.reset();                         // Reset chart data。
call _c.update(_cfgStr); Use _cfgStr to configure to the chart object.

call _c.reset(); to reset the chart data.

Lorsque le code de stratégie a besoin d'insérer des données dans le graphique, il appelle également la fonction membre de la_cl'objet directement, ou passe la référence de_ccomme un paramètre, puis appelle la fonction membre objet (méthode) de_cpour mettre à jour les données du graphique et insérer des opérations.

Par exemple:

_c.add(chartIdx, {{"x", UnixNano()/1000000}, {"title", action},  {"text", format("diff: %f", opPrice)}, {"color", color}});

Après avoir passé la commande, marquez le graphique de ligne K.

Comme suit, lors du dessin d'une ligne K, une référence à l'objet du graphique_cest passé comme paramètre lors de l'appel de la fonction membrefeedde l'annéeBarFeeder class.

void feed(double price, chart *c=nullptr, int chartIdx=0)

C'est à dire, le paramètre formelcde l'annéefeed function.

json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.close}; // construct a json type data
if (c != nullptr) { // The chart object pointer is not equal to the null pointer, do the following.
    if (newBar) { // judge if the new Bar appears
         c->add(chartIdx, point); // call the chart object member function "add" to insert data into the chart object (new k line bar)
         c->reset(1000); // only keep 1000 bar data
     } else {
         c->add(chartIdx, point, -1); // Otherwise update (not new bar), this point (update this bar).
     }
}

Insérer une nouvelle ligne K Bar données dans le graphique en appelant leaddfonction membre de l'objet du graphique_c.

c->add(chartIdx, point);

Test de retour

img img img

Cette stratégie est uniquement destinée à l'apprentissage et à la communication.

Adresse stratégique:https://www.fmz.com/strategy/163447

Des stratégies plus intéressantes se trouvent dans la plateforme FMZ Quant":https://www.fmz.com


Relationnée

Plus de