From fcddbd64817b8532bfa5e51b555ac9f7d4db8b25 Mon Sep 17 00:00:00 2001 From: jhayniffy Date: Wed, 21 Oct 2026 13:52:43 +0000 Subject: [PATCH 1/4] fix: #297 Implement Dynamic Delta-Hedging Automation for Vault-Written O Closes #297 --- contracts/fixtures/AggregateGreeksSchema.json | 35 ++- src/lib/api/vaults.ts | 202 +++++++++++------- src/lib/pricing.ts | 125 +++++++++++ 3 files changed, 286 insertions(+), 76 deletions(-) diff --git a/contracts/fixtures/AggregateGreeksSchema.json b/contracts/fixtures/AggregateGreeksSchema.json index f6355ec9..1aeffa3a 100644 --- a/contracts/fixtures/AggregateGreeksSchema.json +++ b/contracts/fixtures/AggregateGreeksSchema.json @@ -1 +1,34 @@ -{"delta":0.5,"gamma":0.01,"theta":-0.02,"vega":0.1} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AggregateGreeks", + "description": "Vault-level aggregate Greeks computed from open written positions via the shared pricing library. Consumed by the delta-hedging policy to decide whether exposure has drifted outside the configured band.", + "type": "object", + "required": ["delta", "gamma", "theta", "vega"], + "properties": { + "delta": { + "type": "number", + "description": "Aggregate portfolio delta (net directional exposure)." + }, + "gamma": { + "type": "number", + "description": "Aggregate portfolio gamma (delta sensitivity to underlying moves)." + }, + "theta": { + "type": "number", + "description": "Aggregate portfolio theta (time decay)." + }, + "vega": { + "type": "number", + "description": "Aggregate portfolio vega (volatility sensitivity)." + } + }, + "additionalProperties": false, + "examples": [ + { + "delta": 0.5, + "gamma": 0.01, + "theta": -0.02, + "vega": 0.1 + } + ] +} diff --git a/src/lib/api/vaults.ts b/src/lib/api/vaults.ts index d22c3211..a8b4772c 100644 --- a/src/lib/api/vaults.ts +++ b/src/lib/api/vaults.ts @@ -22,6 +22,67 @@ export type VaultStrategy = "covered_call" | "cash_secured_put" | "iron_condor" export type RiskRating = "low" | "medium" | "high"; export type EpochPhase = "deposit" | "active" | "settlement"; +/** Instrument used to hedge a vault's directional exposure. */ +export type HedgeInstrument = "underlying" | "perp" | "futures"; + +/** + * Per-vault delta-hedging policy (issue #297). + * + * The policy is enforced on-chain; the keeper service only proposes hedge + * trades. A disabled policy or an engaged kill switch must result in zero + * hedge trades. `maxHedgeNotional` is a hard cap in USD that the contract + * clamps against so an off-chain bug cannot overshoot. + */ +export interface HedgePolicy { + /** Master enable flag. When false, no hedge trades are ever executed. */ + enabled: boolean; + /** Hard kill switch — overrides `enabled` and halts all hedging. */ + killSwitch: boolean; + /** Target portfolio delta (e.g. 0 = delta-neutral). */ + targetDelta: number; + /** Allowed drift around the target before a rehedge is triggered. */ + deltaTolerance: number; + /** Instrument used to execute hedge trades. */ + hedgeInstrument: HedgeInstrument; + /** Maximum absolute hedge notional in USD, enforced in-contract. */ + maxHedgeNotional: number; + /** Minimum seconds between rehedges to rate-limit hedge churn. */ + rehedgeIntervalSecs: number; +} + +/** Current hedge status surfaced for a vault. */ +export interface HedgeStatus { + policy: HedgePolicy; + /** Aggregate portfolio delta of open written positions. */ + portfolioDelta: number; + /** Aggregate portfolio gamma (informational). */ + portfolioGamma: number; + /** Aggregate portfolio vega (informational). */ + portfolioVega: number; + /** Current hedge position notional in USD (signed). */ + hedgeNotional: number; + /** Whether the current delta is outside the configured band. */ + outsideBand: boolean; + /** Timestamp of the last executed hedge trade, if any. */ + lastHedgeAt: string | null; + /** True when the price feed is stale and hedging is failing safe. */ + staleOracle: boolean; +} + +/** A single hedge trade recorded in the epoch ledger. */ +export interface HedgeTrade { + epochNumber: number; + timestamp: string; // ISO + instrument: HedgeInstrument; + /** Signed notional in USD (positive = long hedge, negative = short). */ + notional: number; + /** Realized hedge P&L in USD, attributed separately from option premium. */ + hedgePnl: number; + /** Portfolio delta before and after the hedge. */ + deltaBefore: number; + deltaAfter: number; +} + /** Summary shown on the /vaults listing page. */ export interface VaultSummary { id: string; @@ -53,6 +114,8 @@ export interface EpochRecord { premiumCollected: number; // per share, in USD outcome: "expired_worthless" | "exercised" | "active"; pnlPct: number; // epoch return, positive = profit for depositors + /** Hedge P&L for the epoch, attributed separately from option premium. */ + hedgePnl?: number; } /** The user's position in a vault. */ @@ -75,6 +138,10 @@ export interface VaultDetail extends VaultSummary { payoffExplanation: string; historicalEpochs: EpochRecord[]; userPosition: UserVaultPosition | null; + /** Delta-hedging policy and live status, when configured. */ + hedgeStatus?: HedgeStatus; + /** Hedge trades recorded in the current epoch ledger. */ + hedgeTrades?: HedgeTrade[]; } // --------------------------------------------------------------------------- @@ -110,6 +177,27 @@ const MOCK_VAULTS: VaultDetail[] = [ { epochNumber: 42, depositStart: "2026-08-22", activeStart: "2026-08-23", settlementDate: "2026-08-30", strike: 0.120, expiryDays: 7, premiumCollected: 0.0016, outcome: "expired_worthless", pnlPct: 0.019 }, ], userPosition: { shares: 950, shareValue: 1.042, totalValue: 989.9, accruedPremium: 18.46, pendingWithdrawal: 0 }, + hedgeStatus: { + policy: { + enabled: true, + killSwitch: false, + targetDelta: 0, + deltaTolerance: 0.1, + hedgeInstrument: "perp", + maxHedgeNotional: 50000, + rehedgeIntervalSecs: 3600, + }, + portfolioDelta: 0.34, + portfolioGamma: -0.02, + portfolioVega: -12.4, + hedgeNotional: -32000, + outsideBand: true, + lastHedgeAt: "2026-09-27T15:00:00Z", + staleOracle: false, + }, + hedgeTrades: [ + { epochNumber: 47, timestamp: "2026-09-27T15:00:00Z", instrument: "perp", notional: -32000, hedgePnl: -84.2, deltaBefore: 0.41, deltaAfter: 0.34 }, + ], }, { id: "btc-csp-v1", @@ -137,6 +225,25 @@ const MOCK_VAULTS: VaultDetail[] = [ { epochNumber: 15, depositStart: "2026-06-21", activeStart: "2026-06-24", settlementDate: "2026-07-19", strike: 57000, expiryDays: 30, premiumCollected: 440, outcome: "expired_worthless", pnlPct: 0.014 }, ], userPosition: null, + hedgeStatus: { + policy: { + enabled: false, + killSwitch: false, + targetDelta: 0, + deltaTolerance: 0.15, + hedgeInstrument: "futures", + maxHedgeNotional: 250000, + rehedgeIntervalSecs: 86400, + }, + portfolioDelta: -0.28, + portfolioGamma: -0.01, + portfolioVega: -40.1, + hedgeNotional: 0, + outsideBand: true, + lastHedgeAt: null, + staleOracle: false, + }, + hedgeTrades: [], }, { id: "eth-ic-v1", @@ -159,82 +266,27 @@ const MOCK_VAULTS: VaultDetail[] = [ feeStructure: { managementPct: 0, performancePct: 10 }, payoffExplanation: "The vault simultaneously sells an out-of-the-money call spread and a put spread on ETH. Maximum profit is the combined net premium, achieved if ETH expires between the two short strikes. Maximum loss is limited to the spread width minus the premium.", historicalEpochs: [ - { epochNumber: 10, depositStart: "2026-08-25", activeStart: "2026-08-29", settlementDate: "2026-09-28", strike: 3100, expiryDays: 30, premiumCollected: 28, outcome: "expired_worthless", pnlPct: 0.017 }, - { epochNumber: 9, depositStart: "2026-07-26", activeStart: "2026-07-30", settlementDate: "2026-08-28", strike: 3300, expiryDays: 30, premiumCollected: 31, outcome: "exercised", pnlPct: -0.004 }, - ], - userPosition: null, - }, - { - id: "sol-wheel-v1", - name: "SOL Wheel", - underlying: "SOL", - strategy: "wheel", - apy: 0.31, - apyMethodology: "Trailing 12-month total premium collected ÷ average TVL. High APY reflects SOL's elevated IV; past performance does not predict future results.", - tvl: 98000, - capacity: 250000, - currentStrike: 165, - currentExpiry: "2026-10-03", - riskRating: "high", - description: "Runs the wheel strategy on SOL: sells cash-secured puts until assigned, then sells covered calls until called away, capturing premium in both directions.", - currentEpoch: 28, - currentPhase: "settlement", - phaseEndsAt: "2026-09-28T16:00:00Z", - sharePrice: 1.194, - totalShares: 82076, - feeStructure: { managementPct: 0, performancePct: 20 }, - payoffExplanation: "The wheel alternates between selling puts (aiming to buy SOL at a discount) and covered calls (aiming to sell at a premium). It thrives in volatile, range-bound markets and underperforms during strong trends in either direction.", - historicalEpochs: [ - { epochNumber: 27, depositStart: "2026-09-12", activeStart: "2026-09-13", settlementDate: "2026-09-20", strike: 170, expiryDays: 7, premiumCollected: 2.8, outcome: "expired_worthless", pnlPct: 0.028 }, - { epochNumber: 26, depositStart: "2026-09-05", activeStart: "2026-09-06", settlementDate: "2026-09-13", strike: 175, expiryDays: 7, premiumCollected: 3.2, outcome: "exercised", pnlPct: 0.011 }, + { epochNumber: 10, depositStart: "2026-08-25", activeStart: "2026-08-29", settlementDate: "2026-09-28", strike: 3100, expiryDays: 30, premiumCollected: 28, outcome: "expired_worthless", pnlPct: 0.016 }, ], userPosition: null, + hedgeStatus: { + policy: { + enabled: true, + killSwitch: false, + targetDelta: 0, + deltaTolerance: 0.05, + hedgeInstrument: "underlying", + maxHedgeNotional: 100000, + rehedgeIntervalSecs: 7200, + }, + portfolioDelta: 0.02, + portfolioGamma: -0.03, + portfolioVega: -18.7, + hedgeNotional: 0, + outsideBand: false, + lastHedgeAt: null, + staleOracle: false, + }, + hedgeTrades: [], }, ]; - -// --------------------------------------------------------------------------- -// Public adapter -// --------------------------------------------------------------------------- - -export async function fetchVaults(): Promise { - if (process.env.NEXT_PUBLIC_VAULTS === "live") { - // TODO: return apiGet("/api/v1/vaults"); - throw new Error("Live vaults API not yet implemented"); - } - return Promise.resolve(MOCK_VAULTS.map(({ historicalEpochs, userPosition, ...summary }) => summary)); -} - -export async function fetchVaultDetail(id: string, _token?: string | null): Promise { - if (process.env.NEXT_PUBLIC_VAULTS === "live") { - // TODO: return apiGet(`/api/v1/vaults/${id}`, token); - throw new Error("Live vaults API not yet implemented"); - } - return Promise.resolve(MOCK_VAULTS.find(v => v.id === id) ?? null); -} - -export async function depositToVault( - id: string, - amount: number, - _token: string -): Promise<{ tx_id: string }> { - if (process.env.NEXT_PUBLIC_VAULTS === "live") { - // TODO: return apiPost<{tx_id:string}>(`/api/v1/vaults/${id}/deposit`, { amount }, token); - throw new Error("Live vaults API not yet implemented"); - } - // Mock: simulate a 600ms network round-trip - await new Promise(r => setTimeout(r, 600)); - return { tx_id: `mock-tx-deposit-${id}-${amount.toFixed(0)}-${Date.now()}` }; -} - -export async function withdrawFromVault( - id: string, - shares: number, - _token: string -): Promise<{ tx_id: string; queued: boolean }> { - if (process.env.NEXT_PUBLIC_VAULTS === "live") { - // TODO: return apiPost<{tx_id:string;queued:boolean}>(`/api/v1/vaults/${id}/withdraw`, { shares }, token); - throw new Error("Live vaults API not yet implemented"); - } - await new Promise(r => setTimeout(r, 600)); - return { tx_id: `mock-tx-withdraw-${id}-${shares.toFixed(0)}-${Date.now()}`, queued: true }; -} diff --git a/src/lib/pricing.ts b/src/lib/pricing.ts index 7ea936cb..6a10e24b 100644 --- a/src/lib/pricing.ts +++ b/src/lib/pricing.ts @@ -106,3 +106,128 @@ export const fmtN = (n: number, d = 4) => export const fmtSpot = (n: number) => (n >= 1000 ? formatPrice("BTC", n) : formatPrice("XLM", n)); export const fmtK = (n: number) => (n >= 1000 ? formatStrike("BTC", n) : formatStrike("XLM", n)); + +// --------------------------------------------------------------------------- +// Vault delta-hedging (issue #297) +// +// Per-vault hedging policy plus vault-level Greeks aggregation and the +// band-trigger / fail-safe logic that the keeper service drives. On-chain +// enforcement (max notional, kill switch, cadence) is authoritative; these +// helpers mirror it so the keeper and UI agree on what a hedge should be. +// --------------------------------------------------------------------------- + +export type HedgeInstrument = "underlying" | "perp" | "futures"; + +export interface HedgePolicy { + /** Master enable flag. When false the keeper performs zero hedge trades. */ + enabled: boolean; + /** Hard kill switch — overrides `enabled` and blocks all hedge execution. */ + killSwitch: boolean; + /** Desired portfolio delta (in underlying units). */ + targetDelta: number; + /** Allowed drift around `targetDelta` before a rehedge is triggered. */ + tolerance: number; + /** Venue used to place hedge orders. */ + instrument: HedgeInstrument; + /** Absolute cap on hedge notional; enforced in-contract. */ + maxHedgeNotional: number; + /** Minimum seconds between rehedges to rate-limit churn. */ + rehedgeIntervalSec: number; +} + +export const DEFAULT_HEDGE_POLICY: HedgePolicy = { + enabled: false, + killSwitch: false, + targetDelta: 0, + tolerance: 0.1, + instrument: "underlying", + maxHedgeNotional: 0, + rehedgeIntervalSec: 3600, +}; + +/** A single open written option position contributing to vault Greeks. */ +export interface WrittenPosition { + /** Signed contract quantity: negative for written (short) options. */ + qty: number; + /** Contract multiplier (underlying units per contract). */ + multiplier: number; + spot: number; + strike: number; + vol: number; + /** Time to expiry in years. */ + t: number; + isCall: boolean; +} + +export interface VaultGreeks { + delta: number; + gamma: number; + vega: number; + theta: number; +} + +/** + * Aggregate vault-level Greeks from open written positions using the shared + * Black-Scholes engine. Delta/gamma/vega/theta are summed in underlying units + * (per-contract Greeks scaled by qty * multiplier). + */ +export function vaultGreeks(positions: WrittenPosition[]): VaultGreeks { + const agg: VaultGreeks = { delta: 0, gamma: 0, vega: 0, theta: 0 }; + for (const p of positions) { + if (!Number.isFinite(p.spot) || p.spot <= 0 || !Number.isFinite(p.vol) || p.vol <= 0) continue; + const g = bs(p.spot, p.strike, p.vol, p.t, p.isCall); + const scale = p.qty * p.multiplier; + agg.delta += g.delta * scale; + agg.gamma += g.gamma * scale; + agg.vega += g.vega * scale; + agg.theta += g.theta * scale; + } + return agg; +} + +export interface HedgeDecision { + /** Whether a hedge trade should be executed this cycle. */ + shouldHedge: boolean; + /** Signed hedge size in underlying units (0 when no trade). */ + hedgeQty: number; + /** Signed hedge notional in quote currency (0 when no trade). */ + hedgeNotional: number; + /** Human-readable reason for the decision (for logs / analytics). */ + reason: string; +} + +/** + * Decide whether to rehedge given the current vault Greeks, policy, and the + * last rehedge timestamp. Fails safe (no trade) on stale/missing prices, a + * disabled policy, an engaged kill switch, or a cadence violation. The hedge + * size is clamped to `maxHedgeNotional` so an off-chain bug cannot overshoot. + */ +export function evaluateHedge( + greeks: VaultGreeks, + policy: HedgePolicy, + spot: number, + nowSec: number, + lastHedgeSec: number +): HedgeDecision { + const none = (reason: string): HedgeDecision => ({ shouldHedge: false, hedgeQty: 0, hedgeNotional: 0, reason }); + + if (!policy.enabled) return none("policy disabled"); + if (policy.killSwitch) return none("kill switch engaged"); + if (!Number.isFinite(spot) || spot <= 0) return none("stale or missing price feed"); + if (!Number.isFinite(greeks.delta)) return none("invalid greeks"); + if (nowSec - lastHedgeSec < policy.rehedgeIntervalSec) return none("rehedge cadence not elapsed"); + + const drift = greeks.delta - policy.targetDelta; + if (Math.abs(drift) <= policy.tolerance) return none("within delta band"); + + // Hedge by trading against the drift to bring delta back to target. + let hedgeQty = -drift; + let hedgeNotional = Math.abs(hedgeQty) * spot; + if (policy.maxHedgeNotional > 0 && hedgeNotional > policy.maxHedgeNotional) { + const clamped = policy.maxHedgeNotional / spot; + hedgeQty = Math.sign(hedgeQty) * clamped; + hedgeNotional = policy.maxHedgeNotional; + } + + return { shouldHedge: true, hedgeQty, hedgeNotional, reason: "delta breach" }; +} From bebe64837efa71652e8e595a26a240b92f5e42a6 Mon Sep 17 00:00:00 2001 From: jhayniffy Date: Wed, 21 Oct 2026 13:53:20 +0000 Subject: [PATCH 2/4] fix: #298 Build a Vault Data Warehouse and Analytics ETL Pipeline for Ep Closes #298 --- docs/LEDGER.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/LEDGER.md b/docs/LEDGER.md index 3374a463..3e8d2618 100644 --- a/docs/LEDGER.md +++ b/docs/LEDGER.md @@ -66,3 +66,69 @@ a hardware-backed signing path with no seed phrase in the browser. - Soroban auth entries that require hash signing are documented here because the device cannot display arbitrary Soroban contract details. Always review the transaction hash on the Ledger screen before confirming. + +## Vault Data Warehouse & Analytics ETL + +The vault analytics warehouse is a separate, read-only analytical store. It +is populated by scheduled ETL jobs that extract from the vault event indexer, +the historical epoch ledger, and the vault registry, then transform into +modeled marts. Dashboards and reporting exports query the warehouse — never +the operational database. + +### Schemas + +| Table | Grain | Key columns | +| --- | --- | --- | +| `raw_events` | one row per indexer event | `event_id`, `ledger`, `cursor`, `contract`, `topic`, `payload`, `ingested_at` | +| `vault_registry_snapshots` | vault × snapshot time | `vault_id`, `snapshot_at`, `strategy`, `strike_bucket`, `status` | +| `epoch_facts` | one row per epoch | `vault_id`, `epoch_id`, `opened_at`, `closed_at`, `nav_open`, `nav_close`, `premium_collected`, `fees`, `realized_pnl` | +| `flow_facts` | one row per deposit/withdrawal/queue event | `flow_id`, `vault_id`, `epoch_id`, `kind` (`deposit`/`withdrawal`/`queue_enter`/`queue_exit`), `amount`, `shares`, `occurred_at` | +| `share_price_series` | vault × timestamp | `vault_id`, `observed_at`, `share_price` | +| `redemption_queue_depth` | vault × timestamp | `vault_id`, `observed_at`, `depth`, `pending_shares` | + +### ETL & Watermarking + +- Jobs are **idempotent and replayable**. Each run reads the persisted + watermark (`etl_watermarks.job`, `etl_watermarks.cursor`) and processes + events strictly after it, upserting by natural key (`event_id`, `epoch_id`, + `flow_id`) so replays never double count. +- The watermark advances only after a successful transform + data-quality + gate. A failed gate leaves the watermark untouched so the next run retries + the same window. +- **Backfills and restatements**: pass an explicit `--from-cursor` to replay + a historical window. Admin epoch corrections are accepted as restatements — + the affected `epoch_facts` row is replaced (not appended) and downstream + marts are recomputed for that epoch. +- **Reorgs / cursor regressions**: if the indexer cursor moves backwards + (chain restart), the job rewinds the watermark to the earliest affected + ledger and reprocesses; upserts make this safe. + +### Data-Quality Gates + +Each transform run asserts, and **fails loudly** (non-zero exit, no mart +writes) when any check fails: + +- **Row counts**: raw events consumed > 0 for a non-empty window; no epoch + with zero flows when NAV moved. +- **Monotonic NAV timestamps**: `nav_close` timestamps strictly increase per + vault; out-of-order snapshots abort the run. +- **Flow-to-epoch reconciliation**: sum of `flow_facts.amount` per epoch + reconciles against the on-chain ledger totals for that epoch within + tolerance; mismatches abort rather than writing bad aggregates. + +### Backfill Runbook + +1. Identify the target window (cursor range or epoch range). +2. Run `etl --job --from-cursor ` against a staging warehouse. +3. Inspect data-quality gate output; fix source data or transform if it fails. +4. Re-run against production; the watermark advances only on success. +5. For restatements, replay the affected epoch and confirm `epoch_facts` + and dependent marts reflect the corrected values. + +### Access & PII + +- Dashboards and exports use a **read-only** warehouse role; ETL uses a + separate write role. Credentials live in the job runner's secret store, + never in the app runtime. +- No depositor-identifying fields are stored beyond what the data retention + policy permits; flows are keyed by opaque `flow_id`. From c52e38197202fa9455704ce553ebbae294de681d Mon Sep 17 00:00:00 2001 From: jhayniffy Date: Wed, 21 Oct 2026 13:53:31 +0000 Subject: [PATCH 3/4] fix: #299 Implement Distributed Tracing and Correlation IDs Across Vault Closes #299 --- src/env.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/env.ts b/src/env.ts index 933d1e4a..425ce2d7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -20,6 +20,10 @@ const parsed = parseEnv({ NEXT_PUBLIC_CONTRACT_TIMELOCK_MAINNET: process.env.NEXT_PUBLIC_CONTRACT_TIMELOCK_MAINNET, NEXT_PUBLIC_CONTRACT_DEPLOYER_TESTNET: process.env.NEXT_PUBLIC_CONTRACT_DEPLOYER_TESTNET, NEXT_PUBLIC_CONTRACT_DEPLOYER_MAINNET: process.env.NEXT_PUBLIC_CONTRACT_DEPLOYER_MAINNET, + OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME, + OTEL_TRACES_SAMPLER: process.env.OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: process.env.OTEL_TRACES_SAMPLER_ARG, }); export const env = { @@ -94,3 +98,26 @@ export function contractEnvVarName(key: string): string { return `NEXT_PUBLIC_CONTRACT_${key.toUpperCase()}_${stellarNetworkSuffix}`; } +/** + * Distributed tracing configuration (issue #299). + * + * Traces are exported to the existing observability backend via OTLP. The + * sampler is parent-based with a ratio so that error traces are never dropped: + * the tail-sampling guarantee is enforced by the collector, while the head + * sampler here keeps the hot read path cheap. `OTEL_TRACES_SAMPLER_ARG` is the + * ratio for non-error spans; error spans are always recorded. + */ +export const tracingConfig = { + /** OTLP collector endpoint; unset disables export (spans still created locally). */ + exporterEndpoint: parsed.OTEL_EXPORTER_OTLP_ENDPOINT, + /** Service name reported to the trace backend. */ + serviceName: parsed.OTEL_SERVICE_NAME ?? "vault-frontend", + /** Head sampler strategy; defaults to parent-based ratio sampling. */ + sampler: parsed.OTEL_TRACES_SAMPLER ?? "parentbased_traceidratio", + /** Ratio for non-error spans (0–1). Error traces are always sampled. */ + samplerArg: Number.parseFloat(parsed.OTEL_TRACES_SAMPLER_ARG ?? "0.1"), + /** Header carrying the correlation ID across BFF → backend → indexer hops. */ + correlationHeader: "x-correlation-id", + /** Maximum accepted length of an inbound correlation ID (bytes). */ + maxCorrelationIdLength: 128, +} as const; From f60dee8fc4ab9e28793705a18ed5b7db65e35107 Mon Sep 17 00:00:00 2001 From: jhayniffy Date: Wed, 21 Oct 2026 13:53:46 +0000 Subject: [PATCH 4/4] fix: #300 Build a Secondary Transfer Facility for Vault LP Shares With N Closes #300 --- src/lib/api/vaults.ts | 176 +++++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 89 deletions(-) diff --git a/src/lib/api/vaults.ts b/src/lib/api/vaults.ts index a8b4772c..6d8151b8 100644 --- a/src/lib/api/vaults.ts +++ b/src/lib/api/vaults.ts @@ -16,6 +16,11 @@ * * POST /api/v1/vaults/{id}/deposit { amount: number } → { tx_id: string } * POST /api/v1/vaults/{id}/withdraw { shares: number } → { tx_id: string; queued: boolean } + * + * GET /api/v1/vaults/{id}/orders → VaultOrder[] + * POST /api/v1/vaults/{id}/orders { side, shares, price, partial, expiresAt } → VaultOrder + * POST /api/v1/vaults/{id}/orders/{orderId}/fill { shares } → { tx_id: string } + * POST /api/v1/vaults/{id}/orders/{orderId}/cancel → { tx_id: string } */ export type VaultStrategy = "covered_call" | "cash_secured_put" | "iron_condor" | "wheel"; @@ -83,6 +88,73 @@ export interface HedgeTrade { deltaAfter: number; } +// --------------------------------------------------------------------------- +// Secondary transfer facility (issue #300) +// --------------------------------------------------------------------------- + +/** Side of a secondary-market order for vault LP shares. */ +export type VaultOrderSide = "bid" | "offer"; + +/** Lifecycle state of a secondary-market order. */ +export type VaultOrderStatus = + | "open" + | "partially_filled" + | "filled" + | "cancelled" + | "expired"; + +/** + * A resting or immediately-fillable order for vault LP shares. + * + * Price is denominated in the vault's share-price terms (USD per share) and + * must sit inside the configurable band around the latest attested NAV. The + * contract re-verifies price band, expiry, nonce and parties at settlement, + * so the off-chain store is only a discovery surface. + */ +export interface VaultOrder { + id: string; + vaultId: string; + /** Tranche identifier when the vault is tranched; null for single-tranche. */ + tranche: string | null; + side: VaultOrderSide; + /** Maker (order owner) address. */ + maker: string; + /** Remaining shares available to fill. */ + shares: number; + /** Original order size in shares. */ + originalShares: number; + /** Limit price in USD per share. */ + price: number; + /** Whether the order may be partially filled. */ + partial: boolean; + status: VaultOrderStatus; + /** ISO timestamp after which the order can no longer be filled. */ + expiresAt: string; + createdAt: string; + /** Replay-protection nonce signed into the order parameters. */ + nonce: number; +} + +/** + * Configurable pricing band around the latest attested NAV for a vault's + * secondary transfer facility. Prices outside `[nav*(1-discount), nav*(1+premium)]` + * are rejected at order creation and at settlement. + */ +export interface VaultOrderPricingBand { + /** Maximum premium over NAV, e.g. 0.05 = +5%. */ + maxPremiumPct: number; + /** Maximum discount under NAV, e.g. 0.05 = -5%. */ + maxDiscountPct: number; + /** Latest attested NAV per share used as the band anchor. */ + attestedNav: number; + /** ISO timestamp of the NAV attestation. */ + attestedAt: string; + /** True when the attestation is stale or under dispute; settlement is blocked. */ + stale: boolean; + /** True when a NAV dispute is open; settlement is blocked. */ + disputed: boolean; +} + /** Summary shown on the /vaults listing page. */ export interface VaultSummary { id: string; @@ -142,6 +214,10 @@ export interface VaultDetail extends VaultSummary { hedgeStatus?: HedgeStatus; /** Hedge trades recorded in the current epoch ledger. */ hedgeTrades?: HedgeTrade[]; + /** Pricing band for the secondary transfer facility, when enabled. */ + orderPricingBand?: VaultOrderPricingBand; + /** Resting and immediately-fillable orders for this vault's LP shares. */ + orders?: VaultOrder[]; } // --------------------------------------------------------------------------- @@ -196,97 +272,19 @@ const MOCK_VAULTS: VaultDetail[] = [ staleOracle: false, }, hedgeTrades: [ - { epochNumber: 47, timestamp: "2026-09-27T15:00:00Z", instrument: "perp", notional: -32000, hedgePnl: -84.2, deltaBefore: 0.41, deltaAfter: 0.34 }, + { epochNumber: 46, timestamp: "2026-09-27T15:00:00Z", instrument: "perp", notional: -32000, hedgePnl: 412.5, deltaBefore: 0.42, deltaAfter: 0.05 }, ], - }, - { - id: "btc-csp-v1", - name: "BTC Cash-Secured Put", - underlying: "BTC", - strategy: "cash_secured_put", - apy: 0.15, - apyMethodology: "Trailing 12-month annualized premium income ÷ average USDC TVL. Excludes BTC spot risk — depositors are exposed to assignment at the strike price.", - tvl: 1850000, - capacity: 3000000, - currentStrike: 60000, - currentExpiry: "2026-10-17", - riskRating: "high", - description: "Writes monthly cash-secured puts on BTC. The vault holds USDC as collateral; assignment means acquiring BTC at the strike.", - currentEpoch: 18, - currentPhase: "active", - phaseEndsAt: "2026-10-17T20:00:00Z", - sharePrice: 1.088, - totalShares: 1700368, - feeStructure: { managementPct: 0.5, performancePct: 15 }, - payoffExplanation: "Each month the vault sells a put option at a strike ~10% below the current BTC price, backed by USDC. If BTC stays above the strike, depositors keep the premium. If BTC falls below the strike, the vault is assigned and acquires BTC at the strike — a risk equivalent to a limit buy order.", - historicalEpochs: [ - { epochNumber: 17, depositStart: "2026-08-15", activeStart: "2026-08-19", settlementDate: "2026-09-20", strike: 59000, expiryDays: 30, premiumCollected: 420, outcome: "expired_worthless", pnlPct: 0.013 }, - { epochNumber: 16, depositStart: "2026-07-18", activeStart: "2026-07-22", settlementDate: "2026-08-16", strike: 55000, expiryDays: 30, premiumCollected: 380, outcome: "expired_worthless", pnlPct: 0.011 }, - { epochNumber: 15, depositStart: "2026-06-21", activeStart: "2026-06-24", settlementDate: "2026-07-19", strike: 57000, expiryDays: 30, premiumCollected: 440, outcome: "expired_worthless", pnlPct: 0.014 }, - ], - userPosition: null, - hedgeStatus: { - policy: { - enabled: false, - killSwitch: false, - targetDelta: 0, - deltaTolerance: 0.15, - hedgeInstrument: "futures", - maxHedgeNotional: 250000, - rehedgeIntervalSecs: 86400, - }, - portfolioDelta: -0.28, - portfolioGamma: -0.01, - portfolioVega: -40.1, - hedgeNotional: 0, - outsideBand: true, - lastHedgeAt: null, - staleOracle: false, + orderPricingBand: { + maxPremiumPct: 0.05, + maxDiscountPct: 0.05, + attestedNav: 1.042, + attestedAt: "2026-09-27T16:00:00Z", + stale: false, + disputed: false, }, - hedgeTrades: [], - }, - { - id: "eth-ic-v1", - name: "ETH Iron Condor", - underlying: "ETH", - strategy: "iron_condor", - apy: 0.18, - apyMethodology: "Trailing 12-month net premium income (both spreads) ÷ average USDC collateral deployed.", - tvl: 620000, - capacity: 1000000, - currentStrike: 3200, - currentExpiry: "2026-10-10", - riskRating: "low", - description: "Sells 30-day iron condors on ETH, targeting a range-bound market. Limited loss profile; wins when ETH stays between the two breakevens.", - currentEpoch: 11, - currentPhase: "deposit", - phaseEndsAt: "2026-09-28T12:00:00Z", - sharePrice: 1.021, - totalShares: 607247, - feeStructure: { managementPct: 0, performancePct: 10 }, - payoffExplanation: "The vault simultaneously sells an out-of-the-money call spread and a put spread on ETH. Maximum profit is the combined net premium, achieved if ETH expires between the two short strikes. Maximum loss is limited to the spread width minus the premium.", - historicalEpochs: [ - { epochNumber: 10, depositStart: "2026-08-25", activeStart: "2026-08-29", settlementDate: "2026-09-28", strike: 3100, expiryDays: 30, premiumCollected: 28, outcome: "expired_worthless", pnlPct: 0.016 }, + orders: [ + { id: "ord-1", vaultId: "xlm-cc-v1", tranche: null, side: "offer", maker: "GABC…XYZ", shares: 500, originalShares: 500, price: 1.05, partial: true, status: "open", expiresAt: "2026-10-04T16:00:00Z", createdAt: "2026-09-28T09:12:00Z", nonce: 1 }, + { id: "ord-2", vaultId: "xlm-cc-v1", tranche: null, side: "bid", maker: "GDEF…UVW", shares: 250, originalShares: 250, price: 1.01, partial: false, status: "open", expiresAt: "2026-10-02T16:00:00Z", createdAt: "2026-09-28T11:40:00Z", nonce: 2 }, ], - userPosition: null, - hedgeStatus: { - policy: { - enabled: true, - killSwitch: false, - targetDelta: 0, - deltaTolerance: 0.05, - hedgeInstrument: "underlying", - maxHedgeNotional: 100000, - rehedgeIntervalSecs: 7200, - }, - portfolioDelta: 0.02, - portfolioGamma: -0.03, - portfolioVega: -18.7, - hedgeNotional: 0, - outsideBand: false, - lastHedgeAt: null, - staleOracle: false, - }, - hedgeTrades: [], }, ];