Paper trading¶
Paper trading is the forward proof of a backtest. It runs on a dedicated Alpaca paper account; the adapter hard-codes the paper origin and there is no live path.
Other paper brokers¶
broker: plugin:<name> uses a paper broker from an installed package (entry point
signalquarry.brokers; sqy doctor lists them). The kernel refuses a broker that does not
declare paper_only = True or lacks part of the paper broker interface, and every guard,
the journal and the arm step apply unchanged. Equity deployments only; market data still
comes from the project's data provider.
Parity first¶
sqy check --parity replays the last 60 sessions of the synthetic check window through
the real paper kernel against an in-memory venue and through the backtest, and fails
unless orders, fills, positions, cash and journalled decisions (wheel events for
options) agree. Costs, dividends and option spread haircuts are removed from both
sides because a paper venue reproduces none of them. The project CI template runs it.
A deployment¶
paper/<alias>.paper.yaml:
schema: signalquarry.paper/v1
alias: demo
strategy: sma-trend
broker: alpaca-paper # or "simulated": offline dry-run only
submission: disabled # a human sets "enabled" after review
expected_account_id_sha256: # optional: refuse any other account
denied_key_id_sha256: [] # keys of other deployments that must never be used here
arm_days: 30
isolation: process # decide runs in a child process without credentials
window: {start: "09:00", end: "09:25"} # New York time, before the open
guards:
max_daily_drawdown: "0.10"
max_clock_skew_seconds: 30
buy_price_buffer_bps: "50"
Keys go in credentials.toml under [paper.<alias>] (or
SIGNALQUARRY_PAPER_KEY_ID / SIGNALQUARRY_PAPER_SECRET_KEY). Use one paper
account per deployment.
Commands¶
| Command | Who | What |
|---|---|---|
sqy paper preflight --alias A |
anyone | Read-only readiness checks. |
sqy paper dry-run --alias A |
anyone | Decide and size the next session; submits nothing. |
sqy paper arm --alias A --reason ... |
human | Type the alias to confirm; binds the frozen configuration and account. |
sqy paper run-once --alias A |
scheduler | Reconcile, decide, submit market-on-open orders. Idempotent. |
sqy paper status --alias A |
anyone | Journal state, sessions, pending orders. |
sqy paper reconcile --alias A |
anyone | Finalize journaled orders and check positions. |
sqy paper halt --alias A --reason ... |
anyone | Stop and cancel open orders; a human re-arms. |
sqy paper schedule --alias A --target systemd |
anyone | Write scheduler templates for review. |
sqy paper drift --alias A |
anyone | Replay sessions, measure slippage, shadow costs and dividends; gate G5. |
sqy paper run --alias A |
service manager | Run run-once until done, retrying while busy or unavailable. |
sqy paper backup --alias A |
anyone | Archive the journal and arm token with a hash manifest. |
sqy paper verify-continuity --alias A --backup F |
anyone | Prove the live journal only grew since a backup. |
How a session runs¶
- Take the deployment lease; verify the journal's hash chain.
- Check the submission switch, the account, the arm token and the broker clock.
- Load bars through the previous session.
- Reconcile: finalize own orders from earlier runs, refuse unmanaged orders or positions, halt on drift that fills and splits cannot explain.
- Decide and size with the backtest's own code; clip buys to settled cash.
- Journal each order before submitting it, with a deterministic client order id, so a crash or network error is resolved by lookup, never by a duplicate order.
run-once exits 0 when done (or nothing to do), 75 to retry later, 69 when the
broker or data is unavailable, and 2 or 78 when a human is needed.
Isolation¶
With isolation: process (the default) the decide-and-size step runs in a child
process that receives only the data it needs, with no broker or data credentials, an
empty config directory, SIGNALQUARRY_OFFLINE=1, and CPU and memory limits; a
strategy that exceeds them halts the deployment (PAPER_STRATEGY_TIMEOUT). This keeps
keys out of strategy code's process, but it is not a sandbox: code running as the
same OS user can read that user's files. Run paper deployments under a dedicated user.
Scheduling¶
sqy paper schedule writes systemd (recommended, on a small VM), launchd,
cron or GitHub Actions (demo only) templates that call run-once every 5
minutes from 09:00 to 09:25 New York time. Review and install them yourself.
For a local scheduler, add --notify-command /absolute/path/to/executable to
paper schedule to include a failure hook in the generated run-once command.
The same option works on a direct paper run-once invocation. The executable is
called once for every non-zero exit, including retryable 69 and 75, with only
HOME, PATH, LANG, SIGNALQUARRY_ALIAS, and SIGNALQUARRY_EXIT_CODE in its
environment. It receives no broker credentials. It must get any notification
credentials from its own configuration. The hook has a 10-second limit; its
failure does not replace the original run-once exit code. Use the hook's own
deduplication if repeated retryable exits should produce one alert.
The systemd target also writes a post-close timer (16:30 New York, weekdays) that runs
sqy commit create --alias A to commit to the journal head, stamped with OpenTimestamps
when the ots client is installed, and a weekly timer that runs ots upgrade on pending
proofs. Allow egress to the OpenTimestamps calendars for those two units.
Drift and the forward gate (G5)¶
sqy paper drift replays every journaled session through the engine with the
inputs recorded that day and requires the same decision and orders (data that the
provider has since revised is reported as unverifiable). It measures each fill
against that session's open (adverse slippage in basis points) and keeps a shadow
ledger: the model's costs and the dividends Alpaca paper does not credit, applied to
the broker's equity. G5 passes with at least 20 clean sessions (parity, orders
final, no halt), no parity mismatch and mean adverse slippage within
guards.max_mean_slippage_bps (default 25). With G5 passed for the current frozen
configuration and a prior claim of walk_forward or better, exports report
paper_forward.
Private account activity evidence¶
For an Alpaca paper deployment with expected_account_id_sha256 configured,
capture a bounded creation-time window without placing orders:
sqy paper capture-activities --alias A --created-after 2026-09-20T00:00:00+00:00 --created-until 2026-09-27T00:00:00+00:00
sqy paper verify-activities --alias A --capture sha256:CAPTURE_HASH
sqy paper observe-activities --alias A --capture sha256:CAPTURE_HASH
sqy paper verify-observations --alias A --observation sha256:OBSERVATION_HASH
sqy paper compare-observations --alias A --left sha256:EARLIER_OBSERVATION --right sha256:LATER_OBSERVATION
The capture reads all activity types with ID-based pagination and writes raw
responses and a hash-only manifest to an owner-only directory under the private
cache, outside the project. The command output contains only counts, timestamps,
and a capture hash. Verification replays the cached pages without credentials or
network access. The observation timestamp is taken after the last page is
received; it does not claim when the provider first made a row available.
The API's after and until filters use activity creation
time, which can differ from the economic effective or settlement date; a
window alone cannot prove that all activities for a historical session were
observed. See Alpaca Trading API, “Retrieve Account Activities.”
The offline observation command writes a second immutable record in the same private cache. It records each row's activity ID, reported type, symbol, date and transaction time when supplied, field names, and raw-row/page hashes. The CLI and API expose only aggregate counts and hashes. A missing field remains missing; neither reported date is treated as the economic effective date. Verification replays the original capture and every normalized row. These observations do not establish a corporate-action event link, asset identity, consideration, or settlement. They cannot be used to resume a halted paper runner.
Observation schema v2 also labels documented REST type/subtype pairs for splits, mergers, name changes and worthless removals. It does not parse quantities, prices, cash, effective dates, successor identities or correction IDs. Unknown pairs remain unclassified; every row remains ineligible for paper use, and the record-level event-link and economic-term flags remain false. Version 1 observations remain verifiable. The category allowlist follows Alpaca's official Activities guide, but has not been validated against rights-cleared account cassettes. The provider-evidence and broker-position/ cash reconciliation gates still apply.
Comparing two verified observations requires the same account and creation-time window. It reports aggregate row-ID additions, absences, changes, and unchanged rows without exposing IDs or row data. An absent row is not proof that the provider deleted an activity; a changed row is not a verified economic revision.
These raw captures are nonredistributable, do not decode corporate-action terms, and do not enable a blocked action or authorize paper submission. Keep the cache private and never add it to the project or an evidence export.
A live feed for published strategies¶
Non-commercial families whose publication policy sets live_feed: allow can publish
a sanitized snapshot of a paper account: sqy perf publish --alias A --out
site/public/feeds/paper writes A/latest.json (signalquarry-public-performance/v1,
identified by its snapshot_hash and chained to the file it replaces through
previous_snapshot_hash). It carries equity history, positions and fills
without any account or order identifiers; figures that would need cash-flow data are
marked unavailable. Commercial families never get a live feed; they publish lagged
exports only.
sqy perf capture --alias A records the same snapshot privately under
paper/A/captures/, chained by previous_snapshot_hash; no publication policy is
needed because nothing leaves the project. The NDA export (sqy evidence export --tier
nda) carries the captures and the paper journal, with broker order ids withheld and the
journal head and length kept so a recipient can match published commitments.
Options deployments¶
A deployment of an options_single_leg strategy polls instead of running once before
the open: sqy paper run-once performs one poll, and sqy paper schedule writes
per-minute templates for the regular session (systemd, launchd or cron; not GitHub
Actions). Each poll reconciles its own limit orders (cancelling entries left unfilled
after options.cancel_after_seconds), applies assignment and expiration activities to
the wheel state machine, checks broker positions against the wheels (a leg awaiting its
expiration activity is retried until the close of the next weekday; anything else halts),
then decides once per underlying with the same context and resolver as the simulator.
Every entry plan must satisfy the plan invariants (strike side, DTE window, limit at the
bid floored to the tick, collateral) before it is journaled and submitted. Quotes come
from Alpaca's data API (the stock's latest quote and the option chain snapshots).
sqy perf capture and sqy perf publish work for options deployments too (positions
include the open leg and any assigned shares); sqy paper reconcile is equity-only.
sqy paper drift evaluates G5 for options from the journal alone: option quotes are
not journaled, so decisions cannot be replayed as they are for equities (the report says
decision_replay: not_available_for_options). A session is clean when it was not halted
and every order it placed reached a final state (plan invariants are enforced before any
order is journaled). G5 passes with at least 20 clean sessions and at least one
expiration or assignment applied from broker activities. Model costs are the
per-contract fees.
Known differences from the backtest¶
- Buys are clipped pre-open at a buffered prior close; the simulator clips at the actual open. They differ only when cash binds.
- Alpaca paper charges no commissions and omits dividends.
- Market-on-open orders need whole shares.