Page 2 of 2
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:40 pm
by PTScalper
Architecture Notes for cTrader
TimeSpan Session Handling: Because cTrader handles timestamps strictly via standard .NET DateTime and TimeSpan structures, IsInSession is written to properly support crossover sessions (e.g., Asian session from 23:00 to 06:00) natively without extra string parsing gymnastics.
Non-Repainting Pivots: The pivot check operates strictly at index - PivotLength. By ensuring the loop checks both pIndex - i and pIndex + i, it guarantees that the $n$ bars to the right of the pivot are completely formed before a label is drawn, adhering to strict price action rules with zero repainting.
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:41 pm
by PTScalper
To bring this up to an institutional standard in cAlgo, the code needs to project the Initial Balance (IB) boundaries visually and manage chart objects cleanly. As you know from building robust .NET architectures, keeping the state lock airtight and preventing chart object memory leaks on every tick is critical.
This upgraded version dynamically renders the Initial Balance range as dotted boundary lines. This gives you immediate visual context: if PB1 forms by piercing back inside the IB range, it is highly likely a liquidity sweep of early breakout traders. If the drive is genuine, PB2 will typically hold outside or exactly at the IB boundary.
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 SessionMicrostructure : Indicator
{
[Parameter("Session Start (HH:mm)", DefaultValue = "08:00", Group = "Session Boundaries")]
public string SessionStartStr { get; set; }
[Parameter("Session End (HH:mm)", DefaultValue = "16:00", Group = "Session Boundaries")]
public string SessionEndStr { get; set; }
[Parameter("Initial Balance Bars", DefaultValue = 12, Group = "Microstructure",
DefaultValue = 12, HelpText = "Number of bars to define the opening range liquidity pool.")]
public int IbBars { get; set; }
[Parameter("Pivot Length (L/R)", DefaultValue = 3, Group = "Microstructure",
HelpText = "Number of bars required to confirm a structural swing high/low.")]
public int PivotLength { get; set; }
private TimeSpan _sessionStart;
private TimeSpan _sessionEnd;
// Session State
private bool _inSession;
private int _sessionStartIndex;
private int _barCount;
private double _ibHigh;
private double _ibLow;
private int _driveDir; // 1 = Bullish Displacement, -1 = Bearish Displacement
private int _pbCount;
protected override void Initialize()
{
_sessionStart = TimeSpan.Parse(SessionStartStr);
_sessionEnd = TimeSpan.Parse(SessionEndStr);
}
public override void Calculate(int index)
{
if (index < PivotLength * 2 + IbBars)
return;
var barTime = Bars.OpenTimes[index];
bool isActive = IsInSession(barTime.TimeOfDay);
bool wasActive = IsInSession(Bars.OpenTimes[index - 1].TimeOfDay);
// =========================================================================
// 1. SESSION BOUNDARY & STATE RESET
// =========================================================================
if (isActive && !wasActive)
{
_inSession = true;
_sessionStartIndex = index;
_barCount = 0;
_ibHigh = double.MinValue;
_ibLow = double.MaxValue;
_driveDir = 0;
_pbCount = 0;
}
if (!isActive)
{
_inSession = false;
return;
}
_barCount++;
// =========================================================================
// 2. INITIAL BALANCE (IB) LIQUIDITY POOL
// =========================================================================
string dateStr = barTime.ToString("yyyyMMdd");
string ibHighName = $"IB_H_{dateStr}";
string ibLowName = $"IB_L_{dateStr}";
if (_barCount <= IbBars)
{
_ibHigh = Math.Max(_ibHigh, Bars.HighPrices[index]);
_ibLow = Math.Min(_ibLow, Bars.LowPrices[index]);
}
// Draw and extend the IB boundaries across the session
Chart.DrawTrendLine(ibHighName, _sessionStartIndex, _ibHigh, index, _ibHigh, Color.FromArgb(120, Color.DimGray), 1, LineStyle.Dots);
Chart.DrawTrendLine(ibLowName, _sessionStartIndex, _ibLow, index, _ibLow, Color.FromArgb(120, Color.DimGray), 1, LineStyle.Dots);
// =========================================================================
// 3. ESTABLISH DRIVE DIRECTION (DISPLACEMENT)
// =========================================================================
if (_barCount > IbBars && _driveDir == 0)
{
if (Bars.ClosePrices[index] > _ibHigh)
_driveDir = 1;
else if (Bars.ClosePrices[index] < _ibLow)
_driveDir = -1;
}
// =========================================================================
// 4. STRUCTURAL PULLBACK IDENTIFICATION
// =========================================================================
int pIndex = index - PivotLength;
if (_driveDir != 0 && _barCount > IbBars + PivotLength)
{
bool isPivotHigh = true;
bool isPivotLow = true;
// Validate pure price action structure (no repainting)
for (int i = 1; i <= PivotLength; i++)
{
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;
}
if (_driveDir == 1 && isPivotLow)
{
ProcessPullback(pIndex, Bars.LowPrices[pIndex], isBelow: true);
}
if (_driveDir == -1 && isPivotHigh)
{
ProcessPullback(pIndex, Bars.HighPrices[pIndex], isBelow: false);
}
}
}
private bool IsInSession(TimeSpan timeOfDay)
{
if (_sessionStart <= _sessionEnd)
return timeOfDay >= _sessionStart && timeOfDay < _sessionEnd;
return timeOfDay >= _sessionStart || timeOfDay < _sessionEnd;
}
private void ProcessPullback(int pIndex, double price, bool isBelow)
{
string tag = $"PB_{Bars.OpenTimes[pIndex]:yyyyMMdd_HHmm}";
// Concurrency/State Lock: Prevent duplicate processing on live ticks
if (Chart.FindObject(tag) != null)
return;
_pbCount++;
string text = _pbCount == 1 ? "PB1 (Inducement)" : "PB2 (Structural)";
Color col = _pbCount == 1 ? Color.DarkOrange : Color.SeaGreen;
var chartText = Chart.DrawText(tag, " " + text, pIndex, price, col);
chartText.VerticalAlignment = isBelow ? VerticalAlignment.Top : VerticalAlignment.Bottom;
chartText.HorizontalAlignment = HorizontalAlignment.Center;
}
}
}
Re: First pullback after London drive vs second pullback: which I take
Posted: Sun Sep 20, 2026 8:42 pm
by PTScalper
Institutional Architecture Details
Visual Liquidity Boundaries: The Chart.DrawTrendLine logic dynamically projects the Initial Balance opening range across the entire session. By using Color.FromArgb(120, Color.DimGray), the lines stay translucent and out of the way of your candlesticks while providing the exact thresholds needed to spot an internal liquidity sweep.
Tick-Safe Object Management: In cTrader, Calculate() fires on every tick. The strict use of unique string tags ($"IB_H_{dateStr}" and $"PB_{...}") ensures that the indicator intelligently updates existing lines rather than drawing thousands of overlapping objects, keeping memory utilization flat.
Pure Microstructure Validation: The pivot loop operates exclusively on historical locked arrays (pIndex - i and pIndex + i), ensuring that the moment a PB1 or PB2 tag prints on your chart, the structural swing is mathematically locked. It will never repaint or vanish on a sudden spread widening.
Re: First pullback after London drive vs second pullback: which I take
Posted: Mon Sep 21, 2026 9:37 pm
by LondonScalper
PTScalper wrote:The first pullback after the initial London drive is structurally vulnerable because it is frequently engineered as a liquidity sweep. Waiting for PB2 — where a higher low or lower high is successfully defended — proves the market structure is real.
That matches my journal curve as well. PB1 after the London drive is often inducement: it traps early continuation and funds the real leg. Waiting for PB2 costs the occasional V-shaped runner and still saves more R across a month of opens. Your NY-overlap exception is fair — if London already locked a heavy unidirectional day and New York continues, the first pullback in that window can hold. In the opening hours of London, PB1 stays a coin flip unless the catalyst was a genuine shock.
Sizing stays 1R on the playbook ticket. Revenge size on PB2 after a PB1 loss is tilt wearing a system badge.
Desk rule:
London open = PB2 default; PB1 only with written catalyst exception; size never changes after a PB1 scratch.
When you do take the NY-overlap PB1, do you require the London drive to have already printed a minimum R move, or is unidirectional tape enough?
Re: First pullback after London drive vs second pullback: which I take
Posted: Thu Sep 24, 2026 8:23 am
by LondonNewsTrader
PTScalper wrote: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.
The PB1 versus PB2 labelling is where I'd spend the effort, because with pivot_len at 3 on a 5-minute chart a lot of small pauses will count as pullbacks. A drive that stalls for fifteen minutes and continues can already be on 'PB2' in the script while a human would say it hasn't pulled back yet. Requiring a minimum retracement, say a third of the leg from the IB break to the latest extreme, before a pivot counts would line the labels up much better with the opening post.
The drive direction is also locked for the session once set. drive_dir is only assigned while it's zero, so if London breaks the IB high at 09:15 and then fully reverses through the IB low by 11:00, the script keeps labelling pullbacks in a bullish drive that no longer exists. A reset on a close beyond the opposite side of the IB would handle that.
On the 0800-1600 window: it includes the 13:30 London slot where most US data lands. Morning pullback structure often gets erased in one candle there, so I'd either stop the count at 13:15 or treat anything after a release as a new sequence.
Agreed that sizing up on PB2 to recover a PB1 loss contaminates the whole model.