Skip to content

feat(compliance): attest the instrument wrapper, not just the underlying - #223

Merged
eaitbrahim merged 1 commit into
mainfrom
feat/instrument-attestation
Aug 11, 2026
Merged

feat(compliance): attest the instrument wrapper, not just the underlying#223
eaitbrahim merged 1 commit into
mainfrom
feat/instrument-attestation

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #202.

The gap

AssetAttestation is keyed on asset — a base-leg symbol — so it can only ever describe the underlying. The honest attestation for the underlying of a BTC CFD is character-for-character BTC's existing, already-admitted spot one (sector=payments, backing=native, pays_yield=False), so the screen admitted it. Swap financing, leverage and counterparty exposure are properties of the contract, and no amount of care taken over the asset claim could ever surface them.

The decision

Keep AssetAttestation exactly as-is — a per-asset claim about the underlying — and add a separate InstrumentAttestation keyed per (venue, product_id) carrying a wrapper. Admission requires both. Sector/backing/yield are facts about the underlying; "what contract is this listing" is a fact about a venue's product.

Keyed on product_id, not (venue, asset), because Coinbase — the one venue keel already uses — lists both BTC-USD (spot) and BTC-PERP-USD against the same base leg. A per-asset wrapper claim would be factually wrong today, not merely imprecise once a second venue lands.

The wrapper is attested, never computed. The id's shape cannot answer it: a cTrader CFD spells itself BTC-USD and parse_spot_product_id reads that as well-formed spot — that is the gap. The venue's own product_type is its self-report about its own product, which makes it excellent input to the human's source and unacceptable as the claim itself.

spot_instrument (the id's grammar) and instrument_wrapper (the human's claim) are complementary and deliberately not merged — both fire for a derivative-shaped id attested as spot, so neither failure can hide behind the other.

Rejected alternatives

  • Add a wrapper axis to AssetAttestation — it is keyed per asset, so it cannot express that BTC-USD and BTC-PERP-USD are different contracts on the same venue.
  • Auto-attest from Coinbase product_type — that is the venue self-reporting on its own product. It is evidence to cite, not the claim.
  • Derive the wrapper from the id shape — a CFD spells itself exactly like spot. This is precisely why the gap exists.
  • A per-trade rail in guards.py — the screen is a curation gate by charter, and guards.check has no broker/venue handle. That is the dead-gate pattern capabilities.py explicitly warns about.
  • Backfill spot for the current allowlist — see below.

⚠️ Accepted consequence: REJECT until attested

keel assets screen now reports REJECT (instrument_wrapper: UNATTESTED) for every product — including BTC-USD, ETH-USD and PAXG-USD — until the operator runs keel assets attest-instrument once per product.

This is the fail-closed default and it is intended. It does not affect live trading: screen_asset has exactly one production caller, and rail 1 gates live buys on config.allowlist directly, not on the screen.

There is deliberately no backfill, no default-to-spot, and no auto-attestation. Seeding spot rows for currently-allowlisted products would fabricate exactly the claim this gap is about, for the three products the project is most likely to stop questioning. The failure message names the exact command that fixes it — that is how the operator is told.

$ keel assets attest-instrument --product BTC-USD --wrapper spot \
    --source "coinbase product spec" --attested-by "<name>"

Not waivable

WAIVABLE_CRITERIA stays exactly frozenset({"history"}), as #202 requires — a waiver here would waive the charter, not a threshold. A stray screen_exceptions row for instrument_wrapper is dropped by the up-front filter and pinned by a test.

instrument_wrapper is deliberately not in DATA_DERIVED_FAILURES: like settlement, it reads an attestation and never touches candles, so it stays a real, assessable verdict at zero bars.

Changes

  • screen.pyInstrumentAttestation, KNOWN_WRAPPERS, MarketFacts.venue (no default, same fail-open rationale as product_id), screen_asset(..., instrument=None) failing closed.
  • Schema v10instrument_attestations keyed (venue, product_id); migration is stamp-only, no backfill.
  • Repositoryupsert/get/get_all trio mirroring the asset-attestation one.
  • CLI — assets attest-instrument, _VENUE constant (the broker-port migration replaces it with the adapter's venue id), lookup in _screen_product so all three callers inherit the check with no per-caller wiring, and assets list rendering.

Gates

ruff check keel tests packages scripts   All checks passed!
mypy                                     Success: no issues found in 217 source files
pytest -q                                2412 passed, 1 skipped
migration_smoke.py                       fresh and downgraded both reach schema 10

The one skip is the expected conformance candles probe (suite.py:254).

🤖 Generated with Claude Code

`AssetAttestation` is keyed on a base-leg symbol, so it can only ever describe
the UNDERLYING. The honest attestation for the underlying of a BTC CFD is
character-for-character BTC's existing spot one -- sector=payments,
backing=native, pays_yield=False -- so the curation screen admitted it. Leverage,
swap financing and counterparty exposure are properties of the CONTRACT, and no
amount of care taken over the asset claim could ever surface them.

Adds a separate `InstrumentAttestation` keyed per `(venue, product_id)` carrying
a `wrapper` field. Admission now requires BOTH claims. Keyed on the product id
rather than `(venue, asset)` because Coinbase -- the one venue keel already uses
-- lists both `BTC-USD` and `BTC-PERP-USD` against the same base leg, so a
per-asset wrapper claim would be factually wrong today, not merely imprecise once
a second venue lands.

The wrapper is attested, never computed. The id's shape cannot answer it: a
cTrader CFD spells itself `BTC-USD` and `parse_spot_product_id` reads that as
well-formed spot, which IS the gap. The venue's own `product_type` is its
self-report about its own product, which makes it evidence to cite in `source`
and not a substitute for a human's claim. `spot_instrument` (grammar) and
`instrument_wrapper` (claim) are complementary and both fire for a
derivative-shaped id attested as spot.

`WAIVABLE_CRITERIA` stays `frozenset({"history"})` -- a waiver here would waive
the charter, not a threshold. `instrument_wrapper` is deliberately NOT in
`DATA_DERIVED_FAILURES`: like `settlement` it reads an attestation and never
touches candles, so it stays assessable at zero bars.

ACCEPTED CONSEQUENCE: `keel assets screen` now reports REJECT
(`instrument_wrapper: UNATTESTED`) for every product, including BTC-USD/ETH-USD/
PAXG-USD, until the operator runs `keel assets attest-instrument` once per
product. That is the fail-closed default and it is intended. Live trading is
unaffected -- rail 1 gates live buys on `config.allowlist` directly, not on the
screen. There is deliberately no backfill, no default-to-spot and no
auto-attestation from venue metadata; the failure message names the exact command.

Schema v10 adds `instrument_attestations`; the migration is stamp-only, because
seeding `spot` rows for currently-allowlisted products would fabricate exactly
the claim this gap is about, for the products the project is most likely to stop
questioning.

Closes #202

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eaitbrahim eaitbrahim added the compliance Shariah/compliance screening or policy (Compliance & rails) label Aug 11, 2026
@eaitbrahim
eaitbrahim merged commit b826a08 into main Aug 11, 2026
1 check passed
@eaitbrahim
eaitbrahim deleted the feat/instrument-attestation branch August 11, 2026 11:22
@eaitbrahim eaitbrahim mentioned this pull request Aug 11, 2026
eaitbrahim added a commit that referenced this pull request Aug 11, 2026
…the CTS scoring fix (#241)

A minor bump, not a patch, for three reasons that each require operator action or
change behaviour the deployment is currently relying on.

SCHEMA. `SCHEMA_VERSION` goes 9 -> 10 (#223). Both deployed databases are at 9 and
must be migrated before this build can use them.

BEHAVIOUR REQUIRING OPERATOR ACTION. #223 adds a second attested claim -- what
CONTRACT a venue listing is, not only what the underlying asset is. It fails closed
with no backfill, deliberately, so after this lands `keel assets screen` REJECTS
every product with `instrument_wrapper: UNATTESTED` until `keel assets
attest-instrument` is run once per product. Live trading is unaffected: rail 1 gates
buys on `config.allowlist`, not on the screen.

LIVE SCORING CHANGED. #227 fixed `is_round_number`, which returned True for every
2dp-quoted price and so handed BTC/ETH/PAXG a free CTS point on every bar. Scores on
those three assets are genuinely lower under this build than under 0.5.7.

Also ships: the Robinhood crypto adapter behind the broker port (#216/#218/#222/#229,
not wired to the live path), the TUI activity feed (#235/#237), the CTS factor
collinearity study (#224), `Preview.synthetic` at the confirm gate (#221), rail 9
seeing a bracket's own stop (#212), and CI gating merges on the `test` check
(#234/#238).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compliance Shariah/compliance screening or policy (Compliance & rails)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AssetAttestation cannot express an instrument wrapper, so the curation screen cannot see a CFD

1 participant