Stage Contracts
Stage ownership rules for visual authors — what each stage owns and must not do across build_indicator_frame, build_signal_frame, build_trade_frame, and build_visuals.
Stage Contracts for Visual Authors#
| Stage | Owns | Must not do |
|---|---|---|
build_indicator_frame | Indicator computation, prepared visual columns, static attrs config. | Render objects directly. |
build_signal_frame | Strategy computation, MTF helpers, mapped execution intent. | Delay risk, entries, or resampling into later stages. |
build_trade_frame | Mapping or delegation to build_mapped_trade_frame(...). | Run TA, request calls, or invent strategy logic. |
build_visuals | Rendering from prepared data and current frame slice. | Compute indicators, mutate input frames, or depend on stale attrs-only coordinates. |
Practical test: if you could delete build_visuals and still preserve all computed columns and strategy
intent, your stage split is probably correct. If deleting it removes calculations, those calculations belong
earlier.
What Each Stage Means for Visual Authors#
build_indicator_frame#
This is where all computation happens. By the time build_visuals runs, every value it needs should
already exist as a column or in frame.attrs. Prepare visual columns here — signal booleans, anchor
prices, display labels — so build_visuals stays read-only.
build_visuals#
This stage is strictly a render pass. It reads the last rows of the frame, unpacks config from
frame.attrs, and assembles ctx.* or ctx.atk.* intents. It must not call ta.*, resample, mutate
the frame, or perform any computation that belongs in the frame builder.
frame.attrs for static config#
Store colors, headers, style tokens, and other stable configuration in frame.attrs inside
build_indicator_frame. Derive coordinates, tail anchors, and slice-specific geometry from the live
frame slice inside build_visuals.
# build_indicator_frame — store style config in attrs
frame.attrs["label_style"] = {"color": "#2962ff", "style": "label_down"}
# build_visuals — read attrs for config, frame for coordinates
def build_visuals(frame, params=None, ctx=None):
if frame is None or frame.empty or ctx is None:
return None
last = frame.iloc[-1]
style = dict(frame.attrs.get("label_style") or {})
return ctx.label.new(
key="signal_label",
x=int(last["index"]),
y=float(last["low"]),
text="SIGNAL",
**style,
)