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.
Design some functions and strategy interface parameters according to the strategy requirements
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:
- Hedge difference:
hedgeDiffPrice
, when the difference exceeds this value, the hedging operation is triggered. - Minimum hedge amount:
minHedgeAmount
, the minimum order amount (coins) that can be hedged. - Maximum hedge amount:
maxHedgeAmount
, the maximum order amount (coins) for one hedging. - Price precision of A:
pricePrecisionA
, the order price precision (number of decimal places) placed by Exchange A. - Amount precision of A:
amountPrecisionA
, the amount precision of the order placed by Exchange A (number of decimal places). - Price precision of B:
pricePrecisionB
, the order price precision (number of decimal places) placed by Exchange B. - Amount precision of B:
amountPrecisionB
, the amount precision of the order placed by Exchange B (number of decimal places). - Rate of Exchange A:
rateA
, the exchange rate conversion of the first added exchange object, the default is 1, not converted. - Rate of Exchange B:
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.
- updateAccs
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.
- cancelAll
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.
- getDepthPrice
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:
- hedge
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.
- keepBalance
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.
Main function design 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 variablenowAccs
is used to record the current account data, using the function we just designedupdateAccs
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). ForinitAccs, 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) }
Trading logic, main loop in main function
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,
depthBRoutineobjects 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 thespread 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:
- Meet the hedging spread first, and only when the spread of the order meets the set spread parameters can it be hedged.
- The amount that can be hedged in the market must meet the minimum hedging amount set in the parameters. Because the minimum order amount that may be limited by different exchanges is different, the smallest of the two should be taken.
- The assets in the exchange of the sell operation are enough to sell, and the assets in the exchange of the buy operation are enough to buy.
When these conditions are met, execute the hedging function to place a hedging order. Before the main function, we declare a variableisTrade
in advance to mark whether hedging occurs. Here, if the hedging is triggered, the variable is set totrue
. And reset the global variablelastKeepBalanceTS
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.
Handling of trading pairs of different denominated currencies
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:
- Price conversion in all market data, order data, and position data.
- The conversion of the denominated currency in the account assets.
For example, the current trading pair isBTC_USDT
, the price unit isUSDT
, and the available denomination currency in the account assets is alsoUSDT
. If I want to convert the value into CNY, setexchange.SetRate(6.8)
in the code to convert the data obtained by all functions under theexchange
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 theSetRate
function.
Complete Strategy: Spot Hedging Strategy of Different Denominated Currencis (Tutorial)