Untuk waktu yang lama, berjangka dan hedge spot umumnya dirancang untuk mendeteksi spread harga, dan mengambil pesanan untuk hedge ketika spread harga terpenuhi.
Dalam pasar yang berbeda dari jenis subjek yang sama atau yang sama, peluang untuk lindung nilai muncul ketika ada perbedaan besar antara pesanan beli dan jual antara kedua pasar. Secara umum, kita akan mengambil pesanan yang tertunda yang memenuhi spread harga dan kemudian memegang posisi lindung nilai. Oleh karena itu, ada dua tujuan lindung nilai. Yang pertama adalah untuk lindung nilai terhadap posisi order, dan yang kedua adalah untuk memastikan bahwa harga spread antara pembelian dan penjualan memenuhi harapan maksimum kita. Keuntungan dari perdagangan order tertunda dalam hal ini adalah bahwa biaya lebih rendah. Kelemahannya adalah tidak mudah untuk mengeksekusi pesanan, dan lebih mudah untuk mengeksekusi pesanan posisi tunggal.
Kemudian, kita mendesain ide trading yang menunggu order dibeli di buku order beli pasar A, dan menunggu order terjual di buku order jual pasar B, dan kemudian mendeteksi akun kami menunggu order, dan melanjutkan ke langkah berikutnya untuk eksekusi order menunggu yang terdeteksi. Misalnya, ketika perubahan order menunggu terdeteksi, segera menyeimbangkan posisi lindung nilai saat ini futures dan spot. Untuk kelebihan posisi futures dan spot, buka beli atau tutup. Menurut peningkatan posisi lindung nilai, sesuaikan jarak antara order menunggu berikutnya di pasar dan tingkat pertama pasar, secara bertahap lindung nilai untuk mendapatkan spread terbesar.
logika lindung nilai
Catatan ditulis langsung dalam kode. Contoh ini hanya digunakan untuk referensi desain dan hanya telah diuji secara singkat pada OKEX V5 simulasi bot. Contoh ini bukan strategi lengkap, jadi silakan gunakan hanya untuk referensi.
// temporary parameters
var fuContractType = "quarter" // futures contract
var fuSymbol = "ETH_USDT" // futures trading pair
var spSymbol = "ETH_USDT" // spot trading pair
var minAmount = 0.1 // trading amount of each time, minimum trading amount, currency amount
var step = 40 // step length of spread
var buff = 5 // buffer spread
var balanceType = "open" // when the single-position execution is balanced, open buy or close
var depthManager = function(fuEx, spEx, fuCt, fuSymbol, spSymbol) {
var self = {}
self.fuExDepth = null
self.spExDepth = null
self.plusPrice = null
self.minusPrice = null
self.update = function() {
spEx.SetCurrency(spSymbol)
if (!IsVirtual()) {
fuEx.SetCurrency(fuSymbol)
}
fuEx.SetContractType(fuCt)
var fuRoutine = fuEx.Go("GetDepth")
var spRoutine = spEx.Go("GetDepth")
var fuDepth = fuRoutine.wait()
var spDepth = spRoutine.wait()
if (!fuDepth || !spDepth) {
return false
}
self.fuExDepth = fuDepth
self.spExDepth = spDepth
if (fuDepth.Bids.length == 0 || fuDepth.Asks.length == 0 || spDepth.Bids.length == 0 || spDepth.Asks.length == 0) {
return false
}
self.plusPrice = fuDepth.Bids[0].Price - spDepth.Asks[0].Price // futures Bid - spot Ask
self.minusPrice = fuDepth.Asks[0].Price - spDepth.Bids[0].Price // futures Ask - spot Bid
return true
}
self.getData = function() {
return {
"fuExDepth" : self.fuExDepth,
"spExDepth" : self.spExDepth,
"plusPrice" : self.plusPrice,
"minusPrice" : self.minusPrice
}
}
return self
}
var positionManager = function(fuEx, spEx, fuCt, fuSymbol, spSymbol, step, buffDiff, balanceType, initSpAcc) {
var self = {}
self.balanceType = balanceType
self.depth = null
self.level = 1
self.lastUpdateTs = 0
self.fuPos = []
self.spPos = []
self.initSpAcc = initSpAcc
self.spAcc = null
self.hedgePos = null
self.hedgePosPrice = 0
self.minAmount = 0.01
self.offset = ["", 0]
self.update = function() {
spEx.SetCurrency(spSymbol)
if (!IsVirtual()) {
fuEx.SetCurrency(fuSymbol)
}
fuEx.SetContractType(fuCt)
self.offset = ["", 0]
var fuRoutine = fuEx.Go("GetPosition")
var spRoutine = spEx.Go("GetAccount")
var fuPos = fuRoutine.wait()
var spAcc = spRoutine.wait()
if (!fuPos || !spAcc) {
return false
}
self.fuPos = fuPos
self.spAcc = spAcc
if (!self.initSpAcc) {
return false
}
self.spPos = (spAcc.Stocks + spAcc.FrozenStocks) - (self.initSpAcc.Stocks + self.initSpAcc.FrozenStocks) // the current one minus the initial one; if the result is a positive number, make long
// detect fuPos
if (fuPos.length > 1) {
return false
}
fuPosAmount = fuPos.length == 0 ? 0 : (fuPos[0].Type == PD_LONG ? fuPos[0].Amount : -fuPos[0].Amount)
if ((fuPosAmount > 0 && self.spPos > 0) || (fuPosAmount < 0 && self.spPos < 0)) {
return false
}
fuPosAmount = self.piece2Coin(fuPosAmount)
self.hedgePos = (fuPosAmount == 0 || self.spPos == 0) ? 0 : (fuPosAmount < 0 && self.spPos > 0 ? Math.min(Math.abs(fuPosAmount), Math.abs(self.spPos)) : -Math.min(Math.abs(fuPosAmount), Math.abs(self.spPos)))
var diffBalance = (spAcc.Balance + spAcc.FrozenBalance) - (self.initSpAcc.Balance + self.initSpAcc.FrozenBalance)
if (self.hedgePos == 0) {
self.hedgePosPrice = 0
} else {
self.hedgePosPrice = fuPos[0].Price - (Math.abs(diffBalance) / Math.abs(self.spPos))
}
self.offset[1] = fuPosAmount + self.spPos // positive number represents long position overflow; negative number represents short position overflow
if (fuPosAmount > 0 && self.spPos < 0) { // reverse arbitrage
self.offset[0] = "minus"
} else if (fuPosAmount < 0 && self.spPos > 0) {
self.offset[0] = "plus"
} else if (fuPosAmount == 0 && self.spPos < 0) {
self.offset[0] = "minus"
} else if (fuPosAmount > 0 && self.spPos == 0) {
self.offset[0] = "minus"
} else if (fuPosAmount == 0 && self.spPos > 0) {
self.offset[0] = "plus"
} else if (fuPosAmount < 0 && self.spPos == 0) {
self.offset[0] = "plus"
}
return true
}
self.getData = function() {
return {
"fuPos" : self.fuPos,
"spPos" : self.spPos,
"initSpAcc" : self.initSpAcc,
"spAcc" : self.spAcc,
"hedgePos" : self.hedgePos,
"hedgePosPrice" : self.hedgePosPrice,
}
}
self.keepBalance = function(depth) {
var fuDepth = depth.fuExDepth
var spDepth = depth.spExDepth
if (self.offset[0] == "plus") {
if (self.offset[1] >= self.minAmount) {
if (self.balanceType == "close") {
// the spot long position amount is large; close spot long positions
spEx.Sell(-1, self.offset[1])
} else if (self.balanceType == "open") {
// the spot long position amount is large; open futures short positions
fuEx.SetDirection("sell")
fuEx.Sell(-1, self.coin2Piece(Math.abs(self.offset[1])))
}
} else if (self.offset[1] <= -self.minAmount) {
if (self.balanceType == "close") {
// the futures short position amount is large; close futures short positions
fuEx.SetDirection("closesell")
fuEx.Buy(-1, self.coin2Piece(Math.abs(self.offset[1])))
} else if (self.balanceType == "open") {
// the futures short position amount is large; open spot long positions
spEx.Buy(-1, spDepth.Asks[0].Price * Math.abs(self.offset[1]))
}
}
return false
} else if (self.offset[0] == "minus") {
if (self.offset[1] >= self.minAmount) {
if (self.balanceType == "close") {
// the futures long position amount is large; close futures long positions
fuEx.SetDirection("closebuy")
fuEx.Sell(-1, self.coin2Piece(self.offset[1]))
} else if (self.balanceType == "open") {
// the futures long position amount is large; open spot short positions
spEx.Sell(-1, self.offset[1])
}
} else if (self.offset[1] <= -self.minAmount) {
if (self.balanceType == "close") {
// the spot short position amount is large; close spot short positions
spEx.Buy(-1, spDepth.Asks[0].Price * Math.abs(self.offset[1]))
} else if (self.balanceType == "open") {
// the spot short position amount is large; open futures long positions
fuEx.SetDirection("buy")
fuEx.Buy(-1, self.coin2Piece(Math.abs(self.offset[1])))
}
}
return false
}
return true
}
self.process = function(depthManager) {
var ts = new Date().getTime()
var depth = depthManager.getData()
var orders = self.getOrders()
if (!orders) {
return
}
self.depth = depth
var fuOrders = orders[0]
var spOrders = orders[1]
if (fuOrders.length == 0 && spOrders.length == 0) {
// reset level
if (self.hedgePos == 0) {
self.level = 1
} else {
self.level = Math.max(1, _N(self.hedgePos / self.minAmount, 0))
}
// limit the maximum position amount
if (Math.abs(self.hedgePos) > 1) {
return
}
// pend orders
var fuDepth = depth.fuExDepth
var spDepth = depth.spExDepth
self.update()
if (self.hedgePos >= 0 && fuDepth.Bids[0].Price - spDepth.Asks[0].Price > 0) { // positive arbitrage
var distance = (step * self.level - (fuDepth.Asks[0].Price - spDepth.Bids[0].Price)) / 2
fuEx.SetDirection("sell")
fuEx.Sell(fuDepth.Asks[0].Price + distance, self.coin2Piece(self.minAmount), fuDepth.Asks[0].Price, "pending order spread:", fuDepth.Asks[0].Price + distance - (spDepth.Bids[0].Price - distance))
spEx.Buy(spDepth.Bids[0].Price - distance, self.minAmount, spDepth.Bids[0].Price)
} else if (self.hedgePos <= 0 && spDepth.Bids[0].Price - fuDepth.Asks[0].Price > 0) { // reverse arbitrage
var distance = (step * self.level - (spDepth.Asks[0].Price - fuDepth.Bids[0].Price)) / 2
fuEx.SetDirection("buy")
fuEx.Buy(fuDepth.Bids[0].Price - distance, self.coin2Piece(self.minAmount), fuDepth.Bids[0].Price, "pending order spread:", spDepth.Asks[0].Price + distance - (fuDepth.Bids[0].Price - distance))
spEx.Sell(spDepth.Asks[0].Price + distance, self.minAmount, spDepth.Asks[0].Price)
}
} else if (fuOrders.length == 1 && spOrders.length == 1) {
var fuDepth = depth.fuExDepth
var spDepth = depth.spExDepth
// judge location
var isCancelAll = false
if (self.hedgePos >= 0 && fuDepth.Bids[0].Price - spDepth.Asks[0].Price > 0) { // positive arbitrage
var distance = (step * self.level - (fuDepth.Asks[0].Price - spDepth.Bids[0].Price)) / 2
if (Math.abs(fuOrders[0].Price - (fuDepth.Asks[0].Price + distance)) > buffDiff || Math.abs(spOrders[0].Price - (spDepth.Bids[0].Price - distance)) > buffDiff) {
isCancelAll = true
}
} else if (self.hedgePos <= 0 && spDepth.Bids[0].Price - fuDepth.Asks[0].Price > 0) { // reverse arbitrage
var distance = (step * self.level - (spDepth.Asks[0].Price - fuDepth.Bids[0].Price)) / 2
if (Math.abs(spOrders[0].Price - (spDepth.Asks[0].Price + distance)) > buffDiff || Math.abs(fuOrders[0].Price - (fuDepth.Bids[0].Price - distance)) > buffDiff) {
isCancelAll = true
}
} else {
isCancelAll = true
}
if (isCancelAll) {
self.cancelAll(fuEx, fuOrders)
self.cancelAll(spEx, spOrders)
self.lastUpdateTs = 0
}
} else {
self.cancelAll(fuEx, fuOrders)
self.cancelAll(spEx, spOrders)
self.lastUpdateTs = 0
}
if (ts - self.lastUpdateTs > 1000 * 60 * 2) {
self.update()
self.keepBalance(depth)
self.update()
self.lastUpdateTs = ts
}
LogStatus(_D()) // the status bar can be designed to export the data and the information that need to be observed
}
self.getOrders = function() {
spEx.SetCurrency(spSymbol)
if (!IsVirtual()) {
fuEx.SetCurrency(fuSymbol)
}
fuEx.SetContractType(fuCt)
var fuRoutine = fuEx.Go("GetOrders")
var spRoutine = spEx.Go("GetOrders")
var fuOrders = fuRoutine.wait()
var spOrders = spRoutine.wait()
if (!fuOrders || !spOrders) {
return false
}
return [fuOrders, spOrders]
}
// convert currency into contract amount
self.coin2Piece = function(amount) {
if (IsVirtual()) {
if (fuEx.GetName() == "Futures_Binance") {
return amount
} else if (fuEx.GetName() == "Futures_OKCoin") {
var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
return _N(amount / (100 / price), 0)
} else {
throw "not support"
}
}
if (fuEx.GetName() == "Futures_OKCoin") {
if (fuEx.GetQuoteCurrency() == "USDT") {
return _N(amount * 10, 0)
} else if (fuEx.GetQuoteCurrency() == "USD") {
var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
return _N(amount / (100 / price), 0)
} else {
throw "not support"
}
} else {
throw "not support"
}
}
// convert contract amount to currency
self.piece2Coin = function(amount) {
if (IsVirtual()) {
if (fuEx.GetName() == "Futures_Binance") {
return amount
} else if (fuEx.GetName() == "Futures_OKCoin") {
var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
return amount * 100 / price
} else {
throw "not support"
}
}
if (fuEx.GetName() == "Futures_OKCoin") {
if (fuEx.GetQuoteCurrency() == "USDT") {
return amount * 0.1
} else if (fuEx.GetQuoteCurrency() == "USD") {
var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
return amount * 100 / price
} else {
throw "not support"
}
} else {
throw "not support"
}
}
self.cancelAll = function(e, orders) {
var isFirst = true
while (true) {
Sleep(500)
if (orders && isFirst) {
isFirst = false
} else {
orders = e.GetOrders()
}
if (!orders) {
continue
} else {
for (var i = 0 ; i < orders.length ; i++) {
e.CancelOrder(orders[i].Id, orders[i])
}
}
if (orders.length == 0) {
break
}
}
}
self.CoverAll = function() {
// close all
// the one-click function of closing positions can be realized here
}
self.setMinAmount = function(minAmount) {
self.minAmount = minAmount
}
self.init = function() {
while(!self.spAcc) {
self.update()
Sleep(1000)
}
if (!self.initSpAcc) {
var positionManager_initSpAcc = _G("positionManager_initSpAcc")
if (!positionManager_initSpAcc) {
self.initSpAcc = self.spAcc
_G("positionManager_initSpAcc", self.initSpAcc)
} else {
self.initSpAcc = positionManager_initSpAcc
}
} else {
_G("positionManager_initSpAcc", self.initSpAcc)
}
// print the initial information
Log("self.initSpAcc:", self.initSpAcc.Balance, self.initSpAcc.FrozenBalance, self.initSpAcc.Stocks, self.initSpAcc.FrozenStocks)
}
self.init()
return self
}
function main() {
_G(null) // vacuum the persistent data
LogReset(1) // rest logs
// use the following code to switch to OKEX simulated bot
// exchanges[0].IO("simulate", true)
// exchanges[1].IO("simulate", true)
var dm = depthManager(exchanges[0], exchanges[1], fuContractType, fuSymbol, spSymbol)
var pm = positionManager(exchanges[0], exchanges[1], fuContractType, fuSymbol, spSymbol, step, buff, balanceType)
pm.setMinAmount(minAmount)
while (true) {
if (!dm.update()) {
Sleep(3000)
continue
}
var cmd = GetCommand()
if (cmd) {
// handle interaction
Log("interactive command:", cmd)
var arr = cmd.split(":")
if (arr[0] == "") {
pm.CoverAll()
}
}
pm.process(dm)
Sleep(5000)
}
}
Dari statistik sistem backtest, akun platform futures kehilangan 0,01666 ETH, dan bursa spot menghasilkan keuntungan sebesar 842.23758 USDT. Menurut harga spot ETH sebesar 4252USDT pada akhir backtest, yaitu-0.01666 * 4252 = -70.83832000000001
Hasilnya ditambah keuntungan spot secara keseluruhan menguntungkan.
Namun, ini hanya backtest, dan masalah yang lebih rinci akan ditangani dalam bot nyata.