Hedging strategies are very good practice strategies for beginners in strategy design. This article implements a simple but live cryptocurrency spot hedging strategy, hoping that beginners can learn some design experience.
First of all, it is clear that the strategy to be designed is a cryptocurrency spot hedging strategy. We design the simplest hedging strategy. We sell on the exchange with the higher price only between the two spot exchanges, and buy on the exchange with the lower price to take the difference. When the exchanges with higher prices are all denominated coins (because the coins with higher prices are sold), and the exchanges with lower prices are all coins (the coins with lower prices are bought), it cannot be hedged. At this time, we can only wait for the price reversal to hedge.
When hedging, the price and quantity of the order are limited by the exchange, and there is also a limit on the minimum order quantity. In addition to the minimum limit, strategy in hedging also needs to consider the maximum order volume at one time. If the order volume is too large, there will not be enough order volume. It is also necessary to consider how to convert the exchange rate if the two exchange-denominated coins are different. When hedging, the handling fee and slippage of the order taker are all transaction costs, not as long as there is a price difference can be hedged. Therefore, the hedging price difference also has a trigger value. If it is lower than a certain price difference, the hedging will lose.
Based on these considerations, the strategy needs to be designed with several parameters:
hedgeDiffPrice
, when the difference exceeds this value, the hedging operation is triggered.minHedgeAmount
, the minimum order amount (coins) that can be hedged.maxHedgeAmount
, the maximum order amount (coins) for one hedging.pricePrecisionA
, the order price precision (number of decimal places) placed by Exchange A.amountPrecisionA
, the amount precision of the order placed by Exchange A (number of decimal places).pricePrecisionB
, the order price precision (number of decimal places) placed by Exchange B.amountPrecisionB
, the amount precision of the order placed by Exchange B (number of decimal places).rateA
, the exchange rate conversion of the first added exchange object, the default is 1, not converted.rateB
, the exchange rate conversion of the second added exchange object, the default is 1, not converted.The hedging strategy needs to keep the number of coins in the two accounts unchanged (that is, not holding positions in any direction, and maintaining neutrality), so there needs to be a balance logic in the strategy to always detect the balance. When checking balance, it is unavoidable to obtain asset data from two exchanges. We need to write a function to use.
function updateAccs(arrEx) {
var ret = []
for (var i = 0 ; i < arrEx.length ; i++) {
var acc = arrEx[i].GetAccount()
if (!acc) {
return null
}
ret.push(acc)
}
return ret
}
After placing the order, if there is no completed order, we need to cancel it in time, and the order cannot be kept pending. This operation needs to be processed in both the balance module and the hedging logic, so it is also necessary to design an order full withdrawal function.
function cancelAll() {
_.each(exchanges, function(ex) {
while (true) {
var orders = _C(ex.GetOrders)
if (orders.length == 0) {
break
}
for (var i = 0 ; i < orders.length ; i++) {
ex.CancelOrder(orders[i].Id, orders[i])
Sleep(500)
}
}
})
}
When balancing the number of coins, we need to find the price accumulated to a certain number of coins in a certain depth data, so we need such a function to handle it.
function getDepthPrice(depth, side, amount) {
var arr = depth[side]
var sum = 0
var price = null
for (var i = 0 ; i < arr.length ; i++) {
var ele = arr[i]
sum += ele.Amount
if (sum >= amount) {
price = ele.Price
break
}
}
return price
}
Then we need to design and write the specific hedging order operation, which needs to be designed to place concurrent orders:
function hedge(buyEx, sellEx, price, amount) {
var buyRoutine = buyEx.Go("Buy", price, amount)
var sellRoutine = sellEx.Go("Sell", price, amount)
Sleep(500)
buyRoutine.wait()
sellRoutine.wait()
}
Finally, let’s complete the design of the balance function, which is slightly complicated.
function keepBalance(initAccs, nowAccs, depths) {
var initSumStocks = 0
var nowSumStocks = 0
_.each(initAccs, function(acc) {
initSumStocks += acc.Stocks + acc.FrozenStocks
})
_.each(nowAccs, function(acc) {
nowSumStocks += acc.Stocks + acc.FrozenStocks
})
var diff = nowSumStocks - initSumStocks
// Calculate the currency difference
if (Math.abs(diff) > minHedgeAmount && initAccs.length == nowAccs.length && nowAccs.length == depths.length) {
var index = -1
var available = []
var side = diff > 0 ? "Bids" : "Asks"
for (var i = 0 ; i < nowAccs.length ; i++) {
var price = getDepthPrice(depths[i], side, Math.abs(diff))
if (side == "Bids" && nowAccs[i].Stocks > Math.abs(diff)) {
available.push(i)
} else if (price && nowAccs[i].Balance / price > Math.abs(diff)) {
available.push(i)
}
}
for (var i = 0 ; i < available.length ; i++) {
if (index == -1) {
index = available[i]
} else {
var priceIndex = getDepthPrice(depths[index], side, Math.abs(diff))
var priceI = getDepthPrice(depths[available[i]], side, Math.abs(diff))
if (side == "Bids" && priceIndex && priceI && priceI > priceIndex) {
index = available[i]
} else if (priceIndex && priceI && priceI < priceIndex) {
index = available[i]
}
}
}
if (index == -1) {
Log("unable to balance")
} else {
// balance order
var price = getDepthPrice(depths[index], side, Math.abs(diff))
if (price) {
var tradeFunc = side == "Bids" ? exchanges[index].Sell : exchanges[index].Buy
tradeFunc(price, Math.abs(diff))
} else {
Log("invalid price", price)
}
}
return false
} else if (!(initAccs.length == nowAccs.length && nowAccs.length == depths.length)) {
Log("errors:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
return true
} else {
return true
}
}
After designing these functions according to the strategy requirements, then start to design the main function of the strategy.
On FMZ platform, the strategy is executed from the main
function. At the beginning of the main
function, we have to do some initialization work of the strategy.
Exchange object name Because many operations in the strategy have to use the exchange objects, such as getting market quotations, placing orders and so on. So it would be cumbersome to use a long name every time, the tip is to use a simple name instead, for example:
var exA = exchanges[0]
var exB = exchanges[1]
This makes it easier to write code later.
Exchange rate, precision related design
// precision, exchange rate settings
if (rateA != 1) {
// set exchange rate A
exA.SetRate(rateA)
Log("Exchange A sets the exchange rate:", rateA, "#FF0000")
}
if (rateB != 1) {
// set exchange rate B
exB.SetRate(rateB)
Log("Exchange B sets the exchange rate:", rateB, "#FF0000")
}
exA.SetPrecision(pricePrecisionA, amountPrecisionA)
exB.SetPrecision(pricePrecisionB, amountPrecisionB)
If the exchange rate parameters rateA
, rateB
are set to 1 (the default is 1), that is, rateA != 1
or rateB != 1
will not trigger, so the exchange rate conversion will not be set.
Reset all data
Sometimes it is necessary to delete all logs and clear the recorded data when the strategy starts. The you can design a strategy interface parameter isReset
, and design the reset code in the initialization part of the strategy, for example:
if (isReset) { // When isReset is true, reset the data
_G(null)
LogReset(1)
LogProfitReset()
LogVacuum()
Log("reset all data", "#FF0000")
}
Restore initial account data, update current account data
In order to judge the balance, the strategy needs to continuously record the initial account assets for comparison with the current one. The variable nowAccs
is used to record the current account data, using the function we just designed updateAccs
to get the account data of the current exchange. initAccs
is used to record the initial account status (the number of coins, the number of denominated coins, etc. on exchanges A and B). For initAccs
, use the _G()
function to restore first (the _G function will record data persistently, and can return the recorded data again, see the API documentation for details: [link](https:// www.fmz.com/api#_gk-v)), if the query does not work, use the current account information to assign value and use the _G
function to record.
Such as the following code:
var nowAccs = _C(updateAccs, exchanges)
var initAccs = _G("initAccs")
if (!initAccs) {
initAccs = nowAccs
_G("initAccs", initAccs)
}
The code in the main loop is the process of each round of execution of the strategy logic, which is executed over and over again to form the main loop of the strategy. Let’s take a look at the process of each execution of the program in the main loop.
Obtain market data and judge the validity of market data
var ts = new Date().getTime()
var depthARoutine = exA.Go("GetDepth")
var depthBRoutine = exB.Go("GetDepth")
var depthA = depthARoutine.wait()
var depthB = depthBRoutine.wait()
if (!depthA || !depthB || depthA.Asks.length == 0 || depthA.Bids.length == 0 || depthB.Asks.length == 0 || depthB.Bids.length == 0) {
Sleep(500)
continue
}
Here we can see that the concurrent function exchange.Go
of the FMZ platform is used to create concurrent objects depthARoutine
, depthBRoutine
that call the GetDepth()
interface. When these two concurrent objects are created, the GetDepth()
interface is called immediately, and both requests for depth data are sent to the exchange.
Then call the wait()
method of the depthARoutine
, depthBRoutine
objects to obtain the depth data.
After obtaining the depth data, it is necessary to check the depth data to determine its validity. In the case of abnormal data, the execution of the continue
statement is triggered to re-execute the main loop.
Use the spread value
parameter or the spread ratio
parameter?
var targetDiffPrice = hedgeDiffPrice
if (diffAsPercentage) {
targetDiffPrice = (depthA.Bids[0].Price + depthB.Asks[0].Price + depthB.Bids[0].Price + depthA.Asks[0].Price) / 4 * hedgeDiffPercentage
}
In terms of parameters, we have made such a design: the parameters of FMZ can be show or hide based on a parameter, so we can make a parameter to decide whether to use price spread
, or spread ratio
.
A parameter diffAsPercentage
has been added to the parameters of the strategy interface. The other two parameter settings to show or hide based on this parameter are:
hedgeDiffPrice@!diffAsPercentage
, which is displayed when diffAsPercentage
is false.
hedgeDiffPercentage@diffAsPercentage
, which is displayed when diffAsPercentage
is true.
After this design, we checked the diffAsPercentage
parameter, which is the hedge trigger condition based on the price difference ratio. Without the diffAsPercentage
parameter checked, the hedge is triggered by the price difference.
Determine the hedging trigger conditions
if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) { // A -> B market conditions are met
var price = (depthA.Bids[0].Price + depthB.Asks[0].Price) / 2
var amount = Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount)
if (nowAccs[0].Stocks > minHedgeAmount && nowAccs[1].Balance / price > minHedgeAmount) {
amount = Math.min(amount, nowAccs[0].Stocks, nowAccs[1].Balance / price, maxHedgeAmount)
Log("trigger A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks) // Tips
hedge(exB, exA, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
} else if (depthB.Bids[0].Price - depthA.Asks[0].Price > targetDiffPrice && Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount) >= minHedgeAmount) { // B -> A market conditions are met
var price = (depthB.Bids[0].Price + depthA.Asks[0].Price) / 2
var amount = Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount)
if (nowAccs[1].Stocks > minHedgeAmount && nowAccs[0].Balance / price > minHedgeAmount) {
amount = Math.min(amount, nowAccs[1].Stocks, nowAccs[0].Balance / price, maxHedgeAmount)
Log("trigger B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks) // Tips
hedge(exA, exB, price, amount)
cancelAll()
lastKeepBalanceTS = 0
isTrade = true
}
}
Hedging trigger conditions are as follows:
isTrade
in advance to mark whether hedging occurs. Here, if the hedging is triggered, the variable is set to true
. And reset the global variable lastKeepBalanceTS
to 0 (lastKeepBalanceTS is used to mark the timestamp of the last balancing operation, setting it to 0 will trigger the balancing operation immediately), and then cancel all pending orders.Balancing operation
if (ts - lastKeepBalanceTS > keepBalanceCyc * 1000) {
nowAccs = _C(updateAccs, exchanges)
var isBalance = keepBalance(initAccs, nowAccs, [depthA, depthB])
cancelAll()
if (isBalance) {
lastKeepBalanceTS = ts
if (isTrade) {
var nowBalance = _.reduce(nowAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
var initBalance = _.reduce(initAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
LogProfit(nowBalance - initBalance, nowBalance, initBalance, nowAccs)
isTrade = false
}
}
}
It can be seen that the balancing function is executed periodically, but if the lastKeepBalanceTS
is reset to 0 after the hedging operation is triggered, the balancing operation will be triggered immediately. The profit will be calculated after a successful balancing.
Status bar information
LogStatus(_D(), "A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, " B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, " targetDiffPrice:", targetDiffPrice, "\n",
"current A, Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n",
"current B, Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n",
"initial A, Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n",
"initial B, Stocks:", initAccs[1].Stocks, "FrozenStocks:", initAccs[1].FrozenStocks, "Balance:", initAccs[1].Balance, "FrozenBalance", initAccs[1].FrozenBalance)
The status bar is not particularly complex in design. It displays the current time, the price difference from Exchange A to Exchange B and the price difference from Exchange B to Exchange A. And it displays the current hedge target spread, the asset data of the exchange A account and the exchange B account.
In terms of parameters, we designed the conversion rate value parameter, and we also designed the exchange rate conversion in the initial operation of the main
function at the beginning of the strategy. It should be noted that the SetRate
exchange rate conversion function needs to be executed first.
Because this function affects two aspects:
BTC_USDT
, the price unit is USDT
, and the available denomination currency in the account assets is also USDT
. If I want to convert the value into CNY, set exchange.SetRate(6.8)
in the code to convert the data obtained by all functions under the exchange
exchange object to CNY.
To convert to what denominated currency, pass in the exchange rate from the current denominated currency to the target denominated currency to the SetRate
function.Complete Strategy: Spot Hedging Strategy of Different Denominated Currencis (Tutorial)