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

@@ -45,6 +45,19 @@ export interface CreateTradeParams {
isTestTrade?: boolean
}
export interface UpdateTradeStateParams {
positionId: string
currentSize: number
tp1Hit: boolean
slMovedToBreakeven: boolean
slMovedToProfit: boolean
stopLossPrice: number
realizedPnL: number
unrealizedPnL: number
peakPnL: number
lastPrice: number
}
export interface UpdateTradeExitParams {
positionId: string
exitPrice: number
@@ -144,6 +157,66 @@ export async function updateTradeExit(params: UpdateTradeExitParams) {
}
}
/**
* Update active trade state (for Position Manager persistence)
*/
export async function updateTradeState(params: UpdateTradeStateParams) {
const prisma = getPrismaClient()
try {
const trade = await prisma.trade.update({
where: { positionId: params.positionId },
data: {
// Store Position Manager state in configSnapshot
configSnapshot: {
...(await prisma.trade.findUnique({
where: { positionId: params.positionId },
select: { configSnapshot: true }
}))?.configSnapshot as any,
// Add Position Manager state
positionManagerState: {
currentSize: params.currentSize,
tp1Hit: params.tp1Hit,
slMovedToBreakeven: params.slMovedToBreakeven,
slMovedToProfit: params.slMovedToProfit,
stopLossPrice: params.stopLossPrice,
realizedPnL: params.realizedPnL,
unrealizedPnL: params.unrealizedPnL,
peakPnL: params.peakPnL,
lastPrice: params.lastPrice,
lastUpdate: new Date().toISOString(),
}
}
},
})
return trade
} catch (error) {
console.error('❌ Failed to update trade state:', error)
// Don't throw - state updates are non-critical
}
}
/**
* Get all open trades (for Position Manager recovery)
*/
export async function getOpenTrades() {
const prisma = getPrismaClient()
try {
const trades = await prisma.trade.findMany({
where: { status: 'open' },
orderBy: { entryTime: 'asc' },
})
console.log(`📊 Found ${trades.length} open trades to restore`)
return trades
} catch (error) {
console.error('❌ Failed to get open trades:', error)
return []
}
}
/**
* Add price update for a trade (for tracking max gain/drawdown)
*/

View File

@@ -498,6 +498,15 @@ export async function closePosition(
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
// If closing 100%, cancel all remaining orders for this market
if (params.percentToClose === 100) {
console.log('🗑️ Position fully closed, cancelling remaining orders...')
const cancelResult = await cancelAllOrders(params.symbol)
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`)
}
}
return {
success: true,
transactionSignature: txSig,
@@ -515,6 +524,68 @@ export async function closePosition(
}
}
/**
* Cancel all open orders for a specific market
*/
export async function cancelAllOrders(
symbol: string
): Promise<{ success: boolean; cancelledCount?: number; error?: string }> {
try {
console.log(`🗑️ Cancelling all orders for ${symbol}...`)
const driftService = getDriftService()
const driftClient = driftService.getClient()
const marketConfig = getMarketConfig(symbol)
const isDryRun = process.env.DRY_RUN === 'true'
if (isDryRun) {
console.log('🧪 DRY RUN: Simulating order cancellation')
return { success: true, cancelledCount: 0 }
}
// Get user account to check for orders
const userAccount = driftClient.getUserAccount()
if (!userAccount) {
throw new Error('User account not found')
}
// Filter orders for this market
const ordersToCancel = userAccount.orders.filter(
(order: any) =>
order.marketIndex === marketConfig.driftMarketIndex &&
order.status === 0 // 0 = Open status
)
if (ordersToCancel.length === 0) {
console.log('✅ No open orders to cancel')
return { success: true, cancelledCount: 0 }
}
console.log(`📋 Found ${ordersToCancel.length} open orders to cancel`)
// Cancel all orders for this market
const txSig = await driftClient.cancelOrders(
undefined, // Cancel by market type
marketConfig.driftMarketIndex,
undefined // No specific direction filter
)
console.log(`✅ Orders cancelled! Transaction: ${txSig}`)
return {
success: true,
cancelledCount: ordersToCancel.length,
}
} catch (error) {
console.error('❌ Failed to cancel orders:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Close entire position for a market
*/

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
}