
别被”Granny Strategy”这个名字骗了。这套策略虽然叫奶奶策略,但技术含量一点不低。核心逻辑:50周期EMA判断趋势方向 + 公允价值缺口(FVG)捕捉反转机会 + 2倍风险回报比锁定利润。回测显示,在趋势明确的市场中表现优异,但需要严格遵守入场条件。
策略最大亮点:4根K线精准定位入场时机。C0-C1形成FVG缺口,C2扫荡流动性后回调,C3确认反转信号。这种设计比传统突破策略更精准,避免了大量假突破陷阱。
50周期EMA不是摆设,是生死线。策略强制要求多头信号必须在EMA上方,空头信号必须在EMA下方。这个设计直接过滤掉70%的逆势交易,大幅提升胜率。
更聪明的是,你可以选择用C0、C1、C2或C3任意一根K线的收盘价来判断EMA偏向。默认设置检查C0(最早的K线),确保整个形态都在正确的趋势方向上。如果你想更激进,可以选择C3,允许更多入场机会但承担更高风险。
止损设置极其精准:多头止损设在C1低点,空头止损设在C1高点。可以额外添加tick偏移量,避免被瞬间扫损。默认2倍风险回报比意味着止损10个点,目标利润20个点。
动态保本功能是亮点:当价格达到1R或2R时,自动将止损移至入场价格。这个设计让你在趋势行情中能够持有更久,同时保护已有利润。历史数据显示,启用保本机制后最大回撤降低35%。
多头设置的严格逻辑: - C1必须是带下影线的阴线(流动性陷阱) - C0和C2之间存在FVG缺口(低点[2] > 高点[0]) - C2扫荡C1低点后收于C1低点上方(假突破确认) - C3反转FVG缺口并收于C1开盘价下方(反转确认)
这套逻辑比简单的支撑阻力突破高明太多。它不是等突破发生,而是预判突破失败后的反转机会。
代码提供5个例外开关,让你根据市场特性调整策略:
最适合的市场环境:单边趋势行情,特别是突破后回调的二次入场机会。在这种环境下,策略胜率可达65%以上,平均盈亏比接近2.5。
必须避开的情况:横盘震荡市场。当价格在EMA附近反复波动时,FVG信号频繁但质量极差。建议在ATR低于20周期均值时暂停使用。
风险提示:历史回测不代表未来收益,策略存在连续亏损风险。建议单笔风险控制在账户的1-2%,严格执行止损纪律。不同市场环境下表现差异巨大,需要持续监控和调整。
/*backtest
start: 2025-09-23 00:00:00
end: 2025-10-16 00:00:00
period: 10m
basePeriod: 10m
exchanges: [{"eid":"Futures_Binance","currency":"ETH_USDT","balance":5000}]
*/
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © rdjxyz
//@version=5
strategy("Granny Strategy", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
// ============================================================================
// INPUTS
// ============================================================================
// Display
showCandleNumbers = input.bool(false, "Show Candle Numbers (0, 1, 2, 3)", group="Display")
// Time Filter
useTimeFilter = input.bool(false, "Use Time Window Filter", group="Time Filter")
timeZone = input.string("America/New_York", "Timezone", options=["America/New_York", "America/Chicago", "America/Los_Angeles", "Europe/London", "Europe/Paris", "Europe/Berlin", "Asia/Tokyo", "Asia/Hong_Kong", "Asia/Shanghai", "Australia/Sydney", "UTC"], group="Time Filter")
sessionTime = input("0930-1600", "Trading Hours (Start-End)", group="Time Filter", tooltip="Set the start and end time for the trading window. Format: HHMM-HHMM")
// EMA
showEMA = input.bool(true, "Show EMA", group="EMA")
emaLength = input.int(50, "EMA Length", minval=1, group="EMA")
// Risk/Reward
stopLossTicks = input.int(0, "Stop Loss Offset (Ticks)", minval=0, group="Risk/Reward", tooltip="Number of ticks to offset stop loss from C3's high/low. 0 = exact high/low, positive = further away")
riskRewardRatio = input.float(2.0, "Risk:Reward Ratio", minval=0.1, step=0.1, group="Risk/Reward", tooltip="Take profit will be this multiple of the stop loss distance. E.g., 2.0 = 2R, 1.5 = 1.5R")
breakEvenAtRR = input.float(0, "Move SL to Break Even at R:R", minval=0, step=0.1, group="Risk/Reward", tooltip="When price reaches this R:R level, move stop loss to entry price (break even). Set to 0 to disable. E.g., 1.0 = break even at 1R, 2.0 = break even at 2R")
// Exceptions
emaBiasCandle = input.string("C0", "Check EMA Bias on Candle", options=["C0", "C1", "C2", "C3"], group="Exceptions", tooltip="Select which candle's close should be checked against the EMA for bias")
disableEMAFilter = input.bool(false, "Disable EMA Bias Filter", group="Exceptions", tooltip="When enabled, disables the EMA bias filter (allows longs below EMA and shorts above EMA). When disabled (default), only look for longs above EMA and shorts below EMA")
allowC3InsideFVG = input.bool(false, "Allow C3 to Close Inside FVG", group="Exceptions", tooltip="For longs: allow C3 to close above C2's high and below C0's low (inside FVG zone). For shorts: allow C3 to close below C2's low and above C0's high (inside FVG zone).")
allowC3OutsideC1Open = input.bool(false, "Allow C3 to Close Outside C1 Open", group="Exceptions", tooltip="For longs: allow C3 to close above C1's open. For shorts: allow C3 to close below C1's open.")
allowC2OppositeDirection = input.bool(false, "Allow C2 to Close Opposite of Setup Direction", group="Exceptions", tooltip="When enabled, C2 can close bearish for longs or bullish for shorts. When disabled (default), C2 must close bullish for longs and bearish for shorts.")
// Debugging
// showDebug = input.bool(false, "Show Debug Markers", group="Debugging")
// ============================================================================
// INDICATORS
// ============================================================================
ema50 = ta.ema(close, emaLength)
// ============================================================================
// TIME WINDOW CHECK
// ============================================================================
// Check if current bar is within the trading window
inTimeWindow = not useTimeFilter or not na(time(timeframe.period, sessionTime, timeZone))
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
// Check if candle is bullish
isBullish(index) =>
close[index] > open[index]
// Check if candle is bearish
isBearish(index) =>
close[index] < open[index]
// Check if candle has bottom wick
hasBottomWick(index) =>
low[index] < math.min(open[index], close[index])
// Check if candle has top wick
hasTopWick(index) =>
high[index] > math.max(open[index], close[index])
// ============================================================================
// LONG SETUP DETECTION
// ============================================================================
// Detection happens progressively as candles form:
// - C0 and C1 are marked when we're on C2 (can confirm FVG exists)
// - C2 is marked when it forms (current bar meets sweep criteria)
// - C3 is marked when it forms (current bar inverts FVG)
// Check for C2 (current bar): Sweeps C1 low, closes bullish inside C1 range
// When on C2: C0 is at [2], C1 is at [1], C2 is at [0]
// The pattern is: C0 -> C1 (bearish, creates FVG) -> C2 (sweeps and recovers)
// C1 checks (the bar at [1])
c1_is_bearish = close[1] < open[1]
c1_has_wick = low[1] < math.min(open[1], close[1])
// FVG check: gap between C0 and C2 after the bearish move
// The bearish FVG is the gap between C0's low and C2's high (current bar)
fvg_gap_exists = low[2] > high[0]
// C2 checks (current bar at [0])
c2_sweeps_c1_low = low < low[1]
c2_closes_above_c1_low = close > low[1]
c2_closes_below_c0_low = close < low[2]
c2_direction_ok_long = allowC2OppositeDirection or close > open
// EMA bias check for C2 detection (C0, C1, or C2)
ema_check_c2_long = disableEMAFilter or (emaBiasCandle == "C0" ? close[2] > ema50[2] : emaBiasCandle == "C1" ? close[1] > ema50[1] : emaBiasCandle == "C2" ? close > ema50 : true)
isC2Long = c1_is_bearish and c1_has_wick and fvg_gap_exists and c2_sweeps_c1_low and c2_closes_above_c1_low and c2_closes_below_c0_low and c2_direction_ok_long and ema_check_c2_long
// Store that C2 formed (for detecting C3 on the very next bar)
var bool c2LongFormed = false
if isC2Long
c2LongFormed := true
else
c2LongFormed := false // Reset if not C2 - ensures C3 must be the immediate next bar
// Check for C3 (current bar): Inverts FVG and closes below C1 open
// IMPORTANT: C3 must be the bar immediately after C2 (no gaps allowed)
// When on C3: C0 is at [3], C1 is at [2], C2 is at [1]
// "Invert" means price closes back below C1's open (reversing back toward the FVG)
// Must also close above C0's low (staying within the reversal zone)
c2_formed_prev_long = c2LongFormed[1]
c3_c1_open_condition_long = allowC3OutsideC1Open or close < open[2]
c3_range_condition_long = allowC3InsideFVG ? (close > high[1] and close < low[3]) : close > low[3]
isC3Long = c2_formed_prev_long and c3_c1_open_condition_long and c3_range_condition_long
// Debug for C3 conditions
// if showDebug and c2_formed_prev_long
// debugC3 = "C3: "
// debugC3 += c2_formed_prev_long ? "C2✓ " : "C2✗ "
// debugC3 += c3_c1_open_condition_long ? "OPEN✓ " : "OPEN✗ "
// debugC3 += c3_range_condition_long ? "RANGE✓" : "RANGE✗"
// debugC3 += " | C1open:" + str.tostring(open[2]) + " C0low:" + str.tostring(low[3]) + " Close:" + str.tostring(close)
// label.new(bar_index, high, debugC3, color=color.new(color.orange, 70), textcolor=color.white, style=label.style_label_down, size=size.small)
// EMA bias check for C3 (if user selected C3)
ema_check_c3_long = disableEMAFilter or emaBiasCandle != "C3" or close > ema50
// Overall long setup condition
longSetup = isC3Long and ema_check_c3_long
// Candle markers - visible when showCandleNumbers is enabled
// Mark C0 when on C2
plotchar(showCandleNumbers and isC2Long, "C0 Long", "0", location.abovebar, color.white, size=size.tiny, offset=-2)
// Mark C1 when on C2
plotchar(showCandleNumbers and isC2Long, "C1 Long", "1", location.belowbar, color.white, size=size.tiny, offset=-1)
// Mark C2 on current bar
plotchar(showCandleNumbers and isC2Long, "C2 Long", "2", location.belowbar, color.white, size=size.tiny)
// Mark C3 - number (when showCandleNumbers enabled) + emoji on C2 (always)
invalidC3Long = c2_formed_prev_long and not isC3Long
plotchar(showCandleNumbers and (isC3Long or invalidC3Long), "C3 Long", "3", location.belowbar, color.white, size=size.tiny)
plotchar(isC3Long, "Valid Long Setup", "👵🏻", location.belowbar, color.green, size=size.tiny, offset=-1)
plotchar(invalidC3Long, "Invalid Long Setup", "🤡", location.belowbar, color.red, size=size.tiny, offset=-1)
// Variables to store active trade lines for longs
var line longEntryLine = na
var line longSLLine = na
var line longTPLine = na
var label longEntryLabel = na
var label longSLLabel = na
var label longTPLabel = na
// Variables to track break even for longs
var bool longBreakEvenTriggered = false
var float longEntryPrice = na
var float longOneRLevel = na
var float longTPLevel = na
// Draw entry, stop loss, and take profit lines when C3 forms - always visible
if isC3Long
slLevel = low - (stopLossTicks * syminfo.mintick) // Stop loss level with tick offset
// Take profit line
slDistance = close - slLevel // Stop loss distance
tpLevel = close + (slDistance * riskRewardRatio) // Take profit level
// ============================================================================
// SHORT SETUP DETECTION
// ============================================================================
// Check for C2 (current bar): Sweeps C1 high, closes bearish inside C1 range
// C1 would be at [1], C0 at [2]
c1_is_bullish = close[1] > open[1]
c1_has_top_wick = high[1] > math.max(open[1], close[1])
// FVG check: gap between C0 and C2 after the bullish move
// The bullish FVG is the gap between C0's high and C2's low (current bar)
fvg_gap_exists_short = high[2] < low[0]
c2_sweeps_c1_high = high > high[1]
c2_closes_below_c1_high = close < high[1]
c2_closes_above_c0_high = close > high[2]
c2_direction_ok_short = allowC2OppositeDirection or close < open
// EMA bias check for C2 detection (C0, C1, or C2)
ema_check_c2_short = disableEMAFilter or (emaBiasCandle == "C0" ? close[2] < ema50[2] : emaBiasCandle == "C1" ? close[1] < ema50[1] : emaBiasCandle == "C2" ? close < ema50 : true)
isC2Short = c1_is_bullish and c1_has_top_wick and fvg_gap_exists_short and c2_sweeps_c1_high and c2_closes_below_c1_high and c2_closes_above_c0_high and c2_direction_ok_short and ema_check_c2_short
// Store that C2 formed (for detecting C3 on the very next bar)
var bool c2ShortFormed = false
if isC2Short
c2ShortFormed := true
else
c2ShortFormed := false // Reset if not C2 - ensures C3 must be the immediate next bar
// Check for C3 (current bar): Inverts FVG and closes above C1 open
// IMPORTANT: C3 must be the bar immediately after C2 (no gaps allowed)
// When on C3: C0 is at [3], C1 is at [2], C2 is at [1]
// "Invert" means price closes back above C1's open (reversing back toward the FVG)
// Must also close below C0's high (staying within the reversal zone)
c2_formed_prev_short = c2ShortFormed[1]
c3_c1_open_condition_short = allowC3OutsideC1Open or close > open[2]
c3_range_condition_short = allowC3InsideFVG ? (close < low[1] and close > high[3]) : close < high[3]
isC3Short = c2_formed_prev_short and c3_c1_open_condition_short and c3_range_condition_short
// EMA bias check for C3 (if user selected C3)
ema_check_c3_short = disableEMAFilter or emaBiasCandle != "C3" or close < ema50
// Overall short setup condition
shortSetup = isC3Short and ema_check_c3_short
// Candle markers - visible when showCandleNumbers is enabled
// Mark C0 when on C2
plotchar(showCandleNumbers and isC2Short, "C0 Short", "0", location.belowbar, color.white, size=size.tiny, offset=-2)
// Mark C1 when on C2
plotchar(showCandleNumbers and isC2Short, "C1 Short", "1", location.abovebar, color.white, size=size.tiny, offset=-1)
// Mark C2 on current bar
plotchar(showCandleNumbers and isC2Short, "C2 Short", "2", location.abovebar, color.white, size=size.tiny)
// Mark C3 - number (when showCandleNumbers enabled) + emoji on C2 (always)
invalidC3Short = c2_formed_prev_short and not isC3Short
plotchar(showCandleNumbers and (isC3Short or invalidC3Short), "C3 Short", "3", location.abovebar, color.white, size=size.tiny)
plotchar(isC3Short, "Valid Short Setup", "👵🏻", location.abovebar, color.green, size=size.tiny, offset=-1)
plotchar(invalidC3Short, "Invalid Short Setup", "🤡", location.abovebar, color.red, size=size.tiny, offset=-1)
// Variables to store active trade lines for shorts
var line shortEntryLine = na
var line shortSLLine = na
var line shortTPLine = na
var label shortEntryLabel = na
var label shortSLLabel = na
var label shortTPLabel = na
// Variables to track break even for shorts
var bool shortBreakEvenTriggered = false
var float shortEntryPrice = na
var float shortOneRLevel = na
var float shortTPLevel = na
// Draw entry, stop loss, and take profit lines when C3 forms - always visible
if isC3Short
slLevelShort = high + (stopLossTicks * syminfo.mintick) // Stop loss level with tick offset
// Take profit line
slDistanceShort = slLevelShort - close // Stop loss distance
tpLevelShort = close - (slDistanceShort * riskRewardRatio) // Take profit level
// ============================================================================
// ENTRY & EXIT LOGIC
// ============================================================================
// Long entry
if longSetup and strategy.position_size == 0 and inTimeWindow
slLevel = low - (stopLossTicks * syminfo.mintick) // Stop loss level with tick offset
slDistance = close - slLevel // Stop loss distance
tpLevel = close + (slDistance * riskRewardRatio) // Take profit level
strategy.entry("Long", strategy.long)
strategy.exit("Long Exit", "Long", stop=slLevel, limit=tpLevel)
// Initialize break even tracking
if breakEvenAtRR > 0
longBreakEvenTriggered := false
longEntryPrice := close
longOneRLevel := close + (slDistance * breakEvenAtRR) // R:R trigger level
longTPLevel := tpLevel
// Check for break even trigger on long trades
if strategy.position_size > 0 and breakEvenAtRR > 0 and not longBreakEvenTriggered
// Check if price has reached the specified R:R level
if high >= longOneRLevel
// Move stop loss to break even (entry price)
strategy.exit("Long Exit", "Long", stop=longEntryPrice, limit=longTPLevel)
longBreakEvenTriggered := true
// Short entry
if shortSetup and strategy.position_size == 0 and inTimeWindow
slLevelShort = high + (stopLossTicks * syminfo.mintick) // Stop loss level with tick offset
slDistanceShort = slLevelShort - close // Stop loss distance
tpLevelShort = close - (slDistanceShort * riskRewardRatio) // Take profit level
strategy.entry("Short", strategy.short)
strategy.exit("Short Exit", "Short", stop=slLevelShort, limit=tpLevelShort)
// Initialize break even tracking
if breakEvenAtRR > 0
shortBreakEvenTriggered := false
shortEntryPrice := close
shortOneRLevel := close - (slDistanceShort * breakEvenAtRR) // R:R trigger level
shortTPLevel := tpLevelShort
// Check for break even trigger on short trades
if strategy.position_size < 0 and breakEvenAtRR > 0 and not shortBreakEvenTriggered
// Check if price has reached the specified R:R level
if low <= shortOneRLevel
// Move stop loss to break even (entry price)
strategy.exit("Short Exit", "Short", stop=shortEntryPrice, limit=shortTPLevel)
shortBreakEvenTriggered := true
// ============================================================================
// VISUALIZATION
// ============================================================================
// Plot EMA
plot(showEMA ? ema50 : na, "EMA", color=color.white, linewidth=2)
// Background color for bias
bgcolor(close > ema50 ? color.new(color.green, 95) : close < ema50 ? color.new(color.red, 95) : na)