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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,53 @@ curl -s http://localhost:3002/price/XLM/USDC | jq '{price, ammPrice, lastUpdated

*(Note: timestamp values above reflect the dynamic hourly anchor at seed execution time and request time).*

## Backfill & Ingestion Gap Detection

Lens provides persistent gap detection and an idempotent historical backfill CLI to recover missing trade history and declare incomplete time windows in API responses.

### 1. Gap Detection & IngestionGap Model
Gaps are tracked per `(network, source, pair)` in the `IngestionGap` table:
- `status`: `'open'` (detected hole in history), `'filled'` (successfully backfilled), or `'permanent'` (data cannot be backfilled).
- Gaps are detected when the elapsed time between consecutive price points exceeds the expected ingestion cadence (default 5 minutes).
- **Idempotency & Merging:** Re-running gap detection never duplicates records; overlapping or adjacent intervals are merged automatically into single continuous ranges.

### 2. Backfill CLI
Run the backfill script for historical sources:

```bash
# Backfill SDEX trades for XLM/USDC
npm run backfill -- --source SDEX --pair XLM/USDC --start 2026-10-15T00:00:00Z --end 2026-10-15T06:00:00Z --network testnet

# Detect gaps and backfill them automatically
npm run backfill -- --source SDEX --pair XLM/USDC --start 2026-10-15T00:00:00Z --end 2026-10-15T06:00:00Z --detect
```

- **History Support:**
- `SDEX` and `AMM`: Supported via Horizon historical trade paging. Refetched points are written through the same idempotent `upsertPricePoints` path as live ingestion. Gaps are marked `filled`.
- `soroswap_amm` and `aquarius_amm`: State-only venues that do not retain historical trade logs. Attempting to backfill them cleanly marks the gap as `permanent` and exits without error.
- **Idempotent Re-runs:** Running the same backfill range a second time produces 0 duplicate records and leaves the gap marked `filled`.

### 3. Incomplete Window Declarations in API Responses
When querying time-window endpoints (such as `/prices/history`, `/candles/:assetA/:assetB`, `/price/:assetA/:assetB/history`, `/price/twap/*`, and `/price/vwap/*`):
- If the requested window overlaps any `open` or `permanent` gaps for that pair and network, the response includes `incomplete: true` along with the list of overlapping `gaps`:
```json
{
"pair": "XLM/USDC",
"interval": "1m",
"incomplete": true,
"gaps": [
{
"source": "SDEX",
"start": "2026-10-15T01:00:00.000Z",
"end": "2026-10-15T03:00:00.000Z",
"status": "open"
}
],
"points": [...]
}
```
- Windows that are clean or cover only `filled` gaps omit the `incomplete` and `gaps` fields.

## Numeric Precision & Serialization

Lens enforces strict arbitrary-precision numeric aggregation across SQL queries to eliminate floating-point representation artifacts and order-dependent rounding errors:
Expand Down
19 changes: 19 additions & 0 deletions clients/typescript/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,8 @@ export interface components {
outlierRejected: number;
/** @enum {string} */
filterMethod: "iqr" | "modified_zscore";
incomplete?: boolean;
gaps?: components["schemas"]["DataGap"][];
};
VwapResponse: {
assetA: string;
Expand All @@ -1254,6 +1256,17 @@ export interface components {
outlierRejected: number;
/** @enum {string} */
filterMethod: "iqr" | "modified_zscore";
incomplete?: boolean;
gaps?: components["schemas"]["DataGap"][];
};
DataGap: {
source: string;
/** Format: date-time */
start: string;
/** Format: date-time */
end: string;
/** @enum {string} */
status: "open" | "permanent";
};
VolumeTotals: {
/** @description Volume summed across primary markets only (excluding aggregator double-counts) */
Expand Down Expand Up @@ -2102,6 +2115,8 @@ export interface operations {
/** @enum {string} */
network?: "testnet" | "mainnet";
window?: string;
incomplete?: boolean;
gaps?: components["schemas"]["DataGap"][];
buckets?: components["schemas"]["PriceBucket"][];
};
};
Expand Down Expand Up @@ -2216,6 +2231,8 @@ export interface operations {
}[];
nextCursor: string | null;
hasMore: boolean;
incomplete?: boolean;
gaps?: components["schemas"]["DataGap"][];
};
};
};
Expand Down Expand Up @@ -2275,6 +2292,8 @@ export interface operations {
from: string;
/** Format: date-time */
to: string;
incomplete?: boolean;
gaps?: components["schemas"]["DataGap"][];
/** @description Number of points in this page (not the total across pages). */
count: number;
points: {
Expand Down
75 changes: 75 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,15 @@
"window": {
"type": "string"
},
"incomplete": {
"type": "boolean"
},
"gaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DataGap"
}
},
"buckets": {
"type": "array",
"items": {
Expand Down Expand Up @@ -1077,6 +1086,15 @@
},
"hasMore": {
"type": "boolean"
},
"incomplete": {
"type": "boolean"
},
"gaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DataGap"
}
}
}
}
Expand Down Expand Up @@ -1246,6 +1264,15 @@
"type": "string",
"format": "date-time"
},
"incomplete": {
"type": "boolean"
},
"gaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DataGap"
}
},
"count": {
"type": "integer",
"description": "Number of points in this page (not the total across pages)."
Expand Down Expand Up @@ -5194,6 +5221,15 @@
"iqr",
"modified_zscore"
]
},
"incomplete": {
"type": "boolean"
},
"gaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DataGap"
}
}
}
},
Expand Down Expand Up @@ -5278,6 +5314,45 @@
"iqr",
"modified_zscore"
]
},
"incomplete": {
"type": "boolean"
},
"gaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DataGap"
}
}
}
},
"DataGap": {
"type": "object",
"required": [
"source",
"start",
"end",
"status"
],
"additionalProperties": false,
"properties": {
"source": {
"type": "string"
},
"start": {
"type": "string",
"format": "date-time"
},
"end": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"enum": [
"open",
"permanent"
]
}
}
},
Expand Down
47 changes: 47 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,12 @@ paths:
enum: [testnet, mainnet]
window:
type: string
incomplete:
type: boolean
gaps:
type: array
items:
$ref: '#/components/schemas/DataGap'
buckets:
type: array
items:
Expand Down Expand Up @@ -759,6 +765,12 @@ paths:
nullable: true
hasMore:
type: boolean
incomplete:
type: boolean
gaps:
type: array
items:
$ref: '#/components/schemas/DataGap'
'400':
description: Invalid parameters
'402':
Expand Down Expand Up @@ -885,6 +897,12 @@ paths:
to:
type: string
format: date-time
incomplete:
type: boolean
gaps:
type: array
items:
$ref: '#/components/schemas/DataGap'
count:
type: integer
description: Number of points in this page (not the total across pages).
Expand Down Expand Up @@ -3344,6 +3362,12 @@ components:
filterMethod:
type: string
enum: [iqr, modified_zscore]
incomplete:
type: boolean
gaps:
type: array
items:
$ref: '#/components/schemas/DataGap'

VwapResponse:
type: object
Expand Down Expand Up @@ -3384,6 +3408,29 @@ components:
filterMethod:
type: string
enum: [iqr, modified_zscore]
incomplete:
type: boolean
gaps:
type: array
items:
$ref: '#/components/schemas/DataGap'

DataGap:
type: object
required: [source, start, end, status]
additionalProperties: false
properties:
source:
type: string
start:
type: string
format: date-time
end:
type: string
format: date-time
status:
type: string
enum: [open, permanent]

VolumeTotals:
type: object
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"db:migration-cycle": "bash scripts/db-migration-cycle.sh",
"db:migrate:partitioning": "tsx scripts/migrate-price-points-partitioning.ts",
"seed": "tsx scripts/seed.ts",
"backfill": "tsx scripts/backfill.ts",
"snippets:gen": "tsx scripts/gen-snippets.ts"
},
"dependencies": {
Expand Down
24 changes: 24 additions & 0 deletions prisma/migrations/20261023140000_add_ingestion_gaps/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- #241: persistent record of detected ingestion gaps, so history/candle/VWAP
-- responses can declare windows that overlap missing data as incomplete.

-- CreateTable
CREATE TABLE "ingestion_gaps" (
"id" TEXT NOT NULL,
"network" TEXT NOT NULL DEFAULT 'testnet',
"source" TEXT NOT NULL,
"pair_key" TEXT NOT NULL,
"start_time" TIMESTAMP(3) NOT NULL,
"end_time" TIMESTAMP(3) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'open',
"detected_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ingestion_gaps_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "ingestion_gaps_network_pair_key_source_status_idx" ON "ingestion_gaps"("network", "pair_key", "source", "status");

-- CreateIndex
CREATE INDEX "ingestion_gaps_network_pair_key_start_time_end_time_idx" ON "ingestion_gaps"("network", "pair_key", "start_time", "end_time");

16 changes: 16 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,22 @@ model ApiKey {
@@map("api_keys")
}

model IngestionGap {
id String @id @default(uuid())
network String @default("testnet")
source String
pair String @map("pair_key")
start DateTime @map("start_time")
end DateTime @map("end_time")
status String @default("open")
detectedAt DateTime @default(now()) @map("detected_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@index([network, pair, source, status])
@@index([network, pair, start, end])
@@map("ingestion_gaps")
}

model VenueDispersion {
id String @id @default(uuid())
network String @default("testnet")
Expand Down
Loading
Loading