The grid trading strategy
(www.fmz.com)
The basic idea of grid trading is very straightforward. Instead of placing one trade, we place multiple trades forming a grid pattern. Usually these are entered as “stop” or “limit” orders around the current price level — but not always. I’ll explain this in more detail below, but that’s the basic idea.

What is grid trading and how does it work?
Grid trading is a play on market volatility. There are two reasons why it’s favored by traders. The first is that it doesn’t “require” you to have a definitive prediction on the market direction.

The second is that it works well in volatile markets, where there isn’t a clear trend — these conditions are very common in the currency markets

Grid trading is a type of technical analysis trading that is based on movement within specific grid patterns. Grid trading is popular in foreign exchange trading. Overall the technique seeks to capitalize on normal price volatility in markets by placing buy and sell orders at certain regular intervals above and below a predefined base price. Such buy and sell orders, generally spaced at 10 or 15 units of intervals, create a trading grid.

Grid can customize direction

Basic trading operation: buy first and then sell.

The grid will start to send buying order at the price that below the first price, which is the price follow by the first price (second latest buying price, third latest buying price…and so on). Each buying order is separated by the “price interval” parameter. The number of pending orders is “single quantity”, and will send the order total until the “total quantity” is filled.

After any buying order is completed, the program will be on the basis of the buying price, add the price of the “price difference” parameter to the sell price, after the order has been sold, and then re-start the progress of this grid strategy (checking, place order, wait until it executed, sell)

Selling short first and then buy to cover: the operation is just the opposite

The biggest risk of this strategy is when the market trend is unilateral moving, and the price fluctuations are exceeding the grid.

The following code has made the grid with automatic stop loss and movement function.

Comments:

The strategy uses a virtual pending order design, which provides a great deal of processing for the exchange to limit the number of pending orders, and solves the problem flexibly.

The grid logic is flexible in design and clever in structure.

Profit and loss calculation, each numerical statistical algorithm can be used for reference, and each condition detection design is rigorous. (for minimize the possibility of BUG)

The source code is very worth learning.

For more information,please see:

https://www.fmz.com/strategy/112633

Source Code:

// Grid can customize direction
// Basic trading operation: buy first and then sell.
// The grid will start to send buying order at the price that below the first price, which is the price follow
// by the first price (second latest buying price, third latest buying price…and so on). Each buying order is separated by
// the "price interval" parameter. The number of pending orders is "single quantity", and will send the order total until
// the "total quantity" is filled.
// After any buying order is completed, the program will be on the basis of the buying price, add the price of the "price
// difference" parameter to the sell price, after the order has been sold, and then re-start the progress of this
// grid strategy (checking, place order, wait until it executed, sell)
// Selling short first and then buy to cover: the operation is just the opposite
// The biggest risk of this strategy is when the market trend is unilateral moving, and the price fluctuations are exceeding the grid.
// The following code has made the grid with automatic stop loss and movement function.
// Comments:
// The strategy uses a virtual pending order design, which provides a great deal of processing for the exchange to limit the number
// of pending orders, and solves the problem flexibly.
// The grid logic is flexible in design and clever in structure.
// Profit and loss calculation, each numerical statistical algorithm can be used for reference, and each condition detection design
// is rigorous. (for minimize the possibility of BUG)
// The source code is very worth learning.
// Source Code:
/* Interface parameters (shown as global variables in the code)
OpType Grid Direction Drop-down box (selected) Buy first then sell | Sell first then buy
FirstPriceAuto initial price automatic boolean (true/false) true
FirstPrice@!FirstPriceAuto initial price numerical (number) 100
AllNum total number numerical (number) 10
PriceGrid Price Interval numerical (number) 1
PriceDiff spread numerical (number) 2
AmountType order size drop-down box (selected) buy and sell the same amount | custom amount
AmountOnce@AmountType==0 Single transaction quantity numerical (number) 0.1
BAmountOnce@AmountType==1 Buying Order Size numerical (number) 0.1
SAmountOnce@AmountType==1 Selling order size numerical (number) 0.1
AmountCoefficient@AmountType==0 Quantity difference String (string) *1
AmountDot The decimal point numerical (number) 3
EnableProtectDiff Turn on spread protection Boolean (true/false) false
ProtectDiff@EnableProtectDiff Entry spread Price Protection numerical (number) 20
CancelAllWS stop cancels all pending orders Boolean (true/false) true
CheckInterval polling interval number numerical (number) 2000
Interval failure retry interval numerical (number) 1300
RestoreProfit restores last profit Boolean (true/false) false
LastProfit@RestoreProfit Last Profit numerical (number) 0
ProfitAsOrg@RestoreProfit Last profit counted as average price Boolean (true/false) false
EnableAccountCheck enable balance verification Boolean (true/false) true
EnableStopLoss@EnableAccountCheck open Stop Loss Boolean (true/false) false
StopLoss@EnableStopLoss maximum floating loss numerical (number) 100
StopLossMode@EnableStopLoss Post-stop loss operation Drop-down box (selected) Recycle and exit | Recycle and re-cast
EnableStopWin@EnableAccountCheck Turn on Take Profit Boolean (true/false) false
StopWin@EnableStopWin Maximum floating profit Number type (number) 120
StopWinMode@EnableStopWin post-take profit operation drop-down box (selected) Recycle and exit | Recycle and re-cast
AutoMove@EnableAccountCheck auto Move Boolean (true/false) false
MaxDistance@AutoMove maximum distance numerical (number) 20
MaxIdle@AutoMove maximum idle (seconds) numerical (number) 7200
EnableDynamic Turns on dynamic pending orders Boolean (true/false) false
DynamicMax@EnableDynamic order expiration distance Number (number) 30
ResetData clears all data at startup Boolean (true/false) true
Precision price decimal length numerical (number) 5
*/
function hasOrder(orders, orderId) {                           // Check if there is an order with order ID in parameter orders
for (var i = 0; i < orders.length; i++) { // Traverse orders to check if there are same ids, if there are then return true
if (orders[i].Id == orderId) {
return true;
}
}
return false; // All traversed, no trigger if means haven't found the order with the ID orderId, return false
}

function cancelPending() {                                     // Cancel all pending order functions
var ret = false; // Set return success tag variable
while (true) { // while loop
if (ret) { // If ret is true then Sleep for a certain time
Sleep(Interval);
}
var orders = _C(exchange.GetOrders); // Call the API to get the order information that the exchange did not executed.
if (orders.length == 0) { // If an empty array is returned, the exchange has no unexecuted orders.
break; // Jump out of the while loop
}
        for (var j = 0; j < orders.length; j++) {              // Traverse the unfinished order array and use orders[j].Id one by one to cancel the order based on the index j.
exchange.CancelOrder(orders[j].Id, orders[j]);
ret = true; // Once there is a cancel operation, ret is assigned a value of true. Used to trigger above Sleep, wait for re-exchange.GetOrders detection
}
}
return ret; // return ret
}
function valuesToString(values, pos) {                      // Value converted to a string
var result = ''; // Declare an empty string result for return
if (typeof(pos) === 'undefined') { // If the pos parameter is not passed, assign pos a value of 0.
pos = 0;
}
for (var i = pos; i < values.length; i++) { // Process values array according to the passed pos
if (i > pos) { // In addition to the first loop, add ' ' a space after the result string
result += ' ';
}
if (values[i] === null) { // If values (function argument list array) the current index's element is null then result adds 'null' string
result += 'null';
} else if (typeof(values[i]) == 'undefined') { // If it is undefined, add 'undefined'
result += 'undefined';
} else { // Remaining type do switch detection separately
switch (values[i].constructor.name) { // Check the name property of the constructor of values[i], which is the type name
case 'Date':
case 'Number':
case 'String':
case 'Function':
result += values[i].toString(); // If it is a date type, a numeric type, a string type, or a function type, call its toString function and convert it to a string, then add
break;
default:
result += JSON.stringify(values[i]); // In other cases, use the JSON.stringify function to convert to a JSON string. Add to result
break;
}
}
}
return result; // return result
}
function Trader() {                                                 // Trader function, using closures.
var vId = 0; // Order increment ID
var orderBooks = []; // Order book
var hisBooks = []; // Historical order book
var orderBooksLen = 0; // Order book length
this.Buy = function(price, amount, extra) { // Buying function, parameters: price, quantity, extended information
if (typeof(extra) === 'undefined') { // If the parameter extra is not passed in, ie typeof returns undefined
extra = ''; // Assign an empty string to extra
} else {
extra = valuesToString(arguments, 2); // The argument arguments passed when calling this.Buy function is passed to the valuesToString function.
}
vId++; //
var orderId = "V" + vId; //
orderBooks[orderId] = { // Add the attribute orderId to the order book array and initialize it with the constructed object.
Type: ORDER_TYPE_BUY, // Constructed Object Type Property: Type buy
Status: ORDER_STATE_PENDING, // State on hold
Id: 0, // orderID 0
Price: price, // price paramter price
Amount: amount, // Order quantity parameter amount
Extra: extra // Extended information A string processed by valuesToString
};
orderBooksLen++; // The length of the order book is increased by 1
return orderId; // Returns the orderId of the order constructed this time (non-exchange order ID, don't confuse.)
};
this.Sell = function(price, amount, extra) { // Basically similar to this.Buy, construct a sell order.
if (typeof(extra) === 'undefined') {
extra = '';
} else {
extra = valuesToString(arguments, 2);
}
vId++;
var orderId = "V" + vId;
orderBooks[orderId] = {
Type: ORDER_TYPE_SELL,
Status: ORDER_STATE_PENDING,
Id: 0,
Price: price,
Amount: amount,
Extra: extra
};
orderBooksLen++;
return orderId;
};
this.GetOrders = function() { // Get unfinished order information
var orders = _C(exchange.GetOrders); // Call API GetOrders to get unfinished order information Assigned to orders
for (orderId in orderBooks) { // Traversing the orderBooks in the Trader object
var order = orderBooks[orderId]; // Take out the order based on orderId
if (order.Status !== ORDER_STATE_PENDING) { // If the state of order is not equal to the suspended state, skip this loop
continue;
}
var found = false; // Initialize the found variable (marked if found) to true
for (var i = 0; i < orders.length; i++) { // Traversing data for unexecuted orders returned by the API
if (orders[i].Id == order.Id) { // When you find an order with the same order id in orderBooks, assign a value of true to find, which means find.
found = true;
break; // Jump out of the current loop
}
}
if (!found) { // If not found, push orderBooks[orderId] to orders.
orders.push(orderBooks[orderId]); // Why do you want to push like this?
}
}
return orders; // return orders
}
this.GetOrder = function(orderId) { // Get order
if (typeof(orderId) === 'number') { // If the passed argument orderId is a numeric type
return exchange.GetOrder(orderId); // Call the API GetOrder to get the order information based on the orderId and return.
}
if (typeof(hisBooks[orderId]) !== 'undefined') { // Typeof(hisBooks[orderId]) if not equal to undefined
return hisBooks[orderId]; // Return data in hisBooks with attribute as orderId
}
if (typeof(orderBooks[orderId]) !== 'undefined') { // As above, if there is a value of orderId in the orderBooks, this data is returned.
return orderBooks[orderId];
}
return null; // Return null if the above conditions are not met
};
this.Len = function() { // Returns the Trader's orderBookLen variable, which returns the order book length.
return orderBooksLen;
};
this.RealLen = function() { // Back In the order book Activate the order quantity.
var n = 0; // Initial count is 0
for (orderId in orderBooks) { // Traversing the order book
if (orderBooks[orderId].Id > 0) { // If the Id of the current order in the traversal is greater than 0, that is, 0 other than the initial time,
// indicating that the order has been placed, the order has been activated.
n++; // Cumulatively activated order
}
}
return n; // Returns the value of n, which returns the true order book length. (number of orders activated)
};
this.Poll = function(ticker, priceDiff) { //
var orders = _C(exchange.GetOrders); // Get all unexecuted orders
for (orderId in orderBooks) { // Traversing the order book
var order = orderBooks[orderId]; // Take the current order assign to order
if (order.Id > 0) { // If the order is active, ie order.Id is not 0 (already placed)
var found = false; // Variable found (mark found) is false
for (var i = 0; i < orders.length; i++) { // Find the same order number in the executed order information returned by the exchange
if (order.Id == orders[i].Id) { // If found, assign a value of true to find, which means it has been found.
found = true;
}
}
if (!found) { // If the current orderId represents an order that is not found in the order of the uncompleted order returned by the exchange.
order.Status = ORDER_STATE_CLOSED; // Updates the order corresponding to orderId in orderBooks (ie the current order variable) and updates
// the Status property to ORDER_STATE_CLOSED (ie closed)
hisBooks[orderId] = order; // The completed order is recorded in the historical order book, ie hisBooks, unified, and the unique order number orderId
delete(orderBooks[orderId]); // Delete the attribute of the order book named orderId value. (The completed order is deleted from it)
orderBooksLen--; // Order book length reduction
continue; // The following code skips the loop.
}
}
var diff = _N(order.Type == ORDER_TYPE_BUY ? (ticker.Buy - order.Price) : (order.Price - ticker.Sell));
// Diff is the difference between the planned opening price of the order in the current order book and the current real-time opening price.
            var pfn = order.Type == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell;   // Assign the corresponding API function reference to pfn according to the type of the order.
// That is, if the order type is a buy order, pfn is a reference to the exchange.Buy function, the same as the sell order.
            if (order.Id == 0 && diff <= priceDiff) {                                // If the order order in the order book is not activated (ie Id is equal to 0) and the current price is less than or 
// equal to the order plan price, the priceDiff passed in the parameter.
var realId = pfn(order.Price, order.Amount, order.Extra + "(distance: " + diff + (order.Type == ORDER_TYPE_BUY ? (" ask price: " + ticker.Buy) : (" bid price: " + ticker.Sell))+")");
// Execute order function, parameter passing price, quantity, order extension information + pending order distance + market data (ask price or bid price), return exchange order id
                if (typeof(realId) === 'number') {    // If the returned realId is a numeric type
order.Id = realId; // Assign the Id attribute of the current order order to the order book.
}
} else if (order.Id > 0 && diff > (priceDiff + 1)) { // If the order is active and the current distance is greater than the distance passed in by the parameter
var ok = true; // Declare a variable for tagging Initially set true
do { // Execute "do" first and then judge while
ok = true; // Ok assign true
exchange.CancelOrder(order.Id, "unnecessary" + (order.Type == ORDER_TYPE_BUY ? "buying" : "selling"), "placed order price:", order.Price, "volume:", order.Amount, ", distance:",
diff, order.Type == ORDER_TYPE_BUY ? ("ask price: " + ticker.Buy) : ("bid price: " + ticker.Sell));
// Cancel the pending order that is out of range. After canceling the order, print the current order information and the current distance diff.
                    Sleep(200);                                   // Wait 200 milliseconds
orders = _C(exchange.GetOrders); // Call the API to get an uncompleted order in the exchange.
for (var i = 0; i < orders.length; i++) { // Traverse these unfinished orders.
if (orders[i].Id == order.Id) { // If the cancelled order is found in the list of orders that have not been completed by the exchange
ok = false; // Assign ok this variable to false, that is, no cancellation is successful.
}
}
} while (!ok); // If ok is false, then !ok is true and while will continue to repeat the loop, continue to cancel the order,
// and check if the cancellation is successful.
order.Id = 0; // Assigning a value of 0 to order.Id means that the current order is inactive.
}
}
};
}
function balanceAccount(orgAccount, initAccount) {               // Balance Account Function Parameter Initial account information when the strategy is started
cancelPending(); // Call the custom function cancelPending() to cancel all pending orders.
var nowAccount = _C(exchange.GetAccount); // Declare a variable nowAccount to record the latest information about the account at the moment.
var slidePrice = 0.2; // Set the slip price when placing the order as 0.2
var ok = true; // Tag variable initially set true
while (true) { // while loop
var diff = _N(nowAccount.Stocks - initAccount.Stocks); // Calculate the difference between the current account and the initial account diff
if (Math.abs(diff) < exchange.GetMinStock()) { // If the absolute value of the currency difference is less than the minimum transaction volume of the exchange,
// the break jumps out of the loop and does not perform balancing operations.
break;
}
var depth = _C(exchange.GetDepth); // Get the exchange depth information Assign to the declared depth variable
var books = diff > 0 ? depth.Bids : depth.Asks; // According to the difference of the currency is greater than 0 or less than 0, extract the buy order array or
// sell order array in depth (equal to 0 will not be processed, it is break when it is judged to be less than GetMinStock)
// The difference between the coins is greater than 0 to sell the balance, so look at the buy order array,
// the difference between the coins is less than 0 is the opposite.
var n = 0; // Statement n initial is 0
var price = 0; // Statement price initial 0
for (var i = 0; i < books.length; i++) { // Traversing the buy or sell order array
n += books[i].Amount; // Accumulate Amount (order quantity) for each order based on the index i traversed
if (n >= Math.abs(diff)) { // If the cumulative order quantity n is greater than or equal to the currency difference, then:
price = books[i].Price; // Get the price of the current indexed order, assign it to price
break; // Jump out of the current for traversal cycle
}
}
var pfn = diff > 0 ? exchange.Sell : exchange.Buy; // Pass the sell order API (exchange.Sell) or the next buy order API (exchange.Buy) reference to the declared pfn
// based on the currency difference greater than 0 or less than 0
var amount = Math.abs(diff); // The amount of the order to be balanced is diff, the difference in the currency, assigned to the declared amount variable.
var price = diff > 0 ? (price - slidePrice) : (price + slidePrice); // The direction of buying and selling according to the difference in the currency, increase or decrease the
// slip price based on the price (slip price is to make it easier to trade), and then assign it to price
Log("start the balance", (diff > 0 ? "sell" : "buy"), amount, "of coins"); // The number of coins that the output log balances.
if (diff > 0) { // According to the direction of the buying and selling, determine whether the account currency or the amount of coins is sufficient.
amount = Math.min(nowAccount.Stocks, amount); // Make sure that the order amount will not exceed the available coins of the current account.
} else {
amount = Math.min(nowAccount.Balance / price, amount); // Make sure that the amount of order placed does not exceed the amount of money available in the current account.
}
if (amount < exchange.GetMinStock()) { // Check if the final order quantity is less than the minimum order quantity allowed by the exchange
Log("Insufficient funds, unable to balance to the initial state"); // If the order quantity is too small, the information is printed.
ok = false; // Tag balance failed
break; // Jump out of the while loop
}
pfn(price, amount); // Execute order API (pfn reference)
Sleep(1000); // Pause for 1 second
cancelPending(); // Cancel all pending orders.
nowAccount = _C(exchange.GetAccount); // Get current account information
}
if (ok) { // Execute the code inside curly braces when ok is true (balance is successful)
LogProfit(_N(nowAccount.Balance - orgAccount.Balance)); // Use the Balance property of the incoming parameter orgAccount (account information before balancing)
// to subtract the Balance property of the current account information, that is, the difference in the amount of money.
// That is, profit and loss (because the number of coins does not change, there is a slight error because some small
// amounts cannot be balanced)
Log("平衡完成", nowAccount); // The output log is balanced.
}
}
var STATE_WAIT_OPEN = 0;                                                        // Used for the state of each node in the fishTable
var STATE_WAIT_COVER = 1; // ...
var STATE_WAIT_CLOSE = 2; // ...
var ProfitCount = 0; // Profit and loss record
var BuyFirst = true; // Initial interface parameters
var IsSupportGetOrder = true; // determine the exchange support the GetOrder API function, a global variable, used to determine the start of the main function
var LastBusy = 0; // Record the last processed time object
function setBusy() {                            // Set Busy time
LastBusy = new Date(); // Assign LastBusy to the current time object
}
function isTimeout() {                                                          // Determine if it times out
if (MaxIdle <= 0) { // Maximum idle time (based on whether the grid is automatically moved),
// if the maximum idle time MaxIdle is set less than or equal to 0
return false; // Returns false, does not judge the timeout. That is, always return false without timeout.
}
var now = new Date(); // Get current time object
if (((now.getTime() - LastBusy.getTime()) / 1000) >= MaxIdle) { // Use the getTime function of the current time object to get the timestamp and the timestamp of LastBusy to calculate the difference,
// Divide by 1000 to calculate the number of seconds between the two time objects.
// Determine if it is greater than the maximum idle time MaxIdle
LastBusy = now; // If it is greater than, update LastBusy to the current time object now
return true; // Returns true, which is a timeout.
}
return false; // Return false no timeout
}
function onexit() {                             // The closing function when the program exits.
if (CancelAllWS) { // To cancel all pending orders when stop, call cancelPending() to cancel all pending orders.
Log("Exiting, try to cancel all pending orders");
cancelPending();
}
Log("Strategy successfully stopped");
Log(_C(exchange.GetAccount)); // Print the account position information when you exit the program.
}

function fishing(orgAccount, fishCount) {    // Casting parameters: account information, number of casting
setBusy(); // Set LastBuys to the current timestamp
var account = _C(exchange.GetAccount); // Declare an account variable to get the current account information and assign it.
Log(account); // Output the account information at the start of the call to the fishing function.
var InitAccount = account; // Declare a variable InitAccount and assign it with account. Here is the initial account funds recorded before
// this casting, used to calculate floating profit and loss.
var ticker = _C(exchange.GetTicker); // Get the quote value assigned to the declared ticker variable
var amount = _N(AmountOnce); // According to the number of interface parameters, use _N to process the decimal places (_N defaults to 2 bits) and assign them to amount.
var amountB = [amount]; // Declare a variable called amountB is an array, initialize an element with amount
var amountS = [amount]; // Declare a variable called amountS ...
if (typeof(AmountType) !== 'undefined' && AmountType == 1) { // According to the custom amount, the order size type, if this interface parameter is not undefined,
// And AmountType is set to a custom amount on the interface, that is, the AmountType value is 1 (the index of the drop-down box)
for (var idx = 0; idx < AllNum; idx++) { // The total number of AllNum. If you set a custom amount, cycle amountB/amountS to the order quantity array according to the total number of cycles.
amountB[idx] = BAmountOnce; // Assign value to the buy order array using interface parameters
amountS[idx] = SAmountOnce; // ... to the sell order...
}
} else { // else
for (var idx = 1; idx < AllNum; idx++) { // Cycles based on the total number of grids.
switch (AmountCoefficient[0]) { // According to the interface parameter difference, the first character of the string, AmountCoefficient[0] is '+', '-', '*', '/'
case '+': // According to the interface parameters, a grid with a single addition and increment is constructed.
amountB[idx] = amountB[idx - 1] + parseFloat(AmountCoefficient.substring(1));
break;
case '-': // ...
amountB[idx] = amountB[idx - 1] - parseFloat(AmountCoefficient.substring(1));
break;
case '*':
amountB[idx] = amountB[idx - 1] * parseFloat(AmountCoefficient.substring(1));
break;
case '/':
amountB[idx] = amountB[idx - 1] / parseFloat(AmountCoefficient.substring(1));
break;
}
amountB[idx] = _N(amountB[idx], AmountDot); // buying order, buying amount, and process the data decimal places.
amountS[idx] = amountB[idx]; // Assignment
}
}
if (FirstPriceAuto) { // If the first parameter is automatically set to true if the interface parameter is set, the code inside the if curly brackets is executed.
FirstPrice = BuyFirst ? _N(ticker.Buy - PriceGrid, Precision) : _N(ticker.Sell + PriceGrid, Precision);
// The interface parameter FirstPrice sets the first price according to the BuyFirst global variable (the initial statement is true,
// and has been assigned according to the OpType at the beginning of the main).
// The price is set by the price ticker and the price parameter PriceGrid price interval.
}
// Initialize fish table
var fishTable = {}; // Declare a grid object
var uuidTable = {}; // Identification code table object
var needStocks = 0; // Required coins variable
var needMoney = 0; // Required money variable
var actualNeedMoney = 0; // Actually needed money
var actualNeedStocks = 0; // Actually needed coins
var notEnough = false; // Underfunded tag variable, initially set to false
var canNum = 0; // Available grid
for (var idx = 0; idx < AllNum; idx++) { // The structure is traversed according to the number of grid AllNum.
var price = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision);
// When traversing the construct, the current index idx price setting is set according to BuyFirst. The spacing between each index price is PriceGrid.
needStocks += amountS[idx]; // The number of coins sold is gradually accumulated with the cycle. (accumulated by the sell order quantity array to needStocks one by one)
needMoney += price * amountB[idx]; // The amount of money required to buy is gradually accumulated with the cycle. (....buy the order quantity array one by one...)
if (BuyFirst) { // Handling buy first
if (_N(needMoney) <= _N(account.Balance)) { // If the grid requires less money than the amount of money available on the account
actualNeedMondy = needMoney; // Assigned to the actual amount of money required
actualNeedStocks = needStocks; // Assigning to the actual number of coins required. Is there something wrong with this?
canNum++; // Cumulative number of available grids
} else { // _N(needMoney) <= _N(account.Balance) If this condition is not met, set the underfunded tag variable to true
notEnough = true;
}
} else { // Handling sell first
if (_N(needStocks) <= _N(account.Stocks)) { // Check if the required number of coins is less than the number of coins available in the account
actualNeedMondy = needMoney; // Assignment
actualNeedStocks = needStocks;
canNum++; // Cumulative number of available grids
} else {
notEnough = true; // Set true if the funding conditions are not met
}
}
fishTable[idx] = STATE_WAIT_OPEN; // According to the current index idx, set the state of the idx member (grid node) of the grid object,
// initially STATE_WAIT_OPEN (waiting to open the position)
uuidTable[idx] = -1; // The numbered object also initializes its own idx value (the node corresponding to the fishTable) to -1 based on the current idx.
}
if (!EnableAccountCheck && (canNum < AllNum)) { // If the funds check is not enabled, and the number of grids (the total number of nodes) where the node is smaller
// than the interface parameter setting can be opened.
Log("Warning, current funds can only be made", canNum, "of Grids, total grid needs", (BuyFirst ? needMoney : needStocks), "Please keep sufficient funds"); // Log outputs a warning message.
canNum = AllNum; // Update the number of openable settings for the interface parameters
}
if (BuyFirst) { // buy first
if (EnableProtectDiff && (FirstPrice - ticker.Sell) > ProtectDiff) { // Open spread protection and enter the market price minus the current bid price more than
// the market entry price protection
throw "The first buying price is higher than the market selling price" + _N(FirstPrice - ticker.Sell, Precision) + ' dollar'; // Throw an error message.
} else if (EnableAccountCheck && account.Balance < _N(needMoney)) { // If the funds check is enabled and the amount of money available for the account is less than
// the amount of money required for the grid.
if (fishCount == 1) { // If it is the first time to cast the grid
throw "Insufficient funds, need" + _N(needMoney) + "dollar"; // Throw an error, insufficient funds
} else {
Log("Insufficient funds, need", _N(needMoney), "dollar, the program only make", canNum, "of grids #ff0000"); // If it is not the first time to cast a grid, output a message.
}
} else { // In other cases, there is no capital inspection, price protection, etc.
Log('Estimated use of funds: ', _N(needMoney), "dollar"); // The output is expected to use funds.
}
} else { // sell first, The following is similar to "buy first"
if (EnableProtectDiff && (ticker.Buy - FirstPrice) > ProtectDiff) {
throw "The first selling price is higher than the market buying price" + _N(ticker.Buy - FirstPrice, Precision) + ' dollar';
} else if (EnableAccountCheck && account.Stocks < _N(needStocks)) {
if (fishCount == 1) {
throw "Insufficient funds, need" + _N(needStocks) + " of coins";
} else {
Log("Insufficient funds, need", _N(needStocks), "of coins, program only make", canNum, "of grids #ff0000");
}
} else {
Log('Estimated use of funds: ', _N(needStocks), "coins, approximately", _N(needMoney), "dollar");
}
}
    var trader = new Trader();                                          // Constructs a Trader object, assigning it to the trader variable declared here.
var OpenFunc = BuyFirst ? exchange.Buy : exchange.Sell; // According to whether to buy and sell first, set the open function OpenFunc to refer to exchange.Buy or exchange.Sell
var CoverFunc = BuyFirst ? exchange.Sell : exchange.Buy; // same as above
if (EnableDynamic) { // Set OpenFunc/CoverFunc again according to whether the interface parameter EnableDynamic is enabled.
OpenFunc = BuyFirst ? trader.Buy : trader.Sell; // The member function Buy that references the trader object is used for dynamic pending orders (mainly because
// some exchanges limit the number of pending orders, so virtual dynamic pending orders are required)
CoverFunc = BuyFirst ? trader.Sell : trader.Buy; // same as above
}
var ts = new Date(); // Create a time object at this time (assigned to ts) to record the time at the moment.
var preMsg = ""; // Declare a variable to record the last message, the initial set to empty string
var profitMax = 0; // Maximum return
while (true) { // The main logic after the grid is casted
var now = new Date(); // Record the time when the current cycle started
var table = null; // Declare a variable
if (now.getTime() - ts.getTime() > 5000) { // Calculate whether the difference between the current time now and the recorded time ts is greater than 5000 milliseconds
if (typeof(GetCommand) == 'function' && GetCommand() == "Receiving grid") { // Check if the strategy interaction control command "receives the grid" is received,
// stops and balances to the initial state.
Log("Start executing commands to perform grid operations"); // Output information
balanceAccount(orgAccount, InitAccount); // Perform a balancing function to balance the number of coins to the initial state
return false; // This time the grid function is fishing and return false
}
ts = now; // Update ts with current time now for next comparison time
var nowAccount = _C(exchange.GetAccount); // Declare the nowAccount variable and initially been set as the current account information.
var ticker = _C(exchange.GetTicker); // Declare the ticker variable and initially been set as the current market information.
if (EnableDynamic) { // If you enable dynamic pending orders
trader.Poll(ticker, DynamicMax); // Call the Poll function of the trader object to detect and process all orders based on the
// current ticker market and the interface parameter DynamicMax.
}
var amount_diff = (nowAccount.Stocks + nowAccount.FrozenStocks) - (InitAccount.Stocks + InitAccount.FrozenStocks); // Calculate the current coin difference
var money_diff = (nowAccount.Balance + nowAccount.FrozenBalance) - (InitAccount.Balance + InitAccount.FrozenBalance); // Calculate the current money difference
var floatProfit = _N(money_diff + (amount_diff * ticker.Last)); // Calculate the current floating profit and loss of this time of casting grid
var floatProfitAll = _N((nowAccount.Balance + nowAccount.FrozenBalance - orgAccount.Balance - orgAccount.FrozenBalance) + ((nowAccount.Stocks + nowAccount.FrozenStocks
- orgAccount.Stocks - orgAccount.FrozenStocks) * ticker.Last));
// Calculate the overall floating profit and loss
            var isHold = Math.abs(amount_diff) >= exchange.GetMinStock();             // If the absolute value of the coin difference at this moment is greater than the minimum trading 
// volume of the exchange, it means that the position has been held.
if (isHold) { // If you have already held a position, execute the setBusy() function, which will update the LastBusy time.
setBusy(); // That is, after opening the position, the opening of the opening mechanism is started.
}
            profitMax = Math.max(floatProfit, profitMax);                             // Refresh the maximum floating profit and loss
if (EnableAccountCheck && EnableStopLoss) { // If you initiate account detection and start a stop loss
if ((profitMax - floatProfit) >= StopLoss) { // If the maximum floating profit or loss minus the current floating profit or loss is greater than or equal to
// the maximum floating loss value, execute the code inside the curly braces
Log("Current floating profit and loss", floatProfit, "Highest profit point:", profitMax, "Start stop loss"); // Output information
balanceAccount(orgAccount, InitAccount); // Balanced account
if (StopLossMode == 0) { // According to the stop loss mode processing, if StopLossMode is equal to 0, the program is exited after the stop loss.
throw "Stop loss exit"; // Throw the error "Stop Loss Exit" strategy to stop.
} else {
return true; // Except for the stop loss exit mode, ie: re-cast the grid after the stop loss.
}
}
}
if (EnableAccountCheck && EnableStopWin) { // If the detection account is turned on and the take profit is turned on
if (floatProfit > StopWin) { // If the floating profit and loss is greater than the take profit
Log("Current floating profit and loss", floatProfit, "Start taking profit"); // Output log
balanceAccount(orgAccount, InitAccount); // Balanced account recovery initial (take profit)
if (StopWinMode == 0) { // According to the take profit mode.
throw "Take profit exit"; // Exit after taking profit
} else {
return true; // Return true after take profit, continue to cast the grid
}
}
}
var distance = 0; // Declare a variable to record the distance
if (EnableAccountCheck && AutoMove) { // If account detection is turned on and the grid moves automatically
if (BuyFirst) { // If it is buy first and then sell
distance = ticker.Last - FirstPrice; // Assign a value to distance: the current price minus the first price, calculate the distance
} else { // Other situations: sell first and then buy
distance = FirstPrice - ticker.Last; // Assign a value to distance: the first price minus the current price, calculate the distance
}
var refish = false; // Whether to re-cast the tag variable
if (!isHold && isTimeout()) { // If there is no position (isHold is false) and timeout (isTimeout returns true)
Log("no position holding for too long, start moving the grid");
refish = true; // Mark re-cast
}
if (distance > MaxDistance) { // If the current distance is greater than the maximum distance set by the interface parameters, the mark is re-cast
Log("The price is too much beyond the grid interval, start moving the grid, the current distance: ", _N(distance, Precision), "current price:", ticker.Last);
refish = true;
}
if (refish) { // If refish is true, execute the balance function
balanceAccount(orgAccount, InitAccount);
return true; // This cast function returns true
}
}
            var holdDirection, holdAmount = "--",                                       // Declare three variables, position direction, number of positions, position price
holdPrice = "--";
if (isHold) { // When holding a position
if (RestoreProfit && ProfitAsOrg) { // If you start to restore the last profit and the last profit is included in the average price
if (BuyFirst) { // If it is buy first and then sell
money_diff += LastProfit; // Add the last profit to money_diff, that is, the last time the income is converted into the money difference
// (in the case of the first buy, the money difference is negative, that is, the cost),
// which is equivalent to the opening cost.
} else { // If it is sell first, then buy
money_diff -= LastProfit; // The difference between buying and selling is positive, why - ?
}
}
                // Dealing with buying first and then selling
holdAmount = amount_diff; // The difference in the value of the coins is given to the number of
// positions (the difference in the coins is the position at the moment)
holdPrice = (-money_diff) / amount_diff; // Calculate the average price of the position by dividing the difference between
// the money and the difference in the coins,
// Note: If money_diff is negative, then amount_diff must be positive, so be sure
// to add a minus sign before money_diff so that the calculated price is a positive number.
// Dealing with selling first and then buying
if (!BuyFirst) { // If it is selling first and then buying, it will trigger the update of the open position and
// the average price of the position.
holdAmount = -amount_diff; // The coins difference is negative, so the counter is reversed
holdPrice = (money_diff) / -amount_diff; // Calculate the average price of the position.
}
holdAmount = _N(holdAmount, 4); // Positions, retain 4 decimal places.
holdPrice = _N(holdPrice, Precision); // The average price of the position, retaining the Precision decimal.
holdDirection = BuyFirst ? "long" : "short"; // According to the "first buy then sell" or "first sell then buy" to holdDirection assigned long or short
} else { // If isHold is false, assign "--" to holdDirection
holdDirection = "--";
}
table = { // Assign an object to the declared table variable to display the table information on the FMZ robot status bar
type: 'table', // See the API documentation LogStatus function, here to initialize the type attribute 'table' for
// display in the status bar as a table
title: 'Operating status', // Title of the table
cols: ['Use funds', 'Holding position', 'Position size', 'Average price of position', 'Total floating profit and loss', 'Current grid profit and loss',
'Number of casting grids', 'Grid offset', 'Real placing order', 'Latest coin price'], // The column name of the table
rows: [ // Progressive data for the table
[_N(actualNeedMondy, 4), holdDirection, holdAmount, holdPrice, _N(floatProfitAll, 4) + ' ( ' + _N(floatProfitAll * 100 / actualNeedMondy, 4) + ' % )',
floatProfit, fishCount, (AutoMove && distance > 0) ? ((BuyFirst ? "up" : "down") + "Deviation: " + _N(distance) + " dollar") : "--", trader.RealLen(), ticker.Last]
// One row of data
]
};

} // Process some tasks every 5 seconds and update the robot status bar table object table

var orders = _C(trader.GetOrders); // Get all unexecuted orders
if (table) { // If the table has been assigned a table object
if (!EnableDynamic) { // If the dynamic order is not activated
table.rows[0][8] = orders.length; // Update the length of the pending order array in the first row and the 9th column of the status bar table.
}
LogStatus('`' + JSON.stringify(table) + '`'); // Call the FMZ platform API LogStatus to display the status bar table of the settings
}
for (var idx = 0; idx < canNum; idx++) { // Traverse the number of available grids nodes.
var openPrice = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision); // As the node index idx traverses, construct the opening price
// of each node (the direction is buying first, then sold,
// or selling first and then buy)
var coverPrice = _N((BuyFirst ? openPrice + PriceDiff : openPrice - PriceDiff), Precision); // Open and close position spread, that is, the profit margin of each node
var state = fishTable[idx]; // Assignment status of the fishnet node
var fishId = uuidTable[idx]; // Numbering

// The judgment here is: filtering unfinished orders
if (hasOrder(orders, fishId)) { // If all unexecuted orders, there is an order with the ID fishId in the pending order array
continue; // Skip this loop and continue the loop
}
            if (fishId != -1 && IsSupportGetOrder) {                                                        // The grid node id is not equal to the initial value, that is, the order is placed, 
// and the exchange supports GetOrder
var order = trader.GetOrder(fishId); // Get the order for the fishId number
// The judgment here is as follows: Filter the grid node where the order is not found, the following logic (state == STATE_WAIT_COVER), etc. will not trigger
if (!order) { // Failed to get an order if !order is true
Log("Failed to get order information, ID: ", fishId); // Output log
continue; // Skip this loop and continue the loop
}
// The judgment here is as follows: Filtering the grid nodes that are in a suspended state, not completed, or not fully completed,
// the logic of the following judgment (state == STATE_WAIT_COVER) and so on will not be triggered.
if (order.Status == ORDER_STATE_PENDING) { // If the order status is pending on the exchange
//Log("Order status is not completed, ID: ", fishId);
continue; // Skip this loop and continue to loop
}
}
            if (state == STATE_WAIT_COVER) {                                                                // If the current node status is waiting to be closed
var coverId = CoverFunc(coverPrice, (BuyFirst ? amountS[idx] : amountB[idx]), (BuyFirst ? 'Complete the buying order:' : 'Complete the selling order:'), openPrice, 'volume:',
(BuyFirst ? amountB[idx] : amountS[idx]));
// Call the closing function CoverFunc to send the closing order
                if (typeof(coverId) === 'number' || typeof(coverId) === 'string') {        // Determine if the value returned by the closing function is a value (returned directly by the FMZ API) 
// or a string (returned by the buy/Sell function of the trader object)
fishTable[idx] = STATE_WAIT_CLOSE; // The closing order has been send and the update status is: STATE_WAIT_CLOSE Waiting for the node task to complete
uuidTable[idx] = coverId; // Store the order number in the idx location corresponding to the uuidTable.
}
} else if (state == STATE_WAIT_OPEN || state == STATE_WAIT_CLOSE) { // If the status is waiting for opening or waiting for completion
var openId = OpenFunc(openPrice, BuyFirst ? amountB[idx] : amountS[idx]); // Open the position order.
if (typeof(openId) === 'number' || typeof(openId) === 'string') { // Determine whether the order placing is successful
fishTable[idx] = STATE_WAIT_COVER; // Update status for waiting for closing
uuidTable[idx] = openId; // Record current node order ID
if (state == STATE_WAIT_CLOSE) { // If it is waiting for completion (it will only be triggered after the open position order is placed)
ProfitCount++; // Cumulative gaining profit times
var account = _C(exchange.GetAccount); // Get current account information
var ticker = _C(exchange.GetTicker); // Get current market information
var initNet = _N(((InitAccount.Stocks + InitAccount.FrozenStocks) * ticker.Buy) + InitAccount.Balance + InitAccount.FrozenBalance, 8);
// Calculate the initial net asset value
var nowNet = _N(((account.Stocks + account.FrozenStocks) * ticker.Buy) + account.Balance + account.FrozenBalance, 8);
// Calculate the current net asset value
var actualProfit = _N(((nowNet - initNet)) * 100 / initNet, 8); // Calculated rate of return
if (AmountType == 0) { // According to the same amount of buy and sell, the custom amount is different.
var profit = _N((ProfitCount * amount * PriceDiff) + LastProfit, 8); // Calculation: the sum of the profit and loss of all profit nodes and the
// last time of the net profit and loss which is the total profit and loss
Log((BuyFirst ? 'Complete the sell order:' : 'Complete the buy order:'), coverPrice, 'volume:', (BuyFirst ? amountS[idx] : amountB[idx]), 'Closing position profit', profit);
// Output order completion information
} else {
Log((BuyFirst ? 'Complete the sell order:' : 'Complete the buy order:'), coverPrice, 'volume:', (BuyFirst ? amountS[idx] : amountB[idx]));
}
}
}
}
}
Sleep(CheckInterval); // Grid logic is mainly while loop detection, each time a pause is checked, CheckInterval is: detection interval
}
return true; // This time of the casting grid is completed, return true
}
function main() {                                    // The main function of the strategy, the program starts from here.
if (ResetData) { // RestData is the interface parameter, the default is true, and it controls whether all data is cleared at startup. All are cleared by default.
LogProfitReset(); // Execute the API LogProfitReset function to clear all revenue.
LogReset(); // Execute the API LogReset function to clear all logs.
}
// exchange.SetMaxDigits(Precision) // Deprecated, use exchange.SetPrecision instead.
exchange.SetPrecision(Precision, 3) // exchange.SetPrecision(2, 3); // Set the price decimal place precision to 2 digits, and the variety order size decimal place precision is 3 digits.
// Precision Interface parameter。
    if (typeof(AmountType) === 'undefined') {        // Order quantity type, 0: "Buy and sell the same amount", 1: "Custom amount", detect if the parameter is undefined, the default setting is 0.
AmountType = 0; // Typeof will detect the type of AmountType. If undefined is "undefined", assign a value of 0 to AmountType.
}
if (typeof(AmountDot) === 'undefined') { // The maximum number of digits of the order quantity decimal point AmountDot is undefined, set AmountDot to 3.
AmountDot = 3; // In fact, it has been set by exchange.SetPrecision(Precision, 3), and it will be truncated at the bottom.
}
if (typeof(EnableDynamic) === 'undefined') { // Check if the dynamic pending order parameter is enabled. If EnableDynamic is undefined, set to false will not be enabled.
EnableDynamic = false;
}
if (typeof(AmountCoefficient) === 'undefined') { // If not defined, the default setting is "*1"
AmountCoefficient = "*1";
}
if (typeof(EnableAccountCheck) === 'undefined') {// If not defined, the Enable Money Verification parameter is set to true, which is turned on.
EnableAccountCheck = true;
}
BuyFirst = (OpType == 0); // Assign value to BuyFirst according to the setting of OpType, OpType sets the grid type, 0: buy first and then sell, 1: sell first and then buy
IsSupportGetOrder = exchange.GetName().indexOf('itstamp') == -1; // Check the name of the exchange, whether it is Bitstamp or not
if (!IsSupportGetOrder) {
Log(exchange.GetName(), "Does not support GetOrder, may affect the stability of the strategy.");
}
    SetErrorFilter("502:|503:|S_U_001|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|refused|EOF|When");
// SetErrorFilter Filter error messages
    exchange.SetRate(1); 
Log('Exchange rate conversion has been disabled, the current currency is', exchange.GetBaseCurrency()); // Disable exchange rate conversion
    if (!RestoreProfit) {     //  If the last profit is restored to false, then LastProfit will be assigned a value of 0, that is, it will not be restored.
LastProfit = 0;
}
    var orgAccount = _C(exchange.GetAccount);     // Get account information, here record the initial account information when the strategy starts running, 
// used to calculate some income, such as: overall floating profit and loss.
// There are several parameters in this strategy that are passed to this variable.
var fishCount = 1; // time of casting grids initial is 1
while (true) { // Strategy main loop
if (!fishing(orgAccount, fishCount)) { // casting grid function fishing
break;
}
fishCount++; // Number of casting grids accumulated
Log("the number of", fishCount, "casting girds..."); // Output Casting grids information.
FirstPriceAuto = true; // Reset first price is automatically true
Sleep(1000); // Polling interval 1000 milliseconds
}
}