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 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 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.
}

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
}
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
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)
}
// 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.
/* 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';
}
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 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++;
}
// 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
// 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);
}
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)*/
(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.
// 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.*/
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

// 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
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