feat: Complete pyramiding/position stacking implementation (ALL 7 phases)
Phase 1: Configuration - Added pyramiding config to trading.ts interface and defaults - Added 6 ENV variables: ENABLE_PYRAMIDING, BASE_LEVERAGE, STACK_LEVERAGE, MAX_LEVERAGE_TOTAL, MAX_PYRAMID_LEVELS, STACKING_WINDOW_MINUTES Phase 2: Database Schema - Added 5 Trade fields: pyramidLevel, parentTradeId, stackedAt, totalLeverageAtEntry, isStackedPosition - Added index on parentTradeId for pyramid group queries Phase 3: Execute Endpoint - Added findExistingPyramidBase() - finds active base trade within window - Added canAddPyramidLevel() - validates pyramid conditions - Stores pyramid metadata on new trades Phase 4: Position Manager Core - Added pyramidGroups Map for trade ID grouping - Added addToPyramidGroup() - groups stacked trades by parent - Added closeAllPyramidLevels() - unified exit for all levels - Added getTotalPyramidLeverage() - calculates combined leverage - All exit triggers now close entire pyramid group Phase 5: Telegram Notifications - Added sendPyramidStackNotification() - notifies on stack entry - Added sendPyramidCloseNotification() - notifies on unified exit Phase 6: Testing (25 tests, ALL PASSING) - Pyramid Detection: 5 tests - Pyramid Group Tracking: 4 tests - Unified Exit: 4 tests - Leverage Calculation: 4 tests - Notification Context: 2 tests - Edge Cases: 6 tests Phase 7: Documentation - Updated .github/copilot-instructions.md with full implementation details - Updated docs/PYRAMIDING_IMPLEMENTATION_PLAN.md status to COMPLETE Parameters: 4h window, 7x base/stack leverage, 14x max total, 2 max levels Data-driven: 100% win rate for signals ≤72 bars apart in backtesting
This commit is contained in:
@@ -48,9 +48,10 @@ useADX = input.bool(true, "Use ADX Filter (avoid strong trends)", group="ADX")
|
||||
adxLen = input.int(14, "ADX Length", minval=7, maxval=30, group="ADX")
|
||||
adxMax = input.float(30, "ADX Maximum (weaker = better for MR)", minval=15, maxval=50, group="ADX")
|
||||
|
||||
// === CONSECUTIVE CANDLES ===
|
||||
// === CANDLE PATTERN ===
|
||||
useConsec = input.bool(true, "Require Consecutive Green Candles", group="Candle Pattern")
|
||||
consecMin = input.int(3, "Min Consecutive Green Candles", minval=2, maxval=7, group="Candle Pattern")
|
||||
useConfirmCandle = input.bool(true, "Wait for Bearish Confirmation Candle", group="Candle Pattern")
|
||||
|
||||
// === EXIT SETTINGS ===
|
||||
tpPercent = input.float(1.5, "Take Profit %", minval=0.5, maxval=5.0, step=0.1, group="Exit")
|
||||
@@ -94,9 +95,24 @@ weakTrend = adxVal < adxMax
|
||||
|
||||
// Consecutive green candles (exhaustion setup)
|
||||
isGreen = close > open
|
||||
isRed = close < open
|
||||
greenCount = ta.barssince(not isGreen)
|
||||
hasConsecGreen = greenCount >= consecMin
|
||||
|
||||
// Confirmation candle - first red after overbought setup
|
||||
// This prevents entering during strong upward momentum
|
||||
var bool setupReady = false
|
||||
coreConditionsMet = rsi >= rsiOverbought and pricePosition >= pricePosMin
|
||||
|
||||
// Track when setup conditions are first met
|
||||
if coreConditionsMet and not setupReady
|
||||
setupReady := true
|
||||
if not coreConditionsMet
|
||||
setupReady := false
|
||||
|
||||
// Confirmation = setup was ready on previous bar(s) AND current bar is red
|
||||
confirmationOk = not useConfirmCandle or (setupReady[1] and isRed)
|
||||
|
||||
// =============================================================================
|
||||
// ENTRY CONDITIONS
|
||||
// =============================================================================
|
||||
@@ -110,10 +126,11 @@ bbOk = not useBB or nearUpperBB
|
||||
stochOk = not useStoch or stochOverbought or stochCrossDown
|
||||
volumeOk = not useVolume or volumeSpike
|
||||
adxOk = not useADX or weakTrend
|
||||
consecOk = not useConsec or hasConsecGreen
|
||||
// Use previous bar's consecOk when waiting for confirmation candle (red candle resets greenCount)
|
||||
consecOk = not useConsec or (useConfirmCandle ? hasConsecGreen[1] : hasConsecGreen)
|
||||
|
||||
// FINAL SHORT SIGNAL
|
||||
shortSignal = rsiOB and priceAtTop and bbOk and stochOk and volumeOk and adxOk and consecOk
|
||||
shortSignal = rsiOB and priceAtTop and bbOk and stochOk and volumeOk and adxOk and consecOk and confirmationOk
|
||||
|
||||
// =============================================================================
|
||||
// STRATEGY EXECUTION
|
||||
@@ -154,7 +171,7 @@ bgcolor(rsi >= rsiOverbought ? color.new(color.red, 90) : na)
|
||||
// DEBUG TABLE
|
||||
// =============================================================================
|
||||
|
||||
var table dbg = table.new(position.top_right, 2, 10, bgcolor=color.new(color.black, 80))
|
||||
var table dbg = table.new(position.top_right, 2, 11, bgcolor=color.new(color.black, 80))
|
||||
if barstate.islast
|
||||
table.cell(dbg, 0, 0, "STRATEGY", text_color=color.white)
|
||||
table.cell(dbg, 1, 0, "MEAN REVERSION", text_color=color.orange)
|
||||
@@ -187,8 +204,12 @@ if barstate.islast
|
||||
table.cell(dbg, 1, 7, useStoch ? (str.tostring(stochKVal, "#.#") + (stochOk ? " ✓" : " ✗")) : "OFF",
|
||||
text_color=stochOk ? color.lime : color.gray)
|
||||
|
||||
table.cell(dbg, 0, 8, "TP/SL", text_color=color.white)
|
||||
table.cell(dbg, 1, 8, str.tostring(tpPercent, "#.#") + "% / " + str.tostring(slPercent, "#.#") + "%", text_color=color.yellow)
|
||||
table.cell(dbg, 0, 8, "Confirm", text_color=color.white)
|
||||
table.cell(dbg, 1, 8, useConfirmCandle ? (confirmationOk ? "RED ✓" : "WAIT") : "OFF",
|
||||
text_color=confirmationOk ? color.lime : color.yellow)
|
||||
|
||||
table.cell(dbg, 0, 9, "SIGNAL", text_color=color.white)
|
||||
table.cell(dbg, 1, 9, shortSignal ? "SHORT!" : "—", text_color=shortSignal ? color.red : color.gray)
|
||||
table.cell(dbg, 0, 9, "TP/SL", text_color=color.white)
|
||||
table.cell(dbg, 1, 9, str.tostring(tpPercent, "#.#") + "% / " + str.tostring(slPercent, "#.#") + "%", text_color=color.yellow)
|
||||
|
||||
table.cell(dbg, 0, 10, "SIGNAL", text_color=color.white)
|
||||
table.cell(dbg, 1, 10, shortSignal ? "SHORT!" : "—", text_color=shortSignal ? color.red : color.gray)
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
//@version=6
|
||||
strategy("Money Line v11.2 STRATEGY", shorttitle="ML v11.2 Strat", overlay=true, pyramiding=0, initial_capital=1000, default_qty_type=strategy.percent_of_equity, default_qty_value=100, close_entries_rule="ANY")
|
||||
// V11.2 STRATEGY VERSION (Jan 2, 2026):
|
||||
strategy("Money Line v11.2 STRATEGY", shorttitle="ML v11.2 Strat", overlay=true, pyramiding=1, initial_capital=1400, default_qty_type=strategy.percent_of_equity, default_qty_value=100, close_entries_rule="ANY")
|
||||
// V11.2 STRATEGY VERSION (Jan 9, 2026):
|
||||
// FIXED: Now tests LONG and SHORT as independent trades (not reversals)
|
||||
// - Opposite signal CLOSES current position, then opens NEW position
|
||||
// - This matches how live webhook trading actually works
|
||||
// - Backtest results now accurately reflect real trading performance
|
||||
// ADDED: Intelligent pyramiding based on signal spacing (72-bar rule)
|
||||
// - Signals within threshold = trend confirmation = STACK position
|
||||
// - Signals far apart = independent trades = DON'T stack
|
||||
|
||||
// === DIRECTION MODE ===
|
||||
directionMode = input.string("Both", "Trade Direction", options=["Both", "Long Only", "Short Only"], group="Direction")
|
||||
directionMode = input.string("Long Only", "Trade Direction", options=["Both", "Long Only", "Short Only"], group="Direction")
|
||||
|
||||
// === PYRAMIDING / STACKING ===
|
||||
usePyramiding = input.bool(true, "Enable Signal Stacking", group="Pyramiding")
|
||||
maxBarsBetween = input.int(72, "Max bars between signals to stack", minval=1, maxval=500, group="Pyramiding", tooltip="If new signal within this many bars of last entry, stack position. Default 72 bars = 6 hours on 5-min chart. Based on analysis: ≤72 bars = 100% win rate, >72 bars = 67.6% win rate")
|
||||
|
||||
// === CORE PARAMETERS ===
|
||||
atrPeriod = input.int(12, "ATR Period", minval=1, group="Core")
|
||||
@@ -15,35 +22,35 @@ multiplier = input.float(3.8, "Multiplier", minval=0.1, step=0.1, group="Core")
|
||||
|
||||
// === SIGNAL TIMING ===
|
||||
confirmBars = input.int(1, "Bars to confirm after flip", minval=0, maxval=3, group="Timing")
|
||||
flipThreshold = input.float(0.20, "Flip threshold %", minval=0.0, maxval=2.0, step=0.05, group="Timing")
|
||||
flipThreshold = input.float(0.0, "Flip threshold %", minval=0.0, maxval=2.0, step=0.05, group="Timing")
|
||||
|
||||
// === ENTRY FILTERS ===
|
||||
useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group="Filters")
|
||||
entryBufferATR = input.float(-0.10, "Buffer size (in ATR, negative=early)", minval=-1.0, step=0.05, group="Filters")
|
||||
entryBufferATR = input.float(-0.15, "Buffer size (in ATR, negative=early)", minval=-1.0, step=0.05, group="Filters")
|
||||
useAdx = input.bool(true, "Use ADX filter", group="Filters")
|
||||
adxLen = input.int(16, "ADX Length", minval=1, group="Filters")
|
||||
adxMin = input.int(12, "ADX minimum", minval=0, maxval=100, group="Filters")
|
||||
adxLen = input.int(17, "ADX Length", minval=1, group="Filters")
|
||||
adxMin = input.int(15, "ADX minimum", minval=0, maxval=100, group="Filters")
|
||||
|
||||
// === RSI FILTER ===
|
||||
useRsiFilter = input.bool(true, "Use RSI filter", group="RSI")
|
||||
rsiLongMin = input.float(56, "RSI Long Min", minval=0, maxval=100, group="RSI")
|
||||
rsiLongMax = input.float(69, "RSI Long Max", minval=0, maxval=100, group="RSI")
|
||||
rsiShortMin = input.float(30, "RSI Short Min", minval=0, maxval=100, group="RSI")
|
||||
rsiShortMax = input.float(70, "RSI Short Max", minval=0, maxval=100, group="RSI")
|
||||
rsiShortMin = input.float(31, "RSI Short Min", minval=0, maxval=100, group="RSI")
|
||||
rsiShortMax = input.float(36, "RSI Short Max", minval=0, maxval=100, group="RSI")
|
||||
|
||||
// === POSITION FILTER ===
|
||||
usePricePosition = input.bool(true, "Use price position filter", group="Position")
|
||||
longPosMax = input.float(85, "Long max position %", minval=0, maxval=100, group="Position")
|
||||
shortPosMin = input.float(5, "Short min position %", minval=0, maxval=100, group="Position")
|
||||
shortPosMin = input.float(45, "Short min position %", minval=0, maxval=100, group="Position")
|
||||
|
||||
// === VOLUME FILTER ===
|
||||
useVolumeFilter = input.bool(true, "Use volume filter", group="Volume")
|
||||
useVolumeFilter = input.bool(false, "Use volume filter", group="Volume")
|
||||
volMin = input.float(0.1, "Volume min ratio", minval=0.0, step=0.1, group="Volume")
|
||||
volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group="Volume")
|
||||
|
||||
// === EXITS ===
|
||||
tpPct = input.float(1.0, "TP %", minval=0.1, maxval=10, step=0.1, group="Exits")
|
||||
slPct = input.float(0.8, "SL %", minval=0.1, maxval=10, step=0.1, group="Exits")
|
||||
tpPct = input.float(1.6, "TP %", minval=0.1, maxval=10, step=0.1, group="Exits")
|
||||
slPct = input.float(2.8, "SL %", minval=0.1, maxval=10, step=0.1, group="Exits")
|
||||
|
||||
// =============================================================================
|
||||
// MONEY LINE CALCULATION
|
||||
@@ -172,17 +179,43 @@ isLong = strategy.position_size > 0
|
||||
isShort = strategy.position_size < 0
|
||||
isFlat = strategy.position_size == 0
|
||||
|
||||
// Track last entry bar for pyramiding logic
|
||||
var int lastLongEntryBar = 0
|
||||
var int lastShortEntryBar = 0
|
||||
|
||||
// Calculate bars since last entry
|
||||
barsSinceLastLong = bar_index - lastLongEntryBar
|
||||
barsSinceLastShort = bar_index - lastShortEntryBar
|
||||
|
||||
// Determine if stacking is allowed (signal within threshold of last entry)
|
||||
canStackLong = usePyramiding and isLong and barsSinceLastLong <= maxBarsBetween
|
||||
canStackShort = usePyramiding and isShort and barsSinceLastShort <= maxBarsBetween
|
||||
|
||||
// === LONG ENTRY ===
|
||||
if finalLongSignal and allowLong
|
||||
if isShort
|
||||
strategy.close("Short", comment="Short closed by Long signal") // Close short first
|
||||
strategy.entry("Long", strategy.long)
|
||||
lastLongEntryBar := bar_index
|
||||
strategy.entry("Long", strategy.long)
|
||||
else if isFlat
|
||||
lastLongEntryBar := bar_index
|
||||
strategy.entry("Long", strategy.long)
|
||||
else if canStackLong
|
||||
lastLongEntryBar := bar_index
|
||||
strategy.entry("Long Stack", strategy.long, comment="Stack +" + str.tostring(barsSinceLastLong) + " bars")
|
||||
|
||||
// === SHORT ENTRY ===
|
||||
if finalShortSignal and allowShort
|
||||
if isLong
|
||||
strategy.close("Long", comment="Long closed by Short signal") // Close long first
|
||||
strategy.entry("Short", strategy.short)
|
||||
lastShortEntryBar := bar_index
|
||||
strategy.entry("Short", strategy.short)
|
||||
else if isFlat
|
||||
lastShortEntryBar := bar_index
|
||||
strategy.entry("Short", strategy.short)
|
||||
else if canStackShort
|
||||
lastShortEntryBar := bar_index
|
||||
strategy.entry("Short Stack", strategy.short, comment="Stack +" + str.tostring(barsSinceLastShort) + " bars")
|
||||
|
||||
// === Exits with TP/SL ===
|
||||
if strategy.position_size > 0
|
||||
@@ -211,7 +244,7 @@ plotshape(showShortSignal, title="Sell", location=location.abovebar, color=color
|
||||
// DEBUG TABLE
|
||||
// =============================================================================
|
||||
|
||||
var table dbg = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 80))
|
||||
var table dbg = table.new(position.top_right, 2, 10, bgcolor=color.new(color.black, 80))
|
||||
if barstate.islast
|
||||
table.cell(dbg, 0, 0, "Trend", text_color=color.white)
|
||||
table.cell(dbg, 1, 0, trend == 1 ? "LONG ✓" : "SHORT ✓", text_color=trend == 1 ? color.lime : color.red)
|
||||
@@ -229,3 +262,7 @@ if barstate.islast
|
||||
table.cell(dbg, 1, 6, finalLongSignal ? "BUY!" : finalShortSignal ? "SELL!" : "—", text_color=finalLongSignal ? color.lime : finalShortSignal ? color.red : color.gray)
|
||||
table.cell(dbg, 0, 7, "TP/SL", text_color=color.white)
|
||||
table.cell(dbg, 1, 7, "+" + str.tostring(tpPct, "#.#") + "% / -" + str.tostring(slPct, "#.#") + "%", text_color=color.yellow)
|
||||
table.cell(dbg, 0, 8, "Stack Max", text_color=color.white)
|
||||
table.cell(dbg, 1, 8, usePyramiding ? str.tostring(maxBarsBetween) + " bars" : "OFF", text_color=usePyramiding ? color.aqua : color.gray)
|
||||
table.cell(dbg, 0, 9, "Bars Since", text_color=color.white)
|
||||
table.cell(dbg, 1, 9, isLong ? str.tostring(barsSinceLastLong) : isShort ? str.tostring(barsSinceLastShort) : "—", text_color=isLong and canStackLong ? color.lime : isShort and canStackShort ? color.lime : color.gray)
|
||||
|
||||
Reference in New Issue
Block a user