healthcheck: add MAX_SLOT_STALENESS_MS and MIN_SLOT_RATE env vars
This commit is contained in:
242
src/athena/example.ts
Normal file
242
src/athena/example.ts
Normal 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();
|
||||
}
|
||||
252
src/athena/testRedisPolling.ts
Normal file
252
src/athena/testRedisPolling.ts
Normal 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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
152
src/scripts/pollAuctionParams.ts
Normal file
152
src/scripts/pollAuctionParams.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user