# SPY 0DTE Sowing & Harvesting Terminal V2.0
# Target chart: SPY, 5-minute, regular-session trading workflow
#
# This is a chart study and alert engine only. It cannot place, manage,
# or liquidate an options position.
declare upper;
#=============================================================================
# INPUTS
#=============================================================================
input ZoneStart = 0930;
input ZoneEnd = 1015;
input CutoffTime = 1530;
input SessionEnd = 1600;
input RangeQualification = {default ATR_ADAPTIVE, FIXED_DOLLARS};
input FixedMinimumRange = 1.50;
input DailyATRLength = 14;
input MinimumRangeATRPercent = 25.0;
input AdaptiveMinimumFloor = 0.75;
input RequireVolumeConfirmation = yes;
input VolumeBaseline = {default SESSION_PHASE, TRAILING_AVERAGE};
input VolumeMultiplier = 1.60;
input QuietTapeRelativeVolume = 0.80;
input TrailingVolumeLength = 20;
input MinimumPhaseBaselineBars = 4;
input BreakoutBuffer = 0.05;
input EMALength = 9;
input MaximumEntriesPerDirection = 2;
input ShowRangeLines = yes;
input ShowTargets = yes;
input ShowActiveEMA = yes;
input ShowSignalArrows = yes;
input ShowSignalBubbles = yes;
input ShowBackground = yes;
input ShowDashboard = yes;
input ShowCutoffLine = yes;
input EnableAlerts = yes;
input EnableTargetAlerts = yes;
#=============================================================================
# CHART AND SESSION VALIDATION
#=============================================================================
def isFiveMinute = GetAggregationPeriod() == AggregationPeriod.FIVE_MIN;
def isTargetZone =
SecondsFromTime(ZoneStart) >= 0 and
SecondsTillTime(ZoneEnd) > 0;
def postZone =
SecondsFromTime(ZoneEnd) >= 0 and
SecondsTillTime(SessionEnd) > 0;
def tradeWindow =
SecondsFromTime(ZoneEnd) >= 0 and
SecondsTillTime(CutoffTime) > 0;
def afterCutoff =
SecondsFromTime(CutoffTime) >= 0 and
SecondsTillTime(SessionEnd) > 0;
def beforeOpen = SecondsTillTime(ZoneStart) > 0;
def afterClose = SecondsFromTime(SessionEnd) >= 0;
def zoneStartBar = isTargetZone and !isTargetZone[1];
def cutoffEvent = afterCutoff and !afterCutoff[1];
#=============================================================================
# MORNING SOWING RANGE
#=============================================================================
def morningHigh = CompoundValue(1,
if zoneStartBar then high
else if isTargetZone then Max(high, morningHigh[1])
else morningHigh[1],
Double.NaN);
def morningLow = CompoundValue(1,
if zoneStartBar then low
else if isTargetZone then Min(low, morningLow[1])
else morningLow[1],
Double.NaN);
def rangeSessionDate = CompoundValue(1,
if zoneStartBar then GetYYYYMMDD()
else rangeSessionDate[1],
0);
def rangeSize = morningHigh - morningLow;
def rangeReady =
postZone and
rangeSessionDate == GetYYYYMMDD() and
!IsNaN(morningHigh) and
!IsNaN(morningLow) and
rangeSize > 0;
#=============================================================================
# ADAPTIVE RANGE QUALIFICATION
# Uses the prior completed daily ATR so the threshold does not move intraday.
#=============================================================================
def dailyHigh = high(period = AggregationPeriod.DAY);
def dailyLow = low(period = AggregationPeriod.DAY);
def dailyClose = close(period = AggregationPeriod.DAY);
def dailyTrueRange = TrueRange(dailyHigh, dailyClose, dailyLow);
def dailyATR = MovingAverage(AverageType.WILDERS, dailyTrueRange, DailyATRLength);
def completedDailyATR = dailyATR[1];
def adaptiveMinimumRange =
if IsNaN(completedDailyATR) then FixedMinimumRange
else Max(AdaptiveMinimumFloor,
completedDailyATR * MinimumRangeATRPercent / 100.0);
def requiredRange =
if RangeQualification == RangeQualification.FIXED_DOLLARS
then FixedMinimumRange
else adaptiveMinimumRange;
def rangeQualified = rangeReady and rangeSize >= requiredRange;
def rangeLockout = rangeReady and !rangeQualified;
#=============================================================================
# SESSION-PHASE RELATIVE VOLUME
#
# Phase 1: 10:15-11:30
# Phase 2: 11:30-14:00
# Phase 3: 14:00-15:30
#
# Each new phase initially uses the completed prior phase (or the opening-range
# average), then transitions to its own developing average. This prevents a
# 2:00 PM bar from being compared directly with the 9:30 AM volume surge.
#=============================================================================
def openingVolumeSum = CompoundValue(1,
if zoneStartBar then volume
else if isTargetZone then openingVolumeSum[1] + volume
else openingVolumeSum[1],
0);
def openingVolumeBars = CompoundValue(1,
if zoneStartBar then 1
else if isTargetZone then openingVolumeBars[1] + 1
else openingVolumeBars[1],
0);
def openingAverageVolume =
if openingVolumeBars > 0
then openingVolumeSum / openingVolumeBars
else Double.NaN;
def earlyPhase =
SecondsFromTime(ZoneEnd) >= 0 and
SecondsTillTime(1130) > 0;
def middayPhase =
SecondsFromTime(1130) >= 0 and
SecondsTillTime(1400) > 0;
def latePhase =
SecondsFromTime(1400) >= 0 and
SecondsTillTime(CutoffTime) > 0;
def phaseNumber =
if earlyPhase then 1
else if middayPhase then 2
else if latePhase then 3
else 0;
def phaseChanged = phaseNumber != phaseNumber[1];
def phaseVolumeSum = CompoundValue(1,
if !tradeWindow then 0
else if phaseChanged then volume
else phaseVolumeSum[1] + volume,
0);
def phaseVolumeBars = CompoundValue(1,
if !tradeWindow then 0
else if phaseChanged then 1
else phaseVolumeBars[1] + 1,
0);
def completedPriorPhaseAverage = CompoundValue(1,
if zoneStartBar then Double.NaN
else if phaseChanged and phaseNumber > 1 and phaseVolumeBars[1] > 0
then phaseVolumeSum[1] / phaseVolumeBars[1]
else completedPriorPhaseAverage[1],
Double.NaN);
def developingPhaseAverage =
if phaseVolumeBars > MinimumPhaseBaselineBars
then (phaseVolumeSum - volume) / (phaseVolumeBars - 1)
else if phaseNumber == 1 then openingAverageVolume
else if !IsNaN(completedPriorPhaseAverage)
then completedPriorPhaseAverage
else openingAverageVolume;
def trailingVolumeAverage = Average(volume[1], TrailingVolumeLength);
def selectedVolumeBaseline =
if VolumeBaseline == VolumeBaseline.SESSION_PHASE
then developingPhaseAverage
else trailingVolumeAverage;
def relativeVolume =
if tradeWindow and selectedVolumeBaseline > 0
then volume / selectedVolumeBaseline
else Double.NaN;
def volumeQualified =
!RequireVolumeConfirmation or
(!IsNaN(relativeVolume) and relativeVolume >= VolumeMultiplier);
def quietTape =
tradeWindow and
rangeQualified and
!IsNaN(relativeVolume) and
relativeVolume < QuietTapeRelativeVolume;
#=============================================================================
# TARGETS AND 9 EMA
#=============================================================================
def bullTarget1Level = morningHigh + rangeSize;
def bullTarget2Level = morningHigh + 2 * rangeSize;
def bearTarget1Level = morningLow - rangeSize;
def bearTarget2Level = morningLow - 2 * rangeSize;
def emaTrail = MovingAverage(AverageType.EXPONENTIAL, close, EMALength);
def entryLimit = Max(1, MaximumEntriesPerDirection);
#=============================================================================
# BREAKOUTS, STATE, AND CONTROLLED RE-ENTRY
#=============================================================================
def rawBullishBreakout =
isFiveMinute and
tradeWindow and
rangeQualified and
close[1] <= morningHigh[1] + BreakoutBuffer and
close > morningHigh + BreakoutBuffer and
volumeQualified;
def rawBearishBreakout =
isFiveMinute and
tradeWindow and
rangeQualified and
close[1] >= morningLow[1] - BreakoutBuffer and
close < morningLow - BreakoutBuffer and
volumeQualified;
def tradeState = {default FLAT, BULL, BEAR};
def bullEntries;
def bearEntries;
def BullishBreakout =
rawBullishBreakout and
tradeState[1] == tradeState.FLAT and
bullEntries[1] < entryLimit;
def BearishBreakout =
rawBearishBreakout and
tradeState[1] == tradeState.FLAT and
bearEntries[1] < entryLimit;
def bullEMAExit =
tradeWindow and
tradeState[1] == tradeState.BULL and
close < emaTrail;
def bearEMAExit =
tradeWindow and
tradeState[1] == tradeState.BEAR and
close > emaTrail;
tradeState =
if zoneStartBar or !tradeWindow then tradeState.FLAT
else if bullEMAExit or bearEMAExit then tradeState.FLAT
else if BullishBreakout then tradeState.BULL
else if BearishBreakout then tradeState.BEAR
else tradeState[1];
if zoneStartBar {
bullEntries = 0;
} else if BullishBreakout {
bullEntries = bullEntries[1] + 1;
} else {
bullEntries = bullEntries[1];
}
if zoneStartBar {
bearEntries = 0;
} else if BearishBreakout {
bearEntries = bearEntries[1] + 1;
} else {
bearEntries = bearEntries[1];
}
#=============================================================================
# ONE-SHOT TARGET EVENTS FOR EACH ACTIVE TRADE
#=============================================================================
def bullT1Hit;
def bullT2Hit;
def bearT1Hit;
def bearT2Hit;
if BullishBreakout {
bullT1Hit = high >= bullTarget1Level;
} else if tradeState == tradeState.BULL {
bullT1Hit = bullT1Hit[1] or high >= bullTarget1Level;
} else {
bullT1Hit = 0;
}
if BullishBreakout {
bullT2Hit = high >= bullTarget2Level;
} else if tradeState == tradeState.BULL {
bullT2Hit = bullT2Hit[1] or high >= bullTarget2Level;
} else {
bullT2Hit = 0;
}
if BearishBreakout {
bearT1Hit = low <= bearTarget1Level;
} else if tradeState == tradeState.BEAR {
bearT1Hit = bearT1Hit[1] or low <= bearTarget1Level;
} else {
bearT1Hit = 0;
}
if BearishBreakout {
bearT2Hit = low <= bearTarget2Level;
} else if tradeState == tradeState.BEAR {
bearT2Hit = bearT2Hit[1] or low <= bearTarget2Level;
} else {
bearT2Hit = 0;
}
def bullT1Event =
tradeWindow and
(tradeState == tradeState.BULL or tradeState[1] == tradeState.BULL) and
high >= bullTarget1Level and
!bullT1Hit[1];
def bullT2Event =
tradeWindow and
(tradeState == tradeState.BULL or tradeState[1] == tradeState.BULL) and
high >= bullTarget2Level and
!bullT2Hit[1];
def bearT1Event =
tradeWindow and
(tradeState == tradeState.BEAR or tradeState[1] == tradeState.BEAR) and
low <= bearTarget1Level and
!bearT1Hit[1];
def bearT2Event =
tradeWindow and
(tradeState == tradeState.BEAR or tradeState[1] == tradeState.BEAR) and
low <= bearTarget2Level and
!bearT2Hit[1];
#=============================================================================
# PLOTS
#=============================================================================
plot RangeHigh =
if ShowRangeLines and rangeReady and postZone
then morningHigh
else Double.NaN;
plot RangeLow =
if ShowRangeLines and rangeReady and postZone
then morningLow
else Double.NaN;
RangeHigh.SetDefaultColor(Color.GRAY);
RangeHigh.SetStyle(Curve.SHORT_DASH);
RangeHigh.SetLineWeight(1);
RangeLow.SetDefaultColor(Color.GRAY);
RangeLow.SetStyle(Curve.SHORT_DASH);
RangeLow.SetLineWeight(1);
plot BullTarget1 =
if ShowTargets and rangeQualified and tradeWindow
then bullTarget1Level
else Double.NaN;
plot BullTarget2 =
if ShowTargets and rangeQualified and tradeWindow
then bullTarget2Level
else Double.NaN;
plot BearTarget1 =
if ShowTargets and rangeQualified and tradeWindow
then bearTarget1Level
else Double.NaN;
plot BearTarget2 =
if ShowTargets and rangeQualified and tradeWindow
then bearTarget2Level
else Double.NaN;
BullTarget1.SetDefaultColor(Color.GREEN);
BullTarget1.SetStyle(Curve.MEDIUM_DASH);
BullTarget1.SetLineWeight(2);
BullTarget2.SetDefaultColor(Color.DARK_GREEN);
BullTarget2.SetStyle(Curve.FIRM);
BullTarget2.SetLineWeight(2);
BearTarget1.SetDefaultColor(Color.RED);
BearTarget1.SetStyle(Curve.MEDIUM_DASH);
BearTarget1.SetLineWeight(2);
BearTarget2.SetDefaultColor(Color.DARK_RED);
BearTarget2.SetStyle(Curve.FIRM);
BearTarget2.SetLineWeight(2);
plot BullTrailEMA =
if ShowActiveEMA and tradeState == tradeState.BULL
then emaTrail
else Double.NaN;
plot BearTrailEMA =
if ShowActiveEMA and tradeState == tradeState.BEAR
then emaTrail
else Double.NaN;
BullTrailEMA.SetDefaultColor(Color.LIME);
BullTrailEMA.SetLineWeight(2);
BearTrailEMA.SetDefaultColor(Color.MAGENTA);
BearTrailEMA.SetLineWeight(2);
plot CallSignal =
if ShowSignalArrows and BullishBreakout
then low - 2 * TickSize()
else Double.NaN;
plot PutSignal =
if ShowSignalArrows and BearishBreakout
then high + 2 * TickSize()
else Double.NaN;
CallSignal.SetPaintingStrategy(PaintingStrategy.ARROW_UP);
CallSignal.SetDefaultColor(Color.LIME);
CallSignal.SetLineWeight(3);
PutSignal.SetPaintingStrategy(PaintingStrategy.ARROW_DOWN);
PutSignal.SetDefaultColor(Color.MAGENTA);
PutSignal.SetLineWeight(3);
AddChartBubble(
ShowSignalBubbles and BullishBreakout,
low,
"CALL | RVOL " + AsText(Round(relativeVolume, 2)),
Color.GREEN,
no
);
AddChartBubble(
ShowSignalBubbles and BearishBreakout,
high,
"PUT | RVOL " + AsText(Round(relativeVolume, 2)),
Color.MAGENTA,
yes
);
AddVerticalLine(
ShowCutoffLine and cutoffEvent,
"3:30 PM 0DTE CUT-OFF",
Color.RED,
Curve.SHORT_DASH
);
#=============================================================================
# BACKGROUND -- ACTIVE TRADE ALWAYS HAS PRIORITY OVER QUIET-TAPE STATUS
#=============================================================================
AssignBackgroundColor(
if !ShowBackground then Color.CURRENT
else if isTargetZone then Color.DARK_GRAY
else if tradeState == tradeState.BULL then CreateColor(15, 50, 15)
else if tradeState == tradeState.BEAR then CreateColor(40, 15, 50)
else if rangeLockout and tradeWindow then CreateColor(60, 15, 15)
else if quietTape then CreateColor(50, 38, 10)
else Color.CURRENT
);
#=============================================================================
# VERTICALLY STACKED DASHBOARD
#=============================================================================
AddLabel(
ShowDashboard and !isFiveMinute,
"WARNING: THIS STUDY REQUIRES A 5-MINUTE CHART",
Color.RED,
location = Location.TOP_LEFT,
size = FontSize.SMALL,
"row ownership" = yes
);
AddLabel(
ShowDashboard and isFiveMinute,
if beforeOpen then "PREMARKET: WAIT FOR 9:30 AM ET"
else if isTargetZone then "PHASE: SOWING | BUILDING 9:30-10:15 RANGE"
else if afterCutoff then "HARVEST OVER: FLATTEN 0DTE POSITIONS"
else if afterClose then "SESSION CLOSED"
else if tradeWindow and !rangeReady then "LOCKOUT: NO VALID MORNING RANGE"
else if rangeLockout and tradeWindow then "LOCKOUT: MORNING RANGE TOO SMALL"
else if tradeState == tradeState.BULL then "HARVESTING: ACTIVE CALLS | TRAIL 9 EMA"
else if tradeState == tradeState.BEAR then "HARVESTING: ACTIVE PUTS | TRAIL 9 EMA"
else if quietTape then "WAIT: QUIET TAPE | BREAKOUT VOLUME NOT READY"
else if tradeWindow then "WATCHING: WAIT FOR A CONFIRMED RANGE BREAK"
else "OUTSIDE ACTIVE WINDOW",
if beforeOpen then Color.GRAY
else if isTargetZone then Color.YELLOW
else if afterCutoff then Color.RED
else if rangeLockout and tradeWindow then Color.LIGHT_RED
else if tradeState == tradeState.BULL then Color.GREEN
else if tradeState == tradeState.BEAR then Color.MAGENTA
else if quietTape then Color.LIGHT_ORANGE
else Color.WHITE,
location = Location.TOP_LEFT,
size = FontSize.SMALL,
"row ownership" = yes
);
AddLabel(
ShowDashboard and isFiveMinute and rangeReady and postZone,
"AM RANGE: $" + AsText(Round(rangeSize, 2)) +
" | REQUIRED: $" + AsText(Round(requiredRange, 2)) +
" | CALLS: " + AsText(bullEntries) + "/" + AsText(entryLimit) +
" | PUTS: " + AsText(bearEntries) + "/" + AsText(entryLimit),
if rangeQualified then Color.GREEN else Color.RED,
location = Location.TOP_LEFT,
size = FontSize.SMALL,
"row ownership" = yes
);
AddLabel(
ShowDashboard and isFiveMinute and tradeWindow and !IsNaN(relativeVolume),
"PHASE RVOL: " + AsText(Round(relativeVolume, 2)) +
"x | BREAKOUT NEEDS: " + AsText(Round(VolumeMultiplier, 2)) + "x",
if relativeVolume >= VolumeMultiplier then Color.GREEN
else if relativeVolume < QuietTapeRelativeVolume then Color.LIGHT_ORANGE
else Color.WHITE,
location = Location.TOP_LEFT,
size = FontSize.SMALL,
"row ownership" = yes
);
#=============================================================================
# ALERTS
#=============================================================================
Alert(
EnableAlerts and BullishBreakout,
"SPY 0DTE: CALL breakout confirmed",
Alert.BAR,
Sound.Chimes
);
Alert(
EnableAlerts and BearishBreakout,
"SPY 0DTE: PUT breakdown confirmed",
Alert.BAR,
Sound.Bell
);
Alert(
EnableAlerts and bullEMAExit,
"SPY 0DTE: CALL trend closed below the 9 EMA",
Alert.BAR,
Sound.Ding
);
Alert(
EnableAlerts and bearEMAExit,
"SPY 0DTE: PUT trend closed above the 9 EMA",
Alert.BAR,
Sound.Ding
);
Alert(
EnableTargetAlerts and bullT1Event,
"SPY 0DTE: Bull Target 1 reached",
Alert.BAR,
Sound.Ding
);
Alert(
EnableTargetAlerts and bullT2Event,
"SPY 0DTE: Bull Target 2 reached",
Alert.BAR,
Sound.Ring
);
Alert(
EnableTargetAlerts and bearT1Event,
"SPY 0DTE: Bear Target 1 reached",
Alert.BAR,
Sound.Ding
);
Alert(
EnableTargetAlerts and bearT2Event,
"SPY 0DTE: Bear Target 2 reached",
Alert.BAR,
Sound.Ring
);
Alert(
EnableAlerts and cutoffEvent,
"3:30 PM ET cutoff reached: flatten all 0DTE positions",
Alert.BAR,
Sound.Ring
);