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, defaultrequired): 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, defaultrequired): 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, defaultrequired): 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, defaultrequired): Boolean array, color applied where Truecolor(str, defaultrequired): Hex color for candle bodywick(str, defaultNone): Optional hex color for wickborder(str, defaultNone): 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, defaultrequired): Boolean array, shapes placed where Trueshape(str, default"triangle_up"): triangle_up, triangle_down, circle, diamond, square, arrow_up, arrow_down, crosslocation(str, default"above"):"above"(above high),"below"(below low),"at"(at close)color(str, default"#00ff00"): Hex color stringsize(str, default"small"):"small","medium","large"text(str, defaultNone): 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, defaultrequired): Bar index where the box startsend_index(int, defaultrequired): Bar index where the box ends. Ignored when extend_right=True.top(float, defaultrequired): Price level for the top edgebottom(float, defaultrequired): Price level for the bottom edgecolor(str, default"#7aa2f7"): Hex color stringopacity(int, default20): Opacity 0-100extend_right(bool, defaultFalse): 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, defaultrequired): Boolean array, True bars get the backgroundcolor(str, default"#7aa2f7"): Hex color stringopacity(int, default20): 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, defaultrequired): Boolean, True starts a boxend_condition(array-like, defaultrequired): Boolean, True ends a boxcolor(str, default"#7aa2f7"): Hex color stringopacity(int, default20): Opacity 0-100top(float, defaultNone): Price level for top edge (None = chart top)bottom(float, defaultNone): 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, defaultrequired): Tag identifier (matches return dict key)description(str, defaultrequired): Tooltip description shown in UIlabel(str, defaultname): Short display labelcolor(str, defaultauto): 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.Truemarks 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[], defaultabsent (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[], defaultabsent (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_shorton that side. block_entriesblocks 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[], defaultabsent (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_entriesonly gates NEW entries. It never force-closes an open position and never suppresses exits.- Replaces the removed
disable_entriestrigger. For breakeven/tick-shift stops usebreakeven_when()/shift_levels(). - Rust strategies use the symmetric
.with_trading_mask(vec)builder wheretrue= 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[], defaultabsent (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
positioninstead (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[], defaultabsent (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 whenpositionis 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[], defaultrequired): Entry signal array (where positions open)entry_price(float[], defaultrequired): Price series to read the entry price from (e.g. df["close"])tag(bool[], defaultrequired): Bars where the stop should move to breakevenoffset_ticks(int, default0): Ticks above/below entry (signed)tick_size(float, default0.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[], defaultrequired): SL or TP array to transformtag(bool[], defaultrequired): Bars from which the shift armsticks(int, defaultrequired): Signed tick delta to applytick_size(float, default0.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_tickstriggers.