Phase 1: Add MAE/MFE tracking and analytics schema
- Added 20+ analytics fields to Trade model (MAE/MFE, fill tracking, timing, market context, slippage) - Implemented real-time MAE/MFE tracking in Position Manager (updates every 5s) - Enhanced database schema with comprehensive trade analytics - Updated all API endpoints to initialize MAE/MFE fields - Modified updateTradeState() to persist MAE/MFE in configSnapshot Database changes: - maxFavorableExcursion/maxAdverseExcursion track best/worst profit % - maxFavorablePrice/maxAdversePrice track exact price levels - Fill tracking: tp1Filled, tp2Filled, softSlFilled, hardSlFilled - Timing metrics: timeToTp1, timeToTp2, timeToSl - Market context: atrAtEntry, adxAtEntry, volumeAtEntry, fundingRateAtEntry, basisAtEntry - Slippage tracking: expectedEntryPrice, entrySlippagePct, expectedExitPrice, exitSlippagePct Position Manager changes: - Track MAE/MFE on every price check (2s interval) - Throttled database updates (5s interval) via updateTradeMetrics() - Persist MAE/MFE in trade state snapshots for recovery Next: Phase 2 (market context capture) or Phase 3 (analytics API)
This commit is contained in:
@@ -241,6 +241,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
|
maxFavorableExcursion: 0,
|
||||||
|
maxAdverseExcursion: 0,
|
||||||
|
maxFavorablePrice: entryPrice,
|
||||||
|
maxAdversePrice: entryPrice,
|
||||||
|
lastDbMetricsUpdate: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Place on-chain TP/SL orders BEFORE adding to Position Manager
|
// CRITICAL FIX: Place on-chain TP/SL orders BEFORE adding to Position Manager
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
|
maxFavorableExcursion: 0,
|
||||||
|
maxAdverseExcursion: 0,
|
||||||
|
maxFavorablePrice: entryPrice,
|
||||||
|
maxAdversePrice: entryPrice,
|
||||||
|
lastDbMetricsUpdate: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to position manager
|
// Add to position manager
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: entryPrice,
|
lastPrice: entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
|
maxFavorableExcursion: 0,
|
||||||
|
maxAdverseExcursion: 0,
|
||||||
|
maxFavorablePrice: entryPrice,
|
||||||
|
maxAdversePrice: entryPrice,
|
||||||
|
lastDbMetricsUpdate: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to position manager for monitoring
|
// Add to position manager for monitoring
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ export interface UpdateTradeStateParams {
|
|||||||
unrealizedPnL: number
|
unrealizedPnL: number
|
||||||
peakPnL: number
|
peakPnL: number
|
||||||
lastPrice: number
|
lastPrice: number
|
||||||
|
maxFavorableExcursion?: number
|
||||||
|
maxAdverseExcursion?: number
|
||||||
|
maxFavorablePrice?: number
|
||||||
|
maxAdversePrice?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTradeExitParams {
|
export interface UpdateTradeExitParams {
|
||||||
@@ -184,6 +188,10 @@ export async function updateTradeState(params: UpdateTradeStateParams) {
|
|||||||
unrealizedPnL: params.unrealizedPnL,
|
unrealizedPnL: params.unrealizedPnL,
|
||||||
peakPnL: params.peakPnL,
|
peakPnL: params.peakPnL,
|
||||||
lastPrice: params.lastPrice,
|
lastPrice: params.lastPrice,
|
||||||
|
maxFavorableExcursion: params.maxFavorableExcursion,
|
||||||
|
maxAdverseExcursion: params.maxAdverseExcursion,
|
||||||
|
maxFavorablePrice: params.maxFavorablePrice,
|
||||||
|
maxAdversePrice: params.maxAdversePrice,
|
||||||
lastUpdate: new Date().toISOString(),
|
lastUpdate: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export interface ActiveTrade {
|
|||||||
peakPnL: number
|
peakPnL: number
|
||||||
peakPrice: number // Track highest price reached (for trailing)
|
peakPrice: number // Track highest price reached (for trailing)
|
||||||
|
|
||||||
|
// MAE/MFE tracking (Maximum Adverse/Favorable Excursion)
|
||||||
|
maxFavorableExcursion: number // Best profit % reached
|
||||||
|
maxAdverseExcursion: number // Worst drawdown % reached
|
||||||
|
maxFavorablePrice: number // Best price hit
|
||||||
|
maxAdversePrice: number // Worst price hit
|
||||||
|
lastDbMetricsUpdate: number // Last time we updated MAE/MFE in DB (throttle to 5s)
|
||||||
|
|
||||||
// Monitoring
|
// Monitoring
|
||||||
priceCheckCount: number
|
priceCheckCount: number
|
||||||
lastPrice: number
|
lastPrice: number
|
||||||
@@ -110,6 +117,11 @@ export class PositionManager {
|
|||||||
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
||||||
peakPnL: pmState?.peakPnL ?? 0,
|
peakPnL: pmState?.peakPnL ?? 0,
|
||||||
peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice,
|
peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice,
|
||||||
|
maxFavorableExcursion: pmState?.maxFavorableExcursion ?? 0,
|
||||||
|
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
|
||||||
|
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
|
||||||
|
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
|
||||||
|
lastDbMetricsUpdate: Date.now(),
|
||||||
priceCheckCount: 0,
|
priceCheckCount: 0,
|
||||||
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
||||||
lastUpdateTime: Date.now(),
|
lastUpdateTime: Date.now(),
|
||||||
@@ -394,6 +406,23 @@ export class PositionManager {
|
|||||||
const accountPnL = profitPercent * trade.leverage
|
const accountPnL = profitPercent * trade.leverage
|
||||||
trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100
|
trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100
|
||||||
|
|
||||||
|
// Track MAE/MFE (Maximum Adverse/Favorable Excursion)
|
||||||
|
if (profitPercent > trade.maxFavorableExcursion) {
|
||||||
|
trade.maxFavorableExcursion = profitPercent
|
||||||
|
trade.maxFavorablePrice = currentPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profitPercent < trade.maxAdverseExcursion) {
|
||||||
|
trade.maxAdverseExcursion = profitPercent
|
||||||
|
trade.maxAdversePrice = currentPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update MAE/MFE in database (throttled to every 5 seconds to avoid spam)
|
||||||
|
if (Date.now() - trade.lastDbMetricsUpdate > 5000) {
|
||||||
|
await this.updateTradeMetrics(trade)
|
||||||
|
trade.lastDbMetricsUpdate = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
// Track peak P&L
|
// Track peak P&L
|
||||||
if (trade.unrealizedPnL > trade.peakPnL) {
|
if (trade.unrealizedPnL > trade.peakPnL) {
|
||||||
trade.peakPnL = trade.unrealizedPnL
|
trade.peakPnL = trade.unrealizedPnL
|
||||||
@@ -708,6 +737,10 @@ export class PositionManager {
|
|||||||
unrealizedPnL: trade.unrealizedPnL,
|
unrealizedPnL: trade.unrealizedPnL,
|
||||||
peakPnL: trade.peakPnL,
|
peakPnL: trade.peakPnL,
|
||||||
lastPrice: trade.lastPrice,
|
lastPrice: trade.lastPrice,
|
||||||
|
maxFavorableExcursion: trade.maxFavorableExcursion,
|
||||||
|
maxAdverseExcursion: trade.maxAdverseExcursion,
|
||||||
|
maxFavorablePrice: trade.maxFavorablePrice,
|
||||||
|
maxAdversePrice: trade.maxAdversePrice,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to save trade state:', error)
|
console.error('❌ Failed to save trade state:', error)
|
||||||
@@ -733,6 +766,29 @@ export class PositionManager {
|
|||||||
symbols,
|
symbols,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update MAE/MFE metrics in database (throttled)
|
||||||
|
*/
|
||||||
|
private async updateTradeMetrics(trade: ActiveTrade): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { getPrismaClient } = await import('../database/trades')
|
||||||
|
const prisma = getPrismaClient()
|
||||||
|
|
||||||
|
await prisma.trade.update({
|
||||||
|
where: { id: trade.id },
|
||||||
|
data: {
|
||||||
|
maxFavorableExcursion: trade.maxFavorableExcursion,
|
||||||
|
maxAdverseExcursion: trade.maxAdverseExcursion,
|
||||||
|
maxFavorablePrice: trade.maxFavorablePrice,
|
||||||
|
maxAdversePrice: trade.maxAdversePrice,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Silent failure to avoid disrupting monitoring loop
|
||||||
|
console.error('Failed to update trade metrics:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Trade" ADD COLUMN "adxAtEntry" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "atrAtEntry" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "basisAtEntry" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "entrySlippagePct" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "exitSlippagePct" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "expectedEntryPrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "expectedExitPrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "fundingRateAtEntry" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "hardSlFilled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "maxAdverseExcursion" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "maxAdversePrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "maxFavorableExcursion" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "maxFavorablePrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "slFillPrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "softSlFilled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "timeToSl" INTEGER,
|
||||||
|
ADD COLUMN "timeToTp1" INTEGER,
|
||||||
|
ADD COLUMN "timeToTp2" INTEGER,
|
||||||
|
ADD COLUMN "tp1FillPrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "tp1Filled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "tp2FillPrice" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "tp2Filled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "volumeAtEntry" DOUBLE PRECISION;
|
||||||
@@ -49,6 +49,39 @@ model Trade {
|
|||||||
maxDrawdown Float? // Peak to valley during trade
|
maxDrawdown Float? // Peak to valley during trade
|
||||||
maxGain Float? // Peak gain reached
|
maxGain Float? // Peak gain reached
|
||||||
|
|
||||||
|
// MAE/MFE Analysis (Maximum Adverse/Favorable Excursion)
|
||||||
|
maxFavorableExcursion Float? // Best profit % reached during trade
|
||||||
|
maxAdverseExcursion Float? // Worst drawdown % during trade
|
||||||
|
maxFavorablePrice Float? // Best price hit (direction-aware)
|
||||||
|
maxAdversePrice Float? // Worst price hit (direction-aware)
|
||||||
|
|
||||||
|
// Exit details - which levels actually filled
|
||||||
|
tp1Filled Boolean @default(false)
|
||||||
|
tp2Filled Boolean @default(false)
|
||||||
|
softSlFilled Boolean @default(false)
|
||||||
|
hardSlFilled Boolean @default(false)
|
||||||
|
tp1FillPrice Float?
|
||||||
|
tp2FillPrice Float?
|
||||||
|
slFillPrice Float?
|
||||||
|
|
||||||
|
// Timing metrics
|
||||||
|
timeToTp1 Int? // Seconds from entry to TP1 fill
|
||||||
|
timeToTp2 Int? // Seconds from entry to TP2 fill
|
||||||
|
timeToSl Int? // Seconds from entry to SL hit
|
||||||
|
|
||||||
|
// Market context at entry
|
||||||
|
atrAtEntry Float? // ATR% when trade opened
|
||||||
|
adxAtEntry Float? // ADX trend strength (0-50)
|
||||||
|
volumeAtEntry Float? // Volume relative to MA
|
||||||
|
fundingRateAtEntry Float? // Perp funding rate at entry
|
||||||
|
basisAtEntry Float? // Perp-spot basis at entry
|
||||||
|
|
||||||
|
// Slippage tracking
|
||||||
|
expectedEntryPrice Float? // Target entry from signal
|
||||||
|
entrySlippagePct Float? // Actual slippage %
|
||||||
|
expectedExitPrice Float? // Which TP/SL should have hit
|
||||||
|
exitSlippagePct Float? // Exit slippage %
|
||||||
|
|
||||||
// Order signatures
|
// Order signatures
|
||||||
entryOrderTx String
|
entryOrderTx String
|
||||||
tp1OrderTx String?
|
tp1OrderTx String?
|
||||||
|
|||||||
Reference in New Issue
Block a user