Files
trading_bot_v4/scripts/discover-drift-markets.ts

309 lines
11 KiB
TypeScript
Raw 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'
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)
})