For the Geeks

This page is for the moment when "the YAML looks right" is no longer enough. It explains the engine model underneath the fields: state machines, token registration, expression evaluation, order identity, OCA behavior,...

Written By Axiom Admin

Last updated 1 day ago

For The Geeks

This page is for the moment when "the YAML looks right" is no longer enough. It explains the engine model underneath the fields: state machines, token registration, expression evaluation, order identity, OCA behavior, and diagnostics.

It does not give you implementation code. It gives you enough of the model to predict behavior and debug without guessing.


The Engine In One Breath

On each bar, the Strategy Lab refreshes tokens, evaluates setup state, evaluates entry units, evaluates exit units, checks risk/runtime constraints, and then submits orders through TradingView's strategy engine.

The YAML does not become Pine code. It becomes structured configuration that the Strategy Lab interprets bar by bar.


Unit State Machines

Setups are state machines. They can be inactive, confirming, or active. ACTIVE is the final setup state for the current lifecycle: the setup context is live and entries or exits attached to it are allowed to evaluate.

For a setup named TREND_FILTER, watch:

Longhand

Shorthand

TREND_FILTER_INACTIVE

TREND_FILTERINACT

TREND_FILTER_CONFIRMING

TREND_FILTERCONF

TREND_FILTER_ACTIVE

TREND_FILTERACT

If an entry belongs to TREND_FILTER, the entry does not get a real chance to fire until TREND_FILTER_ACTIVE is true.

Entries, take profits, and stop losses use the same language plus one extra state:

Longhand

Shorthand

<NAME>_INACTIVE

<NAME>INACT

<NAME>_CONFIRMING

<NAME>CONF

<NAME>_WORKING

<NAME>WORK

<NAME>_ACTIVE

<NAME>ACT

CONFIRMING means the gate and trigger are true, but the unit is still counting confirmation bars or ticks. WORKING means a limit, stop, or stop-limit order has been submitted but has not filled. ACTIVE means the unit has done the thing it was built to do: entry filled, take profit executed, or stop loss executed.

The best sanity test is small: one setup, one entry, one easy market exit. Confirm the setup state tokens first. Then add complexity.


Token Map

Every expression reads from the token map. Built-in prices, bar data, strategy stats, position state, custom indicator outputs, and generated state tokens are all just names in that map.

If a token does not exist, the engine reports a blocking diagnostic. If a token exists but currently has NaN, the expression may produce a warning or a false result depending on where that value flows.

That distinction matters:

Situation

Result

Token name is missing

Blocking unknown-token diagnostic

Token exists but value is NaN

Runtime warning or false comparison

Token exists but is the wrong type

Type/final-type diagnostic

Use Default Tokens for built-ins and Diagnostics & Error Codes when the parser tells you exactly which token or expression failed.


Expression Evaluation

Expressions compile into an internal form and then evaluate against the current token map. The expression parser knows about types, supported functions, history references, and field expectations.

A field that expects a boolean must end as true/false. A field that expects a price must end as a number. If an expression produces an error value or no usable value, the field may produce a warning first and then a blocking final-type error.

This is why you can see both messages for one expression:

Example
WARN: Percent-change calculation needs a non-zero previous value. ERROR: Expression returned the wrong final type for this field.

The warning explains the cause. The error explains why the strategy cannot use the field's final result.


Warmup And Eager Function Inputs

Historical references need history. PRICE_CLOSE[5] does not exist on the first few bars. The parser can infer some warmup needs, but unsafe math still needs a fallback when you expect early bars to stay quiet.

This looks safe, but the function still receives an unavailable value:

Example
entry_trigger_when: BAR_INDEX > 5 && PERCENT_CHANGE(PRICE_CLOSE, PRICE_CLOSE[5]) > 0.10

This is safer because the fallback is inside the calculation:

Example
entry_trigger_when: BAR_INDEX > 5 && SAFE_DIV( PRICE_CLOSE - NZ(PRICE_CLOSE[5], PRICE_CLOSE), NZ(PRICE_CLOSE[5], PRICE_CLOSE), 0 ) * 100 > 0.10

The lesson is unromantic but useful: if the dangerous value is inside a function call, guard the dangerous value before or inside that call.


Entry Identity

Entries use entry_name as their identity. That name becomes the order ID basis and the state-token base after sanitization.

There is no separate entry ID field in the current schema. If you want a clearer order name, name the entry clearly:

Example
- entry_name: L_PULLBACK_50

That gives you readable trade-list identity and tokens like:

Longhand

Shorthand

L_PULLBACK_50_INACTIVE

L_PULLBACK_50INACT

L_PULLBACK_50_CONFIRMING

L_PULLBACK_50CONF

L_PULLBACK_50_WORKING

L_PULLBACK_50WORK

L_PULLBACK_50_ACTIVE

L_PULLBACK_50ACT


Entry OCA

Entries can use OCA groups when orders compete with each other. The two fields are:

Example
entry_oca_group: LONG_ENTRY_COMPETE entry_oca_type: CANCEL

This is for entry-side competition: breakout vs. pullback, first ladder fill vs. alternate ladder fill, and other cases where one entry order should affect another before or as it fills.

If there is no group, OCA type has no meaningful place to operate and the diagnostics will warn you.


Exit Model

Take profits and stop losses are independent units of work against the shared position for the direction.

They are not partitioned by entry. They do not require a from-entry field. They do not need to sum to 100 across the whole exit section.

Each exit asks a simpler question:

Example
Given the position that exists right now, should I try to reduce or close some of it?

That means these can both be valid:

Example
- take_profit_name: TP_FIXED_TARGET take_profit_gate_condition: POSITION_ACTIVE take_profit_trigger_when: TRUE take_profit_order_type: LIMIT take_profit_limit_price: POSITION_AVERAGE_PRICE * 1.03 take_profit_allocation_percent: 100 - take_profit_name: TP_MOMENTUM_FAIL take_profit_gate_condition: POSITION_ACTIVE take_profit_trigger_when: CROSS_UNDER(RSI_K, 70) take_profit_order_type: MARKET take_profit_allocation_percent: 100

They are alternative full-position exits. They are not a 200% allocation plan.


Exit OCA

Exit orders use reduce-style OCA behavior internally. That is the right default for one shared position because multiple working exits should compete to reduce the same remaining exposure without turning into accidental over-exits.

You do not configure exit OCA in YAML. The strategy handles it as part of the exit subsystem.

Use gates to decide which exits are eligible. Use allocation percentages to decide how much each eligible exit attempts to close.


Partial Exits And Remaining Position

When you want actual legs, use allocations below 100 and then gate later exits or stops with position state.

Example
- take_profit_name: TP_HALF take_profit_gate_condition: POSITION_ACTIVE take_profit_trigger_when: TRUE take_profit_order_type: LIMIT take_profit_limit_price: POSITION_AVERAGE_PRICE * 1.02 take_profit_allocation_percent: 50 - stop_loss_name: SL_BREAK_EVEN stop_loss_gate_condition: POSITION_ACTIVE && POSITION_REMAINING_PERCENT <= 50 stop_loss_trigger_when: TRUE stop_loss_order_type: STOP stop_loss_stop_price: POSITION_AVERAGE_PRICE stop_loss_allocation_percent: 100

The remaining-position tokens are the bridge between "a TP should have reduced the position" and "now a different stop should be active."


Price Latching

*_lock_prices: true captures a price when the unit first triggers. The expression may continue to show a recalculated value in diagnostics, but the working order uses the latched price.

That can look confusing:

Example
stop_loss_stop_price: POSITION_AVERAGE_PRICE - ATR_14 * 2 stop_loss_lock_prices: true

The diagnostic label may show today's recalculated POSITION_AVERAGE_PRICE - ATR_14 * 2. The order may still be sitting at the price captured when the stop first armed. That is normal.

Use locked prices for fixed targets and fixed risk. Leave prices unlocked when you intentionally want the order level to follow the current expression.


Diagnostics

The diagnostic layer exists because the engine now has more than one kind of failure:

Failure type

Example

Parse/schema failure

Missing required YAML field

Compile failure

Unknown token or bad function arity

Runtime warning

Historical value is unavailable on this bar

Runtime blocker

Final expression type is wrong for the field

Strategy/runtime warning

Order skipped because state or quantity made it invalid

The first diagnostic is often the most important one. Fix the earliest cause, reload, then see what remains.

If you hit a wall, make the strategy boring on purpose: one setup, one entry, one exit, one token. Get the machine moving. Then put the interesting parts back one at a time.