How to Build a Quant Charts Indicator (Python)

A Quant Charts indicator is a Python class decorated with @indicator that implements calculate(self, df) and calls plot(). This walkthrough builds a working dual-moving-average indicator.

01. Your first Python indicator

A guided 6-step walkthrough that ends with a working dual_sma.py indicator: two moving averages plus a fill that turns green when fast is above slow and red when it crosses below.

You will use @indicator, declare two input.int parameters and two input.color parameters, compute averages with ta.sma, and call plot() and fill(). By the end you will know how to: declare a Python indicator, expose user-tunable parameters, read the OHLC dataframe, plot named series, and fill between them. Read each step in order; the final step has the complete file you can paste into indicators/.

Step 1. Imports

Pull only what you need from quant_charts. Each imported name is documented in the Python tab.

Example

import numpy as np
from quant_charts import indicator, input, ta, plot, fill

Notes

  • ta is the vectorized technical-analysis namespace (ta.sma, ta.ema, ta.rsi, ta.atr, ...).
  • Importing plot and fill here lets you call them as plain functions inside calculate().

Step 2. The decorator

Mark the class as an indicator. The decorator registers it with the engine and exposes its parameters in the chart settings panel.

Example

@indicator(
    name="Dual SMA",
    description="Fast/slow SMA with cross-direction fill.",
    overlay=True,
    data_mode="ohlc",
    required_columns=["close"],
)
class DualSMA:
    pass

Notes

  • overlay=True draws on the price pane. Set to False for a separate subpane (e.g. RSI, MACD).
  • data_mode="ohlc" runs the indicator on aggregated bars. Use "both" if it should also work in tick-by-tick TBBO view.
  • required_columns makes the loader ship only the columns you read across the worker boundary. Keeps sweep memory tight.

Step 3. Parameters

Declare class-level input.* fields. They become editable inputs in the indicator panel and the analyzer parameter form.

Example

class DualSMA:
    fast_period = input.int(20, "Fast Period", min=2, max=500)
    slow_period = input.int(50, "Slow Period", min=2, max=500)
    fast_color = input.color("#7aa2f7", "Fast Color")
    slow_color = input.color("#e0af68", "Slow Color")
    bull_fill = input.color("#22c55e40", "Bull Fill")
    bear_fill = input.color("#ef444440", "Bear Fill")

Notes

  • input.int(default, label, min=, max=, step=) exposes an integer field. input.float is the same shape for floats.
  • input.color(default_hex, label) accepts 6-char #rrggbb or 8-char #rrggbbaa. The trailing 40 here is hex alpha (40/255 ~= 25%).
  • Read parameters as self.fast_period, self.fast_color, etc., inside calculate().

Step 4. The calculate body

Compute both averages and decide the fill color per bar.

Example

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)
        fast = ta.sma(close, self.fast_period)
        slow = ta.sma(close, self.slow_period)
        # element-wise: pick bull color where fast > slow, bear color otherwise
        return fast, slow

Notes

  • Convert pandas columns to numpy with np.asarray(..., dtype=np.float64). The TA namespace expects numpy arrays.
  • ta.sma(close, period) returns an array the same length as close, with NaN for the warmup bars before period samples are available.
  • The return value of calculate() is ignored when you use plot()/fill() directly. Returning is optional for indicators (signal dicts are for strategies, not indicators).

Step 5. Plot the lines and fill between

Two plot() calls register named series on the chart. fill() references those names and shades the area between them.

Example

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)
        fast = ta.sma(close, self.fast_period)
        slow = ta.sma(close, self.slow_period)

        plot(fast, "Fast SMA", color=self.fast_color, linewidth=2)
        plot(slow, "Slow SMA", color=self.slow_color, linewidth=2)

        # fill takes the two plot names; color is hex with optional alpha
        fill("Fast SMA", "Slow SMA", color=self.bull_fill)

Notes

  • The name you pass to plot() is what fill() references. Names must be unique within an indicator.
  • Single-color fill is the simplest path. To get a regime-switching fill (green when bull, red when bear), call fill() twice with two pairs of bookkeeping plots, or use bgcolor() on a per-bar basis. The single-color path is enough for this tutorial.

Step 6. The full file

Save this as dual_sma.py under your workspace/indicators/ and Quant Charts auto-loads it.

Example

import numpy as np
from quant_charts import indicator, input, ta, plot, fill


@indicator(
    name="Dual SMA",
    description="Fast/slow SMA with cross-direction fill.",
    overlay=True,
    data_mode="ohlc",
    required_columns=["close"],
)
class DualSMA:
    fast_period = input.int(20, "Fast Period", min=2, max=500)
    slow_period = input.int(50, "Slow Period", min=2, max=500)
    fast_color = input.color("#7aa2f7", "Fast Color")
    slow_color = input.color("#e0af68", "Slow Color")
    fill_color = input.color("#7aa2f733", "Fill")

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)
        fast = ta.sma(close, self.fast_period)
        slow = ta.sma(close, self.slow_period)

        plot(fast, "Fast SMA", color=self.fast_color, linewidth=2)
        plot(slow, "Slow SMA", color=self.slow_color, linewidth=2)
        fill("Fast SMA", "Slow SMA", color=self.fill_color)

Step 7. Run it

Open the indicator in Quant Charts and attach it to a chart.

  1. Open the Indicator panel from the chart top bar, click "Add", select "Dual SMA".
  2. Tune Fast Period / Slow Period from the panel; the chart re-runs on every change.

Notes

  • Validation runs on save. If it fails, errors print to the in-app terminal panel.
  • Adjusting an input.color from the panel respects the 8-char hex with alpha format.

Complete example: moving averages

# qc-api: 1.0.7
# DISCLAIMER: This software is for educational and informational purposes only and does not constitute
# financial advice, investment advice, or trading advice. Past performance is not indicative of future
# results. Trading futures and other financial instruments involves substantial risk of loss. You are
# solely responsible for your own trading decisions. Quant Charts LLC assumes no liability for any
# losses incurred. All rights reserved. (c) Quant Charts LLC

"""Moving Averages: SMA, EMA, and WMA on one overlay (Python / OHLC).

Required columns: close.
Data mode: OHLC.

The canonical moving-average example. Plots three moving averages of close on
the price pane and emits trend tags from their stack order:

  ma_bull : close > EMA > SMA   (fast above slow, price leading)
  ma_bear : close < EMA < SMA

Use `ta.sma`, `ta.ema`, `ta.wma` rather than a hand-rolled rolling mean: they
are vectorized and JIT-compiled for large series.
"""

import numpy as np
from quant_charts import indicator, input, plot, define_tag, ta


@indicator(
    name="Moving Averages",
    description="SMA + EMA + WMA overlay with bull/bear stack-order tags",
    overlay=True,
    data_mode="ohlc",
    required_columns=["close"],
)
class MovingAverages:
    sma_period = input.int(50, "SMA Period", min=2, max=1000,
                           tooltip="Simple moving average lookback in bars.")
    ema_period = input.int(20, "EMA Period", min=2, max=1000,
                           tooltip="Exponential moving average lookback in bars.")
    wma_period = input.int(20, "WMA Period", min=2, max=1000,
                           tooltip="Weighted moving average lookback in bars.")
    sma_color = input.color("#A1A1AA", "SMA Color")
    ema_color = input.color("#7AA2F7", "EMA Color")
    wma_color = input.color("#73DACA", "WMA Color")

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)

        sma = ta.sma(close, self.sma_period)
        ema = ta.ema(close, self.ema_period)
        wma = ta.wma(close, self.wma_period)

        plot(sma, f"SMA({self.sma_period})", color=self.sma_color, linewidth=2)
        plot(ema, f"EMA({self.ema_period})", color=self.ema_color, linewidth=2)
        plot(wma, f"WMA({self.wma_period})", color=self.wma_color, linewidth=2)

        finite = np.isfinite(sma) & np.isfinite(ema)
        ma_bull = finite & (close > ema) & (ema > sma)
        ma_bear = finite & (close < ema) & (ema < sma)

        define_tag("ma_bull", "close > EMA > SMA", color="#73DACA")
        define_tag("ma_bear", "close < EMA < SMA", color="#F7768E")

        return {
            "ma_bull": ma_bull,
            "ma_bear": ma_bear,
        }
Machine-readable: full API at /llms-full.txt · examples on GitHub.