Multi-platform Hedging Stabilization Arbitrage V2.1 (Annotation Edition)

Author: ruby, Created: 2018-08-27 14:16:49, Updated:

The hedging strategy is a relatively risky and stable type of strategy. Hedging is to achieve the profit by trading in the different markets at the same time, “moving” the currency to the exchange with low price and “flowing” the money to the exchange with high price.

Program logic flow img Strategic interpretation

This strategy can achieve hedging transactions on multiple digital currency spot platforms with simple code and basic hedging capabilities. Since this version is the basic teaching version, the space for optimization is large. For the new users and new developers who are learning strategy writing, it can provide a good example and some skills. It is very helpful to master the techniques of quantitative strategy writing.

The strategy can be put in real market, but because it is the most basic version of teaching, the scalability is still very large, and students who have mastered the idea can also try to reconstruct the strategy.

var initState;
var isBalance = true;
var feeCache = new Array();
var feeTimeout = optFeeTimeout * 60000;
var lastProfit = 0;                       // global variable records last profit
var lastAvgPrice = 0;
var lastSpread = 0;
var lastOpAmount = 0;
function adjustFloat(v) {                 // A custom function that processes the data, which can process and return the parameter v, retained to three decimal places(floor down rounded)
    return Math.floor(v*1000)/1000;       /*Multiply by 1000 to move the decimal point to the left by three digits, take the integer down, round off all fractional parts, then divide by 1000, 
                                          and move the decimal point to the right by three digits, that is, keep three decimals.*/ 
}

function isPriceNormal(v) {               /*Determine if the price is normal,StopPriceL is the down limit value, StopPriceH is the daily limit value, 
                                            and returns true if it's in this interval, return false if the interval is exceeded*/
    return (v >= StopPriceL) && (v <= StopPriceH);  // in this interval
}

function stripTicker(t) {                           // According to the parameter t, format output the data about t.
    return 'Buy: ' + adjustFloat(t.Buy) + ' Sell: ' + adjustFloat(t.Sell);
}

function updateStatePrice(state) {        // Update price
    var now = (new Date()).getTime();     // Record current timestamp
    for (var i = 0; i < state.details.length; i++) {    // Check through state.details according to argument state (the return value of the getExchangesState function)
        var ticker = null;                              // Declare a variable ticker
        var key = state.details[i].exchange.GetName() + state.details[i].exchange.GetCurrency();  //Get the element of the current index i, use the exchange object referenced in it, call GetName & GetCurrency function
                                                                                                  // assign exchange name and currency to key
        var fee = null;                                                                           // Declare a variable fee
        while (!(ticker = state.details[i].exchange.GetTicker())) {                               // Use the current exchange object to call the GetTicker function to get the quote, if fail, execute the loop
            Sleep(Interval);                                                                      // Execute the Sleep function, pause the number of milliseconds set by Interval
        }

        if (key in feeCache) {                                                            // Query in feeCache if find key
            var v = feeCache[key];                                                        // Take out the variable value named key
            if ((now - v.time) > feeTimeout) {                                           // According to the difference between the recording time of the market and the now, if it is greater than the fee update period
                delete feeCache[key];                                                    // Delete expired rate data
            } else {
                fee = v.fee;                                                                      // If it is not greater than the update period, remove the v.fee to assign to the fee.
            }
        }
        if (!fee) {                                                                               // If the fee is not found and it's still initially null, then if is triggered
            while (!(fee = state.details[i].exchange.GetFee())) {                                 // Call the current exchange object GetFee function to get the rate
                Sleep(Interval);
            }
            feeCache[key] = {fee: fee, time: now};                                                // Store the obtained fee and current timestamp in the rate cache data structure feeCache
        }
        // Buy-=fee Sell+=fee
        state.details[i].ticker = {Buy: ticker.Buy * (1-(fee.Sell/100)), Sell: ticker.Sell * (1+(fee.Buy/100))};  //By processing the market price, get the price after the fee is excluded to calculate the difference.
        state.details[i].realTicker = ticker;                                                                     //Actual market price
        state.details[i].fee = fee;                                                                               //Rate
    }
}

function getProfit(stateInit, stateNow, coinPrice) {                // Get the current function of calculating profit and loss
    var netNow = stateNow.allBalance + (stateNow.allStocks * coinPrice);          // Calculate the total asset market value of the current account
    var netInit =  stateInit.allBalance + (stateInit.allStocks * coinPrice);      // Calculate the total asset market value of the initial account
    return adjustFloat(netNow - netInit);                                         // The current minus the initial is the profit and loss, return this profit and loss
}

function getExchangesState() {                                      // Function that gets exchange state
    var allStocks = 0;                                              // all stocks
    var allBalance = 0;                                             // all balance
    var minStock = 0;                                               // Minimum transaction stock
    var details = [];                                               // Details, the array that stores details
    for (var i = 0; i < exchanges.length; i++) {                    // check through arrays of exchange object
        var account = null;                                         // Every loop declares an account variable
        while (!(account = exchanges[i].GetAccount())) {            /*Using the exchange object of the current index value in the exchanges array, call its member function to get the account 
                                                                      information of the current exchange. Return to the account variable, if !account is true then always obtained. */
            Sleep(Interval);                                        /* If !account is true, that is, the account acquisition fails. Call the Sleep function to pause the milliseconds set by the Interval,
                                                                       and then loop again until a valid account information is obtained. */
        }
        allStocks += account.Stocks + account.FrozenStocks;         // Accumulate all account stocks
        allBalance += account.Balance + account.FrozenBalance;      // Accumulate all account balance
        minStock = Math.max(minStock, exchanges[i].GetMinStock());  // Set the minimum volume minStock to be the maximum value of the minimum volume of all exchanges.
        details.push({exchange: exchanges[i], account: account});   // details Combine each exchange object and account information into an object and push it into an array of details
    }
    return {allStocks: adjustFloat(allStocks), allBalance: adjustFloat(allBalance), minStock: minStock, details: details};   /*Return all stocks, all balance of all exchanges,
                                                                                                                               all the maximum value of the minimum volume and arrays of details*/
}

function cancelAllOrders() {                                        // Cancel all orders function
    for (var i = 0; i < exchanges.length; i++) {                    // Check through the array of exchange objects (that is, the exchanges added when the new robot is created, the corresponding object)
        while (true) {                                              // Every time you enter a while loop while checking through
            var orders = null;                                      // Declare a orders variable to receive unfinished orders returned by the API function GetOrders
            while (!(orders = exchanges[i].GetOrders())) {          /* Use the while loop to detect if the API function GetOrders returned valid data (ie if the GetOrders 
                                                                       returns null it will always execute the while loop and re-detect)*/
                                                            // Exchanges[i] is the current loop's exchange object, and we get the unfinished orders by calling the API GetOrders (the member function of exchanges[i]). 
                Sleep(Interval);                            // According to the parameter Interval, the Sleep function makes the program pause the desighed milliseconds(1000 milliseconds = 1 second)
            }

            if (orders.length == 0) {                      // If the unfinished order array obtained is non-null, meaning it passes the while loop above, but orders.length is equal to 0 (empty array, no pending order)  
                break;                                    // Execute break to jump out of the current while loop (ie there are no orders to cancel)
            }

            for (var j = 0; j < orders.length; j++) {               /*Check through the orders array, call the API function CancelOrder to cancel the pending orders 
                                                                      one by one according to the pending order ID CancelOrder cancels the pending order.*/
                exchanges[i].CancelOrder(orders[j].Id, orders[j]);
            }
        }
    }
}

function balanceAccounts() {          // Balance the exchanges, accounts and balance
    // already balance
    if (isBalance) {                  // If isBalance is true, that is, balanced, then there is no need to balance, return immediately
        return;
    }

    cancelAllOrders();                // Cancel the pending orders of all exchanges before balancing.

    var state = getExchangesState();  // Call the getExchangesState function to get all exchanges state (including account information)
    var diff = state.allStocks - initState.allStocks;      /* Calculate the difference between the total number of stocks in the currently acquired exchange state 
                                                              and the total stocks in the initial state(ie, the current number-the initial number)*/
    var adjustDiff = adjustFloat(Math.abs(diff));          // First call Math.abs to calculate the absolute value of diff, then call the custom function adjustFloat to retain 3 decimal places. 
    if (adjustDiff < state.minStock) {                     /* If the processed total currency difference data is less than the data minStock that satisfies the minimum volume of 
                                                              all exchanges, namely the balance condition is not met.*/
        isBalance = true;                                  // Set isBalance to be true , namely the balance state
    } else {                                               // If adjustDiff >= state.minStock, then:
        Log('Total number of initial coins:', initState.allStocks, 'Total amount of coins: ', state.allStocks, 'difference:', adjustDiff);
        // Output information to be balanced.
        // other ways, diff is 0.012, bug A only has 0.006 B only has 0.006, all less then minstock
        // we try to statistical orders count to recognition this situation
        updateStatePrice(state);                           // Update and get the prices of each exchange
        var details = state.details;                       // Remove state.details and assign it to details
        var ordersCount = 0;                               // Declare a variable to record the number of orders
        if (diff > 0) {                                    // Determine if the currency difference is greater than 0. If it is, sell extra coins.
            var attr = 'Sell';                             // The default setting is that the ticker attribute to be acquired is Sell, that is, the price is the latest selling price
            if (UseMarketOrder) {                          // If it's set as UseMarketOrder, then set the property that ticker wants to get as Buy(by assigning atrr)
                attr = 'Buy';
            }
            // Sell adjustDiff, sort by price high to low
            details.sort(function(a, b) {return b.ticker[attr] - a.ticker[attr];}); /* If return is greater than 0, then b is before, a is after, if return is less than 0, then a is before b, 
                                                                                       and the elements in the array are sorted according to the occurrence. */
                                                                                    // Here b - a is used to sort the details array from high to low.
            for (var i = 0; i < details.length && adjustDiff >= state.minStock; i++) {     // Check through the details array
                if (isPriceNormal(details[i].ticker[attr]) && (details[i].account.Stocks >= state.minStock)) {    /* Determine whether the price is abnormal, and whether the current account
                                                                                                                     stocks is greater than the minimum stocks.
                    var orderAmount = adjustFloat(Math.min(AmountOnce, adjustDiff, details[i].account.Stocks));
                    /* Take the minimum of those three: AmountOnce, the currency difference, and the current balance in exchange, and assign to orderAmount. 
                       Because the details have been sorted, the beginning is the highest price, which is selling from the exchange at the highest price.*/
                    var orderPrice = details[i].realTicker[attr] - SlidePrice;               // According to the actual market price (using Sell or buy depends on the setting of UseMarketOrder)
                                                                                             // Because it is to sell the order price, minus the price of SlidePrice. Set the order price
                    if ((orderPrice * orderAmount) < details[i].exchange.GetMinPrice()) {    /* Determine if the minimum transaction amount of the current indexed exchange is sufficient for the 
                                                                                                order amount to be placed this time.*/
                        continue;                                                            // If it is not sufficient, skip this and execute next index.
                    }
                    ordersCount++;                                                           // orderCount plus 1
                    if (details[i].exchange.Sell(orderPrice, orderAmount, stripTicker(details[i].ticker))) {   /* Place orders at the price and volume specified in the above procedure,
                                                                                                                  and output the market data processed after the exclusion of the commission factor. */
                        adjustDiff = adjustFloat(adjustDiff - orderAmount);                  // If the order API returns the order ID, the unbalanced amount is updated according to the current order quantity.
                    }
                    // only operate one platform                                             // Operate balance only on one platform, so the following break jumps out of the for loop
                    break;
                }
            }
        } else {                                           // If the difference is less than 0, that is, the currency is not enough and needs replenishment.
            var attr = 'Buy';                              // Ibid.
            if (UseMarketOrder) {
                attr = 'Sell';
            }
            // Buy adjustDiff, sort by sell-price low to high
            details.sort(function(a, b) {return a.ticker[attr] - b.ticker[attr];});           // The price is from low to high, because the replenishment starts from the exchange at the lowest price .
            for (var i = 0; i < details.length && adjustDiff >= state.minStock; i++) {        // cycle starts from the lowest pice
                if (isPriceNormal(details[i].ticker[attr])) {                                 // If the price is normal, then execute the inside code of if{}
                    var canRealBuy = adjustFloat(details[i].account.Balance / (details[i].ticker[attr] + SlidePrice));
                    var needRealBuy = Math.min(AmountOnce, adjustDiff, canRealBuy);
                    var orderAmount = adjustFloat(needRealBuy * (1+(details[i].fee.Buy/100)));  // Since the fee is deducted from the number of coins, the fee is included.
                    var orderPrice = details[i].realTicker[attr] + SlidePrice;
                    if ((orderAmount < details[i].exchange.GetMinStock()) ||
                        ((orderPrice * orderAmount) < details[i].exchange.GetMinPrice())) {
                        continue;
                    }
                    ordersCount++;
                    if (details[i].exchange.Buy(orderPrice, orderAmount, stripTicker(details[i].ticker))) {
                        adjustDiff = adjustFloat(adjustDiff - needRealBuy);
                    }
                    // only operate one platform
                    break;
                }
            }
        }
        isBalance = (ordersCount == 0);                                                         // If ordersCount is 0, it's true, means it's balanced.
    }

    if (isBalance) {
        var currentProfit = getProfit(initState, state, lastAvgPrice);                          // Calculate current profit
        LogProfit(currentProfit, "Spread: ", adjustFloat((currentProfit - lastProfit) / lastOpAmount), "Balance: ", adjustFloat(state.allBalance), "Stocks: ", adjustFloat(state.allStocks));
        // Print current earnings information
        if (StopWhenLoss && currentProfit < 0 && Math.abs(currentProfit) > MaxLoss) {           // Stop code block when loss exceeds the max.
            Log('Transaction loss exceeds maximum, the program cancels all orders and exits.');
            cancelAllOrders();                                                                  // Cancel all pending orders
            if (SMSAPI.length > 10 && SMSAPI.indexOf('http') == 0) {                            // Notify the code block by SMS
                HttpQuery(SMSAPI);
                Log('SMS notification');
            }
            throw 'stopped';                                                                      // abnormal situation is throwed, stop the program.
        }
        lastProfit = currentProfit;                                                             // use the current profit to update the last profit record
    }
}

function onTick() {                  // Main cycle
    if (!isBalance) {                // Determine whether the global variable isBalance is false (representing imbalance), !isBalance is true, and execute the code inside the if statement.
        balanceAccounts();           // Execute balanceAccounts() function when it's not balanced.
        return;                      // Return after execution. Continue to execute onTick next time
    }

    var state = getExchangesState(); // get all exchanges' state
    // We also need details of price
    updateStatePrice(state);         // Update price, calculate the hedge price value that excludes the impact of the fee.

    var details = state.details;     // Take the details value out of the state
    var maxPair = null;              // the max pair
    var minPair = null;              // the min pair
    for (var i = 0; i < details.length; i++) {      // Check through the details array
        var sellOrderPrice = details[i].account.Stocks * (details[i].realTicker.Buy - SlidePrice);    /* Calculate the total amount of the current account currency sold 
                                                                                                         (the latest buying price of opponent minus the slide price)*/
        if (((!maxPair) || (details[i].ticker.Buy > maxPair.ticker.Buy)) && (details[i].account.Stocks >= state.minStock) &&
            (sellOrderPrice > details[i].exchange.GetMinPrice())) { /* First, see if maxPair is null. If it is not, the price after excluding the fee 
                                                                       factor is greater than the latest buying price of the market data in maxPair.*/
                                                                    /* The remaining condition is to satisfy the minimum tradable quantity, and to satisfy the 
                                                                       minimum transaction amount, the following conditions are fulfilled.*/
            details[i].canSell = details[i].account.Stocks;         //Add a property canSell to the elements of the details array of the current index. Assign the account stocks to it.
            maxPair = details[i];                                   // Reference the current details array element to maxPair for the next comparison of the for loop to get the maximum price.
        }

        var canBuy = adjustFloat(details[i].account.Balance / (details[i].realTicker.Sell + SlidePrice));   // calculate the coins that can be bought in exchange account
        var buyOrderPrice = canBuy * (details[i].realTicker.Sell + SlidePrice);                             // Calculate the amount of the order
        if (((!minPair) || (details[i].ticker.Sell < minPair.ticker.Sell)) && (canBuy >= state.minStock) && // Same to looking for the maxPair when to sell, here to find the minpair.
            (buyOrderPrice > details[i].exchange.GetMinPrice())) {
            details[i].canBuy = canBuy;                             // Increase canBuy property record
            // how much coins we real got with fee                  // The following is to calculate the coins after fee is deducted (the fee charged for purchasing is deducted from coins)
            details[i].realBuy = adjustFloat(details[i].account.Balance / (details[i].ticker.Sell + SlidePrice));   // use the price with fee has been deducted to calculate the real coins
            minPair = details[i];                                   // what mets the condition is recorded as minPair
        }
    }

    if ((!maxPair) || (!minPair) || ((maxPair.ticker.Buy - minPair.ticker.Sell) < MaxDiff) ||       /* Check whether the hedging condition is not met according to the 
                                                                                                       lowest and highest prices of all the exchanges listed above.*/
    !isPriceNormal(maxPair.ticker.Buy) || !isPriceNormal(minPair.ticker.Sell)) {
        return;                                                                                    // Return if not met
    }

    // filter invalid price
    if (minPair.realTicker.Sell <= minPair.realTicker.Buy || maxPair.realTicker.Sell <= maxPair.realTicker.Buy) {   /* Filtering invalid prices, such as the latest selling price is unlikely 
                                                                                                                       to be less than or equal to the latest buying price.*/
        return;
    }

    // what a fuck...
    if (maxPair.exchange.GetName() == minPair.exchange.GetName()) {                                   // The data is abnormal, the lowest and highest price are the same exchange.
        return;
    }

    lastAvgPrice = adjustFloat((minPair.realTicker.Buy + maxPair.realTicker.Buy) / 2);                // Record the average of the highest and lowest prices
    lastSpread = adjustFloat((maxPair.realTicker.Sell - minPair.realTicker.Buy) / 2);                 // Record the spread of selling and buying

    // compute amount                                                                                 // Calculate the order quantity
    var amount = Math.min(AmountOnce, maxPair.canSell, minPair.realBuy);                              // the minimum value of three is used as the order quantity.
    lastOpAmount = amount;                                                                            // Record the order quantity to the global variable
    var hedgePrice = adjustFloat((maxPair.realTicker.Buy - minPair.realTicker.Sell) / Math.max(SlideRatio, 2))  // Calculate the hedge price based on the slip factor
    if (minPair.exchange.Buy(minPair.realTicker.Sell + hedgePrice, amount * (1+(minPair.fee.Buy/100)), stripTicker(minPair.realTicker))) { // buy first
        maxPair.exchange.Sell(maxPair.realTicker.Buy - hedgePrice, amount, stripTicker(maxPair.realTicker));                               // then sell
    }

    isBalance = false;                                                                                // set it as unbalanced
}

function main() {                                         // the main function of strategy
    if (exchanges.length < 2) {                           /*First, determine the number of exchange objects added by the strategy. Exchanges is an array of 
                                                             exchange objects. If exchanges.length is less than 2, execute the code inside the {}. */
        throw "It needs at least two exchanges to complete the hedge.";              // an error is throwed, the program is stopped.
    }

    TickInterval = Math.max(TickInterval, 50);            /*TickInterval is a parameter on the interface that is used to detect the frequency. Use the mathematical 
                                                            object Math of JS and call the function max to limit the minimum value of TickInterval to 50(in milliseconds)*/
    Interval = Math.max(Interval, 50);                    // Ibid.

    cancelAllOrders();                                    // There can be no pending orders at the very beginning. So all pending orders will be checked and cancelled.

    initState = getExchangesState();                      // Call the custom getExchangesState function to get information about all exchanges, and assign it to initState.
    if (initState.allStocks == 0) {                       // If the sum of all exchange currencies is 0, an error is thrown.
        throw "The sum of all stocks is 0. Open a position on any exchange to complete the hedging.";
    }
    if (initState.allBalance == 0) {                      // If all exchanges balance is 0, throw an error.
        throw "The sum of all balance is 0 and hedge cannot continue";
    }

    for (var i = 0; i < initState.details.length; i++) {  // Check through the array of details in the acquired exchange state.
        var e = initState.details[i];                     // Assign the current indexed exchange information to e
        Log(e.exchange.GetName(), e.exchange.GetCurrency(), e.account);   /* Call the member functions of the exchange object referenced in e, GetName , GetCurrency,
                                                                             and e.account stored in the current exchange information, then output them with Log. */
    }

    Log("ALL: Balance: ", initState.allBalance, "Stocks: ", initState.allStocks, "Ver:", Version());  // Print log to output all balance, all stocks and version of docker.


    while (true) {                                        // while cycle
        onTick();                                         // Execute the main logic functionv onTick 
        Sleep(parseInt(TickInterval));
    }
}

More

小小梦 GOOD!