Beyond Basic Grid Trading: Building and Backtesting a Dynamic Strategy for Crypto | by DolphinDB | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
Beyond Basic Grid Trading: Building and Backtesting a Dynamic Strategy for Crypto
DolphinDB
5 min read·<br>Mar 12, 2026
Listen
Share
Press enter or click to view image in full size
Introduction<br>Grid trading is one of the oldest and most intuitive algorithmic strategies in financial markets — place buy orders at regular intervals below the market price, sell orders above it, and profit from the oscillations in between. In stable, range-bound markets, it works beautifully.<br>But cryptocurrency markets are rarely stable. A sudden 30% crash doesn’t just hit your grid — it blows through it. A sharp uptrend that keeps going means your sell orders trigger too early, leaving most of the move uncaptured. Traditional grid strategies, built around a fixed reference price and static spacing, simply weren’t designed for the kind of violent, trending moves that define crypto.<br>The dynamic grid strategy addresses this directly. Rather than executing a trade the moment price touches a grid line, it waits for a rebound confirmation before entering — buying only after a bounce from the low, and selling only after a pullback from the high. Grid lines and reference prices are continuously updated as market conditions evolve. The result is a strategy that captures range-bound oscillations like a classic grid, while being far more resilient to sharp moves in either direction.<br>This post walks through the full implementation and backtesting of a dynamic grid strategy using DolphinDB’s cryptocurrency backtesting engine. Powered by minute-level market data, the framework provides a fast and structured environment to validate strategy logic before any real capital is deployed.<br>Strategy Logic<br>The strategy is built around two key parameters:<br>Grid spacing alpha: The percentage distance between grid lines (set to 2% in this example)<br>Rebound spacing beta: The confirmation buffer required before a trade is executed (set to 1%)<br>Together, these define a two-step trigger mechanism for every trade:<br>Opening (Buy) Logic: When the asset price drops to the n-th grid line below the reference price, the strategy doesn’t buy immediately. It waits for the price to rebound by beta from the lowest point reached. Only then does it buy, with a quantity of n × M / latest price — scaling position size with how far price has fallen.<br>Closing (Sell) Logic: Symmetrically, when the price rises to the n-th grid line above the reference price, the strategy waits for a pullback of beta from the highest point before selling.<br>Reference Price Updates: After every executed trade, the reference price resets to the latest transaction price, and the grid is reconstructed around it. This keeps the strategy anchored to where the market actually is, rather than where it started.<br>Strategy Implementation<br>Initialization<br>The initialize callback runs once when the engine starts. It sets all strategy parameters and initializes per-asset tracking dictionaries for reference prices, grid bounds, and position counters:<br>def initialize(mutable contextDict){<br>print("initialize")<br>// Initial price<br>contextDict["initPrice"] = dict(SYMBOL, ANY)<br>// Grid spacing (percentage)<br>contextDict["alpha"] = 0.01<br>// Rebound spacing (percentage)<br>contextDict["beta"] = 0.005<br>// Trade amount per grid<br>contextDict["M"] = 100000<br>contextDict["baseBuyPrice"] = dict(SYMBOL, ANY)<br>contextDict["baseSellPrice"] = dict(SYMBOL, ANY)<br>contextDict["lowPrice"] = dict(SYMBOL, ANY)<br>contextDict["highPrice"] = dict(SYMBOL, ANY)<br>contextDict["N"] = dict(SYMBOL, ANY)<br>// Fee rate<br>contextDict["feeRatio"] = 0.00015<br>Backtest::setUniverse(contextDict["engine"], contextDict.Universe)<br>}Grid Line Updates<br>A helper function updateBaseBuyPrice handles all grid recalculations. It resets the upper and lower grid boundaries based on the latest price and the current base price. The mode parameter controls the update context — initialization, a downward move, or an upward move:<br>def updateBaseBuyPrice(istock,lastPrice,basePrice,mutable baseBuyPrice,mutable baseSellPrice,mutable N,mutable highPrice,mutable lowPrice,alpha,n,mode=0){<br>// Update grid lines and highest/lowest price based on the latest price and base price<br>baseBuyPrice[istock] = basePrice*(1-alpha)<br>baseSellPrice[istock] = basePrice*(1+alpha)<br>N[istock] = n<br>if(mode==0){<br>// Initialization for buy/sell, etc.<br>lowPrice[istock]=0.<br>highPrice[istock]=10000.<br>}else if(mode==1){<br>// Price drop, update lower grid line<br>lowPrice[istock]=lastPrice<br>highPrice[istock]=10000.
}else if(mode==2){<br>// Price rise, update upper grid line<br>lowPrice[istock]=0.<br>highPrice[istock]=lastPrice<br>}Bar-Level Execution Logic<br>The onBar callback fires on every incoming minute bar. For each asset, it evaluates the latest close price against the current grid state and decides whether to update grid lines, trigger a buy, or trigger a sell.<br>def...