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:
435
lib/trading/position-manager.ts
Normal file
435
lib/trading/position-manager.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* Position Manager
|
||||
*
|
||||
* Tracks active trades and manages automatic exits
|
||||
*/
|
||||
|
||||
import { getDriftService } from '../drift/client'
|
||||
import { closePosition } from '../drift/orders'
|
||||
import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor'
|
||||
import { getMergedConfig, TradingConfig } from '../../config/trading'
|
||||
|
||||
export interface ActiveTrade {
|
||||
id: string
|
||||
positionId: string // Transaction signature
|
||||
symbol: string
|
||||
direction: 'long' | 'short'
|
||||
|
||||
// Entry details
|
||||
entryPrice: number
|
||||
entryTime: number
|
||||
positionSize: number
|
||||
leverage: number
|
||||
|
||||
// Targets
|
||||
stopLossPrice: number
|
||||
tp1Price: number
|
||||
tp2Price: number
|
||||
emergencyStopPrice: number
|
||||
|
||||
// State
|
||||
currentSize: number // Changes after TP1
|
||||
tp1Hit: boolean
|
||||
slMovedToBreakeven: boolean
|
||||
slMovedToProfit: boolean
|
||||
|
||||
// P&L tracking
|
||||
realizedPnL: number
|
||||
unrealizedPnL: number
|
||||
peakPnL: number
|
||||
|
||||
// Monitoring
|
||||
priceCheckCount: number
|
||||
lastPrice: number
|
||||
lastUpdateTime: number
|
||||
}
|
||||
|
||||
export interface ExitResult {
|
||||
success: boolean
|
||||
reason: 'TP1' | 'TP2' | 'SL' | 'emergency' | 'manual' | 'error'
|
||||
closePrice?: number
|
||||
closedSize?: number
|
||||
realizedPnL?: number
|
||||
transactionSignature?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class PositionManager {
|
||||
private activeTrades: Map<string, ActiveTrade> = new Map()
|
||||
private config: TradingConfig
|
||||
private isMonitoring: boolean = false
|
||||
|
||||
constructor(config?: Partial<TradingConfig>) {
|
||||
this.config = getMergedConfig(config)
|
||||
console.log('✅ Position manager created')
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new trade to monitor
|
||||
*/
|
||||
async addTrade(trade: ActiveTrade): Promise<void> {
|
||||
console.log(`📊 Adding trade to monitor: ${trade.symbol} ${trade.direction}`)
|
||||
|
||||
this.activeTrades.set(trade.id, trade)
|
||||
|
||||
console.log(`✅ Trade added. Active trades: ${this.activeTrades.size}`)
|
||||
|
||||
// Start monitoring if not already running
|
||||
if (!this.isMonitoring && this.activeTrades.size > 0) {
|
||||
await this.startMonitoring()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a trade from monitoring
|
||||
*/
|
||||
removeTrade(tradeId: string): void {
|
||||
const trade = this.activeTrades.get(tradeId)
|
||||
if (trade) {
|
||||
console.log(`🗑️ Removing trade: ${trade.symbol}`)
|
||||
this.activeTrades.delete(tradeId)
|
||||
|
||||
// Stop monitoring if no more trades
|
||||
if (this.activeTrades.size === 0 && this.isMonitoring) {
|
||||
this.stopMonitoring()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active trades
|
||||
*/
|
||||
getActiveTrades(): ActiveTrade[] {
|
||||
return Array.from(this.activeTrades.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific trade
|
||||
*/
|
||||
getTrade(tradeId: string): ActiveTrade | null {
|
||||
return this.activeTrades.get(tradeId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Start price monitoring for all active trades
|
||||
*/
|
||||
private async startMonitoring(): Promise<void> {
|
||||
if (this.isMonitoring) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get unique symbols from active trades
|
||||
const symbols = [...new Set(
|
||||
Array.from(this.activeTrades.values()).map(trade => trade.symbol)
|
||||
)]
|
||||
|
||||
if (symbols.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 Starting price monitoring for:', symbols)
|
||||
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
|
||||
await priceMonitor.start({
|
||||
symbols,
|
||||
onPriceUpdate: async (update: PriceUpdate) => {
|
||||
await this.handlePriceUpdate(update)
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('❌ Price monitor error:', error)
|
||||
},
|
||||
})
|
||||
|
||||
this.isMonitoring = true
|
||||
console.log('✅ Position monitoring active')
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop price monitoring
|
||||
*/
|
||||
private async stopMonitoring(): Promise<void> {
|
||||
if (!this.isMonitoring) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🛑 Stopping position monitoring...')
|
||||
|
||||
const priceMonitor = getPythPriceMonitor()
|
||||
await priceMonitor.stop()
|
||||
|
||||
this.isMonitoring = false
|
||||
console.log('✅ Position monitoring stopped')
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle price update for all relevant trades
|
||||
*/
|
||||
private async handlePriceUpdate(update: PriceUpdate): Promise<void> {
|
||||
// Find all trades for this symbol
|
||||
const tradesForSymbol = Array.from(this.activeTrades.values())
|
||||
.filter(trade => trade.symbol === update.symbol)
|
||||
|
||||
for (const trade of tradesForSymbol) {
|
||||
try {
|
||||
await this.checkTradeConditions(trade, update.price)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error checking trade ${trade.id}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any exit conditions are met for a trade
|
||||
*/
|
||||
private async checkTradeConditions(
|
||||
trade: ActiveTrade,
|
||||
currentPrice: number
|
||||
): Promise<void> {
|
||||
// Update trade data
|
||||
trade.lastPrice = currentPrice
|
||||
trade.lastUpdateTime = Date.now()
|
||||
trade.priceCheckCount++
|
||||
|
||||
// Calculate P&L
|
||||
const profitPercent = this.calculateProfitPercent(
|
||||
trade.entryPrice,
|
||||
currentPrice,
|
||||
trade.direction
|
||||
)
|
||||
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100
|
||||
|
||||
// Track peak P&L
|
||||
if (trade.unrealizedPnL > trade.peakPnL) {
|
||||
trade.peakPnL = trade.unrealizedPnL
|
||||
}
|
||||
|
||||
// Log status every 10 checks (~20 seconds)
|
||||
if (trade.priceCheckCount % 10 === 0) {
|
||||
console.log(
|
||||
`📊 ${trade.symbol} | ` +
|
||||
`Price: ${currentPrice.toFixed(4)} | ` +
|
||||
`P&L: ${profitPercent.toFixed(2)}% (${accountPnL.toFixed(1)}% acct) | ` +
|
||||
`Unrealized: $${trade.unrealizedPnL.toFixed(2)} | ` +
|
||||
`Peak: $${trade.peakPnL.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Check exit conditions (in order of priority)
|
||||
|
||||
// 1. Emergency stop (-2%)
|
||||
if (this.shouldEmergencyStop(currentPrice, trade)) {
|
||||
console.log(`🚨 EMERGENCY STOP: ${trade.symbol}`)
|
||||
await this.executeExit(trade, 100, 'emergency', currentPrice)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Stop loss
|
||||
if (!trade.tp1Hit && this.shouldStopLoss(currentPrice, trade)) {
|
||||
console.log(`🔴 STOP LOSS: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
await this.executeExit(trade, 100, 'SL', currentPrice)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Take profit 1 (50%)
|
||||
if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) {
|
||||
console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
await this.executeExit(trade, 50, 'TP1', currentPrice)
|
||||
|
||||
// Move SL to breakeven
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = trade.positionSize * 0.5
|
||||
trade.stopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
0.15, // +0.15% to cover fees
|
||||
trade.direction
|
||||
)
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 SL moved to breakeven: ${trade.stopLossPrice.toFixed(4)}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Profit lock trigger
|
||||
if (
|
||||
trade.tp1Hit &&
|
||||
!trade.slMovedToProfit &&
|
||||
profitPercent >= this.config.profitLockTriggerPercent
|
||||
) {
|
||||
console.log(`🔐 Profit lock trigger: ${trade.symbol}`)
|
||||
|
||||
trade.stopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
this.config.profitLockPercent,
|
||||
trade.direction
|
||||
)
|
||||
trade.slMovedToProfit = true
|
||||
|
||||
console.log(`🎯 SL moved to +${this.config.profitLockPercent}%: ${trade.stopLossPrice.toFixed(4)}`)
|
||||
}
|
||||
|
||||
// 5. Take profit 2 (remaining 50%)
|
||||
if (trade.tp1Hit && this.shouldTakeProfit2(currentPrice, trade)) {
|
||||
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
await this.executeExit(trade, 100, 'TP2', currentPrice)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute exit (close position)
|
||||
*/
|
||||
private async executeExit(
|
||||
trade: ActiveTrade,
|
||||
percentToClose: number,
|
||||
reason: ExitResult['reason'],
|
||||
currentPrice: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`🔴 Executing ${reason} for ${trade.symbol} (${percentToClose}%)`)
|
||||
|
||||
const result = await closePosition({
|
||||
symbol: trade.symbol,
|
||||
percentToClose,
|
||||
slippageTolerance: this.config.slippageTolerance,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`❌ Failed to close ${trade.symbol}:`, result.error)
|
||||
return
|
||||
}
|
||||
|
||||
// Update trade state
|
||||
if (percentToClose >= 100) {
|
||||
// Full close - remove from monitoring
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
this.removeTrade(trade.id)
|
||||
|
||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
} else {
|
||||
// Partial close (TP1)
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
trade.currentSize -= result.closedSize || 0
|
||||
|
||||
console.log(`✅ 50% closed | Realized: $${result.realizedPnL?.toFixed(2)} | Remaining: ${trade.currentSize}`)
|
||||
}
|
||||
|
||||
// TODO: Save to database
|
||||
// TODO: Send notification
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error executing exit for ${trade.symbol}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decision helpers
|
||||
*/
|
||||
private shouldEmergencyStop(price: number, trade: ActiveTrade): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price <= trade.emergencyStopPrice
|
||||
} else {
|
||||
return price >= trade.emergencyStopPrice
|
||||
}
|
||||
}
|
||||
|
||||
private shouldStopLoss(price: number, trade: ActiveTrade): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price <= trade.stopLossPrice
|
||||
} else {
|
||||
return price >= trade.stopLossPrice
|
||||
}
|
||||
}
|
||||
|
||||
private shouldTakeProfit1(price: number, trade: ActiveTrade): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp1Price
|
||||
} else {
|
||||
return price <= trade.tp1Price
|
||||
}
|
||||
}
|
||||
|
||||
private shouldTakeProfit2(price: number, trade: ActiveTrade): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp2Price
|
||||
} else {
|
||||
return price <= trade.tp2Price
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate profit percentage
|
||||
*/
|
||||
private calculateProfitPercent(
|
||||
entryPrice: number,
|
||||
currentPrice: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return ((currentPrice - entryPrice) / entryPrice) * 100
|
||||
} else {
|
||||
return ((entryPrice - currentPrice) / entryPrice) * 100
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price based on percentage
|
||||
*/
|
||||
private calculatePrice(
|
||||
entryPrice: number,
|
||||
percent: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return entryPrice * (1 + percent / 100)
|
||||
} else {
|
||||
return entryPrice * (1 - percent / 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emergency close all positions
|
||||
*/
|
||||
async closeAll(): Promise<void> {
|
||||
console.log('🚨 EMERGENCY: Closing all positions')
|
||||
|
||||
const trades = Array.from(this.activeTrades.values())
|
||||
|
||||
for (const trade of trades) {
|
||||
await this.executeExit(trade, 100, 'emergency', trade.lastPrice)
|
||||
}
|
||||
|
||||
console.log('✅ All positions closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monitoring status
|
||||
*/
|
||||
getStatus(): {
|
||||
isMonitoring: boolean
|
||||
activeTradesCount: number
|
||||
symbols: string[]
|
||||
} {
|
||||
const symbols = [...new Set(
|
||||
Array.from(this.activeTrades.values()).map(t => t.symbol)
|
||||
)]
|
||||
|
||||
return {
|
||||
isMonitoring: this.isMonitoring,
|
||||
activeTradesCount: this.activeTrades.size,
|
||||
symbols,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let positionManagerInstance: PositionManager | null = null
|
||||
|
||||
export function getPositionManager(): PositionManager {
|
||||
if (!positionManagerInstance) {
|
||||
positionManagerInstance = new PositionManager()
|
||||
}
|
||||
return positionManagerInstance
|
||||
}
|
||||
Reference in New Issue
Block a user