Type/to search
8
Follow
1369
Followers
Analyse de la stratégie de récolte des poireaux (1)
HFT
Created 2020-11-12 22:11:32  Updated 2024-12-06 22:20:54
 26
 11443

img

Analyse de la stratégie de récolte des poireaux

Récemment, l'inventeur du groupe de discussion quantitative WeChatprint moneyLe débat sur les robots a été très animé et une très ancienne stratégie a réapparu dans la vision des quants :Récolteuse de poireaux
print moneyLe principe de trading du robot s'appuie sur la stratégie de la cueilleuse de poireaux. Je me reproche de ne pas avoir bien compris la stratégie de la cueilleuse de poireaux à ce moment-là. J'ai donc revu attentivement la stratégie originale et j'ai également examiné la version transplantée sur Inventor Quant.Récolteuse de poireaux Transplant OKCoin
Prenons la version transplantée de la stratégie de récolte de poireaux de la plateforme quantitative Inventor pour analyser la stratégie et explorer les idées qui la sous-tendent. Pour que les utilisateurs de la plateforme puissent apprendre cette idée stratégique.
Dans cet article, nous analyserons davantage du point de vue de la réflexion stratégique, des intentions, etc., et essaierons de réduire le contenu ennuyeux lié à la programmation.

[Code source de la stratégie Transplant OKCoin Leek Harvester :

function LeeksReaper() { var self = {} self.numTick = 0 self.lastTradeId = 0 self.vol = 0 self.askPrice = 0 self.bidPrice = 0 self.orderBook = {Asks:[], Bids:[]} self.prices = [] self.tradeOrderId = 0 self.p = 0.5 self.account = null self.preCalc = 0 self.preNet = 0 self.updateTrades = function() { var trades = _C(exchange.GetTrades) if (self.prices.length == 0) { while (trades.length == 0) { trades = trades.concat(_C(exchange.GetTrades)) } for (var i = 0; i < 15; i++) { self.prices[i] = trades[trades.length - 1].Price } } self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) { // Huobi not support trade.Id if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) { self.lastTradeId = Math.max(trade.Id == 0 ? trade.Time : trade.Id, self.lastTradeId) mem += trade.Amount } return mem }, 0) } self.updateOrderBook = function() { var orderBook = _C(exchange.GetDepth) self.orderBook = orderBook if (orderBook.Bids.length < 3 || orderBook.Asks.length < 3) { return } self.bidPrice = orderBook.Bids[0].Price * 0.618 + orderBook.Asks[0].Price * 0.382 + 0.01 self.askPrice = orderBook.Bids[0].Price * 0.382 + orderBook.Asks[0].Price * 0.618 - 0.01 self.prices.shift() self.prices.push(_N((orderBook.Bids[0].Price + orderBook.Asks[0].Price) * 0.35 + (orderBook.Bids[1].Price + orderBook.Asks[1].Price) * 0.1 + (orderBook.Bids[2].Price + orderBook.Asks[2].Price) * 0.05)) } self.balanceAccount = function() { var account = exchange.GetAccount() if (!account) { return } self.account = account var now = new Date().getTime() if (self.orderBook.Bids.length > 0 && now - self.preCalc > (CalcNetInterval * 1000)) { self.preCalc = now var net = _N(account.Balance + account.FrozenBalance + self.orderBook.Bids[0].Price * (account.Stocks + account.FrozenStocks)) if (net != self.preNet) { self.preNet = net LogProfit(net) } } self.btc = account.Stocks self.cny = account.Balance self.p = self.btc * self.prices[self.prices.length-1] / (self.btc * self.prices[self.prices.length-1] + self.cny) var balanced = false if (self.p < 0.48) { Log("开始平衡", self.p) self.cny -= 300 if (self.orderBook.Bids.length >0) { exchange.Buy(self.orderBook.Bids[0].Price + 0.00, 0.01) exchange.Buy(self.orderBook.Bids[0].Price + 0.01, 0.01) exchange.Buy(self.orderBook.Bids[0].Price + 0.02, 0.01) } } else if (self.p > 0.52) { Log("开始平衡", self.p) self.btc -= 0.03 if (self.orderBook.Asks.length >0) { exchange.Sell(self.orderBook.Asks[0].Price - 0.00, 0.01) exchange.Sell(self.orderBook.Asks[0].Price - 0.01, 0.01) exchange.Sell(self.orderBook.Asks[0].Price - 0.02, 0.01) } } Sleep(BalanceTimeout) var orders = exchange.GetOrders() if (orders) { for (var i = 0; i < orders.length; i++) { if (orders[i].Id != self.tradeOrderId) { exchange.CancelOrder(orders[i].Id) } } } } self.poll = function() { self.numTick++ self.updateTrades() self.updateOrderBook() self.balanceAccount() var burstPrice = self.prices[self.prices.length-1] * BurstThresholdPct var bull = false var bear = false var tradeAmount = 0 if (self.account) { LogStatus(self.account, 'Tick:', self.numTick, ', lastPrice:', self.prices[self.prices.length-1], ', burstPrice: ', burstPrice) } if (self.numTick > 2 && ( self.prices[self.prices.length-1] - _.max(self.prices.slice(-6, -1)) > burstPrice || self.prices[self.prices.length-1] - _.max(self.prices.slice(-6, -2)) > burstPrice && self.prices[self.prices.length-1] > self.prices[self.prices.length-2] )) { bull = true tradeAmount = self.cny / self.bidPrice * 0.99 } else if (self.numTick > 2 && ( self.prices[self.prices.length-1] - _.min(self.prices.slice(-6, -1)) < -burstPrice || self.prices[self.prices.length-1] - _.min(self.prices.slice(-6, -2)) < -burstPrice && self.prices[self.prices.length-1] < self.prices[self.prices.length-2] )) { bear = true tradeAmount = self.btc } if (self.vol < BurstThresholdVol) { tradeAmount *= self.vol / BurstThresholdVol } if (self.numTick < 5) { tradeAmount *= 0.8 } if (self.numTick < 10) { tradeAmount *= 0.8 } if ((!bull && !bear) || tradeAmount < MinStock) { return } var tradePrice = bull ? self.bidPrice : self.askPrice while (tradeAmount >= MinStock) { var orderId = bull ? exchange.Buy(self.bidPrice, tradeAmount) : exchange.Sell(self.askPrice, tradeAmount) Sleep(200) if (orderId) { self.tradeOrderId = orderId var order = null while (true) { order = exchange.GetOrder(orderId) if (order) { if (order.Status == ORDER_STATE_PENDING) { exchange.CancelOrder(orderId) Sleep(200) } else { break } } } self.tradeOrderId = 0 tradeAmount -= order.DealAmount tradeAmount *= 0.9 if (order.Status == ORDER_STATE_CANCELED) { self.updateOrderBook() while (bull && self.bidPrice - tradePrice > 0.1) { tradeAmount *= 0.99 tradePrice += 0.1 } while (bear && self.askPrice - tradePrice < -0.1) { tradeAmount *= 0.99 tradePrice -= 0.1 } } } } self.numTick = 0 } return self } function main() { var reaper = LeeksReaper() while (true) { reaper.poll() Sleep(TickInterval) } }

Aperçu de la stratégie

En général, lorsque vous recevez une stratégie à apprendre, lorsque vous la lisez, vous devez d’abord regarder la structure globale du programme. Cette stratégie n'a pas beaucoup de code, seulement moins de 200 lignes de code, ce qui est très concis, et le degré de restauration de la stratégie d'origine est très élevé, et c'est fondamentalement le même. Le code de la politique s'applique à partir demain()La fonction commence à s'exécuter et l'intégralité du code de stratégie estmain(), est unLeeksReaper()La fonction,LeeksReaper()La fonction est également facile à comprendre. Cette fonction peut être comprise comme le constructeur du module logique de stratégie de récolte de poireaux (un objet). En termes simplesLeeksReaper()Il est responsable de la construction d'une logique de transaction de récolte de poireaux.

Mots clés:
img
img

  • StratégiemainLa première ligne de la fonction :
    var reaper = LeeksReaper(), le code déclare une variable localereaperAppelez ensuite la fonction LeeksReaper() pour construire un objet logique de stratégie et l'affecter àreaper

  • StratégiemainLa fonction est la suivante :

    while (true) { reaper.poll() Sleep(TickInterval) }

    Entrez unwhileBoucle morte, exécution sans finreaperFonction de traitement d'objetpoll()poll()La fonction est la logique principale de la stratégie de trading, et l'ensemble du programme de stratégie commence à exécuter la logique de trading en continu.
    Quant àSleep(TickInterval)Cette ligne est facile à comprendre. Elle permet de contrôler le temps de pause après chaque exécution de la logique de transaction globale, afin de contrôler la fréquence de rotation de la logique de transaction.

ProfilageLeeksReaper()Constructeur

Jetez un oeilLeeksReaper()Comment les fonctions construisent un objet logique de stratégie.

LeeksReaper()Au début de la fonction, un objet vide est déclaré.var self = {},existerLeeksReaper()Au cours de l'exécution de la fonction, certaines méthodes et propriétés seront progressivement ajoutées à cet objet vide, et finalement la construction de cet objet sera terminée, et enfin cet objet sera renvoyé (c'est-à-dire,main()À l'intérieur de la fonctionvar reaper = LeeksReaper()Dans cette étape, l'objet renvoyé est attribué àreaper)。

DonnerselfAjout de propriétés à un objet

Donner ensuiteselfDe nombreuses propriétés ont été ajoutées. Je décrirai chaque propriété ci-dessous afin que vous puissiez rapidement comprendre le but et l'intention de ces propriétés et variables, ce qui facilitera votre compréhension de la stratégie et évitera d'être confus lorsque vous verrez ce tas de code.

self.numTick = 0 # 用来记录poll函数调用时未触发交易的次数,当触发下单并且下单逻辑执行完时,self.numTick重置为0 self.lastTradeId = 0 # 交易市场已经成交的订单交易记录ID,这个变量记录市场当前最新的成交记录ID self.vol = 0 # 通过加权平均计算之后的市场每次考察时成交量参考(每次循环获取一次市场行情数据,可以理解为考察了行情一次) self.askPrice = 0 # 卖单提单价格,可以理解为策略通过计算后将要挂卖单的价格 self.bidPrice = 0 # 买单提单价格 self.orderBook = {Asks:[], Bids:[]} # 记录当前获取的订单薄数据,即深度数据(卖一...卖n,买一...买n) self.prices = [] # 一个数组,记录订单薄中前三档加权平均计算之后的时间序列上的价格,简单说就是每次储存计算得到的订单薄前三档加权平均价格,放在一个数组中,用于后续策略交易信号参考,所以该变量名是prices,复数形式,表示一组价格 self.tradeOrderId = 0 # 记录当前提单下单后的订单ID self.p = 0.5 # 仓位比重,币的价值正好占总资产价值的一半时,该值为0.5,即平衡状态 self.account = null # 记录账户资产数据,由GetAccount()函数返回数据 self.preCalc = 0 # 记录最近一次计算收益时的时间戳,单位毫秒,用于控制收益计算部分代码触发执行的频率 self.preNet = 0 # 记录当前收益数值

DonnerselfMéthode d'ajout d'objet

Après avoir ajouté ces attributs à vous-même, commencez àselfL'ajout de méthodes à un objet permet à l'objet d'effectuer un travail et d'avoir certaines fonctions.

La première fonction ajoutée :

self.updateTrades = function() { var trades = _C(exchange.GetTrades) # 调用FMZ封装的接口GetTrades,获取当前最新的市场成交数据 if (self.prices.length == 0) { # 当self.prices.length == 0时,需要给self.prices数组填充数值,只有策略启动运行时才会触发 while (trades.length == 0) { # 如果近期市场上没有更新的成交记录,这个while循环会一直执行,直到有最新成交数据,更新trades变量 trades = trades.concat(_C(exchange.GetTrades)) # concat 是JS数组类型的一个方法,用来拼接两个数组,这里就是把“trades”数组和“_C(exchange.GetTrades)”返回的数组数据拼接成一个数组 } for (var i = 0; i < 15; i++) { # 给self.prices填充数据,填充15个最新成交价格 self.prices[i] = trades[trades.length - 1].Price } } self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) { # _.reduce 函数迭代计算,累计最新成交记录的成交量 // Huobi not support trade.Id if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) { self.lastTradeId = Math.max(trade.Id == 0 ? trade.Time : trade.Id, self.lastTradeId) mem += trade.Amount } return mem }, 0) }

updateTradesLe but de cette fonction est d'obtenir les dernières données de transaction du marché, d'effectuer certains calculs basés sur les données et de les enregistrer pour les utiliser dans la logique ultérieure de la stratégie.
J'ai écrit les commentaires ligne par ligne directement dans le code ci-dessus.
pour_.reduceLes étudiants qui n'ont pas de bases en programmation peuvent être déroutés. Voici une brève explication._.reduceOuiUnderscore.jsLes fonctions de cette bibliothèque sont prises en charge par la stratégie FMZJS, il est donc très pratique d'utiliser des calculs itératifs.Lien de données Underscore.js

Le sens est également très simple, par exemple :

function main () { var arr = [1, 2, 3, 4] var sum = _.reduce(arr, function(ret, ele){ ret += ele return ret }, 0) Log("sum:", sum) # sum 等于 10 }

C'est-à-dire le tableau[1, 2, 3, 4]Additionnez chaque nombre dans . Revenons à notre stratégie,tradesLes valeurs de volume de transaction de chaque enregistrement de transaction dans le tableau sont additionnées. Obtenez le dernier enregistrement de transaction et le volume total des transactions.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...)S'il vous plaît permettez-moi d'utiliser...Au lieu de ce tas de code. Il n’est pas difficile de voir ici queself.volLe calcul de est également une moyenne pondérée. Autrement dit, le volume total des transactions les plus récentes représente 30 % de la pondération, et le volume des transactions obtenu par le calcul pondéré précédent représente 70 %. Ce ratio est défini artificiellement par l’auteur de la stratégie et peut être lié à l’observation des tendances du marché.
En ce qui concerne votre question, que se passe-t-il si l'interface permettant d'obtenir les dernières données de transaction me renvoie des données anciennes en double ? Les données que j'obtiendrai seront alors erronées, est-ce que cela a un sens de l'utiliser ? Ne vous inquiétez pas, ce problème a été pris en compte lors de la conception de la stratégie, il est donc inclus dans le code.

if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) { ... }

Ce jugement. Il peut être jugé en fonction de l'ID de transaction dans l'enregistrement de transaction. L'accumulation est déclenchée uniquement lorsque l'ID est supérieur à l'ID du dernier enregistrement, ou si l'interface d'échange ne fournit pas l'ID, c'est-à-diretrade.Id == 0, en utilisant l'horodatage dans l'enregistrement de transaction pour déterminer, à ce momentself.lastTradeIdCe qui est stocké est l'horodatage de l'enregistrement de transaction, pas l'ID.

La deuxième fonction ajoutée :

self.updateOrderBook = function() { var orderBook = _C(exchange.GetDepth) self.orderBook = orderBook if (orderBook.Bids.length < 3 || orderBook.Asks.length < 3) { return } self.bidPrice = orderBook.Bids[0].Price * 0.618 + orderBook.Asks[0].Price * 0.382 + 0.01 self.askPrice = orderBook.Bids[0].Price * 0.382 + orderBook.Asks[0].Price * 0.618 - 0.01 self.prices.shift() self.prices.push(_N((orderBook.Bids[0].Price + orderBook.Asks[0].Price) * 0.35 + (orderBook.Bids[1].Price + orderBook.Asks[1].Price) * 0.1 + (orderBook.Bids[2].Price + orderBook.Asks[2].Price) * 0.05)) }

Prochaine montreupdateOrderBookComme son nom l'indique, cette fonction permet de mettre à jour le carnet de commandes. Mais il s’agit de bien plus qu’une simple mise à jour du carnet de commandes. La fonction commence à appeler la fonction API de FMZGetDepth()Obtenez les données actuelles du carnet d'ordres du marché (vendre un...vendre n, acheter un...acheter n) et enregistrez les données du carnet d'ordres dansself.orderBookmilieu. Ensuite, si les données du carnet d'ordres contiennent moins de 3 ordres d'achat et de vente, la fonction est considérée comme non valide et renvoie directement.

Ensuite, deux données ont été calculées :

  • Calculer le prix du connaissement
    Le prix du connaissement est également calculé à l'aide de la moyenne pondérée. Lors du calcul de l'ordre d'achat, l'ordre d'achat se voit attribuer un poids plus important de 61,8 % (0,618) et l'ordre de vente représente le poids restant de 38,2 % (0,382).
    Il en va de même lors du calcul du prix de vente du connaissement, le prix de vente ayant une plus grande importance. Quant à la raison pour laquelle ce nombre est de 0,618, il se peut que l’auteur préfère le nombre d’or. Quant à la légère augmentation ou diminution finale du prix (0,01), elle consiste à le déplacer légèrement vers le centre du marché.

  • Mettre à jour le prix moyen pondéré des trois premiers niveaux du carnet d'ordres sur la série chronologique
    Un calcul de moyenne pondérée est effectué sur les trois premiers niveaux de prix des ordres d'achat et de vente dans le carnet d'ordres, le premier niveau ayant un poids de 0,7, le deuxième niveau ayant un poids de 0,2 et le troisième niveau ayant un poids de 0,1 . Certains étudiants pourraient dire : « Oh, ce n'est pas vrai, il n'y a pas de 0,7, 0,2, 0,1 dans le code »
    Développons le calcul :

    (买一 + 卖一) * 0.35 + (买二 + 卖二) * 0.1 + (买三 + 卖三) * 0.05 -> (买一 + 卖一) / 2 * 2 * 0.35 + (买二 + 卖二) / 2 * 2 * 0.1 + (买三 + 卖三) / 2 * 2 * 0.05 -> (买一 + 卖一) / 2 * 0.7 + (买二 + 卖二) / 2 * 0.2 + (买三 + 卖三) / 2 * 0.1 -> 第一档平均的价格 * 0.7 + 第二档平均的价格 * 0.2 + 第三档平均的价格 * 0.1

    Ici, vous pouvez voir que le prix final calculé reflète en réalité la position de prix médiane des trois niveaux du marché actuel.
    Utilisez ensuite ce prix calculé pour mettre à jourself.pricesTableau, expulse les données les plus anciennes (viashift()fonction), mettre à jour les dernières données (viapush()Les fonctions Function, Shift et Push sont des méthodes d'objets de tableau en langage JS. Vous pouvez consulter les informations JS pour plus de détails). Formant ainsiself.pricesUn tableau est un flux de données ordonné en série chronologique.

Ahem, bois un peu d'eau, je m'arrête ici pour l'instant, à la prochaine~

Related Recommendations
Comment
All comments (21)

    梦总你好,想问一下。self.prices 先填充的15个历史成交价格,然后又填充订单薄前三档加权平均价格. 对吧

    5 years ago

    是的。

    5 years ago

    我想给梦总点个赞

    5 years ago

    可惜了,很多交易所的做市商已经将盘口买一卖一价压到了只剩下一个tick,让策略里这种尝试插入到买一卖一中间的操作没有了意义

    6 years ago

    主要是学习思路,洞察这些可以高频的交易机会。

    6 years ago

    谢谢了,我写了python的版本在山寨币上跑了一下,真是手续费收割机啊。。。不到5分钟20刀没了:)

    6 years ago

    不客气,可以看下草神写的策略原理分析的文章,高频策略是需要一些支持的。

    6 years ago

    太棒了,没有梦总的解释还真的不能完全看懂,感谢大佬的耐心解释!

    6 years ago

    感谢支持~如果喜欢,帮忙分享下,嘿嘿~

    6 years ago

    黄金分割0.618 0.382是用的斐波
    梦总牛批

    6 years ago

    上学那会儿,学到这个黄金分割比例记得特别清楚,说是这样比例长宽的矩形最美~~但是不知道为什么。。。。

    6 years ago

    确实牛批啊这么详细

    6 years ago

    感谢支持!

    6 years ago

    大大的赞,这么详细的解答.感谢分享

    6 years ago

    感谢支持!

    6 years ago

    梦总牛p

    6 years ago

    感谢支持!

    6 years ago

    梦总,牛批!!
    虽然注释了,但是看起来还是非常复杂.....

    6 years ago

    其实不复杂,这次注释的比较啰嗦,逐行都尽量用最容易理解的方式描述了。通篇看下来应该理解不难。

    6 years ago

    梦总,牛批!!

    6 years ago

    感谢支持。

    6 years ago
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)