For the Geeks

This page is for readers who want to understand why the strategy engine behaves the way it does — not just what it does. It covers the five internal mechanics that shape how your rules are actually processed. Understa...

Written By Axiom Admin

Last updated About 1 month ago

For the Geeks

This page is for readers who want to understand why the strategy engine behaves the way it does — not just what it does. It covers the five internal mechanics that shape how your rules are actually processed. Understanding these is not required to use the tool, but it is required to debug the tool when it does something you did not expect.

Nothing on this page gives you implementation code or reproducible formulas. What it gives you is a mental model accurate enough to predict behavior, use the diagnostics to verify your predictions, and reason through edge cases without guessing.


1. The setup state machine

What it is

Every named setup in your YAML is governed by a three-state lifecycle: INACTIVE → CONFIRMING → CONFIRMED. The state determines whether entries attached to that setup are allowed to evaluate. If the setup is not CONFIRMED, its entries do not run — period.

Note: the engine publishes a <SETUP_NAME>_ACTIVE token, but this is not a distinct state — it is a convenience boolean that means "not INACTIVE" (i.e., either CONFIRMING or CONFIRMED). The actual states assigned by the state machine are INACTIVE, CONFIRMING, and CONFIRMED.

Why it exists

A simple "enter when condition is true" approach has no concept of persistence. A condition can flash true for a single bar and produce an entry in a market that immediately reverses. The state machine lets you require that conditions not only be true, but stay true for a measured period (confirmation) before any entry is permitted. It also lets you define cancellation conditions with their own confirmation delay, cooldown periods before re-activation, and entry caps per activation window.

The result is a gating system that separates "the market looks interesting" (activation) from "the market has been interesting long enough to act on" (confirmation). This distinction is invisible in most strategy builders, which treat conditions as binary switches. Here, conditions have duration, and duration matters.

The tradeoff

Complexity. The state machine is the single hardest concept in the tool to debug because the states are not visible by default. You need to enable expression diagnostics and watch the setup's state tokens (<SETUP_NAME>_INACTIVE, <SETUP_NAME>_ACTIVE, <SETUP_NAME>_CONFIRMING, <SETUP_NAME>_CONFIRMED) to know what the setup is doing on any given bar.

The simpler alternative — no state machine, entries always eligible — is the GLOBAL setup. It works. But it cannot express "wait for this regime to establish itself before trading."

How to verify

Create a minimal test: one named setup with a 3-bar confirmation, one entry attached to it, and no other logic. Enable expression diagnostics. Watch the setup state tokens as you scroll through the chart. You should see the setup go from INACTIVE to CONFIRMING when the activation expression first fires (the _ACTIVE token will also become true at this point), and to CONFIRMED when the confirmation count is reached. The attached entry should only produce its first trade after the setup reaches CONFIRMED.

If the setup never reaches CONFIRMED, check: is the gate condition passing? Is the activation expression true for enough consecutive bars? Is a cancel condition firing before confirmation completes?

What not to assume

Do not assume that the state machine evaluates gate, activate, cancel, and reset in the order you would expect on a single bar. When multiple conditions are true simultaneously on the same bar, the engine processes them in a specific internal order that may not match your intuition. If you have a scenario where activation and cancellation fire on the same bar, test it — do not reason through it on paper.

Do not assume that consuming an entry cap cancels the setup. It does not. The setup can remain confirmed while the cap simply blocks additional entries for that activation window. Also: when a setup cancels or resets, the engine clears the per-activation fill counter. A later reactivation starts fresh rather than carrying the old cap usage forward.


2. The expression evaluation pipeline

What it is

Every condition in your YAML — gates, triggers, activations, cancellations — is an expression string that gets compiled into an internal representation and evaluated against the current token map on every bar.

Why it exists

The expression language is what makes this tool low-code. Instead of writing Pine Script functions, you write readable conditions like PRICE_CLOSE > EMA_50 && RSI_K < 30. The engine compiles these into an efficient evaluable form once (on the first bar), then evaluates them each bar by looking up token values and applying operators.

The token map is the key concept. Every named value the engine knows about — prices, bar data, strategy statistics, position state, custom indicator outputs — is registered as a named token. Your expressions reference those names. If a name exists in the map, the expression gets the current value. If it does not exist, the engine raises an "Unknown identifier/token" error during first-bar validation. This error prevents the strategy from running and is reported in the error table. For the complete list of built-in tokens, see Default Tokens. For the full expression syntax including operators and functions, see Expression Reference.

The tradeoff

Power vs. safety. A dropdown-based condition builder would prevent typos and missing tokens entirely, but it would limit you to predefined conditions. The expression language lets you write anything the token system supports, which includes novel cross-references (one entry's gate checking whether another setup is confirmed, using derived state tokens). A typo in a token name will produce a validation error that prevents the strategy from running — the engine does not silently substitute defaults for missing tokens.

How to verify

Enable the expression value label. Every operational expression appears with its current value and its evaluation status: EVAL (being evaluated), SKIP (not reached, usually because the owning setup or gate is blocking), or EMPTY (not defined).

The most important verification move: simplify a complex expression to a single condition (like PRICE_CLOSE > 0, which should always be true) and confirm the intent fires. Then add conditions back one at a time. If the intent stops firing when you add a specific condition, you have found the constraint. This is more reliable than introducing typos, which will just produce a validation error and block execution.

Another useful verification move: simplify a complex expression to a single condition (like PRICE_CLOSE > 0, which should always be true) and confirm that the expression evaluates and the intent fires. Then add conditions back one at a time. If the intent stops firing when you add a specific condition, you have found the constraint.

What not to assume

Do not confuse a missing token with a token whose value is na. A missing token (one that does not exist in the token map at all) causes an "Unknown identifier/token" error that blocks execution. A token that exists but has a value of na (because it does not have enough history to compute, for example) does not error — it propagates na through arithmetic and causes comparisons to return false. These are different failure modes with different symptoms.


3. Anchored exit sizing

What it is

When you define take profits and stop losses with allocation percentages, the engine needs a stable number to calculate the exit quantity from. That number is the anchor: the peak open quantity observed for the entry group while it was open.

Why it exists

Without an anchor, exit sizing becomes path-dependent. Imagine two take profits, each at 50%. The first one fires and closes 50% of the current position. Now the second one fires — but 50% of what? If it calculates against the remaining position, it closes 50% of the remaining 50%, which is only 25% of the original. Your two 50% TPs close 75% total, not 100%.

Anchored sizing fixes this. Both TPs calculate their quantity against the same baseline — the peak open quantity for that entry group. Each closes 50% of the anchor, regardless of whether the other has already filled. The two exits together close 100% of the original position.

The tradeoff

Anchored sizing assumes that the peak position size is the right baseline. If the position was added to via pyramiding and later partially closed, the anchor reflects the peak — which includes the adds. This is correct for sizing exits relative to the full exposure, but it can feel unintuitive if you are thinking of exits as percentages of the original entry.

When total TP allocation exceeds 100%, the engine scales all allocations down proportionally so the total equals 100%. This prevents exits from trying to close more than the anchor, but it means your specified percentages may not match the actual quantities. The largest-allocation leg receives any rounding residual.

How to verify

Create a test with one entry and two take profits at 50% each. Let the entry fill, then let both TPs trigger. In the trade list, each TP should close approximately half the original position, regardless of which one fills first. If you change the allocations to 60% and 60% (total 120%), both should scale down to 50% each.

What not to assume

Do not assume that the anchor is the same as the initial entry quantity. If the entry uses pyramiding and adds to the position, the anchor is the peak — which includes all adds. Exits sized against the anchor account for the full peak exposure, not just the first fill.

Do not assume that the allocation percentages you wrote in the YAML are the percentages that execute. If your three TPs are defined at 40%, 40%, and 40% (total 120%), proportional scaling will reduce each to 33.3%. The YAML says 40%. The execution says 33.3%. This is not a bug — the engine prevents exits from closing more than the anchor allows — but it will surprise you if you are reading the trade list and wondering why each TP closed less than the YAML specified.


4. Order reconciliation

What it is

The engine needs to know, on every bar, which of your defined entries have open trades and which exits have already filled. It does this by reading TradingView's order fill reports and matching them back to your YAML definitions using structured order IDs.

Why it exists

When you have multiple setups, multiple entries per setup, and multiple exit legs per entry, the tracking problem is significant. Which of the three open trades came from Entry A vs. Entry B? Did TP-1 on Entry A already fill, or is it still working? Has SL-1 been cancelled because the position cycle reset?

The engine encodes the full provenance — direction, setup name, entry key, exit role, and exit name — into every order ID it submits. When TradingView reports a fill, the engine parses the ID to route the fill back to the correct intent and update the right counters.

For exit ladders, the engine uses OCA.reduce groups. OCA (One-Cancels-All) with reduce behavior means that when one exit in a group fills, it reduces the available quantity for the remaining exits in that group rather than cancelling them. This lets multiple exits share a position budget without overshoot.

The tradeoff

Structured IDs and OCA groups make complex multi-entry, multi-exit strategies possible. But they also mean the connection between your YAML definitions and the tester's trade list is not immediately obvious. The order comments in the trade list will show the exit name you defined, but the underlying coordination between exits happens through the OCA mechanism, which is not directly visible.

How to verify

Create a strategy with one entry and two exits (one TP and one SL). Look at the trade list in the strategy tester. Each exit should appear with a comment that matches the name you defined in the YAML. The total quantity closed by both exits should not exceed the open quantity from the entry.

Then add a second entry and observe that each entry group's exits track independently. TP-1 on Entry A does not interact with TP-1 on Entry B — they belong to different entry groups and work against different position budgets.

What not to assume

Do not assume that exit fill order in the trade list reflects the order of trigger evaluation. Exits are sorted by expected fill proximity (closest first) before submission. The tester fills orders based on price interaction with the bar's OHLC, not based on the order of expression evaluation.


5. Price latching

What it is

When lock_prices is enabled on an exit (or entry with a limit/stop price), the engine captures the limit or stop price at the moment the trigger condition first fires, then holds that price constant for the lifetime of the working order.

Why it exists

Some strategies need a price target calculated at a specific moment — the bar where the condition first becomes true — and then held stable while the order works toward a fill. Without latching, the price expression recalculates every bar, which means the order target moves with the market. In monitoring mode (no latch), this is correct: the target tracks the market. In latched mode, the target is frozen at the moment of decision.

The distinction matters most for exits. A stop loss calculated as "entry price minus 2 ATR" should probably be set once and held — you do not want the stop to drift away as ATR changes over subsequent bars. A trailing stop, on the other hand, should not be latched — it needs to recalculate on every bar.

The tradeoff

Latched prices provide stability but can become stale. If the market moves significantly after the latch captures a price, the frozen target may no longer make sense — a stop loss that was 2% away at latch time may be 10% away a week later if the market rallied. The order still works at the latched price, which may be a poor target in the new context.

Non-latched prices stay current but create chase risk. If a take profit's target expression keeps moving higher in a rally, the TP order chases the market and may never fill — or may fill at a level the user did not intend.

Neither mode is right or wrong. The choice depends on whether the price target represents a fixed decision or a moving calculation.

How to verify

Create two identical exits — one with lock_prices = true and one with lock_prices = false (or omitted). Use a price expression that changes each bar (like a moving average offset). Observe the behavior:

  • The latched exit's working price should freeze at the value it had when the trigger first fired. It will not change on subsequent bars.

  • The non-latched exit's price should update every bar as the expression recalculates.

Check the expression diagnostics to see the current value of each exit's price expression. For the latched exit, the diagnostic shows the recalculated value, but the working order uses the latched one. This is by design — the diagnostic tells you what the expression says now, not what the order is working at.

What not to assume

Do not assume that latching applies only to the first bar after trigger. The latch captures the price once and holds it for the entire lifetime of that working order. The latch is only cleared when the order fills, the position cycle resets, or the exit is cancelled. If you expected the latch to refresh periodically, it does not — you would need to structure your logic differently to achieve that.

Do not assume that the expression diagnostics show you the latched price. The diagnostic label shows the expression's current recalculated value — what the price would be if it were recalculated right now. The working order uses the latched value from when the trigger first fired. These two numbers will diverge over time, and the divergence is normal. If you are comparing the diagnostic value to the order in the trade list and they do not match, this is probably why.