Skill v0.1.0
currentAutomated scan100/100name: grid-trading description: Use when writing a profit-laddered position-adjustment strategy on Superior Trade — anything described as a grid bot, range fade, range harvest, ladder buy, scaling-in, pyramiding, or "buy more when it dips and sell partials when it rallies". Note this is a profit-driven ladder, not a true 20-rung order-book grid; explain that limitation when the user asks for true grid trading. metadata: version: 0.1.0 updated: 2026-05-07
Strategy: Grid · Range Fade (laddered)
When to use
A user asks for "grid trading", "grid bot", "range fade", "ladder buy", "scale into the dip", "pyramid into a position", "DCA on drawdown" (not on calendar — that's strategy-dca-weekly). Anything where the trigger to add is a price drawdown, and there are partial take-profits on the way up.
Important caveat — explain this upfront
Freqtrade is a one-trade-per-pair engine. A real 20-rung grid bot — placing 20 limit orders simultaneously on the order book and refilling each as it fills — is not possible without engine changes. What you can implement is a profit-laddered position adjustment:
- 1 initial entry at a trigger price
- Up to N additional entries, each at a deeper drawdown step (−1%, −2%, …)
- Partial take-profits at progressive profit steps (+1.5%, +3%, +4.5%, …)
- Hard exit on a band breakout
This is a working, profitable approximation of the spirit of grid trading. If the user explicitly wants 100s of small fills per day on a tight book, say so and recommend running a separate grid runtime alongside Freqtrade.
Backtest reference
| Window | ETH/USDC 15m, 2026-03-01 → 2026-05-01 (61 days) | |
|---|---|---|
| Trades | 4 | |
| Win rate | 100% | |
| Wallet PnL | +0.66% / +$65.58 | |
| Sharpe | 2.02 | |
| Profit per trade | $15-30 | |
| Avg holding | 14 days | |
| Max DD | 0% (intraday only) | |
| Backtest ID | 01kqyz25d0zrwwf5fzccjk44dk |
Order pattern per trade: 2 entries ("" initial + grid_buy_1) + 4 partial exits at grid_tp_* tags. Sparse — 4 trades over 61 days — because the 24h VWAP −1% trigger fires rarely on ETH. Tighten the trigger (e.g. vwap × 0.995) for more activity.
Reference implementation
from freqtrade.strategy import IStrategyfrom freqtrade.persistence import Tradefrom datetime import datetimeimport pandas as pdclass EthGridStrategy(IStrategy):minimal_roi = {"0": 100.0} # never auto-close on ROI; partials handled in adjust_trade_positionstoploss = -0.30 # safety net, deeper than the deepest ladder rungtrailing_stop = Falsetimeframe = "15m"process_only_new_candles = Truestartup_candle_count = 200can_short = Falseposition_adjustment_enable = Truemax_entry_position_adjustment = 5 # 5 ladder rungs below entrymax_dca_multiplier = 6.0 # 1 + 5 addsdef populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:# 24h VWAP on 15m bars (96 bars).tp = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3.0pv = tp * dataframe["volume"]dataframe["vwap_24h"] = (pv.rolling(96).sum() / dataframe["volume"].rolling(96).sum())return dataframedef populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:# First grid rung: 1% below 24h VWAP.dataframe.loc[(dataframe["close"] <= dataframe["vwap_24h"] * 0.99)& (dataframe["volume"] > 0),"enter_long",] = 1return dataframedef populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:# Hard close on band breakout up.dataframe.loc[dataframe["close"] >= dataframe["vwap_24h"] * 1.06,"exit_long",] = 1return dataframedef custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,proposed_stake: float, min_stake, max_stake: float,leverage: float, entry_tag, side: str, **kwargs) -> float:return proposed_stake / self.max_dca_multiplierdef adjust_trade_position(self, trade: Trade, current_time: datetime,current_rate: float, current_profit: float,min_stake, max_stake: float,current_entry_rate: float, current_exit_rate: float,current_entry_profit: float, current_exit_profit: float,**kwargs):if trade.has_open_orders:return Nonen_entries = trade.nr_of_successful_entriesn_exits = trade.nr_of_successful_exits# Ladder buys: every -1% from average entry, up to 5 adds.if n_entries <= 5 and current_profit <= -0.01 * n_entries:filled = trade.select_filled_orders(trade.entry_side)first_stake = filled[0].stake_amount_filled if filled else (min_stake or 10)return (first_stake, f"grid_buy_{n_entries}")# Partial profit-take: every +1.5% above avg entry, up to 3 ladders.if n_exits < 3 and current_profit >= 0.015 * (n_exits + 1):return (-(trade.stake_amount / 4.0), f"grid_tp_{n_exits}")return None
Config requirements
{"exchange": { "name": "hyperliquid", "pair_whitelist": ["ETH/USDC"] },"stake_currency": "USDC","stake_amount": 1000,"dry_run_wallet": {"USDC": 10000},"timeframe": "15m","max_open_trades": 1,"stoploss": -0.30,"minimal_roi": { "0": 100.0 },"entry_pricing": { "price_side": "same" },"exit_pricing": { "price_side": "same" },"pairlists": [{ "method": "StaticPairList" }]}
dry_run_wallet ≥ stake_amount is enforced strictly. With 6 ladder rungs, leave headroom — dry_run_wallet ≥ stake_amount × 1.5 is comfortable.
Tunable parameters
| Knob | Effect | |
|---|---|---|
0.99 (entry trigger) | Tighter (0.995) → more entries, more chop. Looser (0.97) → rarer, deeper fades. | |
0.01 * n_entries (ladder spacing) | Tighter spacing → faster ladder fills, smaller gain per rung. Wider spacing → fewer rungs in chop. | |
max_entry_position_adjustment | More rungs → bigger position when fully laddered, more wallet exposure. | |
0.015 * (n_exits + 1) (TP step) | Tighter TPs → more partial closes, less per close. | |
1.06 (band breakout) | Tighter (1.04) → exit earlier on rallies, capture less. | |
trade.stake_amount / 4.0 (TP size) | Smaller divisor → bigger partial closes. / 2.0 halves the position per TP. |
Common pitfalls
- Naive single-rung implementation. Using
populate_entry_trendwithclose < vwap × 0.94andpopulate_exit_trendwithclose > vwap × 1.06produced 0 trades on the same window — ETH never reached the lower band. The laddered version captures the moves the band misses. - `stoploss` too shallow. With 5 ladder rungs at −1% spacing, a
−6%stop kills the trade before the deepest rung fills. Use−30%(or deeper) and rely on partial exits. - Letting `minimal_roi` close trades early. With the default
{"0": 0.02}, the trade exits at +2% before the partial-TP ladder ever runs. Set{"0": 100.0}to disable. - Forgetting `current_profit` is signed.
current_profit <= -0.01 * n_entriesreads "drawdown is at least n × 1%". Inverting the sign disables the ladder.
Variants
- Wider band:
0.97entry /1.10exit for trending pairs (BTC, SOL). - Asymmetric ladder: more buys than sells (
max_entry_position_adjustment = 8, only 2 partial TPs) for accumulation modes. - Volatility-scaled steps: replace fixed
0.01withatr_pct * 0.5to make ladder spacing follow regime.
When grid is the wrong tool
- Strong trends (the band breakout closes the trade after one cycle).
- Pairs that gap (Hyperliquid index pairs sometimes skip the trigger price entirely).
- Tight fee budgets — every ladder rung pays maker/taker fees twice (entry and partial exit). See
fees-optimizationsfor cost analysis.
Sources
- Freqtrade
adjust_trade_position— https://www.freqtrade.io/en/stable/strategy-callbacks/#adjust-trade-position - Internal audit —
docs/standard-strategies-audit.md, backtest01kqyz25d0zrwwf5fzccjk44dk