Declarations and Scope¶
This page defines the binding forms that PalmScript accepts and the visibility rules attached to them.
Top-Level-Only Forms¶
The following forms must appear only at the top level of a script:
intervalsourceexecutionusefnconstinputexportregimetriggercooldownmax_bars_in_trademax_positionsmax_long_positionsmax_short_positionsmax_gross_exposure_pctmax_net_exposure_pctportfolio_groupentryexitprotecttarget
Top-level let, if, and expression statements are allowed.
Base Interval¶
Every script must declare exactly one base interval:
The compiler rejects a script with no base interval or with more than one base interval.
Source Declarations¶
A source declaration has this form:
Rules:
- the alias must be an identifier
- the alias must be unique across declared sources, unless a matching
executiondeclaration intentionally mirrors the same alias, template, and symbol - the template must resolve to one of the supported source templates
- the symbol argument must be a string literal
Execution Declarations¶
An execution declaration names a venue target separately from the market data
source bindings:
Rules:
- the alias must be an identifier
- the alias must be unique across declared executions, unless it intentionally mirrors a declared source with the same alias, template, and symbol
- the template must resolve to one of the supported exchange-backed templates
- the symbol argument must be a string literal
executiondeclarations do not create market series bindings- orders may target a declared execution alias with
venue = <execution_alias_expr> - any script that declares trading signal roles requires at least one declared
executiontarget - scripts still require at least one declared
sourcefor market data
use Declarations¶
Supplemental intervals are declared per source:
Rules:
- the alias must name a declared source
- the interval must not be lower than the base interval
- duplicate
use <alias> <interval>declarations are rejected - an interval equal to the base interval is accepted but redundant
Functions¶
User-defined functions are top-level, expression-bodied declarations:
Rules:
- function names must be unique
- a function name must not collide with a builtin name
- parameter names within one function must be unique
- recursive and cyclic function graphs are rejected
- function bodies may reference their parameters, declared source series, and top-level immutable
const/inputbindings - function bodies must not call
plot - function bodies must not capture
letbindings from surrounding statement scopes
Functions are specialized by argument type and update clock.
let Bindings¶
let creates a binding in the current block scope:
Rules:
- a duplicate
letin the same scope is rejected - inner scopes may shadow outer bindings
- the bound value may be scalar or series
nais permitted and is treated as a numeric-like placeholder during compilation
PalmScript also supports tuple destructuring for immediate tuple-valued builtin results:
Additional rules:
- tuple destructuring is a first-class
letform - the right-hand side must currently be an immediate tuple-valued builtin result
- tuple arity must match exactly
- tuple-valued expressions must be destructured before further use
const And input¶
PalmScript supports top-level immutable bindings for strategy configuration:
input fast_len = 21 optimize(int, 8, 34, 1)
input target_atr_mult = 2.5 optimize(float, 1.5, 4.0, 0.25)
input regime_selector = 21 optimize(choice, 8, 13, 21, 34)
const neutral_rsi = 50
Rules:
- both forms are top-level only
- duplicate names in the same scope are rejected
- both forms are scalar-only in v1:
float,bool,ma_type,tif,trigger_ref,position_side,exit_kind, orna inputis compile-time only in v1inputvalues must be scalar literals or enum literalsconstvalues may reference previously declaredconst/inputbindings and pure scalar builtins- windowed builtins and series indexing accept immutable numeric bindings anywhere an integer literal is required
inputmay optionally end withoptimize(...)metadata so the optimizer can infer a search space directly from the scriptoptimize(int, low, high[, step])requires an integer-valued default inside the inclusive range and aligned to the stepoptimize(float, low, high[, step])requires a finite default inside the inclusive range; when a step is supplied the default must align to itoptimize(choice, v1, v2, ...)requires the default value to be one of the listed numeric choices- optimizer metadata is descriptive only; it does not change the compiled runtime value of the
input
Outputs¶
export, regime, trigger, execution, first-class strategy signals, declarative risk controls, and order-facing backtest declarations are top-level only:
export trend = ema(spot.close, 20) > ema(spot.close, 50)
regime trend_long = state(ema(spot.close, 20) > ema(spot.close, 50), ema(spot.close, 20) < ema(spot.close, 50))
execution exec = binance.spot("BTCUSDT")
portfolio_group "majors" = [spot, hedge]
trigger breakout = spot.close > spot.high[1]
entry1 long = spot.close > spot.high[1]
entry2 long = crossover(spot.close, ema(spot.close, 20))
order entry1 long = limit(price = spot.close[1], tif = tif.gtc, post_only = false, venue = exec)
protect long = stop_market(trigger_price = position.entry_price - 2 * atr(spot.high, spot.low, spot.close, 14), trigger_ref = trigger_ref.last, venue = exec)
protect_after_target1 long = stop_market(trigger_price = position.entry_price, trigger_ref = trigger_ref.last, venue = exec)
target1 long = take_profit_market(trigger_price = position.entry_price + 4, trigger_ref = trigger_ref.last, venue = exec)
target2 long = take_profit_market(trigger_price = position.entry_price + 8, trigger_ref = trigger_ref.last, venue = exec)
size entry1 long = 0.5
size entry2 long = 0.5
size entry3 long = risk_pct(0.01, stop_price)
size module breakout = 0.5
size target1 long = 0.5
cooldown long = 12
max_bars_in_trade short = 48
max_positions = 2
max_long_positions = 2
max_short_positions = 0
max_gross_exposure_pct = 0.8
max_net_exposure_pct = 0.8
Rules:
- all forms are top-level only
- duplicate names in the same scope are rejected
regimerequiresbool,series<bool>, ornaand is intended for persistent market-state seriesregimenames become bindings after the declaration point and are recorded with ordinary exported diagnostics plus dedicated active-regime cohortstriggernames become bindings after the declaration pointentry longandentry shortare compatibility aliases forentry1 longandentry1 shortentry1,entry2, andentry3are staged backtest entry signal declarationsexit longandexit shortremain single discretionary full-position exitscooldown long|short = <bars>blocks new same-side staged entries for the next<bars>execution bars after a full close on that sidemax_bars_in_trade long|short = <bars>forces a same-side market exit at the next execution open once the open trade has been held for<bars>execution barsmax_positions = <N>caps the number of simultaneously open execution-alias positions when portfolio mode is activemax_long_positions = <N>caps open long alias positions when portfolio mode is activemax_short_positions = <N>caps open short alias positions when portfolio mode is activemax_gross_exposure_pct = <N>caps the sum of absolute open notionals divided by current portfolio equitymax_net_exposure_pct = <N>caps the signed open notional divided by current portfolio equityportfolio_group "name" = [alias1, alias2, ...]declares a named alias group for diagnostics and future group-scoped controls- both declarative risk controls are compile-time only in v1 and require a non-negative whole-number scalar expression
- portfolio controls are evaluated at runtime once per bar and can use literal, time-aware, or regime-aware numeric expressions
- count controls require numeric,
series<float>, orna; compile-time constant count caps must still be non-negative whole numbers - exposure controls require numeric,
series<float>, orna; compile-time constant exposure caps must still be finite non-negative fractions - if a portfolio control evaluates to
na, a negative value, a non-finite value, or a fractional count cap on a bar, PalmScript blocks new entries for that control on that bar - portfolio groups require unique group names, at least one alias, and aliases that were declared by
source - portfolio controls do not resize orders or force exits; they only block new entries that would exceed the configured caps
order entry ...andorder exit ...attach an execution template to a matching signal role- order constructors accept the legacy positional argument form and a named-argument form
- named order arguments may appear in any order but may not be mixed with positional arguments in the same constructor call
venue = <execution_alias_expr>binds an order role to a declaredexecutionaliasprotect,protect_after_target1..3, andtarget1..3declare staged attached exits that arm only while the matching position is opensize entry1..3 long|shortoptionally size a staged entry fill with eithercapital_fraction(x)/ legacy bare numeric fraction semantics, orrisk_pct(pct, stop_price)for risk-based entry sizingsize module <name>optionally sizes the staged entry fill bound by a matchingmodule <name> = entry...declaration using the same entry-sizing semanticssize target1..3 long|shortoptionally size a stagedtargetfill as a fraction of the open position- at most one
orderdeclaration is allowed per signal role - at most one declaration is allowed per staged role
- any script that declares trading signal roles requires an explicit
order ...declaration for each declaredentry/exitsignal role size entry ...andsize target ...each require a matching stagedorder ...or staged attachedtarget ...declaration for the same rolesize module ...requires a matching module declaration that resolves to a staged entry rolerisk_pct(...)is only valid on staged entry size declarations in v1- staged attached exits are sequential: only the next target stage and the current protect stage are active at once
position.*is only available insideexit,protect, andtargetdeclarationsposition_event.*is available anywhere aseries<bool>is valid and is intended to anchor logic to actual backtest fills- current
position_eventfields are:long_entry_fill,short_entry_fill,long_exit_fill,short_exit_fill,long_protect_fill,short_protect_fill,long_target_fill,short_target_fill,long_signal_exit_fill,short_signal_exit_fill,long_reversal_exit_fill,short_reversal_exit_fill,long_liquidation_fill, andshort_liquidation_fill - staged entry and target fill fields are also available:
long_entry1_fill..long_entry3_fill,short_entry1_fill..short_entry3_fill,long_target1_fill..long_target3_fill, andshort_target1_fill..short_target3_fill last_exit.*,last_long_exit.*, andlast_short_exit.*are available anywhere ordinary expressions are valid- current
last_*_exitfields arekind,stage,side,price,time,bar_index,realized_pnl,realized_return, andbars_held last_*_exit.kindincludesexit_kind.liquidationin addition to the existing exit kinds- reserved trigger names such as
trigger long_entry = ...are no longer executable aliases; use first-classentry/exitdeclarations plus matchingorder ...templates
Conditional Scope¶
if introduces two child scopes:
Rules:
- the condition must evaluate to
bool,series<bool>, orna - both branches are scoped independently
- bindings created inside one branch are not visible outside the
if