Merge branch 'master' of https://github.com/mindesbunister/trading_bot_v4
This commit is contained in:
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next"
|
||||
}
|
||||
3480
package-lock.json
generated
3480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit"
|
||||
"test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit",
|
||||
"discover-markets": "npx tsx scripts/discover-drift-markets.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@drift-labs/sdk": "^2.75.0",
|
||||
@@ -38,6 +39,8 @@
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
||||
352
scripts/discover-drift-markets.ts
Normal file
352
scripts/discover-drift-markets.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
Reference in New Issue
Block a user