Some Thoughts on the Logic of Digital Currency Futures Trading

Author: Lydia, Created: 2022-11-30 17:12:20, Updated: 2023-09-11 19:59:55

img

Some Thoughts on the Logic of Digital Currency Futures Trading

Problem scenario

For a long time, the data delay problem of the position API interface of the digital currency exchange has always bothered me. I haven’t found a proper way to deal with this problem. Let me reproduce the scene of the problem. Usually, the market price order provided by the contract exchange is actually the counterparty price, so sometimes it is not reliable to use this so-called “market price order”. Therefore, when we write digital currency futures trading strategies, we use the limit order most. After each order is placed, we need to check the position to see if the order is closed and the corresponding position is held. The problem lies in this position information. If the order is closed, the data returned by the position information interface of the exchange (that is, the exchange interface actually accessed by the bottom layer when we call exchange.GetPosition) should include the new position information. However, if the data returned by the exchange is the old data, that is, the position information before the order just placed is closed, there exists a problem. The trading logic may deem that the order has not been completed, and continue to place the order. However, the interface for the exchange to place an order is not delayed. Instead, the transaction is filled very fast, and the order is placed. This will cause a serious consequence that the strategy will place orders repeatedly when triggering the opening position operation once.

Practical experience

Because of this problem, I have seen a strategy open full of long positions crazily. Fortunately, the market soared at that time, and the floating profit exceeded 10BTC. Fortunately, the market increased sharply. If it decreased sharply, we can imagine the outcome.

Solutions

  • Solution 1 The strategy can be designed to place only one order, and the order price is the price of the current trading opponent plus a large sliding price, so as to take a certain depth of the opponent’s order. The advantage of this is that only one order will be placed, and it will not be judged based on the position information. This can avoid the problem of repeated orders, but sometimes placing an order may trigger the price limit mechanism of the exchange when the price change is relatively large, and it is possible to increase the sliding price and still fail to make a deal, thus missing the opportunity.

  • Solution 2 With the market price order function of the exchange, the price is transferred to - 1 on the FMZ as the market price order. At present, the OKEX futures interface has been upgraded to support the real market price order.

  • Solution 3 We still use the previous trading logic and place orders with price limit orders, but we add some detection in the trading logic to try to solve the problem caused by the position data delay. Check whether the order has disappeared directly from the list of pending orders without cancellation (there are two possibilities for disappearance from the list of pending orders: 1. Cancellation and 2. Filled). If such a situation is detected, and the quantity of the order placed again is the same as that of the last order, it is important to note that whether the position data is delayed. Let the program enter the waiting logic to reacquire the position information, or even continue to optimize and increase the number of times to trigger the waiting, if it exceeds a certain number of times, it indicates that the position interface data delay is serious, which causes the trading logic to terminate.

Design based on Solution 3

// Parameters
/*
var MinAmount = 1
var SlidePrice = 5
var Interval = 500
*/

function GetPosition(e, contractType, direction) {
    e.SetContractType(contractType)
    var positions = _C(e.GetPosition);
    for (var i = 0; i < positions.length; i++) {
        if (positions[i].ContractType == contractType && positions[i].Type == direction) {
            return positions[i]
        }
    }

    return null
}

function Open(e, contractType, direction, opAmount) {
    var initPosition = GetPosition(e, contractType, direction);
    var isFirst = true;
    var initAmount = initPosition ? initPosition.Amount : 0;
    var nowPosition = initPosition;
    var directBreak = false 
    var preNeedOpen = 0
    var timeoutCount = 0
    while (true) {
        var ticker = _C(e.GetTicker)
        var needOpen = opAmount;
        if (isFirst) {
            isFirst = false;
        } else {
            nowPosition = GetPosition(e, contractType, direction);
            if (nowPosition) {
                needOpen = opAmount - (nowPosition.Amount - initAmount);
            }
            // Check directBreak and the position remains unchanged
            if (preNeedOpen == needOpen && directBreak) {
                Log("Suspected position data is delayed, wait for 30 seconds", "#FF0000")
                Sleep(30000)
                nowPosition = GetPosition(e, contractType, direction);
                if (nowPosition) {
                    needOpen = opAmount - (nowPosition.Amount - initAmount);
                }
                /*
                timeoutCount++
                if (timeoutCount > 10) {
                    Log("Suspected position is delayed for 10 consecutive times, and the order is failed!", "#FF0000")
                    break
                }
                */
            } else {
                timeoutCount = 0
            }
        }
        if (needOpen < MinAmount) {
            break;
        }
        
        var amount = needOpen;
        preNeedOpen = needOpen
        e.SetDirection(direction == PD_LONG ? "buy" : "sell");
        var orderId;
        if (direction == PD_LONG) {
            orderId = e.Buy(ticker.Sell + SlidePrice, amount, "open long positions", contractType, ticker);
        } else {
            orderId = e.Sell(ticker.Buy - SlidePrice, amount, "open short positions", contractType, ticker);
        }

        directBreak = false
        var n = 0
        while (true) {
            Sleep(Interval);
            var orders = _C(e.GetOrders);
            if (orders.length == 0) {
                if (n == 0) {
                    directBreak = true
                }
                break;
            }
            for (var j = 0; j < orders.length; j++) {
                e.CancelOrder(orders[j].Id);
                if (j < (orders.length - 1)) {
                    Sleep(Interval);
                }
            }
            n++
        }
    }

    var ret = {
        price: 0,
        amount: 0,
        position: nowPosition
    };
    if (!nowPosition) {
        return ret;
    }
    if (!initPosition) {
        ret.price = nowPosition.Price;
        ret.amount = nowPosition.Amount;
    } else {
        ret.amount = nowPosition.Amount - initPosition.Amount;
        ret.price = _N(((nowPosition.Price * nowPosition.Amount) - (initPosition.Price * initPosition.Amount)) / ret.amount);
    }
    return ret;
}

function Cover(e, contractType, opAmount, direction) {
    var initPosition = null;
    var position = null;
    var isFirst = true;

    while (true) {
        while (true) {
            Sleep(Interval);
            var orders = _C(e.GetOrders);
            if (orders.length == 0) {
                break;
            }
            for (var j = 0; j < orders.length; j++) {
                e.CancelOrder(orders[j].Id);
                if (j < (orders.length - 1)) {
                    Sleep(Interval);
                }
            }
        }

        position = GetPosition(e, contractType, direction)
        if (!position) {
            break
        }
        if (isFirst == true) {
            initPosition = position;
            opAmount = Math.min(opAmount, initPosition.Amount)
            isFirst = false;
        }

        var amount = opAmount - (initPosition.Amount - position.Amount)
        if (amount <= 0) {
            break
        }

        var ticker = _C(exchange.GetTicker)
        if (position.Type == PD_LONG) {
            e.SetDirection("closebuy");
            e.Sell(ticker.Buy - SlidePrice, amount, "close long positions", contractType, ticker);
        } else if (position.Type == PD_SHORT) {
            e.SetDirection("closesell");
            e.Buy(ticker.Sell + SlidePrice, amount, "close short positions", contractType, ticker);
        }

        Sleep(Interval)
    }

    return position
}

$.OpenLong = function(e, contractType, amount) {
    if (typeof(e) == "string") {
        amount = contractType
        contractType = e
        e = exchange
    }

    return Open(e, contractType, PD_LONG, amount);
}

$.OpenShort = function(e, contractType, amount) {
    if (typeof(e) == "string") {
        amount = contractType
        contractType = e
        e = exchange
    }

    return Open(e, contractType, PD_SHORT, amount);
};

$.CoverLong = function(e, contractType, amount) {
    if (typeof(e) == "string") {
        amount = contractType
        contractType = e
        e = exchange
    }

    return Cover(e, contractType, amount, PD_LONG);
};

$.CoverShort = function(e, contractType, amount) {
    if (typeof(e) == "string") {
        amount = contractType
        contractType = e
        e = exchange
    }

    return Cover(e, contractType, amount, PD_SHORT);
};


function main() {
    Log(exchange.GetPosition())
    var info = $.OpenLong(exchange, "quarter", 100)
    Log(info, "#FF0000")

    Log(exchange.GetPosition())
    info = $.CoverLong(exchange, "quarter", 30)
    Log(exchange.GetPosition())
    Log(info, "#FF0000")

    info = $.CoverLong(exchange, "quarter", 80)
    Log(exchange.GetPosition())
    Log(info, "#FF0000")
}

Template address: https://www.fmz.com/strategy/203258

The template interface is called in the same way as $.OpenLong, $.CoverLong in the main function above. The template is a beta version, and you are welcome to make suggestions. We will continue to optimize it so that we can handle the problem of position data delay.


Related

More