feat: Add Drift market discovery script for finding market indices and oracle addresses

Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-05 14:38:16 +00:00
parent 830c08dfc7
commit e9472175ba
4 changed files with 3795 additions and 1 deletions

View File

@@ -0,0 +1,308 @@
/**
* Drift Market Discovery Script
*
* Queries the Drift SDK to list all available perpetual markets with their
* complete specifications including market index, oracle address, and order sizes.
*
* Usage:
* npm run discover-markets
* npx tsx scripts/discover-drift-markets.ts
* npx tsx scripts/discover-drift-markets.ts FARTCOIN # Search for specific symbol
*/
import { initializeDriftService, getDriftService } from '../lib/drift/client'
import { OracleSource } from '@drift-labs/sdk'
// Helper to decode market name from bytes (Drift stores names as 32-byte arrays)
function decodeMarketName(nameBytes: number[] | Uint8Array | undefined): string {
if (!nameBytes) return ''
// Convert to string and trim null bytes
const bytes = Array.isArray(nameBytes) ? nameBytes : Array.from(nameBytes)
const name = String.fromCharCode(...bytes.filter(b => b !== 0))
return name.trim()
}
// Map oracle source enum to human-readable string
function getOracleSourceName(oracleSource: OracleSource | number): string {
// OracleSource enum values from Drift SDK
const sourceMap: Record<number, string> = {
0: 'Pyth',
1: 'Switchboard',
2: 'QuoteAsset',
3: 'Pyth1K',
4: 'Pyth1M',
5: 'PythStableCoin',
6: 'Prelaunch',
7: 'PythPull',
8: 'Pyth1KPull',
9: 'Pyth1MPull',
10: 'PythStableCoinPull',
11: 'SwitchboardOnDemand',
}
// Handle both enum and number types
const sourceValue = typeof oracleSource === 'number' ? oracleSource : Object.values(oracleSource)[0]
return sourceMap[sourceValue as number] || `Unknown(${sourceValue})`
}
// Format number with appropriate decimals
function formatNumber(value: number, decimals: number = 6): string {
if (value === 0) return '0'
if (value < 0.0001) return value.toExponential(2)
return value.toFixed(decimals).replace(/\.?0+$/, '')
}
interface MarketInfo {
index: number
symbol: string
oracleSource: string
minOrderSize: number
orderStepSize: number
tickSize: number
oracleAddress: string
contractType: string
imfFactor: number
marginRatioInitial: number
marginRatioMaintenance: number
status: string
}
async function discoverMarkets(searchSymbol?: string): Promise<void> {
console.log('\n🔍 Discovering Drift Protocol Markets...\n')
try {
// Initialize Drift service
await initializeDriftService()
const driftService = getDriftService()
const driftClient = driftService.getClient()
console.log('✅ Connected to Drift Protocol\n')
// Get all perp market accounts
const perpMarketAccounts = driftClient.getPerpMarketAccounts()
if (!perpMarketAccounts || perpMarketAccounts.length === 0) {
console.log('❌ No perpetual markets found')
return
}
console.log(`📊 Found ${perpMarketAccounts.length} Perpetual Markets:\n`)
// Collect market information
const markets: MarketInfo[] = []
let foundSearchSymbol: MarketInfo | null = null
for (let i = 0; i < perpMarketAccounts.length; i++) {
const market = perpMarketAccounts[i]
if (!market) continue
// Extract market name from the name field (stored as bytes)
let symbol = decodeMarketName(market.name as unknown as number[])
if (!symbol) {
symbol = `Market-${i}`
}
// Get oracle information
const oracleAddress = market.amm?.oracle?.toString() || 'N/A'
const oracleSource = market.amm?.oracleSource
// Get order sizing (values are stored with different precision)
// Base asset precision is typically 9 decimals
const minOrderSize = market.amm?.minOrderSize
? Number(market.amm.minOrderSize) / 1e9
: 0
const orderStepSize = market.amm?.orderStepSize
? Number(market.amm.orderStepSize) / 1e9
: 0
// Tick size for price (quote asset precision is 6 decimals for USDC)
const tickSize = market.amm?.orderTickSize
? Number(market.amm.orderTickSize) / 1e6
: 0.0001
// Get margin requirements (stored as percentages with 4 decimal precision)
const marginRatioInitial = market.marginRatioInitial
? Number(market.marginRatioInitial) / 10000
: 0
const marginRatioMaintenance = market.marginRatioMaintenance
? Number(market.marginRatioMaintenance) / 10000
: 0
// IMF factor (insurance margin fraction)
const imfFactor = market.imfFactor
? Number(market.imfFactor) / 1e6
: 0
// Market status
const statusMap: Record<number, string> = {
0: 'Initialized',
1: 'Active',
2: 'FundingPaused',
3: 'AmmPaused',
4: 'FillPaused',
5: 'WithdrawPaused',
6: 'ReduceOnly',
7: 'Settlement',
8: 'Delisted',
}
const status = statusMap[Object.values(market.status)[0] as number] || 'Unknown'
// Contract type
const contractTypeMap: Record<number, string> = {
0: 'Perpetual',
1: 'Future',
}
const contractType = contractTypeMap[Object.values(market.contractType)[0] as number] || 'Unknown'
const marketInfo: MarketInfo = {
index: i,
symbol,
oracleSource: getOracleSourceName(oracleSource),
minOrderSize,
orderStepSize,
tickSize,
oracleAddress,
contractType,
imfFactor,
marginRatioInitial,
marginRatioMaintenance,
status,
}
markets.push(marketInfo)
// Check if this matches our search
if (searchSymbol && symbol.toUpperCase().includes(searchSymbol.toUpperCase())) {
foundSearchSymbol = marketInfo
}
}
// Print table header
console.log('Index | Symbol | Oracle | Min Order | Step Size | Tick Size | Status | Oracle Address')
console.log('------|----------------------|-------------|------------|------------|------------|-----------|' + '-'.repeat(50))
// Print each market
for (const market of markets) {
const isHighlighted = searchSymbol && market.symbol.toUpperCase().includes(searchSymbol.toUpperCase())
const highlight = isHighlighted ? ' ← FOUND!' : ''
const row = [
market.index.toString().padStart(5),
market.symbol.padEnd(20),
market.oracleSource.padEnd(11),
formatNumber(market.minOrderSize).padStart(10),
formatNumber(market.orderStepSize).padStart(10),
formatNumber(market.tickSize).padStart(10),
market.status.padEnd(9),
market.oracleAddress.substring(0, 44) + '...' + highlight,
].join(' | ')
if (isHighlighted) {
console.log(`\x1b[32m${row}\x1b[0m`) // Green highlight
} else {
console.log(row)
}
}
// If searching for a specific symbol
if (searchSymbol) {
console.log('\n' + '='.repeat(100))
if (foundSearchSymbol) {
console.log(`\n🎯 ${foundSearchSymbol.symbol} Configuration:\n`)
// Generate copy-paste config for config/trading.ts
console.log('Copy this into config/trading.ts (SUPPORTED_MARKETS):')
console.log('```typescript')
console.log(`'${foundSearchSymbol.symbol}': {`)
console.log(` symbol: '${foundSearchSymbol.symbol}',`)
console.log(` driftMarketIndex: ${foundSearchSymbol.index},`)
console.log(` pythPriceFeedId: '${foundSearchSymbol.oracleAddress}',`)
console.log(` minOrderSize: ${foundSearchSymbol.minOrderSize},`)
console.log(` tickSize: ${foundSearchSymbol.tickSize},`)
console.log(` positionSize: 50, // Customize as needed`)
console.log(` leverage: 15, // Customize as needed`)
console.log(`},`)
console.log('```')
// Generate symbol normalizer snippet
const symbolBase = foundSearchSymbol.symbol.replace('-PERP', '').replace('-SPOT', '')
console.log('\nAlso add to normalizeTradingViewSymbol():')
console.log('```typescript')
console.log(`if (upper.includes('${symbolBase}')) return '${foundSearchSymbol.symbol}'`)
console.log('```')
// Print detailed market info
console.log('\n📋 Full Market Details:')
console.log(' Market Index:', foundSearchSymbol.index)
console.log(' Symbol:', foundSearchSymbol.symbol)
console.log(' Contract Type:', foundSearchSymbol.contractType)
console.log(' Status:', foundSearchSymbol.status)
console.log(' Oracle Source:', foundSearchSymbol.oracleSource)
console.log(' Oracle Address:', foundSearchSymbol.oracleAddress)
console.log(' Min Order Size:', foundSearchSymbol.minOrderSize, 'units')
console.log(' Order Step Size:', foundSearchSymbol.orderStepSize, 'units')
console.log(' Tick Size (Price):', foundSearchSymbol.tickSize, 'USD')
console.log(' Initial Margin Ratio:', (foundSearchSymbol.marginRatioInitial * 100).toFixed(2) + '%')
console.log(' Maintenance Margin Ratio:', (foundSearchSymbol.marginRatioMaintenance * 100).toFixed(2) + '%')
console.log(' IMF Factor:', foundSearchSymbol.imfFactor)
// Calculate max leverage from margin requirement
if (foundSearchSymbol.marginRatioInitial > 0) {
const maxLeverage = Math.floor(1 / foundSearchSymbol.marginRatioInitial)
console.log(' Max Leverage (calculated):', maxLeverage + 'x')
}
} else {
console.log(`\n⚠ ${searchSymbol.toUpperCase()} not found in available markets.`)
console.log('\nPossible reasons:')
console.log(' 1. The market may not exist on Drift yet')
console.log(' 2. The symbol name might be different (try variations)')
console.log(' 3. The market might be delisted or not yet active')
console.log('\nTip: Check Drift app manually: https://app.drift.trade')
// Suggest similar symbols
const similar = markets.filter(m =>
m.symbol.toUpperCase().includes(searchSymbol.substring(0, 3).toUpperCase())
)
if (similar.length > 0) {
console.log('\nSimilar symbols found:')
similar.forEach(m => console.log(` - ${m.symbol} (index: ${m.index})`))
}
}
}
// Summary
console.log('\n' + '='.repeat(100))
console.log(`\n📊 Summary: ${markets.length} perpetual markets available`)
const activeMarkets = markets.filter(m => m.status === 'Active')
console.log(` Active markets: ${activeMarkets.length}`)
const pythMarkets = markets.filter(m => m.oracleSource.includes('Pyth'))
console.log(` Pyth oracle markets: ${pythMarkets.length}`)
// Cleanup
await driftService.disconnect()
console.log('\n✅ Discovery complete\n')
} catch (error) {
console.error('❌ Error discovering markets:', error)
throw error
}
}
// Main entry point
const searchArg = process.argv[2]
discoverMarkets(searchArg)
.then(() => {
process.exit(0)
})
.catch((error) => {
console.error('\n❌ Discovery failed:', error)
process.exit(1)
})