feat: Complete Trading Bot v4 with Drift Protocol integration
Features: - Autonomous trading system with Drift Protocol on Solana - Real-time position monitoring with Pyth price feeds - Dynamic stop-loss and take-profit management - n8n workflow integration for TradingView signals - Beautiful web UI for settings management - REST API for trade execution and monitoring - Next.js 15 with standalone output mode - TypeScript with strict typing - Docker containerization with multi-stage builds - PostgreSQL database for trade history - Singleton pattern for Drift client connection pooling - BN.js for BigNumber handling (Drift SDK requirement) - Configurable stop-loss and take-profit levels - Breakeven trigger and profit locking - Daily loss limits and trade cooldowns - Slippage tolerance controls - DRY_RUN mode for safe testing - Real-time risk calculator - Interactive sliders for all parameters - Live preview of trade outcomes - Position sizing and leverage controls - Beautiful gradient design with Tailwind CSS - POST /api/trading/execute - Execute trades - POST /api/trading/close - Close positions - GET /api/trading/positions - Monitor active trades - GET /api/trading/check-risk - Validate trade signals - GET /api/settings - View configuration - POST /api/settings - Update configuration - Fixed Borsh serialization errors (simplified order params) - Resolved RPC rate limiting with singleton pattern - Fixed BigInt vs BN type mismatches - Corrected order execution flow - Improved position state management - Complete setup guides - Docker deployment instructions - n8n workflow configuration - API reference documentation - Risk management guidelines - Runs on port 3001 (external), 3000 (internal) - Uses Helius RPC for optimal performance - Production-ready with error handling - Health monitoring and logging
This commit is contained in:
260
lib/pyth/price-monitor.ts
Normal file
260
lib/pyth/price-monitor.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Pyth Price Feed Integration
|
||||
*
|
||||
* Real-time price monitoring using Pyth Network oracles
|
||||
*/
|
||||
|
||||
import { Connection, PublicKey } from '@solana/web3.js'
|
||||
import { PriceServiceConnection } from '@pythnetwork/price-service-client'
|
||||
import { getMarketConfig } from '../../config/trading'
|
||||
|
||||
export interface PriceUpdate {
|
||||
symbol: string
|
||||
price: number
|
||||
confidence: number
|
||||
timestamp: number
|
||||
slot?: number
|
||||
expo: number
|
||||
}
|
||||
|
||||
export interface PriceMonitorConfig {
|
||||
symbols: string[] // e.g., ['SOL-PERP', 'BTC-PERP']
|
||||
onPriceUpdate: (update: PriceUpdate) => void | Promise<void>
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Pyth Price Monitor
|
||||
*
|
||||
* Monitors prices via WebSocket with RPC polling fallback
|
||||
*/
|
||||
export class PythPriceMonitor {
|
||||
private priceService: PriceServiceConnection
|
||||
private connection: Connection
|
||||
private isMonitoring: boolean = false
|
||||
private priceCache: Map<string, PriceUpdate> = new Map()
|
||||
private pollingIntervals: Map<string, NodeJS.Timeout> = new Map()
|
||||
private lastUpdateTime: Map<string, number> = new Map()
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
hermesUrl: string = 'https://hermes.pyth.network'
|
||||
) {
|
||||
this.connection = connection
|
||||
this.priceService = new PriceServiceConnection(hermesUrl, {
|
||||
priceFeedRequestConfig: {
|
||||
binary: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Pyth price monitor created')
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring prices for multiple symbols
|
||||
*/
|
||||
async start(config: PriceMonitorConfig): Promise<void> {
|
||||
if (this.isMonitoring) {
|
||||
console.warn('⚠️ Price monitor already running')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 Starting Pyth price monitor for:', config.symbols)
|
||||
|
||||
try {
|
||||
// Get Pyth price feed IDs for all symbols
|
||||
const priceIds = config.symbols.map(symbol => {
|
||||
const marketConfig = getMarketConfig(symbol)
|
||||
return marketConfig.pythPriceFeedId
|
||||
})
|
||||
|
||||
console.log('📡 Subscribing to Pyth WebSocket...')
|
||||
|
||||
// Subscribe to Pyth WebSocket for real-time updates
|
||||
this.priceService.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
|
||||
try {
|
||||
const price = priceFeed.getPriceUnchecked()
|
||||
|
||||
// Find which symbol this feed belongs to
|
||||
const symbol = config.symbols.find(sym => {
|
||||
const marketConfig = getMarketConfig(sym)
|
||||
return marketConfig.pythPriceFeedId === `0x${priceFeed.id}`
|
||||
})
|
||||
|
||||
if (symbol && price) {
|
||||
const priceNumber = Number(price.price) * Math.pow(10, price.expo)
|
||||
const confidenceNumber = Number(price.conf) * Math.pow(10, price.expo)
|
||||
|
||||
const update: PriceUpdate = {
|
||||
symbol,
|
||||
price: priceNumber,
|
||||
confidence: confidenceNumber,
|
||||
timestamp: Date.now(),
|
||||
expo: price.expo,
|
||||
}
|
||||
|
||||
// Cache the update
|
||||
this.priceCache.set(symbol, update)
|
||||
this.lastUpdateTime.set(symbol, Date.now())
|
||||
|
||||
// Notify callback
|
||||
Promise.resolve(config.onPriceUpdate(update)).catch(error => {
|
||||
if (config.onError) {
|
||||
config.onError(error as Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error processing Pyth price update:', error)
|
||||
if (config.onError) {
|
||||
config.onError(error as Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ Pyth WebSocket subscribed')
|
||||
|
||||
// Start polling fallback (every 2 seconds) in case WebSocket fails
|
||||
this.startPollingFallback(config)
|
||||
|
||||
this.isMonitoring = true
|
||||
console.log('✅ Price monitoring active')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start price monitor:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling fallback - checks prices every 2 seconds via RPC
|
||||
*/
|
||||
private startPollingFallback(config: PriceMonitorConfig): void {
|
||||
console.log('🔄 Starting polling fallback (every 2s)...')
|
||||
|
||||
for (const symbol of config.symbols) {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
// Only poll if WebSocket hasn't updated in 5 seconds
|
||||
const lastUpdate = this.lastUpdateTime.get(symbol) || 0
|
||||
const timeSinceUpdate = Date.now() - lastUpdate
|
||||
|
||||
if (timeSinceUpdate > 5000) {
|
||||
console.log(`⚠️ WebSocket stale for ${symbol}, using polling fallback`)
|
||||
await this.fetchPriceViaRPC(symbol, config.onPriceUpdate)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Polling error for ${symbol}:`, error)
|
||||
if (config.onError) {
|
||||
config.onError(error as Error)
|
||||
}
|
||||
}
|
||||
}, 2000) // Poll every 2 seconds
|
||||
|
||||
this.pollingIntervals.set(symbol, interval)
|
||||
}
|
||||
|
||||
console.log('✅ Polling fallback active')
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch price via RPC (fallback method)
|
||||
*/
|
||||
private async fetchPriceViaRPC(
|
||||
symbol: string,
|
||||
onUpdate: (update: PriceUpdate) => void | Promise<void>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const priceIds = [getMarketConfig(symbol).pythPriceFeedId]
|
||||
const priceFeeds = await this.priceService.getLatestPriceFeeds(priceIds)
|
||||
|
||||
if (priceFeeds && priceFeeds.length > 0) {
|
||||
const priceFeed = priceFeeds[0]
|
||||
const price = priceFeed.getPriceUnchecked()
|
||||
|
||||
const priceNumber = Number(price.price) * Math.pow(10, price.expo)
|
||||
const confidenceNumber = Number(price.conf) * Math.pow(10, price.expo)
|
||||
|
||||
const update: PriceUpdate = {
|
||||
symbol,
|
||||
price: priceNumber,
|
||||
confidence: confidenceNumber,
|
||||
timestamp: Date.now(),
|
||||
expo: price.expo,
|
||||
}
|
||||
|
||||
this.priceCache.set(symbol, update)
|
||||
this.lastUpdateTime.set(symbol, Date.now())
|
||||
|
||||
await onUpdate(update)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ RPC fetch failed for ${symbol}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached price (instant, no network call)
|
||||
*/
|
||||
getCachedPrice(symbol: string): PriceUpdate | null {
|
||||
return this.priceCache.get(symbol) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached prices
|
||||
*/
|
||||
getAllCachedPrices(): Map<string, PriceUpdate> {
|
||||
return new Map(this.priceCache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if monitoring is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.isMonitoring
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isMonitoring) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🛑 Stopping price monitor...')
|
||||
|
||||
// Clear polling intervals
|
||||
this.pollingIntervals.forEach(interval => clearInterval(interval))
|
||||
this.pollingIntervals.clear()
|
||||
|
||||
// Close Pyth WebSocket (if implemented by library)
|
||||
// Note: PriceServiceConnection doesn't have explicit close method
|
||||
// WebSocket will be garbage collected
|
||||
|
||||
this.priceCache.clear()
|
||||
this.lastUpdateTime.clear()
|
||||
this.isMonitoring = false
|
||||
|
||||
console.log('✅ Price monitor stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let pythPriceMonitorInstance: PythPriceMonitor | null = null
|
||||
|
||||
export function getPythPriceMonitor(): PythPriceMonitor {
|
||||
if (!pythPriceMonitorInstance) {
|
||||
const connection = new Connection(
|
||||
process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
|
||||
'confirmed'
|
||||
)
|
||||
|
||||
const hermesUrl = process.env.PYTH_HERMES_URL || 'https://hermes.pyth.network'
|
||||
|
||||
pythPriceMonitorInstance = new PythPriceMonitor(connection, hermesUrl)
|
||||
}
|
||||
|
||||
return pythPriceMonitorInstance
|
||||
}
|
||||
Reference in New Issue
Block a user