Timeframe
1h
Direction
Long Only
Stoploss
-15.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
100
Indicators
3
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
HonestGrid — Grid trading for range-bound markets.
Complements HonestTrendGeneric: enters only when ADX is LOW (sideways market).
Places staggered buy entries as price pulls back inside the range, exits when
price recovers by one ATR-sized grid step.
Core logic:
- Regime filter: ADX < adx_max (default 20) → range-bound, OK to enter
- Grid spacing: grid_atr_multiplier × ATR(atr_period) as % of close price
- Entry: close crosses below price_sma − 0.5 × ATR (pullback into range)
AND ADX < adx_max AND volume > volume_sma
- Exit: close crosses above price_sma + 0.5 × ATR (recovery to mid-range)
- Stoploss: −15% (wide; avoids premature exit inside the range)
- Timeframe: 1h
- Pairs: BTC/USDT, ETH/USDT (spot only)
Hyperoptable parameters:
adx_max max ADX to allow entry [10–30, default 20]
grid_atr_multiplier grid spacing in ATR units [1.0–3.0, default 1.5]
atr_period ATR lookback period [7–28, default 14]
Suggested config additions:
"max_open_trades": 3
"stake_amount": "unlimited" # or a fixed USDT amount
"position_adjustment_enable": false
"""
import csv
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter, Trade
logger = logging.getLogger(__name__)
# Fear & Greed history — same loading pattern as HonestTrendGeneric
FNG_DATA: dict[str, int] = {}
_fng_path = Path(__file__).parent.parent / "data" / "fng_history.csv"
if _fng_path.exists():
with open(_fng_path) as _f:
for _row in csv.DictReader(_f):
FNG_DATA[_row["date"]] = int(_row["value"])
class HonestGrid(IStrategy):
"""
Range-bound grid strategy.
Enters on pullbacks inside a sideways regime (ADX < threshold).
Uses ATR-normalised grid spacing for both entry levels and exit targets.
Designed to be run alongside HonestTrendGeneric, which handles trending markets.
"""
INTERFACE_VERSION = 3
can_short = False
# Wide ROI — we rely on custom_exit / exit signal for per-grid exits
minimal_roi = {"0": 100}
stoploss = -0.15
use_exit_signal = True
process_only_new_candles = True
startup_candle_count = 100 # enough for SMA(20) + ATR(28) warm-up
timeframe = "1h"
# Do NOT pyramid grid entries
position_adjustment_enable = False
# ---- Hyperoptable parameters ----
adx_max = IntParameter(10, 30, default=20, space="buy", optimize=True)
grid_atr_multiplier = DecimalParameter(1.0, 3.0, default=1.5, decimals=1,
space="buy", optimize=True)
atr_period = IntParameter(7, 28, default=14, space="buy", optimize=True)
# FnG block — don't buy into a range when euphoria is extreme (greed > 80)
FNG_BLOCK = 80
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": True,
}
order_time_in_force = {"entry": "GTC", "exit": "GTC"}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Compute ADX, ATR, SMA(20), volume SMA, and FnG overlay."""
atr_p = self.atr_period.value
dataframe["adx"] = ta.ADX(dataframe)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=atr_p)
dataframe["price_sma"] = ta.SMA(dataframe, timeperiod=20)
# Volume SMA: 1 calendar day of 1h candles = 24 bars
dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=24)
# Derived grid bands
half_atr = dataframe["atr"] * 0.5
dataframe["lower_band"] = dataframe["price_sma"] - half_atr
dataframe["upper_band"] = dataframe["price_sma"] + half_atr
# FnG overlay (date-keyed, fills forward with 50 if missing)
dataframe["fng"] = dataframe["date"].apply(
lambda x: FNG_DATA.get(x.strftime("%Y-%m-%d"), 50)
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Enter when price pulls back below lower_band inside a range-bound regime.
Conditions:
1. ADX < adx_max (not trending)
2. close crossed below price_sma − 0.5×ATR (pullback into range)
3. volume > volume_sma (participation confirms the move)
4. FnG < FNG_BLOCK (not in extreme greed)
"""
dataframe.loc[
(qtpylib.crossed_below(dataframe["close"], dataframe["lower_band"]))
& (dataframe["adx"] < self.adx_max.value)
& (dataframe["volume"] > dataframe["volume_sma"])
& (dataframe["fng"] < self.FNG_BLOCK)
& (dataframe["volume"] > 0),
["enter_long", "enter_tag"],
] = (1, "grid_pullback")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit when price recovers above upper_band (back to mid-range or above).
This fires the exit signal; actual per-tranche take-profit at one grid
step above entry is handled by custom_exit below.
"""
dataframe.loc[
(qtpylib.crossed_above(dataframe["close"], dataframe["upper_band"]))
& (dataframe["volume"] > 0),
["exit_long", "exit_tag"],
] = (1, "grid_recovery")
return dataframe
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> Optional[str]:
"""
Exit each tranche when profit >= one grid step.
Grid step = grid_atr_multiplier × ATR / open_rate (approx %).
We use a fixed ATR snapshot at entry; the ATR column is not directly
available here so we approximate from trade metadata or fall back to
a percentage derived from the open rate.
"""
# Approximate grid step from the hyperopt param + current volatility proxy.
# Freqtrade does not pass the dataframe here, so we use a simple
# heuristic: grid_atr_multiplier × 1.5% as a baseline grid step.
# In a full deployment you'd store ATR at entry in trade.meta.
base_step_pct = self.grid_atr_multiplier.value * 0.015
if current_profit >= base_step_pct:
return "grid_step_exit"
return None
def confirm_trade_exit(
self,
pair: str,
trade: Trade,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
exit_reason: str,
current_time: datetime,
**kwargs,
) -> bool:
"""Always allow stoploss and forced exits; allow all others immediately."""
if exit_reason in ("force_exit", "stoploss"):
return True
return True