Metadata-Version: 2.4
Name: tradepose-client
Version: 3.8.0
Summary: Python client SDK for TradePose trading platform
Project-URL: Homepage, https://tradepose.com
Author-email: TradePose Team <admin@tradepose.com>
License: MIT
Keywords: client,quantitative,sdk,trading
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.13
Requires-Dist: filelock>=3.16.0
Requires-Dist: httpx[http2]>=0.28.1
Requires-Dist: nest-asyncio>=1.6.0
Requires-Dist: polars==1.33.1
Requires-Dist: pyarrow
Requires-Dist: pydantic-settings>=2.7.0
Requires-Dist: pydantic>=2.12.1
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: rich>=13.0.0
Requires-Dist: tradepose-models<3.0.0,>=2.13.0
Requires-Dist: typer>=0.16.0
Provides-Extra: optional
Requires-Dist: orjson>=3.10.0; extra == 'optional'
Requires-Dist: tenacity>=9.0.0; extra == 'optional'
Description-Content-Type: text/markdown

# TradePose Client

Client 3.8.0 is paired with Models 2.13.0. Installation and migration details are
provided in the Client package’s `docs/MIGRATION_3_8.md`. Analyzer is installed locally
from the same release source and is not published to PyPI.

TradePose Client is the public Python SDK and command-line workspace for reproducible
quantitative trading research. It keeps strategy source, experiment definitions, and
research evidence connected from the first local check through portfolio selection.

The primary workflow is:

```text
Strategy Family -> Experiment -> Preview -> Run -> Evidence -> PortfolioVersion -> Risk
```

TradePose Client is Alpha software. Expect the authoring and research interfaces to
evolve between releases. Client 3.8.0 is an Alpha minor-breaking release and
requires coordinated Gateway/Worker task and expression contracts. Preserve active
workspace evidence and follow the packaged `docs/MIGRATION_3_8.md` guide before upgrading.

## Requirements and installation

- Python 3.13 or newer
- macOS or Linux
- A TradePose account and API key only when you choose remote execution

For a new project, install with [uv](https://docs.astral.sh/uv/):

```bash
uv init --python 3.13
uv add 'tradepose-client==3.8.0' 'tradepose-models==2.13.0'
```

If you already have an activated Python 3.13+ environment, `pip install
tradepose-client` is also supported.

Client 3.8.0 requires `tradepose-models>=2.13.0,<3.0.0`. Upgrade the pair together.
For Client 3.7.0, preserve the existing lock or pin Models 2.12.0, including during
the Models-first publication window. `tradepose-analyzer` is not a Client runtime dependency or installable Client
extra.

## Five-minute local workflow

Start in the clean project directory created above:

```bash
uv run tradepose init .
uv run tradepose doctor

uv run tradepose strategy new rsi_reversion --template rsi-reversion
uv run tradepose strategy show rsi_reversion
uv run tradepose strategy check rsi_reversion

uv run tradepose experiment new rsi_2024 \
  --source working:rsi_reversion \
  --year 2024

uv run tradepose experiment check rsi_2024
uv run tradepose experiment preview rsi_2024 --verbose
```

Everything through Preview is local-only. These commands do not construct a Gateway
client, create a Run, or consume remote execution usage. Preview resolves the exact
source revision, parameter selection, execution configuration, request identity, and
remote-work count before anything is submitted.

The generated Working Source is
`playbook/strategies/rsi_reversion.py`. Edit its typed parameters and recipe, then rerun
the Strategy and Experiment checks to catch authoring errors locally.

## Remote execution is explicit

Set an API key only when the preview is ready to run:

```bash
export TRADEPOSE_API_KEY="..."
uv run tradepose experiment run rsi_2024
```

`experiment run` is the explicit remote-execution boundary. Before submission it shows
the exact remote-work count and asks for confirmation. Remote execution is subject to
the usage limits applicable to your account.

For intentional automation, `--yes` skips the confirmation prompt. Use it only when the
automation has already reviewed the preview and remote-work count.

Each accepted execution creates one durable Run. An unprotected terminal Run becomes
eligible for local cleanup after seven days by default. Preserve important evidence as
an explicit research decision:

```bash
uv run tradepose run keep <run-id> --reason "selected for forward evaluation"
```

`run unkeep` removes that protection. `state clean` previews eligible cleanup unless you
explicitly apply it, and local removal never cancels remote work.
Restrictive database references also protect Runs from removal and appear as
`storage_reference` protection reasons. Cleanup preserves those references and continues
removing other eligible Runs without changing the workspace schema.

## Research lifecycle and evidence

A Strategy Family owns the stable research idea. Its Working Source is the editable
Python implementation. Exact source revisions let Experiments and Runs retain the code
that produced their results even after the Working Source changes.

An Experiment records periods, Strategy Family members, parameter selection, and build
mode. It is revisioned rather than overwritten, so changes remain reviewable. Useful
local commands include:

```bash
uv run tradepose experiment show rsi_2024
uv run tradepose experiment history rsi_2024
uv run tradepose experiment diff rsi_2024
uv run tradepose experiment clone rsi_2024 --as rsi_2025
```

A Run is the evidence root for one remote execution. It connects the submitted request,
source snapshots, results, and selected configurations. Inspect evidence locally with:

```bash
uv run tradepose run list
uv run tradepose run show <run-id> --verbose
uv run tradepose inspect run:<run-id>
```

Verified Result selections publish an immutable Gateway PortfolioVersion with explicit
name, allocation and reference notional. Use `result.select(selection_id, allocation_weight=...)`
and `client.portfolios.publish_verified_run_version(portfolio_name=..., reference_notional=..., selections=...)`.
Memory and saved Runs share this seam; publication never creates a SQLite store.

Published catalogs enter the research lifecycle through explicit resolution.
`client.portfolios.materialize_evaluation_experiment(research, version=version, period=...)`
returns an actionable memory Experiment pinned to immutable Gateway bytes. It supports
`preview()`, explicit `save(name)`, and `run(preview=...)` without a catalog lookup.
Current Working Source never replaces published content. Definition-only materialization
remains available through `client.definitions.materialize_experiment(...)`.

Catalog publication is also explicit. Publish one complete compiled Definition/Policy
set with `client.definitions.register(compilation)`. After a Run is complete and locally
verified, use `client.portfolios.publish_verified_run_version(...)`; the Gateway
independently revalidates the completed remote work, admitted source binding, and canonical
artifact bundle before publishing one immutable `PortfolioVersion`. Neither operation is
part of Preview or ordinary Run recovery.

The public async client exposes `client.risk_policies` for the post-publication sizing
handoff: register an immutable policy/account binding, inspect published evidence,
request formal batch evaluation, and resolve a sizing Engagement's canonical context.
These calls return typed `tradepose-models` contracts; the removed local
Portfolio-to-order-event handoff has no sizing fallback. Returned quantities are
authoritative Gateway pre-execution sizing
evidence, not live margin, open-heat, liquidity, or broker-execution authorization.

## Strategy authoring model

TradePose strategies are typed Python modules. A source declares market data and
indicators, a Base opportunity describes the market event, and optional post-Base
policies describe entry and exit decisions. Parameters remain separate from assembly so
one definition can produce reproducible baseline, sweep, or policy cases.

The generated RSI template is executable documentation. Its central shape is:

```python
from tradepose_client import authoring as tp


@tp.strategy(RsiReversionParams)
def rsi_reversion(
    builder: tp.DefinitionBuilder,
    params: RsiReversionParams,
) -> None:
    """Build the documented RSI mean-reversion Base opportunity."""

    primary = params.primary
    opportunity = params.opportunity
    rsi = builder.col(primary.rsi)
    long_entry = rsi < opportunity.lower_level
    long_exit = rsi >= 50.0
    short_entry = rsi > (100.0 - opportunity.lower_level)
    short_exit = rsi <= 50.0
    entry, exit = (
        (long_entry, long_exit)
        if opportunity.direction == tp.TradeDirection.LONG
        else (short_entry, short_exit)
    )
    builder.data.set_volatility_scale(primary.volatility_atr)
    builder.base(
        direction=opportunity.direction,
        trend=opportunity.trend,
        entry=entry,
        exit=exit,
    )
```

Use `strategy show` to inspect the public parameter interface and `strategy check` to
validate source identity, completed-bar causality, indicator dependencies, and build
contracts. Experiment Preview then expands parameter selections and reports exact work
without crossing the remote boundary.

Policy and Experiment authoring describe executable entry and exit behavior only. Position
sizing belongs to the Gateway-owned `RiskPolicy` resource after verified selection and
explicit Portfolio Version publication; it is not a Policy sweep or compatibility input.

## Discover published portfolios and risk policies

Gateway discovery uses tenant-scoped typed resources:

```python
async with TradePoseClient(api_key=api_key) as client:
    portfolios = await client.portfolios.list(limit=20)
    for portfolio in portfolios.portfolios:
        print(portfolio.name, portfolio.portfolio_ref, portfolio.version_count)
        versions = await client.portfolios.list_versions(
            portfolio_ref=portfolio.portfolio_ref,
            publication_status="approved",
            limit=20,
        )
        for version in versions.versions:
            exact = await client.portfolios.get_version(version.portfolio_version_ref)
            policies = await client.risk_policies.list(
                portfolio_version_ref=exact.portfolio_version_ref, limit=20
            )
            for summary in policies.risk_policies:
                print(summary.portfolio_name, summary.summary, summary.risk_policy_ref)
                policy = await client.risk_policies.get(summary.risk_policy_ref)
```

All three lists use `limit` (default 50, range 1–100) and `offset` (default 0,
nonnegative). `count` is the number of entries on the returned page. Follow
`next_offset` until it is `None`, retaining the same filters. Ordering is descending
creation time, then descending exact ref (`portfolio_ref` for Portfolios); ties do not duplicate
or omit entries while the collection is unchanged. Offset pagination is not a
transactional snapshot across concurrent creates/deletes. Empty and out-of-range
pages contain no entries and have `next_offset=None`.

The HTTP endpoints are `GET /api/v1/portfolios`, `GET /api/v1/portfolios/{portfolio_ref}`,
`GET /api/v1/portfolio-versions`,
`GET /api/v1/portfolio-versions/{portfolio_version_ref}`, `GET /api/v1/risk-policies`,
and `GET /api/v1/risk-policies/{risk_policy_ref}`. Omitted parent filters include
all tenant-owned resources. A supplied missing or inaccessible parent ref returns
the same 404 response; malformed refs, pagination values, and unsupported publication
statuses return 422. The only publication status is `approved`. Exact version
reads and version discovery validate persisted publication evidence; corrupted
authorized publication data returns 409, including Portfolio latest-version summaries.

Portfolio list items expose the current `name`, `portfolio_ref`, `created_at`,
`is_archived`, `version_count`, and `latest_version`. Read current metadata by
exact ref with `client.portfolios.get(portfolio_ref)`. Published selections are
returned by `get_version(portfolio_version_ref)`.
`archived=True` includes archived Portfolios; it does not mean archived-only.
Versions and policies remain discoverable after their Portfolio is archived, and
exact version reads remain available. Portfolios without versions and versions without
RiskPolicies are retained in discovery.

`portfolio.name` is the current mutable display label. `latest_version.portfolio_name`,
`version.portfolio_name`, and RiskPolicy summaries' `portfolio_name` are publication-time
snapshot labels. A rename does not alter published content or exact identity. RiskPolicy
`summary` describes risk fraction, history lookback, minimum samples, and evaluator;
it is a display aid, not an identity or unique name. Use exact refs for subsequent reads.

RiskPolicy detail returns effective stored rules, omitting overrides whose optional
fields are all `None`. Partial overrides retain `None` and inherit defaults without
expanding them. Selector ref order, Decimal values, UTC timestamps, canonical expression
encoding/payload, evaluator revision, and the stored `risk_policy_ref` are preserved.
Discovery does not guarantee reconstruction of the original registration payload:
omitted and explicitly empty overrides may produce distinct stored identities with the
same discovered effective rules. Those policies remain separate list entries with their
own refs and creation times. A policy bound to multiple accounts appears once.
Discovery does not register policies, evaluate sizing, append decisions, or schedule work.

## Local state and retention

Workspace metadata lives in `.tradepose/state.sqlite3`. Canonical request bytes and
source snapshots stay with Run records; larger downloaded artifacts live below
`results/runs/<run-id>/`.

```bash
uv run tradepose state info
uv run tradepose state clean
```

Keep `.tradepose/state.sqlite3` and retained result artifacts together when backing up a
workspace. SQLite schema 11 is authoritative. Close running clients before executing
`tradepose state migrate` to upgrade a schema 10 workspace. The command creates a complete
SQLite/results backup under `.tradepose/migration-backups/`, preserves historical Run
records and artifacts verbatim, and restores that backup if activation fails. Ordinary
commands refuse SQLite access while an interrupted migration journal exists. Close clients
and rerun `tradepose state migrate` to recover explicitly before creating new work.
Ordinary commands never migrate or delete existing data. Historical Runs cannot be read or resumed;
prepare new Runs using current Experiment inputs after upgrading. Earlier SQLite schemas
must remain intact in their original workspace; create a new workspace for current work.

## Instruments and optional agent skills

The workspace instrument catalog supplies canonical identifiers and market metadata.
After configuring remote access, synchronize it deliberately:

```bash
uv run tradepose instruments sync
uv run tradepose instruments status
```

The Client also ships optional Claude and Codex skills for strategy authoring and
research workflow guidance. Install and verify them in a workspace with:

```bash
uv run tradepose skills install --agents claude,codex
uv run tradepose skills check
```

After upgrading the SDK, synchronize the selected agents and check the result so the
workspace uses the bundled guidance from the installed version:

```bash
uv run tradepose skills sync --agents claude,codex
uv run tradepose skills check
```

These generated guidance files are local tooling. They do not submit research or grant
an agent remote-execution authority.

## Interactive notebook API

`ResearchWorkspace` provides one authoring and execution entry point. Experiments start
in memory and accept the imported recipe, typed Params and immutable UTC `Period`.
`experiment.save(name)` explicitly saves settings; lookup can select an exact revision.
Remote work requires a Preview, and `run(preview=preview)` expresses submission intent.
All handles share the workspace's single lazy SubmissionEngine runtime owner.
Backtests always produce trade artifacts; `persist_trades=False` is the default. Set it
to `True` only to additionally save trades to Gateway PostgreSQL.

The executable examples in `examples/research_single.py` and
`examples/research_three_members.py` run independently in an empty research directory.
Personal notebooks remain local ignored files.

The example uses the actual `rsi-reversion` strategy created as `alpha.py`:

```python
from playbook.strategies.alpha import RsiReversionParams, rsi_reversion
from tradepose_client import Period, ResearchWorkspace

with ResearchWorkspace.open(".") as research:
    experiment = research.experiments.backtest(
        source=rsi_reversion, params=RsiReversionParams(), period=Period.from_year(2024),
    )
    preview = experiment.preview()
    run = experiment.run(preview=preview)
    trades = run.wait(timeout=1_800).result().trades()
    # Call run.save() here if this same execution should survive the session.
```

Runs default to memory. `experiment.run(preview=preview, save=True)` explicitly saves
identity, exact requests, source bytes and evidence before the first remote submission.
`run.save()` returns the same handle, never submits work, and retains its ID. Saving during
execution switches subsequent observed state and results to local storage after the
snapshot transaction succeeds. Failed saves can be retried; repeated saves create no
additional Run. Failed or incomplete Runs may be saved for diagnosis but never yield
partial analysis results. New execution always requires a Preview matching current settings
and source; opening an existing Run never creates replacement work.

Use `research.runs.open(run_id)` for an exact session or saved identity. Only saved Runs
survive restart. `experiment.prepare(preview=preview, save=True)` separates the durable
boundary from submission; without `save=True`, preparation remains in memory.
`RunSubmissionError`, `RunSubmissionInterruptedError`, `RunSaveError` and
`RunSaveInterruptedError` carry the affected Run ID and original cause. After an interrupted
save, inspect `run.is_saved` and retry on the same handle. A validation failure before Run
creation does not invent an ID.

`run.result()` accepts only complete verified evidence and artifacts. Backtest `trades()`,
OHLCV `indicators()` and OHLCV `signals()` return kind-specific Polars DataFrames with
readable identity columns. `raw_frame()` is the advanced canonical frame and follows the
same complete verification. Shared Workloads retain `member_ids` and exact `selection_ids`
without duplicating physical rows or implying portfolio performance.

`run.evidence()` and `result.evidence()` expose named periods, members, Params selections,
sources and request states. `preview.compare(run)` reports named differences and separates
research equality, request equality and Run identity. Notebook display reads captured
metadata only. Query saved identities with `research.runs.list()`, then open the selected
ID; `open(run_id, kind=ExperimentKind.BACKTEST)` checks the expected kind for typed analysis.
The Result guide (`docs/RESULTS.md` in the source distribution) covers dtypes, ordering, zero trades,
raw access and verification limits. Results retain their content owner for offline use;
the last remaining owner controls its lifetime.

`memory_limit_bytes` defaults to 512 MiB per encoded download and for retained aggregate
artifact bytes. Concurrent buffers and decoded DataFrames need additional memory. A
`RunMemoryLimitError` never spills to disk: explicitly save that Run, then wait again, or
choose local saving initially. Workspace configuration, named settings and source/instrument
caches may write catalog state independently; memory Runs write no Run rows, execution
evidence, result files or artifact temporary files. Local saving does not change remote
retention or `persist_trades`.

One workspace owns a shared lazy event loop, transport client and concurrency budgets.
Handle/workspace close, Ctrl-C and a local wait timeout stop observation without cancelling
accepted remote work. Closing does not promise background downloads. Resume a saved Run
later to continue observing and downloading its existing remote work.

Run network recovery is operation-aware and bounded. An ambiguous submission replays
only the exact captured request bytes with its idempotency key before the known
confirmation deadline, so it resolves
to the same logical remote work; a key/content conflict is terminal. Polling and
downloads retry only network failures, temporary rate limits, and selected server
failures with capped jittered backoff. Temporary rate limits honor a bounded
`Retry-After`; authentication, validation, and other deterministic failures do
not retry. Explicitly temporary submission capacity/rate refusals retry the exact
bytes and key, for at most three attempts. Retry-After is never shortened: a server
delay beyond the retry policy budget (default 60 seconds) ends automatic retry.
An unknown or expired confirmation deadline prevents replay of ambiguous outcomes.
If the attempt cap or local wait deadline is reached, reopen the same Run to
continue from its captured state. Completed artifact bundles are still checksum-, schema-,
identity-, and compatibility-verified before they become locally available.

SDK submission, polling and download concurrency default to the local HTTP
connection capacity (100), configured with `TRADEPOSE_MAX_CONNECTIONS`.
`TRADEPOSE_MAX_KEEPALIVE_CONNECTIONS` defaults to 20. These are local resource
settings, not membership quotas. Separate poll/download budgets cover active I/O;
a remote execution waiting for completion does not block observation of later work.
`TRADEPOSE_POLL_INTERVAL` defaults to two seconds with no increasing poll backoff;
`run.wait(poll_interval=...)` overrides it for that wait. Faster polling consumes
more of the shared HTTP allowance. Server rate/capacity refusals remain visible
when retries are exhausted.

Server defaults per user are:

| Membership | Shared HTTP requests/min | Research ingest starts/min | Occupied execution slots |
| --- | ---: | ---: | ---: |
| Free | 30 | 10 | 10 |
| Pro | 360 | 120 | 120 |
| Enterprise | 720 | 240 | 240 |

Submission, status queries and downloads share the HTTP counter. Ingest starts
have a separate counter; threefold HTTP headroom does not mean a Run uses only
three requests. Global defaults are 1,200 ingest starts/min and 1,200 occupied
execution slots. Monthly usage is telemetry, not an admission quota. Temporary refusal
is retried only within the attempt, Retry-After and confirmation budgets described
above. A 429 after an unconfirmed write does not prove the original remote execution was rejected;
reopen the same Run with its persisted bytes/key to recover within its deadline.

Remote executions have fixed service limits: 300 seconds queued, 900 seconds executing
(including save and publication), and 1800 seconds for a sharded parent through merge.
Finished executions and their artifacts remain available for 24 hours from termination;
querying does not renew that window. Resume cannot resubmit expired or failed remote
work. Download and verify within the window; already downloaded results remain usable
locally. First Portfolio publication needs live verified evidence, while an already
published Portfolio retains its own business Workload independently of remote artifact retention.

Gateway retains SQL execution history and execution statistics through batched lifecycle
events. Those records may lag live Redis state; a verified terminal event can correct
a deadline inference. Historical status is neither live execution permission nor proof
of a business save, and cannot restore expired remote artifacts.

## Support, status, and license

- Homepage: [tradepose.com](https://tradepose.com)
- Support: [admin@tradepose.com](mailto:admin@tradepose.com)
- Status: Alpha
- License: MIT

## Compilation and selection evidence

`StrategyRecipe.compile()` produces one complete canonical Definition, its exact Workload,
and BuildEvidence for each typed Params occurrence. Expand a `ParamGrid` with `.expand(params)`
and compile every result independently. A `PolicySet` supplies the complete managed Policy
set for one occurrence. Strategy check and Experiment check/preview use this same compiler.

Preview JSON exposes `workloads`, `policies`, and exact refs. Each selection retains its
DefinitionRef, WorkloadRef, PolicyRef, AuthoringOccurrence, actual resolved Params, and
member lineage. AuthoringOccurrence also retains the exact `volatility_scale_node` selected
by the recipe, even when multiple logical indicators share one physical computation.
Identical complete Workloads share a request per Period. Run preparation,
resume, results, and Portfolio publication verify these exact refs and retained bytes.

<!-- research-contract:start -->
## Python research contract

Use public imports, a real strategy source, typed Params and `Period` directly.
Create an Experiment and review `preview = experiment.preview()` before calling
`experiment.run(preview=preview)`. Catalog publication and named Experiment saving
are optional. Preview is local and does not authorize remote work; an agent must
have explicit research execution intent within the user's approved bounds.
Memory storage still submits remote work and may incur usage.

Runs default to memory, including `experiment.prepare(preview=preview)`. Choose
`save=True` initially or call `run.save()` during execution or after completion.
Saving returns the same Run and ID; subsequent observed states and downloaded
results persist locally. `persist_trades` separately controls Gateway PostgreSQL
trade-table persistence; backtests return trade artifacts by default either way.
Closing a handle/workspace or a `RunWaitTimeoutError` stops local observation and
does not cancel accepted remote work or promise background downloads. Save before
closing, then list `research.runs.list()` and explicitly open that exact saved ID
with `research.runs.open(run_id)`, call `resume()` and `wait()` as needed. Running
an Experiment again creates a different Run, even when requests are identical.

Only all-success, fully verified Runs provide a Result. Use `trades()` for backtests,
`indicators()` or `signals()` for OHLCV. OHLCV accepts one Period and one distinct
Workload. `raw_frame()`, descriptor tables and artifact bytes obey the same complete
verification. Three members may share two Workloads: inspect Preview's exact remote
work and never duplicate physical trade rows per member. Successful zero trades
retain the complete schema. Preserve `RunFailedError` and
`RunResultUnavailableError.kind`; never turn failure into an empty DataFrame.
A changed source requires a fresh Preview; recovery uses the existing Run's captured
evidence. Historical unsupported Runs require no data migration for new research;
create current sources and Runs in a fresh workspace.
<!-- research-contract:end -->


## Shared offline sizing

`tradepose_client.sizing` provides `SizingDraft`, typed per-Policy budgets, a native
Polars expression profile, a finite-history planner, and `SizingInputs.evaluate()`.
`tradepose_analyzer.TradesPerformance(result)` or `(result.frame)` accepts every
NoOrder row, retaining raw points separately from USD outcomes. Models owns the
shared floor rounding and cost calculation.
Given validated `SizingInputs`, evaluation is entirely offline:

```python
from tradepose_client.sizing import SizingDraft
from tradepose_analyzer import TradesPerformance

result = inputs.evaluate(SizingDraft(risk_amount_usd=1000))
performance = TradesPerformance(result)
```

The default budget is USD risk per trade, not purchase notional. Snapshot adapters
must verify transport, artifact digests, identity, and coverage before supplying
inputs; hand-built fixtures do not establish that verification or save/open.

### Policy discovery 與 sizing snapshot

`client.definitions.list_policies(...)`／`get_policy_contexts(...)` 回傳 tenant-owned
Policy 與 typed Definition contexts。`SizingSnapshotRequest.for_policies(...)` 接受
複數 refs／typed contexts，`for_portfolio(...)` 接受 exact published PortfolioVersion。
`await client.definitions.get_sizing_snapshot(request)` 原子取得並驗證整批 artifact。

來源必須有已提交完整 extraction coverage；合法零交易有獨立證據，未知／缺口明確拒絕，
不觸發 Worker。Snapshot 複製 canonical source bytes，不依賴暫存結果的保留期限。
`SizingSnapshot.save/open/evaluate` 支援完全離線反覆 sizing，結果直接交 Analyzer。
使用者範例 `examples/sizing_snapshot.py` 支援 Policy discovery 與 Portfolio 兩個入口；
`examples/sizing_snapshot_offline.py` 可開啟已保存 artifact。

## Verified Result、Portfolio publication 與再研究

Gateway Portfolio metadata 管理名稱與版本集合，`PublishedPortfolioVersion` 保存 immutable
refs、allocation、reference notional 與 execution evidence。研究選擇由完整 verified
`BacktestRunResult` 產生；memory 與 saved Run 使用同一公開入口：

```python
from decimal import Decimal

result = run.result()
candidates = tuple(item for item in result.evidence().selections if item.resolved_policy is not None)
chosen = result.select(candidates[0].selection_id, allocation_weight=Decimal("100"))
version = await client.portfolios.publish_verified_run_version(
    portfolio_name="chosen_research",
    reference_notional=Decimal("100000"),
    selections=(chosen,),
)
evaluation = await client.portfolios.materialize_evaluation_experiment(
    research, version=version, period=result.periods,
)
preview = evaluation.preview()
# 使用者明確要求新研究時才呼叫 evaluation.run(preview=preview)。
```

選擇候選、名稱及 allocation 是使用者決策。SDK 推導 exact Run／Definition／Policy／remote work refs，
發布時重新驗證完整 Result evidence；任意 frame 無法建立 typed selection。
此步驟不建立 SQLite、不強迫 memory Run 先 save。Saved Run 可由
`research.runs.open(run_id).result()` 取得相同選擇。保存研究不延長 remote work 的首次發布期限；
Gateway 首次發布需要尚未過期的 tenant remote work、sealed binding 及 canonical artifacts。
已存在相同 content hash 的版本可冪等讀回；直接 `get_version(ref)` 不重新要求 remote work evidence。

`materialize_evaluation_experiment(research, version=version, period=Period(...))` 回傳可操作
memory Experiment；`save(name)` 才保存設定，`run(preview=...)` 才產生新工作。
它固定 published Definition／Policy bytes 與 evidence，不編譯目前本地 source。

Sizing 使用既有 `SizingSnapshotRequest.for_portfolio(version, periods=...)` 或
`SizingSnapshotRequest.for_policies(chosen_contexts, periods=...)`，再呼叫
`client.definitions.get_sizing_snapshot(request)`。Policy-only 入口不需要 Portfolio。
Snapshot 的 `save/open` 不建立 remote work、SQLite 或 session；離線重複
`snapshot.evaluate(SizingDraft(risk_amount_usd="1000"))` 不下載資料。
`TradesPerformance(sized_result)` 直接接受 quantity／USD／NoOrder 及 raw facts，
每個 Policy／Period 保持獨立。Analyzer scoring 與組合挑選仍由既有 Analyzer API 負責。

完整範例：`examples/portfolio_research.py`（Run → publication → snapshot → sizing → Analyzer）
與 `examples/sizing_snapshot.py`（複數 Policy contexts → snapshot → sizing → Analyzer）。

### Operational schedules and refresh

`client.risk_policies` provides typed `create_schedule`, `get_schedule`, `list_schedules`,
`update_schedule`, `pause_schedule`, `resume_schedule`, `request_refresh` and `get_refresh`.
Keep the same operation identity after a timeout. Accepted/running receipts do not imply
completed sizing; schedule pause does not change binding eligibility or Live Authorization.
Use Models requests with `operation_ref`, `reason`, `expected_version` and an explicit
IANA timezone. Cadence accepts five or six Unix cron fields.
