Files
trading_bot_v4/scripts/discover-drift-markets.ts
2025-12-05 14:43:06 +00:00

353 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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'
// 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()
}
/**
* Extract numeric value from Anchor-style enum objects
*
* Drift SDK uses Anchor which represents enums as objects like:
* - { pyth: {} } for OracleSource.Pyth
* - { active: {} } for MarketStatus.Active
*
* This helper safely extracts the key name and maps it to a numeric index,
* or handles direct numeric values for backwards compatibility.
*
* @param enumValue - The enum value (object or number)
* @param keyMap - Map of enum key names to their string representations
* @returns The string representation of the enum value
*/
function getEnumName<T extends Record<string, string>>(
enumValue: unknown,
keyMap: T
): string {
// Handle null/undefined
if (enumValue === null || enumValue === undefined) {
return 'Unknown'
}
// Handle direct numeric values (legacy support)
if (typeof enumValue === 'number') {
const values = Object.values(keyMap)
return values[enumValue] || `Unknown(${enumValue})`
}
// Handle Anchor-style enum objects like { pyth: {} } or { active: {} }
if (typeof enumValue === 'object') {
const keys = Object.keys(enumValue as object)
if (keys.length === 1) {
const key = keys[0].toLowerCase()
// Find matching key in keyMap (case-insensitive)
for (const [mapKey, mapValue] of Object.entries(keyMap)) {
if (mapKey.toLowerCase() === key) {
return mapValue
}
}
// Return the key name capitalized if no map match
return keys[0].charAt(0).toUpperCase() + keys[0].slice(1)
}
}
return `Unknown(${JSON.stringify(enumValue)})`
}
// Oracle source name mappings (Anchor enum key to display name)
const ORACLE_SOURCE_MAP: Record<string, string> = {
pyth: 'Pyth',
switchboard: 'Switchboard',
quoteAsset: 'QuoteAsset',
pyth1K: 'Pyth1K',
pyth1M: 'Pyth1M',
pythStableCoin: 'PythStableCoin',
prelaunch: 'Prelaunch',
pythPull: 'PythPull',
pyth1KPull: 'Pyth1KPull',
pyth1MPull: 'Pyth1MPull',
pythStableCoinPull: 'PythStableCoinPull',
switchboardOnDemand: 'SwitchboardOnDemand',
}
// Market status name mappings
const MARKET_STATUS_MAP: Record<string, string> = {
initialized: 'Initialized',
active: 'Active',
fundingPaused: 'FundingPaused',
ammPaused: 'AmmPaused',
fillPaused: 'FillPaused',
withdrawPaused: 'WithdrawPaused',
reduceOnly: 'ReduceOnly',
settlement: 'Settlement',
delisted: 'Delisted',
}
// Contract type name mappings
const CONTRACT_TYPE_MAP: Record<string, string> = {
perpetual: 'Perpetual',
future: 'Future',
}
// 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 - use robust enum extraction
const status = getEnumName(market.status, MARKET_STATUS_MAP)
// Contract type - use robust enum extraction
const contractType = getEnumName(market.contractType, CONTRACT_TYPE_MAP)
const marketInfo: MarketInfo = {
index: i,
symbol,
oracleSource: getEnumName(oracleSource, ORACLE_SOURCE_MAP),
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)
})