이윤 수확자 전략 분석 (1)

저자:니나바다스, 창작: 2022-04-26 11:31:51, 업데이트:

이윤 수확자 전략 분석 (1)

최근 FMZ Quant WeChat 그룹의 사용자들은print money토론은 매우 뜨겁고, 매우 오래된 전략은 퀀트의 비전에 다시 들어왔다:수익 수확기- 그래요 보트 거래 원칙print money이윤수집 전략에 기반을 두고 있습니다. 저는 그 당시 이윤수집 전략에 대해 잘 이해하지 못했기 때문에 스스로를 비난합니다. 그래서 저는 원래 전략을 다시 진지하게 읽었습니다. 그리고 또한 포트된 버전을 읽었습니다.OKCoin 수익 하버스터의 포팅- 그래요 예를 들어 FMZ의 수익 수확 전략의 포트 버전을 들자면, 우리는 이 전략을 분석하고 전략의 아이디어를 발굴하여 플랫폼 사용자들이 이 전략 아이디어를 배울 수 있도록 할 것입니다. 이 기사에서는 프로그래밍과 관련된 지루한 콘텐츠를 최소화하기 위해 전략적 사고, 의도 등 수준에서 더 많은 것을 분석합니다.

[포트 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("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)
    }
}

전략의 개요

일반적으로, 당신이 배우기 위해 전략을 얻을 때, 읽기 때, 먼저 전체 프로그램 구조를 참조하십시오. 전략 코드는 길지 않습니다, 200 줄 미만; 그것은 매우 단순화되었다고 말할 수 있습니다, 그리고 기본적으로이와 같은 원본 버전의 전략에 대한 높은 복원을 가지고 있습니다. 전략 코드가 실행되는 경우,main()함수입니다. 모든 전략 코드,main(), 함수 이름만 있습니다.LeeksReaper().LeeksReaper()이 함수는 이윤 수확자 전략 논리 모듈 (물질) 의 구성 요소로 이해할 수 있습니다. 간단히 말해서,LeeksReaper()이윤 추출자의 거래 논리를 구성하는 책임이 있습니다.

  • 첫 번째 줄은main전략의 역할:var reaper = LeeksReaper(); 코드는 로컬 변수를 선언합니다.reaper, 그리고 나서 LeeksReaper() 함수를 호출하여 전략 논리 객체를 구성하고reaper.

  • 다음 부분에서main기능:

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

    a를 입력합니다while무한 루프, 지속적으로 처리 기능을 실행reaper물체poll(), 그리고poll()이 함수는 전략의 주요 논리이기 때문에 전체 전략 프로그램은 지속적으로 거래 논리를 실행하기 시작합니다.

    그리고 그 선에 대해Sleep(TickInterval), 그것은 이해하기 쉽다, 전체 거래 논리의 실행 후 휴식 시간을 제어하는 것입니다, 그리고 그것의 목적은 거래 논리의 회전 주파수를 제어하는 것입니다.

건축자 를 분석 하라LeeksReaper()

이 함수가 어떻게 작용하는지 봅시다LeeksReaper()전략 논리에서 객체를 구성합니다.

LeeksReaper()함수는 null 객체를 시작하고 선언합니다.var self = {}이 법 집행 중LeeksReaper()함수, 이 null 객체는 몇 가지 방법과 속성을 추가함으로써 점차 수정됩니다. 마지막으로, 객체의 구축이 완료되고 객체가 반환됩니다 (즉,var reaper = LeeksReaper()main()함수, 반환된 객체는reaper).

객체에 속성을 추가합니다self

다음으로, 저는 많은 속성을 추가했습니다self이 속성과 변수의 목적과 의도를 빠르게 이해할 수 있습니다. 이는 전략을 더 쉽게 이해할 수 있도록 하고, 코드 덩어리를 볼 때 혼란을 피할 수 있습니다.

    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  

객체에 메서드를 추가self

이 속성을 self에 추가한 후, 메소드를 추가하기 시작self이 객체가 어떤 연산을 수행하고 어떤 기능을 가질 수 있도록

첫 번째 추가 함수:

    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)

    }

그 역할은updateTrades시장에서 최신 실행 데이터를 얻고, 데이터에 따라 몇 가지 계산을하고 기록하고, 후속 전략 논리에 사용할 결과를 제공하는 것입니다. 저는 위의 코드에 직접 댓글을 줄 줄로 썼습니다. 프로그래밍 기초가 없는 학생들은_.reduce, 그래서 다음은 간단한 소개입니다._.reduce함수입니다.Underscore.jsFMZJS 전략에 의해 지원되는 라이브러리입니다. 따라서, 반복 계산을 하기 위해 그것을 사용하는 것이 매우 편리합니다.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 is 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입니다.

두 번째 추가 기능:

    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을 사)in self.orderBook다음으로, 그것은 구매 주문과 판매 주문에 대한 주문 책 데이터의 레벨이 3 레벨 이하인지 판단합니다. 만약 그렇다면, 그것은 유효하지 않은 함수라고 판단되며 직접 반환됩니다.

그 후 두 가지 데이터 계산이 수행되었습니다.

  • 배송 주문 가격을 계산 배달 가격의 계산은 또한 가중 평균 계산에 기초한다. 구매 주문의 입찰 가격을 계산할 때, 구매 1 가격에 더 큰 무게를 부여, 즉 61.8% (0.618), 판매 1 가격은 38.2% (0.382) 의 나머지 무게를 차지한다. 배달 주문의 요청 가격을 계산할 때도 마찬가지이며, 판매 1 가격에 더 많은 무게를 부여한다. 왜 0.618인지에 관해서는 개발자가 황금 비율을 선호하는 것일 수 있다. 마지막에 약간의 가격 (0.01) 을 추가하거나 빼는 것에 관해서는 시장의 중심으로 조금 더 치우쳐야 한다.

  • 시간 계열에 대한 주문 포크의 첫 세 레벨의 가중 평균 가격을 업데이트 오더북의 첫 세 레벨의 구매 주문과 판매 주문에 대해 가중 평균 계산을 수행합니다. 첫 번째 레벨의 무게는 0.7, 두 번째 레벨의 무게는 0.2, 세 번째 레벨의 무게는 0.1. 일부 학생들은 이렇게 말할 수 있습니다.

    확장된 계산을 보겠습니다.

    (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
    

    여기서 최종 계산된 가격은 실제로 현재의 시장에서 세 번째 레벨의 중간 가격 위치를 반영한다는 것을 볼 수 있습니다. 이 계산된 가격을 사용하여self.prices배열, 가장 오래된 데이터 (에 의해shift()가장 최신 데이터에 따라 업데이트합니다.push()함수; shiftpush 함수는 모두 JS 배열 객체의 메소드입니다. 더 자세한 내용은 JS 관련 자료를 검색할 수 있습니다.)self.prices시간 계열 순서에서의 데이터 흐름입니다.

    잠시 쉬세요, 분석을 끝낼게요, 다음 번 보자!


더 많은