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.


1. Strategy Principles and Market Relevance

1.1 Strategy Background

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.

1.2 Prevalence in the Crypto Market

Similar correlation phenomena are not uncommon in the crypto market:

  • Different contracts or derivatives of the same project: Due to the same underlying assets or team background, the prices of different products often have a strong linkage.
  • Cross-exchange arbitrage: The same asset on different exchanges may have a small price difference due to differences in liquidity and matching mechanisms.
  • Stablecoins and fiat-pegged products: These products often have expected exchange rate deviations, and arbitrageurs can profit from slight fluctuations.

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.

2. Detailed Explanation of Code Logic

The code consists of several parts, each module corresponds to the key steps in the arbitrage strategy.

2.1 Auxiliary Function Description

Get position information

functionGetPosition(pair){
    let pos = exchange.GetPosition(pair)
    if(pos.length == 0){
        return {amount:0, price:0, profit:0}
    }elseif(pos.length > 1){
        throw'Bidirectional positions are not supported'
    }elseif(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')
        returnnull
    }
}
  • Function: This function is used to obtain the position information of the specified trading pair uniformly, and convert the long and short positions into positive and negative numbers.
  • Logic: If the position is empty, return the default zero position; if there is more than one order, report an error (to ensure unidirection position); otherwise return the direction, price and floating profit and loss of the current position.

Initialize account

functionInitAccount(){
    let account = _C(exchange.GetAccount)
    let total_eq = account.Equitylet init_eq = 0if(!_G('init_eq')){
        init_eq = total_eq
        _G('init_eq', total_eq)
    }else{
        init_eq = _G('init_eq')
    }

    return init_eq
}
  • Function: This function is used to initialize and record account equity as a basis for subsequent profit and loss calculations.

Cancel pending order

functionCancelPendingOrders() {
    orders = exchange.GetOrders();  // Get orderfor (let order of orders) {
        if (order.Status == ORDER_STATE_PENDING) {  // Cancel only unfulfilled orders
            exchange.CancelOrder(order.Id);  // Cancel pending order
        }
    }
}
  • Function: Before placing an order, make sure to cancel the previous uncompleted order to prevent order conflicts or duplicate orders.

2.2 Main Tading Logic

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.

  • Calculation formula:

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:

  • If ratio is greater than diffLevel, it is believed that the market is about to rise, so a buy order is issued for Pair_B (buy long position).
  • If ratio is less than -diffLevel, it is considered that the market is about to fall and a sell order is issued (a short position is opened).

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:

  • Long position (buy): The take profit price is set to the position price multiplied by (1 + stopProfitLevel), and the stop loss price is set to the position price multiplied by (1 - stopLossLevel).
  • Short position (sell): The take profit price is set to the position price multiplied by (1 - stopProfitLevel), and the stop loss price is set to the position price multiplied by (1 + stopLossLevel).

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.

3. Strategy Optimization and Expansion Methods

While this strategy exploits the subtle delay between two highly correlated contracts, there are still many areas that can be improved:

3.1 Parameter Optimization and Dynamic Adjustment

  • Threshold adjustment: Parameters such as diffLevel, stopProfitLevel, and stopLossLevel may need to be adjusted in different market environments. These parameters can be optimized automatically through historical data backtesting or real-time dynamic adjustment models (such as machine learning algorithms).
  • Position management: The current strategy uses a fixed Trade_Number to open positions. In the future, we can consider introducing dynamic position management or a mechanism of opening positions in batches and taking profits gradually to reduce the risk of a single transaction.

3.2 Trading Signal Filtering

  • Multi-factor signal: Calculating ratios based solely on price fluctuations may be affected by noise. Consider introducing trading volume, order book depth, and technical indicators (such as RSI, MACD, etc.) to further filter out false signals.
  • Compensation delay: Considering that MELANIA has a 1-2 second delay, developing a more accurate time synchronization and signal prediction mechanism will help improve the accuracy of entry timing.

3.3 System Robustness and Risk Control

  • Error handling: Add exception handling and logging to ensure timely response when encountering network delays or exchange interface anomalies to prevent unexpected losses caused by system failures.
  • Risk control strategy: Combine capital management and maximum drawdown control to set a daily or single transaction loss limit to prevent serial losses in extreme market environments.

3.4 Code and Architecture Optimization

  • Asynchronous processing: Currently, the strategy loop is executed every 100 milliseconds. Through asynchronous processing and multi-threaded optimization, the risk of delay and execution blocking can be reduced.
  • Strategy backtest and simulation: Introduce a complete backtesting system and real-time simulated trading environment to verify the performance of strategies under different market conditions and help the strategies run more stably in live trading.

4. Summary

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

functionGetPosition(pair){
    let pos = exchange.GetPosition(pair)
    if(pos.length == 0){
        return {amount:0, price:0, profit:0}
    }elseif(pos.length > 1){
        throw'Bidirectional positions are not supported'
    }elseif(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')
        returnnull
    }
}

functionInitAccount(){
    let account = _C(exchange.GetAccount)
    let total_eq = account.Equitylet init_eq = 0if(!_G('init_eq')){
        init_eq = total_eq
        _G('init_eq', total_eq)
    }else{
        init_eq = _G('init_eq')
    }

    return init_eq
}

functionCancelPendingOrders() {
    orders = exchange.GetOrders();  // Get orderfor (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";


functionmain() {
    exchange.IO('simulate', true);
    LogReset(0);
    Log('The strategy starts running')

    var precision = exchange.GetMarkets();
    var ratio = 0var 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 tradedif (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;
                } elseif (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 lossif (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 = nullLog('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 = nullLog('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 completedif (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 completedif (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 lossif (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 loopslet curEquity = exchange.GetAccount().Equity// Output trading information tablelet 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);
    }
}

From: A Brief Analysis of Arbitrage Strategy: How to Capture Low-risk Opportunities with Short Time Lags

Leave a Reply

Your email address will not be published. Required fields are marked *