Styling & Tags

Bar/wick/border coloring with 8-char hex alpha, plotshape markers, region shading, and define_tag for signal classification.

bar_color

bar_color(colors)

Set candle body color per bar.

Pass an array of hex color strings, same length as bars. Use None for bars that should keep their default color.

Accepts 6-char (#rrggbb) or 8-char (#rrggbbaa) hex. The 8-char form encodes per-bar alpha, so you can mark a bar 30% transparent without touching the global candle opacity setting.

Parameters

  • colors (array-like, default required): Array of hex color strings (6- or 8-char) or None values

Returns

None

Example

import numpy as np
from quant_charts import bar_color, ta, close

sma = ta.sma(close, 20)
# 8-char hex makes high-vol bars translucent green, default elsewhere
colors = np.where(np.array(close) > sma, "#22c55ecc", None)
bar_color(colors)

wick_color

wick_color(colors)

Set candle wick color per bar.

Same format as bar_color: array of hex strings or None.

Parameters

  • colors (array-like, default required): Array of hex color strings or None values

Returns

None

Example

wick_color(np.where(volume > avg_vol, "#ffffff", None))

border_color

border_color(colors)

Set candle border color per bar (independent from body).

Sets the border color independently from bar_color. With body color unchanged and a contrasting border, candles render in the TradingView "hollow candle" style. Useful for highlighting trades that hit TP vs SL, regime transitions, or volume outliers without changing the up/down body color.

Parameters

  • colors (array-like, default required): Array of hex color strings or None values, same length as bars. None entries use the body color (default LWC behavior).

Returns

None

Example

import numpy as np
from quant_charts import border_color, volume_series

vol = volume_series(df)
avg = np.nanmean(vol)
# white outline on high-volume bars, default border elsewhere
border_color(np.where(vol > 2 * avg, "#ffffff", None))

Notes

  • 8-char hex (#rrggbbaa) encodes per-bar alpha, e.g. "#ffffff80" for 50% white.
  • Pair with bar_color() for the hollow-candle look (body translucent, border solid).
  • Mismatched array length raises ValueError.

set_bar_color

set_bar_color(condition, color, wick?, border?)

Apply color where condition is True.

Convenience wrapper. Multiple calls compose (last-writer-wins per bar). Easier than building a full color array. Optional wick overrides the wick color and border overrides the candle outline color independently.

Parameters

  • condition (bool array, default required): Boolean array, color applied where True
  • color (str, default required): Hex color for candle body
  • wick (str, default None): Optional hex color for wick
  • border (str, default None): Optional hex color for candle border / outline

Returns

None

Example

from quant_charts import set_bar_color, ta, close

rsi = ta.rsi(close, 14)
set_bar_color(rsi > 70, "#ef4444")                              # Red when overbought
set_bar_color(rsi < 30, "#22c55e")                              # Green when oversold
set_bar_color(rsi > 70, "#ef4444", wick="#ff6666")              # Body + wick
set_bar_color(rsi > 70, "#ef4444", wick="#ff6666", border="#aa0000")  # Body + wick + border

plotshape

plotshape(condition, shape?, location?, color?, size?, text?)

Plot shape markers on the chart where condition is True.

Draws visual markers at specific bars. Useful for marking entry/exit signals, divergences, or pattern detections.

Parameters

  • condition (bool array, default required): Boolean array, shapes placed where True
  • shape (str, default "triangle_up"): triangle_up, triangle_down, circle, diamond, square, arrow_up, arrow_down, cross
  • location (str, default "above"): "above" (above high), "below" (below low), "at" (at close)
  • color (str, default "#00ff00"): Hex color string
  • size (str, default "small"): "small", "medium", "large"
  • text (str, default None): Optional text label next to shape

Returns

None

Example

from quant_charts import plotshape, cross_above, cross_below

plotshape(cross_above(fast, slow),
    shape="triangle_up", color="#22c55e")
plotshape(cross_below(fast, slow),
    shape="triangle_down", location="below",
    color="#ef4444")
plotshape(vol_spike, shape="diamond",
    color="#e0af68", text="VOL")

draw_box

draw_box(start_index, end_index, top, bottom, color?, opacity?, extend_right?)

Draw a single rectangle at exact bar indices and exact price bounds.

Unlike box() which colors regions where a boolean condition is True, draw_box() places one rectangle at specific coordinates. Use for precise control: FVG (fair value gap) zones, order blocks, marked-up support / resistance bands, archived value-area boxes.

Parameters

  • start_index (int, default required): Bar index where the box starts
  • end_index (int, default required): Bar index where the box ends. Ignored when extend_right=True.
  • top (float, default required): Price level for the top edge
  • bottom (float, default required): Price level for the bottom edge
  • color (str, default "#7aa2f7"): Hex color string
  • opacity (int, default 20): Opacity 0-100
  • extend_right (bool, default False): If True, the box extends to the right edge of the chart regardless of end_index.

Returns

None

Example

from quant_charts import draw_box

# Mark a fair value gap zone between bars 100 and 140
draw_box(100, 140, top=4250.50, bottom=4248.75, color="#7aa2f7", opacity=15)

# Open-ended order block that extends right until invalidated
draw_box(200, 0, top=4255.0, bottom=4253.0, color="#f7768e", opacity=20, extend_right=True)

bgcolor

bgcolor(condition, color?, opacity?)

Draw full-height colored background on bars where condition is True.

Creates colored bands behind candles for session highlighting, zone marking, or any condition-based background.

Opacity is 0-100 (default 20). Color is a hex string.

Parameters

  • condition (array-like, default required): Boolean array, True bars get the background
  • color (str, default "#7aa2f7"): Hex color string
  • opacity (int, default 20): Opacity 0-100

Returns

None

Example

from quant_charts import indicator, bgcolor, hour, minute

@indicator("Session Colors", overlay=True)
class SessionColors:
    def calculate(self, df):
        asian = (hour >= 19) | (hour < 1)
        european = (hour >= 1) & (hour < 9)
        american = (hour >= 9) & (hour < 16)

        bgcolor(asian, color="#FF5722", opacity=10)
        bgcolor(european, color="#4CAF50", opacity=10)
        bgcolor(american, color="#2196F3", opacity=10)
        return {}

box

box(start_condition, end_condition, color?, opacity?, top?, bottom?)

Draw rectangular regions from start to end bars.

Each True in start_condition begins a box. The next True in end_condition closes it.

If top/bottom are None, the box spans full chart height. Set them to price levels for bounded boxes.

Parameters

  • start_condition (array-like, default required): Boolean, True starts a box
  • end_condition (array-like, default required): Boolean, True ends a box
  • color (str, default "#7aa2f7"): Hex color string
  • opacity (int, default 20): Opacity 0-100
  • top (float, default None): Price level for top edge (None = chart top)
  • bottom (float, default None): Price level for bottom edge (None = chart bottom)

Returns

None

Example

from quant_charts import indicator, box, hour, minute

@indicator("Session Box", overlay=True)
class SessionBox:
    def calculate(self, df):
        session_open = (hour == 9) & (minute == 30)
        session_close = (hour == 16) & (minute == 0)
        box(session_open, session_close, color="#7aa2f7", opacity=15)
        return {}

define_tag

define_tag(name, description, label?, color?)

Declare a tag with display metadata for the UI.

Tags are boolean arrays that mark conditions on each bar. Strategies use them to filter trades (e.g., only trade during "morning" bars). define_tag() sets the label, description, and color shown in the tag dropdown.

You can also skip define_tag(). Returning a dict from calculate() auto-creates tags from the dict keys.

Tags drive preventive trade filtering in the analyzer: with a tag selected, the Rust engine receives a trading_mask and refuses to open trades where the tag is False on that bar.

Parameters

  • name (str, default required): Tag identifier (matches return dict key)
  • description (str, default required): Tooltip description shown in UI
  • label (str, default name): Short display label
  • color (str, default auto): Hex color for the tag badge

Returns

None

Example

from quant_charts import indicator, input, plot, ta, define_tag, close

@indicator("RSI", overlay=False)
class RSI:
    period = input.int(14, "Period")

    def calculate(self, df):
        rsi = ta.rsi(close, self.period)
        plot(rsi, "RSI", color="#9C27B0")

        define_tag("overbought", f"RSI > 70", color="#DC2626")
        define_tag("oversold", f"RSI < 30", color="#16A34A")

        return {
            "overbought": rsi > 70,
            "oversold": rsi < 30,
        }

Notes

  • Tags returned from calculate() are boolean arrays. True marks bars where the condition holds.
  • Tags enable preventive trade filtering in the backtester. Only allow trades during tagged periods.
  • If you skip define_tag(), tags are auto-generated from the return dict keys with default colors.

entry_limit_long

entry_limit_longreturn {..., 'entry_limit_long': price_array, 'entry_limit_short': price_array}

Resting limit-entry prices. A non-NaN value rests a PASSIVE limit at that price; the engine fills it AT the level (no adverse slippage, never crossing the spread) instead of the default next-bar-open market fill. NaN = no order.

Return entry_limit_long / entry_limit_short as price arrays (same length as the data, NaN where there is no order) alongside your signals. These are PASSIVE resting orders, filled by aggressive flow trading THROUGH the level: a long limit rests on the bid and fills AT the level when the bid trades DOWN through it (a seller consumed your level); a short limit rests on the ask and fills AT the level when the ask trades UP through it (a buyer lifted your level). They never cross the spread or act as market orders, so a limit placed beyond the market just rests until price trades through it. A non-NaN limit on a side suppresses that side's market entry_long / entry_short for that bar. block_entries blocks limit fills too. The limit arrays shift +1 bar per trading day exactly like entry_long. A resting order cancels at session close (per-day) or at rollover / a large session gap (continuous), and the crossing reference resets at each boundary so a gap cannot retro-fill. Fully backward compatible: a strategy that never sets them behaves exactly as before.

Parameters

  • entry_limit_long (float[], default absent (market entries)): Resting LONG limit price per bar; passive, fills AT the level when the bid trades down through it (a seller hit your level). NaN = no order.
  • entry_limit_short (float[], default absent (market entries)): Resting SHORT limit price per bar; passive, fills AT the level when the ask trades up through it (a buyer lifted your level). NaN = no order.

Returns

part of the calculate() return dict

Example

import numpy as np
import quant_charts as qc

@qc.strategy("Value-Area Fade", timeframe="5m")
class ValueAreaFade:
    def calculate(self, df):
        vp = qc.volume_profile(df)
        setup = df.close < vp.poc  # only rest a bid below the POC
        return {
            # rest a long limit at the value-area low; fills AT the level
            "entry_limit_long": np.where(setup, vp.val, np.nan),
            "exit_long":        df.close > vp.poc,
        }

Notes

  • Fills AT the level with no adverse entry slippage (mirrors the TP-at-level convention).
  • A non-NaN limit suppresses the same-bar market entry_long / entry_short on that side.
  • block_entries blocks limit fills; the order cancels at session close (per-day) or rollover/gap (continuous).
  • Rust strategies use the .with_entry_limit_long(vec) / .with_entry_limit_short(vec) builders on the signal output.

block_entries

block_entriesreturn {..., 'block_entries': bool_array}

Per-bar entry gate returned from calculate(). A truthy value blocks NEW entries on that bar.

Return a block_entries boolean array (same length as the data) alongside your signals. On bars where it is truthy, the engine refuses to open a new position; bars that are absent or falsy are allowed. Open positions and exit signals are unaffected. This is the array-shaped replacement for the old disable_entries action: gate entries declaratively instead of dispatching a runtime action.

For stop-modification patterns, return the SL/TP you want directly in sl_long/tp_long/sl_short/tp_short (the engine ratchets them favorably so a trailing stop just works), or build them with the breakeven_when / shift_levels helpers. To close a position, set exit_long/exit_short.

Parameters

  • block_entries (bool[], default absent (all allowed)): Truthy bar = block new entries; absent/falsy = allowed

Returns

part of the calculate() return dict

Example

from quant_charts import strategy, use_indicator, cross_above, cross_below

@strategy(name="Gated MA", overlay=True)
class GatedMA:
    def calculate(self, df):
        fast = use_indicator('sma', period=10)
        slow = use_indicator('sma', period=30)
        atr  = use_indicator('atr', period=14)

        # block entries when volatility is too low to be worth trading
        low_vol = atr < atr.rolling(50).mean() * 0.5

        return {
            'entry_long': cross_above(fast, slow),
            'exit_long':  cross_below(fast, slow),
            'block_entries': low_vol,
        }

Notes

  • block_entries only gates NEW entries. It never force-closes an open position and never suppresses exits.
  • Replaces the removed disable_entries trigger. For breakeven/tick-shift stops use breakeven_when() / shift_levels().
  • Rust strategies use the symmetric .with_trading_mask(vec) builder where true = allowed.

size

sizereturn {..., 'size': scalar_or_array}

Per-position contract quantity. A scalar broadcasts to every bar (flat sizing); a per-bar array sizes dynamically.

Return size alongside your signals to set how many contracts each position trades. A scalar (5) applies to every entry; a per-bar array sizes each new position by the value at its entry bar. size is read at the entry (open) and held for that position's life: changing it mid-trade does NOT rescale the open position (it applies to the next entry). NaN or <= 0 reads as 1. Omit size entirely for unit size (the default). PnL, equity, MAE/MFE, and the per-trade fee all scale by the entry-time quantity, so a size = 5 trade pays 5x the fee. There is no scale-in / scale-out / pyramiding: the engine holds one position at a time and size is its magnitude. Direction comes from the entry/exit booleans, not from size (it is always positive).

Parameters

  • size (float | float[], default absent (unit size)): Contracts per position. Scalar broadcasts; array sizes per entry bar. NaN/<=0 -> 1.

Returns

part of the calculate() return dict

Example

import numpy as np
import quant_charts as qc

@qc.strategy("Vol-scaled Cross", timeframe="15m")
class VolScaled:
    def calculate(self, df):
        fast = qc.ta.ema(df.close, 20)
        slow = qc.ta.ema(df.close, 50)
        atr  = qc.ta.atr(df.high, df.low, df.close, 14)
        # smaller size when volatile, 1..3 contracts, sized at each entry
        size = np.clip(2.0 / atr, 1, 3).round()
        return {
            "entry_long":  qc.cross_above(fast, slow),
            "exit_long":   qc.cross_below(fast, slow),
            "entry_short": qc.cross_below(fast, slow),
            "exit_short":  qc.cross_above(fast, slow),
            "size": size,
        }

Notes

  • Read at the entry bar and fixed for the position's life. No mid-trade rescale, no pyramiding.
  • Replaces the old global Contracts knob. Sweep it as an input param (qc.input.int(...)) for a flat size you can optimize.
  • Rust strategies use the .with_size_long(vec) / .with_size_short(vec) builders on the signal output.
  • For scale-in / pyramiding / mid-trade rescale, return position instead (the exact net to hold each bar).

position

positionreturn {..., 'position': scalar_or_signed_array}

The exact NET signed position to hold each bar. When present it is authoritative: the engine drives the held net directly off it, so direction, sizing, scale-in/out, and reverse are all just what you emit.

Return position to own position management entirely in code. It is the signed quantity you want held at each bar: +2 = long 2, -1.5 = short 1.5, 0 = flat, NaN = carry the previous target (emit changes only). When position is present the engine IGNORES the entry/exit booleans, size, and the limit columns; it diffs position[i] against the realized net each bar and acts: a larger same-side magnitude scales in (a new FIFO lot), a smaller one scale-outs the oldest lots (whole-unit FIFO, a partial close keeps each lot's own entry price so per-trade MAE/MFE/SNR stay meaningful), a sign flip closes everything then opens the other way (a reverse = two trades, spread crossed twice), and 0 flattens. sl_long/tp_long/sl_short/tp_short STILL apply as brackets keyed by the net's sign (tick-accurate; a hit flattens and the same-bar rebuy is suppressed). EOD/rollover force-flat closes all lots. PnL/equity/fees scale with the held contracts, so equity and drawdown reflect summed concurrent exposure (an N-lot position is N contracts of risk).

Parameters

  • position (float | float[], default absent (use entry/exit + size)): Signed net contracts to hold. Scalar broadcasts; array drives per bar. NaN = carry, 0 = flat, sign = side.

Returns

part of the calculate() return dict

Example

import numpy as np
import quant_charts as qc

@qc.strategy("Pyramiding Trend", timeframe="5m")
class Pyramid:
    def calculate(self, df):
        fast = qc.ta.ema(df.close, 20)
        slow = qc.ta.ema(df.close, 50)
        up = fast > slow
        # hold 0 when flat, scale 1 -> 2 -> 3 as the trend persists, flip short
        held = np.zeros(len(df))
        run = 0
        for i in range(len(df)):
            run = run + 1 if up[i] else 0
            held[i] = min(run, 3) if up[i] else (-1 if fast[i] < slow[i] else 0)
        return { "position": held }

Notes

  • Authoritative: entry/exit booleans, size, and limit columns are ignored when position is set (a warning lists them).
  • Scale-in / partial scale-out is whole-FIFO (no weighted-average entry), so each closed lot keeps its own entry price.
  • SL/TP arrays still work as net-position brackets; equity/drawdown reflect summed concurrent exposure.
  • Rust strategies use the .with_position(vec) builder on the signal output.

breakeven_when

breakeven_when(entries, entry_price, tag, offset_ticks=0, tick_size=0.25)

Build an SL-shaped array that snaps the stop to the entry price (plus an optional tick offset) on every bar where tag is True.

Authoring-time helper. Forward-fills the most recent entry price since the last entries signal, then writes entry_price ± offset_ticks*tick_size wherever tag is True (NaN elsewhere). Return the result as sl_long (or sl_short). It cannot see engine-side fills/exits, but the favorable-only SL ratchet makes a stale breakeven harmless (a value that would loosen the stop is rejected). Replaces the old set_sl_breakeven trigger.

Parameters

  • entries (bool[], default required): Entry signal array (where positions open)
  • entry_price (float[], default required): Price series to read the entry price from (e.g. df["close"])
  • tag (bool[], default required): Bars where the stop should move to breakeven
  • offset_ticks (int, default 0): Ticks above/below entry (signed)
  • tick_size (float, default 0.25): Instrument tick size

Returns

np.ndarray (sl-shaped, NaN where inactive)

Example

from quant_charts import breakeven_when

out['sl_long'] = breakeven_when(entry_long, df['close'], regime_flip, offset_ticks=2)

Notes

  • Best-effort replacement for the stateful set_sl_breakeven; documented limitation: no live fill/exit feedback.

shift_levels

shift_levels(levels, tag, ticks, tick_size=0.25)

Return a copy of an SL/TP array shifted by ticks*tick_size from each True in tag onward (sticky-forward).

Authoring-time helper. From each True in tag, every non-NaN value in levels is shifted by ticks*tick_size (negative tightens). The shift is sticky-forward, mirroring the old shift_sl_ticks/shift_tp_ticks triggers which moved the stop until the position closed.

Parameters

  • levels (float[], default required): SL or TP array to transform
  • tag (bool[], default required): Bars from which the shift arms
  • ticks (int, default required): Signed tick delta to apply
  • tick_size (float, default 0.25): Instrument tick size

Returns

np.ndarray (copy of levels)

Example

from quant_charts import shift_levels

out['sl_long'] = shift_levels(sl_long, tighten_now, ticks=-4)

Notes

  • Replaces the removed shift_sl_ticks / shift_tp_ticks triggers.