Builder Contracts and Stage Ownership
The most important authoring contract in PyneScript — which stage owns what, and what must never cross stage boundaries.
Builder Contracts and Stage Ownership#
Indicator contract#
- Required:
indicator(...). - Required:
build_indicator_frame(df, params)or equivalent frame builder path used by the runtime. - Optional: static plot declarations such as
plot,hline,fill,plotshape,plotcandle. - Optional:
build_visuals(frame, params=None, ctx=None)for object-style or custom ATK bridge visuals.
Strategy contract#
- Required:
strategy(...). - Required:
build_signal_frame(df, params). - Recommended:
build_trade_frame(signal_df, params, styles)returningbuild_mapped_trade_frame(signal_df). - Optional: visual declarations or
build_visuals(...), but only for rendering already-prepared data.
Stage ownership table#
| Stage | What belongs here | What must not happen here |
|---|---|---|
build_indicator_frame | All indicator series computation, source resolution, warmup handling, row-oriented visual_* prep, static attrs config. | Final drawing calls through ctx.*. |
build_signal_frame | Signal logic, filter logic, canonical trade-frame execution fields, strategy visuals prep. | Order schema normalization beyond mapping-only helpers. |
build_trade_frame | Mapping-only normalization, usually build_mapped_trade_frame(...). | ta.*, request.*, rolling windows, dataframe grouping, signal recomputation. |
build_visuals | Render-only mapping into ctx.* or ctx.atk.*. | Mutating the frame, computing TA, storing stale full-frame dynamic payloads in frame.attrs. |
How data should flow#
Inputs → params merge → frame builder → visual mapping. Inputs define defaults. Runtime params override them. The frame builder creates plain dataframe columns. Only after those columns exist should declarations or build_visuals consume them.
Why stage separation matters#
When compute, trade mapping, and rendering are mixed together, the script becomes hard to debug, hard to slice, and hard for the runtime to keep consistent across chart reloads. The stage split is not style advice; it is the stability contract.
Library contract#
- Required:
library(...). - Recommended: export small, reusable helpers or enum types.
- Not expected:
build_indicator_frame,build_signal_frame, or chart visuals. - Import through
import_library(...)orlibrary_import(...).
Keep library code deterministic, narrow, and reusable. It should own helper logic such as length resolution, bias labeling, transform helpers, or shared signal utilities — not chart rendering or strategy mapping.
Stage placement guide#
| If you need to... | Put it here | Reason |
|---|---|---|
Choose between open, close, hl2, or a user-selected source column | build_indicator_frame or build_signal_frame | Source selection is part of computation, not rendering. |
| Compute EMA, ATR, BBands, MACD, or MTF trend filters | build_indicator_frame or build_signal_frame | All TA and request work belongs in compute stages. |
Emit entry_price, sl, tp | build_signal_frame | Strategies should fully prepare execution fields before trade mapping. |
| Convert canonical trade-frame columns into the runtime's normalized schema | build_trade_frame | This stage exists for normalization, not for new trading logic. |
| Create a dashboard table, zone box, or object bundle from prepared payload | build_visuals | It is pure rendering of already-prepared data. |
| Store a static style bundle such as table colors, widths, headers, or label templates | frame.attrs | Static config is safe here because it does not depend on the current slice length. |
| Store last-row x/y anchors, projected tails, or slice-dependent indices | Do not pre-store them wholesale in frame.attrs | Those values can go stale when the runtime passes only a slice into build_visuals. |
Important: If you use frame.attrs for object-like visuals, store only static config there and derive dynamic x, y, tail, or slice-dependent values from the actual frame passed into build_visuals().
Authoring rule: choose the declaration by runtime responsibility, not by visual appearance. A script that draws arrows is still an indicator if it does not emit mapped trade fields. A script with clean chart visuals is still a strategy if its purpose is execution mapping.