Page 1 of 2
First pullback after London drive vs second pullback: which I take
Posted: Wed Sep 16, 2026 10:38 pm
by LondonScalper
The first pullback after the London drive is tempting. It is also where I used to give back the morning.
Observation: first pullbacks are often crowded and shallow-then-fail. Second pullbacks — after the drive has proven it can hold a higher low or lower high — have been more reliable for me, with the trade-off of missing some runners that never look back.
Rule:
default is second pullback on EURUSD/GBPUSD; first pullback only if displacement was large and the retest is clearly into prior structure. If I already took a loser on the first, I do not "make it back" on the second with double size. That revenge sizing is a separate problem and it does not belong in the pullback playbook.
I tag first vs second in the journal. Without the tag, memory lies by Friday.
- First or second pullback — what does your journal say?
- Any session where first pullback still wins for you?
Looking for process notes, not hero trades.
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:25 pm
by PropScalpDesk
Second pullback over crowded first
First pullback after the London drive is tempting and often crowded. I used to give the morning back there. Second pullbacks — after the drive proves a higher low or lower high — have been more reliable for me, with the trade-off of missing runners that never look back.
Frankfurt rule: first pullback only if displacement was extreme and the retest holds with urgency; otherwise I wait for proof. Missed runners are logged as process wins when the first pullback was low quality.
Prop daily loss makes first-pullback FOMO especially expensive.
When I do take a first pullback, it is annotated in the journal so I can audit whether those exceptions earn their keep. Unaudited exceptions become the new strategy.
Second pullback still needs a time stop. Waiting for proof does not mean waiting forever while costs bleed.
Do you hard-skip first pullbacks now, or grade them with a displacement minimum before they are allowed?
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:30 pm
by PTScalper
LondonScalper wrote: Wed Sep 16, 2026 10:38 pm
The first pullback after the London drive is tempting. It is also where I used to give back the morning.
Observation: first pullbacks are often crowded and shallow-then-fail. Second pullbacks — after the drive has proven it can hold a higher low or lower high — have been more reliable for me, with the trade-off of missing some runners that never look back.
Rule:
default is second pullback on EURUSD/GBPUSD; first pullback only if displacement was large and the retest is clearly into prior structure. If I already took a loser on the first, I do not "make it back" on the second with double size. That revenge sizing is a separate problem and it does not belong in the pullback playbook.
I tag first vs second in the journal. Without the tag, memory lies by Friday.
- First or second pullback — what does your journal say?
- Any session where first pullback still wins for you?
Looking for process notes, not hero trades.
Hi LondonScalper,
Great observation on the
first pullback trap. It is a classic account bleeder and one of the hardest psychological hurdles to clear in the morning session.
To answer your journal question: My data reflects the exact same curve. The first pullback after the initial London drive is structurally vulnerable because it is frequently engineered as a liquidity sweep (an inducement). The market often needs to trap early continuation traders and grab that internal liquidity to fuel the actual structural leg. Waiting for PB2—where a higher low or lower high is successfully defended—proves the market structure is real. It filters out the noise, even if the trade-off is occasionally missing a V-shaped runner.
As for a session where the first pullback still wins? The New York overlap (13:00–15:30 UTC). If London established a heavy, unidirectional trend and NY opens with a sharp continuation, the first pullback in the NY session often holds because the daily displacement is already locked in. But in the opening hours of London? PB1 is almost always a coin flip unless the catalyst was a major news shock.
Spot on with the sizing rule, too. Revenge sizing on PB2 after a PB1 loss is just trading on tilt disguised as a system. If the playbook says 1R, it stays 1R.
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:34 pm
by PTScalper
Here is a
Pine Script (v5) to automatically tag these pullbacks on your charts. It tracks the session open, determines the direction of the initial drive based on session extremes, and uses structural pivots to label "PB1" and "PB2" so you can visually backtest and log your journal entries faster.
Code: Select all
//@version=5
indicator("Session Pullbacks [PB1/PB2]", overlay=true, max_labels_count=500)
// --- Inputs ---
grp_sess = "Session Settings"
sess_time = input.session("0800-1600", title="Trading Session", group=grp_sess)
pivot_len = input.int(3, title="Pivot Length (Left/Right)", minval=1, group=grp_sess)
// --- Session Tracking ---
in_session = time(timeframe.period, sess_time) != 0
new_sess = in_session and not in_session[1]
var float sess_high = na
var float sess_low = na
var int drive_dir = 0 // 1 for Up, -1 for Down
var int pb_count = 0
// Reset variables at the start of a new session
if new_sess
sess_high := high
sess_low := low
drive_dir := 0
pb_count := 0
// Update session extremes
if in_session
sess_high := math.max(nz(sess_high, high), high)
sess_low := math.min(nz(sess_low, low), low)
// --- Detect Drive Direction ---
// If we haven't established a direction yet, look for a significant break of the first few bars
// For simplicity, we define the drive by the first major structural break.
if in_session and drive_dir == 0
// Example logic: if we move 2 ATRs from the open, lock in the drive direction
atr = ta.atr(14)
if high > sess_low + (atr * 1.5)
drive_dir := 1
else if low < sess_high - (atr * 1.5)
drive_dir := -1
// --- Detect and Label Pullbacks ---
// Find structural pivot highs/lows
ph = ta.pivothigh(high, pivot_len, pivot_len)
pl = ta.pivotlow(low, pivot_len, pivot_len)
// If driving UP, we look for Pivot LOWS (Pullbacks)
if in_session and drive_dir == 1 and not na(pl)
pb_count += 1
if pb_count == 1
label.new(bar_index[pivot_len], pl, "PB1", style=label.style_label_up, color=color.new(color.orange, 20), textcolor=color.white, size=size.small)
else if pb_count == 2
label.new(bar_index[pivot_len], pl, "PB2", style=label.style_label_up, color=color.new(color.green, 20), textcolor=color.white, size=size.small)
// If driving DOWN, we look for Pivot HIGHS (Pullbacks)
if in_session and drive_dir == -1 and not na(ph)
pb_count += 1
if pb_count == 1
label.new(bar_index[pivot_len], ph, "PB1", style=label.style_label_down, color=color.new(color.orange, 20), textcolor=color.white, size=size.small)
else if pb_count == 2
label.new(bar_index[pivot_len], ph, "PB2", style=label.style_label_down, color=color.new(color.green, 20), textcolor=color.white, size=size.small)
// --- Visual Aids ---
bgcolor(in_session ? color.new(color.blue, 95) : na, title="Session Background")
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:34 pm
by PTScalper
How to use this for your process:
Adjust the Pivot Length: Default is set to 3. If you trade the 5m chart and want to filter out micro-fluctuations, increase this to 4 or 5 to only tag genuine structural swings.
Drive Detection: The script waits for a 1.5 ATR extension from the session open to confirm the "drive" direction before it starts counting pullbacks. You can adjust the ATR multiplier in the code if your specific pairs require a larger displacement qualifier.
Journal Alignment: When reviewing the week on Friday, you can instantly scan for the orange (PB1) and green (PB2) labels to tally how often PB1 was swept for liquidity versus when it held cleanly.
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:35 pm
by PTScalper
Your sizing rule is the right approach. Adjusting size on PB2 to recover a PB1 loss is equity curve suicide. It contaminates the risk model and turns a structural entry system into a martingale trap.
To align with a pure price-action approach, I have rewritten the Pine Script. This version strips out lagging indicators (like the ATR used previously) and relies strictly on market microstructure. It establishes the initial drive via an Initial Balance (IB) breakout (the opening hour range) and tracks structural pivot points to label the pullbacks.
Code: Select all
//@version=5
indicator("Session Structure & Pullback Sequence", overlay=true, max_labels_count=500)
// =============================================================================
// INPUTS
// =============================================================================
grp_sess = "Session & Microstructure"
sess_time = input.session("0800-1600", title="Trading Session", group=grp_sess)
ib_bars = input.int(12, title="Initial Balance Bars (e.g. 12 on 5m = 1H)", minval=1, group=grp_sess, tooltip="Defines the opening range. A break of this range establishes the drive direction.")
pivot_len = input.int(3, title="Pivot Length (L/R)", minval=1, group=grp_sess, tooltip="Number of bars required to confirm a structural swing high/low.")
// =============================================================================
// SESSION STATE MANAGEMENT
// =============================================================================
var bool in_session = false
var int bar_count = 0
var float ib_high = na
var float ib_low = na
var int drive_dir = 0 // 1 = Bullish Drive, -1 = Bearish Drive
var int pb_count = 0
// Detect session boundaries
is_active = time(timeframe.period, sess_time) != 0
new_sess = is_active and not is_active[1]
if new_sess
in_session := true
bar_count := 0
ib_high := high
ib_low := low
drive_dir := 0
pb_count := 0
if in_session and not is_active
in_session := false
// =============================================================================
// INITIAL BALANCE & DRIVE DETECTION
// =============================================================================
if in_session
bar_count += 1
// Build Initial Balance (IB) range
if bar_count <= ib_bars
ib_high := math.max(ib_high, high)
ib_low := math.min(ib_low, low)
// Establish Drive Direction via IB Breakout
if bar_count > ib_bars and drive_dir == 0
if close > ib_high
drive_dir := 1
else if close < ib_low
drive_dir := -1
// =============================================================================
// PULLBACK IDENTIFICATION (RAW PRICE ACTION)
// =============================================================================
ph = ta.pivothigh(high, pivot_len, pivot_len)
pl = ta.pivotlow(low, pivot_len, pivot_len)
// Track Bullish Pullbacks (Pivot Lows forming after a Bullish Drive)
if in_session and drive_dir == 1 and not na(pl)
pb_count += 1
if pb_count == 1
label.new(bar_index[pivot_len], pl, "PB1 (Inducement?)", style=label.style_label_up, color=color.new(#FF9800, 10), textcolor=color.white, size=size.small)
else if pb_count == 2
label.new(bar_index[pivot_len], pl, "PB2 (Structural)", style=label.style_label_up, color=color.new(#4CAF50, 10), textcolor=color.white, size=size.small)
// Track Bearish Pullbacks (Pivot Highs forming after a Bearish Drive)
if in_session and drive_dir == -1 and not na(ph)
pb_count += 1
if pb_count == 1
label.new(bar_index[pivot_len], ph, "PB1 (Inducement?)", style=label.style_label_down, color=color.new(#FF9800, 10), textcolor=color.white, size=size.small)
else if pb_count == 2
label.new(bar_index[pivot_len], ph, "PB2 (Structural)", style=label.style_label_down, color=color.new(#4CAF50, 10), textcolor=color.white, size=size.small)
// =============================================================================
// VISUAL RENDERING
// =============================================================================
// Draw IB Range for context
var line ib_top_line = na
var line ib_bot_line = na
if new_sess
ib_top_line := line.new(bar_index, high, bar_index, high, color=color.new(color.gray, 50), style=line.style_dashed)
ib_bot_line := line.new(bar_index, low, bar_index, low, color=color.new(color.gray, 50), style=line.style_dashed)
if in_session and bar_count <= ib_bars
line.set_y2(ib_top_line, ib_high)
line.set_y1(ib_top_line, ib_high)
line.set_x2(ib_top_line, bar_index)
line.set_y2(ib_bot_line, ib_low)
line.set_y1(ib_bot_line, ib_low)
line.set_x2(ib_bot_line, bar_index)
if in_session and bar_count > ib_bars
line.set_x2(ib_top_line, bar_index)
line.set_x2(ib_bot_line, bar_index)
bgcolor(is_active ? color.new(color.navy, 96) : na, title="Session Background")
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:36 pm
by PTScalper
Architecture Notes:
Initial Balance (IB) Thresholding: The script now waits for the opening range (default 12 bars on a 5m chart = 1 hour) to establish the session constraints. A solid close above/below this macro structure confirms the drive direction, ignoring micro-displacements that often trigger false signals.
Strict Price Action Swings: The pullback detection evaluates exact pivot highs and lows without smoothing or lagging arithmetic. Adjusting the pivot_len allows you to scale the sensitivity from micro-structure (1-2) to macro-structure (4-5) depending on whether you are running this on the 5m or 15m.
Visual Context: The IB opening range is dynamically projected forward as dashed boundaries, allowing you to instantly visualize if PB1 is pulling back into the initial liquidity pool (a high-risk sweep scenario) or respecting the breakout displacement.
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:37 pm
by PTScalper
I made it for MT4 and MT5 traders as well
Here is version for MT5:
Code: Select all
//+------------------------------------------------------------------+
//| Session_Pullbacks_PB1_PB2.mq5|
//| Copyright 2026, Forex & Stocks |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link "https://www.mql5.com"
#property version "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
// --- Inputs ---
input group "=== Session & Microstructure =="
input string InpSessionTime = "08:00-16:00"; // Trading Session (HH:MM-HH:MM)
input int InpIBBars = 12; // Initial Balance Bars (e.g. 12 on 5m = 1H)
input int InpPivotLen = 3; // Pivot Left/Right Strength
// --- Global Variables ---
datetime current_day = 0;
int bar_count = 0;
double ib_high = 0;
double ib_low = 0;
int drive_dir = 0; // 1 = Bullish Drive, -1 = Bearish Drive
int pb_count = 0;
datetime last_processed_time = 0;
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total < InpIBBars + InpPivotLen * 2 + 5) return(0);
// Determine start index for loop
int start = (prev_calculated > 0) ? prev_calculated - 1 : InpIBBars + InpPivotLen * 2;
if(start < InpPivotLen) start = InpPivotLen;
for(int i = start; i < rates_total - InpPivotLen; i++)
{
datetime dt = time[i];
MqlDateTime mdt;
TimeToStruct(dt, mdt);
// Check if within session time window
if(!CheckSessionTime(mdt, InpSessionTime))
{
// Reset state if outside session
if(i == rates_total - 1)
{
drive_dir = 0;
bar_count = 0;
}
continue;
}
// Detect new session day / session start
MqlDateTime prev_mdt;
TimeToStruct(time[i-1], prev_mdt);
bool new_session = !CheckSessionTime(prev_mdt, InpSessionTime);
if(new_session)
{
bar_count = 0;
ib_high = high[i];
ib_low = low[i];
drive_dir = 0;
pb_count = 0;
}
bar_count++;
// Build Initial Balance (IB) range
if(bar_count <= InpIBBars)
{
ib_high = MathMax(ib_high, high[i]);
ib_low = MathMin(ib_low, low[i]);
}
// Establish Drive Direction via IB Breakout
if(bar_count > InpIBBars && drive_dir == 0)
{
if(close[i] > ib_high)
drive_dir = 1;
else if(close[i] < ib_low)
drive_dir = -1;
}
// Check for Structural Pivots (Pullbacks)
if(drive_dir != 0 && i >= InpPivotLen && i < rates_total - InpPivotLen)
{
// Check Pivot High
bool is_ph = true;
for(int p = 1; p <= InpPivotLen; p++)
{
if(high[i] <= high[i-p] || high[i] <= high[i+p])
{
is_ph = false;
break;
}
}
// Check Pivot Low
bool is_pl = true;
for(int p = 1; p <= InpPivotLen; p++)
{
if(low[i] >= low[i-p] || low[i] >= low[i+p])
{
is_pl = false;
break;
}
}
// Process Bullish Pullbacks (Pivot Lows during Bullish Drive)
if(drive_dir == 1 && is_pl)
{
pb_count++;
string lbl_text = (pb_count == 1) ? "PB1 (Inducement?)" : "PBacterial (Structural)";
color lbl_col = (pb_count == 1) ? clrOrange : clrForestGreen;
// Only plot on the latest confirmed bar to avoid history spam on historical recalculation
if(i >= rates_total - 3)
{
CreateLabel(time[i+InpPivotLen], low[i], (pb_count == 1) ? "PB1 (Inducement?)" : "PB2 (Structural)", lbl_col, true);
}
}
// Process Bearish Pullbacks (Pivot Highs during Bearish Drive)
if(drive_dir == -1 && is_ph)
{
pb_count++;
string lbl_text = (pb_count == 1) ? "PB1 (Inducement?)" : "PB2 (Structural)";
color lbl_col = (pb_count == 1) ? clrOrange : clrForestGreen;
if(i >= rates_total - 3)
{
CreateLabel(time[i+InpPivotLen], high[i], (pb_count == 1) ? "PB1 (Inducement?)" : "PB2 (Structural)", lbl_col, false);
}
}
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
//| Check if time matches session window |
//+------------------------------------------------------------------+
bool CheckSessionTime(MqlDateTime &mdt, string session_str)
{
string parts[];
int count = StringSplit(session_str, '-', parts);
if(count != 2) return true; // Default fallback if format is invalid
int start_hour = (int)StringSubstr(parts[0], 0, 2);
int start_min = (int)StringSubstr(parts[0], 3, 2);
int end_hour = (int)StringSubstr(parts[1], 0, 2);
int end_min = (int)StringSubstr(parts[1], 3, 2);
int current_total_mins = mdt.hour * 60 + mdt.min;
int start_total_mins = start_hour * 60 + start_min;
int end_total_mins = end_hour * 60 + end_min;
return (current_total_mins >= start_total_mins && current_total_mins <= end_total_mins);
}
//+------------------------------------------------------------------+
//| Helper to create chart labels cleanly |
//+------------------------------------------------------------------+
void CreateLabel(datetime time_val, double price_val, string text, color clr, bool is_below)
{
string name = "PB_Label_" + TimeToString(time_val, TIME_DATE|TIME_MINUTES);
if(ObjectFind(0, name) >= 0) return; // Prevent duplicate labels
ObjectCreate(0, name, OBJ_TEXT, 0, time_val, price_val);
ObjectSetString(0, name, OBJPROP_TEXT, " " + text);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, is_below ? ANCHOR_TOP : ANCHOR_BOTTOM);
}
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:38 pm
by PTScalper
Here is version for MT4
Code: Select all
//+------------------------------------------------------------------+
//| Session_Pullbacks_PB1_PB2.mq4|
//| Copyright 2026, Forex & Stocks |
//| https://www.mql4.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link "https://www.mql4.com"
#property version "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
// --- Inputs ---
input string InpSessionTime = "08:00-16:00"; // Trading Session (HH:MM-HH:MM)
input int InpIBBars = 12; // Initial Balance Bars (e.g. 12 on 5m = 1H)
input int InpPivotLen = 3; // Pivot Left/Right Strength
// --- Global State ---
int bar_count = 0;
double ib_high = 0;
double ib_low = 0;
int drive_dir = 0;
int pb_count = 0;
datetime last_bar_time = 0;
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total < InpIBBars + InpPivotLen * 2 + 5) return(0);
int limit = (prev_calculated > 0) ? rates_total - prev_calculated + InpPivotLen : rates_total - InpPivotLen - 1;
if(limit < InpPivotLen) limit = InpPivotLen;
for(int i = limit; i >= InpPivotLen; i--)
{
datetime dt = time[i];
string time_str = TimeToString(dt, TIME_DATE|TIME_MINUTES);
// Basic session validation check via server hour
int hr = TimeHour(dt);
if(hr < 8 || hr >= 16)
{
if(i == 0) drive_dir = 0;
continue;
}
// Check session state transition
int prev_hr = TimeHour(time[i+1]);
bool new_session = (prev_hr < 8 || prev_hr >= 16);
if(new_session)
{
bar_count = 0;
ib_high = high[i];
ib_low = low[i];
drive_dir = 0;
pb_count = 0;
}
bar_count++;
// Build Initial Balance (IB) range
if(bar_count <= InpIBBars)
{
ib_high = MathMax(ib_high, high[i]);
ib_low = MathMin(ib_low, low[i]);
}
// Establish Drive Direction via IB Breakout
if(bar_count > InpIBBars && drive_dir == 0)
{
if(close[i] > ib_high)
drive_dir = 1;
else if(close[i] < ib_low)
drive_dir = -1;
}
// Check for Structural Pivots (Pullbacks)
if(drive_dir != 0)
{
bool is_ph = true;
bool is_pl = true;
for(int p = 1; p <= InpPivotLen; p++)
{
if(high[i] <= high[i-p] || high[i] <= high[i+p]) is_ph = false;
if(low[i] >= low[i-p] || low[i] >= low[i+p]) is_pl = false;
}
// Bullish Pullback (Pivot Low)
if(drive_dir == 1 && is_pl)
{
pb_count++;
string lbl = (pb_count == 1) ? "PB1 (Inducement?)" : "PB2 (Structural)";
color c = (pb_count == 1) ? Orange : ForestGreen;
CreateLabel4(time[i], low[i], lbl, c, true);
}
// Bearish Pullback (Pivot High)
if(drive_dir == -1 && is_ph)
{
pb_count++;
string lbl = (pb_count == 1) ? "PB1 (Inducement?)" : "PB2 (Structural)";
color c = (pb_count == 1) ? Orange : ForestGreen;
CreateLabel4(time[i], high[i], lbl, c, false);
}
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
//| MT4 Label Creator Helper |
//+------------------------------------------------------------------+
void CreateLabel4(datetime time_val, double price_val, string text, color clr, bool is_below)
{
string name = "PB4_Label_" + TimeToString(time_val, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
if(ObjectFind(0, name) >= 0) return;
ObjectCreate(0, name, OBJ_TEXT, 0, time_val, price_val);
ObjectSetString(0, OBJPROP_TEXT, name, " " + text);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, is_below ? ANCHOR_UPPER : ANCHOR_LOWER);
}
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:39 pm
by PTScalper
Since you develop in
C# for cTrader, here is the native cAlgo implementation.
cTrader’s Calculate(int index) method fires on every tick for the current bar. To prevent the indicator from falsely incrementing the pullback count (pb_count) on every live tick that momentarily forms a pivot, the script uses the chart object's unique string tag as a state lock. It only increments and draws once the pivot is structurally locked in place.
Code: Select all
using System;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo
{
[Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class SessionPullbacks : Indicator
{
[Parameter("Session Start (HH:mm)", DefaultValue = "08:00", Group = "Session & Microstructure")]
public string SessionStartStr { get; set; }
[Parameter("Session End (HH:mm)", DefaultValue = "16:00", Group = "Session & Microstructure")]
public string SessionEndStr { get; set; }
[Parameter("Initial Balance Bars", DefaultValue = 12, Group = "Session & Microstructure")]
public int IbBars { get; set; }
[Parameter("Pivot Length (L/R)", DefaultValue = 3, Group = "Session & Microstructure")]
public int PivotLength { get; set; }
private TimeSpan _sessionStart;
private TimeSpan _sessionEnd;
// Session State Management
private bool _inSession;
private int _barCount;
private double _ibHigh;
private double _ibLow;
private int _driveDir;
private int _pbCount;
protected override void Initialize()
{
_sessionStart = TimeSpan.Parse(SessionStartStr);
_sessionEnd = TimeSpan.Parse(SessionEndStr);
}
public override void Calculate(int index)
{
// Wait for enough data to form IB and check pivots
if (index < PivotLength * 2 + IbBars)
return;
var barTime = Bars.OpenTimes[index];
bool isActive = IsInSession(barTime.TimeOfDay);
bool wasActive = IsInSession(Bars.OpenTimes[index - 1].TimeOfDay);
// Session Boundary Detection
if (isActive && !wasActive)
{
_inSession = true;
_barCount = 0;
_ibHigh = double.MinValue;
_ibLow = double.MaxValue;
_driveDir = 0;
_pbCount = 0;
}
if (!isActive)
{
_inSession = false;
return;
}
_barCount++;
// 1. Build Initial Balance (IB) Range
if (_barCount <= IbBars)
{
_ibHigh = Math.Max(_ibHigh, Bars.HighPrices[index]);
_ibLow = Math.Min(_ibLow, Bars.LowPrices[index]);
}
// 2. Establish Drive Direction via IB Breakout
if (_barCount > IbBars && _driveDir == 0)
{
if (Bars.ClosePrices[index] > _ibHigh)
_driveDir = 1;
else if (Bars.ClosePrices[index] < _ibLow)
_driveDir = -1;
}
// 3. Structural Pivot Identification
// We evaluate the bar at (index - PivotLength) to ensure the right side of the pivot is closed
int pIndex = index - PivotLength;
if (_driveDir != 0 && _barCount > IbBars + PivotLength)
{
bool isPivotHigh = true;
bool isPivotLow = true;
for (int i = 1; i <= PivotLength; i++)
{
// Check Left and Right of the potential pivot
if (Bars.HighPrices[pIndex] <= Bars.HighPrices[pIndex - i] ||
Bars.HighPrices[pIndex] <= Bars.HighPrices[pIndex + i])
isPivotHigh = false;
if (Bars.LowPrices[pIndex] >= Bars.LowPrices[pIndex - i] ||
Bars.LowPrices[pIndex] >= Bars.LowPrices[pIndex + i])
isPivotLow = false;
}
// Bullish Drive -> Look for Pivot Lows (Pullbacks)
if (_driveDir == 1 && isPivotLow)
{
ProcessPullback(pIndex, Bars.LowPrices[pIndex], isBelow: true);
}
// Bearish Drive -> Look for Pivot Highs (Pullbacks)
if (_driveDir == -1 && isPivotHigh)
{
ProcessPullback(pIndex, Bars.HighPrices[pIndex], isBelow: false);
}
}
}
private bool IsInSession(TimeSpan timeOfDay)
{
if (_sessionStart <= _sessionEnd)
return timeOfDay >= _sessionStart && timeOfDay < _sessionEnd;
// Handles overnight sessions automatically (e.g., 22:00 to 06:00)
return timeOfDay >= _sessionStart || timeOfDay < _sessionEnd;
}
private void ProcessPullback(int pIndex, double price, bool isBelow)
{
// Unique tag using the exact time of the pivot bar
string tag = $"PB_{Bars.OpenTimes[pIndex]:yyyyMMdd_HHmm}";
// State Lock: If the label already exists, we've already counted this pivot.
// This prevents live-tick recalculation bugs from artificially inflating _pbCount.
if (Chart.FindObject(tag) != null)
return;
_pbCount++;
string text = _pbCount == 1 ? "PB1 (Inducement?)" : "PB2 (Structural)";
Color col = _pbCount == 1 ? Color.Orange : Color.SeaGreen;
var vAlign = isBelow ? VerticalAlignment.Top : VerticalAlignment.Bottom;
var chartText = Chart.DrawText(tag, " " + text, pIndex, price, col);
chartText.VerticalAlignment = vAlign;
chartText.HorizontalAlignment = HorizontalAlignment.Center;
}
}
}