feat: Position Manager persistence + order cleanup + improved stop loss
- Add Position Manager state persistence to survive restarts - Auto-restore open trades from database on startup - Save state after TP1, SL adjustments, profit locks - Persist to configSnapshot JSON field - Add automatic order cancellation - Cancel all TP/SL orders when position fully closed - New cancelAllOrders() function in drift/orders.ts - Prevents orphaned orders after manual closes - Improve stop loss management - Move SL to +0.35% after TP1 (was +0.15%) - Gives more breathing room for retracements - Still locks in half of TP1 profit - Add database sync when Position Manager closes trades - Auto-update Trade record with exit data - Save P&L, exit reason, hold time - Fix analytics showing stale data - Add trade state management functions - updateTradeState() for Position Manager persistence - getOpenTrades() for startup restoration - getInitializedPositionManager() for async init - Create n8n database analytics workflows - Daily report workflow (automated at midnight) - Pattern analysis (hourly/daily performance) - Stop loss effectiveness analysis - Database analytics query workflow - Complete setup guide (N8N_DATABASE_SETUP.md)
This commit is contained in:
@@ -8,6 +8,7 @@ import { getDriftService } from '../drift/client'
|
||||
import { closePosition } from '../drift/orders'
|
||||
import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor'
|
||||
import { getMergedConfig, TradingConfig } from '../../config/trading'
|
||||
import { updateTradeExit, updateTradeState, getOpenTrades } from '../database/trades'
|
||||
|
||||
export interface ActiveTrade {
|
||||
id: string
|
||||
@@ -46,7 +47,7 @@ export interface ActiveTrade {
|
||||
|
||||
export interface ExitResult {
|
||||
success: boolean
|
||||
reason: 'TP1' | 'TP2' | 'SL' | 'emergency' | 'manual' | 'error'
|
||||
reason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'emergency' | 'manual' | 'error'
|
||||
closePrice?: number
|
||||
closedSize?: number
|
||||
realizedPnL?: number
|
||||
@@ -58,12 +59,74 @@ export class PositionManager {
|
||||
private activeTrades: Map<string, ActiveTrade> = new Map()
|
||||
private config: TradingConfig
|
||||
private isMonitoring: boolean = false
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(config?: Partial<TradingConfig>) {
|
||||
this.config = getMergedConfig(config)
|
||||
console.log('✅ Position manager created')
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and restore active trades from database
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔄 Restoring active trades from database...')
|
||||
|
||||
try {
|
||||
const openTrades = await getOpenTrades()
|
||||
|
||||
for (const dbTrade of openTrades) {
|
||||
// Extract Position Manager state from configSnapshot
|
||||
const pmState = (dbTrade.configSnapshot as any)?.positionManagerState
|
||||
|
||||
// Reconstruct ActiveTrade object
|
||||
const activeTrade: ActiveTrade = {
|
||||
id: dbTrade.id,
|
||||
positionId: dbTrade.positionId,
|
||||
symbol: dbTrade.symbol,
|
||||
direction: dbTrade.direction as 'long' | 'short',
|
||||
entryPrice: dbTrade.entryPrice,
|
||||
entryTime: dbTrade.entryTime.getTime(),
|
||||
positionSize: dbTrade.positionSizeUSD,
|
||||
leverage: dbTrade.leverage,
|
||||
stopLossPrice: pmState?.stopLossPrice ?? dbTrade.stopLossPrice,
|
||||
tp1Price: dbTrade.takeProfit1Price,
|
||||
tp2Price: dbTrade.takeProfit2Price,
|
||||
emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02),
|
||||
currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD,
|
||||
tp1Hit: pmState?.tp1Hit ?? false,
|
||||
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
|
||||
slMovedToProfit: pmState?.slMovedToProfit ?? false,
|
||||
realizedPnL: pmState?.realizedPnL ?? 0,
|
||||
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
||||
peakPnL: pmState?.peakPnL ?? 0,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
}
|
||||
|
||||
this.activeTrades.set(activeTrade.id, activeTrade)
|
||||
console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`)
|
||||
}
|
||||
|
||||
if (this.activeTrades.size > 0) {
|
||||
console.log(`🎯 Restored ${this.activeTrades.size} active trades`)
|
||||
await this.startMonitoring()
|
||||
} else {
|
||||
console.log('✅ No active trades to restore')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to restore active trades:', error)
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new trade to monitor
|
||||
*/
|
||||
@@ -72,6 +135,9 @@ export class PositionManager {
|
||||
|
||||
this.activeTrades.set(trade.id, trade)
|
||||
|
||||
// Save initial state to database
|
||||
await this.saveTradeState(trade)
|
||||
|
||||
console.log(`✅ Trade added. Active trades: ${this.activeTrades.size}`)
|
||||
|
||||
// Start monitoring if not already running
|
||||
@@ -238,17 +304,20 @@ export class PositionManager {
|
||||
console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
await this.executeExit(trade, 50, 'TP1', currentPrice)
|
||||
|
||||
// Move SL to breakeven
|
||||
// Move SL to secure profit after TP1
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = trade.positionSize * 0.5
|
||||
trade.stopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
0.15, // +0.15% to cover fees
|
||||
0.35, // +0.35% to secure profit and avoid stop-out on retracement
|
||||
trade.direction
|
||||
)
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 SL moved to breakeven: ${trade.stopLossPrice.toFixed(4)}`)
|
||||
console.log(`🔒 SL moved to +0.35% (half of TP1): ${trade.stopLossPrice.toFixed(4)}`)
|
||||
|
||||
// Save state after TP1
|
||||
await this.saveTradeState(trade)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -268,6 +337,9 @@ export class PositionManager {
|
||||
trade.slMovedToProfit = true
|
||||
|
||||
console.log(`🎯 SL moved to +${this.config.profitLockPercent}%: ${trade.stopLossPrice.toFixed(4)}`)
|
||||
|
||||
// Save state after profit lock
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// 5. Take profit 2 (remaining 50%)
|
||||
@@ -305,8 +377,29 @@ export class PositionManager {
|
||||
if (percentToClose >= 100) {
|
||||
// Full close - remove from monitoring
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
this.removeTrade(trade.id)
|
||||
|
||||
// Save to database (only for valid exit reasons)
|
||||
if (reason !== 'error') {
|
||||
try {
|
||||
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: result.closePrice || currentPrice,
|
||||
exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency',
|
||||
realizedPnL: trade.realizedPnL,
|
||||
exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE',
|
||||
holdTimeSeconds,
|
||||
maxDrawdown: 0, // TODO: Track this
|
||||
maxGain: trade.peakPnL,
|
||||
})
|
||||
console.log('💾 Trade saved to database')
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save trade exit to database:', dbError)
|
||||
// Don't fail the close if database fails
|
||||
}
|
||||
}
|
||||
|
||||
this.removeTrade(trade.id)
|
||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
} else {
|
||||
// Partial close (TP1)
|
||||
@@ -316,7 +409,6 @@ export class PositionManager {
|
||||
console.log(`✅ 50% closed | Realized: $${result.realizedPnL?.toFixed(2)} | Remaining: ${trade.currentSize}`)
|
||||
}
|
||||
|
||||
// TODO: Save to database
|
||||
// TODO: Send notification
|
||||
|
||||
} catch (error) {
|
||||
@@ -404,6 +496,29 @@ export class PositionManager {
|
||||
console.log('✅ All positions closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Save trade state to database (for persistence across restarts)
|
||||
*/
|
||||
private async saveTradeState(trade: ActiveTrade): Promise<void> {
|
||||
try {
|
||||
await updateTradeState({
|
||||
positionId: trade.positionId,
|
||||
currentSize: trade.currentSize,
|
||||
tp1Hit: trade.tp1Hit,
|
||||
slMovedToBreakeven: trade.slMovedToBreakeven,
|
||||
slMovedToProfit: trade.slMovedToProfit,
|
||||
stopLossPrice: trade.stopLossPrice,
|
||||
realizedPnL: trade.realizedPnL,
|
||||
unrealizedPnL: trade.unrealizedPnL,
|
||||
peakPnL: trade.peakPnL,
|
||||
lastPrice: trade.lastPrice,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save trade state:', error)
|
||||
// Don't throw - state save is non-critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monitoring status
|
||||
*/
|
||||
@@ -426,10 +541,26 @@ export class PositionManager {
|
||||
|
||||
// Singleton instance
|
||||
let positionManagerInstance: PositionManager | null = null
|
||||
let initPromise: Promise<void> | null = null
|
||||
|
||||
export function getPositionManager(): PositionManager {
|
||||
if (!positionManagerInstance) {
|
||||
positionManagerInstance = new PositionManager()
|
||||
|
||||
// Initialize asynchronously (restore trades from database)
|
||||
if (!initPromise) {
|
||||
initPromise = positionManagerInstance.initialize().catch(error => {
|
||||
console.error('❌ Failed to initialize Position Manager:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
return positionManagerInstance
|
||||
}
|
||||
|
||||
export async function getInitializedPositionManager(): Promise<PositionManager> {
|
||||
const manager = getPositionManager()
|
||||
if (initPromise) {
|
||||
await initPromise
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user