Skip to content

Data and feature examples

Strategy research starts with explicit identity, observation, availability, and alignment rules. These examples use synthetic results first and move provider calls into a separate section so the offline documentation check never requires credentials.

Create normalized synthetic bars

from persistra.data import synthetic

first = synthetic.bars("FIRST", periods=80, seed=1)
second = synthetic.bars("SECOND", periods=80, seed=2)

print(first.instrument)
print(first.frame.tail())
print(first.metadata)

A BarSet keeps instrument identity, normalized observations, and acquisition provenance separate. Synthetic results obey the same schemas as provider-backed results.

Build a fixed-universe price panel

from persistra.data import pivot_bars

prices = pivot_bars([first, second], field="close")
prices.columns = ["asset-a", "asset-b"]

assert prices.columns.tolist() == ["asset-a", "asset-b"]
print(prices.tail())

The original pivot columns are provider and instrument identity pairs. Rename them only at a deliberate research boundary. Missing observations remain missing.

Calculate returns without hidden filling

from persistra.analysis import simple_returns

returns = simple_returns(prices)

assert returns.index.equals(prices.index)
print(returns.tail())

Choose simple or log returns explicitly. Resampling, alignment, and missing-value choices belong before model fitting, not inside a regression or optimizer.

Align independent sources

from persistra.data import align

aligned = align(
    {"prices": prices, "returns": returns},
    how="intersection",
)

assert aligned["prices"].index.equals(aligned["returns"].index)

Use an inner join when every downstream row requires every input. Use an outer join when missing coverage is meaningful and consumers have an explicit missing-data policy.

Build future labels separately

from persistra.research import forward_returns

labels = forward_returns(prices, horizon=5)

print(labels.frame.tail())
print(labels.label_ends.tail())

The final rows remain missing because their future horizon does not exist. label_ends makes purging and embargo decisions inspectable.

Transform cross-sectional signals

from persistra.research import (
    clip_cross_section,
    rank_cross_section,
    standardize_cross_section,
)

raw_signal = returns.rolling(10, min_periods=10).mean()
clipped_signal = clip_cross_section(raw_signal, lower_quantile=0.05, upper_quantile=0.95)
standard_signal = standardize_cross_section(clipped_signal)
ranked_signal = rank_cross_section(raw_signal, percentile=True)

assert standard_signal.columns.equals(prices.columns)
assert ranked_signal.columns.equals(prices.columns)

Cross-sectional operations retain the complete ordered asset axis. A missing cell makes that asset unavailable for the date; it does not silently change the universe definition.

Save normalized source results

from pathlib import Path
from tempfile import TemporaryDirectory

from persistra.data import DuckDBStore

with TemporaryDirectory() as directory:
    database = Path(directory) / "strategy-inputs.duckdb"
    with DuckDBStore.create(database) as store:
        snapshot_id = store.save(first)
        restored = store.load_bars(first.instrument.instrument_id)

    assert restored is not None
    assert restored.frame.equals(first.frame)
    print(snapshot_id)

Acquisition never writes automatically. Save only reviewed normalized results, and keep the retrieval cutoff or snapshot identity with each research run.

Provider-backed acquisition

The remaining examples require credentials or previously populated raw caches and are not run by the documentation smoke check.

Acquire Alpha Vantage bars

from persistra.data import AlphaVantageClient
from persistra.model import InstrumentKind

client = AlphaVantageClient.from_env()
bars = client.securities.bars(
    "IBM",
    kind=InstrumentKind.EQUITY,
    interval="daily",
    outputsize="full",
)

print(bars.metadata.cache_status)
print(bars.frame.tail())

Set offline=True after the exact request is cached. Use refresh=True only when a run is meant to retrieve new provider bytes.

Acquire raw intraday execution inputs

intraday = client.securities.bars(
    "IBM",
    kind=InstrumentKind.EQUITY,
    interval="5min",
    adjusted=False,
    outputsize="full",
)

Trading Engine scenarios need raw unadjusted bars, an explicit clock policy, synchronized instruments, and corporate actions. A daily calendar label cannot supply an execution instant.

Retrieve the latest quote and top of book

quote = client.quotes.latest("IBM", kind=InstrumentKind.EQUITY)
book = client.quotes.top_of_book(["IBM"], kind=InstrumentKind.EQUITY)

print(quote.frame)
print(book.frame)

Entitlement and delay fields remain part of the normalized result. Do not treat delayed data as realtime because it arrived recently.

Retrieve a historical option chain

from datetime import date

chain = client.options.historical_chain("IBM", date=date(2025, 1, 17))
print(chain.frame[["contract_id", "strike", "expiration", "option_type"]].head())

Historical option examples support research and visualization. Persistra does not provide a realtime option execution model.

Retrieve current FRED observations

from persistra.data import FredClient

fred = FredClient.from_env()
current_gdp = fred.series.latest("GDPC1", observation_start="2020-01-01")

print(current_gdp.definition)
print(current_gdp.frame.tail())

Retrieve ALFRED revision history

gdp_history = fred.series.vintages(
    "GDPC1",
    realtime_start="2019-01-01",
    observation_start="2018-01-01",
)

Use the availability intervals to select the version known at each decision. Retrieval time is the time Persistra fetched the response, not the time the economic value became knowable.

Build point-in-time features

FeatureSpec binds a vintage source, selection policy, lag, and maximum staleness. Supply decision dates explicitly:

import pandas as pd

from persistra.research import FeatureSpec, build_feature_panel

decision_dates = pd.date_range("2024-01-31", periods=12, freq="ME")
feature_panel = build_feature_panel(
    [
        FeatureSpec(
            name="real-gdp",
            source=gdp_history,
            publication_lag=pd.Timedelta(days=1),
            maximum_staleness=pd.Timedelta(days=120),
        )
    ],
    decision_dates=decision_dates,
)

print(feature_panel.frame)
print(feature_panel.provenance)

An unavailable, stale, deleted, or explicitly missing latest observation stays missing. The builder does not fall back to an older nonmissing value.

Prepare reusable experiment inputs

Persist normalized source results, keep feature provenance, create forward labels independently, and identify external model or configuration artifacts in a research manifest. Then pass only aligned date-by-asset and date-by-factor panels to model code. This keeps acquisition and vintage choices reviewable after a strategy target has been produced.