
En matière de stratégies de couverture, il existe une variété de stratégies, une variété de combinaisons et une variété d’idées sur différents marchés. Nous commencerons par la couverture inter-périodes la plus classique pour explorer les idées de conception et les concepts des stratégies de couverture. Aujourd’hui, le marché des devises numériques est beaucoup plus actif qu’à l’époque de sa création, et de nombreuses bourses de contrats ont vu le jour, offrant un grand nombre d’opportunités d’arbitrage et de couverture. Il existe d’innombrables stratégies, telles que l’arbitrage spot-cross-market, l’arbitrage de couverture spot-futures, l’arbitrage cross-period-futures, l’arbitrage cross-market-futures, etc. Ensuite, examinons une stratégie de couverture inter-périodes « hardcore » écrite en langage C++, le marché de négociation étant la bourse de contrats OKEX. La stratégie est écrite sur la base de la « plateforme de trading quantitative Inventor ».
La raison pour laquelle la stratégie est quelque peu difficile est qu’elle est écrite en langage C++, ce qui la rend légèrement plus difficile à lire. Cependant, cela n’empêche pas les lecteurs d’apprendre l’essence de cette conception et de ces idées stratégiques. La stratégie est relativement concise et la longueur du code est modérée, seulement plus de 500 lignes. En termes d’acquisition de données de marché, contrairement aux stratégies précédentes qui utilisent l’interface REST, cette stratégie utilise l’interface WebSocket pour recevoir les données de marché des échanges. En termes de conception, la structure de la stratégie est raisonnable, le couplage du code est très faible et il est très pratique de l’étendre ou de l’optimiser. La pensée logique est claire et cette conception est non seulement facile à utiliser et à développer. En tant que stratégie d’enseignement, la conception d’une stratégie d’apprentissage est également un bon exemple. Le principe de la stratégie est relativement simple : il s’agit d’utiliser des contrats à terme et des contrats à court terme pour une couverture positive et négative, ce qui est fondamentalement le même que la couverture inter-périodes des contrats à terme sur matières premières. Arbitrage positif, contrats à terme courts et contrats à court terme longs. Couverture, positions longues sur les contrats à terme et positions courtes sur les contrats à court terme. Maintenant que les principes de base sont clairs, il reste à déterminer comment la stratégie déclenche les positions de couverture, comment fermer les positions et comment augmenter les positions. Méthodes de contrôle de position et traitement des détails de la stratégie. La stratégie de couverture se concentre principalement sur la fluctuation de la différence de prix de l’actif sous-jacent et effectue des transactions de régression sur la différence de prix. Toutefois, le spread peut fluctuer légèrement, fortement ou dans une seule direction. Cela entraîne une incertitude dans la couverture des profits et des pertes, mais le risque est toujours bien moindre qu’une tendance unilatérale. De nombreuses optimisations diverses des stratégies inter-périodes choisissent de démarrer à partir du niveau de contrôle de position, à partir des déclencheurs d’ouverture et de fermeture. Par exemple, l’indicateur de Bollinger classique est utilisé comme points d’ouverture et de fermeture d’arbitrage positif et négatif lorsque la différence de prix fluctue. En raison de sa conception raisonnable et de son faible degré de couplage, cette stratégie peut également être facilement modifiée en une stratégie de couverture inter-périodes basée sur un indicateur Bollinger.
#### En regardant le code en général, nous pouvons conclure que le code est principalement divisé en quatre parties.
main fonction.main La fonction est la fonction d’entrée de la stratégie. La boucle principale est exécutée dans cette fonction. En outre, cette fonction effectue également une opération importante, à savoir accéder à l’interface Websocket de l’échange pour obtenir les données du marché des ticks poussés sous forme de données brutes matériau du générateur de données de la ligne K. données.#### En comprenant le code de la stratégie dans son ensemble, nous pouvons désormais analyser progressivement chaque lien pour apprendre pleinement la conception, les idées et les techniques de la stratégie.
State déclarationenum State { // 枚举类型 定义一些 状态
STATE_NA, // 非正常状态
STATE_IDLE, // 空闲
STATE_HOLD_LONG, // 持多仓
STATE_HOLD_SHORT, // 持空仓
};
Étant donné que certaines fonctions du code renvoient un certain état, ces états sont définis dans le type d’énumérationStatemilieu.
Voir le code apparaîtreSTATE_NA C’est-à-dire qu’il s’agit d’un état anormal.STATE_IDLE Il s’agit d’un état inactif, c’est-à-dire d’un état dans lequel des opérations de couverture peuvent être effectuées.STATE_HOLD_LONG L’état de détention d’une position de couverture positive.STATE_HOLD_SHORT L’état de détention d’une position de couverture.
string replace(string s, const string from, const string& to)
toHexinline unsigned char toHex(unsigned char x)
std::string urlencode(const std::string& str)
uint64_t _Time(string &s)
class BarFeeder { // K线 数据生成器类
public:
BarFeeder(int period) : _period(period) { // 构造函数,参数为 period 周期, 初始化列表中初始化
_rs.Valid = true; // 构造函数体中初始化 K线数据的 Valid属性。
}
void feed(double price, Chart *c=nullptr, int chartIdx=0) { // 输入数据,nullptr 空指针类型,chartIdx 索引默认参数为 0
uint64_t epoch = uint64_t(Unix() / _period) * _period * 1000; // 秒级时间戳祛除不完整时间周期(不完整的_period 秒数),转为 毫秒级时间戳。
bool newBar = false; // 标记 新K线Bar 的标记变量
if (_rs.size() == 0 || _rs[_rs.size()-1].Time < epoch) { // 如果 K线数据 长度为 0 。 或者 最后一bar 的时间戳小于 epoch(K线最后一bar 比当前最近的周期时间戳还要靠前)
Record r; // 声明一个 K线bar 结构
r.Time = epoch; // 构造当前周期的K线bar
r.Open = r.High = r.Low = r.Close = price; // 初始化 属性
_rs.push_back(r); // K线bar 压入 K线数据结构
if (_rs.size() > 2000) { // 如果K线数据结构长度超过 2000 , 就剔除最早的数据。
_rs.erase(_rs.begin());
}
newBar = true; // 标记
} else { // 其它情况,不是出现新bar 的情况下的处理。
Record &r = _rs[_rs.size() - 1]; // 引用 数据中最后一bar 的数据。
r.High = max(r.High, price); // 对引用数据的最高价更新操作。
r.Low = min(r.Low, price); // 对引用数据的最低价更新操作。
r.Close = price; // 对引用数据的收盘价更新操作。
}
auto bar = _rs[_rs.size()-1]; // 取最后一柱数据 ,赋值给 bar 变量
json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.Close}; // 构造一个 json 类型数据
if (c != nullptr) { // 图表对象指针不等于 空指针,执行以下。
if (newBar) { // 根据标记判断,如果出现新Bar
c->add(chartIdx, point); // 调用图表对象成员函数add,向图表对象中插入数据(新增K线bar)
c->reset(1000); // 只保留1000 bar的数据
} else {
c->add(chartIdx, point, -1); // 否则就更新(不是新bar),这个点(更新这个bar)。
}
}
}
Records & get() { // 成员函数,获取K线数据的方法。
return _rs; // 返回对象的私有变量 _rs 。(即 生成的K线数据)
}
private:
int _period;
Records _rs;
};
Cette classe est principalement responsable du traitement des données de ticks acquises en ligne K de différence de prix pour piloter la logique de couverture de la stratégie. Certains lecteurs peuvent se demander pourquoi nous avons besoin d’utiliser des données de ticks ? Pourquoi avons-nous besoin de construire un tel générateur de données K-line ? N’est-il pas préférable d’utiliser directement les données de la ligne K ? Ces trois questions me sont venues à l’esprit lorsque j’écrivais certaines stratégies de couverture. J’ai trouvé la réponse lorsque j’ai écrit la stratégie de couverture du spread Bollinger. Parce que les données K-line d’un seul contrat sont les statistiques de variation de prix de ce contrat au cours d’une certaine période. Les données K-line de la différence de prix entre deux contrats sont les statistiques de la différence de prix sur une certaine période. Par conséquent, nous ne pouvons pas simplement soustraire les données K-line des deux contrats pour calculer la différence de chaque donnée sur chaque K-line. Ligne Bar. Valeur, comme différence de prix. L’erreur la plus évidente est que le prix le plus élevé et le prix le plus bas de deux contrats ne sont pas nécessairement identiques. Par conséquent, la valeur soustraite n’a pas beaucoup de sens. Par conséquent, nous devons utiliser des données de ticks en temps réel, calculer la différence de prix en temps réel et établir des statistiques sur les variations de prix au cours d’une certaine période en temps réel (c’est-à-dire l’ouverture haute et la clôture basse sur la colonne K-line). De cette manière, nous avons besoin d’un générateur de données K-line en tant que classe distincte pour séparer la logique de traitement.
class Hedge { // 对冲类,策略主要逻辑。
public:
Hedge() { // 构造函数
...
};
State getState(string &symbolA, Depth &depthA, string &symbolB, Depth &depthB) { // 获取状态,参数: 合约A名称 、合约A深度数据, 合约B名称、 合约B深度数据
...
}
bool Loop(string &symbolA, Depth &depthA, string &symbolB, Depth &depthB, string extra="") { // 开平仓 策略主要逻辑
...
}
private:
vector<double> _addArr; // 对冲加仓列表
string _state_desc[4] = {"NA", "IDLE", "LONG", "SHORT"}; // 状态值 描述信息
int _countOpen = 0; // 开仓次数
int _countCover = 0; // 平仓次数
int _lastCache = 0; //
int _hedgeCount = 0; // 对冲次数
int _loopCount = 0; // 循环计数(循环累计次数)
double _holdPrice = 0; // 持仓价格
BarFeeder _feederA = BarFeeder(DPeriod); // A合约 行情 K线生成器
BarFeeder _feederB = BarFeeder(DPeriod); // B合约 行情 K线生成器
State _st = STATE_NA; // 对冲类型 对象的 对冲持仓状态
string _cfgStr; // 图表配置 字符串
double _holdAmount = 0; // 持仓量
bool _isCover = false; // 是否平仓 标记
bool _needCheckOrder = true; // 设置是否 检查订单
Chart _c = Chart(""); // 图表对象,并初始化
};
Comme le code est assez long, une partie est omise. Il montre principalement la structure de la classe hedge. Le constructeur Hedge n’est pas mentionné, car il sert principalement à l’initialisation des objets. Il reste principalement deux fonctions.
Cette fonction gère principalement la détection des commandes, l’annulation des commandes, la détection des positions, l’équilibrage des positions, etc. Parce que dans le processus de transactions de couverture, la situation à une seule étape (c’est-à-dire qu’un contrat est négocié et l’autre non) est inévitable. Si elle est détectée dans la logique de l’ordre, l’ordre de suivi ou la clôture de la position est alors traité , la logique de la stratégie sera chaotique. Nous avons donc adopté une autre approche lors de la conception de cette pièce. Si l’opération de couverture est déclenchée, un ordre est placé une fois. Indépendamment du fait qu’une couverture à une seule étape se produise ou non, la couverture est considérée comme réussie par défaut. Ensuite, le solde de la position est vérifié dans la fonction getState, et la logique de vérification et de traitement de l’état est exécutée. le solde est séparé.
La logique de trading de la stratégie est encapsulée dans cette fonction, où l’appelgetState , utilisez l’objet générateur de données K-line pour générer les données K-line de la différence de prix et portez des jugements sur la logique de couverture de l’ouverture, de la fermeture et de l’ajout de positions. Il existe également certaines opérations de mise à jour des données pour les graphiques.
void main() {
...
string realSymbolA = exchange.SetContractType(symbolA)["instrument"]; // 获取设置的A合约(this_week / next_week / quarter ) ,在 OKEX 合约 当周、次周、季度 对应的真实合约ID 。
string realSymbolB = exchange.SetContractType(symbolB)["instrument"]; // ...
string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump()); // 对 ws 接口的要传的参数进行 json 编码、 url 编码
Log("try connect to websocket"); // 打印连接 WS接口的信息。
auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs); // 调用FMZ API Dial 函数 访问 OKEX 期货的 WS 接口
Log("connect to websocket success");
Depth depthA, depthB; // 声明两个 深度数据结构的变量 用于储存A合约和B合约 的深度数据
auto fillDepth = [](json &data, Depth &d) { // 用接口返回的json 数据,构造 Depth 数据的代码。
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; // 时间 字符串 A
string timeB; // 时间 字符串 B
while (true) {
auto buf = ws.read(); // 读取 WS接口 推送来的数据
...
}
Une fois la stratégie démarrée, elle commence à s’exécuter à partir de la fonction principale. Lors de l’initialisation de la fonction principale, la stratégie s’abonne au marché des ticks de l’interface WebSocket. La tâche principale de la fonction principale est de construire une boucle principale, de recevoir en continu les informations de tick poussées par l’interface Websocket de l’échange, puis d’appeler la fonction membre de l’objet de classe hedge : la fonction Loop. La logique de trading dans la fonction Loop est pilotée par les données du marché. Une chose qui doit être expliquée est que le marché des ticks mentionné ci-dessus est en fait l’interface de données de profondeur du carnet d’ordres souscrit, qui obtient les données du carnet d’ordres de chaque niveau. Cependant, la stratégie utilise uniquement les données du premier niveau, qui sont en fait similaires aux données du marché des ticks. La stratégie n’utilise pas les données des autres niveaux, ni les valeurs du volume des commandes du premier niveau. Examinons de plus près comment la stratégie s’abonne aux données de l’interface WebSocket et comment elle est définie.
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 success");
Tout d’abord, vous devez encoder l’URL du paramètre JSON du message d’abonnement transmis par l’interface d’abonnement, c’est-à-direpayload La valeur du paramètre. Ensuite, l’étape la plus importante consiste à appeler la fonction d’interface API de la plateforme de trading quantitative InventorDial fonction.Dial La fonction peut être utilisée pour accéder à l’interface Websocket d’échange. Nous effectuons ici quelques réglages pour permettre à l’objet de contrôle de connexion WebSocket ws d’être créé pour se reconnecter automatiquement après une déconnexion (le message d’abonnement utilise toujours la chaîne de valeur qs du paramètre payload). Pour réaliser cette fonction, vous devezDial Ajoutez des options de configuration à la chaîne de paramètres de la fonction.
DialLes paramètres de la fonction commencent comme suit :
wss://real.okex.com:10442/ws/v3
Il s’agit de l’adresse de l’interface WebSocket à laquelle il faut accéder, puis l’utiliser.| Séparation.
compress=gzip_raw&mode=recv&reconnect=true&payload="+qs Ce sont tous des paramètres de configuration.
| Nom du paramètre | Description |
|---|---|
| compress | compress est la méthode de compression. L’interface WebSocket OKEX utilise gzip_raw, elle est donc définie sur gzip_raw |
| mode | mode est le mode et les options sont double, envoi et réception. double signifie bidirectionnel, envoi de données compressées et réception de données compressées. envoyer consiste à envoyer des données compressées. recv reçoit les données compressées et les décompresse localement. |
| reconnect | reconnect indique si la reconnexion doit être définie. reconnect=true active la reconnexion. Si elle n’est pas définie, la reconnexion est désactivée par défaut. |
| payload | La charge utile est le message d’abonnement qui doit être envoyé lorsque ws se reconnecte. |
Après ce paramètre, même si la connexion WebSocket est déconnectée, le système sous-jacent du dépositaire de la plateforme de trading quantitative Inventor se reconnectera automatiquement et obtiendra les dernières données de marché en temps opportun. Saisissez chaque fluctuation de différence de prix et capturez rapidement le marché de couverture approprié.
Le contrôle de position utilise un ratio de position de couverture similaire à la série « Porfinacci » pour contrôler.
for (int i = 0; i < AddMax + 1; i++) { // 构造 控制加仓数量的数据结构,类似 波菲纳契数列 对冲数量 比例。
if (_addArr.size() < 2) { // 前两次加仓量变化为: 加一倍对冲数量 递增
_addArr.push_back((i+1)*OpenAmount);
}
_addArr.push_back(_addArr[_addArr.size()-1] + _addArr[_addArr.size()-2]); // 最后 两个加仓数量相加,算出当前的加仓数量储存到 _addArr数据结构中。
}
On peut voir que le nombre de positions ajoutées à chaque fois est la somme des deux positions les plus récentes. Un tel contrôle de position peut permettre que plus la différence de prix est grande, plus le montant de la couverture d’arbitrage est important et que les positions peuvent être dispersées, de manière à saisir les faibles fluctuations de différence de prix avec de petites positions et à augmenter les positions de manière appropriée avec une grande différence de prix. fluctuations.
Spread de take profit fixe, spread stop loss. Lorsque la différence de prix de la position atteint la position take-profit ou la position stop-loss, le take-profit ou le stop loss sera exécuté.
La période contrôlée par le paramètre NPeriod fournit un certain degré de contrôle dynamique sur l’ouverture et la fermeture des positions de la stratégie.
La stratégie génère automatiquement un graphique en chandeliers de différence de prix et marque les informations de transaction pertinentes.
L’opération de dessin de graphique personnalisé de la stratégie C++ est également très simple. Vous pouvez voir que dans le constructeur de la classe hedge, nous utilisons la chaîne de configuration de graphique écrite _cfgStr pour configurer l’objet graphique _c._c est la classe de couverture. Lorsque le membre privé est initialisé, la fonction d’interface API du graphique personnalisé quantitatif de l’inventeur est appelée.Chart L’objet graphique construit par la fonction.
_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); // 用图表配置 更新图表对象
_c.reset(); // 重置图表数据。
_c.update(_cfgStr); utiliser _cfgStr Configuration de l’objet graphique._c.reset(); Réinitialiser les données du graphique.Lorsque le code de stratégie doit insérer des données dans le graphique, il est également appelé directement_fonction membre de l’objet c, ou_La référence de c est passée en paramètre, puis la fonction membre objet (méthode) de _c est appelée pour effectuer les opérations de mise à jour et d’insertion de données de graphique. Par exemple:
_c.add(chartIdx, {{"x", UnixNano()/1000000}, {"title", action}, {"text", format("diff: %f", opPrice)}, {"color", color}});
Après avoir passé une commande, marquez-la sur le graphique en chandeliers.
Comme indiqué ci-dessous, le dessin de la ligne K se fait en appelant la fonction membre de la classe BarFeederfeed Lorsque l’objet graphique_Une référence à c est passée en paramètre.
void feed(double price, Chart *c=nullptr, int chartIdx=0)
Tout de suitefeedLe paramètre c de la fonction.
json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.Close}; // 构造一个 json 类型数据
if (c != nullptr) { // 图表对象指针不等于 空指针,执行以下。
if (newBar) { // 根据标记判断,如果出现新Bar
c->add(chartIdx, point); // 调用图表对象成员函数add,向图表对象中插入数据(新增K线bar)
c->reset(1000); // 只保留1000 bar个数据
} else {
c->add(chartIdx, point, -1); // 否则就更新(不是新bar),这个点(更新这个bar)。
}
}
En appelant l’objet graphique_caddFonction membre, insère de nouvelles données de barre K-line dans le graphique.
Code:c->add(chartIdx, point);



Cette stratégie est uniquement destinée à l’apprentissage et à la communication. Lorsque vous l’utilisez dans le cadre d’un trading réel, veuillez la modifier et l’optimiser en fonction de la situation réelle du trading.
Adresse de la stratégie : https://www.fmz.com/strategy/163447
Des stratégies plus intéressantes sont disponibles sur la « Plateforme de trading quantitative Inventor » : https://www.fmz.com