Copy execution transparency (Phase 3)
Per-copy execution facts — latency, realized slippage, fill vs failure — captured every copy attempt,
surfaced as per-profile transparency report. Off by default; enable with
App:Copy:TransparencyEnabled=true. When off, copy engine byte-for-byte unchanged: host emits
to no-op sink, nothing written.
How it works
CopyEngineHost ──Record(fact)──▶ ICopyEventSink
│
(transparency off) NullCopyEventSink → discards (default; zero hot-path cost)
(transparency on) ChannelCopyEventSink → bounded in-memory channel (DropOldest)
│
▼
CopyExecutionDrainer (BackgroundService)
│ batches every App drain interval
▼
CopyExecution append-only table ◀── GET /api/copy/profiles/{id}/transparency
- Hot path stays free of I/O. Host calls
ICopyEventSink.Record(...)— non-blocking, never-throwing enqueue. Never awaits, never touches DB, never blocks order execution. - Loss preferred over back-pressure. Channel bounded (
CopyExecutionChannelCapacity) withDropOldest: if DB drainer stalls, oldest transparency rows dropped rather than delay a copy. Transparency = best-effort telemetry, not trading dependency. - Out-of-band persistence.
CopyExecutionDrainerdrains channel in batches (CopyExecutionDrainBatchSize) onCopyExecutionDrainInterval, writesCopyExecutionrows through scopedDataContext. Final flush on shutdown. - Facts, not commands.
CopyExecution= append-only log (likeInstanceLog/AuditLog), not aggregate. Read model queries it directly (CQRS-lite), aggregates in memory.
What is recorded
One CopyExecutionRecord per copy attempt on one destination:
| Kind | When | Carries |
|---|---|---|
Opened | copy order placed | symbol, side, wire volume, master price, realized slippage (points), latency (ms) |
Failed | copy open threw/rejected | symbol, side, master volume/price, latency, failure reason (exception type) |
(Closed/Skipped/Reconciled exist in enum for future expansion.)
The report
GET /api/copy/profiles/{id}/transparency (owner-scoped) returns, over most recent 500 facts:
- Summary — total, opened, failed, fill rate, average latency (ms), average slippage (points).
- Recent — raw recent facts (destination, source position, symbol, side, volume, master price, slippage, latency, reason, timestamp).
Configuration (App:Copy)
| Setting | Default | Effect |
|---|---|---|
TransparencyEnabled | false | Turn per-copy fact capture + drainer on for node. |
Channel capacity, drain batch size, drain interval = CopyDefaults constants
(CopyExecutionChannelCapacity / CopyExecutionDrainBatchSize / CopyExecutionDrainInterval).
Tests
- Unit (
CopyTransparencyTests) — successful open emitsOpenedfact with right symbol/side/volume/latency; rejected open emitsFailedfact with reason. Driven through capturing sink. - Integration (
CopyExecutionDrainerTests, real Postgres) — drainer persists buffered facts toCopyExecutionlog; empty sink writes nothing. - DST — host change fire-and-forget with no-op default sink, so deterministic copy stress suite stays green (23/23).