Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The current package retains configuration and compatibility ownership. Broker cr

- [English operator guide](../web/strategy-switch-console/README.md#reading-by-task) · [中文操作入口](../web/strategy-switch-console/README.zh-CN.md#按任务阅读): installation/configuration, interfaces and operations grouped without moving contract text.
- [Console API](console-api.md): interface details.
- [Snapshot completeness](snapshot-completeness.md): cloud archive checks, daily Binance detail, and approval-bound activation.
- [English fork guide](strategy-switch-fork-guide.md) · [中文 Fork 指南](strategy-switch-fork-guide.zh-CN.md): installation in a separately configured environment.
- [English permission model](manual-strategy-switch-permission-control.md) · [中文权限说明](manual-strategy-switch-permission-control.zh-CN.md).

Expand Down
148 changes: 148 additions & 0 deletions docs/snapshot-completeness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Snapshot completeness

`python/scripts/snapshot_completeness.py` audits private daily archives in a
Cloud Run Job. It checks the requested window independently of stored samples,
so an empty archive, a leading gap and a trailing outage remain visible. It does
not call a trading or runtime-control route. The CLI refuses to run outside a
Cloud Run Job; offline tests inject storage, history and notification clients.

## Date and field contract

Supply a private configuration file through `SNAPSHOT_COMPLETENESS_CONFIG`.
The [synthetic configuration example](../examples/snapshot-completeness.example.json)
shows the shape; its one-day calendar deliberately cannot pass a rolling
production audit. Replace it with verified private configuration before approval.
`window_days` defaults to 30 and is bounded to 366. The end is the previous UTC
date, preserving 30 complete dates rather than counting the unfinished current
day; only dates whose collection deadline has passed are expected. A continuous
UTC calendar is used for crypto. Exchange calendars must include `id`, `source`,
`timezone`, inclusive `start`/`end`, and the authoritative `sessions` list of
`date` and timezone-aware `close_at`. Set `grace_seconds` for the collector's
publication deadline. Published close instants carry holidays, early closes and
DST; the guard never substitutes weekdays for an unavailable exchange calendar.
Calendar coverage must span the requested window or the target is unknown.

Account region does not identify its trading market. Select the calendar from
the broker account's actual covered instruments and collection contract. A
multi-market account needs its explicitly approved combined session calendar.
Session snapshots must be observed on that local session date after close.
Continuous-calendar snapshots retain their original UTC date. Use an actual
broker as-of timestamp for historical statements; do not relabel collection time.

The protected `targets` inventory includes every account/binding to audit.
Platforms are `longbridge`, `firstrade`, `schwab`, `ibkr`, `binance`, `alpaca`,
`qmt` and `ftmo`. `status: active` requires a complete contract; other statuses
produce `not_configured`, never a green result. Omitted platforms are also
reported as `not_configured`. The operator must reconcile this inventory
with the protected account registry before activation.

Each target declares:

- `identity`: dotted payload paths and exact values, including schema, platform,
target and account/source-binding identity. These values stay out of Git and
public output.
- `as_of_path`: source observation or broker historical as-of timestamp.
- `archive`: a GCS `prefix` ending in `/`, with `{date}` or `{yyyy}/{mm}/{dd}`.
The guard lists bounded pages and reads each JSON object at its listed
generation. `payload_path` optionally selects a nested account snapshot.
- `fields`: rules for all four groups, `cash`, `positions`, `orders`, `fills`.
Each rule supplies `path`, `complete_path`, `complete_value` (exactly `true`,
`complete` or `complete_for_scope`), nonempty
`required` row paths and `numeric` row paths. Missing values, nonfinite numbers,
unknown completeness and malformed rows fail. Actual broker-reported zero is
valid; the guard never creates a value.
- `empty_proof`: for a legitimately empty positions/orders/fills list, supply
`query_ok`, `row_count` and `pagination_complete` payload paths. Their values
must be exactly `true`, integer `0` and `true`, derived from the broker query.
An empty list plus a `complete` label alone fails. Empty cash is not a complete
cash snapshot.

Existing platform archive shapes are selected through these explicit paths;
missing producer fields remain incomplete. Do not map an open-order-only list
to full orders, a strategy-only holding list to the whole account, or a wallet
valuation summary to asset detail. The latest observation is checked, so an old
complete record cannot hide a newer incomplete one. Access/identity failures
stop that target while other targets continue.

## Binance daily detail

The Worker now persists the entire validated `binance_account_facts.v1` report in
`binance_detail_daily`, keyed by account, source binding and UTC observation date.
The latest report for a day wins; older input and conflicting equal timestamps
are rejected. The existing DO transaction validates an immediate SQL readback
before the ingestion response can acknowledge success. This table has no TTL or
automatic deletion. The two-day latest KV cache and 366-day wallet valuation
projection keep their existing contracts.

`GET /api/internal/binance-account-facts/archive?date=YYYY-MM-DD` uses the existing
dedicated Binance facts bearer authorization and trusted binding. It returns
the private raw daily report, `404` for absence, and `503` for unavailable or
invalid data. It is not a public/session API. A guard target uses
`archive: {kind: binance_detail, url: <private archive URL>, token_env: <mounted
environment variable name>}`. Keep this bearer credential within the approved
client. Sharing it with a new job requires a separate secret/access review.
The reader currently selects the active binding; retaining a retired binding
does not automatically authorize a new client to read it.

This preserves only what the collector actually supplied: Spot and Flexible
Earn detail. Funding, Margin, Futures, Locked Earn, full orders and fills are
not invented. Existing expired KV data cannot be reconstructed from wallet
valuation history.

## Backfill and alerts

Backfill defaults off. With `backfill_enabled: true`, only `archive_missing`
may invoke a configured `history` hook. It must have
`kind: broker_readable_history`, an HTTPS `/history` or `/archive` URL,
`token_env` and `payload_path`. The method is always GET with a `date` query;
redirects are refused. Configure only a broker-readable historical statement
endpoint approved for this account. There is no shell hook, order call, current
snapshot fallback, zero fill or interpolation.

The returned payload must pass the same identity, date and four-field checks.
A new `broker-history-backfill.json` is written with generation-match zero,
then independently listed/read and compared. Existing objects are never
overwritten. Nested-envelope backfill needs a producer adapter and remains
blocked; this prevents writing a payload the archive reader cannot consume.
An absent or incomplete historical source leaves the gap unresolved. A limited
recent-order response cannot reconstruct old cash or positions.

Private reports go under `state_prefix`, using create-only writes and readback.
Telegram uses mounted `TELEGRAM_TOKEN` and `GLOBAL_TELEGRAM_CHAT_ID` values, never
Secret Manager discovery or log output. Alerts contain platform, ordinal target
number, dates and fixed reason codes; no account identifiers, resource names or
payload values. A per-day content claim precedes sending. A send with unknown
outcome is not retried automatically. An ACK means Telegram sender acceptance,
not recipient receipt. A configuration or report-storage failure attempts one
fixed failure alert without persistent deduplication; it never retries an
already attempted notification. Read/configuration failures exit nonzero;
Cloud Run job failure monitoring must remain enabled even if storage or Telegram
is down.

## Cloud activation requires owner approval

Build context is the repository root; the container definition is
`python/Dockerfile.snapshot-completeness`. No new Python package is needed.
This change creates no cloud resources, secrets, IAM bindings or schedules.
Do not add a Mac timer or execute a broker backfill during code validation.

Before activation, submit the exact private target/calendar/field mapping,
image digest, configuration mount, archive/state prefixes, service accounts,
secret references and scheduler proposal to the owner. The proposed scheduler
is hourly at minute 17 in UTC, invoking one Cloud Run Job task with no automatic
job retries. Collection grace is per target. A dedicated runtime identity needs
object list/get on source prefixes and create/get on its state prefix; approved
backfill additionally needs create on the selected archive prefix. Scheduler
invocation and mounted-secret access are separate narrowly scoped grants.

After approval, deploy the reviewed Worker and guard image, install the
protected configuration, and activate the cloud schedule. Verify a natural job
execution, private report readback, a known missing-day alert, Binance detail
readback after cache expiry, and a real-history-only recovery when available.
Report deployment, scheduler activation, sender ACK and recipient receipt
separately. Rollback pauses only the new scheduler and restores the prior
Worker/image; retain archived financial records. Any gcloud operator command
must include an explicit `--account` argument.

Storage semantics follow [Cloud Storage generation preconditions](https://docs.cloud.google.com/storage/docs/request-preconditions)
and [Durable Object synchronous transactions](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transactions).
134 changes: 134 additions & 0 deletions examples/snapshot-completeness.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"window_days": 30,
"state_prefix": "gs://synthetic-private/audit",
"backfill_enabled": false,
"targets": [
{
"platform": "longbridge",
"status": "active",
"identity": {
"schema": "synthetic.broker.v1",
"platform": "longbridge",
"account": "synthetic-account",
"binding": "synthetic-binding"
},
"as_of_path": "observed_at",
"archive": {
"prefix": "gs://synthetic-private/archive/{date}/"
},
"calendar": {
"kind": "sessions",
"id": "synthetic-exchange",
"source": "synthetic-example-not-for-production",
"timezone": "Asia/Hong_Kong",
"start": "2026-10-20",
"end": "2026-10-20",
"grace_seconds": 3600,
"sessions": [
{
"date": "2026-10-20",
"close_at": "2026-10-20T16:00:00+08:00"
}
]
},
"fields": {
"cash": {
"path": "cash",
"complete_path": "coverage.cash",
"complete_value": "complete",
"required": [
"asset",
"quantity"
],
"numeric": [
"quantity"
],
"empty_proof": {
"query_ok": "proof.cash.query_ok",
"row_count": "proof.cash.row_count",
"pagination_complete": "proof.cash.pagination_complete"
}
},
"positions": {
"path": "positions",
"complete_path": "coverage.positions",
"complete_value": "complete",
"required": [
"asset",
"quantity"
],
"numeric": [
"quantity"
],
"empty_proof": {
"query_ok": "proof.positions.query_ok",
"row_count": "proof.positions.row_count",
"pagination_complete": "proof.positions.pagination_complete"
}
},
"orders": {
"path": "orders",
"complete_path": "coverage.orders",
"complete_value": "complete",
"required": [
"asset",
"quantity"
],
"numeric": [
"quantity"
],
"empty_proof": {
"query_ok": "proof.orders.query_ok",
"row_count": "proof.orders.row_count",
"pagination_complete": "proof.orders.pagination_complete"
}
},
"fills": {
"path": "fills",
"complete_path": "coverage.fills",
"complete_value": "complete",
"required": [
"asset",
"quantity"
],
"numeric": [
"quantity"
],
"empty_proof": {
"query_ok": "proof.fills.query_ok",
"row_count": "proof.fills.row_count",
"pagination_complete": "proof.fills.pagination_complete"
}
}
}
},
{
"platform": "firstrade",
"status": "unverified"
},
{
"platform": "schwab",
"status": "unverified"
},
{
"platform": "ibkr",
"status": "unverified"
},
{
"platform": "binance",
"status": "unverified"
},
{
"platform": "alpaca",
"status": "unverified"
},
{
"platform": "qmt",
"status": "unverified"
},
{
"platform": "ftmo",
"status": "unverified"
}
]
}
6 changes: 6 additions & 0 deletions python/Dockerfile.snapshot-completeness
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.12-slim@sha256:e70237b264442328526ff29d6b3c43bd8abcf6cbab78a949322fb69b7d91d6b6
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY python/scripts/snapshot_completeness.py /app/snapshot_completeness.py
USER 65532:65532
CMD ["python", "/app/snapshot_completeness.py"]
Loading
Loading