Skip to content

Your own data ​

Most desks already have data: a vendor subscription, a recorder of their own, years of exports. None of it has to be reshaped by hand to fit this package. You say what each column is and what unit it is in, and the package does the rest: it reads the file, turns every row into its own shape, checks it, and tells you about every row it could not use. An agent never reads your file directly. It reads the figures the system computed from it, with their age and where they came from.

What goes where ​

You haveFormatsWhere it goesWhat happens to it
trade history (prints)CSV, .csv.gz, a .zip of one CSV, Parquettra data audit FILE --mapping mapping.tomleach row becomes a canonical trade, the file is audited, and a clean dataset is written as Parquet with its manifest. Backtests, research sessions and the file:data link of a live chain read it from there
a dated series: rates, flows, a sentiment score, funding history, anything with a time and a valueCSV, JSON, Parquetone [[series]] entry in sources/<class>.tomlread with the publication lag you declare, so a figure counts only from when it was knowable; graded against your thresholds; shown with its age; ignored past its max_age
dated text: releases, statements, your own notesCSV, JSONsources/news.tomlread as events by the macro analyst and as notes by the sentiment analyst
a live feed from a data vendor: candles, the book, funding, open interestanything your code can reada source plugin, named in a chain in data.tomlasked like any other source, its answer stamped with its age and origin, refused when older than the chain's ttl

How it is stored ​

Whatever shape a file arrives in, the package keeps one shape of its own, and where the data is large it keeps it in a columnar store:

DataArrives asKept as
trade historya venue archive, a CSV export, your own Parquetthe canonical trade, one Parquet file per dataset under data/: prices and sizes as decimal128 at the scale the source used, time as UTC nanoseconds, zstd compressed, with a manifest.json beside it (source files and their SHA-256, row counts, the audit's findings)
candles for a cyclebuilt from that Parquet, or fetched livein the cycle's snapshot.json, with the source and age of each value
dated series and textCSV, JSON, Parquetread in place, through the as-of rule, on every cycle; they are small, and a file you correct is read as corrected on the next cycle
what the desk decidedwritten by the deskone directory per cycle (briefs, answers, intents, orders) as JSON, and one index line per cycle per day

There is no database server to run. The datasets are plain Parquet, so the same files open in DuckDB, Polars, pandas or Spark for your own analysis, without an export step:

sql
-- DuckDB, over every dataset of one instrument
SELECT date_trunc('hour', ts) AS hour, count(*) AS trades, sum(qty) AS volume
FROM 'data/BTCUSDT-*.parquet' GROUP BY 1 ORDER BY 1;

The rules every file is held to ​

These are the same for a venue's archive, a vendor's export and a file you wrote yourself:

  • Every unit is declared. A timestamp column says whether it is seconds, milliseconds, microseconds, nanoseconds or ISO 8601. Nothing is inferred from the size of a number.
  • A time needs a zone. An ISO time with no offset is refused, not assumed to be UTC. A bare date (2026-10-01) is the one exception: it is read as UTC midnight, and the publication lag moves it to when the figure was actually known.
  • Prices and sizes stay exact. They are read as decimals with the precision the source used, stored as decimal128 in Parquet, and never rounded. A value that cannot be stored exactly at the declared scale is refused.
  • A bad row is shown, not dropped. A row whose time, price, size or side cannot be read becomes a rejection with its row number and the reason. Above 0.1 percent of rejected rows the audit fails. A missing column stops the file with the header it actually had.
  • Zero is not a value. A trade with a zero price or size is a defaulted field, and refused.
  • A row about something else is refused. With an instrument column declared, a row for another instrument is a rejection, not a silent mix.

Trade history ​

A mapping.toml names each field:

toml
[mapping]
source = "vendor-x"            # recorded as the trade's venue
ts = "time"
ts_unit = "ns"                 # s | ms | us | ns | iso8601
price = "px"
qty = "size"
trade_id = "id"
side = { column = "aggressor", buy = ["B"], sell = ["S"] }
# instrument = "symbol"        # optional; rows for another instrument are rejected
bash
tra data audit vendor.parquet --mapping mapping.toml --instrument BTCUSDT --max-gap 5m \
    --manifest data/manifest.json --parquet data/BTCUSDT-2025-01-01.parquet
vendor-x BTCUSDT: 2000 trades, 0 rejected
2025-01-01 00:00:00Z to 2025-01-01 00:33:19Z
wrote 2000 trades to data/BTCUSDT-2025-01-01.parquet (decimal scales 2/3)
PASSED

The audit checks order, duplicates, gaps, sequence resets and changes of scale; the full list is in The data gate. The dataset is written only when the audit passed.

Parquet ​

A Parquet file carries types that a CSV leaves to the map, so each type is read the way its meaning requires and then goes through the same map as a CSV:

Parquet typeRead asDeclare
timestamp with a time zonenanoseconds since the epoch, exactts_unit = "ns"
timestamp without a zonea time with no offset, refused by every ts_unitstore it with its zone
dateYYYY-MM-DD, UTC midnight before the publication lagts_unit = "iso8601"
decimalits digits as stored, scale kept
floatthe shortest text that reads back as the same float
nullan empty cell; a row that needs it is rejected

Reading Parquet needs the data extra (pip install 'trading-research-agents[data]').

Dated series ​

toml
[[series]]
name = "us10y"
file = "macro/us10y.parquet"   # .csv, .json or .parquet
ts = "date"
ts_unit = "iso8601"
value = "yield"
unit = "percent"
publish_lag = "1d"             # known the day after the date it describes
source = "your vendor"
max_age = "4d"
threshold = { bearish_above = 4.5, bullish_below = 3.5 }

How series reach the analysts, how they are refreshed and how their age is shown is in Data sources.

A live feed from a vendor ​

A vendor that answers in real time arrives as a source plugin: a small class in your own package, registered under the trading_research_agents.sources entry point and then named in a chain like any bundled source. This package never depends on the vendor.

toml
# pyproject.toml of your plugin package
[project.entry-points."trading_research_agents.sources"]
vendorx = "your_package.vendorx:source"
python
import time
from decimal import Decimal

from trading_research_agents.data.schema import Candle
from trading_research_agents.data.sources.base import Fetched, SourceError


class VendorX:
    name = "vendorx"
    tier = "paid"

    def kinds(self) -> tuple[str, ...]:
        return ("ohlcv",)

    def fetch(self, kind: str, instrument: str, **options) -> Fetched:
        rows = your_client.candles(instrument, options["interval_s"], options["limit"])
        if not rows:
            raise SourceError(f"vendorx has no candles for {instrument}")
        candles = [
            Candle(r.start_ns, Decimal(r.o), Decimal(r.h), Decimal(r.l), Decimal(r.c), Decimal(r.v))
            for r in rows
        ]
        closed = candles[-1].start_ns + options["interval_s"] * 1_000_000_000
        return Fetched(kind, instrument, candles, self.name, self.tier, time.time(), closed)


def source(target: str, settings: dict) -> VendorX:
    return VendorX()
toml
# data.toml
[data.ohlcv]
chain = ["vendorx", "venue", "file:data"]
ttl = "90s"

The source does the normalising: whatever the vendor sends, what comes back is the package's own shape, with the market time the value describes. In venue mode the book and the funding rate must come from the venue the orders go to, so a plugin that serves them says which venue with a venue attribute, and one that does not say is refused for those two kinds. Candles and open interest may come from anywhere you choose, and tra check names the source each kind comes from.

Not covered yet ​

  • A file of candles alone, without the trades they were built from, as a dataset. Today candles for a backtest are built from trades; a live chain can take candles from a source plugin.
  • Order book history from a file.

Both are on the roadmap. Until then, a plugin covers live candles and books from any vendor.

Released under the MIT License.