request.security()

request.security() lets you access data from other symbols or timeframes within your script. PyneCore runs each security context as a separate OS process with its own Series history, enabling true multi-symbol and multi-timeframe analysis.

Quick Start

1. Prepare OHLCV Data

Each security context needs its own OHLCV data file. Convert your data sources to PyneCore’s binary format using the CLI:

# Chart data (5-minute EURUSD)
pyne data convert-from EURUSD_5m.csv

# Security data (daily EURUSD for HTF analysis)
pyne data convert-from EURUSD_1D.csv

# Or aggregate from existing data
pyne data aggregate EURUSD_5m -tf 1D

This creates .ohlcv + .toml file pairs in workdir/data/.

2. Write Your Script

Use request.security() like you would in Pine Script:

"""@pyne"""

from pynecore.lib import *
from pynecore.types import *


@script.indicator("Multi-Timeframe SMA")
def main():
    # Fetch daily SMA(20) while running on 5-minute chart
    daily_sma: Series[float] = lib.request.security(
        lib.syminfo.tickerid, "1D", lib.ta.sma(lib.close, 20)
    )

    lib.plot.plot(daily_sma, "Daily SMA", color=lib.color.blue)
    lib.plot.plot(lib.close, "Close")

3. Run with Security Data

Use the --security flag to provide OHLCV data for each security context. The flag can be repeated for multiple contexts:

# Single security context (daily data for same symbol)
pyne run multi_tf_sma.py EURUSD_5m --security "1D=EURUSD_1D"

# Multiple security contexts (different symbols)
pyne run advance_decline.py SPX_1D \
  --security "USI:ADVN.NY=USI_ADVN_NY" \
  --security "USI:DECL.NY=USI_DECL_NY"

The format is "KEY=DATA_NAME" where:

  • KEY is "TIMEFRAME" or "SYMBOL:TIMEFRAME" (matching the request.security() call)
  • DATA_NAME is the OHLCV data name in workdir/data/ (without extension)

Key Matching Rules

The security_data dict keys are matched against each request.security() call’s symbol and timeframe:

Key formatExampleMatches
"TIMEFRAME""1D"Any security call with timeframe "1D"
"SYMBOL:TIMEFRAME""AAPL:1H"Exact match on both symbol and timeframe
"SYMBOL""USI:ADVN.NY"Any security call with that symbol

Timeframe-only keys are convenient when all security calls use the same symbol (the chart symbol).

For programmatic usage (ScriptRunner API), see Providing Security Data.

Tip — skip the per-run --security flags. To translate TradingView symbols to your provider-native data once, for every run, declare them in a workdir-level Symbol Map (config/symbol_map.toml) instead. An explicit --security mapping still overrides the symbol map when you need a one-off.

Examples

Multi-Timeframe Indicator

"""@pyne"""

from pynecore.lib import *
from pynecore.types import *


@script.indicator("MTF RSI")
def main():
    rsi_5m: Series[float] = lib.ta.rsi(lib.close, 14)

    # Get RSI from higher timeframes
    rsi_1h: Series[float] = lib.request.security(
        lib.syminfo.tickerid, "60", lib.ta.rsi(lib.close, 14)
    )
    rsi_daily: Series[float] = lib.request.security(
        lib.syminfo.tickerid, "1D", lib.ta.rsi(lib.close, 14)
    )

    lib.plot.plot(rsi_5m, "RSI 5m")
    lib.plot.plot(rsi_1h, "RSI 1H")
    lib.plot.plot(rsi_daily, "RSI Daily")
security_data = {
    "60": "workdir/data/EURUSD_60",  # 1-hour bars
    "1D": "workdir/data/EURUSD_1D",  # daily bars
}

Multi-Symbol Analysis (Advance/Decline Ratio)

"""@pyne"""

from pynecore.lib import *
from pynecore.types import *


@script.indicator("Advance/Decline Ratio")
def main():
    advancing: Series[float] = lib.request.security("USI:ADVN.NY", "", lib.close)
    declining: Series[float] = lib.request.security("USI:DECL.NY", "", lib.close)

    ratio: Series[float] = lib.nz(advancing) / lib.nz(declining, 1.0)
    lib.plot.plot(ratio, "A/D Ratio")
security_data = {
    "USI:ADVN.NY": "workdir/data/USI_ADVN_NY",
    "USI:DECL.NY": "workdir/data/USI_DECL_NY",
}

Note: When the timeframe argument is "" (empty string), the chart’s own timeframe is used.

Supported Features

FeatureStatusNotes
Different timeframesupportedHTF (1D, 1W, 1M, etc.) from lower TF chart
Different symbolsupportedAny symbol with available OHLCV data
Lower timeframe (LTF)supportedrequest.security_lower_tf() returns arrays
Multiple security callssupportedEach gets its own process
Conditional callssupportedInside if/for/while blocks
Nested security callssupportedsecurity(... security(...) ...)
Dependent security callssupportedOne context’s expression reads another’s value (see below); a producer must precede its consumer
barmerge.gaps_offsupportedForward-fills last value (default)
barmerge.gaps_onsupportedReturns na between periods
barmerge.lookahead_offsupportedMost recently closed bar — historical + live (same-symbol HTF in live)
barmerge.lookahead_last_closedsupportedPyneSys-native synonym for “last closed”; identical transport to lookahead_off
barmerge.lookahead_onsupportedSame-symbol HTF: TV-compatible (live steps into developing HTF bar). Cross-symbol HTF: close[0] is na inside an open HTF period, close[1] at boundary delivers just-closed bar
ignore_invalid_symbolsupportedReturns na for missing symbols
currency parametersupportedAuto-converts result using CurrencyRateProvider
ticker.heikinashi()supportedTransformed to Heikin Ashi bars in the security child, backtest and live (see below)
ticker.renko() / pointfigure() / kagi() / linebreak()unsupportedPath-dependent chart types need tick data PyneCore does not have

How It Works

Under the hood, each request.security() call spawns a separate OS process:

  1. AST transformation rewrites the call into signal/write/read/wait protocol functions
  2. ScriptRunner detects the transformed code, creates shared memory, and spawns processes
  3. Each security process loads its own OHLCV data and runs the script independently
  4. Processes communicate results via shared memory
  5. The chart process waits for security results only when a new period is confirmed
  6. Pipeline parallelism: security processes run on separate CPU cores concurrently

Bar pairing: one close rule

Every pairing — the chart reading a context, or one context reading another — follows a single rule:

A consumer bar sees the peer’s last bar whose scheduled close is at or before the consumer’s as-of instant.

Scheduled close. A bar’s close is its real one, never a nominal timeframe addition:

  • intraday — min(open + span, next open, session end), so a session-closing stub (15:30 → 16:00 on a 60-minute grid) and a bar shortened by the next open both close where they really do;
  • daily/weekly/monthly — the end of the last scheduled session inside the period. An equity weekly bar closes Friday 16:00, an FX weekly bar Friday 17:00 New York; never the following Monday’s open, and never a nominal month length.

As-of instant. For a chart bar it is that bar’s own scheduled close (on a developing live bar: the round’s fixed tick instant, read once per bar cycle so every context of that bar agrees). A security context consuming another one takes the minimum of its own scheduled close, the calendar extension below, and the chart’s as-of for that peer — the last term is what guarantees a consumer never waits for a bar the chart does not confirm.

Calendar extension. When the peer is a daily/weekly/monthly context on the same trading calendar (same exchange timezone and opening_hours) and the as-of instant falls inside a scheduled break, it extends to the session open that ends that break. So a 15:00–16:00 equity hourly bar, closing at 16:00 in the break, already sees that day’s daily bar; a 09:30–10:30 bar, closing inside the session, does not. A 24h symbol has no break and is never extended, and a peer on a different calendar never is either — it may trade during this one’s break.

The rule was measured against TradingView on three pairings, which all reduce to it:

PairingTradingView’s answerAgreement
Weekly consumer of a daily producerThe week’s LAST daily bar8000/8000
Daily consumer of a weekly producerThe last weekly bar CLOSED by the day’s close8000/8000
Daily consumer of another symbol’s daily barThe bar paired by close instant8000/8000
Chart bars (5m):    10:00  10:05  10:10 ... 23:50  23:55  00:00
                                                    ^
                                              Closes at 00:00 — exactly when the
                                              daily bar closes, so the day's value
                                              is confirmed on THIS chart bar

For same-timeframe contexts (different symbol), values are confirmed on every bar.

Lower-timeframe windows

request.security_lower_tf() and a scalar request.security() on a finer timeframe take the same rule: a chart bar sees the intrabars whose scheduled close falls inside its own period. On a nested grid (a 1-minute context on a 5-minute chart) that is exactly the chart bar’s window. On a non-nested grid, or behind a shortened session, an intrabar can straddle the chart bar’s close — it opens inside the bar but closes after it. Such an intrabar carries a price from after the chart bar ended, so it is delivered on the next chart bar instead, the one its close falls into. Nothing is lost or duplicated: every intrabar is delivered exactly once, but a chart bar in which no intrabar closes returns an empty array (na for the scalar form).

Chart types (Heikin Ashi)

request.security(ticker.heikinashi(symbol), timeframe, expr) returns expr evaluated on Heikin Ashi candles instead of ordinary ones. Heikin Ashi is a deterministic recurrence over the requested timeframe’s candles:

haClose = (open + high + low + close) / 4
haOpen  = (haOpen[1] + haClose[1]) / 2       # first bar: (open + close) / 2
haHigh  = max(high, haOpen, haClose)
haLow   = min(low,  haOpen, haClose)

The security child transforms each bar to Heikin Ashi before the script reads it, so close returns haClose, open returns haOpen, and any indicator (ta.sma(close, 20), ta.atr(14), …) is computed on the Heikin Ashi series. This works in both backtest and live: on a developing realtime bar haOpen stays fixed (from the prior closed HA bar) while haClose/haHigh/haLow move, and the value commits when the bar closes — matching TradingView. The two carried HA values ride the child’s var-snapshot, so a developing bar recomputes from the fixed prior-close baseline. On a higher timeframe the bars are aggregated to the period first, then transformed — matching TradingView, which builds Heikin Ashi from the requested timeframe’s candles. Inside the child, chart.is_heikinashi reads true.

The source feed is the chart’s own .ohlcv when the symbol is the chart symbol (no --security mapping needed), the mapped file for a cross-symbol backtest, or the live provider stream. ticker.heikinashi() evaluates on every bar in Pine — a cond ? security(...) : x still runs the transform even when cond is false, exactly as TradingView does.

Only Heikin Ashi is supported: renko, pointfigure, kagi and linebreak are path-dependent chart types that need intrabar tick data PyneCore does not carry, and they raise a clear error.

gaps_on vs gaps_off

ModeNew period confirmedBetween periods
gaps_offReturn new valueReturn last value (forward-fill)
gaps_onReturn new valueReturn na

Lookahead modes

request.security() accepts a lookahead argument. Three modes are recognized:

ModeBehavior (historical / backtest)Behavior (live mode, same-symbol HTF)
barmerge.lookahead_off (default)Most recently CLOSED security bar (TV-faithful)Most recently CLOSED security bar; each HTF period close is shipped via the chart-side HTFAggregator (the static .ohlcv file cannot grow at runtime). No developing exposure.
barmerge.lookahead_last_closedMost recently CLOSED security bar (functionally equivalent to lookahead_off)Most recently CLOSED security bar — uses the same closed-bar transport as lookahead_off; repaint-free.
barmerge.lookahead_onDeveloping (containing) HTF bar, built from the chart timeframe up to the current chart bar — never the period’s final valueSame as historical: developing (containing) HTF bar with barstate.isconfirmed=False; OHLCV is aggregated from the chart timeframe by HTFAggregator. On HTF period close the just-closed bar is delivered first (so the security bar_index advances), then the new developing bar. TV-compatible close[1] idiom returns the previously closed bar.

Why lookahead_last_closed — in historical backtests it matches lookahead_off, and in live mode it stays repaint-free (it never shows the developing security bar). Prefer it when you want stable last-closed values without depending on the TV close[1] idiom.

Why a bare close under lookahead_on differs from TradingView — TV’s classical lookahead_on behavior is to expose the containing HTF bar’s final close on every chart bar of the period, producing a future-leak that silently inflates backtest results. PyneCore does not reproduce lookahead (see No lookahead, ever), so the subprocess is given the containing period as aggregated up to the current chart bar rather than being allowed to read that period’s completed bar from its own data file. Historical and live mode take the same path — the HTFAggregator is fed on every chart bar, warmup included.

The daily-pivot idiom request.security(sym, "D", close[1], lookahead_on) is unaffected and TV-exact: close[1] names a period that has genuinely closed. Only the bare close[0] form differs, and only inside an open period — on a period’s last chart bar the developing bar and the closed bar are the same bar, so the values coincide there too.

Cross-symbol HTF — the chart-side HTFAggregator aggregates the chart symbol’s OHLCV; it cannot produce OHLCV for a different security symbol. Cross-symbol HTF contexts therefore keep no aggregator and read closed bars directly from the security’s own .ohlcv (historical) or live feed.

  • lookahead_off / lookahead_last_closed — closed-bar semantics, identical to same-symbol behaviour. Repaint-free.
  • lookahead_on — the containing developing bar is unknown (cannot be aggregated from the wrong instrument). Chart-side __sec_read__ returns na for every chart bar inside an open HTF period; the subprocess still advances on closed cross-symbol HTF bars, so close[1] on the first chart bar of a fresh HTF period delivers the just-closed cross-symbol HTF close. The TV lookahead_on + close[1] idiom continues to work at the period boundary. Behaviour is identical in historical and live mode — the backtest never silently emits a value live could not produce.
# Default — most recently closed bar
htf_close = lib.request.security(lib.syminfo.tickerid, "60", lib.close)

# PyneSys-native — repaint-free in both historical and live mode
htf_close = lib.request.security(
    lib.syminfo.tickerid, "60", lib.close,
    lookahead=lib.barmerge.lookahead_last_closed,
)

# TV-compatible — live mode exposes the developing bar; the close[1] idiom
# is the canonical TV way to read the most recently closed HTF bar.
htf_close_prev = lib.request.security(
    lib.syminfo.tickerid, "60", lib.close[1],
    lookahead=lib.barmerge.lookahead_on,
)

Dependent security expressions

A security expression may read the value of another security context:

rsi_m = lib.request.security(lib.syminfo.tickerid, "M", lib.ta.rsi(lib.close, 14))
sma_m = lib.request.security(lib.syminfo.tickerid, "M", lib.ta.sma(rsi_m, 14))

The inner value is evaluated in the requested context, so sma_m is the monthly SMA of the monthly RSI — identical to the fully nested form security(tickerid, "M", ta.sma(ta.rsi(close, 14), 14)), warmup included. Chains of any depth work, and so do peers on different symbols, calendars and timeframes; each pairing uses the close rule above.

Rules and limits of dependent expressions:

  • Only earlier calls are dependencies. A context’s expression can read the result of any request.security() call that appears before it in the script, including contexts whose symbol or timeframe is only known at runtime. A value carried from a later call (through a var, i.e. the previous bar’s result) is read as na inside the requested context: waiting for a later call would stall the warmup, where a context replays many bars in one step. Order the calls so a producer comes before its consumer.
  • lookahead_on peers use close semantics. Reading a lookahead_on context from another context pairs by scheduled close like every other peer — it does not expose the peer’s developing bar.
  • A chart-context peer is na during the consumer’s warmup. A context evaluated on the chart itself (same symbol, same timeframe) has no value before the chart’s first bar, so a consumer replaying its own warmup reads na there.
  • request.security_lower_tf() as a peer is supported for file-backed contexts: the consumer receives the intrabars closing inside its own bar, not the chart bar’s window. A lower-timeframe context fed by a live stream publishes whole chart-bar windows and has no per-intrabar history, so reading one from another context returns the default (an empty array) without waiting.
  • A security call inside a loop writes several times per bar. The first write of a bar publishes; an identical repeat is a no-op; a repeat with a different value raises loop-varying security expression is not supported, because the chart may already have read the first one. Loop-invariant arguments (the usual case) are unaffected.

Limitations

  • Cross-symbol HTF — the live HTF transport aggregates chart OHLCV, so it works only when the security symbol matches the chart symbol. Cross-symbol HTF lookahead_off / lookahead_last_closed deliver closed bars from the security’s own .ohlcv / live feed. Cross-symbol HTF lookahead_on returns na for the current chart bar inside any open HTF period (the developing bar cannot be aggregated cross-instrument); close[1] at the period boundary still delivers the just-closed cross-symbol HTF close, preserving the TV idiom. For continuous developing-bar coverage of a cross-symbol HTF, run that context as a separate same-symbol chart.
  • Standalone mode — python script.py data.csv does not support --security yet. Use pyne run or the ScriptRunner API.
  • Chart types + LTF — a chart type combined with request.security_lower_tf() raises at security-context setup (it would need per-intrabar transformation). Ordinary (non-chart-type) LTF requests are unaffected.

Debugging Security Contexts

log.info(), log.warning(), and log.error() calls inside security processes are suppressed by default (matching TradingView behavior). To enable logging for debugging, set the PYNE_SECURITY_LOG environment variable:

PYNE_SECURITY_LOG=security.log pyne run my_script.py my_data --security "1D=my_data_1D"

Each line is prefixed with the security context identifier:

[AAPL 1D] [2025-07-05 14:30:00-0400] bar:    42 INFO    Daily SMA: 150.25
[EURUSD 1H] [2025-07-05 14:00:00+0000] bar:   100 INFO    RSI: 72.5

See Debugging for more details.

Session anchoring of intraday higher timeframes

For intraday higher timeframes (minutes and hours), PyneCore anchors the HTF bar grid to the session open, matching TradingView. On a market that opens at 09:30, a 1H security therefore produces bars at 09:30, 10:30, 11:30… rather than 09:00, 10:00, 11:00. The grid steps in real time from the open, so it stays correct across daylight-saving transitions, and the alignment is derived from the symbol’s own session — a cross-symbol security in a different exchange session anchors to its open, not the chart’s. Markets that open on a whole-tf boundary (24/7 crypto, on-hour forex and futures) are unaffected: the session-anchored grid is identical to a plain clock-floor there.

The session open is read from the symbol’s session_starts metadata. If a security’s session information is missing, intraday bars fall back to the UTC clock-floor for that symbol.

Known Differences from TradingView

Holidays and unscheduled early closes. A holiday or an exchange early close is not in the trading schedule, and from the bar data alone it is indistinguishable from a data gap. PyneCore therefore never infers one: a daily/weekly/monthly bar shortened by such a day becomes visible one consumer bar later than on TradingView, rather than being guessed from the bar grid. A weekly bar ending on a holiday Friday, for example, appears on the following Monday. Backtest and live mode behave identically here — live has always followed the schedule — and no value is ever a price from after the consumer bar’s own close.

Daily/weekly/monthly peers on a different calendar. When a D/W/M context keeps a different trading calendar than the consumer (a different exchange timezone or opening_hours), no calendar extension applies: the peer may trade during the consumer’s break, so only its scheduled close counts. The peer’s bar therefore arrives one consumer bar later than TradingView shows it, instead of the consumer reading data from past the peer’s close.

On markets with shortened trading sessions (e.g., half-day sessions before holidays), minor differences may occur when the chart symbol and the security symbol follow different session calendars — one closes early while the other trades a full day. This can cause period boundary alignment to differ slightly from TradingView. In practice, this is rare and only affects a handful of bars on specific calendar dates.

Straddling lower-timeframe bars. An intrabar that opens inside a chart bar but closes after it (a non-nested grid, or a session-shortened intrabar) is delivered on the next chart bar. See Lower-timeframe windows.

Markets with an intraday recess (e.g. a lunch break) keep a single grid anchored to the day’s primary session open; the bar whose window spans the recess simply holds the data of the first trade after it, as on TradingView for the common case.

For technical implementation details (AST transformation, shared memory layout, process lifecycle), see the request.security() Internals page.