avatar of 发明者量化-小小梦 发明者量化-小小梦
tập trung vào tin nhắn riêng tư
4
tập trung vào
1271
Người theo dõi

Phân tích Chiến lược thu hoạch tỏi tây (1)

Được tạo ra trong: 2020-11-12 22:11:32, cập nhật trên: 2024-12-06 22:20:54
comments   26
hits   10642

Phân tích Chiến lược thu hoạch tỏi tây (1)

Phân tích chiến lược thu hoạch tỏi tây

Gần đây, người phát minh ra cuộc thảo luận nhóm WeChat định lượngprint moneyCuộc thảo luận về robot diễn ra rất sôi nổi và một chiến lược rất cũ đã quay trở lại tầm nhìn của các nhà phân tích định lượng:Máy gặt tỏi tâyprint moneyNguyên lý giao dịch của robot dựa trên chiến lược thu hoạch tỏi tây. Tôi tự trách mình vì đã không hiểu rõ chiến lược thu hoạch tỏi tây vào thời điểm đó. Vì vậy, tôi đã xem xét lại cẩn thận chiến lược ban đầu và cũng xem xét lại phiên bản được chuyển sang Inventor Quant.Máy thu hoạch tỏi tây OKCoin cấy ghép。 Chúng ta hãy lấy phiên bản cấy ghép của chiến lược thu hoạch tỏi tây của Nền tảng định lượng Inventor để phân tích chiến lược và khám phá những ý tưởng đằng sau chiến lược. Để người dùng nền tảng có thể tìm hiểu ý tưởng chiến lược này. Trong bài viết này, chúng ta sẽ phân tích sâu hơn dưới góc độ tư duy chiến lược, ý định, v.v. và cố gắng giảm bớt nội dung nhàm chán liên quan đến lập trình.

[[Cấy ghép OKCoin Leek Harvester] Mã nguồn chiến lược:

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)
    }
}

Tổng quan về chiến lược

Nhìn chung, khi bạn có một chiến lược để học, khi đọc nó, trước tiên bạn nên xem xét cấu trúc chương trình tổng thể. Chiến lược này không có nhiều mã, chỉ có chưa đến 200 dòng mã, rất súc tích, mức độ phục hồi về chiến lược ban đầu rất cao, về cơ bản là giống nhau. Mã chính sách chạy từmain()Chức năng bắt đầu thực thi và toàn bộ mã chiến lược đượcmain(), là mộtLeeksReaper()Chức năng,LeeksReaper()Hàm này cũng dễ hiểu. Hàm này có thể được hiểu là hàm tạo của mô-đun logic chiến lược thu hoạch tỏi tây (một đối tượng). Nói một cách đơn giảnLeeksReaper()Nó có trách nhiệm xây dựng logic giao dịch thu hoạch tỏi tây.

Từ khóa: Phân tích Chiến lược thu hoạch tỏi tây (1) Phân tích Chiến lược thu hoạch tỏi tây (1)

  • Chiến lượcmainDòng đầu tiên của hàm: var reaper = LeeksReaper(), mã khai báo một biến cục bộreaperSau đó gọi hàm LeeksReaper() để xây dựng một đối tượng logic chiến lược và gán nó choreaper

  • Chiến lượcmainChức năng tiếp theo là:

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

Nhập mộtwhileVòng lặp chết, thực thi vô tậnreaperChức năng xử lý đối tượngpoll()poll()Chức năng này là logic chính của chiến lược giao dịch và toàn bộ chương trình chiến lược bắt đầu thực hiện logic giao dịch liên tục. Đối vớiSleep(TickInterval)Dòng này dễ hiểu. Nó dùng để kiểm soát thời gian tạm dừng sau mỗi lần thực hiện logic giao dịch tổng thể, nhằm kiểm soát tần suất luân chuyển của logic giao dịch.

Hồ sơLeeksReaper()Người xây dựng

hãy nhìn xemLeeksReaper()Cách các hàm xây dựng đối tượng logic chiến lược.

LeeksReaper()Khi bắt đầu hàm, một đối tượng rỗng được khai báo.var self = {},hiện hữuLeeksReaper()Trong quá trình thực hiện hàm, một số phương thức và thuộc tính sẽ được dần dần thêm vào đối tượng rỗng này, cuối cùng việc xây dựng đối tượng này sẽ hoàn tất và cuối cùng đối tượng này sẽ được trả về (tức làmain()Bên trong chức năngvar reaper = LeeksReaper()Trong bước này, đối tượng trả về được gán choreaper)。

Đưa choselfThêm thuộc tính vào một đối tượng

Tiếp theo choselfRất nhiều thuộc tính đã được thêm vào. Tôi sẽ mô tả từng thuộc tính bên dưới để bạn có thể nhanh chóng hiểu được mục đích và ý định của các thuộc tính và biến này, cũng như dễ dàng hiểu được chiến lược, để tránh bị nhầm lẫn khi bạn nhìn thấy đống mã này.

    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          # 记录当前收益数值

Đưa choselfPhương pháp thêm đối tượng

Sau khi thêm các thuộc tính này vào bản thân, hãy bắt đầuselfViệc thêm phương thức vào một đối tượng cho phép đối tượng đó thực hiện một số công việc và có một số chức năng.

Chức năng đầu tiên được thêm vào:

    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)

    }

updateTradesMục đích của chức năng này là thu thập dữ liệu giao dịch thị trường mới nhất, thực hiện một số tính toán dựa trên dữ liệu và ghi lại chúng để sử dụng trong logic tiếp theo của chiến lược. Tôi đã viết các bình luận từng dòng trực tiếp trong mã ở trên. vì_.reduceHọc sinh không có nền tảng lập trình có thể bị bối rối. Sau đây là lời giải thích ngắn gọn._.reduceĐúngUnderscore.jsCác chức năng của thư viện này được hỗ trợ bởi chiến lược FMZJS, do đó rất thuận tiện khi sử dụng các phép tính lặp.Liên kết dữ liệu Underscore.js

Ý nghĩa cũng rất đơn giản, ví dụ:

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
}

Đó là mảng[1, 2, 3, 4]Cộng tất cả các số trong . Quay lại chiến lược của chúng tôi,tradesGiá trị khối lượng giao dịch của mỗi bản ghi giao dịch trong mảng được cộng lại. Nhận hồ sơ giao dịch mới nhất và tổng khối lượng giao dịch.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...)Xin vui lòng cho phép tôi sử dụng...Thay vì một đống mã đó. Không khó để thấy ở đây rằngself.volPhép tính này cũng là phép tính trung bình có trọng số. Nghĩa là tổng khối lượng giao dịch mới nhất chiếm 30% trọng số, khối lượng giao dịch thu được theo phép tính trọng số trước đó chiếm 70%. Tỷ lệ này được tác giả chiến lược thiết lập một cách nhân tạo và có thể liên quan đến việc quan sát các mô hình thị trường. Còn câu hỏi của bạn, nếu giao diện lấy dữ liệu giao dịch mới nhất trả về dữ liệu cũ trùng lặp cho tôi thì sao? Khi đó dữ liệu tôi nhận được sẽ sai, vậy có ích gì khi sử dụng nó không? Đừng lo lắng, vấn đề này đã được tính đến khi thiết kế chiến lược, do đó nó được đưa vào mã.

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

Phán quyết này. Có thể đánh giá dựa trên ID giao dịch trong hồ sơ giao dịch. Tích lũy chỉ được kích hoạt khi ID lớn hơn ID của hồ sơ cuối cùng hoặc nếu giao diện trao đổi không cung cấp ID, nghĩa làtrade.Id == 0, sử dụng dấu thời gian trong hồ sơ giao dịch để xác định, tại thời điểm nàyself.lastTradeIdNhững gì được lưu trữ là dấu thời gian của bản ghi giao dịch chứ không phải là ID.

Chức năng bổ sung thứ hai:

    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))
    }

Đồng hồ tiếp theoupdateOrderBookĐúng như tên gọi, chức năng này được sử dụng để cập nhật sổ lệnh. Tuy nhiên, nó không chỉ đơn thuần là cập nhật sổ lệnh. Chức năng này bắt đầu gọi hàm API của FMZGetDepth()Lấy dữ liệu sổ lệnh thị trường hiện tại (bán một…bán n, mua một…mua n) và ghi lại dữ liệu sổ lệnh trongself.orderBookở giữa. Tiếp theo, nếu dữ liệu sổ lệnh chứa ít hơn 3 lệnh mua và bán, hàm này được coi là không hợp lệ và trả về trực tiếp.

Sau đó, hai dữ liệu đã được tính toán:

  • Tính giá vận đơn Giá vận đơn cũng được tính bằng cách sử dụng giá trị trung bình có trọng số. Khi tính toán lệnh mua, lệnh mua được đưa ra trọng số lớn hơn là 61,8% (0,618) và lệnh bán chiếm trọng số còn lại là 38,2% (0,382). Điều tương tự cũng được áp dụng khi tính giá bán theo vận đơn, trong đó giá bán được tính trọng số lớn hơn. Về lý do tại sao lại là 0,618, có thể là do tác giả thích tỷ lệ vàng hơn. Đối với mức tăng hoặc giảm giá nhẹ cuối cùng (0,01), mục đích là dịch chuyển nhẹ giá về phía trung tâm thị trường.

  • Cập nhật giá trung bình có trọng số của ba bậc đầu tiên của sổ lệnh theo chuỗi thời gian Tính toán trung bình có trọng số được thực hiện trên ba bậc đầu tiên của giá lệnh mua và bán trong sổ lệnh, trong đó bậc đầu tiên có trọng số là 0,7, bậc thứ hai có trọng số là 0,2 và bậc thứ ba có trọng số là 0,1 . Một số học sinh có thể nói: “Ồ, không đúng, không có 0,7, 0,2, 0,1 trong mã” Chúng ta hãy mở rộng phép tính:

  (买一 + 卖一) * 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

Ở đây bạn có thể thấy rằng giá tính toán cuối cùng thực sự phản ánh vị trí giá trung bình của ba mức trên thị trường hiện tại. Sau đó sử dụng giá đã tính toán này để cập nhậtself.pricesMảng, loại bỏ dữ liệu cũ nhất (thông quashift()chức năng), cập nhật dữ liệu mới nhất (thông quapush()Hàm, hàm shift và hàm push là các phương thức của đối tượng mảng ngôn ngữ JS. Bạn có thể kiểm tra thông tin JS để biết chi tiết). Do đó hình thànhself.pricesMảng là luồng dữ liệu được sắp xếp theo chuỗi thời gian.

À, uống nước đi, tôi dừng ở đây thôi, hẹn gặp lại lần sau~