Engineering13 min read

Build in Public: The same price can be premium and discount at once

Premium and discount sound binary until one price sits above a local 1H range, below a broken 4H leg, and inside 1D discount at the same time. We built a mandatory multi-timeframe price-location tool for our entry and position-management agents — then made sure its output could never become an automatic trade filter.

By

agentsSMC/ICTpremium-discountmarket-structureposition-managementbuild-in-publicPythondeterminism

In brief

  • Calling the price-location tool is mandatory for every entry and position-management review; acting on its result is not. Premium or discount can add context, but cannot approve, reject, resize or manage a trade by itself.
  • The reference price matters: the entry agent locates the planned limit entry, while position management normally locates live market price. Analyzing current price for a resting limit order answers the wrong question.
  • Retracement uses that same reference price and structural leg. Continuation beyond the terminal and movement beyond the origin are explicit states, not ambiguous negative or above-100% retracement values.
  • Dealing ranges come from the latest confirmed pair of opposite structural swings after the open candle and every unconfirmed edge swing are removed — including synthetic endpoints created by the SMC library.
  • Premium and discount are fractal. A 1D discount plus 1H premium can be a healthy long setup when the lower-timeframe premium is the result of bullish displacement inside the broader discount.

At one point in a live BTCUSDT check, the same reference price produced three answers:

Timeframe Location
1H 116.76% — above range
4H −55.96% — below range
1D 42.09% — discount

That is not a contradiction. It is the market being fractal.

The 1H range described a recent local expansion. The 4H range described a bullish structural leg that price had already broken below. The 1D range still placed the same price in broader discount. Three ranges, three valid statements, one market price.

The tempting engineering mistake is to turn those statements into votes. Two bearish-looking labels versus one bullish-looking label. Count them, produce a score, reject or approve the setup.

We built the opposite: a tool the agent must always call, whose output is never allowed to make the decision alone.

The missing question in a limit-order system

Our entry agent already had market structure, order blocks, fair value gaps, liquidity, displacement, volume profile, momentum, order flow and path-to-target analysis. It could describe what the market was doing in considerable detail.

But it could not answer one simple question consistently:

Where does the price we actually intend to trade sit inside the current structural dealing range?

The phrase price we intend to trade matters. LiquidMind uses limit orders. Current market price may be far away from the order that will eventually execute.

Suppose BTC trades at 65,000 while a planned bullish limit entry rests at 63,800. Asking where 65,000 sits in the range tells us about the market now. It does not tell us whether 63,800 is a discount retracement, an equilibrium entry or a level below the identified structure.

The tool therefore accepts an explicit reference price:

{
  "symbol": "BTCUSDT",
  "interval": ["1H", "4H", "1D"],
  "reference_price": 63800
}

That reference is a strict attribution boundary. Every returned zone, range position, boundary distance and reference retracement describes exactly 63800 in this example. It does not describe the BPR or POI that motivated the order, nor current market, stop loss or take profit. To locate one of those prices, that exact price must be supplied as the reference.

For entry analysis, the reference is normally the planned limit price. For an open-position review, the tool normally fetches live price once and locates that across every requested timeframe.

The distinction sounds small. It changes the question from where is the market? to where is our decision?

The origin zone is not the execution entry

We found a sharper version of that bug while tracing a real QQQ long. The higher-timeframe BPR was 721.54–721.74, with a midpoint of 721.64. But the completed lower-timeframe entry model had selected a bullish displacement FVG at 723.86–724.23. The strategy's actual long entry was the proximal FVG boundary: 724.23.

Those prices answer different questions. 721.64 describes the origin zone. 724.23 describes the limit order the system will place. Letting the model choose either number for price-location analysis meant it could assess one price and execute another.

We removed that ambiguity from the automated path:

HTF origin POI
  -> ChoCH/BOS event
  -> deterministic LTF displacement FVG
  -> system-owned planned entry
  -> forced price-location reference
  -> the same price at order placement

For a long, the entry is the FVG top; for a short, it is the FVG bottom. The binding happens before the agent's first turn. If the model supplies a POI midpoint, current price or any other level, the runtime replaces it with the system-owned entry. The assessment records the POI, event, entry timeframe, FVG boundaries and a fingerprint, and fails closed if that provenance is absent or changes before execution.

This enforcement is deliberately limited to automated entries. On-demand analysis can still inspect an explicit user-provided level, while position management normally locates live market price. Three workflows, three reference-price owners.

Mandatory observation, non-binding interpretation

We made get_price_location part of the entry agent's core stage. Every entry assessment must call it exactly once, with several relevant timeframes in one request. The global position-management agent follows the same rule on every review.

This is a data-completeness requirement, not a trading rule.

mandatory:  obtain price-location context
forbidden:  let price location decide the trade by itself

A long in premium is not automatically bad. A short in discount is not automatically bad. An open long reaching premium does not automatically deserve a partial take-profit. The labels have to be interpreted with structure, POI reaction, liquidity, displacement, momentum, order flow, current R multiple and the history of previous management actions.

This separation is important in agent design. If a useful tool is optional, the model can skip it and produce inconsistent depth between runs. If its output is a hard gate, one simplified metric can overrule the rest of the market. We wanted consistent observation without deterministic overreach.

The same principle appears in our daily bias-flip position review: a context change is something the agent must inspect, not a reflexive command to close a trade.

A dealing range needs structure, not a rolling window

The simplest implementation would take the highest high and lowest low of the last 200 candles.

That would be easy, deterministic and frequently meaningless.

A rolling extreme does not explain which structural move the market is retracing. It can pair a six-week-old wick with yesterday's low, call the distance between them a range and produce a mathematically precise percentage with no coherent market leg underneath it.

Instead, we use confirmed structural swings:

confirmed swing low

        └──────── bullish structural leg ──────── confirmed swing high

or the bearish mirror:

confirmed swing high

        └──────── bearish structural leg ──────── confirmed swing low

The active dealing range is the latest chronological pair of confirmed opposite swings. Its lower and upper levels become range_low and range_high; the later swing determines whether the leg is bullish or bearish.

That keeps premium, discount and retracement anchored to the same structural context.

The edge swing that looked confirmed but was invented

LiquidMind already depends on smartmoneyconcepts for structural swing detection. Its swing function also does something easy to miss: after identifying internal swings, it inserts an opposite swing at the beginning and end of the supplied series to complete the sequence.

That behavior is useful for some downstream indicators. It is unsafe if the final row is treated as a genuine confirmed market extreme.

Imagine the last closed candles are still rising. A synthetic low inserted at the final row can pair with the previous high and fabricate a new bearish dealing range at the edge of the data. The range looks fresh precisely because it is not real.

Our filtering layer removes the entire unconfirmed edge window:

fully_confirmed = (
    position >= swing_length
    and position < len(closed_candles) - swing_length
)

With swing_length=5, a swing needs closed candles on both sides. Everything inside the first or last five rows is ineligible, including the package's synthetic endpoints.

We also remove the newest market candle before running the analysis. An open candle cannot confirm a swing. In the live BTC audit, the newest selected swing had six closed candles to its right on 1H, eight on 4H and nine on 1D. None came from the open edge.

This introduces deliberate delay. A newly printed high is not a confirmed swing until enough closed candles exist after it. That is the price of avoiding lookahead and repainting dressed up as structure.

The filtered result now lives in one internal StructuralSnapshot. Both get_market_structure and get_price_location consume that snapshot, including the same swing IDs, timestamps, active pair and BOS/ChoCH events. Their public contracts remain separate, but they can no longer disagree because one analyzed a synthetic endpoint while the other removed it. A request-scoped cache also means the two tools fetch candles and build the snapshot only once per symbol and timeframe during an agent evaluation; the cache is discarded before the next evaluation.

That provenance is useful to code, tests and traces, but not to the language model. At the agent boundary we serialize an analytical view containing levels, timestamps, direction, range position and retracement while removing snapshot hashes, swing/event IDs, internal positions and the duplicate active-pair object. The source of truth stays auditable without spending model context on implementation details.

The calculation is simple once the range is honest

For a confirmed range:

equilibrium = (range_low + range_high) / 2
 
range_position_pct = (
    (reference_price - range_low)
    / (range_high - range_low)
) * 100

We do not clamp the result.

Position Classification
below 0% below_range
0% to below 47.5% discount
47.5% to 52.5% equilibrium
above 52.5% to 100% premium
above 100% above_range

The equilibrium band is intentionally small: 50% ±2.5 percentage points. Exact equality at 50 is too brittle for a market price; a broad neutral zone would make the label uninformative.

Values outside the range are not calculation failures. They tell the agent that price has extended beyond the structural leg used for the analysis.

Retracement now uses the same reference price and range boundaries. For a bullish leg it is (range_high - reference_price) / range_width; for a bearish leg it is (reference_price - range_low) / range_width. Inside the range, bullish retracement is 100 - range_position_pct, while bearish retracement equals range_position_pct.

The response avoids naked values below 0% or above 100%. within_range returns a 0–100% retracement_pct. beyond_terminal returns null retracement plus the percentage extended beyond the terminal swing in the leg direction. beyond_origin returns null plus the percentage beyond the origin after a full retracement. The upstream historical deepest retracement is intentionally absent because it describes the market path, not the supplied reference price.

These are location states, not trading verdicts or structural events. above_range, below_range, beyond_terminal and beyond_origin do not independently prove BOS or ChoCH; market structure must confirm that separately.

One call, several timeframes

The agent does not call the tool three times. It sends one array:

{
  "symbol": "BTCUSDT",
  "interval": ["1H", "4H", "1D"],
  "reference_price": 64217.3,
  "limit": 200
}

The wrapper maps intervals through the existing venue abstraction, routes stocks and ETFs to Alpaca and crypto to the existing Bybit path, fetches current price once when needed, and reuses candle data for duplicate normalized intervals.

This matters for the same reason staged tool disclosure matters in our measurement of tool definitions in a vertical agent: context efficiency is not only about schema tokens. One multi-timeframe call avoids repeated reasoning turns, repeated current-price calls and repeated result wrappers.

The return is deliberately compact. It contains structural facts, not raw candles or a swing-history dump.

Consensus without democracy

The result includes convenience groups:

{
  "premium_timeframes": [],
  "discount_timeframes": ["1D"],
  "above_range_timeframes": ["1H"],
  "below_range_timeframes": ["4H"],
  "zone_alignment": "mixed",
  "highest_timeframe": "1D",
  "highest_timeframe_zone": "discount"
}

What it does not include is a vote.

mixed means the ranges describe different local structures. It does not mean the evidence is contradictory. The highest timeframe is reported separately so the model can preserve hierarchy instead of flattening every interval into one bucket.

The most important case is this one:

1D discount

reaction from higher-timeframe demand

1H bullish change of character and displacement

1H premium

That can be an excellent long sequence. The 1H premium is not necessarily resistance; it can be evidence that the bullish reversal already expanded. Treating 1D discount + 1H premium as a conflict would erase the sequence that produced the setup.

The concise rule in every affected agent prompt is:

premium != resistance
discount != support

They are locations inside a range, not promises of reversal.

This is also why price location remains separate from the session liquidity facts described in our session-liquidity POI architecture. A session low can provide a liquidity event; discount describes where that event sits in a structural range. They can reinforce each other, but they are not interchangeable.

Partial failure is part of the contract

Multi-timeframe data does not fail atomically. A daily request may succeed while a 4H venue call fails or a 1H series contains too few confirmed opposite swings.

The tool keeps successful results:

{
  "1H": { "status": "ok" },
  "4H": {
    "status": "error",
    "error": "insufficient confirmed swings"
  },
  "1D": { "status": "ok" }
}

The agent still satisfies its obligation because it made the call. It must name the missing timeframe and reason over the data that exists. It may not invent the absent range, and a data failure is not permission to turn uncertainty into rejection.

This distinction keeps operational reliability separate from trade quality. A broken API request says something about observability, not automatically about the setup.

Position management is about change, not an oscillator reading

For an open long, a plausible progression is:

entry in higher-timeframe discount

position moves into profit

4H reaches premium

1D approaches its range high

That progression can add evidence for protecting gains. It still does not mean take a partial now.

A strong trend can remain in premium for a long time. Acting on the label alone would convert the tool into an oscillator with unfamiliar branding and encourage the position manager to cut winners early.

The management agents therefore compare price location with the original entry thesis and the history of previous actions. A move into premium matters differently if momentum is expanding, if opposing liquidity has just been swept, if displacement is fading, if the position is at 0.4R versus 3R, or if a partial was taken one candle ago.

Observation is mandatory. Intervention requires a broader case.

What we deliberately did not add

It would be easy to turn this into a general support-and-resistance endpoint by attaching the nearest order block, FVG, equal highs and lows, previous-day levels, session pools and volume-profile nodes.

We did not.

Those concepts already have their own data and semantics. Bundling them now would make it harder to tell whether the tool answers where is price in the range? or what obstacles surround price?

The first version stays cohesive:

confirmed structural dealing range
+ retracement
+ premium / equilibrium / discount
+ distance to range boundaries
+ multi-timeframe hierarchy

The system can compose that output with its other tools. Composition is the agent's job; hiding several analytical engines behind one label would make the trace less auditable.

The invariant we wanted

There are two common ways to misuse market context in an agent:

  1. make it optional, so analysis quality depends on whether the model remembered to ask;
  2. make it decisive, so one simplified label overrules the rest of the market.

get_price_location is designed between them.

Every relevant agent has to look. None is allowed to obey blindly.

That is the durable architecture lesson: required evidence does not have to become a required conclusion. In a system that can eventually move an order, the distinction is not philosophical. It is a guardrail against both missing context and false certainty.

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…