Files
trade-drift-dlob/src/dlob-subscriber/DLOBSubscriberIO.ts
2024-05-22 14:06:54 +10:00

372 lines
10 KiB
TypeScript

import {
BN,
DLOBSubscriber,
DLOBSubscriptionConfig,
DriftEnv,
L2OrderBookGenerator,
MarketType,
PositionDirection,
groupL2,
isVariant,
} from '@drift-labs/sdk';
import { RedisClient } from '@drift/common';
import {
SubscriberLookup,
addMarketSlotToResponse,
addOracletoResponse,
l2WithBNToStrings,
} from '../utils/utils';
import { setHealthStatus, HEALTH_STATUS } from '../core/metrics';
type wsMarketArgs = {
marketIndex: number;
marketType: MarketType;
marketName: string;
depth: number;
includeVamm: boolean;
numVammOrders?: number;
grouping?: number;
fallbackL2Generators?: L2OrderBookGenerator[];
updateOnChange?: boolean;
};
const PERP_MAKRET_STALENESS_THRESHOLD = 30 * 60 * 1000;
const SPOT_MAKRET_STALENESS_THRESHOLD = 60 * 60 * 1000;
const PERP_MARKETS_TO_SKIP_SLOT_CHECK = {
'mainnet-beta': [17, 25],
devnet: [17, 21, 23],
};
const SPOT_MARKETS_TO_SKIP_SLOT_CHECK = {
'mainnet-beta': [6, 8, 10],
devnet: [],
};
export type wsMarketInfo = {
marketIndex: number;
marketName: string;
};
export class DLOBSubscriberIO extends DLOBSubscriber {
public marketArgs: wsMarketArgs[] = [];
public lastSeenL2Formatted: Map<MarketType, Map<number, any>>;
redisClient: RedisClient;
public killSwitchSlotDiffThreshold: number;
public lastMarketSlotMap: Map<
MarketType,
Map<number, { slot: number; ts: number }>
>;
public skipSlotStalenessCheckMarketsPerp: number[];
public skipSlotStalenessCheckMarketsSpot: number[];
constructor(
config: DLOBSubscriptionConfig & {
env: DriftEnv;
redisClient: RedisClient;
perpMarketInfos: wsMarketInfo[];
spotMarketInfos: wsMarketInfo[];
spotMarketSubscribers: SubscriberLookup;
killSwitchSlotDiffThreshold?: number;
}
) {
super(config);
this.redisClient = config.redisClient;
this.killSwitchSlotDiffThreshold =
config.killSwitchSlotDiffThreshold || 200;
// Set up appropriate maps
this.lastSeenL2Formatted = new Map();
this.lastSeenL2Formatted.set(MarketType.SPOT, new Map());
this.lastSeenL2Formatted.set(MarketType.PERP, new Map());
this.lastMarketSlotMap = new Map();
this.lastMarketSlotMap.set(MarketType.SPOT, new Map());
this.lastMarketSlotMap.set(MarketType.PERP, new Map());
this.skipSlotStalenessCheckMarketsPerp =
PERP_MARKETS_TO_SKIP_SLOT_CHECK[config.env];
this.skipSlotStalenessCheckMarketsSpot =
SPOT_MARKETS_TO_SKIP_SLOT_CHECK[config.env];
for (const market of config.perpMarketInfos) {
const perpMarket = this.driftClient.getPerpMarketAccount(
market.marketIndex
);
const includeVamm = !isVariant(perpMarket.status, 'ammPaused');
this.marketArgs.push({
marketIndex: market.marketIndex,
marketType: MarketType.PERP,
marketName: market.marketName,
depth: -1,
numVammOrders: 100,
includeVamm,
updateOnChange: false,
fallbackL2Generators: [],
});
}
for (const market of config.spotMarketInfos) {
this.marketArgs.push({
marketIndex: market.marketIndex,
marketType: MarketType.SPOT,
marketName: market.marketName,
depth: -1,
includeVamm: false,
updateOnChange: false,
fallbackL2Generators: [
config.spotMarketSubscribers[market.marketIndex].phoenix,
config.spotMarketSubscribers[market.marketIndex].serum,
].filter((a) => !!a),
});
}
}
override async updateDLOB(): Promise<void> {
await super.updateDLOB();
for (const marketArgs of this.marketArgs) {
try {
this.getL2AndSendMsg(marketArgs);
this.getL3AndSendMsg(marketArgs);
} catch (error) {
console.error(error);
console.log(`Error getting L2 ${marketArgs.marketName}`);
}
}
}
getL2AndSendMsg(marketArgs: wsMarketArgs): void {
const clientPrefix = this.redisClient.forceGetClient().options.keyPrefix;
const grouping = marketArgs.grouping;
const { marketName, ...l2FuncArgs } = marketArgs;
const l2 = this.getL2(l2FuncArgs);
const slot = l2.slot;
const lastMarketSlotAndTime = this.lastMarketSlotMap
.get(marketArgs.marketType)
.get(marketArgs.marketIndex);
if (!lastMarketSlotAndTime) {
this.lastMarketSlotMap
.get(marketArgs.marketType)
.set(marketArgs.marketIndex, { slot, ts: Date.now() });
}
if (slot) {
delete l2.slot;
}
const marketType = isVariant(marketArgs.marketType, 'perp')
? 'perp'
: 'spot';
let l2Formatted: any;
if (grouping) {
const groupingBN = new BN(grouping);
l2Formatted = l2WithBNToStrings(
groupL2(l2, groupingBN, marketArgs.depth)
);
} else {
l2Formatted = l2WithBNToStrings(l2);
}
if (marketArgs.updateOnChange) {
if (
this.lastSeenL2Formatted
.get(marketArgs.marketType)
?.get(marketArgs.marketIndex) === JSON.stringify(l2Formatted)
)
return;
}
this.lastSeenL2Formatted
.get(marketArgs.marketType)
?.set(marketArgs.marketIndex, JSON.stringify(l2Formatted));
l2Formatted['marketName'] = marketName?.toUpperCase();
l2Formatted['marketType'] = marketType?.toLowerCase();
l2Formatted['marketIndex'] = marketArgs.marketIndex;
l2Formatted['ts'] = Date.now();
l2Formatted['slot'] = slot;
addOracletoResponse(
l2Formatted,
this.driftClient,
marketArgs.marketType,
marketArgs.marketIndex
);
addMarketSlotToResponse(
l2Formatted,
this.driftClient,
marketArgs.marketType,
marketArgs.marketIndex
);
// Check if slot diffs are too large for oracle
const skipSlotCheck =
(marketType === 'perp' &&
this.skipSlotStalenessCheckMarketsPerp.includes(
marketArgs.marketIndex
)) ||
(marketType === 'spot' &&
this.skipSlotStalenessCheckMarketsSpot.includes(
marketArgs.marketIndex
));
if (
Math.abs(slot - parseInt(l2Formatted['oracleData']['slot'])) >
this.killSwitchSlotDiffThreshold &&
!skipSlotCheck
) {
console.log(`Unhealthy process due to slot diffs for market ${marketName}:
dlobProvider slot: ${slot}
oracle slot: ${l2Formatted['oracleData']['slot']}
`);
setHealthStatus(HEALTH_STATUS.Restart);
}
// Check if times and slots are too different for market
const MAKRET_STALENESS_THRESHOLD =
marketType === 'perp'
? PERP_MAKRET_STALENESS_THRESHOLD
: SPOT_MAKRET_STALENESS_THRESHOLD;
if (
lastMarketSlotAndTime &&
l2Formatted['marketSlot'] === lastMarketSlotAndTime.slot &&
Date.now() - lastMarketSlotAndTime.ts > MAKRET_STALENESS_THRESHOLD &&
!skipSlotCheck
) {
console.log(`Unhealthy process due to same slot for market ${marketName} after > ${MAKRET_STALENESS_THRESHOLD}ms:
dlobProvider slot: ${slot}
market slot: ${l2Formatted['marketSlot']}
`);
setHealthStatus(HEALTH_STATUS.Restart);
} else if (
lastMarketSlotAndTime &&
l2Formatted['marketSlot'] !== lastMarketSlotAndTime.slot
) {
console.log(
`Updating market slot for ${marketArgs.marketName} with slot ${l2Formatted['marketSlot']}`
);
this.lastMarketSlotMap
.get(marketArgs.marketType)
.set(marketArgs.marketIndex, {
slot: l2Formatted['marketSlot'],
ts: Date.now(),
});
}
const l2Formatted_depth100 = Object.assign({}, l2Formatted, {
bids: l2Formatted.bids.slice(0, 100),
asks: l2Formatted.asks.slice(0, 100),
});
const l2Formatted_depth20 = Object.assign({}, l2Formatted, {
bids: l2Formatted.bids.slice(0, 20),
asks: l2Formatted.asks.slice(0, 20),
});
const l2Formatted_depth5 = Object.assign({}, l2Formatted, {
bids: l2Formatted.bids.slice(0, 5),
asks: l2Formatted.asks.slice(0, 5),
});
this.redisClient.publish(
`${clientPrefix}orderbook_${marketType}_${marketArgs.marketIndex}`,
l2Formatted
);
this.redisClient.set(
`last_update_orderbook_${marketType}_${marketArgs.marketIndex}`,
l2Formatted_depth100
);
this.redisClient.set(
`last_update_orderbook_${marketType}_${marketArgs.marketIndex}_depth_100`,
l2Formatted_depth100
);
this.redisClient.set(
`last_update_orderbook_${marketType}_${marketArgs.marketIndex}_depth_20`,
l2Formatted_depth20
);
this.redisClient.set(
`last_update_orderbook_${marketType}_${marketArgs.marketIndex}_depth_5`,
l2Formatted_depth5
);
const oraclePriceData =
marketType === 'spot'
? this.driftClient.getOracleDataForSpotMarket(marketArgs.marketIndex)
: this.driftClient.getOracleDataForPerpMarket(marketArgs.marketIndex);
const bids = this.dlob
.getBestMakers({
marketIndex: marketArgs.marketIndex,
marketType: marketArgs.marketType,
direction: PositionDirection.LONG,
slot: slot,
oraclePriceData,
numMakers: 4,
})
.map((x) => x.toString());
const asks = this.dlob
.getBestMakers({
marketIndex: marketArgs.marketIndex,
marketType: marketArgs.marketType,
direction: PositionDirection.SHORT,
slot,
oraclePriceData,
numMakers: 4,
})
.map((x) => x.toString());
this.redisClient.set(
`last_update_orderbook_best_makers_${marketType}_${marketArgs.marketIndex}`,
{ bids, asks, slot }
);
}
getL3AndSendMsg(marketArgs: wsMarketArgs): void {
const { marketName, ...l2FuncArgs } = marketArgs;
const l3 = this.getL3(l2FuncArgs);
const slot = l3.slot;
const lastMarketSlotAndTime = this.lastMarketSlotMap
.get(marketArgs.marketType)
.get(marketArgs.marketIndex);
if (!lastMarketSlotAndTime) {
this.lastMarketSlotMap
.get(marketArgs.marketType)
.set(marketArgs.marketIndex, { slot, ts: Date.now() });
}
if (slot) {
delete l3.slot;
}
const marketType = isVariant(marketArgs.marketType, 'perp')
? 'perp'
: 'spot';
for (const key of Object.keys(l3)) {
for (const idx in l3[key]) {
const level = l3[key][idx];
l3[key][idx] = {
...level,
price: level.price.toString(),
size: level.size.toString(),
};
}
}
l3['marketName'] = marketName?.toUpperCase();
l3['marketType'] = marketType?.toLowerCase();
l3['marketIndex'] = marketArgs.marketIndex;
l3['ts'] = Date.now();
l3['slot'] = slot;
addOracletoResponse(
l3,
this.driftClient,
marketArgs.marketType,
marketArgs.marketIndex
);
addMarketSlotToResponse(
l3,
this.driftClient,
marketArgs.marketType,
marketArgs.marketIndex
);
this.redisClient.set(
`last_update_orderbook_l3_${marketType}_${marketArgs.marketIndex}`,
l3
);
}
}