Indicator API
#[indicator], IndicatorOutput builders, all 7 PlotTypes, per-point color builders, ShapeSpec, fills, hlines.
#[indicator]
#[indicator](name, description?, overlay?, data_mode, timeframe?, cross_view?, version?, columns?)
Attribute macro. Marks a struct as a Rust indicator.
The struct must #[derive(Default)] and implement the Indicator trait. overlay = true draws on the price chart; overlay = false puts the indicator in its own pane.
data_mode is required. "tick" feeds the indicator raw TBBO ticks; "ohlc" feeds OHLC bars at the declared timeframe (the source can be either a native OHLC parquet OR a TBBO file aggregated at runtime). Authors must choose explicitly - there is no default.
timeframe is required when data_mode = "ohlc". Format "1m", "5m", "15m", etc.
cross_view (default false) opts a tick-mode indicator into rendering on OHLC chart views (canonical case: Volume Profile - tick-fidelity compute, render on any view). Symmetric for the rare ohlc + cross_view case (render bar values on a tick chart). Without it a tick-mode indicator is hidden in OHLC view and an ohlc-mode indicator is hidden in tick view.
Decision tree:
- Per-bar overlay computed from bars (EMA, BB, regime line) ->
data_mode = "ohlc", timeframe = "1m"(or your TF) - Tick-fidelity indicator that should render on candlestick charts (VP, VWAP) ->
data_mode = "tick", cross_view = true - Per-tick output that only makes sense in tick view (raw spread, raw imbalance) ->
data_mode = "tick", cross_view = false
Parameters
name(str, defaultrequired): Display name in the indicator paneldescription(str, default""): Optional tooltip descriptionoverlay(bool, defaultfalse): Draw on price chart vs separate panedata_mode(str, defaultrequired): "tick" (raw TBBO) or "ohlc" (bars attimeframe). Required.timeframe(str, defaultrequired when ohlc): "1m" / "5m" / etc. Required when data_mode = "ohlc".cross_view(bool, defaultfalse): Render across both tick and OHLC chart views (Volume Profile pattern).version(str, default"1.0.0"): Author-managed semver-ish version surfaced to the UI for cache invalidation. Bump on API-breaking edits.columns([&str], default[]): Extra column names this indicator reads (UI hint)
Example
// Tick-fidelity Volume Profile that also renders on OHLC charts
#[indicator(
name = "VP",
description = "Per-tick volume distribution",
overlay = true,
data_mode = "tick",
cross_view = true,
)]
#[derive(Default)]
pub struct Vp { /* params */ }
// OHLC overlay; renders only on OHLC chart views
#[indicator(
name = "EMA",
description = "Bar-close EMA",
overlay = true,
data_mode = "ohlc",
timeframe = "1m",
)]
#[derive(Default)]
pub struct Ema { /* params */ }
Indicator trait
Indicator traitimpl Indicator for MyIndicator
One required method: calculate(). Optional prepare() for hoisted per-day work.
Example
impl Indicator for MyIndicator {
fn prepare(data: &TickData) -> DayPrep {
let mut prep = DayPrep::empty();
prep.insert_f64("baseline", rolling_mean(&data.spread, 200));
prep
}
fn calculate(&self, data: &TickData, prep: &DayPrep) -> IndicatorOutput {
let baseline = prep.f64("baseline").unwrap_or(&[]);
let smoothed = rolling_mean(&data.spread, self.smoothing);
IndicatorOutput::new()
.plot_line("Spread", smoothed, "#7aa2f7")
.plot_line("Baseline", baseline.to_vec(), "#a1a1aa")
}
}
IndicatorOutput
Builder for plots, fills, hlines, shapes, regions, and tags.
Default-constructed via IndicatorOutput::new(). Builder methods chain to add visual elements. Final value is returned from calculate().
Example
IndicatorOutput::new()
.with_overlay(false)
.plot_line("MA", ma_values, "#7aa2f7")
.plot_histogram("Volume", vols, "#3a3a45")
.hline("Zero", 0.0, "#63636e")
.fill("Upper", "Lower", "#7aa2f7", 15)
.with_tag("breakout", breakout_mask)
.with_tag_config("breakout", "Breakout Bar", "#26A69A")
PlotType (Rust)
Seven plot types: Line, Histogram, Area, Columns, Cross, Circles, StepLine.
Parameters
PlotType::Line(enum): Continuous linePlotType::Histogram(enum): Vertical bars from zero. Per-barpoint_colorssupported.PlotType::Area(enum): Filled area under the linePlotType::Columns(enum): Vertical columns. Per-barpoint_colorssupported.PlotType::Cross(enum): Per-point cross marker. Per-pointpoint_colorssupported.PlotType::Circles(enum): Per-point circle marker. Per-pointpoint_colorssupported.PlotType::StepLine(enum): Step / staircase line
Notes
- Per-point colors are valid only for Histogram, Columns, Cross, Circles. Lines/Areas/StepLines stay single-color.
plot_line / plot_histogram
plot_line / plot_histogram.plot_line(name, data, color) / .plot_histogram(name, data, color)
Convenience builder methods for the two most common plot types.
Example
IndicatorOutput::new()
.plot_line("EMA(20)", ta::ema(&data.mid, 20), "#7aa2f7")
.plot_histogram("Delta", data.col_or("delta", &[]).to_vec(), "#73daca")
plot_histogram_colored (Rust)
plot_histogram_colored (Rust).plot_histogram_colored(name, values, base_color, colors)
Histogram with per-bar color overrides.
Per-bar coloring for Histogram plots. The colors Vec must equal values.len(). Each entry is Option<String>: Some("#hex") overrides the bar, None falls back to base_color.
Validation is loud: assert_eq!(colors.len(), values.len()) panics during indicator dev if you mismatch lengths.
Parameters
name(&str, defaultrequired): Series name (legend label)values(Vec<f64>, defaultrequired): Histogram values, one per tickbase_color(&str, defaultrequired): Hex fallback when colors[i] is Nonecolors(Vec<Option<String>>, defaultrequired): Per-bar color overrides
Example
// "block trade = yellow" pattern
let mut colors: Vec<Option<String>> = vec![None; data.len()];
for i in 0..data.len() {
if let Some(v) = data.col("volume").and_then(|c| c.get(i)) {
if *v > big_threshold {
colors[i] = Some("#e0af68".to_string()); // yellow
}
}
}
IndicatorOutput::new()
.plot_histogram_colored("Volume", volumes, "#3a3a45", colors)
plot_columns_colored / plot_cross_colored / plot_circles_colored
Same per-point coloring shape as plot_histogram_colored, applied to Columns, Cross, Circles.
Same signature as plot_histogram_colored. Use Columns for vertical column bars, Cross/Circles for per-point markers (e.g. divergence flags, signal markers, microstructure events).
Example
// Cross markers, red on losing trade entries, green on winning
let mut marker_colors: Vec<Option<String>> = vec![None; n];
for i in 0..n {
if entry_signal[i] {
marker_colors[i] = Some(if was_winner[i] { "#22c55e" } else { "#ef4444" }.to_string());
}
}
IndicatorOutput::new()
.plot_cross_colored("Entries", entry_prices, "#a1a1aa", marker_colors)
PlotSpec (general plot)
Construct a PlotSpec directly when you need full control (line width, opacity, visibility).
For anything beyond the convenience builders, build a PlotSpec and pass it to .plot(spec).
Example
let spec = PlotSpec {
name: "Smoothed Delta".to_string(),
plot_type: PlotType::Area,
color: "#73daca".to_string(),
line_width: 2,
opacity: 30,
visible: true,
data: smoothed,
};
IndicatorOutput::new().plot(spec)
hline / fill
hline / fill.hline(name, value, color) / .fill(plot_a, plot_b, color, opacity)
Horizontal reference line; fill between two named plots.
Example
IndicatorOutput::new()
.plot_line("Upper", upper, "#7aa2f7")
.plot_line("Lower", lower, "#7aa2f7")
.fill("Upper", "Lower", "#7aa2f7", 15)
.hline("Zero", 0.0, "#63636e")
ShapeSpec
Per-tick shape markers (arrow_up, circle, cross, etc.) at specific indices.
For sparse markers (a few hundred points across millions of ticks), use ShapeSpec instead of plot_*_colored. Push the specific tick indices and a single shape/color/size.
Example
output.shapes.push(ShapeSpec {
indices: signal_indices,
shape: "arrow_up".to_string(),
location: "below".to_string(),
color: "#22c55e".to_string(),
size: "normal".to_string(),
});
with_align / with_width_px
with_align / with_width_px.with_align(HistogramAlign) / .with_width_px(u32)
Anchor a histogram/columns/area plot to a chart edge or visible-range edge.
Reshapes geometry instead of laying bars along the time axis. Pair with with_width_px(...) for the strip width (PinnedLeft/PinnedRight) or thickness (PinnedTop/PinnedBottom). Apply via PlotSpec or chain after plot_histogram(...).
Use cases: side-anchored cumulative delta (PinnedLeft/PinnedRight), volume-pane layout (PinnedBottom - bars time-align to candles, height normalized to visible-range max), session-anchored volume profile (RightOfRange), full-range overlay (OverRange).
Parameters
HistogramAlign::PinnedTop(enum): Horizontal strip glued to the top edge; bars hang downward, time-aligned to candles.HistogramAlign::PinnedBottom(enum): Horizontal strip glued to the bottom edge (volume-pane layout); bars rise upward, time-aligned to candles.HistogramAlign::PinnedLeft(enum): Vertical strip glued to the chart left edge; bars extend right, anchored to price axis.HistogramAlign::PinnedRight(enum): Vertical strip glued to the chart right edge; bars extend left.HistogramAlign::LeftOfRange(enum): Anchor at the first visible bar of the series time range.HistogramAlign::RightOfRange(enum): Anchor at the last visible bar.HistogramAlign::OverRange(enum): Stretch across the visible range.
Example
IndicatorOutput::new()
.plot_histogram("Cum Delta", cum_delta, "#7aa2f7")
.with_align(HistogramAlign::PinnedLeft)
.with_width_px(60)
Notes
with_alignonly affects the LAST plot pushed to the output - chain it directly after the plot builder.- Per-bar
point_colorsstill apply to pinned histograms; bars stack vertically by row index.
with_border_color
with_border_color.with_border_color(Vec<Option<String>>)
Per-bar candle border color (independent from with_bar_color).
Setting just the border (without changing the body) gives the TradingView "hollow candle" look. Some("#hex") overrides; None falls back to the body color.
Mirrors Python border_color(). Accepts 6-char #rrggbb or 8-char #rrggbbaa hex.
Parameters
colors(Vec<Option<String>>, defaultrequired): Same length as bars. None entries inherit the body color.
Example
let borders: Vec<Option<String>> = (0..n).map(|i| {
if hit_tp[i] { Some("#22c55e".into()) }
else if hit_sl[i] { Some("#ef4444".into()) }
else { None }
}).collect();
IndicatorOutput::new()
.plot_line("EMA", ema, "#7aa2f7")
.with_border_color(borders)
with_custom_layer
with_custom_layer.with_custom_layer(CustomDrawSpec)
Attach a CanvasLayer for arbitrary 2D draws.
Build a layer via CanvasLayer::new(key), chain style and path ops, finalize with .into_spec(), then attach. Multiple layers can be attached; each gets its own zOrder bucket.
See the Rust Custom Canvas section for full builder API.
Parameters
spec(CustomDrawSpec, defaultrequired): Built viaCanvasLayer::new(...).into_spec().
Example
use qc_strategy_api::prelude::*;
let layer = CanvasLayer::new("tf-boundaries")
.z(ZOrder::Top)
.style(StyleDelta::default()
.stroke("#ffffff44")
.line_width(1.0)
.line_dash(vec![4.0, 3.0]));
let mut layer = layer;
for ms in boundaries {
layer = layer
.begin()
.move_to_chart(ms, f64::NAN /* Y_MIN sentinel */)
.line_to_chart(ms, f64::NAN /* Y_MAX sentinel */)
.stroke();
}
IndicatorOutput::new().with_custom_layer(layer.into_spec())
Notes
- Hard cap of 50,000 ops per spec at the renderer.
- Use coord sentinels (
CoordRef::YMin/YMax/XMin/XMax) to span the viewport without knowing the price/time scale.
with_tag / with_tag_config
with_tag / with_tag_config.with_tag(name, values) / .with_tag_config(name, label, color)
Attach boolean tag arrays to the indicator output for preventive trade filtering.
Tags from indicators feed the same preventive-tag-filter pipeline as strategy tags. with_tag_config is optional: undeclared tags get auto-generated UI labels and colors. Declared tags via #[tag(...)] give you control.
Example
let breakout = above_series(&data.mid, &upper_band);
let breakdown = below_series(&data.mid, &lower_band);
IndicatorOutput::new()
.plot_line("Upper", upper_band, "#7aa2f7")
.plot_line("Lower", lower_band, "#7aa2f7")
.with_tag("breakout", breakout)
.with_tag("breakdown", breakdown)
.with_tag_config("breakout", "Upper Break", "#26A69A")
.with_tag_config("breakdown", "Lower Break", "#EF5350")