Detailed Explanation of Futures Backhand Doubling Algorithm Strategy Notes
Tags: algorithms, strategy, fmzquant, trading
Published: 2024-05-07 06:29:16

This strategy is a strategy that was applied to Digital Currency 796 Futures Exchange long time ago. Futures contracts are currency-based, that is, the margin is deducted by currency (for example, BTC contracts are deducted by BTC). The order quantity of contracts can be decimal, similar to currency-based contracts of Binance.
The strategy is used for learning strategy design, the logic processing is still very good, and the strategy is mainly used for learning.

Strategy code notes
```
var FirstTradeType = [ORDER_TYPE_BUY, ORDER_TYPE_SELL][OpType];   // The first opening position direction is determined according to the parameter OpType opening position direction. The value of the global variable FirstTradeType is ORDER_ TYPE_ BUY or ORDER_ TYPE_ SELL
var OrgAccount = null;                                            // Global variable, recording account assets
var Counter = {s : 0, f: 0};                                      // Declare a variable Counter, the value is initialized to an object (a similar structure python named dictionary), s represents the number of win time, f represents the number of losses
var LastProfit = 0;                                               // Recent P&L
var AllProfit = 0;                                                // Total P&L
var _Failed = 0;                                                  // Number of stop-losses

// Cancel all orders other than those with an ID in the list of pending orders. If the orderId parameter was not passed in, cancel all pending orders of the current trading pair. The parameter e is the reference of the exchange object. For example, when exchange is passed in as a parameter, e is the alias of exchange
function StripOrders(e, orderId) {                                
    var order = null;                               // Initialize the variable order to null
    if (typeof(orderId) == 'undefined') {           // If the parameter orderId is not written when it is passed in, then typeof (orderId)=='undefined' holds, the code block of the if statement is executed, and the orderId is assigned to null
        orderId = null;
    }
    while (true) {                                  // Process loop
        var dropped = 0;                            //  Process number of markers
        var orders = _C(e.GetOrders);               // Call GetOrders to get the current pending orders (orders that are not dealt) and assign the value to orders
        for (var i = 0; i < orders.length; i++) {   // Traverse the list of outstanding orders
            if (orders[i].Id == orderId) {          // If the order ID is the same as the order ID passed in the parameter, assign the value orders[i] to the local variable order in the function. Orders [i] refers to the current order structure when traversing
                order = orders[i];
            } else {                                // If the IDs are not the same, perform revocation
                var extra = "";                     // Set the extension information extra according to the partial deals
                if (orders[i].DealAmount > 0) {
                    extra = "Deal:" + orders[i].DealAmount;
                } else {
                    extra = "Unsettled";
                }
                e.SetDirection(orders[i].Type == ORDER_TYPE_BUY ? "buy" : "sell");
                e.CancelOrder(orders[i].Id, orders[i].Type == ORDER_TYPE_BUY ? "purchase order" : "selling order", extra);    // Cancel the order, with extra information output, which will be displayed in the log
                dropped++;                           // dropped count accumulation
            }
        }
        if (dropped == 0) {                          // When the traversal is completed, dropped is equal to 0, that is, there is no cancellation during traversal (there is no order to be canceled), that is, the cancellation processing is completed, and the while loop is skipped
            break;
        }
        Sleep(300);                                  // Prevent the rotation frequency from being too fast, with a certain interval each time
    }
    return order;                                    // Return the order to find
}


var preMsg = "";                                      // Variables for recording cache information
function GetAccount(e, waitFrozen) {                  // Obtain the account asset information. The parameter e is also a reference to exchange. The parameter waitFrozen controls whether to wait for freezing
    if (typeof(waitFrozen) == 'undefined') {          // If the waitFrozen parameter is not passed in during the call, the parameter waitFrozen is assigned a value of false, that is, the default is not to wait for freezing
        waitFrozen = false;
    }
    var account = null;
    var alreadyAlert = false;                         // Mark if the variable has been alerted
    while (true) {                                    // Get the current account information and detect freezing. If you do not wait for freezing, it will skip the while loop directly
        account = _C(e.GetAccount);
        if (!waitFrozen || account.FrozenStocks < MinStock) {
            break;
        }
        if (!alreadyAlert) {
            alreadyAlert = true;                      // Once the reminder is triggered, the alreadyAlert will be reset to avoid repeated reminders
            Log("find frozen money or currencies in the account", account);       // Output reminder log
        }
        Sleep(Interval);
    }
    msg = "successful:" + Counter.s + "times, failed:" + Counter.f + "times, current account currencies: " + account.Stocks;
    if (account.FrozenStocks > 0) {
        msg += "frozen currencies:" + account.FrozenStocks;
    }

    if (msg != preMsg) {                              // Check if the current message is different from the last one, and update it on the status bar if it is different
        preMsg = msg;
        LogStatus(msg, "#ff0000");
    }
    return account;                                   // Function returns to account structure of the account information
}

function GetPosition(e, orderType) {                  // Get the position, or get the position in the specified direction
    var positions = _C(e.GetPosition);                // Get the position
    if (typeof(orderType) == 'undefined') {           // The orderType parameter is the position type speicfied to be obtained. If no orderType parameter is passed in, all positions will be returned directly
        return positions;
    }
    for (var i = 0; i < positions.length; i++) {      // Traverse the list of positions
        if (positions[i].Type == orderType) {         // If the current traversed position data is the direction to be found (orderType)
            return positions[i];                      // Return to the position of orderType
        }
    }
    return null;
}

function GetTicker(e) {                               // Get ticker market data
    while (true) {
        var ticker = _C(e.GetTicker);                 // Get ticker market information
        if (ticker.Buy > 0 && ticker.Sell > 0 && ticker.Sell > ticker.Buy) {   // Check the reliability of the market data
            return ticker;                            // Return to ticker data
        }
        Sleep(100);
    }
}
// mode = 0 : direct buy, 1 : buy as buy1
function Trade(e, tradeType, tradeAmount, mode, slidePrice, maxSpace, retryDelay) {      // Trading functions
    // e Exchange object reference, trading direction of tradeType (buy/sell), trading volume of tradeAmount, the trading mode is mode, slidePrice means slide price, maxSpace means maximum distance of pending order, retryDelay means retry intervals
    var initPosition = GetPosition(e, tradeType);      // Get the position data of the specified direction, name it as initPosition
    var nowPosition = initPosition;                    // Declare another variable, nowPosition, and assign it with initPosition
    var orderId = null;
    var prePrice = 0;            // The price of placing order at last loop
    var dealAmount = 0;          // Number of transactions already made
    var diffMoney = 0;           
    var isFirst = true;          // The marker of first loop execution
    var tradeFunc = tradeType == ORDER_TYPE_BUY ? e.Buy : e.Sell;     // Function for placing orders, and call e.Buy or e.Sell according to the parameter tradeType
    var isBuy = tradeType == ORDER_TYPE_BUY;                          // Marker for buying
    while (true) {                                                    // while loop
        var account = _C(e.GetAccount);                               // Obtain the current account asset data
        var ticker = GetTicker(e);                                    // Obtain the current market data
        var tradePrice = 0;                                           // Set transaction prices based on mode parameters
        if (isBuy) {
            tradePrice = _N((mode == 0 ? ticker.Sell : ticker.Buy) + slidePrice, 4);
        } else {
            tradePrice = _N((mode == 0 ? ticker.Buy : ticker.Sell) - slidePrice, 4);
        }
        if (orderId == null) {
            if (isFirst) {                                            // Judge according to the isFirst marker variable, if it is the first execution, do nothing
                isFirst = false;                                      // Set the isFirst marker as false, indicating that it was not executed for the first time
            } else {                                                  // Non-first execution, update the position data
                nowPosition = GetPosition(e, tradeType);
            }
            dealAmount = _N((nowPosition ? nowPosition.Amount : 0) - (initPosition ? initPosition.Amount : 0), 6);   // Calculate the number of transactions according to the initial position data and current position data
            var doAmount = Math.min(tradeAmount - dealAmount, account.Stocks * MarginLevel, 4);      // Calculate the remaining amount to be transacted according to the number of transactions and the available assets of the account
            if (doAmount < MinStock) {                                                               // If the calculated transaction amount is less than the minimum transaction amount, stop the logic and skip out of the while loop
                break;
            }
            prePrice = tradePrice;                                                                   // Cache the transaction price in the current loop
            e.SetDirection(tradeType == ORDER_TYPE_BUY ? "buy" : "sell");                            // Set the direction of futures trading
            orderId = tradeFunc(tradePrice, doAmount);                                               // Place a transaction, the parameters are the calculated price, the number of orders placed for this time
        } else {                                                                                     // If the orderId of the recorded order variable is not null, it indicates that an order has been placed
            if (mode == 0 || Math.abs(tradePrice - prePrice) > maxSpace) {                           // If it is the maker mode, the current price and the last cached price exceed the maximum maker range
                orderId = null;                                                                      // Reset the orderId to a null value, and the order will be placed again in the next loop
            }
            var order = StripOrders(exchange, orderId);                                              // Call StripOrders to find the order with ID of orderId in the list of pending orders
            if (order == null) {                                                                     // If it cannot be found, reset the orderId to a null value, too, and continue the next round of order placement
                orderId = null;
            }
        }
        Sleep(retryDelay);                                                                           // A certain time is tentatively determined to control the frequency of the loop
    }

    if (dealAmount <= 0) {                                                                           // At the end of the while loop, if the amount of dealAmount is less than or equal to 0, it means that the transaction failed to return the null value
        return null;
    }

    return nowPosition;                                                                              // If it's normal condition, returns to the latest position data
}

function coverFutures(e, orderType) {                               // Function for closing the position
    var coverAmount = 0;                                            // Declare a variable coverAmount with an initial assignment of 0 to record the amount of closed positions
    while (true) {
        var positions = _C(e.GetPosition);                          // Obtain the position
        var ticker = GetTicker(e);                                  // Obtain the current quotations
        var found = 0;                                              // Find the marker
        for (var i = 0; i < positions.length; i++) {                // Iterate through the array of positions
            if (positions[i].Type == orderType) {                   // Find the position needed
                if (coverAmount == 0) {                             
                    coverAmount = positions[i].Amount;              // Record the position amount at the beginning, that is, the amount to be closed
                }
                if (positions[i].Type == ORDER_TYPE_BUY) {          // Perform the closing position operation according to the position type
                    e.SetDirection("closebuy");                     // Set the futures trading direction
                    e.Sell(ticker.Buy, positions[i].Amount);        // Order function
                } else {
                    e.SetDirection("closesell");
                    e.Buy(ticker.Sell, positions[i].Amount);
                }
                found++;                                            // Marker accumulation
            }
        }
        if (found == 0) {                                           // If the marker variable found is 0, then there are no positions to process and the while loop is skipped
            break;
        }
        Sleep(2000);                                                // 2 seconds interval
        StripOrders(e);                                             // Cancel all the current pending orders
    }
    return coverAmount;                                             // Return to the number of closed positions
}


function loop(pos) {
    var tradeType = null;                         // Trading direction of initialization
    if (typeof(pos) == 'undefined' || !pos) {     // Determine if it is the first round of execution
        tradeType = FirstTradeType;
        pos = Trade(exchange, tradeType, OpAmount, OpMode, SlidePrice, MaxSpace, Interval);     // First transaction
        if (!pos) {
            throw "Failure to open a position";
        } else {
            Log(tradeType == ORDER_TYPE_BUY ? "Open long positions to complete" : "Open short positions to complete", "average price:", pos.Price, "amount:", pos.Amount);
        }
    } else {
        tradeType = pos.Type;        // Continue to specify the direction of the transaction according to the direction of the position
    }
    var holdPrice = pos.Price;       // Position price
    var holdAmount = pos.Amount;     // Position amount

    var openFunc = tradeType == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell;     // Long position, open position to buy, otherwise open position to sell
    var coverFunc = tradeType == ORDER_TYPE_BUY ? exchange.Sell : exchange.Buy;    // Long position, close position to sell, otherwise close position to buy

    var reversePrice = 0;           // Backhand price
    var coverPrice = 0;             // The price of closing the position
    var canOpen = true;             // Mark of opening position

    if (tradeType == ORDER_TYPE_BUY) {                         
        reversePrice = _N(holdPrice * (1 - StopLoss), 4);     // Stop loss price
        coverPrice = _N(holdPrice * (1 + StopProfit), 4);     // Stop profit price
    } else {
        reversePrice = _N(holdPrice * (1 + StopLoss), 4);
        coverPrice = _N(holdPrice * (1 - StopProfit), 4);
    }

    var coverId = null;
    var msg = "Position price" + holdPrice + "stop loss price:" + reversePrice;

    for (var i = 0; i < 10; i++) {               // It controls up to 10 orders
        if (coverId) {                           // The order ID is empty, and the break is not triggered. Continue the loop until 10 times
            break;
        }
        if (tradeType == ORDER_TYPE_BUY) {       // Place an order according to the direction and place a closing position order, i.e. an order to stop profit
            exchange.SetDirection("closebuy");
            coverId = exchange.Sell(coverPrice, holdAmount, msg);
        } else {
            exchange.SetDirection("closesell");
            coverId = exchange.Buy(coverPrice, holdAmount, msg);
        }

        Sleep(Interval);
    }

    if (!coverId) {                // Failed to place an order for 10 times and threw an error, the strategy stopped
        StripOrders(exchange);     // Cancel all the pending orders
        Log("Failed to place an order", "@")        // Add push alerts
        throw "Failed to place an order";           // Throw an error to stop the robot
    }


    while (true) {                 // Enter the loop of detecting the backhand
        Sleep(Interval);           
        var ticker = GetTicker(exchange);                                // Get the latest quotation
        if ((tradeType == ORDER_TYPE_BUY && ticker.Last < reversePrice) || (tradeType == ORDER_TYPE_SELL && ticker.Last > reversePrice)) {   // Detect trigger stop loss and backhand
            StripOrders(exchange);                                       // Cancel all the pending orders
            var coverAmount = coverFutures(exchange, tradeType);         // Closinng all the positions
            if (_Failed >= MaxLoss) {                                    // If the maximum stop loss times (backhand times) are exceeded, skip out of the loop and start again
                Counter.f++;                                             // Record a failure
                Log("Exceed  the maximum number of failures", MaxLoss);
                break;                                                   // Skip out of the loop
            }
            var reverseAmount = _N(coverAmount * ReverseRate, 4);        // Double the trading volume according to the amount of closed positions

            var account = GetAccount(exchange, true);                    // Update the account information. At this time, no assets can be frozen
            // Check whether the account assets are sufficient. If not, skip out of the loop and start again, as _ Failed >= MaxLoss
            if (_N(account.Stocks * MarginLevel, 4) < reverseAmount) {   // Check whether the assets are sufficient
                Log("Open a backhand position if there is no currencies, it needs to be opened: ", reverseAmount, "amount, only", account.Stocks, "currencies");
                Counter.f++;
                break;
            }
            var reverseType = tradeType;                                 // Record the reverse operation type. The default is forward position
            if (ReverseMode == 0) {                                      // The backhand mode affects the adjustment, that is, if the parameter is set to the reverse position, adjust here
                reverseType = tradeType == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; // Reverse the position means that if you have a long position just now, you shall go short at this time. If you have a short position just now, you shall go long at this time
            }
            var pos = Trade(exchange, reverseType, reverseAmount, OpMode, SlidePrice, MaxSpace, Interval);    // Double the operation of placing an order
            if (pos) {                                                                                        // Detect the positions after trading logic execution
                Log(reverseType == ORDER_TYPE_BUY ? "long position" : "short position", "Complete it by doubling the opening position");
            }
            return pos;                                                                                       // Return to the position structure
        } else {                                          // Logic executed when no backhand is triggered
            var orders = _C(exchange.GetOrders);          // When the stop-profit order is filled, the number of wins recorded will be increased by 1
            if (orders.length == 0) {
                Counter.s++;
                var account = GetAccount(exchange, true); // Update account assets
                LogProfit(account.Stocks, account);       // Print account assets
                break;
            }
        }
    }

    // If it is not returned normally in the while loop, return to null. For example, if the profit stop is successful, the number of failures is exceeded, and the asset is insufficient
    return null;
}

function onexit() {          // When the robot stops, execute the round-off function onexit
    StripOrders(exchange);   // Cancel all the pending orders
    Log("Exit");
}

function main() {
    if (exchange.GetName().indexOf("Futures") == -1) {    // Check whether the first exchange object currently added is a futures exchange
        throw "Only futures are supported, but not spots for now";
    }
    // EnableLogLocal(SaveLocal);
    if (exchange.GetRate() != 1) {                        // Do not enable exchange rate conversion
        Log("Exchange rate conversion disabled");
        exchange.SetRate(1);
    }

    StopProfit /= 100;                                    // The parameter is processed as a decimal number. If StopProfit is 1, it means that stop profit when it is 1%. Recalculate the assignment. The value of StopProfit is 0.01, that is, 1%
    StopLoss /= 100;                                      // Stop loss (backhand), same as above

    var eName = exchange.GetName();
    if (eName == "Futures_CTP") {                         // Detect whether the first exchange object added is a commodity futures. If so, throw an error message to stop the robot
        throw "Only digital currency futures are supported for now"
    }
    exchange.SetContractType(Symbol);                     // Set the digital currency contract code, i.e. the contract to be traded and operated
    exchange.SetMarginLevel(MarginLevel);                 // Set leverage
    Interval *= 1000;                                     // The polling interval parameter is converted from seconds to milliseconds
    SetErrorFilter("502:|503:|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|EOF");    // Set the type of error to be blocked
    StripOrders(exchange);                                // Cancel all the pending orders
    OrgAccount = GetAccount(exchange, true);              // Get the current account information
    LogStatus("start successfully");                                 // Update status bar information
    var pos = null;                                       // Initialize the local variable pos to null in the main function, which is used to record the position data structure
    var positions = GetPosition(exchange);                // Get the current positions. Call the encapsulated GetPosition without the orderType parameter to get all positions. Note that the API interface exchange.GetPosition is not called
    if (positions.length == 1) {                          // If there is a position at the beginning, assign a value to the pos variable
        pos = positions[0];
        Log("A position was found, and the progress has been restored automatically");
    } else if (positions.length > 1) {                    // When there are multiple positions, the strategy cannot be operated and the strategy throws an error to stop the robot
        throw "More than 1 position was found";
    }
    while (true) {                                        // Strategy main loop
        pos = loop(pos);                                  // The main function loop of the trading logic is executed, and pos is used as a parameter, and the new position data structure is returned
        if (!pos) {                                       // This condition is triggered and null is returned. For example, if the profit is stopped successfully, the number of failures is exceeded, and the asset is insufficient
            _Failed = 0;                                  // Reset the time of stop loss to 0 and start again
        } else {
            _Failed++;                                    // Accumulated time of stop loss
        }
        Sleep(Interval);
    }
}
```
### Strategy logic
After reading the strategy code, we can find that the strategy logic is not complicated and there are not many codes, but the design is ingenious and it can be used for reference in many places.
The main function of the strategy trading logic is the loop function, which is called repeatedly in the main loop of the main function. When the loop function starts executing, it places an order position first, then place an oder to stop profit and waits for the stop-profit order to be filled. After that, it enters the detection state and detects the two items.

  - Detect whether the pending stop-profit order is fullfilled. If so, that is, profit, exit the detection loop, reset the logic, and start again.
  - Detect whether to trigger the stop loss (backhand), trigger the stop loss, that is, cancel all the pending orders, close the position, and then set whether to backhand forward position or backhand reverse position to double the backhand order according to the parameter. Generate position, continue to hang out the stop-profit order, and enter the detection state again (monitor the stop-profit and backhand).

The strategy logic is simply described, but there are still some other details, such as the setting of the maximum backhand times, the detection of the available account assets, and the processing of the maximum number of order failures of 10 times.
Some functions in the strategy are designed to behave differently according to different parameters, such as StripOrders function, GetAccount function, GetPosition function. These functions have different behaviors according to the difference of parameter passed in. In this way, the code is well reused, code redundancy is avoided, and the strategy design is concise and easy to understand.

Original strategy: https://www.fmz.com/strategy/3648

Backhand doubling has certain risks, especially in the futures, the strategy is only for learning purpose, caution in the real bot, please feel free to leave your comments and we can have a discussion.

From: https://blog.mathquant.com/2022/11/10/detailed-explanation-of-futures-backhand-doubling-algorithm-strategy-notes.html