> ## Documentation Index
> Fetch the complete documentation index at: https://takeprofit.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> Read arbitrary time-stamped rows from a CSV in Indie. Declare a dataclass schema, process rows with @data_context or read them directly with request_series.

# Typed external data

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

```py theme={null}
from dataclasses import dataclass
from indie import Optional

@dataclass
class ModelOutput:
    signal: float
    confidence: Optional[float]
```

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`](/docs/indie/Data-types-in-Indie#indie-optionalt-class) 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`:

```py theme={null}
from dataclasses import dataclass

@dataclass
class Risk:
    beta: float

@dataclass
class Factors:
    signal: float
    risk: Risk
```

```txt factors.csv theme={null}
time,signal,risk.beta
1704153600,0.42,1.1
1704240000,-0.17,0.9
```

The `ModelOutput` schema from above maps to a file like this:

```txt signals.csv theme={null}
time,signal,confidence
1704153600,0.42,0.9
1704240000,-0.17,0.8
1704326400,0.05,
```

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](/docs/indie/External-data/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.

```py External model signal theme={null}
# indie:lang_version = 5
from dataclasses import dataclass
from indie import indicator, MainContext, data_context, Optional
from indie.data import sources

@dataclass
class ModelOutput:
    signal: float
    confidence: Optional[float]

@data_context[ModelOutput]
def ModelSignal(self):
    return self.data[0].signal * self.data[0].confidence.value_or(1.0)

@indicator('External model signal')
class Main(MainContext):
    def __init__(self):
        self._signal = self.calc_on(
            ModelSignal,
            source=sources.Csv('https://example.com/signals.csv'))

    def calc(self):
        return self._signal[0]
```

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:

```py Risk-adjusted close theme={null}
# indie:lang_version = 5
from dataclasses import dataclass
from indie import indicator, MainContext, request_series
from indie.data import sources

@dataclass
class RiskFactor:
    value: float

@indicator('Risk-adjusted close')
class Main(MainContext):
    def __init__(self):
        self._risk = request_series[RiskFactor](
            source=sources.Csv('https://example.com/risk.csv'))

    def calc(self):
        row = self._risk.get(0, RiskFactor(1.0))
        return self.close[0] * row.value
```

`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.

<Note>
  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.
</Note>

## 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](/docs/indie/External-data/External-candles-from-CSV#how-external-candles-align-with-the-chart). 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](/docs/indie/External-data/CSV-format-and-limits).
