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:
mindesbunister
2025-10-27 10:39:05 +01:00
parent f571d459e4
commit d3c04ea9c9
9 changed files with 1122 additions and 8 deletions

View File

@@ -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
}