Trail
pip install trail-lang·import trail· CLItrail
The reference implementation of Trail - a small, total, declarative language for computing financial indicators, scores, and screening strategies over panels of securities. Trail expressions compile to vectorized Polars operations.
The language specification (grammar, reference, standard library) lives in trail-language/spec.
model quality on us_main at annual {
operating_margin = income.operating_income / income.revenue
score om_score weight 7 {
2 if operating_margin > 0.12
1 if operating_margin > 0.05
else 0
}
export composite = weighted_score()
}
Install
pip install trail-lang # or: uv add trail-lang
CLI
trail validate models/quality.trail # parse, kind-check, dry-run
trail run models/quality.trail --model quality # evaluate a model
trail run models/quality.trail --model quality --config trail.yaml
trail catalog # discover fields, functions, sources
trail catalog income # fields in a namespace
trail catalog cagr # describe a function
The standard library is loaded implicitly; pass --no-stdlib to opt out.
Library
from trail.pipeline import prepare
from trail.compiler import compile_model
from trail.fixtures import load_panel # a bundled demo panel
program = prepare("model m { export margin = income.operating_income / income.revenue }")
model = next(d for d in program.decls if type(d).__name__ == "ModelDecl")
result = compile_model(model, {}).run(load_panel())
Data sources
Trail evaluates over a (entity × time) panel supplied by a data source. With no config a
bundled in-memory fixture source is used (a demo panel), so trail run works out of the box.
Real data comes from provider packages — install one and it registers a driver you name in
trail.yaml:
pip install trail-fmp # Financial Modeling Prep → driver: fmp
pip install trail-edgar # SEC EDGAR (10-K / 10-Q) → driver: edgar
pip install trail-gmd # Global Macro Database → driver: gmd
Each provider registers under the trail.sources entry-point group, so pip install trail-<name>
makes driver: <name> usable by name — no import wiring. Run trail catalog to list the sources
and fields available in your environment.
trail.yaml (auto-loaded from the working directory, or pass --config) wires sources to models:
sources: # name → driver + provider-specific options (see each provider's README)
edgar:
driver: edgar
options: { identity: "Jane Quant jane@example.com", tickers: [AAPL, MSFT, NVDA] }
fmp:
driver: fmp
options: { api_key: ${FMP_API_KEY}, tickers: [AAPL, MSFT, NVDA] }
gmd:
driver: gmd # country-keyed macro; bridges onto stocks via meta.country
precedence: # which source serves each field, keyed by namespace
income: [edgar, fmp] # income.* → EDGAR first, FMP fills the gaps
balance: [edgar, fmp]
cash: [edgar, fmp]
default: [fmp, gmd] # every other namespace
panel:
periods: [2015, 2024] # inclusive year bounds (fetch hint + filter)
pit: auto # "auto" (default, lookahead-safe) | "naive" (period-end placement)
strict: false # true → a non-conforming source panel is a hard error
- Precedence is per-namespace — a field's first dotted segment (
income,price,fmp,gmd, …), falling back todefault. A chain with more than one source coalesces per(entity, period)cell (first non-null down the chain), so you can layer a precise primary over a broad fallback. - Pin one source inline with
income.revenue @ edgar(skips coalescing). - Point-in-time is on by default (
pit: auto): each value is placed by when it became knowable (its filing date), so a backtest never sees a statement before it was filed. Usepit: naive(globally, oroptions.pit: naiveper source) for pure period-end fundamental analysis.
Provider-specific options live in each provider's README: trail-fmp · trail-edgar · trail-gmd.
Writing a data source
A provider is a class implementing the DataSource contract, registered under trail.sources.
Three methods are mandatory:
import polars as pl
from trail.source import DataSource, LoadRequest, Capabilities
class MySource(DataSource):
name = "mine"
def load(self, request: LoadRequest) -> pl.DataFrame:
# Return a panel: columns `entity` (Utf8), `time` (Datetime, period-end), and one
# column per requested canonical field. request.fields/frequency/periods/entities
# scope the fetch; a superset is fine (the runtime re-filters).
...
def available_fields(self, frequency: str | None = None) -> set[str]:
return {"income.revenue", "income.net_income"} # what you serve
def capabilities(self) -> Capabilities:
return Capabilities(frequency="annual", frequencies=("annual", "quarterly"))
Optional refinements:
- Point-in-time — emit a reserved
__date:<name>column (Datetime, e.g.__date:filing_date) and point a field at it viadescribe_field(f) → FieldInfo(..., aligns_on="filing_date"). The engine then places that field's values by their known-date. A field with no coordinate is naive (placed at period-end). - A field vocabulary — contribute new fields (e.g.
mine.*) with atrail.schemaentry point resolving to a{column: kind}mapping; they then reference like any built-in field. - A coarse dimension — if your
entityaxis is a coarser key (e.g. country), setCapabilities(entity_dim="country", bridge_field="meta.country")and the engine remaps it onto entities through that bridge meta field.
Register in your package's pyproject.toml:
[project.entry-points."trail.sources"]
mine = "my_pkg.source:MySource"
[project.entry-points."trail.schema"] # optional — only if you add a mine.* vocabulary
mine = "my_pkg.schema:FIELDS" # a {column: kind} dict
trail.testing.assert_source_conforms(src, fields) checks your adapter against the panel
contract. The full contract (LoadRequest, Capabilities, FieldInfo, the __date:*
convention, coalescing) is in the spec §5 and §11.
Architecture
Parser (Lark LALR) → typed AST → macro expansion (def inlining) → kind-checked validation →
Polars compiler. The engine carries only irreducible primitives; the large derived
function library is written in Trail itself and shipped as trail/stdlib/*.trail (canonical
copy in the spec).
Development
uv sync
uv run pytest -q
uv run ruff check .
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file trail_lang-0.18.0.tar.gz.
File metadata
- Download URL: trail_lang-0.18.0.tar.gz
- Upload date:
- Size: 258.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8152ec2b5bd7239e9b3d777d332c654af6ba148503b547d9da9b39f6bd116fb2
|
|
| MD5 |
077515a37d16b41d227997646f682bb4
|
|
| BLAKE2b-256 |
205d2937063e0f91f6da87afee734e98f0a1fe6745660d4264bdf1d09089e4ae
|
Provenance
The following attestation bundles were made for trail_lang-0.18.0.tar.gz:
Publisher:
release.yml on trail-language/trail-lang
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trail_lang-0.18.0.tar.gz -
Subject digest:
8152ec2b5bd7239e9b3d777d332c654af6ba148503b547d9da9b39f6bd116fb2 - Sigstore transparency entry: 2393635715
- Sigstore integration time:
-
Permalink:
trail-language/trail-lang@c353548334de5b046045e63abc11448d9e17198f -
Branch / Tag:
refs/tags/v0.18.0 - Owner: https://github.com/trail-language
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c353548334de5b046045e63abc11448d9e17198f -
Trigger Event:
push
-
Statement type:
File details
Details for the file trail_lang-0.18.0-py3-none-any.whl.
File metadata
- Download URL: trail_lang-0.18.0-py3-none-any.whl
- Upload date:
- Size: 122.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
17606e44ed89b33fed3bce2cb8962ed77b2b699b26a041601af418f2ff39a0ef
|
|
| MD5 |
6e9ec78a00fa703709601ad1de5c8078
|
|
| BLAKE2b-256 |
9476789d5fc54cb4f8ed18af70dfa6258b8bdc20f1ccce7902d97c1b3315904b
|
Provenance
The following attestation bundles were made for trail_lang-0.18.0-py3-none-any.whl:
Publisher:
release.yml on trail-language/trail-lang
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trail_lang-0.18.0-py3-none-any.whl -
Subject digest:
17606e44ed89b33fed3bce2cb8962ed77b2b699b26a041601af418f2ff39a0ef - Sigstore transparency entry: 2393635828
- Sigstore integration time:
-
Permalink:
trail-language/trail-lang@c353548334de5b046045e63abc11448d9e17198f -
Branch / Tag:
refs/tags/v0.18.0 - Owner: https://github.com/trail-language
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c353548334de5b046045e63abc11448d9e17198f -
Trigger Event:
push
-
Statement type: