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, default required): Display name in the indicator panel
  • description (str, default ""): Optional tooltip description
  • overlay (bool, default false): Draw on price chart vs separate pane
  • data_mode (str, default required): "tick" (raw TBBO) or "ohlc" (bars at timeframe). Required.
  • timeframe (str, default required when ohlc): "1m" / "5m" / etc. Required when data_mode = "ohlc".
  • cross_view (bool, default false): 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 line
  • PlotType::Histogram (enum): Vertical bars from zero. Per-bar point_colors supported.
  • PlotType::Area (enum): Filled area under the line
  • PlotType::Columns (enum): Vertical columns. Per-bar point_colors supported.
  • PlotType::Cross (enum): Per-point cross marker. Per-point point_colors supported.
  • PlotType::Circles (enum): Per-point circle marker. Per-point point_colors supported.
  • 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, default required): Series name (legend label)
  • values (Vec<f64>, default required): Histogram values, one per tick
  • base_color (&str, default required): Hex fallback when colors[i] is None
  • colors (Vec<Option<String>>, default required): 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_align only affects the LAST plot pushed to the output - chain it directly after the plot builder.
  • Per-bar point_colors still 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>>, default required): 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, default required): Built via CanvasLayer::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")