The Signal language
A Signal is a {timestamp, value} timeseries computed over one symbol and one date range. The Signal language is the optional code path that sits under the interface: you write it in the editor on the site, there is no public API and no SDK, and "code" always means language typed here. Every program you write follows one contract. Its last expression must evaluate to a Signal. Read only that sentence and you can already tell a valid program from an invalid one: a program that ends on bar.close() or anything built from it is valid, and a program that ends on 0.20 is not.
What a Signal is
A Signal is a two-column timeseries. The platform produces it by evaluating your expression minute by minute over real market data for one symbol across one date range.
The two columns are fixed:
timestampis a tz-awareDatetimeinAmerica/New_York.valueisFloat64and nullable. Missing data isnull, never silently dropped.
A Signal also carries an asset_classes list, set by the accessors the expression reads from.
A scalar is the other thing a value can be. 100, 0.5, and true stay plain values. They are never wrapped in a Signal, and a Signal is never called a series, a dataframe, or a vector. One concept, one word: Signal.
UI first, DSL when you need it
Most users build signals and backtests in the interface and never write a line of the Signal language. The interface leads. The Signal language is the opt-in power path for the cases the form cannot express: a cross-leg strike offset, a custom rolling statistic, a combination of two data accessors on one spine.
There is no public API and no SDK, and there will not be one soon. The editor on the site is the only place you write code. So reach for the Signal language when the interface runs out of room, and not before. If a form field already says what you mean, stay in the form.
The one rule: a program ends on a Signal
There is no return keyword. The last non-blank, non-comment statement is the program's result.
For a standalone signal that result must be a Signal, which means its final expression references at least one accessor (bar.*, option.*, time.*, or signal(...)) somewhere in its ancestry. Build the value from market data and it is a Signal. End on a bare scalar or boolean and the program is rejected at compile time with:
DSL program must return a {timestamp, value} Signal; got 'float'Here is the minimal valid program. It ends on an expression built from bar.close(), so the result is a Signal:
// Valid: ends on a Signal (references bar.close())
daily_close = bar.close(sampling=sampling(period="day", time="close"))
rolling_std(log_change(daily_close, 1), 20) * sqrt(252)And here is the same idea broken. The last statement is a scalar literal, so nothing about the earlier accessor call saves it:
// Invalid: ends on a scalar, rejected: "DSL program must return a {timestamp, value} Signal"
option.iv(30)
0.20The fix is always the same. Make the last expression evaluate to a Signal.
Program shape at a glance
A program is newline-separated statements. Each statement is an assignment (name = expr) or a bare expression. Blank lines are allowed and do nothing.
Variables are immutable. A name is assigned once. Reassigning it is rejected:
Variable 'x' has already been assigned and cannot be reassignedThis is a static check over every statement, including dead code, so a second assignment raises even when neither one feeds the result.
Comments are // to the end of the line. There is no block comment, and # is not a comment. It is a lex error.
Only statements that transitively feed the final expression run. An assignment the result never reaches is skipped, so an unused accessor call issues no data query. The execution model covers how that evaluation works, and operators and functions covers what you can call. This page stops at the shape.
Scalars vs Signals
Scalars and Signals compose. A scalar operand applies element-wise to a Signal with no alignment step: daily_close * sqrt(252) scales every observation by the same constant.
Comparisons and logical operators return a Float64 Signal of 1.0 and 0.0, never a boolean column. That is what lets a condition act as a mask you multiply against a value series, so an AND is a multiply:
// Zero out IV wherever 30-DTE IV sits below 20%
iv = option.iv(30)
mask = iv > 0.20 // Float64 Signal: 1.0 or 0.0
mask * iv // IV where IV > 0.20, zero elsewhereoption.iv(30) is a 1-minute series, so the comparison runs per minute, not per day.
Combining two Signals of different cadence pulls in the union spine and the difference between null and NaN. The execution model resolves both.
When to drop to the DSL
Reach for the Signal language when the form cannot say what you mean:
- A cross-leg strike offset, where one leg's strike is defined against another:
strike = leg("short_put").strike - 5. - A custom rolling statistic the form does not expose.
- A combination of two accessors on one spine.
- An import of another saved signal with
signal("slug").
If the interface already expresses the logic, stay in the interface. When you do write code, writing and saving a signal covers the how, including the no-code path through the signal-library modal. Cross-leg offsets and the other backtest-context accessors live in signals inside a backtest.