The inspiration for this strategy comes from the opportunity post by Zhihu author “Dream Dealer” - “TRUMP and MELANIA low-risk correlation arbitrage model”. This article explores the price correlation between two contracts (TRUMP and MELANIA) launched on BN, and uses the subtle time delay between the two to try to capture short-term market fluctuations, thereby achieving low-risk arbitrage. Next, we will explain the principles of this strategy in detail, the code implementation logic, and explore possible optimization directions.
It is important to note in advance that this strategy is equivalent to a manual trading task. Only when two suitable trading pairs are found can there be a certain profit opportunity, and the profit life of the trading pair may be short. When it is found that there is no profit opportunity, it is necessary to stop the strategy in time to prevent profit drawdown or even loss.
Both TRUMP and MELANIA contracts are issued by the same issuing team and have the same controlling funds, so the price movements of the two are highly synchronized most of the time. However, due to factors such as contract design or market execution, the price of MELANIA often lags behind TRUMP by 1-2 seconds. This slight delay provides arbitrageurs with the opportunity to capture price differences and conduct high-frequency copy trading. Simply put, when TRUMP fluctuates rapidly, MELANIA tends to follow up very soon, and by taking advantage of this delay, transactions can be completed with lower risks.
Similar correlation phenomena are not uncommon in the crypto market:
This correlation provides high-frequency traders and arbitrageurs with stable trading signals and lower-risk operating opportunities, but it also requires trading strategies to be highly sensitive to subtle market changes and be able to respond in real time.
The code consists of several parts, each module corresponds to the key steps in the arbitrage strategy.
Get position information
function GetPosition(pair){
let pos = exchange.GetPosition(pair)
if(pos.length == 0){
return {amount:0, price:0, profit:0}
}else if(pos.length > 1){
throw 'Bidirectional positions are not supported'
}else if(pos.length == 1){
return {amount:pos[0].Type == 0 ? pos[0].Amount : -pos[0].Amount, price:pos[0].Price, profit:pos[0].Profit}
}else{
Log('No position data obtained')
return null
}
}
Initialize account
function InitAccount(){
let account = _C(exchange.GetAccount)
let total_eq = account.Equity
let init_eq = 0
if(!_G('init_eq')){
init_eq = total_eq
_G('init_eq', total_eq)
}else{
init_eq = _G('init_eq')
}
return init_eq
}
Cancel pending order
function CancelPendingOrders() {
orders = exchange.GetOrders(); // Get order
for (let order of orders) {
if (order.Status == ORDER_STATE_PENDING) { // Cancel only unfulfilled orders
exchange.CancelOrder(order.Id); // Cancel pending order
}
}
}
The main function uses an infinite loop to continuously execute the following steps continuously:
1. Data acquisition and market calculation
At the beginning of each loop, the market data of Pair_A and Pair_B are obtained through exchange.GetRecords
respectively.
By comparing the increase or decrease of the two, we can determine whether there is an abnormal price difference. When the price difference exceeds the preset diffLevel, the opening condition is triggered.
2. Determine the opening conditions and place an order When there is no current position (position_B.amount == 0) and trading (afterTrade==1) is allowed:
Before placing an order, the order cancellation function will be called to ensure that the current order status is cleared.
3. Take-profit and stop-loss logic Once a position is established, the strategy will set corresponding take-profit and stop-loss orders according to the position direction:
The system will monitor the real-time market price. Once the take-profit or stop-loss conditions are triggered, the original pending order will be cancelled and an order will be placed to close the position.
4. Profit statistics and log records after closing a position After each position is closed, the system will obtain the changes in account equity and count the number of profits, losses, and cumulative profit/loss amounts. At the same time, the current position information, transaction statistics, and loop delay are displayed in real time using tables and graphs to facilitate subsequent strategy effect analysis.
While this strategy exploits the subtle delay between two highly correlated contracts, there are still many areas that can be improved:
This article details the basic principles and implementation code of a short-time lagging contract correlation arbitrage strategy. From using price fluctuation differences to capture entry opportunities to setting take-profit and stop-loss for position management, this strategy utilizes the high correlation between assets in the crypto market. At the same time, we also put forward a number of optimization suggestions, including dynamic parameter adjustment, signal filtering, system robustness, and code optimization, in order to further improve the stability and profitability of the strategy in live trading applications.
Although the strategy is inspired and simple to implement, any arbitrage operation should be treated with caution in the high-frequency and volatile crypto market. I hope this article can provide valuable reference and inspiration for friends who are keen on quantitative trading and arbitrage strategies.
Note: The strategy test environment is OKX Demo, and specific details can be modified for different exchanges
function GetPosition(pair){
let pos = exchange.GetPosition(pair)
if(pos.length == 0){
return {amount:0, price:0, profit:0}
}else if(pos.length > 1){
throw 'Bidirectional positions are not supported'
}else if(pos.length == 1){
return {amount:pos[0].Type == 0 ? pos[0].Amount : -pos[0].Amount, price:pos[0].Price, profit:pos[0].Profit}
}else{
Log('No position data obtained')
return null
}
}
function InitAccount(){
let account = _C(exchange.GetAccount)
let total_eq = account.Equity
let init_eq = 0
if(!_G('init_eq')){
init_eq = total_eq
_G('init_eq', total_eq)
}else{
init_eq = _G('init_eq')
}
return init_eq
}
function CancelPendingOrders() {
orders = exchange.GetOrders(); // Get order
for (let order of orders) {
if (order.Status == ORDER_STATE_PENDING) { // Cancel only unfulfilled orders
exchange.CancelOrder(order.Id); // Cancel pending order
}
}
}
var pair_a = Pair_A + "_USDT.swap";
var pair_b = Pair_B + "_USDT.swap";
function main() {
exchange.IO('simulate', true);
LogReset(0);
Log('The strategy starts running')
var precision = exchange.GetMarkets();
var ratio = 0
var takeProfitOrderId = null;
var stopLossOrderId = null;
var successCount = 0;
var lossCount = 0;
var winMoney = 0;
var failMoney = 0;
var afterTrade = 1;
var initEq = InitAccount();
var curEq = initEq
var pricePrecision = precision[pair_b].PricePrecision;
while (true) {
try{
let startLoopTime = Date.now();
let position_B = GetPosition(pair_b);
let new_r_pairB = exchange.GetRecords(pair_b, 1).slice(-1)[0];
if (!new_r_pairB || !position_B) {
Log('Skip the current loop');
continue;
}
// Combined trading conditions: Check whether a position can be opened and traded
if (afterTrade == 1 && position_B.amount == 0) {
let new_r_pairA = exchange.GetRecords(pair_a, 1).slice(-1)[0];
if (!new_r_pairA ) {
Log('Skip the current loop');
continue;
}
ratio = (new_r_pairA.Close - new_r_pairA.Open) / new_r_pairA.Open - (new_r_pairB.Close - new_r_pairB.Open) / new_r_pairB.Open;
if (ratio > diffLevel) {
CancelPendingOrders();
Log('Real-time ratio:', ratio, 'buy:', pair_b, position_B.amount);
exchange.CreateOrder(pair_b, "buy", -1, Trade_Number);
afterTrade = 0;
} else if (ratio < -diffLevel) {
CancelPendingOrders();
Log('Real-time ratio:', ratio, 'sell:', pair_b, position_B.amount);
exchange.CreateOrder(pair_b, "sell", -1, Trade_Number);
afterTrade = 0;
}
}
// Determine the take profit and stop loss
if (position_B.amount > 0 && takeProfitOrderId == null && stopLossOrderId == null && afterTrade == 0) {
Log('Long position price:', position_B.price, 'Take profit price:', position_B.price * (1 + stopProfitLevel), 'Stop loss price:', position_B.price * (1 - stopLossLevel));
takeProfitOrderId = exchange.CreateOrder(pair_b, "closebuy", position_B.price * (1 + stopProfitLevel), position_B.amount);
Log('Take profit order:', takeProfitOrderId);
}
if (position_B.amount > 0 && takeProfitOrderId != null && stopLossOrderId == null && new_r_pairB.Close < position_B.price * (1 - stopLossLevel) && afterTrade == 0) {
CancelPendingOrders();
takeProfitOrderId = null
Log('Long position stop loss');
stopLossOrderId = exchange.CreateOrder(pair_b, "closebuy", -1, position_B.amount);
Log('Long position stop loss order:', stopLossOrderId);
}
if (position_B.amount < 0 && takeProfitOrderId == null && stopLossOrderId == null && afterTrade == 0) {
Log('Short position price:', position_B.price, 'Take profit price:', position_B.price * (1 - stopProfitLevel), 'Stop loss price:', position_B.price * (1 + stopLossLevel));
takeProfitOrderId = exchange.CreateOrder(pair_b, "closesell", position_B.price * (1 - stopProfitLevel), -position_B.amount);
Log('Take profit order:', takeProfitOrderId, 'Current price:', new_r_pairB.Close );
}
if (position_B.amount < 0 && takeProfitOrderId != null && stopLossOrderId == null && new_r_pairB.Close > position_B.price * (1 + stopLossLevel) && afterTrade == 0) {
CancelPendingOrders();
takeProfitOrderId = null
Log('Short position stop loss');
stopLossOrderId = exchange.CreateOrder(pair_b, "closesell", -1, -position_B.amount);
Log('Short position stop loss order:', stopLossOrderId);
}
// The market order has not been completed
if (takeProfitOrderId == null && stopLossOrderId != null && afterTrade == 0) {
let stoplosspos = GetPosition(pair_b)
if(stoplosspos.amount > 0){
Log('The market order to close long positions has not been completed')
exchange.CreateOrder(pair_b, 'closebuy', -1, stoplosspos.amount)
}
if(stoplosspos.amount < 0){
Log('The market order to close short positions has not been completed')
exchange.CreateOrder(pair_b, 'closesell', -1, -stoplosspos.amount)
}
}
// The closing position has not been completed
if (Math.abs(position_B.amount) < Trade_Number && Math.abs(position_B.amount) > 0 && afterTrade == 0){
Log('The closing position has not been completed')
if(position_B.amount > 0){
exchange.CreateOrder(pair_b, 'closebuy', -1, position_B.amount)
}else{
exchange.CreateOrder(pair_b, 'closesell', -1, -position_B.amount)
}
}
// Calculate profit and loss
if (position_B.amount == 0 && afterTrade == 0) {
if (stopLossOrderId != null || takeProfitOrderId != null) {
stopLossOrderId = null;
takeProfitOrderId = null;
let afterEquity = exchange.GetAccount().Equity;
let curAmount = afterEquity - curEq;
curEq = afterEquity
if (curAmount > 0) {
successCount += 1;
winMoney += curAmount;
Log('Profit amount:', curAmount);
} else {
lossCount += 1;
failMoney += curAmount;
Log('Amount of loss:', curAmount);
}
afterTrade = 1;
}
}
if (startLoopTime % 10 == 0) { // Record every 10 loops
let curEquity = exchange.GetAccount().Equity
// Output trading information table
let table = {
type: "table",
title: "trading information",
cols: [
"initial equity", "current equity", Pair_B + "position", Pair_B + "holding price", Pair_B + "returns", Pair_B + "price",
"number of profits", "profit amount", "number of losses", "loss amount", "win rate", "profit-loss ratio"
],
rows: [
[
_N(_G('init_eq'), 2), // Initial equity
_N(curEquity, 2), // Current equity
_N(position_B.amount, 1), // Pair B position
_N(position_B.price, pricePrecision), // Pair B holding price
_N(position_B.profit, 1), // Pair B profits
_N(new_r_pairB.Close, pricePrecision), // Pair B price
_N(successCount, 0), // Profitable times
_N(winMoney, 2), // Profit amount
_N(lossCount, 0), // Number of losses
_N(failMoney, 2), // Amount of loss
_N(successCount + lossCount === 0 ? 0 : successCount / (successCount + lossCount), 2), // Win rate
_N(failMoney === 0 ? 0 : winMoney / failMoney * -1, 2) // Profit-loss ratio
]
]
};
$.PlotMultLine("ratio plot", "Amplitude change difference", ratio, startLoopTime);
$.PlotMultHLine("ratio plot", diffLevel, "Spread cap", "red", "ShortDot");
$.PlotMultHLine("ratio plot", -diffLevel, "Spread limit", "blue", "ShortDot");
LogStatus("`" + JSON.stringify(table) + "`");
LogProfit(curEquity - initEq, '&')
}
}catch(e){
Log('Strategy error:', e)
}
Sleep(200);
}
}