Phân tích chiến lược thu hoạch lợi nhuận (1)

Tác giả:Ninabadass, Tạo: 2022-04-26 11:31:51, Cập nhật:

Phân tích chiến lược thu hoạch lợi nhuận (1)

Gần đây, người dùng trong nhóm FMZ Quant WeChat đã thảo luận về bot củaprint moneyCuộc thảo luận rất nóng, và một chiến lược rất cũ đã trở lại tầm nhìn của quants:Lợi nhuận Máy thu hoạch. Nguyên tắc giao dịch bot củaprint moneyTôi tự trách mình vì không hiểu được chiến lược thu hoạch lợi nhuận quá rõ vào thời điểm đó.Porting OKCoin Profit Harvester. Lấy phiên bản chuyển đổi của chiến lược thu hoạch lợi nhuận của FMZ làm ví dụ, chúng tôi sẽ phân tích chiến lược này và đào ra các ý tưởng của chiến lược, để người dùng nền tảng của chúng tôi có thể tìm hiểu ý tưởng chiến lược này. Trong bài viết này, chúng tôi phân tích nhiều hơn từ mức độ suy nghĩ chiến lược, ý định, vv, để giảm thiểu nội dung nhàm chán liên quan đến lập trình.

[Port OKCoin Lợi nhuận thu hoạch] Chiến lược mã nguồn:

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("start to balance", 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("start to balance", 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

Nói chung, khi bạn có một chiến lược để học, khi đọc, trước tiên hãy nhìn vào cấu trúc chương trình tổng thể. Mã chiến lược không dài, ít hơn 200 dòng; nó có thể được nói là rất đơn giản, và có sự phục hồi cao cho chiến lược phiên bản ban đầu, về cơ bản giống như phiên bản này. Khi mã chiến lược đang chạy, nó bắt đầu từmain()Toàn bộ mã chiến lược, ngoại trừmain(), chỉ có một hàm tên làLeeksReaper().LeeksReaper()chức năng cũng rất dễ hiểu. chức năng này có thể được hiểu như là người xây dựng của mô-đun logic chiến lược thu hoạch lợi nhuận (một đối tượng).LeeksReaper()chịu trách nhiệm xây dựng logic giao dịch của một người thu hoạch lợi nhuận.

  • Dòng đầu tiên củamainvai trò trong chiến lược:var reaper = LeeksReaper(); mã khai báo một biến địa phươngreaper, và sau đó gọi hàm LeeksReaper() để xây dựng một đối tượng logic chiến lược và gán nó choreaper.

  • Phần sau củamainchức năng:

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

    Nhập mộtwhilevòng lặp vô hạn, liên tục thực hiện chức năng xử lý củareaperđối tượngpoll(), vàpoll()chức năng là logic chính của chiến lược, vì vậy toàn bộ chương trình chiến lược bắt đầu liên tục thực hiện logic giao dịch.

    Còn về dòngSleep(TickInterval), nó dễ hiểu, đó là để kiểm soát thời gian tạm dừng sau mỗi lần thực hiện tổng hợp logic giao dịch, và mục đích của nó là để kiểm soát tần suất xoay của logic giao dịch.

Phân tích Đấng Xây dựngLeeksReaper()

Chúng ta hãy xem hàmLeeksReaper()xây dựng một đối tượng trong logic chiến lược.

CácLeeksReaper()hàm bắt đầu và tuyên bố một đối tượng null,var self = {}Trong quá trình thi hànhLeeksReaper()Cuối cùng, việc xây dựng đối tượng được hoàn thành, và đối tượng được trả về (tức là,var reaper = LeeksReaper()trongmain()chức năng, và đối tượng được trả về được gán choreaper).

Thêm thuộc tính vào đối tượngself

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

    self.numTick = 0         # it is used to record the number of times that the trading is not triggered when the poll function is called. When placing an order is triggered and the order logic is executed, reset self.numTick to 0
    self.lastTradeId = 0     # the trading record ID of the order that has been executed in the trading market; this variable records the current latest execution record ID in the market
    self.vol = 0             # after the weighted average calculation, the trading volume reference of the market at each inspection (the market data is obtained once per time of the loop, which can be understood as the inspection of the market once)
    self.askPrice = 0        # ask price of delivery order, which can be understood as the price of the pending sell order calculated by the strategy 
    self.bidPrice = 0        # bid price of delivery order
    self.orderBook = {Asks:[], Bids:[]}    # record the currently obtained order book data, that is, depth data (sell 1...sell n, buy 1...buy n)
    self.prices = []                       # an array that records the price in the time series after the weighted average calculation of the first three levels in the order book. Simply put, it is the weighted average price of the first three levels of the order book obtained by storing each time, and put them in an array as reference of the subsequent strategic trading signals. Therefore, the variable name is "prices", plural, which means a set of prices
    self.tradeOrderId = 0    # record the order ID after currently lading and ordering 
    self.p = 0.5             # position proportion; when the currency value is exactly half of the total asset value, the value of it is 0.5, which means the balanced state 
    self.account = null      # record the account asset data, which will be returned by the function GetAccount()
    self.preCalc = 0         # record the timestamp when calculating the return of the latest time, in milliseconds, which is used to control the frequency of triggering execution of the return calculation code
    self.preNet = 0          # record the current return value  

Thêm phương thức vào đối tượngself

Sau khi thêm các thuộc tính này vào self, bắt đầu thêm các phương thức vàoselfđối tượng, để đối tượng này có thể thực hiện một số hoạt động và có một số chức năng.

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

    self.updateTrades = function() {
        var trades = _C(exchange.GetTrades)  # call the encapsulated interface "GetTrades" of FMZ, to obtain the currently latest execution data in the market
        if (self.prices.length == 0) {       # when self.prices.length == 0, you need to fill values in the array of self.prices, which is only triggered when the strategy is started 
            while (trades.length == 0) {     # if there is no latest execution record in the market, the while loop will run infinitely, until there is new execution data; the, update the variable "trades"
                trades = trades.concat(_C(exchange.GetTrades))   # "concat" is a method of JS array type, which is used to match two arrays; here we use it to match the array "trades" and the array returned by "_C(exchange.GetTrades)" into one array 
            }
            for (var i = 0; i < 15; i++) {   # fill in values for "self.prices"; fill 15 latest execution prices 
                self.prices[i] = trades[trades.length - 1].Price
            }
        }
        self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) {  # _.reduce function iteratively calculates the accumulated execution volume of the latest execution record 
            // 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)

    }

Chức năng củaupdateTradeslà để có được dữ liệu thực hiện mới nhất trên thị trường, và để thực hiện một số tính toán và ghi lại theo dữ liệu, và cung cấp các kết quả để sử dụng trong logic chiến lược tiếp theo. Tôi đã trực tiếp viết các bình luận dòng theo dòng trong mã trên. Học sinh có thể không có nền tảng lập trình sẽ bị nhầm lẫn về_.reduce, vì vậy đây là một giới thiệu ngắn gọn._.reducelà một hàm củaUnderscore.jsVì vậy, nó rất thuận tiện để sử dụng nó để thực hiện các tính toán lặp lại.liên kết thông tin của Underscore.js

Rất dễ hiểu, 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 is 10
}

Điều này là cộng lên mỗi số trong mảng, cụ thể là[1, 2, 3, 4]Trở lại với chiến lược của chúng tôi, nó là để cộng lên giá trị khối lượng của mỗi dữ liệu ghi thực thi trongtradescho một mảng, kết quả là tổng của khối lượng thực thi gần đây nhất.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...), cho phép tôi thay thế đoạn mã đó bằng...Không khó để thấy ở đây rằng tính toán củaself.volcũng là một mức trung bình trọng số. nghĩa là tổng khối lượng thực thi mới tạo ra chiếm 30% và khối lượng thực thi được tính toán trước đó chiếm 70%. Tỷ lệ này được thiết lập nhân tạo bởi nhà phát triển chiến lược, có thể liên quan đến việc quan sát các quy tắc thị trường.

Đối với câu hỏi của bạn tôi nên làm gì nếu giao diện để lấy dữ liệu thực thi mới nhất trả lại cho tôi các dữ liệu cũ trùng lặp? sau đó, dữ liệu thu được là tất cả sai, vì vậy có bất kỳ ý nghĩa nào để sử dụng? "

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

Phán quyết có thể dựa trên ID thực thi trong hồ sơ thực thi giao dịch. Chỉ khi ID lớn hơn ID lần trước, tích lũy được kích hoạt. Hoặc, nếu giao diện nền tảng không cung cấp ID, nghĩa là,trade.Id == 0, sử dụng dấu thời gian trong ghi chép thực thi giao dịch để đánh giá.self.lastTradeIdlà dấu thời gian của bản ghi thực thi, 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))
    }

Tiếp theo, hãy xemupdateOrderBookfunction. Từ ý nghĩa của tên hàm, có thể thấy rằng hàm là để cập nhật sổ lệnh. Tuy nhiên, nó không chỉ cập nhật sổ lệnh. Chức năng, đầu tiên gọi hàm FMZ APIGetDepth()để thu thập dữ liệu sổ lệnh thị trường hiện tại (bán 1...bán n, mua 1...mua n), và ghi lại dữ liệu sổ lệnh trongin self.orderBookSau đó, nó đánh giá xem mức độ dữ liệu sổ lệnh cho các lệnh mua và bán ít hơn 3 mức; nếu vậy, nó được đánh giá là một hàm không hợp lệ và trả về trực tiếp.

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

  • Tính toán giá đơn đặt hàng Khi tính toán giá thầu của lệnh mua, cho giá mua 1 trọng lượng lớn hơn, cụ thể là 61.8% (0.618), và giá bán 1 chiếm trọng lượng còn lại là 38.2% (0.382). Điều tương tự cũng đúng khi tính toán giá thầu của lệnh giao hàng, cho giá bán 1 trọng lượng nhiều hơn. Đối với lý do tại sao nó là 0.618, có thể là nhà phát triển thích tỷ lệ vàng. Đối với một chút giá (0.01) được thêm hoặc trừ vào cuối, nó là để bù đắp một chút xa hơn đến trung tâm của thị trường.

  • Cập nhật giá trung bình trọng số của ba mức đầu tiên của sổ đơn đặt hàng trên chuỗi thời gian Đối với ba cấp đầu tiên của lệnh mua và bán trong sổ đơn đặt hàng, tính toán trung bình trọng số được thực hiện. Trọng lượng của cấp đầu tiên là 0,7, trọng lượng của cấp thứ hai là 0,2, và trọng lượng của cấp thứ ba là 0,1. Một số sinh viên có thể nói: Oh, sai, không có 0,7, 0,2, hoặc 0,1 trong mã!

    Hãy xem tính toán mở rộng:

    (Buy 1 + Sell 1) * 0.35 + (Buy 2 + Sell 2) * 0.1 + (Buy 3 + Sell 3) * 0.05
    ->
    (Buy 1 + Sell 1) / 2 * 2 * 0.35 + (Buy 2 + Sell 2) / 2 * 2 * 0.1 + (Buy 3 + Sell 3) / 2 * 2 * 0.05
    ->
    (Buy 1 + Sell 1) / 2 * 0.7 + (Buy 2 + Sell 2) / 2 * 0.2 + (Buy 3 + Sell 3) / 2 * 0.1
    ->
    average price of the first level * 0.7 + average price of the second level * 0.2 + average price of the third level * 0.1
    

    Có thể thấy ở đây rằng giá cuối cùng được tính toán thực sự phản ánh vị trí giá trung bình của các cấp thứ ba 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, đá ra dữ liệu cũ nhất (bằngshift()(bằng cách cập nhật các dữ liệu mới nhất).push()hàm; shiftpush hàm là tất cả các phương thức của các đối tượng mảng JS, bạn có thể tìm kiếm tài liệu liên quan đến JS để biết thêm chi tiết).self.priceslà một luồng dữ liệu theo thứ tự chuỗi thời gian.

    Hãy nghỉ ngơi, chúng ta hãy kết thúc phân tích ở đây, và gặp lại lần sau!


Thêm nữa