Skip to content

Point-in-time research

Import public research functions and result types from persistra.research. Submodule sections group selection, cross-sectional transformation, evaluation, labeling, splitting, reproducibility, summarization, and their typed outputs.

Vintage selection and feature panels

persistra.research.features

Point-in-time selection and feature-panel construction.

ZERO_DAYS = pd.Timedelta(0) module-attribute

FEATURE_PROVENANCE_COLUMNS = ('decision_date', 'feature', 'series_id', 'provider', 'provider_series', 'period_label', 'observation_date', 'available_from', 'available_through', 'source_retrieved_at', 'matched_age', 'publication_lag', 'maximum_staleness', 'is_deleted', 'selected_value') module-attribute

VintagePolicy = Literal['final_vintage', 'first_release', 'real_time'] module-attribute

AnalysisError

Bases: PersistraError, ValueError

Raised when data violates a mathematical assumption.

VintageSeriesSet dataclass

One validated scalar-series revision history and its provenance.

FeaturePanel dataclass

Point-in-time feature values with row-level source-version provenance.

FeaturePolicy dataclass

Recorded source identity and policy for one constructed feature.

FeatureSpec dataclass

One named vintage source and its explicit availability policy.

VintageSelection dataclass

Source versions applicable on one explicit knowledge date.

calendar_date(value: date | datetime | str | pd.Timestamp, *, name: str) -> pd.Timestamp

Return one normalized timezone-naive calendar date.

calendar_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique calendar-date index.

require_whole_days(value: pd.Timedelta, *, name: str) -> None

Require a nonnegative duration expressed in whole calendar days.

select_vintage(source: VintageSeriesSet, *, known_on: date | datetime | str | pd.Timestamp, publication_lag: pd.Timedelta = ZERO_DAYS) -> VintageSelection

Select each observation version known on a calendar date.

publication_lag delays all source availability intervals. The selected source rows keep their original interval boundaries, while the returned result records the chosen lag.

project_vintage_history(source: VintageSeriesSet, policy: VintagePolicy) -> VintageSeriesSet

Project one retained revision history under an explicit content policy.

Real-time history keeps every provider interval. First-release history keeps the earliest retained version of each observation and ignores later revisions. Final-vintage history keeps the last retained version but makes it visible from the first recorded release date, deliberately exposing future revision content for bias measurement.

build_feature_panel(specs: Iterable[FeatureSpec], *, decision_dates: pd.DatetimeIndex) -> FeaturePanel

Build point-in-time features under explicit lag and staleness policies.

_latest_observation(eligible: pd.DataFrame, spec: FeatureSpec) -> pd.Series | None

_policy(spec: FeatureSpec) -> FeaturePolicy

_provenance_row(spec: FeatureSpec, decision_date: pd.Timestamp, row: pd.Series | None, age: pd.Timedelta | None, value: object) -> dict[str, Any]

Forward labels

persistra.research.labels

Forward research labels built separately from feature construction.

ForwardReturnLabels dataclass

Forward simple-return labels and the end date of every label horizon.

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

datetime_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique datetime index without changing its timezone.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

forward_returns(levels: pd.DataFrame, *, horizon: int) -> ForwardReturnLabels

Construct forward simple returns over an observation-count horizon.

Factor regression models

persistra.research.factor_models

Regression-based factor models with explicit panel alignment.

FactorCovarianceEstimator = Literal['sample', 'diagonal_shrinkage', 'constant_correlation', 'ledoit_wolf', 'ewma', 'supplied'] module-attribute

RegressionCovariance = Literal['classical', 'hc3', 'newey_west'] module-attribute

_INTERCEPT = 'intercept' module-attribute

_DIAGNOSTIC_COLUMNS = ['observations', 'rank', 'degrees_of_freedom', 'r_squared', 'adjusted_r_squared', 'condition_number', 'status'] module-attribute

CrossSectionalCovariance = Literal['classical', 'hc3']

_SurvivalFunction = Callable[[np.ndarray, float], np.ndarray]

AnalysisError

Bases: PersistraError, ValueError

Raised when data violates a mathematical assumption.

CrossSectionalFactorModelResult dataclass

Period factor-return estimates from time-varying supplied exposures.

FactorPremiaResult dataclass

Average factor premia with time-series inference.

FactorRegressionResult dataclass

Aligned estimates and diagnostics from static time-series regressions.

FactorRiskModel dataclass

Factor and idiosyncratic components of one asset covariance estimate.

manifest_parameters: Mapping[str, Any] property

Return portable covariance settings for a research manifest.

FamaMacBethResult dataclass

Cross-sectional factor returns and their time-series premia summary.

ForwardReturnLabels dataclass

Forward simple-return labels and the end date of every label horizon.

RollingFactorRegressionResult dataclass

Point-in-time coefficient histories from rolling or expanding regressions.

_Fit dataclass

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

aligned_panel(frame: pd.DataFrame, reference: pd.DataFrame, *, name: str) -> pd.DataFrame

Copy a panel whose date and asset axes exactly match a reference panel.

cross_sectional_frame(frame: pd.DataFrame, *, name: str, positive: bool = False) -> pd.DataFrame

Validate one numeric date-by-asset panel with a fixed explicit universe.

datetime_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique datetime index without changing its timezone.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

fit_time_series_factor_model(asset_returns: pd.DataFrame, factor_returns: pd.DataFrame, *, weights: pd.DataFrame | None = None, intercept: bool = True, covariance: RegressionCovariance = 'classical', hac_lags: int | None = None) -> FactorRegressionResult

Fit one factor-return regression for each asset.

The two return panels must use the same sorted date index. Missing observations are removed independently for each asset. weights enables weighted least squares and must use the asset-return axes. A rank-deficient design retains its least-norm coefficients but reports unavailable inference and an explicit diagnostic status.

rolling_time_series_factor_model(asset_returns: pd.DataFrame, factor_returns: pd.DataFrame, *, window: int | None, minimum_observations: int | None = None, weights: pd.DataFrame | None = None, intercept: bool = True, covariance: RegressionCovariance = 'classical', hac_lags: int | None = None) -> RollingFactorRegressionResult

Fit causal rolling or expanding asset factor regressions.

A positive window selects a rolling observation window. None selects an expanding window. Every estimate dated t uses observations no later than t. Rows before the required history remain present with an insufficient_observations status.

estimate_cross_sectional_factor_returns(asset_returns: pd.DataFrame | ForwardReturnLabels, exposures: pd.DataFrame, *, weights: pd.DataFrame | None = None, intercept: bool = True, covariance: CrossSectionalCovariance = 'hc3') -> CrossSectionalFactorModelResult

Estimate one cross-sectional factor-return regression per date.

exposures uses a unique, sorted (date, asset) MultiIndex and one column per caller-defined factor. Forward labels retain their horizon in the result. Missing returns, exposures, or nonpositive weights are removed for that date only.

summarize_factor_premia(factor_returns: pd.DataFrame, *, covariance: Literal['classical', 'newey_west'] = 'newey_west', hac_lags: int | None = None) -> FactorPremiaResult

Estimate average premia and classical or Newey-West inference.

fama_macbeth_regression(labels: ForwardReturnLabels, exposures: pd.DataFrame, *, weights: pd.DataFrame | None = None, intercept: bool = True, cross_sectional_covariance: CrossSectionalCovariance = 'hc3', hac_lags: int | None = None) -> FamaMacBethResult

Run cross-sectional regressions and summarize their factor premia.

build_factor_risk_model(exposures: pd.DataFrame, factor_returns: pd.DataFrame, residual_returns: pd.DataFrame, *, shrinkage: float = 0.0, covariance: FactorCovarianceEstimator | pd.DataFrame = 'diagonal_shrinkage', ewma_decay: float = 0.94, window: int | None = None, as_of: pd.Timestamp | None = None) -> FactorRiskModel

Build an asset covariance matrix with one explicit factor covariance policy.

_estimate_factor_covariance(sample: pd.DataFrame, *, covariance: FactorCovarianceEstimator | pd.DataFrame, shrinkage: float, ewma_decay: float, factors: pd.Index) -> tuple[np.ndarray, FactorCovarianceEstimator, dict[str, float | str], float]

_constant_correlation_target(covariance: np.ndarray) -> np.ndarray

_ledoit_wolf(values: np.ndarray) -> tuple[np.ndarray, float]

_supplied_covariance(covariance: pd.DataFrame, factors: pd.Index) -> np.ndarray

_factor_risk_as_of(sample_index: pd.DatetimeIndex, as_of: pd.Timestamp | None) -> pd.Timestamp

Validate and return the point-in-time boundary for an aligned sample.

_time_series_inputs(asset_returns: pd.DataFrame, factor_returns: pd.DataFrame, *, weights: pd.DataFrame | None) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame | None]

_factor_return_frame(frame: pd.DataFrame, *, name: str, allow_intercept: bool = False) -> pd.DataFrame

_factor_columns(columns: pd.Index, *, allow_intercept: bool = False) -> None

_plain_axis(index: pd.Index, *, name: str) -> None

_regression_weights(weights: pd.DataFrame | None, returns: pd.DataFrame) -> pd.DataFrame | None

_exposure_frame(exposures: pd.DataFrame, *, returns: pd.DataFrame) -> pd.DataFrame

_terms(factors: pd.Index, *, intercept: bool) -> pd.Index

_term_frame(index: pd.Index, terms: pd.Index) -> pd.DataFrame

_weight_values(weights: pd.DataFrame | None, position: int) -> np.ndarray | None

_design(factors: np.ndarray, *, intercept: bool) -> np.ndarray

_fit_regression(y: np.ndarray, x: np.ndarray, weights: np.ndarray | None, *, covariance: RegressionCovariance, hac_lags: int | None, minimum_observations: int | None = None) -> _Fit

_coefficient_covariance(x: np.ndarray, residuals: np.ndarray, *, covariance: RegressionCovariance, degrees_of_freedom: int, hac_lags: int | None) -> np.ndarray

_weighted_r_squared(y: np.ndarray, residuals: np.ndarray, sqrt_weight: np.ndarray) -> float

_condition_number(singular_values: np.ndarray) -> float

_diagnostics(fit: _Fit) -> dict[str, float | int | str]

_validate_covariance(covariance: str, *, hac_lags: int | None) -> None

_resolved_hac_lags(observations: int, hac_lags: int | None) -> int

_unit_interval(value: float, *, name: str) -> float

Factor portfolio forecasts and attribution

persistra.research.factor_portfolio

Point-in-time factor forecasts and portfolio attribution.

FactorCovarianceEstimator = Literal['sample', 'diagonal_shrinkage', 'constant_correlation', 'ledoit_wolf', 'ewma', 'supplied'] module-attribute

FactorPortfolioForecastStep = FactorPortfolioForecastSuccess | FactorPortfolioForecastUnavailable

AnalysisError

Bases: PersistraError, ValueError

Raised when data violates a mathematical assumption.

FactorPortfolioAttribution dataclass

Expected-return and variance attribution for supplied portfolio weights.

FactorPortfolioForecast dataclass

Point-in-time expected returns and covariance from caller-defined factors.

manifest_parameters: Mapping[str, Any] property

Return portable covariance settings for a research manifest.

FactorPortfolioForecastSuccess dataclass

One available dated factor portfolio forecast.

FactorPortfolioForecastUnavailable dataclass

One dated forecast that lacked the declared estimation history.

FactorRiskModel dataclass

Factor and idiosyncratic components of one asset covariance estimate.

manifest_parameters: Mapping[str, Any] property

Return portable covariance settings for a research manifest.

RollingFactorPortfolioForecastResult dataclass

Ordered successful and unavailable point-in-time factor forecasts.

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

datetime_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique datetime index without changing its timezone.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

build_factor_risk_model(exposures: pd.DataFrame, factor_returns: pd.DataFrame, residual_returns: pd.DataFrame, *, shrinkage: float = 0.0, covariance: FactorCovarianceEstimator | pd.DataFrame = 'diagonal_shrinkage', ewma_decay: float = 0.94, window: int | None = None, as_of: pd.Timestamp | None = None) -> FactorRiskModel

Build an asset covariance matrix with one explicit factor covariance policy.

build_factor_portfolio_forecast(risk_model: FactorRiskModel, factor_premia: pd.Series, *, alpha: pd.Series | None = None, as_of: pd.Timestamp | None = None) -> FactorPortfolioForecast

Combine supplied factor premia and a factor risk model without hidden scaling.

rolling_factor_portfolio_forecasts(exposures: pd.DataFrame, factor_returns: pd.DataFrame, residual_returns: pd.DataFrame, factor_premia: pd.DataFrame, *, alpha: pd.DataFrame | None = None, window: int | None = None, minimum_observations: int = 2, covariance: FactorCovarianceEstimator | pd.DataFrame = 'diagonal_shrinkage', shrinkage: float = 0.0, ewma_decay: float = 0.94) -> RollingFactorPortfolioForecastResult

Build causal rolling or expanding factor forecasts for every supplied date.

attribute_factor_portfolio(forecast: FactorPortfolioForecast, weights: pd.Series, *, benchmark_weights: pd.Series | None = None) -> FactorPortfolioAttribution

Attribute a portfolio, or its active weights, to factor and specific components.

_aligned_series(values: pd.Series, index: pd.Index, *, name: str) -> pd.Series

_rolling_exposure_frame(exposures: pd.DataFrame, *, dates: pd.DatetimeIndex, assets: pd.Index, factors: pd.Index) -> pd.DataFrame

_unavailable_forecast_reason(*, factor_count: int, residual_count: pd.Series, minimum: int, exposures: pd.DataFrame, premia: pd.Series, alpha: pd.Series | None) -> str | None

_declared_covariance_parameters(covariance: FactorCovarianceEstimator | pd.DataFrame, *, shrinkage: float, ewma_decay: float) -> dict[str, object]

_forecast_as_of(risk_as_of: pd.Timestamp | None, requested: pd.Timestamp | None) -> pd.Timestamp | None

Cross-sectional transforms

persistra.research.transforms

Cross-sectional transforms for explicit fixed-universe signal panels.

RankMethod = Literal['average', 'min', 'max', 'first', 'dense'] module-attribute

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

aligned_panel(frame: pd.DataFrame, reference: pd.DataFrame, *, name: str) -> pd.DataFrame

Copy a panel whose date and asset axes exactly match a reference panel.

cross_sectional_frame(frame: pd.DataFrame, *, name: str, positive: bool = False) -> pd.DataFrame

Validate one numeric date-by-asset panel with a fixed explicit universe.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

rank_cross_section(signals: pd.DataFrame, *, method: RankMethod = 'average', percentile: bool = True, ascending: bool = True) -> pd.DataFrame

Rank each date across the panel's explicit asset columns.

clip_cross_section(signals: pd.DataFrame, *, lower_quantile: float = 0.01, upper_quantile: float = 0.99) -> pd.DataFrame

Clip each date to explicit cross-sectional quantile bounds.

standardize_cross_section(signals: pd.DataFrame, *, ddof: int = 0) -> pd.DataFrame

Center and scale each date across available assets.

neutralize_cross_section(signals: pd.DataFrame, *, groups: pd.DataFrame | None = None, exposures: Mapping[str, pd.DataFrame] | None = None) -> pd.DataFrame

Return per-date residuals after group and numeric exposure controls.

The regression includes an intercept. A time-varying group panel contributes fixed effects, and each named exposure contributes one numeric regressor. Rows without enough complete observations to estimate the requested controls remain missing.

_neutralize_row(values: pd.Series, groups: pd.Series | None, exposures: Mapping[str, pd.Series]) -> pd.Series

Signal evaluation

persistra.research.evaluation

Cross-sectional signal evaluation with explicit label and sample semantics.

CorrectionMethod = Literal['bonferroni', 'benjamini-hochberg'] module-attribute

_STANDARD_NORMAL = NormalDist() module-attribute

SharpeSelectionDiagnostic = SharpeSelectionSuccess | SharpeSelectionUnavailable

AnalysisError

Bases: PersistraError, ValueError

Raised when data violates a mathematical assumption.

BenchmarkComparison dataclass

Candidate results compared with one aligned benchmark series.

ForwardReturnLabels dataclass

Forward simple-return labels and the end date of every label horizon.

GroupSignalResult dataclass

Signal and forward-return statistics for time-varying classifications.

InformationCoefficientResult dataclass

Pearson and rank information coefficients with pairwise sample counts.

MultipleTestingResult dataclass

Raw and adjusted p-values for one explicit repeated-search correction.

QuantilePortfolioResult dataclass

Quantile returns, formation weights, linear costs, and diagnostics.

SharpeSelectionSuccess dataclass

One available nonnormality-aware Sharpe selection diagnostic.

SharpeSelectionUnavailable dataclass

One Sharpe selection diagnostic that could not be estimated.

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

aligned_panel(frame: pd.DataFrame, reference: pd.DataFrame, *, name: str) -> pd.DataFrame

Copy a panel whose date and asset axes exactly match a reference panel.

cross_sectional_frame(frame: pd.DataFrame, *, name: str, positive: bool = False) -> pd.DataFrame

Validate one numeric date-by-asset panel with a fixed explicit universe.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

information_coefficients(signals: pd.DataFrame, labels: ForwardReturnLabels, *, groups: pd.DataFrame | None = None, minimum_count: int = 3) -> InformationCoefficientResult

Calculate per-date Pearson and rank ICs with pairwise sample counts.

quantile_portfolios(signals: pd.DataFrame, labels: ForwardReturnLabels, *, quantiles: int = 5, groups: pd.DataFrame | None = None, volumes: pd.DataFrame | None = None, weights: pd.DataFrame | None = None, costs: float | pd.Series | pd.DataFrame = 0.0) -> QuantilePortfolioResult

Form weighted signal quantiles and report gross, cost, and net results.

Quantiles are assigned within each date and, when supplied, within each classification. Signal ties remain together. A cross-section or group with fewer assets than requested quantiles is left unassigned instead of creating misleading sparse portfolios. Costs are decimal return charges per unit of absolute asset weight traded.

summarize_groups(signals: pd.DataFrame, labels: ForwardReturnLabels, groups: pd.DataFrame, *, minimum_count: int = 3) -> GroupSignalResult

Summarize signal levels, forward returns, and ICs by time-varying group.

compare_benchmark(candidates: pd.DataFrame, benchmark: pd.Series, *, benchmark_name: str = 'benchmark') -> BenchmarkComparison

Compare candidate return series with one aligned simple-return benchmark.

adjust_pvalues(pvalues: pd.Series, *, method: CorrectionMethod = 'benjamini-hochberg', alpha: float = 0.05) -> MultipleTestingResult

Adjust explicit hypothesis p-values for repeated searches.

probabilistic_sharpe_ratio(returns: pd.Series, *, periods_per_year: float, benchmark_sharpe: float, skewness: float, kurtosis: float) -> SharpeSelectionDiagnostic

Estimate the probability that Sharpe exceeds a caller-declared benchmark.

skewness and kurtosis are caller-supplied standardized population moments; kurtosis uses the Pearson convention where a normal distribution has value three.

deflated_sharpe_ratio(returns: pd.Series, *, periods_per_year: float, trial_count: int, trial_sharpe_standard_deviation: float, skewness: float, kurtosis: float) -> SharpeSelectionDiagnostic

Estimate Sharpe significance after an explicit repeated strategy search.

trial_sharpe_standard_deviation is the dispersion of annualized Sharpe ratios across the declared trials. The expected maximum independent-trial Sharpe becomes the benchmark.

_sharpe_selection_policy(*, periods_per_year: float, benchmark_sharpe: float, skewness: float, kurtosis: float) -> tuple[float, float, float, float]

_sharpe_selection_diagnostic(returns: pd.Series, *, method: Literal['probabilistic_sharpe', 'deflated_sharpe'], periods_per_year: float, trial_count: int, benchmark_sharpe: float, skewness: float, kurtosis: float, trial_sharpe_standard_deviation: float | None) -> SharpeSelectionDiagnostic

_unavailable_sharpe_selection(*, reason: str, method: Literal['probabilistic_sharpe', 'deflated_sharpe'], sample_count: int, periods_per_year: float, trial_count: int, benchmark_sharpe: float, skewness: float, kurtosis: float, trial_sharpe_standard_deviation: float | None) -> SharpeSelectionUnavailable

_finite_scalar(value: object, *, name: str, minimum: float | None = None) -> float

_evaluation_inputs(signals: pd.DataFrame, labels: ForwardReturnLabels, *, groups: pd.DataFrame | None) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame | None]

_correlations(signals: pd.Series, returns: pd.Series, minimum_count: int) -> tuple[int, float, float]

_information_coefficient_statistics(rows: list[dict[str, Any]], date_index: pd.DatetimeIndex) -> pd.DataFrame

_grouped_statistics(rows: list[dict[str, Any]], columns: list[str], *, date_index: pd.DatetimeIndex) -> pd.DataFrame

_quantile_assignments(signals: pd.DataFrame, quantiles: int, groups: pd.DataFrame | None) -> pd.DataFrame

_assign_row(signals: pd.Series, quantiles: int) -> pd.Series

_linear_cost_panel(costs: float | pd.Series | pd.DataFrame, reference: pd.DataFrame) -> pd.DataFrame

_normalize_quantile_weights(membership: pd.DataFrame, raw_weights: pd.DataFrame, groups: pd.DataFrame | None) -> pd.DataFrame

_normalize_weight_sleeve(weights: pd.Series) -> pd.Series

_weight_diagnostic_row(date: object, quantile: int, weights: pd.Series, assigned_count: int, effective_membership: int) -> dict[str, Any]

_quantile_weight_diagnostics(rows: list[dict[str, Any]], date_index: pd.DatetimeIndex) -> pd.DataFrame

_capacity_row(date: object, quantile: int, volumes: pd.Series | None) -> dict[str, Any]

_quantile_capacity(rows: list[dict[str, Any]], date_index: pd.DatetimeIndex) -> pd.DataFrame

_quantile_summary(returns: pd.DataFrame, costs: pd.DataFrame, net_returns: pd.DataFrame, counts: pd.DataFrame, turnover: pd.DataFrame, capacity: pd.DataFrame, spread: pd.Series, spread_costs: pd.Series, net_spread: pd.Series, *, horizon: int) -> pd.DataFrame

_cumulative_return(returns: pd.Series, horizon: int) -> float

Temporal splits

persistra.research.splits

Ordered expanding and rolling temporal research splits.

AnalysisError

Bases: PersistraError, ValueError

Raised when data violates a mathematical assumption.

ForwardReturnLabels dataclass

Forward simple-return labels and the end date of every label horizon.

NestedTemporalSplit dataclass

One outer evaluation split with ordered inner model-selection splits.

TemporalSplit dataclass

One ordered split with separately recorded purged and embargoed observations.

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

expanding_window_splits(labels: ForwardReturnLabels, *, initial_train_size: int, evaluation_size: int, step: int | None = None, embargo: int = 0) -> tuple[TemporalSplit, ...]

Generate expanding splits with label purging and an observation-count embargo.

rolling_window_splits(labels: ForwardReturnLabels, *, train_size: int, evaluation_size: int, step: int | None = None, embargo: int = 0) -> tuple[TemporalSplit, ...]

Generate rolling splits with label purging and an observation-count embargo.

nested_expanding_window_splits(labels: ForwardReturnLabels, *, outer_initial_train_size: int, outer_evaluation_size: int, inner_initial_train_size: int, inner_evaluation_size: int, outer_step: int | None = None, inner_step: int | None = None, outer_embargo: int = 0, inner_embargo: int = 0) -> tuple[NestedTemporalSplit, ...]

Generate expanding outer and inner splits for unbiased model selection.

nested_rolling_window_splits(labels: ForwardReturnLabels, *, outer_train_size: int, outer_evaluation_size: int, inner_train_size: int, inner_evaluation_size: int, outer_step: int | None = None, inner_step: int | None = None, outer_embargo: int = 0, inner_embargo: int = 0) -> tuple[NestedTemporalSplit, ...]

Generate rolling outer and inner splits for unbiased model selection.

validate_nested_temporal_split(split: NestedTemporalSplit, labels: ForwardReturnLabels) -> None

Reject inner leakage outside the outer training observations.

_nested_splits(labels: ForwardReturnLabels, *, outer_train_size: int, outer_evaluation_size: int, inner_train_size: int, inner_evaluation_size: int, outer_step: int | None, inner_step: int | None, outer_embargo: int, inner_embargo: int, outer_expanding: bool, inner_expanding: bool) -> tuple[NestedTemporalSplit, ...]

validate_temporal_split(split: TemporalSplit, labels: ForwardReturnLabels) -> None

Reject observation or label-horizon leakage into an evaluation period.

_generate_splits(labels: ForwardReturnLabels, *, train_size: int, evaluation_size: int, step: int | None, embargo: int, expanding: bool, index: pd.DatetimeIndex | None = None) -> Iterator[TemporalSplit]

_validate_sizes(train_size: int, evaluation_size: int, step: int | None, embargo: int) -> tuple[int, int, int | None, int]

Regime summaries

persistra.research.summary

Coverage and regime-conditioned research summaries.

ResearchSummary dataclass

Coverage and regime-conditioned return statistics.

coverage_summary(frame: pd.DataFrame) -> pd.DataFrame

Summarize observed and missing labels for each column.

datetime_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique datetime index without changing its timezone.

numeric_frame(frame: pd.DataFrame, *, positive: bool = False) -> pd.DataFrame

Copy a finite numeric frame and optionally require positive observations.

summarize_regimes(returns: pd.DataFrame, regimes: pd.Series, *, periods_per_year: float | None = None) -> ResearchSummary

Summarize coverage and returns within explicit regimes.

Volatility is the sample standard deviation and is annualized only when periods_per_year is supplied. Drawdown resets at regime changes and missing returns so separate regime episodes are never compounded together.

_regime_drawdown(values: pd.Series, regime_mask: pd.Series) -> float

Research manifests

persistra.research.manifest

Portable research manifests without a managed experiment database.

EnvironmentExtra = Literal['viz', 'inspect'] module-attribute

_REQUIREMENT_NAME = re.compile('^\\s*([A-Za-z0-9][A-Za-z0-9_.-]*)') module-attribute

_EXTRA_MARKER = re.compile('\\bextra\\s*==\\s*[\'\\"]([^\'\\"]+)[\'\\"]') module-attribute

ArtifactIdentity dataclass

Portable identity for one external research output artifact.

DatasetScope dataclass

One dataset scope with deeply immutable portable JSON values and identity.

ResearchManifest dataclass

Immutable record of research data, parameters, environment, and outputs.

atomic_write_bytes(path: Path, document: bytes, *, overwrite: bool = False) -> None

Publish complete bytes from a private same-directory file.

freeze_portable_mapping(value: Mapping[str, Any], *, name: str, redact_api_keys: bool = False) -> Mapping[str, Any]

Copy and recursively freeze one portable JSON mapping.

thaw_portable_mapping(value: Mapping[str, Any]) -> dict[str, Any]

Return a mutable JSON-compatible copy of one frozen mapping.

research_manifest_schema(version: int = 1) -> Mapping[str, Any]

Load an immutable copy of the supported research-manifest JSON Schema.

environment_distributions(*, extras: Sequence[EnvironmentExtra] = ()) -> tuple[str, ...]

Return declared direct distributions for the base package and selected extras.

environment_versions(distributions: Sequence[str] | None = None, *, extras: Sequence[EnvironmentExtra] = ()) -> dict[str, str]

Return installed versions for direct dependencies or an explicit custom set.

runtime_environment(overrides: Mapping[str, str] | None = None) -> dict[str, str]

Return stable Python and platform facts with explicit caller overrides.

identify_artifact(path: str | Path, *, name: str | None = None) -> ArtifactIdentity

Calculate the SHA-256 identity and byte size of one output artifact.

create_research_manifest(datasets: Sequence[DatasetScope], *, feature_parameters: Mapping[str, Any], label_parameters: Mapping[str, Any], split_parameters: Mapping[str, Any], benchmark_parameters: Mapping[str, Any], model_parameters: Mapping[str, Any] | None = None, manifest_version: Literal[1] = 1, random_seeds: Mapping[str, int] | None = None, execution_status: Literal['not-run', 'succeeded', 'failed'] = 'not-run', artifacts: Sequence[ArtifactIdentity] = (), environment: Mapping[str, str] | None = None, include_runtime: bool = True, runtime_overrides: Mapping[str, str] | None = None) -> ResearchManifest

Build a versioned manifest after validating that its values are portable JSON.

manifest_to_json(manifest: ResearchManifest, *, indent: int | None = 2) -> str

Serialize a manifest as stable portable JSON with a trailing newline.

manifest_from_json(document: str) -> ResearchManifest

Parse and validate a versioned research manifest JSON document.

write_research_manifest(manifest: ResearchManifest, path: str | Path, *, indent: int | None = 2, overwrite: bool = False) -> None

Atomically write a UTF-8 research manifest without replacing by default.

read_research_manifest(path: str | Path) -> ResearchManifest

Read a UTF-8 research manifest and validate its complete schema.

_manifest_dictionary(manifest: ResearchManifest) -> dict[str, object]

_mapping(value: object, *, name: str) -> dict[str, Any]

_string_mapping(value: object, *, name: str) -> dict[str, str]

_dataset_from_mapping(value: object) -> DatasetScope

_artifact_from_mapping(value: object) -> ArtifactIdentity

persistra.research.verification

Filesystem verification for research-manifest artifacts.

DataValidationError

Bases: PersistraError, ValueError

Raised when normalized data violates its contract.

ResearchManifest dataclass

Immutable record of research data, parameters, environment, and outputs.

ArtifactVerificationFinding dataclass

One structured artifact-integrity finding.

ArtifactVerification dataclass

Inspection-friendly result of verifying every declared artifact.

is_valid: bool property

Return whether all declared artifacts match and no extras exist.

raise_for_errors() -> None

Raise one normalized error when verification found any problem.

verify_manifest_artifacts(manifest: ResearchManifest, root: str | Path, *, report_unexpected: bool = True) -> ArtifactVerification

Verify declared files under an explicit trusted directory without following symlinks.

_file_identity(path: Path) -> tuple[str, int]

_unexpected_findings(root: Path, declared_paths: set[Path]) -> list[ArtifactVerificationFinding]

_finding(code: str, name: str, path: Path, message: str) -> ArtifactVerificationFinding

Time-varying universes

persistra.research.universe

Point-in-time investable-universe membership and panel alignment.

UNIVERSE_COLUMNS = ('asset_id', 'valid_from', 'valid_through', 'state', 'source', 'source_as_of', 'retrieved_at') module-attribute

DatasetScope dataclass

One dataset scope with deeply immutable portable JSON values and identity.

InclusionState

Bases: StrEnum

Declared membership state during one effective interval.

MissingMembershipPolicy

Bases: StrEnum

Behavior when no interval covers an asset and date.

DelistingPolicy

Bases: StrEnum

Behavior when a covering interval declares an asset delisted.

UniverseMembership dataclass

Immutable dated membership intervals with source provenance.

content_identity: str property

Return a stable SHA-256 identity for normalized membership and provenance.

dataset_scope(*, name: str = 'investable_universe') -> DatasetScope

Return the manifest dataset identity for this exact universe history.

calendar_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique calendar-date index.

align_universe(universe: UniverseMembership, dates: pd.Index, assets: Sequence[str] | pd.Index, *, missing: MissingMembershipPolicy = MissingMembershipPolicy.ERROR, delistings: DelistingPolicy = DelistingPolicy.EXCLUDE) -> pd.DataFrame

Return an exact date-by-asset membership mask without forward filling.

apply_universe(panel: pd.DataFrame, universe: UniverseMembership, *, missing: MissingMembershipPolicy = MissingMembershipPolicy.ERROR, delistings: DelistingPolicy = DelistingPolicy.EXCLUDE) -> pd.DataFrame

Mask nonmembers in a feature, label, evaluation, or portfolio input panel.

_isoformat(value: Any) -> str

_optional_isoformat(value: Any) -> str | None

Research policies and results

persistra.research.model

Typed outputs and policies for point-in-time research.

ZERO_DAYS = pd.Timedelta(0) module-attribute

VintagePolicy = Literal['final_vintage', 'first_release', 'real_time'] module-attribute

RegressionCovariance = Literal['classical', 'hc3', 'newey_west'] module-attribute

FactorCovarianceEstimator = Literal['sample', 'diagonal_shrinkage', 'constant_correlation', 'ledoit_wolf', 'ewma', 'supplied'] module-attribute

FEATURE_PROVENANCE_COLUMNS = ('decision_date', 'feature', 'series_id', 'provider', 'provider_series', 'period_label', 'observation_date', 'available_from', 'available_through', 'source_retrieved_at', 'matched_age', 'publication_lag', 'maximum_staleness', 'is_deleted', 'selected_value') module-attribute

SharpeSelectionMethod = Literal['probabilistic_sharpe', 'deflated_sharpe'] module-attribute

FactorPortfolioForecastStep = FactorPortfolioForecastSuccess | FactorPortfolioForecastUnavailable

SharpeSelectionDiagnostic = SharpeSelectionSuccess | SharpeSelectionUnavailable

VintageSeriesSet dataclass

One validated scalar-series revision history and its provenance.

FeatureSpec dataclass

One named vintage source and its explicit availability policy.

FeaturePolicy dataclass

Recorded source identity and policy for one constructed feature.

VintageSelection dataclass

Source versions applicable on one explicit knowledge date.

FeaturePanel dataclass

Point-in-time feature values with row-level source-version provenance.

ForwardReturnLabels dataclass

Forward simple-return labels and the end date of every label horizon.

FactorRegressionResult dataclass

Aligned estimates and diagnostics from static time-series regressions.

RollingFactorRegressionResult dataclass

Point-in-time coefficient histories from rolling or expanding regressions.

CrossSectionalFactorModelResult dataclass

Period factor-return estimates from time-varying supplied exposures.

FactorPremiaResult dataclass

Average factor premia with time-series inference.

FamaMacBethResult dataclass

Cross-sectional factor returns and their time-series premia summary.

FactorRiskModel dataclass

Factor and idiosyncratic components of one asset covariance estimate.

manifest_parameters: Mapping[str, Any] property

Return portable covariance settings for a research manifest.

FactorPortfolioForecast dataclass

Point-in-time expected returns and covariance from caller-defined factors.

manifest_parameters: Mapping[str, Any] property

Return portable covariance settings for a research manifest.

FactorPortfolioForecastSuccess dataclass

One available dated factor portfolio forecast.

FactorPortfolioForecastUnavailable dataclass

One dated forecast that lacked the declared estimation history.

RollingFactorPortfolioForecastResult dataclass

Ordered successful and unavailable point-in-time factor forecasts.

FactorPortfolioAttribution dataclass

Expected-return and variance attribution for supplied portfolio weights.

TemporalSplit dataclass

One ordered split with separately recorded purged and embargoed observations.

NestedTemporalSplit dataclass

One outer evaluation split with ordered inner model-selection splits.

ResearchSummary dataclass

Coverage and regime-conditioned return statistics.

InformationCoefficientResult dataclass

Pearson and rank information coefficients with pairwise sample counts.

QuantilePortfolioResult dataclass

Quantile returns, formation weights, linear costs, and diagnostics.

GroupSignalResult dataclass

Signal and forward-return statistics for time-varying classifications.

BenchmarkComparison dataclass

Candidate results compared with one aligned benchmark series.

MultipleTestingResult dataclass

Raw and adjusted p-values for one explicit repeated-search correction.

SharpeSelectionSuccess dataclass

One available nonnormality-aware Sharpe selection diagnostic.

SharpeSelectionUnavailable dataclass

One Sharpe selection diagnostic that could not be estimated.

DatasetScope dataclass

One dataset scope with deeply immutable portable JSON values and identity.

ArtifactIdentity dataclass

Portable identity for one external research output artifact.

ResearchManifest dataclass

Immutable record of research data, parameters, environment, and outputs.

freeze_portable_mapping(value: Mapping[str, Any], *, name: str, redact_api_keys: bool = False) -> Mapping[str, Any]

Copy and recursively freeze one portable JSON mapping.

require_integer(value: object, *, name: str, minimum: int | None = None) -> int

Return a normalized integer after enforcing an optional inclusive minimum.

calendar_date(value: date | datetime | str | pd.Timestamp, *, name: str) -> pd.Timestamp

Return one normalized timezone-naive calendar date.

calendar_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique calendar-date index.

datetime_index(index: pd.Index, *, name: str) -> pd.DatetimeIndex

Validate a sorted unique datetime index without changing its timezone.

require_whole_days(value: pd.Timedelta, *, name: str) -> None

Require a nonnegative duration expressed in whole calendar days.

_copy_regression_frames(result: object) -> None

_validate_sharpe_selection_policy(result: SharpeSelectionDiagnostic) -> None

_portable_mapping(value: Mapping[str, Any], *, name: str) -> Mapping[str, Any]