healthcheck: add MAX_SLOT_STALENESS_MS and MIN_SLOT_RATE env vars

This commit is contained in:
wphan
2026-01-05 13:39:15 -08:00
parent 207a8eae04
commit 7f4cbf8a65
5 changed files with 654 additions and 4 deletions

242
src/athena/example.ts Normal file
View File

@@ -0,0 +1,242 @@
/**
* Example script demonstrating how to use the Athena integration
*
* Usage:
* ts-node src/athena/example.ts
*
* Make sure to set the following environment variables:
* - AWS_REGION (optional, defaults to us-east-1)
* - AWS_ACCESS_KEY_ID
* - AWS_SECRET_ACCESS_KEY
* - ATHENA_DATABASE (optional, defaults to staging-archive)
* - ATHENA_OUTPUT_BUCKET (optional, defaults to staging-data-ingestion-bucket)
*/
import { FillQualityAnalyticsRepository } from './repositories/fillQualityAnalytics';
import { Athena } from './client';
require('dotenv').config();
async function exampleBasicQuery() {
console.log('\n=== Example 1: Basic Query ===\n');
const { query } = Athena();
// Simple query to get recent trades
const results = await query(`
SELECT
ts,
markettype,
marketindex,
action,
taker,
maker
FROM eventtype_traderecord
WHERE year = '2024'
AND month = '10'
AND day = '19'
AND markettype = 'perp'
AND marketindex = 0
LIMIT 10
`);
console.log('Results:', JSON.stringify(results, null, 2));
console.log(`Found ${results.length} trades`);
}
async function exampleFillQualityAnalytics() {
console.log('\n=== Example 2: Fill Quality Analytics ===\n');
const repository = FillQualityAnalyticsRepository();
// Get taker fill vs oracle bps for ALL perp markets
// Last 24 hours
const filterMarket = '15';
const smoothingMinutes = 60;
const nowMs = Date.now();
const yesterdayMs = nowMs - 24 * (smoothingMinutes * 60 * 1000);
console.log(
`Querying from ${new Date(yesterdayMs).toISOString()} to ${new Date(
nowMs
).toISOString()}`
);
const results = await repository.getTakerFillVsOracleBps(
yesterdayMs, // from (milliseconds)
nowMs, // to (milliseconds)
9, // baseDecimals
smoothingMinutes // smoothingMinutes
);
console.log(`Retrieved ${results.length} data points across all markets`);
// Group by market to show summary
const marketGroups = results.reduce((acc, result) => {
const market = result.MarketIndex;
if (!acc[market]) acc[market] = [];
acc[market].push(result);
return acc;
}, {} as Record<string, typeof results>);
console.log(`\nFound data for ${Object.keys(marketGroups).length} markets`);
// Show last few results for the first market (last results have full rolling window data)
if (results.length > 0) {
// const firstMarket = Object.keys(marketGroups)[0];
// const firstMarketData = marketGroups[firstMarket];
// console.log(`\nLast 3 results for Market ${firstMarket} (total: ${firstMarketData.length} rows):`);
// firstMarketData.slice(-3).forEach((result, i) => {
// console.log(`\n[${firstMarketData.length - 2 + i}] Time: ${result.Time}, Market: ${result.MarketIndex}`);
// console.log(` Buy BPS ($0-1k): ${result.TakerBuyBpsFromOracle_1e0}`);
// console.log(` Buy BPS (>$1M): ${result.TakerBuyBpsFromOracle_1e6}`);
// console.log(` Sell BPS ($0-1k): ${result.TakerSellBpsFromOracle_1e0}`);
// console.log(` Sell BPS (>$1M): ${result.TakerSellBpsFromOracle_1e6}`);
// });
// Find and show some non-null results
console.log('\n--- Looking for non-null results ---');
const nonNullResults = results.filter(
(r) =>
(r.TakerBuyBpsFromOracle_1e0 !== null ||
r.TakerSellBpsFromOracle_1e0 !== null ||
r.TakerBuyBpsFromOracle_1e3 !== null ||
r.TakerSellBpsFromOracle_1e3 !== null ||
r.TakerBuyBpsFromOracle_1e4 !== null ||
r.TakerSellBpsFromOracle_1e4 !== null ||
r.TakerBuyBpsFromOracle_1e5 !== null ||
r.TakerSellBpsFromOracle_1e5 !== null ||
r.TakerBuyBpsFromOracle_1e6 !== null ||
r.TakerSellBpsFromOracle_1e6 !== null ||
r.TakerBuyBpsFromOracle_ALL !== null ||
r.TakerSellBpsFromOracle_ALL !== null) &&
r.MarketIndex === filterMarket
);
console.log(
`Found ${nonNullResults.length} rows with non-null BPS values (${(
(nonNullResults.length / results.length) *
100
).toFixed(1)}% of total)`
);
// console.log(results.filter(r => r.MarketIndex === filterMarket));
const uniqueMarketIndexes = [
...new Set(results.map((r) => Number(r.MarketIndex))),
].sort((a, b) => a - b);
console.log(
`Unique market indexes (total: ${
uniqueMarketIndexes.length
}): ${uniqueMarketIndexes.join(', ')}`
);
if (nonNullResults.length > 0) {
console.log('\nLatest non-null result for Market 0:');
const sample = nonNullResults[nonNullResults.length - 1];
console.log(`Time: ${sample.Time}, Market: ${sample.MarketIndex}`);
console.log(
` Buy BPS ($0-1k): ${sample.TakerBuyBpsFromOracle_1e0}`
);
console.log(
` Buy BPS ($1k-10k): ${sample.TakerBuyBpsFromOracle_1e3}`
);
console.log(
` Buy BPS ($10k-100k): ${sample.TakerBuyBpsFromOracle_1e4}`
);
console.log(
` Buy BPS ($100k-1M): ${sample.TakerBuyBpsFromOracle_1e5}`
);
console.log(
` Buy BPS (>$1M): ${sample.TakerBuyBpsFromOracle_1e6}`
);
console.log(
` Buy BPS (ALL): ${sample.TakerBuyBpsFromOracle_ALL}`
);
console.log(
` Sell BPS ($0-1k): ${sample.TakerSellBpsFromOracle_1e0}`
);
console.log(
` Sell BPS ($1k-10k): ${sample.TakerSellBpsFromOracle_1e3}`
);
console.log(
` Sell BPS ($10k-100k): ${sample.TakerSellBpsFromOracle_1e4}`
);
console.log(
` Sell BPS ($100k-1M): ${sample.TakerSellBpsFromOracle_1e5}`
);
console.log(
` Sell BPS (>$1M): ${sample.TakerSellBpsFromOracle_1e6}`
);
console.log(
` Sell BPS (ALL): ${sample.TakerSellBpsFromOracle_ALL}`
);
} else {
console.log(
'\n⚠ WARNING: No non-null BPS values found in any results!'
);
console.log(' This might indicate:');
console.log(' - No trade data in the time range');
console.log(' - Orders/trades not joining properly');
console.log(' - Issue with the query logic');
}
}
console.log(
`Took ${Date.now() - nowMs}ms to run exampleFillQualityAnalytics`
);
}
async function exampleBatchQuery() {
console.log('\n=== Example 3: Batch Query ===\n');
const { batchQuery } = Athena();
// Run multiple queries in parallel
const { results, errors } = await batchQuery({
queries: [
{
query: `
SELECT COUNT(*) as trade_count
FROM eventtype_traderecord
WHERE year = '2024' AND month = '10' AND day = '19'
AND markettype = 'perp' AND marketindex = 0
`,
},
{
query: `
SELECT COUNT(*) as order_count
FROM eventtype_orderrecord
WHERE year = '2024' AND month = '10' AND day = '19'
AND "order".markettype = 'perp' AND "order".marketindex = 0
`,
},
],
});
console.log('Trade count:', results[0]?.[0]?.trade_count || 'N/A');
console.log('Order count:', results[1]?.[0]?.order_count || 'N/A');
if (errors.length > 0) {
console.log('Errors:', errors);
}
}
async function main() {
console.log('Athena Integration Examples');
console.log('===========================');
try {
// Run examples
// await exampleBasicQuery();
await exampleFillQualityAnalytics();
// await exampleBatchQuery();
console.log('\n✅ All examples completed successfully!');
} catch (error) {
console.error('\n❌ Error:', error);
process.exit(1);
}
}
// Run if executed directly
if (require.main === module) {
main();
}

View File

@@ -0,0 +1,252 @@
import { RedisClient, RedisClientPrefix } from '@drift/common/clients';
import { logger, setLogLevel } from '../utils/logger';
setLogLevel('info');
const MARKET_INDEXES_TO_TEST = [0, 1, 2]; // SOL-PERP, BTC-PERP, ETH-PERP
const POLL_INTERVAL_MS = 10_000; // Poll every 10 seconds
interface FillQualityData {
time: string;
marketIndex: string;
takerBuyBpsFromOracle: {
all: string | null;
'1e0': string | null;
'1e3': string | null;
'1e4': string | null;
'1e5': string | null;
'1e6': string | null;
};
takerSellBpsFromOracle: {
all: string | null;
'1e0': string | null;
'1e3': string | null;
'1e4': string | null;
'1e5': string | null;
'1e6': string | null;
};
updatedAtTs: number;
}
interface SummaryData {
markets: string[];
lastUpdated: string;
lookbackMs: number;
smoothingMinutes: number;
}
// Track last seen update times for each market
const lastSeenUpdates = new Map<number, number>();
const pollRedis = async (redisClient: RedisClient, iteration: number) => {
logger.info(`\n========== Polling iteration ${iteration} ==========`);
const pollTime = Date.now();
try {
// Check summary key
logger.info('\n📊 Market Data:');
// Check each market
for (const marketIndex of MARKET_INDEXES_TO_TEST) {
const key = `taker_fill_vs_oracle_bps:market:${marketIndex}`;
const dataRaw = await redisClient.get(key);
if (!dataRaw || typeof dataRaw !== 'string') {
logger.warn(` ❌ Market ${marketIndex}: No data found`);
continue;
}
const data: FillQualityData = JSON.parse(dataRaw);
const lastSeen = lastSeenUpdates.get(marketIndex);
const isNew = !lastSeen || data.updatedAtTs > lastSeen;
const ageSeconds = Math.floor((pollTime - data.updatedAtTs) / 1000);
// Update tracking
if (isNew) {
lastSeenUpdates.set(marketIndex, data.updatedAtTs);
}
const status = isNew ? '🆕 NEW UPDATE' : '📌 unchanged';
const marketName =
['SOL-PERP', 'BTC-PERP', 'ETH-PERP'][marketIndex] ||
`Market ${marketIndex}`;
logger.info(` ${status} - ${marketName} (Market ${marketIndex})`);
logger.info(` Data timestamp: ${data.time}`);
logger.info(
` Updated at: ${new Date(data.updatedAtTs).toISOString()}`
);
logger.info(` Age: ${ageSeconds}s ago`);
// Show some sample data
const buyAll = data.takerBuyBpsFromOracle.all;
const sellAll = data.takerSellBpsFromOracle.all;
if (buyAll !== null && sellAll !== null) {
logger.info(
` Taker Buy (all): ${parseFloat(buyAll).toFixed(2)} bps`
);
logger.info(
` Taker Sell (all): ${parseFloat(sellAll).toFixed(2)} bps`
);
// Show cohort breakdown if available
const buy1e6 = data.takerBuyBpsFromOracle['1e6'];
const sell1e6 = data.takerSellBpsFromOracle['1e6'];
if (buy1e6 !== null && sell1e6 !== null) {
logger.info(
` Large orders (>$1M): Buy=${parseFloat(buy1e6).toFixed(
2
)}bps, Sell=${parseFloat(sell1e6).toFixed(2)}bps`
);
}
} else {
logger.warn(` ⚠️ No data available for this market`);
}
// Warn if data is stale
if (ageSeconds > 600) {
// More than 10 minutes old
logger.warn(
` ⚠️ WARNING: Data is stale (${Math.floor(
ageSeconds / 60
)} minutes old)`
);
}
}
// Summary of updates seen
logger.info('\n📈 Update Tracking:');
if (iteration === 1) {
logger.info(' First iteration - establishing baseline');
} else {
const updatedMarkets = MARKET_INDEXES_TO_TEST.filter((idx) => {
const lastSeen = lastSeenUpdates.get(idx);
if (!lastSeen) return false;
const currentRaw = redisClient.get(
`taker_fill_vs_oracle_bps:market:${idx}`
);
return true; // If we have a last seen, we've tracked it
});
if (updatedMarkets.length === MARKET_INDEXES_TO_TEST.length) {
logger.info(
` ✅ All ${MARKET_INDEXES_TO_TEST.length} markets have data`
);
} else {
logger.warn(
` ⚠️ Only ${updatedMarkets.length}/${MARKET_INDEXES_TO_TEST.length} markets have data`
);
}
}
// Check if keys exist but are not in expected format
logger.info('\n🔍 Validation:');
let allValid = true;
for (const marketIndex of MARKET_INDEXES_TO_TEST) {
const key = `taker_fill_vs_oracle_bps:market:${marketIndex}`;
const dataRaw = await redisClient.get(key);
if (dataRaw && typeof dataRaw === 'string') {
try {
const data = JSON.parse(dataRaw);
if (!data.updatedAtTs || !data.marketIndex) {
logger.error(` ❌ Market ${marketIndex}: Invalid data structure`);
allValid = false;
}
} catch (error) {
logger.error(
` ❌ Market ${marketIndex}: Failed to parse JSON - ${error.message}`
);
allValid = false;
}
}
}
if (allValid) {
logger.info(' ✅ All data structures are valid');
}
} catch (error) {
logger.error(`Error polling Redis: ${error.message}`);
logger.error(error.stack);
}
};
const main = async () => {
logger.info('🚀 Starting Fill Quality Analytics Redis Polling Test');
logger.info(` Testing markets: ${MARKET_INDEXES_TO_TEST.join(', ')}`);
logger.info(` Poll interval: ${POLL_INTERVAL_MS / 1000}s`);
logger.info(` Press Ctrl+C to stop\n`);
// Log Redis connection details
const redisHost = process.env.ELASTICACHE_HOST || 'localhost';
const redisPort = process.env.ELASTICACHE_PORT || '6379';
// For local development, disable TLS and cluster mode
const isLocal = !process.env.ELASTICACHE_HOST;
if (isLocal) {
logger.info('📡 Using local Redis (TLS disabled)');
// Set environment variables to disable TLS for local Redis
// getTlsConfiguration() checks these to determine TLS usage
process.env.RUNNING_LOCAL = 'true';
process.env.LOCAL_CACHE = 'true';
}
logger.info(`📡 Connecting to Redis:`);
logger.info(` Host: ${redisHost}`);
logger.info(` Port: ${redisPort}`);
logger.info(` Prefix: ${RedisClientPrefix.DLOB}`);
const redisClient = new RedisClient({
prefix: RedisClientPrefix.DLOB,
});
try {
await redisClient.connect();
logger.info('✅ Connected to Redis\n');
} catch (error) {
logger.error('❌ Failed to connect to Redis:', error.message);
logger.error('\n💡 Troubleshooting:');
logger.error(' 1. Make sure Redis is running locally:');
logger.error(' $ redis-server');
logger.error(' OR');
logger.error(' $ docker run -d -p 6379:6379 redis:latest\n');
logger.error(' 2. Check if Redis is accessible:');
logger.error(' $ redis-cli ping');
logger.error(' Expected: PONG\n');
logger.error(' 3. Set environment variables if using non-default Redis:');
logger.error(' $ export ELASTICACHE_HOST=your-redis-host');
logger.error(' $ export ELASTICACHE_PORT=your-redis-port\n');
process.exit(1);
}
let iteration = 0;
// Initial poll
iteration++;
await pollRedis(redisClient, iteration);
// Set up periodic polling
const intervalId = setInterval(async () => {
iteration++;
await pollRedis(redisClient, iteration);
}, POLL_INTERVAL_MS);
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('\n\n🛑 Shutting down...');
clearInterval(intervalId);
await redisClient.disconnect();
logger.info('✅ Disconnected from Redis');
logger.info(
`\n📊 Final Stats: Ran ${iteration} polling iterations over ${Math.floor(
(iteration * POLL_INTERVAL_MS) / 1000
)}s`
);
process.exit(0);
});
};
main().catch((error) => {
logger.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -12,11 +12,11 @@ export enum HEALTH_STATUS {
const HEALTH_CHECK_CONFIG = {
CHECK_INTERVAL_MS: 2000,
// Maximum time allowed between slot updates
MAX_SLOT_STALENESS_MS: 5000,
MAX_SLOT_STALENESS_MS: parseInt(process.env.MAX_SLOT_STALENESS_MS || '5000'),
// Minimum expected slot advancement rate. Sometimes usermap is used for slot source, so the slot rate
// may be much lower than 2 per sec. 0.03 is 1 slot per 33s
MIN_SLOT_RATE: 0.03,
} as const;
MIN_SLOT_RATE: parseFloat(process.env.MIN_SLOT_RATE || '0.03'),
};
/**
* Tracks the health state of the slot subscriber

View File

@@ -1050,7 +1050,11 @@ const main = async (): Promise<void> => {
}
});
const inputParams = createMarketBasedAuctionParams(auctionParamsInput, undefined, apiVersion);
const inputParams = createMarketBasedAuctionParams(
auctionParamsInput,
undefined,
apiVersion
);
const result = await mapToMarketOrderParams(
inputParams,

View File

@@ -0,0 +1,152 @@
/**
yarn run ts-node src/scripts/pollAuctionParams.ts [mainnet|staging]
*/
import { logger } from '../utils/logger';
import { setTimeout as sleep } from 'timers/promises';
import { request } from 'undici';
import * as fs from 'fs';
import * as path from 'path';
const envArg = process.argv[2];
const ENV = envArg ?? 'mainnet';
if (ENV !== 'mainnet' && ENV !== 'staging') {
throw new Error(`Invalid environment: ${ENV}`);
}
const VERSION = '&version=2';
// @ts-ignore
const TARGET_URL = `https://${
ENV === 'staging' ? 'staging.' : ''
}dlob.drift.trade/auctionParams?assetType=base&marketType=perp&marketIndex=2&direction=long&maxLeverageSelected=false&maxLeverageOrderSize=18446744073709551615&amount=550000000&reduceOnly=false&auctionDuration=20&auctionStartPriceOffset=-0.1&auctionEndPriceOffset=0.1&auctionStartPriceOffsetFrom=mark&auctionEndPriceOffsetFrom=worst&slippageTolerance=dynamic&isOracleOrder=true&forceUpToSlippage=false${VERSION}`;
// staging
// const TARGET_URL = 'https://staging.dlob.drift.trade/auctionParams?assetType=base&marketType=perp&marketIndex=0&direction=long&maxLeverageSelected=false&maxLeverageOrderSize=18446744073709551615&amount=2150000000&reduceOnly=false&auctionDuration=20&auctionStartPriceOffset=-0.1&auctionEndPriceOffset=0.1&auctionStartPriceOffsetFrom=mark&auctionEndPriceOffsetFrom=worst&slippageTolerance=dynamic&isOracleOrder=true&forceUpToSlippage=false';
const OUTPUT_CSV = process.env.OUTPUT_CSV || `auctionParams-${ENV}.csv`;
const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS || 10_000);
const csvPath = path.resolve(process.cwd(), OUTPUT_CSV);
function ensureCsvHeader(): void {
if (!fs.existsSync(csvPath)) {
const header = [
'timestamp_iso',
'entryPrice',
'bestPrice',
'worstPrice',
'oraclePrice',
'markPrice',
'priceImpact',
'slippageTolerance',
'auctionDuration',
'auctionStartPrice',
'auctionEndPrice',
'oraclePriceOffset',
'direction',
'baseAssetAmount',
'raw_json',
].join(',');
fs.writeFileSync(csvPath, header + '\n');
logger.info(`Created CSV with header at ${csvPath}`);
}
}
function csvEscape(value: unknown): string {
if (value === null || value === undefined) return '';
const str = String(value);
if (/[",\n]/.test(str)) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
}
async function fetchAuctionParams(): Promise<any> {
const { statusCode, body } = await request(TARGET_URL, { method: 'GET' });
if (statusCode < 200 || statusCode >= 300) {
throw new Error(`HTTP ${statusCode}`);
}
const text = await body.text();
try {
return JSON.parse(text);
} catch (e) {
throw new Error(`Invalid JSON: ${(e as Error).message}`);
}
}
function appendRow(resp: any): void {
const nowIso = new Date().toISOString();
// Safely read expected fields if present
const data = resp?.data ?? {};
const entryPrice = data?.entryPrice ?? '';
const bestPrice = data?.bestPrice ?? '';
const worstPrice = data?.worstPrice ?? '';
const oraclePrice = data?.oraclePrice ?? '';
const markPrice = data?.markPrice ?? '';
const priceImpact = data?.priceImpact ?? '';
const slippageTolerance = data?.slippageTolerance ?? '';
const params = data?.params ?? {};
const auctionDuration = params?.auctionDuration ?? '';
const auctionStartPrice = params?.auctionStartPrice ?? '';
const auctionEndPrice = params?.auctionEndPrice ?? '';
const oraclePriceOffset = params?.oraclePriceOffset ?? '';
const direction = params?.direction ?? '';
const baseAssetAmount = params?.baseAssetAmount ?? '';
const raw = JSON.stringify(resp);
const row = [
csvEscape(nowIso),
csvEscape(entryPrice),
csvEscape(bestPrice),
csvEscape(worstPrice),
csvEscape(oraclePrice),
csvEscape(markPrice),
csvEscape(priceImpact),
csvEscape(slippageTolerance),
csvEscape(auctionDuration),
csvEscape(auctionStartPrice),
csvEscape(auctionEndPrice),
csvEscape(oraclePriceOffset),
csvEscape(direction),
csvEscape(baseAssetAmount),
csvEscape(raw),
].join(',');
// Use appendFileSync for durability between iterations
fs.appendFileSync(csvPath, row + '\n');
}
async function main(): Promise<void> {
ensureCsvHeader();
logger.info(
`Polling ${TARGET_URL} every ${POLL_INTERVAL_MS}ms; writing to ${csvPath}`
);
while (true) {
try {
const start = Date.now();
const resp = await fetchAuctionParams();
logger.info(
`[${ENV} - ${VERSION}] Fetched auction params in ${
Date.now() - start
}ms`
);
appendRow(resp);
} catch (e) {
logger.error(
`Failed to fetch or write auction params: ${(e as Error).message}`
);
}
await sleep(POLL_INTERVAL_MS);
}
}
main().catch((e) => {
logger.error(`Fatal error: ${e?.message ?? e}`);
process.exit(1);
});