Simple grid strategy in Python version
There are not many Python strategies on the strategy square. A Python version of the grid strategy is written here. The principle of the strategy is very simple. A series of grid nodes are generated by a fixed price distance within a price range. When the market changes and the price reaches a grid node price position, a buy order is placed. When the order is closed, that is, according to the price of the pending order plus the profit spread, pend a sell order to close the position. Capture fluctuations within the set price range.
It goes without saying that the risk of grid strategy is that any grid-type strategy is a bet that the price fluctuates in a certain range. Once the price breaks out of the grid range, it may cause serious floating losses. Therefore, the purpose of writing this strategy is to provide reference for Python strategy writing ideas or program design. This strategy is used for learning only, and it may be risky in the real bot.
The explanation of strategy ideas is written directly in the strategy code comments.
Strategy code
python'''backtest
start: 2019-07-01 00:00:00
end: 2020-01-03 00:00:00
period: 1m
exchanges: [{"eid":"OKEX","currency":"BTC_USDT"}]
'''
import json
# Parameters
beginPrice = 5000 # Grid interval begin price
endPrice = 8000 # Grid interval end price
distance = 20 # Price distance of each grid node
pointProfit = 50 # Profit spread per grid node
amount = 0.01 # Number of pending orders per grid node
minBalance = 300 # Minimum fund balance of the account (at the time of purchase)
# Global variables
arrNet = []
arrMsg = []
acc = None
def findOrder (orderId, NumOfTimes, ordersList = []) :
for j in range(NumOfTimes) :
orders = None
if len(ordersList) == 0:
orders = _C(exchange.GetOrders)
else :
orders = ordersList
for i in range(len(orders)):
if orderId == orders[i]["Id"]:
return True
Sleep(1000)
return False
def cancelOrder (price, orderType) :
orders = _C(exchange.GetOrders)
for i in range(len(orders)) :
if price == orders[i]["Price"] and orderType == orders[i]["Type"]:
exchange.CancelOrder(orders[i]["Id"])
Sleep(500)
def checkOpenOrders (orders, ticker) :
global arrNet, arrMsg
for i in range(len(arrNet)) :
if not findOrder(arrNet[i]["id"], 1, orders) and arrNet[i]["state"] == "pending" :
orderId = exchange.Sell(arrNet[i]["coverPrice"], arrNet[i]["amount"], arrNet[i], ticker)
if orderId :
arrNet[i]["state"] = "cover"
arrNet[i]["id"] = orderId
else :
# Cancel
cancelOrder(arrNet[i]["coverPrice"], ORDER_TYPE_SELL)
arrMsg.append("Pending order failed!" + json.dumps(arrNet[i]) + ", time:" + _D())
def checkCoverOrders (orders, ticker) :
global arrNet, arrMsg
for i in range(len(arrNet)) :
if not findOrder(arrNet[i]["id"], 1, orders) and arrNet[i]["state"] == "cover" :
arrNet[i]["id"] = -1
arrNet[i]["state"] = "idle"
Log(arrNet[i], "The node closes the position and resets to the idle state.", "#FF0000")
def onTick () :
global arrNet, arrMsg, acc
ticker = _C(exchange.GetTicker) # Get the latest current ticker every time
for i in range(len(arrNet)): # Iterate through all grid nodes, find out the position where you need to pend a buy order according to the current market, and pend a buy order.
if i != len(arrNet) - 1 and arrNet[i]["state"] == "idle" and ticker.Sell > arrNet[i]["price"] and ticker.Sell < arrNet[i + 1]["price"]:
acc = _C(exchange.GetAccount)
if acc.Balance < minBalance : # If there is not enough money left, you can only jump out and do nothing.
arrMsg.append("Insufficient funds" + json.dumps(acc) + "!" + ", time:" + _D())
break
orderId = exchange.Buy(arrNet[i]["price"], arrNet[i]["amount"], arrNet[i], ticker) # Pending buy orders
if orderId :
arrNet[i]["state"] = "pending" # Update the grid node status and other information if the buy order is successfully pending
arrNet[i]["id"] = orderId
else :
# Cancel h/the order
cancelOrder(arrNet[i]["price"], ORDER_TYPE_BUY) # Cancel orders by using the cancel function
arrMsg.append("Pending order failed!" + json.dumps(arrNet[i]) + ", time:" + _D())
Sleep(1000)
orders = _C(exchange.GetOrders)
checkOpenOrders(orders, ticker) # Check the status of all buy orders and process them according to the changes.
Sleep(1000)
orders = _C(exchange.GetOrders)
checkCoverOrders(orders, ticker) # Check the status of all sell orders and process them according to the changes.
# The following information about the construction status bar can be found in the FMZ API documentation.
tbl = {
"type" : "table",
"title" : "grid status",
"cols" : ["node index", "details"],
"rows" : [],
}
for i in range(len(arrNet)) :
tbl["rows"].append([i, json.dumps(arrNet[i])])
errTbl = {
"type" : "table",
"title" : "record",
"cols" : ["node index", "details"],
"rows" : [],
}
orderTbl = {
"type" : "table",
"title" : "orders",
"cols" : ["node index", "details"],
"rows" : [],
}
while len(arrMsg) > 20 :
arrMsg.pop(0)
for i in range(len(arrMsg)) :
errTbl["rows"].append([i, json.dumps(arrMsg[i])])
for i in range(len(orders)) :
orderTbl["rows"].append([i, json.dumps(orders[i])])
LogStatus(_D(), "\n", acc, "\n", "arrMsg length:", len(arrMsg), "\n", "`" + json.dumps([tbl, errTbl, orderTbl]) + "`")
def main (): # Strategy execution starts here
global arrNet
for i in range(int((endPrice - beginPrice) / distance)): # The for loop constructs a data structure for the grid based on the parameters, a list that stores each grid node, with the following information for each grid node:
arrNet.append({
"price" : beginPrice + i * distance, # Price of the node
"amount" : amount, # Number of orders
"state" : "idle", # pending / cover / idle # Node Status
"coverPrice" : beginPrice + i * distance + pointProfit, # Node closing price
"id" : -1, # ID of the current order related to the node
})
while True: # After the grid data structure is constructed, enter the main strategy loop
onTick() # Processing functions on the main loop, the main processing logic
Sleep(500) # Control polling frequency
The main design idea of the strategy is to compare the current list of pending orders returned by the GetOrders
interface according to the grid data structure maintained by yourself. Analyze the changes of pending orders (whether they are closed or not), update the grid data structure, and make subsequent operations. In addition, pending orders will not be cancelled until the transaction is completed, even if the price deviates, because the digital currency market often has the situation of pins, these pending orders may also receive the orders of pins (if the number of pending orders is limited in the exchange, it will be adjusted).
Strategy data visualization uses the LogStatus
function to display data on the status bar in real time.
go tbl = {
"type" : "table",
"title" : "grid status",
"cols" : ["node index", "details"],
"rows" : [],
}
for i in range(len(arrNet)) :
tbl["rows"].append([i, json.dumps(arrNet[i])])
errTbl = {
"type" : "table",
"title" : "record",
"cols" : ["node index", "details"],
"rows" : [],
}
orderTbl = {
"type" : "table",
"title" : "orders",
"cols" : ["node index", "details"],
"rows" : [],
}
Three tables are constructed. The first table displays the information of each node in the current grid data structure, the second table displays abnormal information, and the third table displays the actual listing information of the exchange.
Backtest
Strategy Address
The strategy is for learning and backtesting purpose only, and it can be optimized and upgraded if you are interested.
From: https://blog.mathquant.com/2022/12/23/simple-grid-strategy-in-python-version.html
Subscribe to my newsletter
Read articles from FMZ Quant directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
FMZ Quant
FMZ Quant
Quantitative Trading For Everyone