Skip to main content
External data does not have to be candles. Any time-stamped rows — signals, ML model outputs, factors, spreads — can be read from a CSV as typed rows. You describe the shape of a row with a Python @dataclass, and each CSV column is mapped to a field of that dataclass.

Declaring a schema

Schema rules:
  • Field types can be float, int, bool, str, a nested @dataclass, or Optional[...] of a scalar type.
  • Optional marks a field as nullable: an empty CSV cell reads as None. Optional is only allowed on scalar fields, not on nested dataclasses. Reading such a field gives an indie.Optional value: test it with is None or unwrap it with .value() / .value_or().
  • list fields are not supported here, even though @dataclass allows them elsewhere in Indie. Recursive dataclass references are not supported either.
  • Field names must be ASCII identifiers; the name _ is reserved.
  • A schema can have at most 64 scalar fields in total. Fields inside nested dataclasses count toward the limit; the field holding the nested dataclass itself does not.
  • int fields must fit into a 32-bit range; declare the field as float if you need larger values.

Matching CSV columns to fields

Columns are matched to fields by header name:
  • The match is exact and case-sensitive: field signal reads the column signal, not Signal.
  • Fields of nested dataclasses use a dotted path: a field beta inside a nested dataclass stored in field risk reads the column risk.beta.
  • The time column is separate from the schema. Its default name is time (case-insensitive) and can be changed with the time_column argument of sources.Csv().
  • Unknown extra columns are ignored; a missing schema column is an error.
For example, a nested dataclass in field risk with a field beta reads its column as risk.beta:
factors.csv
The ModelOutput schema from above maps to a file like this:
signals.csv
The last row has an empty confidence cell — valid because confidence is declared Optional[float]. Timestamps must be strictly increasing; accepted time formats are listed in CSV format and limits.

Processing rows with a callback: @data_context

A @data_context[T] callback is the typed-data counterpart of @sec_context: it runs once per external row, and its results are merged into the chart timescale of the calling context.
External model signal
Inside a @data_context callback the external rows are available as self.data — a series of your dataclass, where self.data[0] is the current row. An external source carries no candles, so the candle members of the context (self.open, self.high, self.low, self.close, self.volume and derived values) are not available and produce a compile error. self.time, self.bar_index and the bar-state flags remain available. Context.calc_on() with a @data_context callback requires the source argument and does not accept exchange or ticker.

Reading rows without a callback: request_series

If you do not need per-row processing, request the typed series directly:
Risk-adjusted close
request_series[T] returns a Series[T] aligned to the calling context: on each bar, [0] is the latest external row with a timestamp at or before that bar.
On chart bars before the first external row, reading a typed series with [0] raises an error: No data at this bar yet. Use .get(offset, default) with a default row to read this series during warm-up. Pass a default row as in the example above: .get(0, RiskFactor(1.0)) returns the default until the first row arrives. Float series merged from a @data_context callback do not raise: on bars before the first row they read the first row’s result.

Time frame: omitted or explicit

For typed data the time_frame argument is optional. If you omit it, the rows are treated as point-in-time events with no cadence. Each row becomes visible on the first chart bar that opens at or after the row’s timestamp. When several rows fall inside one chart bar, series reads see the latest of them, while a @data_context callback still runs once per row. With an explicit time_frame=, the rows are treated as a regular series on that time frame, with the same close-time alignment and lookahead behavior as external candles. Rows must not be more frequent than the declared time frame. lookahead=True always requires an explicit time_frame.

One schema per source

Within one indicator, the same CSV source (the same URL and time_column) must always be requested with the same dataclass schema — requesting it with two different schemas is an error. Each distinct combination of source, time frame and lookahead counts as one additional instrument toward the shared limit; identical requests are counted once. For file requirements, size limits and the full list of error codes, see CSV format and limits.