Metadata-Version: 2.5
Name: nlalpha-backtest
Version: 0.8.0
Summary: Backtest execution MCP tool for nlalpha
Project-URL: Homepage, https://github.com/ruimarj/nlalpha-backtest
Project-URL: Repository, https://github.com/ruimarj/nlalpha-backtest
Project-URL: Issues, https://github.com/ruimarj/nlalpha-backtest/issues
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Requires-Python: <3.13,>=3.12
Requires-Dist: mcp<2.0,>=1.0
Requires-Dist: nlalpha-core[json-log,redis,telemetry]>=0.7.0
Requires-Dist: nlalpha-data>=0.4.0
Requires-Dist: nlalpha-strategy>=0.2.0
Requires-Dist: numpy<3,>=2
Requires-Dist: pandas<3,>=2.3
Requires-Dist: pyarrow<27,>=25
Requires-Dist: pydantic<3,>=2.11
Requires-Dist: starlette>=0.37
Requires-Dist: uvicorn>=0.30
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# nlalpha-backtest

Nautilus sets proportional fees per instrument: fill `maker_fee` and/or `taker_fee` in `contract.instruments[<symbol>]`. Both default to `0.0` and must satisfy `0 <= fee < 1`; `0.001` means `0.1%`. Native bar execution charges `taker_fee`, and the result reports total commission. This is not the same configuration contract as Vector's `execution.fee_rate`. Unknown-key validation messages list the allowed fields for the current configuration scope and point out unsupported keys first.

`nlalpha-backtest` is an independent MCP server for strategy validation and backtesting; the package version is `0.8.0`. The core has no host sandbox, profile, object-storage, or platform-address knowledge.

## Runtime architecture

```text
MCP tools -> BacktestJobService -> BacktestRunner -> ExecutionBackend
                                                └-> LocalExecutor (default)
```

`ExecutionBackend` owns four semantic operations: execute, observe, cancel, and shutdown. `BacktestRunner` never branches on a concrete backend type.

In addition to the event-driven `run` path, the Python API exposes `run_batch` for providers that declare support for a versioned batch input schema. The batch dispatcher resolves an engine against both `runtime_mode` and `input_schema`; an explicit engine that does not support the request fails before provider execution. The vector MCP batch path generates strategy signals and simulates them inside one asynchronous backend worker job using the raw Parquet `data_ref`.

The default `LocalExecutor` runs every accepted operation in a bounded, terminable child process. Once spawned, the process, pipes, and executor handle remain under cleanup ownership through progress callbacks and execution; callback failures, execution errors, timeout, and cancellation all clean up the child process before releasing its concurrency slot. Timeout and cancellation terminate the process instead of merely abandoning an `asyncio` wait. SQLite is persistent by default at `./nlalpha-backtest-jobs.db`; after restart, non-terminal local jobs become explicit `failed/interrupted` records because their worker processes no longer exist.

## Supported execution combinations

The job store and execution backend form one support decision. Use SQLite with `LocalExecutor` for a single process and local persistence. Use Redis with a durable remote sandbox adapter for multi-replica execution, where the backend can authoritatively observe and cancel remote work. SQLite with Sandbox and Redis with `LocalExecutor` are unsupported combinations: local process ownership cannot establish that another replica's worker has stopped, so shared job records alone do not provide safe high availability.

Each record separates `user_id` from `owner_id`: the former scopes user reads and cancellation, while the latter fences writes to the execution lease. Admission creates a `queued` record; backend acceptance is recorded separately, and `running` begins when execution is observed. `cancelling` records cancellation intent; `completed`, `failed`, `timeout`, and `cancelled` are terminal outcomes confirmed through backend observation. Recovery observes and reconciles existing work; expired or lost remote work is not automatically replayed.

The data handoff uses core `DataArtifactStore` on both sides. `nlalpha-data` publishes immutable bytes and returns a `data_ref`; this service resolves the reference within the request's environment and owner scope, then validates its declared media type and schema before decoding it. A reference is not authorization by itself: the host identity mapping supplies the scope. Event and batch tools preserve the public `data_ref`/`data_refs` request shape.

The public `nlalpha_backtest.execution.run_strategy_code()` API is a synchronous, event-driven compile-and-run entry point that executes in the caller's current process and returns only a clean successful result. A caller that invokes it directly must provide its own process isolation, timeout, and cancellation handling. `BacktestRunner` continues to delegate the hosted workflow to an `ExecutionBackend`; its default `LocalExecutor` provides the bounded child-process isolation and real timeout/cancellation behavior described above.

Hosting outside this package is supplied by a separate adapter package. Installing this package alone does not install `aiohttp`, `aiobotocore`, or any host integration code.

## Modules

| Module | Responsibility |
| --- | --- |
| `server` | FastMCP tools, provider job lifecycle, result projection, application factory |
| `execution.backend` | `BacktestRequest`, backend protocol, canonical status/cancel/error types |
| `execution.local_executor` | Bounded process execution with real cancel and timeout |
| `execution.runner` | Domain validation and backend delegation |
| `jobs` | SQLite/Redis status and result persistence |
| `artifacts` | Scoped immutable data reference resolution via core `DataArtifactStore` |
| `engine` | Entry-point based engine discovery |

## Batch MCP schema contract

Both `run_batch_backtest` and `validate_batch_strategy` publish the shared `VectorBatchStrategySpec` as `strategy_spec`. Its JSON Schema requires `family` as a string with enum `vector`; `additionalProperties` remains allowed for the evolving strategy contract, and the server does not synthesize a missing family. The same Pydantic field definition validates the request at runtime before provider/job/artifact work. Callers must therefore submit the complete strategy spec, including `"family":"vector"`; prose in a tool description cannot replace a required schema property.

The structured schema and runtime validation are covered by the real FastMCP publication tests and batch tests. The package is released as 0.8.0.

## MCP tools

| Tool | Result |
| --- | --- |
| `run_backtest` | Accepts an event strategy with one `data_ref` or a multi-symbol `data_refs` map, returns `job_id` |
| `get_backtest_status` | Returns provider job status; a hosted backend may recover truth after restart |
| `get_backtest_result` | Reads hot cache, persistent result, then provider status |
| `cancel_backtest` | Cancels queued work or propagates cancellation to the accepted backend operation |
| `validate_strategy` | Runs a mock-data validation |
| `run_batch_backtest` | Submits a vector batch backtest using raw Parquet `data_ref` for one symbol or `data_refs` for multiple symbols |
| `validate_batch_strategy` | Validates a vector strategy through the backend worker mock-data path |
| `list_engines` | Lists discovered engines |
| `confess` | Returns an engine's capabilities and limitations |

The existing `run_backtest` supports event-driven single- and multi-symbol references. Vector batch tools require `family="vector"`, `execution.engine="vector"`, and `execution.runtime_mode="batch_signals"`; the runner resolves engine compatibility against both runtime mode and input schema. A single-symbol request supplies one `data_ref`; a multi-symbol request supplies `data_refs`, a map whose keys exactly match the ordered unique `universe.symbols` and whose values are distinct caller-owned references. The server loads each reference through `DataArtifactStore`, combines Arrow rows without rewriting symbol identities, and lets the worker validate and normalize the canonical panel. Batch requests sent through event-only tools are rejected before job submission, artifact loading, or mock-data validation. Existing `list_engines` fields remain available, with `runtime_modes` and `input_schemas` describing provider compatibility. Batch tools are callable only after their server image and tool metadata are published and the live catalog exposes them.

## Engine selection

For hosted strategy requests, `strategy_spec.execution.engine` takes precedence over the top-level `strategy_spec.engine`. On the Python `run` and `run_batch` APIs, an explicit `engine=` argument takes precedence over the `execution_config.engine` host hint. The dispatcher then resolves the provider against both runtime mode and batch input schema. An explicit incompatible engine fails before execution and is never silently replaced.

## Validation operation identity

`BacktestRunner.run_validation` keeps operation identity independent from any host platform. The caller may provide an explicit `operation_id`; it must be a string containing non-whitespace characters, is used as supplied, and is never silently replaced. A non-string value or a blank/whitespace-only string fails immediately.

When `operation_id` is omitted, the runner content-addresses the complete validation request. It builds this semantic payload:

```json
{
  "strategy_spec": "<complete strategy specification>",
  "data": "<complete validation data>",
  "validation": true
}
```

Before hashing, the payload is strictly normalized by `execution/runner.py`: dataclasses become dictionaries, dates and times use ISO-8601 strings, tuples become arrays, object keys must be strings, JSON keys are sorted, separators are compact, Unicode is kept as UTF-8, and non-finite numbers are rejected. The UTF-8 SHA-256 digest becomes `validation:<hex-digest>`.

Therefore, identical complete validation requests receive the same default operation identity, while any semantic change to the strategy specification or validation data receives a different identity. This rule is a property of the independent backtest package; it does not import or encode platform concepts. It prevents a mutable `strategy_spec.id` from being reused as the sole identity of semantically different validation requests while preserving stable retry behavior for the same request.

`BacktestRunner.run` has a different contract for a formal backtest: the caller must provide a non-blank `operation_id`. Missing, non-string, or blank/whitespace-only values fail immediately; the runner no longer falls back to `strategy_spec.id` and does not content-hash the potentially large market-data request by default. The MCP `BacktestJobService` creates the provider `job_id` and the hosted execution path passes that value as `operation_id`, so the existing hosted flow is unchanged.

`ExecutionOutcome` carries the domain result separately from backend evidence references. Successful and failed operations can both preserve those fields, and a host may archive them without teaching this package about its evidence service.

`server.create_app()` is the hosting boundary. It accepts the execution backend and DNS-rebinding host allow-list before constructing the SSE application; importing the module does not construct an ASGI app. The standalone command uses the local backend and localhost allow-list, while hosting adapters must pass their finalized runtime settings explicitly.

## Result integrity and strategy contract

The shared core `BacktestResult` accepts only `status == "succeeded"` or `status == "failed"`; unknown values, including `"completed"`, are rejected at construction. Its `is_successful` property is true only when the status is `succeeded` and the `errors` list is empty. `BacktestRunner` requires that clean-success predicate for backtest and validation results and raises a domain failure otherwise.

The event `validate_strategy` and vector `validate_batch_strategy` MCP tools return failures as exactly `{"status": "failed", "error": <canonical error object>}`; there is no top-level `details` or duplicate error metadata outside the canonical object. The canonical error object orders its fields by value priority: `type` (one of `invalid_request`, `strategy_error`, `data_error`, `execution_error`, `infrastructure_error`, or `internal_error`), `message` (≤2 KiB), optional `stage`, `retryable`, optional `suggestion`, optional `traceback_tail` (last 15 user/strategy frames, ≤4 KiB), and optional `details` (≤8 KiB, with lists trimmed to 10 entries × 512 B plus `*_count`/`errors_total` markers). The independent `LocalExecutor` carries the child worker's canonical error dict on the `ExecutionFailed.canonical_error` attribute; the former duplicated worker error record is gone. Success responses are bounded to 256 KiB with head-priority field ordering and summarized sections, preserving diagnostic context without changing success criteria.

On validation failure, `confess` guides the Agent using the available structured diagnostics and the fields needed for its current assessment; complete logs are not required by default. Missing or truncated content is unknown. If more evidence is needed, obtain it only through diagnostic or artifact-reading capabilities explicitly provided by the current tool contract; a reference alone does not guarantee read access. If required evidence is unavailable, report the gap and limit the conclusion accordingly.

An engine result and a host job are different objects with different status vocabularies. The job store can mark a clean result's job `completed`; the progress callback can also emit the text `completed`. Neither is a `BacktestResult.status`. Hosted status mapping belongs to the adapter that owns the host.

The core backtest contracts preserve the run context, inputs, results, and public strategy contracts. Execution timing, fees, account validation, and assumptions are provided by the actual engine; the host preserves them as supplied.

## Configuration

| Variable | Default | Meaning |
| --- | --- | --- |
| `JOB_STORE_DSN` | `sqlite:///./nlalpha-backtest-jobs.db` | Job state and results; SQLite supports local single-process execution, `redis://` selects Redis |
| `JOB_STORE_PREFIX` | `nlalpha:jobs:v1` | Redis key prefix |
| `NLALPHA_MAX_CONCURRENT_BACKTESTS` | `min(4, CPU count)` | Local execution concurrency limit |
| `NLALPHA_ARTIFACT_BACKEND` | `redis` | Artifact backend: `redis` or `s3` |
| `NLALPHA_ARTIFACT_REDIS_URL` | `redis://127.0.0.1:6379/0` | Redis artifact connection |
| `NLALPHA_ARTIFACT_TTL_SECONDS` | `86400` | Per-artifact logical lifetime |
| `NLALPHA_ARTIFACT_MAX_BYTES` | unset | Optional artifact size limit; there is no default 10 MiB cap |
| `NLALPHA_ARTIFACT_S3_BUCKET` | unset | Required bucket when S3 is selected |
| `NLALPHA_ARTIFACT_S3_ENDPOINT` | unset | Optional S3-compatible endpoint, such as MinIO |
| `NLALPHA_ARTIFACT_S3_ACCESS_KEY` / `NLALPHA_ARTIFACT_S3_SECRET_KEY` | unset | S3 credentials when required by the endpoint |
| `NLALPHA_ARTIFACT_S3_REGION` | unset | Optional S3 region |
| `NLALPHA_ARTIFACT_S3_RETENTION_DAYS` | `1` | Required object lifecycle retention in days |
| `NLALPHA_IDENTITY_MODE` | `single-user` | `single-user` rejects identity headers; `trusted-host` reads the configured host identity header |
| `NLALPHA_IDENTITY_USER_HEADER` | `X-NLAlpha-User-ID` | Trusted host identity header |
| `NLALPHA_ENV` | `local` | Environment component of artifact scope |
| `NLALPHA_ALLOWED_HOSTS` | localhost/loopback | DNS-rebinding host allow-list |

`BacktestService` and `create_app()` default to `max_active=100`, a 30-second owner lease, and a 5-second heartbeat interval. These are application-factory parameters, not environment variables.

The app factory owns MCP, runner, job service, stores, and their connections through its lifespan. `/health` reports process liveness. `/ready` checks application readiness, artifact backend health (`PING` for Redis or `HeadBucket` for S3), and job-store availability; a failed dependency probe returns 503.

Artifact storage is independent of job storage: selecting S3 for artifacts does not change the job store, and Redis artifacts do not imply Redis job state. MinIO is an optional S3-compatible backend. See the `nlalpha-data` package for publisher metadata and artifact lifecycle details.

Resolved artifact payloads are read into the service process. Set `NLALPHA_ARTIFACT_MAX_BYTES` to fit the deployment's per-artifact memory and storage budget; when unset, there is no package-level payload cap and the selected backend's deployment budget applies. `max_active` bounds concurrent jobs at the application service layer.

## Installation

```bash
pip install nlalpha-backtest
nlalpha-backtest
```

For Redis:

```bash
pip install "nlalpha-backtest[redis]"
JOB_STORE_DSN=redis://localhost:6379/1 nlalpha-backtest
```

The container image stores SQLite state under `/data`; mount that volume for standalone persistence.

The member's `pyproject.toml` is a standalone PEP 517 package definition. To build its wheel from an independent checkout or copied package source, run this from the package directory:

```bash
uv build --no-config --no-sources --default-index https://pypi.org/simple/
```

## Multi-symbol event execution
The standard `run_backtest` request accepts either one `data_ref` for a single-symbol strategy or a `data_refs` mapping for a configured multi-symbol universe. Mapping keys must exactly match the ordered, unique configured symbols, values must be distinct references visible to the authenticated caller, and each source table's `symbol` values must match its key. The server preserves symbol identity and does not let request data overwrite caller identity. Strategy validation uses the configured symbols and interval through the same mock-provider path used by the hosted worker.

Event execution defaults to Nautilus. It requires a positive initial balance and explicit account currency, one venue and valuation currency, NETTING, and explicit per-symbol instrument definitions. Native OHLCV bar execution uses `execution.mode="native_bar"` and a positive configured `execution.bar_interval` matching the data interval. Per-symbol timestamps must be unique and strictly increasing. `on_bar` accepts sparse symbol timestamps; `on_bars` requires a complete shared timestamp grid. Results expose actual per-symbol positions, free cash/equity, fees, fills and native order denials. `trade_count` counts fills; total return and maximum drawdown are ratios.

## Event-order results

Strategies submit market, limit, stop, and stop-limit orders with day, gtc, ioc, or fok time-in-force. A local `queued` state does not claim native acceptance or execution. Cancellation is an intent until the engine confirms it. Check pending orders before submitting duplicates, use `remaining_volume` after partial fills, and respond to native denial, rejection, and cancel feedback. A helper `price` is only a reference, not the execution price.

Full Nautilus results expose `diagnostics.final_orders` and `diagnostics.order_events`; the latter preserves native event order and nanosecond timestamps. Each projected diagnostic array is capped at 100 rows with count/truncated metadata; the full result remains stored. Callbacks receive native notifications only while the strategy is running; initial events in `on_init` / `on_start` may not be delivered as callbacks, so read snapshots to inspect engine state. Commands submitted or canceled in `on_stop` fail the job. These orders and fills belong to historical simulation and are not sent to a broker or exchange.

## Recovery result evidence

When a managed operation is observed after recovery, a successful result retains grouped metrics, equity curve, trade details, assumptions, diagnostics, reproducibility, cost breakdown, and recorded request metadata such as runtime mode, engine ID, input metadata, execution defaults, and strategy assumptions/warnings. Backend observation evidence is preserved as well. Failed outcomes retain available request metadata and backend evidence; an invalid successful result becomes a failed job with result-integrity diagnostics.

If the recovered result's `assumptions.runtime_mode` conflicts with the runtime mode recorded for the accepted request, the job fails while retaining recorded request metadata and backend evidence in its result details. This check validates runtime mode; it does not assert that every provider's `engine_id` matches the recorded request. A failed recovery observation or missing operation is surfaced as failure, never wrapped as a successful result.


## Annualized basic statistics

The result keeps `metrics.performance.annual_return`, `metrics.risk.annual_volatility`, and `metrics.risk.sharpe_ratio`; each may be `null`. Annual return and volatility are ratios; Sharpe is dimensionless. Configure `execution.annualization` as a positive integer number of equity observation periods per year. If it is absent, all three are unavailable with `diagnostics.statistics.unavailable` reason `annualization_not_provided`; the engine never guesses 252 from the bar interval. `execution.risk_free_rate` is a finite annual rate greater than -1; its default is 0 with source `engine_default`. When annualization is configured, the per-observation rate is `expm1(log1p(rate) / annualization)`.

Returns use net account equity: the initial-equity baseline to the first completed bar-close group is the first observation period, followed by each completed bar-close group's simple return. Fees are already reflected in net equity and are not subtracted again. Uneven observation spacing is disclosed, not filled or resampled; annualization expresses an explicit observations-per-year assumption, not actual-time CAGR. `diagnostics.statistics` records the count and source of settings, `variance_ddof=1`, return and annualization bases, period risk-free rate, unavailable reasons, initial/first/last nanosecond timestamps, observation count, minimum/maximum spacing, irregularity, and fee treatment. No returns, one return, zero variance, non-positive equity, and numerical overflow have explicit unavailable behavior; `null` is not zero.

## Engine reports and retrieval

`BacktestResult.reports` preserves engine-specific tables in a common envelope. Result retrieval exposes the report catalog; `get_backtest_report(job_id, report_name, offset=0, limit=50, columns=None, user_id=None)` reads selected pages with a limit from 1 to 200. Ownership is checked before cache access. Original full results remain in existing storage and Sandbox artifacts (32 MiB limit); the response is not silently clipped. `not_recorded` does not cause a full-mode rerun. The callable contract must be confirmed from the current runtime catalog and capability description; local documentation does not prove that an operation is published or available.

## Drawdown episodes

The shared drawdown summary is `metrics.risk.drawdowns` with `episode_count`, `recovered_episode_count`, `unrecovered_episode_count`, and `deepest`, `longest`, `current` episode rows (each nullable when absent). `metrics.risk.max_drawdown` remains the scalar maximum and agrees with `deepest.depth`, or is zero when observations exist without an episode. `reports.drawdowns` uses the standard report envelope and columns: `episode_id`, `status`, `peak_equity`, `peak_timestamp_ns`, `peak_observation_index`, `peak_is_initial`, `start_timestamp_ns`, `start_observation_index`, `trough_equity`, `trough_timestamp_ns`, `trough_observation_index`, `recovery_equity`, `recovery_timestamp_ns`, `recovery_observation_index`, `end_timestamp_ns`, `end_observation_index`, `depth`, `decline_duration_ns`, `recovery_duration_ns`, `duration_ns`, `observed_duration_ns`, `duration_is_lower_bound`, `decline_observations`, `recovery_observations`, `duration_observations`. The Vector and Nautilus schema IDs and sources remain engine-specific.

Episodes use completed-bar-close net equity, with fees already reflected. The initial-capital reference is index 0 but has no economic timestamp; its actual reference timestamp, when known, is informational metadata only. The high-water mark resets on equal or greater equity (latest equal peak); a drawdown starts strictly below it, recovery is the first observation at or above the episode peak, and equal troughs keep the earliest trough. Depth is `(peak_equity - trough_equity) / peak_equity`, a proportion. `deepest` chooses greatest depth (ties: earlier episode); `longest` means longest observed drawdown, ordered by observed elapsed duration, then observation intervals, then earlier episode.

If the peak is initial capital, peak timestamp, decline duration and total duration are null because the economic start is unknown. Observed duration from the first underwater point is only a lower bound (`duration_is_lower_bound=true`). Recovery duration is measured from trough to recovery only for recovered episodes; unrecovered recovery fields remain null. Elapsed nanoseconds and observation interval counts are separate, exact quantities: the initial reference to the first actual observation counts as an interval, never as a fabricated 1 ns. An episode still open at sample end uses the last observed point as its observational end without assuming recovery. No observations yield `unavailable/no_equity_observations`; available with zero rows means observations existed and no episode occurred.

The report describes sampled closes, not intrabar peaks/troughs. It does not resample calendars, infer annualization, or synthesize timestamps. Nautilus always records its available observed path, while Vector summary keeps bounded O(1) tracker state and aggregate rows but marks episode history `not_recorded/record_mode_summary` with the known episode count; Vector full records all episodes. Continue using `get_backtest_report` for paged drawdown rows (maximum 200 rows per page); the existing 32 MiB artifact cap remains.

## Monthly and yearly net returns

`metrics.performance.period_returns` summarizes observed `monthly` and `yearly` buckets; detailed rows are exposed by the existing `reports.monthly_returns` and `reports.yearly_returns` tables and retrieved with `get_backtest_report`. No tool ID or input schema is added. Each report has an engine-specific schema and the shared report envelope. Rows identify the Gregorian period in `execution.analysis_timezone` and its exact calendar bounds represented as UTC nanoseconds, reference/end equity and timestamps, endpoint net PnL/return, observation indexes/count, sample-boundary flags, cross-period attribution, skipped periods, non-positive-equity observation count, and per-field unavailable reasons.

Buckets use local Gregorian month/year boundaries from raw nanosecond timestamps in `execution.analysis_timezone` (default `UTC`); boundaries are represented as UTC nanoseconds. A bucket return is the net-equity change assigned to the period of its ending observation; the initial bucket uses starting capital as reference and has no economic reference timestamp. Later buckets use the preceding real observation as reference. Fees and slippage are already reflected in net equity. Non-positive reference equity makes return unavailable; valid negative/zero end equity is preserved. Missing calendar periods are omitted, never filled with zero or carried equity. Skipped-period and cross-period interval diagnostics disclose when a bucket endpoint return spans a gap. `diagnostics.period_returns` records the analysis timezone, pinned timezone database version and TZIF hash, and strict local-midnight boundary policy; ambiguous or nonexistent month boundaries fail explicitly.

Summary counts, positive fraction, best/worst/current include valid observed buckets, including partial first and last sample periods; denominators are valid observed buckets, not all elapsed or only complete calendar periods. `yearly_returns` are observed yearly endpoint changes, not `annual_return` or CAGR and are not compounded from monthly rows. A `summary` run retains these bounded aggregates but marks report rows `not_recorded`; `full` stores rows. Vector uses O(1) aggregation without per-bar date conversion within a bucket. Nautilus independently derives its period rows from its own raw-nanosecond net-equity path, excluding its artificial baseline from buckets while preserving native execution and reporting. Engine schemas and sources remain distinct.

Each frequency summary reports `observed_period_count`, `valid_period_count`, `unavailable_period_count`, positive/negative/flat counts and fraction, skipped-period and cross-period interval counts, skipped-gap bucket count, and best/worst/current rows. Row fields include `period`, local calendar start/end represented as UTC nanoseconds, reference and ending equity/timestamp/index, `net_pnl`, `net_return`, `observation_count`, first/last flags, `closed_by_next_period`, `cross_period_interval_count`, `skipped_periods_before`, `non_positive_equity_observation_count`, and `unavailable` reasons. `yearly` and `monthly` use the same conceptual fields under their own table schema.

## Source time, analysis calendar, and display

`strategy_spec.data` separates source interpretation from analysis: canonical `data_ref`s carry `nlalpha.time_contract` metadata declaring UTC `open_time`/`close_time` bounds, Bar precision, and per-asset source timezone/semantics/calendar/session labels. Use the returned `time_contract` from `load_bars`; do not infer timezone from timestamp appearance or `session_date`. Canonical data does not need a redundant source `data.timezone`; any supplied source declaration must agree with retained source metadata. For raw custom data, aware timestamp columns are normalized from their own offsets. Naive mixed-source inputs use `data.time_contracts` keyed by symbol; `data.timezone` and `data.timestamp_semantics` are global declarations only when valid for all affected symbols. A conflict fails. Normalize each source before concatenation. `open_time`/`close_time` are actual endpoints and may have session or half-day widths different from the nominal `data.interval`; one-column `datetime` needs explicit `open` or `close` semantics. `trading_date` is a session label and requires a calendar with exact session bounds.

`execution.analysis_timezone` is an independent supported IANA timezone (default `UTC`) for monthly/yearly statistics; it does not reinterpret input timestamps or alter execution. The tzdata version/hash and local-calendar boundary policy are included in diagnostics. UTC remains the canonical result/report storage representation. Optional `display_timezone` on result/report reads adds a display-only timestamp sidecar: selected metric/diagnostic timestamps or declared report timestamp columns are formatted with nine fractional digits. It does not change stored UTC values or convert elapsed durations; default responses remain canonical UTC.

## Agent contract guidance

For Agent workflows, read the current backtest capability description and the selected engine’s full capability guidance before validation or execution. This README documents package behavior; the live descriptions own the current callable contract. A job ending or a mock check passing does not, by itself, establish a successful historical result.

## Execution and result semantics

Choose a provider from the user's requested behavior and the currently discovered manifests, runtime modes, input schemas, and execution tools. Read its current confession and confirm that it declares the required timing, account feedback, and order lifecycle. If no available route preserves the request, report the gap; do not silently change semantics or run providers in parallel. There is no default dual run or cross-engine validation. `not_recorded` summary tables are not evidence of zero fills. A business backtest is successful only when the engine result is genuinely successful; Temporal job `completed` alone is insufficient. On failure, keep existing runtime/input/default metadata and refresh failure evidence instead of reading an earlier cached success.

`read_work_result` performs a single bounded read of one original ref (default 512 KiB, hard cap 1 MiB). When the serialized item is over budget, it returns `{result_head, output_truncated: true, returned_bytes, total_bytes}` with the original structured fields dropped; the read is deterministic and not pageable. `get_backtest_report` retains row pagination with `offset` and `limit` 1–200. These are separate APIs.

## NLAlpha App documentation

The live runtime descriptions own the current callable contract.

## Focused service verification

The event/batch catalog and per-asset source contract group passed 461 tests and 395 subtests, with 48 warnings, in 27.90 seconds. Nine `S3IntegrationTest` cases were explicitly deselected, so this run does not establish S3 integration coverage. Remote execution version binding and release startup order remain separate pending work.

## Canonical backtest guidance

The service's generic execution, validation, job, report, and evidence guidance is packaged in `src/nlalpha_backtest/knowledge/execution.md`. Engine-owned configuration stays with each engine: Nautilus uses `nlalpha-nautilus/src/nlalpha_nautilus/knowledge/backtest-config.md`, and Vector uses `nlalpha-engine-vector/src/nlalpha_engine_vector/knowledge/backtest-config.md`. These Markdown resources are canonical content for a later consumer integration; they are not registered or loaded by this README change. A future `backtest.confess(engine_id)` consumer should combine this shared resource with the selected provider's engine-owned `backtest-config.md`. The shared execution guidance leaves engine choice to the user's request and the currently discovered manifests, modes, schemas, and tools.


## Runtime descriptions and telemetry ownership

The service `about` description and execution-engine `confess` description are generated from the installed package version, current engine manifests, execution backends, and packaged guidance. They use `nlalpha.description.v1`; selected sections preserve the full description's `description_revision`. The backend capability query is separate from publication: Local and Sandbox capability records are implemented and consumed by runtime descriptions, while workflow release ordering that publishes matching server and profile metadata remains pending.

The process entry point owns telemetry SDK setup, flush, and shutdown. Importing or constructing the app factory does not create or shut down process-wide telemetry providers. `runner.execute_task` is task logic; the runner process wrapper owns telemetry lifecycle and emits one terminal record after shutdown. If the task and telemetry shutdown both fail, terminal data preserves both errors.

Artifact readers treat 64 KiB as the metadata protocol limit, not a payload cap. Redis performs one bounded range read. S3 validates fixed header and bounded metadata first, checks the configured payload budget, then reads at most 64 KiB per chunk through declared-size plus one byte and closes the body. Reads that exceed an explicit budget fail without deleting a still-live artifact. `NLALPHA_ARTIFACT_MAX_BYTES` unset means no package default 10 MiB cap; deployment owners still budget deserialization memory and concurrent requests.

## License

`nlalpha-backtest` is one of the 10 community distributions under AGPL-3.0-or-later. Host-specific adapters and private enhancement packages are separate distributions; third-party dependencies retain their own licenses.

## Container images

The community Redis image installs the non-editable service wheel closure on Python 3.12 slim. A separate S3 image explicitly selects the S3 artifact backend. Hosting server and runner images are built from the same source tree with the enhanced lock; the Vector native wheel is compiled in a builder stage and runtime images contain no Rust toolchain. Redis remains the default artifact backend and does not require MinIO.
