Engineering11 min read

Build in Public: Every session has a high and a low — and both get hunted

PDH/PDL gave the engine two liquidity magnets a day. But institutional desks don't just hunt yesterday's high — they hunt Asia's low before London commits, and London's high before New York reverses it. Here's how six new POI types, a shared invalidation core, and a full SMC/ICT context block got wired into the pipeline — and the naming mistake I made twice before shipping it.

By

POISMC/ICTliquidityarchitecturebuild-in-publicPythonRedisagents

In brief

  • Six new POI types — one high/low pair per killzone (Asia, London, New York) — give the engine intraday liquidity magnets on top of the existing PDH/PDL, using the same entry-model confirmation pipeline.
  • A generic invalidate_single_level_poi() core now backs PDH/PDL, PWH/PWL, and the new session levels — one implementation instead of three near-identical copies.
  • The entry agent doesn't just see a price level: it gets sweep depth, reclaim status, both-sides-taken state, HTF liquidity confluence, and a deterministic Judas Sweep flag — all computed in code, never inferred by the LLM.
  • The POI type naming went through two rounds of collisions before shipping — SSL collided with Sell-Side Liquidity, then SL collided with Stop Loss — solved by making the session itself part of the type instead of trying to disambiguate in prose.

The engine has had PDH/PDL — previous day high and low — for a while. Two liquidity magnets, refreshed once a day, rotated through the same pipeline as every other POI: price approaches, the LTF radar wakes up, it hunts for a ChoCH and a displacement, and only then does an entry get considered.

It's a good mechanism. It's also incomplete, because institutional desks don't only hunt yesterday's high. They hunt this morning's Asian range before London commits to a direction. They hunt London's high before New York reverses it. The session clock — not just the daily clock — produces liquidity pools that get swept on a schedule far tighter than once a day.

PDH/PDL gives you two chances a day. Session liquidity gives you up to six.

Why the session clock matters here specifically

I'd already wired the session clock into position management — five boundary events that make the Sentinel re-review open positions when London opens or New York closes. That work gave the platform DST-aware, timezone-correct session boundaries as a shared module.

This is a different use of the same clock. Instead of "review positions when a session changes," the question is "did this session's range just get raided, and is that raid the setup?"

The SMC/ICT reasoning is specific:

  • Asia's range (23:00–02:00 UTC) is typically tight, low-volume, low-conviction. Its high and low are exactly the kind of resting liquidity that gets swept once real volume shows up.
  • London open (06:00–09:00 UTC) is where that sweep usually happens — the so-called Judas Swing. Price often raids the Asian extreme, triggers retail stops sitting there, then reverses into the actual daily direction.
  • New York (12:00–15:00 UTC) is the London-NY overlap, peak liquidity, where London's move either gets confirmed or gets swept in turn.

Same mechanic as PDH/PDL — magnet, sweep, confirmation, entry — just running on a tighter, more mechanically-timed clock. And because the entry model still requires a confirmed LTF ChoCH and displacement before anything fires, a session level is exactly as strict a POI as a daily one. It's not a lower bar. It's a different clock.

Six types, not two

The design was a pair per session: ASH/ASL (Asia High/Low), LSH/LSL (London), NYSH/NYSL (New York). A session field carries which killzone the level belongs to, independently of the type string.

SESSION_POI_TYPES: Dict[str, str] = {
    "ASH": "Asia Session High",   "ASL": "Asia Session Low",
    "LSH": "London Session High", "LSL": "London Session Low",
    "NYSH": "New York Session High", "NYSL": "New York Session Low",
}

Every one of the six ends in exactly two characters — SH or SL — so direction logic never branches six ways. It's a suffix check:

def is_session_high_type(poi_type): return poi_type in SESSION_POI_TYPES and poi_type.endswith("SH")
def is_session_low_type(poi_type):  return poi_type in SESSION_POI_TYPES and poi_type.endswith("SL")
 
def session_poi_opposite_type(poi_type: str) -> str:
    """The sibling level of the same session — 'NYSH' -> 'NYSL'."""
    return poi_type[:-2] + ("SL" if poi_type.endswith("SH") else "SH")

That last function matters more than it looks — it's how the entry context later answers "has the other side of this range already been swept."

The window that decides whether a level exists yet

Killzone math looks trivial until you have to decide, at any arbitrary moment, which window is "the last completed one." Asia crosses midnight. And if the current moment is inside a killzone, the window that just closed is yesterday's, not today's.

def last_completed_killzone_window_utc(session, now):
    """Returns (window_start, window_end, next_window_start) for the most
    recently COMPLETED killzone window. next_window_start is also the
    level's natural expiry — the start of the next same-session window."""

At 00:30 UTC, mid-Asia-session, the function correctly returns yesterday's Asia window as the last completed one — and its next_window_start (23:00 UTC today) is already in the past relative to "the next Asia session," which tells the worker: don't create this level, it's stale. That guard is what stops the worker from resurrecting a dead level the moment its own session starts again.

I deliberately used the same killzone windows the platform already uses for per-session PnL stats and RAG grouping (trading_sessions.py), not the DST-aware full-session boundaries from the position-management post. Different job, different clock: position management needs to know when institutional desks are opening and closing; a session liquidity POI needs to match the exact window the rest of the platform calls "Asia" when it reports your session-by-session edge.

How long does a session level stay alive?

This was the one open design question I sat with before writing any code: does Asia's low stay valid only until London closes, or all the way until the next Asia session starts?

I went with the wider window — a level lives until the start of the next same-session window, roughly 24 hours. Asia's low stays a legitimate target through both London and New York, mirroring how PDH/PDL already behaves (a daily level is valid all day, not just for the next few hours). The alternative — expiring at the next session's close — would have quietly cut off exactly the "NY sweeps Asia's low" setups that are some of the better session-liquidity plays.

def invalidate_expired_session_poi(poi, now=None) -> dict:
    """Level-triggered check: now >= expires_at. Robust to bot downtime —
    doesn't require catching the exact session-boundary moment."""

expires_at is written once, at creation, as the next window's start timestamp. The check is a plain comparison run on every worker cycle — not an edge-triggered "did we just cross midnight" event. If the bot is down when a boundary passes, the very next tick after restart still catches it and expires the level correctly. Redis TTL backs it up as a second line of defense, set to expires_at plus a margin.

One invalidation core instead of three copies

invalidate_poi_pdl_pdh and invalidate_poi_pwh_pwl were, before this, two ~180-line functions that differed in exactly one thing: which direction counted as a breakout. Adding a third near-identical function for session levels was the wrong move, so the first actual commit here was a refactor, not a feature:

def invalidate_single_level_poi(
    poi, executor, break_direction, level_label,
    validate_tf="60", require_double_close=True,
    max_wait_ltf_candles=10, entry_model_calls=None,
) -> dict:
    """break_direction: +1.0 = high-type level (close above invalidates),
    -1.0 = low-type, None = timeout-only (no price-based invalidation)."""

invalidate_poi_pdl_pdh and invalidate_poi_pwh_pwl became thin wrappers over this — same signatures, same call sites in both trade managers, zero regression risk. Session levels call the same core directly with a 15-minute validation timeframe and a shorter timeout window, because an intraday level shouldn't sit around waiting hours for confirmation the way a weekly one can.

Three invalidation paths (clean breakout close, displacement impulse after a first close, and timeout-without-confirmation) now have exactly one implementation instead of three copies drifting slowly apart.

What the entry agent actually sees

This is the part that doesn't exist for PDH/PDL, and it's the reason session liquidity got its own module instead of just reusing the daily-level code path.

A bare price level is a weak signal on its own. What makes a session sweep tradeable — in the ICT sense — is everything around the sweep: was it a clean stop-hunt with a fast reclaim, or did price just keep going? Has the other side of the range already been taken, meaning the dealing range is done? Is this level stacked on top of a daily or weekly liquidity pool? Did the raid happen inside the textbook Judas Swing window?

None of that should be left to the LLM to eyeball from raw candles. It's all computable, so it's all computed — in core/poi/session_context.py, before the model ever sees the setup:

### SESSION LIQUIDITY CONTEXT (ASL — Asia Low)
- Level: 61234.5 (Asia session low, 2026-07-08, window 23:00–02:00 UTC)
- Session range: 61234.5 – 62100.0 (1.41%, 0.5x avg of last 20 Asia ranges,
  session direction: bearish (open 62050.0 → close 61300.0))
- Sweep: 06:42 UTC during London session, 4h42m after Asia close,
  depth 0.18% below level, price reclaimed level: YES (SFP)
- Current session: London (84 min in), entry inside killzone: YES
- Range position: price at 8.0% of Asia range — discount (below equilibrium) 61667.25
- Opposite level (Asia High 62100.0): untouched — one-sided raid,
  external liquidity on the other side intact
- HTF liquidity confluence: PDL 61180.0 (-0.1%)
- Daily open (00:00 UTC): 62500.0 — price -1.9% vs open
- Pattern: JUDAS SWEEP (Asia low raided within 90 min of London open)

Every line there is a fact, not an inference. reclaimed: YES comes from comparing the last closed 5-minute candle's close against the level after the sweep — not from asking the model to eyeball a chart. range_vs_avg comes from a rolling Redis list of the last 20 Asia range sizes, so the model knows whether today's range is unusually tight (often the setup for an expansion day) without having to remember yesterday's numbers. The opposite_level block tells it whether this is a one-sided raid or whether the dealing range is already complete — which changes the entire read of the setup from "reversal in progress" to "range is done, expect chop."

The Judas Sweep flag is the one I like most, because it turns a named ICT pattern into a boolean instead of a hope that the model recognizes it from prose: it's True only when the session's extreme got swept within 90 minutes of the next killzone opening. No judgment call, no vibes — a timestamp comparison.

The prompt's job, correspondingly, shrank to interpretation rules instead of computation: "reclaim = NO is a strong contraindication — that's acceptance, not a sweep. BOTH SIDES TAKEN means the range is done, downgrade continuation logic." The system prompt never computes a percentage or checks a clock. It reacts to numbers that are already sitting in the context.

Builder resilience mattered here too — every enrichment step (the HTF confluence lookup, the daily-open fetch, the sibling-level check) is wrapped so a failure just omits that one field instead of failing the whole entry evaluation. A setup should never get silently dropped because one enrichment API call timed out.

The naming mistake I made twice

This part isn't flattering, but it's the honest build-in-public version of events.

First pass: SSH/SSL, mirroring PDH/PDL/PWH/PWL. Made sense on paper — Session High, Session Low.

First collision: the entry prompts already used SSL for something completely different — Sell-Side Liquidity, standard ICT shorthand sitting right next to BSL (Buy-Side Liquidity) in the mandatory liquidity-sweep-analysis section. Same three letters, two unrelated meanings, in the same document the model reads on every single entry evaluation. Renamed to SH/SL.

Second collision, worse than the first: bare SL is Stop Loss. Not adjacent terminology — the exact same abbreviation, used six times in the very prompt file where I'd just placed the new POI type, in a section about user-provided execution levels. A prompt disambiguation note ("this SL means Session Low, not Stop Loss — check context") is a patch, not a fix, and I didn't trust it to hold up once the model is juggling a dozen other things in a long ReAct loop.

The actual fix wasn't a better disambiguation sentence. It was removing the possibility of the collision entirely: fold the session into the type itself. NYSL cannot be mistaken for Stop Loss. ASH cannot be mistaken for Sell-Side Liquidity. There's no two-letter abbreviation left to collide with anything, because there's no bare two-letter abbreviation anymore.

The lesson generalizes past this feature: when a short code collides with existing vocabulary, the fix usually isn't a longer explanation next to the short code — it's making the short code less short, in the specific direction that kills the ambiguity. SL needed one more character of information (which session), not one more sentence of caveat.

Where this fits

Every session-liquidity POI still runs through the exact seven-state machine every other POI does — ACTIVE on creation, PENDING on price touch, READY only after LTF ChoCH and displacement confirm, and so on. Nothing about the state machine changed. What's new is entirely upstream of it: a worker that decides which levels exist and for how long, and a context builder that gives the entry agent SMC/ICT-grade reasoning material instead of a bare number.

The feature is fully wired — worker, invalidation, entry-loop integration, scoring, LLM context — and sitting behind an opt-in flag, off by default, until it's had a pass through the POI radar UI and a stretch of demo-account observation. Six extra liquidity magnets a day is a meaningful increase in surface area, and I'd rather watch it work quietly before it starts placing real orders.

ShareX / TwitterThreads
sc4mp avatar

Public pen name of LiquidMind's founder and builder. Writing first-hand engineering notes and transparent performance reviews from the system's internal ledger.

Stay Liquid

New posts on transparency, engineering, and the LiquidMind thesis — no noise.

Loading discussion…