feat(compliance): attest the instrument wrapper, not just the underlying - #223
Merged
Conversation
`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>
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #202.
The gap
AssetAttestationis keyed onasset— 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
AssetAttestationexactly as-is — a per-asset claim about the underlying — and add a separateInstrumentAttestationkeyed per(venue, product_id)carrying awrapper. 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 bothBTC-USD(spot) andBTC-PERP-USDagainst 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-USDandparse_spot_product_idreads that as well-formed spot — that is the gap. The venue's ownproduct_typeis its self-report about its own product, which makes it excellent input to the human'ssourceand unacceptable as the claim itself.spot_instrument(the id's grammar) andinstrument_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
AssetAttestation— it is keyed per asset, so it cannot express thatBTC-USDandBTC-PERP-USDare different contracts on the same venue.product_type— that is the venue self-reporting on its own product. It is evidence to cite, not the claim.guards.py— the screen is a curation gate by charter, andguards.checkhas no broker/venue handle. That is the dead-gate patterncapabilities.pyexplicitly warns about.spotfor the current allowlist — see below.keel assets screennow reportsREJECT (instrument_wrapper: UNATTESTED)for every product — includingBTC-USD,ETH-USDandPAXG-USD— until the operator runskeel assets attest-instrumentonce per product.This is the fail-closed default and it is intended. It does not affect live trading:
screen_assethas exactly one production caller, and rail 1 gates live buys onconfig.allowlistdirectly, not on the screen.There is deliberately no backfill, no default-to-spot, and no auto-attestation. Seeding
spotrows 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.Not waivable
WAIVABLE_CRITERIAstays exactlyfrozenset({"history"}), as #202 requires — a waiver here would waive the charter, not a threshold. A strayscreen_exceptionsrow forinstrument_wrapperis dropped by the up-front filter and pinned by a test.instrument_wrapperis deliberately not inDATA_DERIVED_FAILURES: likesettlement, it reads an attestation and never touches candles, so it stays a real, assessable verdict at zero bars.Changes
screen.py—InstrumentAttestation,KNOWN_WRAPPERS,MarketFacts.venue(no default, same fail-open rationale asproduct_id),screen_asset(..., instrument=None)failing closed.instrument_attestationskeyed(venue, product_id); migration is stamp-only, no backfill.Repository—upsert/get/get_alltrio mirroring the asset-attestation one.assets attest-instrument,_VENUEconstant (the broker-port migration replaces it with the adapter's venue id), lookup in_screen_productso all three callers inherit the check with no per-caller wiring, andassets listrendering.Gates
The one skip is the expected conformance candles probe (
suite.py:254).🤖 Generated with Claude Code