Preface
The previous article introduced the principle and backtesting of pair trading, https://www.fmz.com/bbs-topic/10459. Here is a practical source code based on the FMZ platform. The strategy is simple and clear, suitable for beginners to learn. The FMZ platform has recently upgraded some APIs to be more friendly to multi-trading pair strategies. This article will introduce the JavaScript source code of this strategy in detail. Although the strategy code is only one hundred lines, it contains all the aspects required for a complete strategy. The specific API can be viewed in the API document, which is very detailed. The strategy public address: https://www.fmz.com/strategy/456143 can be copied directly.
FMZ Platform Usage
If you are not familiar with the FMZ platform, I strongly recommend you to read this tutorial: https://www.fmz.com/bbs-topic/4145 . It introduces the basic functions of the platform in detail, as well as how to deploy a robot from scratch.
Strategy Framework
The following is a simple strategy framework. The main function is the entry point. The infinite loop ensures that the strategy is executed continuously, and a small sleep time is added to prevent the access frequency from exceeding the exchange limit.
function main(){
while(true){
//strategy content
Sleep(Interval * 1000) //Sleep
}
}
Record Historical Data
The robot will restart repeatedly for various reasons, such as errors, parameter updates, strategy updates, etc., and some data needs to be saved for the next startup. Here is a demonstration of how to save the initial equity for calculating returns. The _G() function can store various data. _G(key, value) can store the value of value and call it out with _G(key), where key is a string.
let init_eq = 0 //defining initial equity
if(!_G('init_eq')){ //If there is no storage, _G('init_eq') returns null.
init_eq = total_eq
_G('init_eq', total_eq) //Since there is no storage, the initial equity is the current equity and is stored here
}else{
init_eq = _G('init_eq') //If stored, read the value of the initial equity
}
Strategy Fault Tolerance
When obtaining data such as positions and market conditions through the API, errors may be returned due to various reasons. Directly calling the data will cause the strategy to stop due to errors, so a fault-tolerant mechanism is needed. The _C() function will retry automatically after an error until the correct data is returned. Or check whether the data is available after returning.
let pos = _C(exchange.GetPosition, pair)
let ticker_A = exchange.GetTicker(pair_a)
let ticker_B = exchange.GetTicker(pair_b)
if(!ticker_A || !ticker_B){
continue //If the data is not available, exit the loop.
}
Multi-Currency Compatible API
Functions like GetPosition, GetTicker, and GetRecords can add a trading pair parameter to get the corresponding data, without having to set the exchange-bound trading pair, which greatly facilitates the compatibility of multiple trading pair strategies. For specific upgrade content, see the article: https://www.fmz.com/bbs-topic/10456. Of course, the latest docker is required to support it. If your docker is too old, you need to upgrade.
Strategy Parameters
- Pair_A: Trading pair A that needs to be paired for trading. You need to choose the trading pair yourself. You can refer to the introduction and backtesting in the previous article.
- Pair_B: Trading pair B that needs to be paired.
- Quote: The margin currency of the futures exchange, usually in USDT.
- Pct: How much deviation to add positions, see the article on strategy principles for details, due to handling fees and slippage reasons, it should not be set too small.
- Trade_Value: The trading value of adding positions for each deviation from the grid size.
- Ice_Value: If the transaction value is too large, you can use the iceberg commission value to open a position. Generally, it can be set to the same value as the transaction value.
- Max_Value: Maximum holdings of a single currency, to avoid the risk of holding too many positions.
- N: The parameter used to calculate the average price ratio, the unit is hour, for example, 100 represents the average of 100 hours.
- Interval: The sleep time between each cycle of the strategy.
Complete Strategy Notes
If you still don't understand, you can use FMZ's API documentation, debugging tools, and commonly used AI dialogue tools on the market to solve your questions.
function GetPosition(pair){
let pos = _C(exchange.GetPosition, pair)
if(pos.length == 0){ //Returns null to indicate no position
return {amount:0, price:0, profit:0}
}else if(pos.length > 1){ //The strategy should be set to unidirectional position mode
throw 'Bidirectional positions are not supported'
}else{ //For convenience, long positions are positive and short positions are negative
return {amount:pos[0].Type == 0 ? pos[0].Amount : -pos[0].Amount, price:pos[0].Price, profit:pos[0].Profit}
}
}
function GetRatio(){
let kline_A = exchange.GetRecords(Pair_A+"_"+Quote+".swap", 60*60, N) //Hourly K-line
let kline_B = exchange.GetRecords(Pair_B+"_"+Quote+".swap", 60*60, N)
let total = 0
for(let i= Math.min(kline_A.length,kline_B.length)-1; i >= 0; i--){ //Calculate in reverse to avoid the K-line being too short.
total += kline_A[i].Close / kline_B[i].Close
}
return total / Math.min(kline_A.length,kline_B.length)
}
function GetAccount(){
let account = _C(exchange.GetAccount)
let total_eq = 0
if(exchange.GetName == 'Futures_OKCoin'){ //Since the API here is not compatible, only OKX Futures Exchange obtains the total equity currently.
total_eq = account.Info.data[0].totalEq //The equity information of other exchanges is also included. You can look for it yourself in the exchange API documentation.
}else{
total_eq = account.Balance //Temporary use of available balances on other exchanges will cause errors in calculating returns, but will not affect the use of strategies.
}
let init_eq = 0
if(!_G('init_eq')){
init_eq = total_eq
_G('init_eq', total_eq)
}else{
init_eq = _G('init_eq')
}
LogProfit(total_eq - init_eq)
return total_eq
}
function main(){
var precision = exchange.GetMarkets() //Get the precision here
var last_get_ratio_time = Date.now()
var ratio = GetRatio()
var total_eq = GetAccount()
while(true){
let start_loop_time = Date.now()
if(Date.now() - last_get_ratio_time > 10*60*1000){ //Update the average price and account information every 10 minutes
ratio = GetRatio()
total_eq = GetAccount()
last_get_ratio_time = Date.now()
}
let pair_a = Pair_A+"_"+Quote+".swap" //The trading pair is set as BTC_USDT.swap
let pair_b = Pair_B+"_"+Quote+".swap"
let CtVal_a = "CtVal" in precision[pair_a] ? precision[pair_a].CtVal : 1 //Some exchanges use sheets to represent quantity, such as one sheet represents 0.01 coin, so you need to convert.
let CtVal_b = "CtVal" in precision[pair_b] ? precision[pair_b].CtVal : 1 //No need to include this field
let position_A = GetPosition(pair_a)
let position_B = GetPosition(pair_b)
let ticker_A = exchange.GetTicker(pair_a)
let ticker_B = exchange.GetTicker(pair_b)
if(!ticker_A || !ticker_B){ //If the returned data is abnormal, jump out of this loop
continue
}
let diff = (ticker_A.Last / ticker_B.Last - ratio) / ratio //Calculate the ratio of deviation
let aim_value = - Trade_Value * diff / Pct //Target holding position
let id_A = null
let id_B = null
//The following is the specific logic of opening a position
if( -aim_value + position_A.amount*CtVal_a*ticker_A.Last > Trade_Value && position_A.amount*CtVal_a*ticker_A.Last > -Max_Value){
id_A = exchange.CreateOrder(pair_a, "sell", ticker_A.Buy, _N(Ice_Value / (ticker_A.Buy * CtVal_a), precision[pair_a].AmountPrecision))
}
if( -aim_value - position_B.amount*CtVal_b*ticker_B.Last > Trade_Value && position_B.amount*CtVal_b*ticker_B.Last < Max_Value){
id_B = exchange.CreateOrder(pair_b, "buy", ticker_B.Sell, _N(Ice_Value / (ticker_B.Sell * CtVal_b), precision[pair_b].AmountPrecision))
}
if( aim_value - position_A.amount*CtVal_a*ticker_A.Last > Trade_Value && position_A.amount*CtVal_a*ticker_A.Last < Max_Value){
id_A = exchange.CreateOrder(pair_a, "buy", ticker_A.Sell, _N(Ice_Value / (ticker_A.Sell * CtVal_a), precision[pair_a].AmountPrecision))
}
if( aim_value + position_B.amount*CtVal_b*ticker_B.Last > Trade_Value && position_B.amount*CtVal_b*ticker_B.Last > -Max_Value){
id_B = exchange.CreateOrder(pair_b, "sell", ticker_B.Buy, _N(Ice_Value / (ticker_B.Buy * CtVal_b), precision[pair_b].AmountPrecision))
}
if(id_A){
exchange.CancelOrder(id_A) //Cancel directly here
}
if(id_B){
exchange.CancelOrder(id_B)
}
let table = {
type: "table",
title: "trading Information",
cols: ["initial equity", "current equity", Pair_A+"position", Pair_B+"position", Pair_A+"holding price", Pair_B+"holding price", Pair_A+"profits", Pair_B+"profits", Pair_A+"price", Pair_B+"price", "current price comparison", "average price comparison", "deviation from average price", "loop delay"],
rows: [[_N(_G('init_eq'),2), _N(total_eq,2), _N(position_A.amount*CtVal_a*ticker_A.Last, 1), _N(position_B.amount*CtVal_b*ticker_B.Last,1),
_N(position_A.price, precision[pair_a].PircePrecision), _N(position_B.price, precision[pair_b].PircePrecision),
_N(position_A.profit, 1), _N(position_B.profit, 1), ticker_A.Last, ticker_B.Last,
_N(ticker_A.Last / ticker_B.Last,6), _N(ratio, 6), _N(diff, 4), (Date.now() - start_loop_time)+"ms"
]]
}
LogStatus("`" + JSON.stringify(table) + "`") //This function will display a table containing the above information on the robot page.
Sleep(Interval * 1000) //Sleep time in ms
}
}