Недавно пользователи в группе FMZ Quant WeChat обсуждали ботаprint money
Дискуссия была очень горячей, и очень старая стратегия снова вошла в видение квантов:Прибыль Уборщик- Да, конечно.
Принцип торговли ботамиprint money
Я обвиняю себя в том, что не очень хорошо понимал стратегию по уборке прибыли в то время.Портирование OKCoin Profit Harvester- Да, конечно.
В качестве примера мы рассмотрим порт-версию стратегии FMZ по сбору прибыли. Мы проанализируем эту стратегию и извлечем идеи стратегии, чтобы пользователи нашей платформы могли изучить эту стратегию.
В этой статье мы анализируем больше с уровня стратегического мышления, намерения и т. Д., Чтобы минимизировать скучный контент, связанный с программированием.
[Port OKCoin Profit 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("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)
}
Введитеwhile
бесконечная петля, непрерывно выполнять функцию обработкиreaper
объектpoll()
, иpoll()
функция является основной логикой стратегии, так что вся стратегия программы начинает непрерывно выполнять логику торговли.
Что касается линииSleep(TickInterval)
, это легко понять, который должен контролировать время паузы после каждого выполнения общей логики торговли, и цель которого - контролировать частоту вращения логики торговли.
LeeksReaper()
Давайте посмотрим, как функцияLeeksReaper()
создает объект в логике стратегии.
ВLeeksReaper()
функция начинается и объявляет нулевой объект,var self = {}
Во время исполненияLeeksReaper()
Наконец, конструкция объекта завершена, и объект возвращается (то есть,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.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.jsПоэтому очень удобно использовать его для выполнения итеративных вычислений.информационная ссылка на 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)) {
...
}
Решение может основываться на идентификаторе исполнения в записи исполнения торговли. Только когда идентификатор больше, чем идентификатор в последний раз, аккумуляция запускается. Или, если интерфейс платформы не предоставляет идентификатор, то есть,trade.Id == 0
, используйте временную метку в записи исполнения торговли, чтобы судить.self.lastTradeId
является временной меткой записи исполнения, а не идентификатором.
Вторая добавленная функция:
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 APIGetDepth()
Получить текущие данные книги заказов рынка (продать 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. Некоторые студенты могут сказать: "О, неправильно, в коде нет 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()
self.prices
является потоком данных в порядке временных рядов.
Давай закончим анализ здесь, и увидимся в следующий раз!