feat: Automated multi-timeframe price tracking system
Implemented comprehensive price tracking for multi-timeframe signal analysis. **Components Added:** - lib/analysis/blocked-signal-tracker.ts - Background job tracking prices - app/api/analytics/signal-tracking/route.ts - Status/metrics endpoint **Features:** - Automatic price tracking at 1min, 5min, 15min, 30min intervals - TP1/TP2/SL hit detection using ATR-based targets - Max favorable/adverse excursion tracking (MFE/MAE) - Analysis completion after 30 minutes - Background job runs every 5 minutes - Entry price captured from signal time **Database Changes:** - Added entryPrice field to BlockedSignal (for price tracking baseline) - Added maxFavorablePrice, maxAdversePrice fields - Added maxFavorableExcursion, maxAdverseExcursion fields **Integration:** - Auto-starts on container startup - Tracks all DATA_COLLECTION_ONLY signals - Uses same TP/SL calculation as live trades (ATR-based) - Calculates profit % based on direction (long vs short) **API Endpoints:** - GET /api/analytics/signal-tracking - View tracking status and metrics - POST /api/analytics/signal-tracking - Manually trigger update (auth required) **Purpose:** Enables data-driven multi-timeframe comparison. After 50+ signals per timeframe, can analyze which timeframe (5min vs 15min vs 1H vs 4H vs Daily) has best win rate, profit potential, and signal quality. **What It Tracks:** - Price at 1min, 5min, 15min, 30min after signal - Would TP1/TP2/SL have been hit? - Maximum profit/loss during 30min window - Complete analysis of signal profitability **How It Works:** 1. Signal comes in (15min, 1H, 4H, Daily) → saved to BlockedSignal 2. Background job runs every 5min 3. Queries current price from Pyth 4. Calculates profit % from entry 5. Checks if TP/SL thresholds crossed 6. Updates MFE/MAE if new highs/lows 7. After 30min, marks analysisComplete=true **Future Analysis:** After 50+ signals per timeframe: - Compare TP1 hit rates across timeframes - Identify which timeframe has highest win rate - Determine optimal signal frequency vs quality trade-off - Switch production to best-performing timeframe User requested: "i want all the bells and whistles. lets make the powerhouse more powerfull. i cant see any reason why we shouldnt"
This commit is contained in:
166
app/api/analytics/signal-tracking/route.ts
Normal file
166
app/api/analytics/signal-tracking/route.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Blocked Signal Tracking Status API
|
||||||
|
*
|
||||||
|
* GET: View tracking status and statistics
|
||||||
|
* POST: Manually trigger tracking update (requires auth)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPrismaClient } from '@/lib/database/trades'
|
||||||
|
import { getBlockedSignalTracker } from '@/lib/analysis/blocked-signal-tracker'
|
||||||
|
|
||||||
|
// GET: View tracking status
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const prisma = getPrismaClient()
|
||||||
|
|
||||||
|
// Get tracking statistics
|
||||||
|
const total = await prisma.blockedSignal.count({
|
||||||
|
where: { blockReason: 'DATA_COLLECTION_ONLY' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const incomplete = await prisma.blockedSignal.count({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
analysisComplete: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const complete = await prisma.blockedSignal.count({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
analysisComplete: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get completion rates by timeframe
|
||||||
|
const byTimeframe = await prisma.blockedSignal.groupBy({
|
||||||
|
by: ['timeframe'],
|
||||||
|
where: { blockReason: 'DATA_COLLECTION_ONLY' },
|
||||||
|
_count: { id: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get signals with price data
|
||||||
|
const withPriceData = await prisma.blockedSignal.count({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
priceAfter1Min: { not: null }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get TP/SL hit rates
|
||||||
|
const tp1Hits = await prisma.blockedSignal.count({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
wouldHitTP1: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const slHits = await prisma.blockedSignal.count({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
wouldHitSL: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get recent tracked signals
|
||||||
|
const recentSignals = await prisma.blockedSignal.findMany({
|
||||||
|
where: { blockReason: 'DATA_COLLECTION_ONLY' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
timeframe: true,
|
||||||
|
symbol: true,
|
||||||
|
direction: true,
|
||||||
|
signalQualityScore: true,
|
||||||
|
priceAfter1Min: true,
|
||||||
|
priceAfter5Min: true,
|
||||||
|
priceAfter15Min: true,
|
||||||
|
priceAfter30Min: true,
|
||||||
|
wouldHitTP1: true,
|
||||||
|
wouldHitTP2: true,
|
||||||
|
wouldHitSL: true,
|
||||||
|
analysisComplete: true,
|
||||||
|
createdAt: true
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
tracking: {
|
||||||
|
total,
|
||||||
|
complete,
|
||||||
|
incomplete,
|
||||||
|
completionRate: total > 0 ? ((complete / total) * 100).toFixed(1) : '0.0'
|
||||||
|
},
|
||||||
|
byTimeframe: byTimeframe.map(tf => ({
|
||||||
|
timeframe: tf.timeframe,
|
||||||
|
count: tf._count.id
|
||||||
|
})),
|
||||||
|
metrics: {
|
||||||
|
withPriceData,
|
||||||
|
tp1Hits,
|
||||||
|
slHits,
|
||||||
|
tp1HitRate: complete > 0 ? ((tp1Hits / complete) * 100).toFixed(1) : '0.0',
|
||||||
|
slHitRate: complete > 0 ? ((slHits / complete) * 100).toFixed(1) : '0.0'
|
||||||
|
},
|
||||||
|
recentSignals: recentSignals.map((signal: any) => ({
|
||||||
|
id: signal.id,
|
||||||
|
timeframe: `${signal.timeframe}min`,
|
||||||
|
symbol: signal.symbol,
|
||||||
|
direction: signal.direction,
|
||||||
|
quality: signal.signalQualityScore,
|
||||||
|
price1min: signal.priceAfter1Min,
|
||||||
|
price5min: signal.priceAfter5Min,
|
||||||
|
price15min: signal.priceAfter15Min,
|
||||||
|
price30min: signal.priceAfter30Min,
|
||||||
|
hitTP1: signal.wouldHitTP1,
|
||||||
|
hitTP2: signal.wouldHitTP2,
|
||||||
|
hitSL: signal.wouldHitSL,
|
||||||
|
complete: signal.analysisComplete,
|
||||||
|
time: signal.createdAt
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting signal tracking status:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Failed to get tracking status' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Manually trigger tracking update
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check auth
|
||||||
|
const authHeader = request.headers.get('Authorization')
|
||||||
|
const apiKey = process.env.API_SECRET_KEY
|
||||||
|
|
||||||
|
if (!authHeader || !apiKey || authHeader !== `Bearer ${apiKey}`) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracker = getBlockedSignalTracker()
|
||||||
|
|
||||||
|
// Trigger manual update by restarting
|
||||||
|
console.log('🔄 Manual tracking update triggered')
|
||||||
|
tracker.stop()
|
||||||
|
tracker.start()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Tracking update triggered'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error triggering tracking update:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Failed to trigger update' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
|
|
||||||
console.log('🔍 Risk check for:', body)
|
console.log('🔍 Risk check for:', body)
|
||||||
|
|
||||||
|
// 🔬 MULTI-TIMEFRAME DATA COLLECTION
|
||||||
|
// Allow all non-5min signals to bypass risk checks (they'll be saved as data collection in execute endpoint)
|
||||||
|
const timeframe = body.timeframe || '5'
|
||||||
|
if (timeframe !== '5') {
|
||||||
|
console.log(`📊 DATA COLLECTION: ${timeframe}min signal bypassing risk checks (will save in execute endpoint)`)
|
||||||
|
return NextResponse.json({
|
||||||
|
allowed: true,
|
||||||
|
reason: 'Multi-timeframe data collection',
|
||||||
|
details: `${timeframe}min signal will be saved for analysis but not executed`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const config = getMergedConfig()
|
const config = getMergedConfig()
|
||||||
|
|
||||||
// Check for existing positions on the same symbol
|
// Check for existing positions on the same symbol
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/positi
|
|||||||
import { createTrade, updateTradeExit } from '@/lib/database/trades'
|
import { createTrade, updateTradeExit } from '@/lib/database/trades'
|
||||||
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
|
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
|
||||||
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
||||||
|
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
|
||||||
|
|
||||||
export interface ExecuteTradeRequest {
|
export interface ExecuteTradeRequest {
|
||||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||||
@@ -111,6 +112,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
if (timeframe !== '5') {
|
if (timeframe !== '5') {
|
||||||
console.log(`📊 DATA COLLECTION: ${timeframe}min signal from ${driftSymbol}, saving for analysis (not executing)`)
|
console.log(`📊 DATA COLLECTION: ${timeframe}min signal from ${driftSymbol}, saving for analysis (not executing)`)
|
||||||
|
|
||||||
|
// Get current price for entry tracking
|
||||||
|
const priceMonitor = getPythPriceMonitor()
|
||||||
|
const latestPrice = priceMonitor.getCachedPrice(driftSymbol)
|
||||||
|
const currentPrice = latestPrice?.price || body.signalPrice || 0
|
||||||
|
|
||||||
// Save to BlockedSignal for cross-timeframe analysis
|
// Save to BlockedSignal for cross-timeframe analysis
|
||||||
const { createBlockedSignal } = await import('@/lib/database/trades')
|
const { createBlockedSignal } = await import('@/lib/database/trades')
|
||||||
try {
|
try {
|
||||||
@@ -125,13 +131,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
volumeRatio: body.volumeRatio,
|
volumeRatio: body.volumeRatio,
|
||||||
pricePosition: body.pricePosition,
|
pricePosition: body.pricePosition,
|
||||||
timeframe: timeframe,
|
timeframe: timeframe,
|
||||||
signalPrice: body.signalPrice || 0,
|
signalPrice: currentPrice,
|
||||||
signalQualityScore: 0, // Not scored since not executed
|
signalQualityScore: 0, // Not scored since not executed
|
||||||
signalQualityVersion: 'data-collection',
|
signalQualityVersion: 'data-collection',
|
||||||
minScoreRequired: 0,
|
minScoreRequired: 0,
|
||||||
scoreBreakdown: {},
|
scoreBreakdown: {},
|
||||||
})
|
})
|
||||||
console.log(`✅ ${timeframe}min signal saved to database for future analysis`)
|
console.log(`✅ ${timeframe}min signal saved at $${currentPrice.toFixed(2)} for future analysis`)
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error(`❌ Failed to save ${timeframe}min signal:`, dbError)
|
console.error(`❌ Failed to save ${timeframe}min signal:`, dbError)
|
||||||
}
|
}
|
||||||
|
|||||||
284
lib/analysis/blocked-signal-tracker.ts
Normal file
284
lib/analysis/blocked-signal-tracker.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* Blocked Signal Price Tracker
|
||||||
|
*
|
||||||
|
* Automatically tracks price movements for blocked signals to determine
|
||||||
|
* if they would have been profitable trades. This enables data-driven
|
||||||
|
* multi-timeframe analysis.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Price tracking at 1min, 5min, 15min, 30min intervals
|
||||||
|
* - TP1/TP2/SL hit detection using ATR-based targets
|
||||||
|
* - Max favorable/adverse excursion tracking
|
||||||
|
* - Automatic analysis completion after 30 minutes
|
||||||
|
* - Background job runs every 5 minutes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getPrismaClient } from '../database/trades'
|
||||||
|
import { getPythPriceMonitor } from '../pyth/price-monitor'
|
||||||
|
import { getMergedConfig } from '../../config/trading'
|
||||||
|
|
||||||
|
interface BlockedSignalWithTracking {
|
||||||
|
id: string
|
||||||
|
symbol: string
|
||||||
|
direction: 'long' | 'short'
|
||||||
|
entryPrice: number
|
||||||
|
atr: number
|
||||||
|
adx: number
|
||||||
|
createdAt: Date
|
||||||
|
priceAfter1Min: number | null
|
||||||
|
priceAfter5Min: number | null
|
||||||
|
priceAfter15Min: number | null
|
||||||
|
priceAfter30Min: number | null
|
||||||
|
wouldHitTP1: boolean | null
|
||||||
|
wouldHitTP2: boolean | null
|
||||||
|
wouldHitSL: boolean | null
|
||||||
|
maxFavorablePrice: number | null
|
||||||
|
maxAdversePrice: number | null
|
||||||
|
maxFavorableExcursion: number | null
|
||||||
|
maxAdverseExcursion: number | null
|
||||||
|
analysisComplete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlockedSignalTracker {
|
||||||
|
private prisma = getPrismaClient()
|
||||||
|
private intervalId: NodeJS.Timeout | null = null
|
||||||
|
private isRunning = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the background tracking job
|
||||||
|
* Runs every 5 minutes to update price data for blocked signals
|
||||||
|
*/
|
||||||
|
public start(): void {
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.log('⚠️ Blocked signal tracker already running')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔬 Starting blocked signal price tracker...')
|
||||||
|
this.isRunning = true
|
||||||
|
|
||||||
|
// Run immediately on start
|
||||||
|
this.trackPrices().catch(error => {
|
||||||
|
console.error('❌ Error in initial price tracking:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then run every 5 minutes
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.trackPrices().catch(error => {
|
||||||
|
console.error('❌ Error in price tracking:', error)
|
||||||
|
})
|
||||||
|
}, 5 * 60 * 1000) // 5 minutes
|
||||||
|
|
||||||
|
console.log('✅ Blocked signal tracker started (runs every 5 minutes)')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the background tracking job
|
||||||
|
*/
|
||||||
|
public stop(): void {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId)
|
||||||
|
this.intervalId = null
|
||||||
|
}
|
||||||
|
this.isRunning = false
|
||||||
|
console.log('⏹️ Blocked signal tracker stopped')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main tracking logic - processes all incomplete blocked signals
|
||||||
|
*/
|
||||||
|
private async trackPrices(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get all incomplete signals from last 24 hours
|
||||||
|
const signals = await this.prisma.blockedSignal.findMany({
|
||||||
|
where: {
|
||||||
|
blockReason: 'DATA_COLLECTION_ONLY',
|
||||||
|
analysisComplete: false,
|
||||||
|
createdAt: {
|
||||||
|
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (signals.length === 0) {
|
||||||
|
console.log('📊 No blocked signals to track')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 Tracking ${signals.length} blocked signals...`)
|
||||||
|
|
||||||
|
for (const signal of signals) {
|
||||||
|
await this.trackSignal(signal as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Price tracking complete for ${signals.length} signals`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error in trackPrices:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a single blocked signal
|
||||||
|
*/
|
||||||
|
private async trackSignal(signal: BlockedSignalWithTracking): Promise<void> {
|
||||||
|
try {
|
||||||
|
const now = Date.now()
|
||||||
|
const signalTime = signal.createdAt.getTime()
|
||||||
|
const elapsedMinutes = (now - signalTime) / (60 * 1000)
|
||||||
|
|
||||||
|
// Get current price
|
||||||
|
const priceMonitor = getPythPriceMonitor()
|
||||||
|
const latestPrice = priceMonitor.getCachedPrice(signal.symbol)
|
||||||
|
|
||||||
|
if (!latestPrice || !latestPrice.price) {
|
||||||
|
console.log(`⚠️ No price available for ${signal.symbol}, skipping`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPrice = latestPrice.price
|
||||||
|
const entryPrice = Number(signal.entryPrice)
|
||||||
|
|
||||||
|
// Calculate profit percentage
|
||||||
|
const profitPercent = this.calculateProfitPercent(
|
||||||
|
entryPrice,
|
||||||
|
currentPrice,
|
||||||
|
signal.direction
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate TP/SL levels using ATR
|
||||||
|
const config = getMergedConfig()
|
||||||
|
const { tp1Percent, tp2Percent, slPercent } = this.calculateTargets(
|
||||||
|
Number(signal.atr),
|
||||||
|
entryPrice,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update prices at intervals
|
||||||
|
const updates: any = {}
|
||||||
|
|
||||||
|
if (elapsedMinutes >= 1 && !signal.priceAfter1Min) {
|
||||||
|
updates.priceAfter1Min = currentPrice
|
||||||
|
console.log(` 📍 ${signal.symbol} ${signal.direction} @ 1min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsedMinutes >= 5 && !signal.priceAfter5Min) {
|
||||||
|
updates.priceAfter5Min = currentPrice
|
||||||
|
console.log(` 📍 ${signal.symbol} ${signal.direction} @ 5min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsedMinutes >= 15 && !signal.priceAfter15Min) {
|
||||||
|
updates.priceAfter15Min = currentPrice
|
||||||
|
console.log(` 📍 ${signal.symbol} ${signal.direction} @ 15min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsedMinutes >= 30 && !signal.priceAfter30Min) {
|
||||||
|
updates.priceAfter30Min = currentPrice
|
||||||
|
updates.analysisComplete = true
|
||||||
|
console.log(` ✅ ${signal.symbol} ${signal.direction} @ 30min: $${currentPrice.toFixed(2)} (${profitPercent.toFixed(2)}%) - COMPLETE`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update max favorable/adverse excursion
|
||||||
|
const currentMFE = signal.maxFavorableExcursion || 0
|
||||||
|
const currentMAE = signal.maxAdverseExcursion || 0
|
||||||
|
|
||||||
|
if (profitPercent > currentMFE) {
|
||||||
|
updates.maxFavorableExcursion = profitPercent
|
||||||
|
updates.maxFavorablePrice = currentPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profitPercent < currentMAE) {
|
||||||
|
updates.maxAdverseExcursion = profitPercent
|
||||||
|
updates.maxAdversePrice = currentPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TP1/TP2/SL would have been hit
|
||||||
|
if (signal.wouldHitTP1 === null && Math.abs(profitPercent) >= tp1Percent) {
|
||||||
|
updates.wouldHitTP1 = profitPercent > 0
|
||||||
|
console.log(` 🎯 ${signal.symbol} ${signal.direction} hit ${profitPercent > 0 ? 'TP1' : 'SL'} (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signal.wouldHitTP2 === null && Math.abs(profitPercent) >= tp2Percent) {
|
||||||
|
updates.wouldHitTP2 = profitPercent > 0
|
||||||
|
console.log(` 🎯 ${signal.symbol} ${signal.direction} hit TP2 (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signal.wouldHitSL === null && profitPercent <= -slPercent) {
|
||||||
|
updates.wouldHitSL = true
|
||||||
|
console.log(` 🛑 ${signal.symbol} ${signal.direction} hit SL (${profitPercent.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database if we have changes
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await this.prisma.blockedSignal.update({
|
||||||
|
where: { id: signal.id },
|
||||||
|
data: updates
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error tracking signal ${signal.id}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate profit percentage based on direction
|
||||||
|
*/
|
||||||
|
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 TP/SL targets using ATR
|
||||||
|
*/
|
||||||
|
private calculateTargets(
|
||||||
|
atr: number,
|
||||||
|
entryPrice: number,
|
||||||
|
config: any
|
||||||
|
): { tp1Percent: number; tp2Percent: number; slPercent: number } {
|
||||||
|
// ATR as percentage of price
|
||||||
|
const atrPercent = (atr / entryPrice) * 100
|
||||||
|
|
||||||
|
// TP1: ATR × 2.0 multiplier
|
||||||
|
let tp1Percent = atrPercent * config.atrMultiplierTp1
|
||||||
|
tp1Percent = Math.max(config.minTp1Percent, Math.min(config.maxTp1Percent, tp1Percent))
|
||||||
|
|
||||||
|
// TP2: ATR × 4.0 multiplier
|
||||||
|
let tp2Percent = atrPercent * config.atrMultiplierTp2
|
||||||
|
tp2Percent = Math.max(config.minTp2Percent, Math.min(config.maxTp2Percent, tp2Percent))
|
||||||
|
|
||||||
|
// SL: ATR × 3.0 multiplier
|
||||||
|
let slPercent = atrPercent * config.atrMultiplierSl
|
||||||
|
slPercent = Math.max(config.minSlPercent, Math.min(config.maxSlPercent, slPercent))
|
||||||
|
|
||||||
|
return { tp1Percent, tp2Percent, slPercent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
let trackerInstance: BlockedSignalTracker | null = null
|
||||||
|
|
||||||
|
export function getBlockedSignalTracker(): BlockedSignalTracker {
|
||||||
|
if (!trackerInstance) {
|
||||||
|
trackerInstance = new BlockedSignalTracker()
|
||||||
|
}
|
||||||
|
return trackerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBlockedSignalTracking(): void {
|
||||||
|
const tracker = getBlockedSignalTracker()
|
||||||
|
tracker.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopBlockedSignalTracking(): void {
|
||||||
|
if (trackerInstance) {
|
||||||
|
trackerInstance.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -504,6 +504,7 @@ export async function createBlockedSignal(params: CreateBlockedSignalParams) {
|
|||||||
direction: params.direction,
|
direction: params.direction,
|
||||||
timeframe: params.timeframe,
|
timeframe: params.timeframe,
|
||||||
signalPrice: params.signalPrice,
|
signalPrice: params.signalPrice,
|
||||||
|
entryPrice: params.signalPrice, // Use signal price as entry for tracking
|
||||||
atr: params.atr,
|
atr: params.atr,
|
||||||
adx: params.adx,
|
adx: params.adx,
|
||||||
rsi: params.rsi,
|
rsi: params.rsi,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getInitializedPositionManager } from '../trading/position-manager'
|
|||||||
import { initializeDriftService } from '../drift/client'
|
import { initializeDriftService } from '../drift/client'
|
||||||
import { getPrismaClient } from '../database/trades'
|
import { getPrismaClient } from '../database/trades'
|
||||||
import { getMarketConfig } from '../../config/trading'
|
import { getMarketConfig } from '../../config/trading'
|
||||||
|
import { startBlockedSignalTracking } from '../analysis/blocked-signal-tracker'
|
||||||
|
|
||||||
let initStarted = false
|
let initStarted = false
|
||||||
|
|
||||||
@@ -42,6 +43,10 @@ export async function initializePositionManagerOnStartup() {
|
|||||||
if (status.activeTradesCount > 0) {
|
if (status.activeTradesCount > 0) {
|
||||||
console.log(`📊 Monitoring: ${status.symbols.join(', ')}`)
|
console.log(`📊 Monitoring: ${status.symbols.join(', ')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start blocked signal price tracking
|
||||||
|
console.log('🔬 Starting blocked signal price tracker...')
|
||||||
|
startBlockedSignalTracking()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to initialize Position Manager on startup:', error)
|
console.error('❌ Failed to initialize Position Manager on startup:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,9 +181,12 @@ model BlockedSignal {
|
|||||||
indicatorVersion String? // Pine Script version (v5, v6, etc.)
|
indicatorVersion String? // Pine Script version (v5, v6, etc.)
|
||||||
|
|
||||||
// Block reason
|
// Block reason
|
||||||
blockReason String // "QUALITY_SCORE_TOO_LOW", "DUPLICATE", "COOLDOWN", etc.
|
blockReason String // "QUALITY_SCORE_TOO_LOW", "DUPLICATE", "COOLDOWN", "DATA_COLLECTION_ONLY", etc.
|
||||||
blockDetails String? // Human-readable details
|
blockDetails String? // Human-readable details
|
||||||
|
|
||||||
|
// Entry tracking (for multi-timeframe analysis)
|
||||||
|
entryPrice Float @default(0) // Price at signal time
|
||||||
|
|
||||||
// For later analysis: track if it would have been profitable
|
// For later analysis: track if it would have been profitable
|
||||||
priceAfter1Min Float? // Price 1 minute after (filled by monitoring job)
|
priceAfter1Min Float? // Price 1 minute after (filled by monitoring job)
|
||||||
priceAfter5Min Float? // Price 5 minutes after
|
priceAfter5Min Float? // Price 5 minutes after
|
||||||
@@ -192,6 +195,13 @@ model BlockedSignal {
|
|||||||
wouldHitTP1 Boolean? // Would TP1 have been hit?
|
wouldHitTP1 Boolean? // Would TP1 have been hit?
|
||||||
wouldHitTP2 Boolean? // Would TP2 have been hit?
|
wouldHitTP2 Boolean? // Would TP2 have been hit?
|
||||||
wouldHitSL Boolean? // Would SL have been hit?
|
wouldHitSL Boolean? // Would SL have been hit?
|
||||||
|
|
||||||
|
// Max favorable/adverse excursion (mirror Trade model)
|
||||||
|
maxFavorablePrice Float? // Price at max profit
|
||||||
|
maxAdversePrice Float? // Price at max loss
|
||||||
|
maxFavorableExcursion Float? // Best profit % during tracking
|
||||||
|
maxAdverseExcursion Float? // Worst loss % during tracking
|
||||||
|
|
||||||
analysisComplete Boolean @default(false) // Has post-analysis been done?
|
analysisComplete Boolean @default(false) // Has post-analysis been done?
|
||||||
|
|
||||||
@@index([symbol])
|
@@index([symbol])
|
||||||
|
|||||||
Reference in New Issue
Block a user