Writing and saving a signal
A signal is a short program written in the Signal language. There is no visual signal builder, so the no-code path is not a form: it is the signal-library modal, where you pick a signal someone already wrote. Everything on this page turns on one distinction. Evaluating runs a program and shows you the result with nothing kept. Saving persists both the program and its computed values under a name, so the signal can be referenced by other programs and extended later without recomputing from scratch. A saved signal is a single record keyed by its name. Save again under the same name and you overwrite it in place. There is no version history and no draft-versus-published state.
Write it in the Signal language
You write a signal as a sequence of statements that ends on a Signal-valued expression. Assign a name once, never reassign it, comment with //, and let the last line be the value the signal emits. Because the last expression is the result, it has to reference an accessor somewhere in its ancestry (see the execution model for why a program that ends on a scalar is rejected).
The editor opens on a default program you can run as-is or edit:
// Intraday IV term-structure spread: short-dated vs long-dated IV
iv_7 = option.iv(dte=7) // short-dated ATM IV
iv_30 = option.iv(dte=30) // medium-dated ATM IV
// Positive = normal term structure; negative = inverted (near-term stress)
iv_30 - iv_7The program reads two IV Signals off the option accessor and subtracts them. Both sides align on the union spine before the subtraction, so the result carries a value at every timestamp either input does. The Signal language page covers the language itself, and the execution model page covers how the two frequencies align. This page is about what you do with the program once you have it.
The no-code path: the signal library
If you do not want to write one, you pick a saved signal from the signal-library modal.
The same modal opens at every point a signal is selected: the Signal page when you set up EDA on the time-series chart, the feature picker when you build a model, and the point where you add a signal to a backtest. It carries three things: your own signal library, the official Backtest.ai signal library, and an option to browse community signals and clone one into your library.
A saved signal is referenced by its slug, so picking one from the modal resolves to the same signal("slug") import described below.
This is how someone who does not write code reaches the Signal stage: pick a saved signal instead of authoring one.
Evaluate without saving
Evaluating runs the program over one symbol and a start and end date, and streams the resulting Signal back with nothing persisted. The result arrives one batch per calendar month and draws onto the chart as each batch lands, so a multi-year range fills in progressively rather than all at once. This is the fast loop: change a line, run it, look at the series, change it again.
Two errors surface before a run produces any values. A program that does not compile is rejected with the compile error, so a malformed expression never reaches the data layer. A standalone signal that calls a backtest-only accessor is also rejected, because book(), position(), and leg() read state that exists only inside a running backtest. Those accessors work only when the program is a backtest slot, not when it is evaluated on its own.
For which symbols and date ranges you can evaluate against, see data coverage.
Save a signal
Saving runs the program and persists it under a name. The name is required and unique. Alongside it goes a slug, lowercased and sanitized to [a-z0-9-], with runs of dashes collapsed and leading and trailing dashes trimmed; the UI seeds the slug field from the name, so Intraday IV Spread starts as intraday-iv-spread. You can attach optional markdown docs. Saving stores both the computed values and the resume state, which is what lets the signal be referenced by other programs and extended later without a full recompute.
Saving with a name that already exists overwrites that record in place. There is no version history and a saved signal has no version id; the latest save is the signal. If a save is interrupted mid-stream, for example a dropped connection before the run finishes, it may not persist a usable signal. Re-save if a save does not complete.
Renaming and cloning a saved signal
A saved signal is a single record fetched internally by its slug, which shapes how renaming and cloning behave:
- Rename by saving under a new name creates a second record. A new name is not a naming conflict, so the original stays exactly where it was and you now have two signals.
- Clone copies a signal into your library from the signal-library modal above. Re-saving a program under a new name is the manual equivalent.
Reference another signal with signal("slug")
signal("slug") imports the values of a saved signal onto the importing program's spine. It returns that signal's value at every timestamp on the importing program's spine, which is the same result as inlining the imported signal's expression for the same symbol and range.
// Term-structure spread relative to its own 60-day average
spread = signal("intraday-iv-spread")
spread_daily = sample(spread, sampling=sampling(period="day", time="close"))
spread_daily - rolling_mean(spread_daily, 60)The resolver picks one of three modes automatically for the requested symbol and range:
use_as_isextendrecomputeTwo of those modes have a side effect worth stating plainly: resolving an import can extend or recompute the referenced signal, and the extended or recomputed result is re-persisted. Importing a signal is not always read-only against the thing you import.
The import surface has hard limits:
- Maximum import depth is 10. A chain of imports deeper than that is rejected.
- Maximum 50 distinct dependencies per evaluation. Beyond that the evaluation is rejected.
- Duplicate imports of the same slug within one run are cached, so repeating
signal("x")costs one resolution. - A signal that imports itself, directly or through a chain, is rejected. The cycle is caught when you save, not when you run, so a cyclic signal never persists.
There is no version pin in the DSL. signal("slug") takes only the slug string, so an import always resolves against the latest stored extent. Combined with the extend and recompute modes, that means the signal you import is a live reference to the current saved record, not a snapshot of it as of when you wrote the importing program.
How regeneration works
Regenerating extends a saved signal to a later end date. It restores the stored resume state and appends only the new range, which is the incremental path and the common case. It falls back to a full rebuild when there is no stored resume state, when the program's own definition has changed since the last save, or when the saved state is incompatible with the current engine. Either way the signal ends up covering the new range and is re-persisted.
Frame it as cheap forward extension of a result you already have, not a re-run from the start date. When the incremental path applies, only the appended months cost compute.
Three outcomes are worth naming:
- An end date earlier than the signal's current end date is an error. Regeneration only moves forward.
- Regenerating a signal that does not exist reports that the signal is not found.
- Regenerating a signal that is already current is a no-op. No new range means zero batches and nothing re-persisted.
Determinism: re-run equals single-run
For fixed inputs, a re-run gives the same answer as a single run over the full range. That guarantee is what makes saving and extending safe to lean on.
The incremental path is byte-identical to the batch path by construction (see the execution model for how the incremental and batch evaluators are held identical). It stays on the incremental path as long as the program's own definition and the engine agree with the saved state; a change to either forces a full rebuild from the start date, exactly as the regeneration fallback above describes. Either way, extension is guarded by the program fingerprint and the engine codegen version.
The practical statement: extending or re-running never rewrites a value you already saw against the same inputs. New range gets appended, or the whole thing gets rebuilt from the same inputs to the same output. A value once emitted does not change under you.