add auction anchoring in v2

This commit is contained in:
wphan
2025-10-21 16:33:30 -07:00
parent a627c89e0a
commit 3d4cf24296
8 changed files with 826 additions and 26 deletions

View File

@@ -43,6 +43,10 @@ import { handleHealthCheck } from '../core/middleware';
import { setGlobalDispatcher, Agent } from 'undici';
import { Metrics } from '../core/metricsV2';
import { OrderSubscriberFiltered } from '../dlob-subscriber/OrderSubscriberFiltered';
import {
FillQualityAnalyticsRepository,
TakerFillVsOracleBpsRedisResult,
} from '../athena/repositories/fillQualityAnalytics';
setGlobalDispatcher(
new Agent({
@@ -93,6 +97,10 @@ const tobStuckGauge = metricsV2.addGauge(
'tob_stuck_duration',
'Duration TOB has been stuck for each market'
);
const takerFillBpsFromOracleGauge = metricsV2.addGauge(
'taker_fill_bps_from_oracle',
'Taker fill BPS from oracle by market, side, and cohort'
);
metricsV2.finalizeObservables();
//@ts-ignore
@@ -109,7 +117,7 @@ const endpoint = process.env.ENDPOINT;
const grpcEndpoint = useGrpc
? process.env.GRPC_ENDPOINT ?? endpoint + `/${token}`
: '';
const grpcClient = process.env.GRPC_CLIENT ?? 'yellowstone'
const grpcClient = process.env.GRPC_CLIENT ?? 'yellowstone';
const wsEndpoint = process.env.WS_ENDPOINT;
const useOrderSubscriber =
@@ -122,6 +130,16 @@ const WS_FALLBACK_FETCH_INTERVAL = 60_000;
const KILLSWITCH_SLOT_DIFF_THRESHOLD =
parseInt(process.env.KILLSWITCH_SLOT_DIFF_THRESHOLD) || 200;
// Fill Quality Analytics configuration
const ENABLE_FILL_QUALITY_ANALYTICS =
process.env.ENABLE_FILL_QUALITY_ANALYTICS?.toLowerCase() === 'true';
const FILL_QUALITY_ANALYTICS_INTERVAL =
parseInt(process.env.FILL_QUALITY_ANALYTICS_INTERVAL) || 300_000; // 5 minutes default
const FILL_QUALITY_ANALYTICS_LOOKBACK_MS =
parseInt(process.env.FILL_QUALITY_ANALYTICS_LOOKBACK_MS) || 86_400_000; // 24 hours default
const FILL_QUALITY_ANALYTICS_SMOOTHING_MINUTES =
parseInt(process.env.FILL_QUALITY_ANALYTICS_SMOOTHING_MINUTES) || 60;
// TOB monitoring configuration - defaults to true if not set
const ENABLE_TOB_MONITORING =
!process.env.ENABLE_TOB_MONITORING ||
@@ -169,6 +187,22 @@ if (ENABLE_TOB_MONITORING) {
`TOB Monitoring Markets: ${TOB_MONITORING_ENABLED_PERP_MARKETS.join(', ')}`
);
}
logger.info(
`Fill Quality Analytics: ${
ENABLE_FILL_QUALITY_ANALYTICS ? 'enabled' : 'disabled'
}`
);
if (ENABLE_FILL_QUALITY_ANALYTICS) {
logger.info(
`Fill Quality Analytics Interval: ${FILL_QUALITY_ANALYTICS_INTERVAL}ms`
);
logger.info(
`Fill Quality Analytics Lookback: ${FILL_QUALITY_ANALYTICS_LOOKBACK_MS}ms`
);
logger.info(
`Fill Quality Analytics Smoothing: ${FILL_QUALITY_ANALYTICS_SMOOTHING_MINUTES} minutes`
);
}
let MARKET_SUBSCRIBERS: SubscriberLookup = {};
@@ -504,8 +538,7 @@ const main = async () => {
'grpc.keepalive_timeout_ms': 1_000,
'grpc.keepalive_permit_without_calls': 1,
},
client: grpcClient
client: grpcClient,
},
commitment: stateCommitment,
};
@@ -817,6 +850,196 @@ const main = async () => {
// Start TOB monitoring
setInterval(checkTobForStuckOrders, TOB_CHECK_INTERVAL);
// Track last known values for fill quality metrics (per market/side/cohort)
const lastFillQualityValues = new Map<string, number>();
// Helper function to update fill quality metric with null tracking
const updateFillQualityMetric = (
value: string | number | null | undefined,
marketIndex: string | number,
side: string,
cohort: string
) => {
const marketIndexStr = marketIndex.toString();
const key = `${marketIndexStr}:${side}:${cohort}`;
const isNull = value === null || value === undefined;
let valueToUse: number;
if (isNull) {
// Use last known value, or 0 if no previous value exists
valueToUse = lastFillQualityValues.get(key) || 0;
logger.debug(
`Null value for market ${marketIndexStr} ${side} ${cohort}, using last known value: ${valueToUse}`
);
} else {
// Use current value and update the last known value
valueToUse = Number(value);
lastFillQualityValues.set(key, valueToUse);
}
takerFillBpsFromOracleGauge.setLatestValue(valueToUse, {
marketIndex: marketIndexStr,
side,
cohort,
null: isNull ? 'true' : 'false',
});
};
// Fill Quality Analytics function - fetch and store in Redis
const fetchAndStoreFillQualityAnalytics = async () => {
if (!ENABLE_FILL_QUALITY_ANALYTICS) {
return;
}
try {
logger.info('Starting fill quality analytics fetch');
const startTime = Date.now();
const fillQualityRepo = FillQualityAnalyticsRepository();
const toMs = Date.now();
const fromMs = toMs - FILL_QUALITY_ANALYTICS_LOOKBACK_MS;
const results = await fillQualityRepo.getTakerFillVsOracleBps(
fromMs,
toMs,
9, // baseDecimals
FILL_QUALITY_ANALYTICS_SMOOTHING_MINUTES
);
logger.info(
`Fetched fill quality analytics for ${results.length} markets in ${
Date.now() - startTime
}ms`
);
// Store results in Redis - one key per market
const startRedisSet = Date.now();
for (const result of results) {
const redisKey = `taker_fill_vs_oracle_bps:market:${result.MarketIndex}`;
const redisValue = JSON.stringify({
marketIndex: result.MarketIndex,
takerBuyBpsFromOracle: {
all: result.TakerBuyBpsFromOracle_ALL,
'1e0': result.TakerBuyBpsFromOracle_1e0,
'1e3': result.TakerBuyBpsFromOracle_1e3,
'1e4': result.TakerBuyBpsFromOracle_1e4,
'1e5': result.TakerBuyBpsFromOracle_1e5,
'1e6': result.TakerBuyBpsFromOracle_1e6,
},
takerSellBpsFromOracle: {
all: result.TakerSellBpsFromOracle_ALL,
'1e0': result.TakerSellBpsFromOracle_1e0,
'1e3': result.TakerSellBpsFromOracle_1e3,
'1e4': result.TakerSellBpsFromOracle_1e4,
'1e5': result.TakerSellBpsFromOracle_1e5,
'1e6': result.TakerSellBpsFromOracle_1e6,
},
updatedAtTs: Date.now(),
} as TakerFillVsOracleBpsRedisResult);
await redisClient.set(redisKey, redisValue);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_ALL,
result.MarketIndex,
'buy',
'all'
);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_1e0,
result.MarketIndex,
'buy',
'1e0'
);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_1e3,
result.MarketIndex,
'buy',
'1e3'
);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_1e4,
result.MarketIndex,
'buy',
'1e4'
);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_1e5,
result.MarketIndex,
'buy',
'1e5'
);
updateFillQualityMetric(
result.TakerBuyBpsFromOracle_1e6,
result.MarketIndex,
'buy',
'1e6'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_ALL,
result.MarketIndex,
'sell',
'all'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_1e0,
result.MarketIndex,
'sell',
'1e0'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_1e3,
result.MarketIndex,
'sell',
'1e3'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_1e4,
result.MarketIndex,
'sell',
'1e4'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_1e5,
result.MarketIndex,
'sell',
'1e5'
);
updateFillQualityMetric(
result.TakerSellBpsFromOracle_1e6,
result.MarketIndex,
'sell',
'1e6'
);
logger.debug(
`Stored fill quality analytics for market ${result.MarketIndex}`
);
}
logger.info(
`Successfully stored fill quality analytics for ${
results.length
} markets in Redis in ${Date.now() - startRedisSet}ms`
);
} catch (error) {
logger.error('Error fetching/storing fill quality analytics:', error);
}
};
// Run fill quality analytics fetch immediately on startup, then on interval
if (ENABLE_FILL_QUALITY_ANALYTICS) {
fetchAndStoreFillQualityAnalytics().catch((error) => {
logger.error('Initial fill quality analytics fetch failed:', error);
});
setInterval(
fetchAndStoreFillQualityAnalytics,
FILL_QUALITY_ANALYTICS_INTERVAL
);
logger.info(
`Fill quality analytics will run every ${FILL_QUALITY_ANALYTICS_INTERVAL}ms`
);
}
const handleStartup = async (_req, res, _next) => {
if (driftClient.isSubscribed && dlobProvider.size() > 0) {
res.writeHead(200);