
最近、定量的なWeChatグループディスカッションの発明者がprint moneyロボットに関する議論は非常に白熱し、非常に古い戦略がクオンツのビジョンに再び登場しました。ネギ収穫機。
print moneyロボットの取引原理は、ネギ収穫機の戦略を参考にしています。当時、ネギ収穫機の戦略を明確に理解していなかったことを私は責めています。そこで、元の戦略をもう一度慎重に検討し、Inventor Quant に移植されたバージョンも検討しました。OKCoin ネギ収穫機の移植。
Inventor Quantitative Platform のネギ収穫機戦略の移植版を取り上げ、戦略を分析し、その背後にあるアイデアを探ってみましょう。プラットフォームユーザーがこの戦略的なアイデアを学習できるようにするためです。
この記事では、戦略的思考、意図などの観点からさらに分析し、プログラミングに関する退屈な内容を減らすように努めます。
[OKCoin ネギ収穫機を移植]戦略ソースコード:
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)
}
}
一般的に、学習する戦略を入手してそれを読むときは、まず全体的なプログラム構造を確認する必要があります。この戦略には多くのコードがなく、200行未満のコードのみで非常に簡潔であり、元の戦略への復元度が非常に高く、基本的に同じです。ポリシーコードはmain()関数の実行が開始され、戦略コード全体がmain()は、LeeksReaper()機能、LeeksReaper()この関数も理解しやすいです。この関数は、ネギ収穫戦略ロジックモジュール(オブジェクト)のコンストラクタとして理解できます。簡単に言うとLeeksReaper()ネギ収穫機のトランザクション ロジックの構築を担当します。
キーワード:

戦略main関数の最初の行:
var reaper = LeeksReaper()コードはローカル変数を宣言するreaper次に、LeeksReaper()関数を呼び出して戦略ロジックオブジェクトを構築し、それをreaper。
戦略main関数は次のとおりです。
while (true) {
reaper.poll()
Sleep(TickInterval)
}
入力してくださいwhileデッドループ、無限実行reaperオブジェクト処理機能poll(),poll()この関数は取引戦略のメインロジックであり、戦略プログラム全体が取引ロジックを継続的に実行し始めます。
に関してはSleep(TickInterval)この行は分かりやすいです。トランザクションロジック全体の実行後の一時停止時間を制御し、トランザクションロジックの回転頻度を制御するためのものです。
LeeksReaper()コンストラクタ見てくださいLeeksReaper()関数が戦略ロジック オブジェクトを構築する方法。
LeeksReaper()関数の先頭で、空のオブジェクトが宣言されます。var self = {}、存在するLeeksReaper()関数の実行中に、この空のオブジェクトにいくつかのメソッドとプロパティが徐々に追加され、最終的にこのオブジェクトの構築が完了し、最終的にこのオブジェクトが返されます(つまり、main()関数の内部var reaper = LeeksReaper()このステップでは、返されたオブジェクトはreaper)。
selfオブジェクトにプロパティを追加する次に与えるself多くのプロパティが追加されました。この大量のコードを見て混乱しないように、これらのプロパティと変数の目的と意図をすぐに理解し、戦略を簡単に理解できるように、以下で各プロパティについて説明します。
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 # 记录当前收益数值
selfオブジェクト追加メソッドこれらの属性を自分自身に追加した後、selfオブジェクトにメソッドを追加すると、オブジェクトは何らかの作業を実行したり、何らかの機能を持つことができます。
追加された最初の機能:
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)
}
updateTradesこの機能の目的は、最新の市場取引データを取得し、そのデータに基づいていくつかの計算を実行し、戦略の後続のロジックで使用するために記録することです。
上記のコードには行ごとのコメントを直接書きました。
のために_.reduceプログラミングの基礎知識がない学生は混乱するかもしれません。ここで簡単に説明します。_.reduceはいUnderscore.jsこのライブラリの機能は FMZJS 戦略でサポートされているため、反復計算を使用するのに非常に便利です。Underscore.js データリンク
意味も非常に単純です。たとえば、次のようになります。
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
}
つまり、配列[1, 2, 3, 4]の各数字を合計します。私たちの戦略に戻ると、trades配列内の各取引レコードの取引量の値が合計されます。最新の取引記録と総取引量を取得します。self.vol = 0.7 * self.vol + 0.3 * _.reduce(...)使用を許可してください...その大量のコードの代わりに。ここで分かるのは、self.volの計算も加重平均です。つまり、最新の総取引量が重みの 30% を占め、前回の重み付け計算によって得られた取引量が 70% を占めます。この比率は戦略作成者によって人為的に設定され、市場パターンの観察に関連している可能性があります。
ご質問ですが、最新の取引データを取得するためのインターフェースが重複した古いデータを返した場合はどうなりますか? その場合、取得するデータは間違ったものになりますので、それを使用する意味はありますか?心配しないでください。この問題は戦略の設計時に考慮されていたため、コードに含まれています。
if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) {
...
}
この判決。取引記録内の取引IDに基づいて判断できます。累積は、IDが最後の記録のIDより大きい場合、または交換インターフェースがIDを提供しない場合にのみトリガーされます。trade.Id == 0取引記録のタイムスタンプを使用して、この時点でself.lastTradeId保存されるのは ID ではなく、トランザクション レコードのタイムスタンプです。
2番目に追加された機能:
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))
}
次の時計updateOrderBook名前が示すように、この機能は注文書を更新するために使用されます。ただし、注文書を更新するだけではありません。関数はFMZのAPI関数の呼び出しを開始しますGetDepth()現在の市場の注文書データ(売り1…売りn、買い1…買いn)を取得し、注文書データをself.orderBook真ん中。次に、注文書データに含まれる買い注文と売り注文が 3 件未満の場合、関数は無効とみなされ、直接戻ります。
その後、次の 2 つのデータが計算されました。
船荷証券価格を計算する 船荷証券価格も加重平均を使用して計算されます。買い注文を計算する場合、買い注文に 61.8% (0.618) の大きな重みが与えられ、売り注文は残りの 38.2% (0.382) の重みを占めます。 船荷証券の販売価格を計算する場合も同様であり、販売価格がより重視されます。なぜ 0.618 なのかについては、作者が黄金比を好んでいるからかもしれません。最終的な価格のわずかな上昇または下落(0.01)については、市場の中心に向かってわずかにシフトすることです。
時系列の注文書の最初の3層の加重平均価格を更新します。 注文簿の最初の3つの層の買い注文価格と売り注文価格に対して加重平均計算が実行され、第1層の重みは0.7、第2層の重みは0.2、第3層の重みは0.1になります。 。生徒の中には、「それは違います。コードには 0.7、0.2、0.1 はありません」と言う人もいるかもしれません。 計算を拡張してみましょう:
(买一 + 卖一) * 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
ここで、最終的に計算された価格が、現在の市場における 3 つのレベルの中間価格の位置を実際に反映していることがわかります。
次に、この計算された価格を使用して更新しますself.prices配列から最も古いデータ(shift()機能)、最新のデータを更新する(push()関数、シフト、プッシュ関数は JS 言語配列オブジェクトのメソッドです。詳細については JS 情報を確認してください。こうしてself.prices配列は時系列に順序付けられたデータ ストリームです。
えーっと、水を飲んで、今日はここまでにします。また次回お会いしましょう〜