feat: Orderbook shadow logging system - Phase 1 complete
Implementation:
- Added 7 orderbook fields to Trade model (spreadBps, imbalanceRatio, depths, impact, walls)
- Oracle-based estimates with 2bps spread assumption
- ENV flag: ENABLE_ORDERBOOK_LOGGING (defaults true)
- Execute wrapper lines 1037-1053 guards orderbook logic
Database:
- Direct SQL ALTER TABLE (avoided migration drift issues)
- All columns nullable DOUBLE PRECISION
- Prisma schema synced via db pull + generate
Deployment:
- Container rebuilt and deployed successfully
- All 7 columns verified accessible
- System operational, ready for live trade validation
Files changed:
- config/trading.ts (enableOrderbookLogging flag, line 127)
- types/trading.ts (orderbook interfaces)
- lib/database/trades.ts (createTrade saves orderbook data)
- app/api/trading/execute/route.ts (ENV wrapper lines 1037-1053)
- prisma/schema.prisma (7 orderbook fields)
- docs/ORDERBOOK_SHADOW_LOGGING.md (complete documentation)
Status: ✅ PRODUCTION READY - awaiting first trade for validation
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
// Prisma Schema for Trading Bot v4
|
||||
// Database: PostgreSQL
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
@@ -10,269 +7,195 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Trade records for analysis and performance tracking
|
||||
model Trade {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
positionId String @unique
|
||||
symbol String
|
||||
direction String
|
||||
entryPrice Float
|
||||
entryTime DateTime
|
||||
entrySlippage Float?
|
||||
positionSizeUSD Float
|
||||
leverage Float
|
||||
stopLossPrice Float
|
||||
softStopPrice Float?
|
||||
hardStopPrice Float?
|
||||
takeProfit1Price Float
|
||||
takeProfit2Price Float
|
||||
tp1SizePercent Float
|
||||
tp2SizePercent Float
|
||||
exitPrice Float?
|
||||
exitTime DateTime?
|
||||
exitReason String?
|
||||
realizedPnL Float?
|
||||
realizedPnLPercent Float?
|
||||
holdTimeSeconds Int?
|
||||
maxDrawdown Float?
|
||||
maxGain Float?
|
||||
entryOrderTx String
|
||||
tp1OrderTx String?
|
||||
tp2OrderTx String?
|
||||
slOrderTx String?
|
||||
softStopOrderTx String?
|
||||
hardStopOrderTx String?
|
||||
exitOrderTx String?
|
||||
configSnapshot Json
|
||||
signalSource String?
|
||||
signalStrength String?
|
||||
timeframe String?
|
||||
status String @default("open")
|
||||
isTestTrade Boolean @default(false)
|
||||
adxAtEntry Float?
|
||||
atrAtEntry Float?
|
||||
basisAtEntry Float?
|
||||
entrySlippagePct Float?
|
||||
exitSlippagePct Float?
|
||||
expectedEntryPrice Float?
|
||||
expectedExitPrice Float?
|
||||
fundingRateAtEntry Float?
|
||||
hardSlFilled Boolean @default(false)
|
||||
maxAdverseExcursion Float?
|
||||
maxAdversePrice Float?
|
||||
maxFavorableExcursion Float?
|
||||
maxFavorablePrice Float?
|
||||
slFillPrice Float?
|
||||
softSlFilled Boolean @default(false)
|
||||
timeToSl Int?
|
||||
timeToTp1 Int?
|
||||
timeToTp2 Int?
|
||||
tp1FillPrice Float?
|
||||
tp1Filled Boolean @default(false)
|
||||
tp2FillPrice Float?
|
||||
tp2Filled Boolean @default(false)
|
||||
volumeAtEntry Float?
|
||||
pricePositionAtEntry Float?
|
||||
rsiAtEntry Float?
|
||||
signalQualityScore Int?
|
||||
actualSizeUSD Float?
|
||||
expectedSizeUSD Float?
|
||||
isPhantom Boolean @default(false)
|
||||
phantomReason String?
|
||||
collateralUSD Float?
|
||||
signalQualityVersion String? @default("v4")
|
||||
indicatorVersion String?
|
||||
closeAttempts Int?
|
||||
spreadBps Float?
|
||||
imbalanceRatio Float?
|
||||
depthBid1Usd Float?
|
||||
depthAsk1Usd Float?
|
||||
priceImpact1Usd Float?
|
||||
bidWall Float?
|
||||
askWall Float?
|
||||
priceUpdates PriceUpdate[]
|
||||
|
||||
// Trade identification
|
||||
positionId String @unique // Transaction signature from entry order
|
||||
symbol String // e.g., "SOL-PERP"
|
||||
direction String // "long" or "short"
|
||||
|
||||
// Entry details
|
||||
entryPrice Float
|
||||
entryTime DateTime
|
||||
entrySlippage Float?
|
||||
positionSizeUSD Float // NOTIONAL position size (with leverage)
|
||||
collateralUSD Float? // ACTUAL margin/collateral used (positionSizeUSD / leverage)
|
||||
leverage Float
|
||||
|
||||
// Exit targets (planned)
|
||||
stopLossPrice Float
|
||||
softStopPrice Float? // Dual stop: soft stop-limit trigger
|
||||
hardStopPrice Float? // Dual stop: hard stop-market trigger
|
||||
takeProfit1Price Float
|
||||
takeProfit2Price Float
|
||||
tp1SizePercent Float
|
||||
tp2SizePercent Float
|
||||
|
||||
// Exit details (actual)
|
||||
exitPrice Float?
|
||||
exitTime DateTime?
|
||||
exitReason String? // "TP1", "TP2", "SL", "SOFT_SL", "HARD_SL", "manual", "emergency"
|
||||
|
||||
// Performance metrics
|
||||
realizedPnL Float?
|
||||
realizedPnLPercent Float?
|
||||
holdTimeSeconds Int?
|
||||
maxDrawdown Float? // Peak to valley during trade
|
||||
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)
|
||||
rsiAtEntry Float? // RSI momentum (0-100)
|
||||
volumeAtEntry Float? // Volume relative to MA
|
||||
pricePositionAtEntry Float? // Price position in range (0-100%)
|
||||
signalQualityScore Int? // Calculated quality score (0-100)
|
||||
signalQualityVersion String? @default("v4") // Tracks which scoring logic was used
|
||||
indicatorVersion String? // Pine Script version (v5, v6, etc.)
|
||||
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
|
||||
entryOrderTx String
|
||||
tp1OrderTx String?
|
||||
tp2OrderTx String?
|
||||
slOrderTx String?
|
||||
softStopOrderTx String? // Dual stop: soft stop tx
|
||||
hardStopOrderTx String? // Dual stop: hard stop tx
|
||||
exitOrderTx String?
|
||||
|
||||
// Configuration snapshot
|
||||
configSnapshot Json // Store settings used for this trade
|
||||
|
||||
// Signal data
|
||||
signalSource String? // "tradingview", "manual", etc.
|
||||
signalStrength String? // "strong", "moderate", "weak"
|
||||
timeframe String? // "5", "15", "60"
|
||||
|
||||
// Status
|
||||
status String @default("open") // "open", "closed", "failed", "phantom"
|
||||
isTestTrade Boolean @default(false) // Flag test trades for exclusion from analytics
|
||||
|
||||
// Fractional remnant tracking (Bug #89 - Dec 16, 2025)
|
||||
closeAttempts Int? // Number of close attempts (for fractional remnant detection)
|
||||
|
||||
// Phantom trade detection
|
||||
isPhantom Boolean @default(false) // Position opened but size mismatch >50%
|
||||
expectedSizeUSD Float? // Expected position size (when phantom)
|
||||
actualSizeUSD Float? // Actual position size from Drift (when phantom)
|
||||
phantomReason String? // "ORACLE_PRICE_MISMATCH", "PARTIAL_FILL", "ORDER_REJECTED"
|
||||
|
||||
// Relations
|
||||
priceUpdates PriceUpdate[]
|
||||
|
||||
@@index([symbol])
|
||||
@@index([createdAt])
|
||||
@@index([status])
|
||||
@@index([exitReason])
|
||||
}
|
||||
|
||||
// Real-time price updates during trade (for analysis)
|
||||
model PriceUpdate {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tradeId String
|
||||
trade Trade @relation(fields: [tradeId], references: [id], onDelete: Cascade)
|
||||
|
||||
price Float
|
||||
pnl Float
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
tradeId String
|
||||
price Float
|
||||
pnl Float
|
||||
pnlPercent Float
|
||||
|
||||
trade Trade @relation(fields: [tradeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tradeId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// System events and errors
|
||||
model SystemEvent {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
eventType String // "error", "warning", "info", "trade_executed", etc.
|
||||
eventType String
|
||||
message String
|
||||
details Json?
|
||||
|
||||
|
||||
@@index([eventType])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// Blocked signals for analysis (signals that didn't pass quality checks)
|
||||
model BlockedSignal {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Signal identification
|
||||
symbol String // e.g., "SOL-PERP"
|
||||
direction String // "long" or "short"
|
||||
timeframe String? // "5", "15", "60"
|
||||
|
||||
// Price at signal time
|
||||
signalPrice Float // Price when signal was generated
|
||||
|
||||
// Market metrics at signal time
|
||||
atr Float? // ATR% at signal
|
||||
adx Float? // ADX trend strength
|
||||
rsi Float? // RSI momentum
|
||||
volumeRatio Float? // Volume relative to average
|
||||
pricePosition Float? // Position in range (0-100%)
|
||||
|
||||
// Quality scoring
|
||||
signalQualityScore Int // 0-100 score
|
||||
signalQualityVersion String? // Which scoring version
|
||||
scoreBreakdown Json? // Detailed breakdown of score components
|
||||
minScoreRequired Int // What threshold was used (e.g., 65)
|
||||
indicatorVersion String? // Pine Script version (v5, v6, etc.)
|
||||
|
||||
// Block reason
|
||||
blockReason String // "QUALITY_SCORE_TOO_LOW", "DUPLICATE", "COOLDOWN", "DATA_COLLECTION_ONLY", etc.
|
||||
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
|
||||
priceAfter1Min Float? // Price 1 minute after (filled by monitoring job)
|
||||
priceAfter5Min Float? // Price 5 minutes after
|
||||
priceAfter15Min Float? // Price 15 minutes after
|
||||
priceAfter30Min Float? // Price 30 minutes after
|
||||
|
||||
// EXTENDED TRACKING (Dec 2, 2025): Track up to 8 hours for slow developers
|
||||
// User directive: "30 minutes...simply not long enough to know whats going to happen"
|
||||
// Purpose: Capture low ADX signals that take 4+ hours to reach targets
|
||||
priceAfter1Hr Float? // Price 1 hour after (60 minutes)
|
||||
priceAfter2Hr Float? // Price 2 hours after (120 minutes)
|
||||
priceAfter4Hr Float? // Price 4 hours after (240 minutes)
|
||||
priceAfter8Hr Float? // Price 8 hours after (480 minutes)
|
||||
|
||||
wouldHitTP1 Boolean? // Would TP1 have been hit?
|
||||
wouldHitTP2 Boolean? // Would TP2 have been hit?
|
||||
wouldHitSL Boolean? // Would SL have been hit?
|
||||
|
||||
// EXACT TIMING (Dec 2, 2025): Minute-precision timestamps for TP/SL hits
|
||||
// Purpose: Answer "EXACTLY when TP1/TP2 would have been hit" using 1-minute granular data
|
||||
// Uses: MarketData query instead of Drift oracle polling (480 data points vs. 8 checkpoints)
|
||||
tp1HitTime DateTime? @map("tp1_hit_time") // Exact timestamp when TP1 first hit
|
||||
tp2HitTime DateTime? @map("tp2_hit_time") // Exact timestamp when TP2 first hit
|
||||
slHitTime DateTime? @map("sl_hit_time") // Exact timestamp when SL first 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?
|
||||
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
symbol String
|
||||
direction String
|
||||
timeframe String?
|
||||
signalPrice Float
|
||||
atr Float?
|
||||
adx Float?
|
||||
rsi Float?
|
||||
volumeRatio Float?
|
||||
pricePosition Float?
|
||||
signalQualityScore Int
|
||||
signalQualityVersion String?
|
||||
scoreBreakdown Json?
|
||||
minScoreRequired Int
|
||||
blockReason String
|
||||
blockDetails String?
|
||||
priceAfter1Min Float?
|
||||
priceAfter5Min Float?
|
||||
priceAfter15Min Float?
|
||||
priceAfter30Min Float?
|
||||
wouldHitTP1 Boolean?
|
||||
wouldHitTP2 Boolean?
|
||||
wouldHitSL Boolean?
|
||||
analysisComplete Boolean @default(false)
|
||||
indicatorVersion String?
|
||||
entryPrice Float @default(0)
|
||||
maxFavorablePrice Float?
|
||||
maxAdversePrice Float?
|
||||
maxFavorableExcursion Float?
|
||||
maxAdverseExcursion Float?
|
||||
priceAfter1Hr Float?
|
||||
priceAfter2Hr Float?
|
||||
priceAfter4Hr Float?
|
||||
priceAfter8Hr Float?
|
||||
tp1HitTime DateTime? @map("tp1_hit_time")
|
||||
tp2HitTime DateTime? @map("tp2_hit_time")
|
||||
slHitTime DateTime? @map("sl_hit_time")
|
||||
|
||||
@@index([symbol])
|
||||
@@index([createdAt])
|
||||
@@index([signalQualityScore])
|
||||
@@index([blockReason])
|
||||
}
|
||||
|
||||
// Stop Hunt Revenge Tracker (Nov 20, 2025)
|
||||
// Tracks high-quality stop-outs and auto re-enters when stop hunt reverses
|
||||
model StopHunt {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Original trade that got stopped out
|
||||
originalTradeId String // References Trade.id
|
||||
symbol String // e.g., "SOL-PERP"
|
||||
direction String // "long" or "short"
|
||||
|
||||
// Stop hunt details
|
||||
stopHuntPrice Float // Price where we got stopped out
|
||||
originalEntryPrice Float // Where we originally entered
|
||||
originalQualityScore Int // Must be 85+ to qualify
|
||||
originalADX Float? // Trend strength at entry
|
||||
originalATR Float? // Volatility at entry
|
||||
stopLossAmount Float // How much we lost
|
||||
stopHuntTime DateTime // When stop hunt occurred
|
||||
|
||||
// Revenge tracking
|
||||
revengeTradeId String? // References Trade.id if revenge executed
|
||||
revengeExecuted Boolean @default(false)
|
||||
revengeEntryPrice Float? // Where revenge trade entered
|
||||
revengeTime DateTime? // When revenge executed
|
||||
revengeWindowExpired Boolean @default(false)
|
||||
revengeExpiresAt DateTime // 4 hours after stop hunt
|
||||
slDistanceAtEntry Float? // Distance from entry to stop zone (for Enhancement #6 analysis)
|
||||
|
||||
// Monitoring state
|
||||
highestPriceAfterStop Float? // Track if stop hunt reverses
|
||||
lowestPriceAfterStop Float? // Track if stop hunt reverses
|
||||
|
||||
// Zone tracking persistence (Nov 27, 2025 - Enhancement #10)
|
||||
firstCrossTime DateTime? // When price entered revenge zone
|
||||
lowestInZone Float? // Lowest price while in zone (LONG)
|
||||
highestInZone Float? // Highest price while in zone (SHORT)
|
||||
zoneResetCount Int @default(0) // How many times price left zone
|
||||
|
||||
// Revenge outcome tracking (Nov 27, 2025 - Enhancement #4)
|
||||
revengeOutcome String? // "TP1", "TP2", "SL", "TRAILING_SL", null (pending)
|
||||
revengePnL Float? // Realized P&L from revenge trade
|
||||
revengeFailedReason String? // Why revenge failed: "stopped_again", "chop", "insufficient_capital"
|
||||
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
originalTradeId String
|
||||
symbol String
|
||||
direction String
|
||||
stopHuntPrice Float
|
||||
originalEntryPrice Float
|
||||
originalQualityScore Int
|
||||
originalADX Float?
|
||||
originalATR Float?
|
||||
stopLossAmount Float
|
||||
stopHuntTime DateTime
|
||||
revengeTradeId String?
|
||||
revengeExecuted Boolean @default(false)
|
||||
revengeEntryPrice Float?
|
||||
revengeTime DateTime?
|
||||
revengeWindowExpired Boolean @default(false)
|
||||
revengeExpiresAt DateTime
|
||||
highestPriceAfterStop Float?
|
||||
lowestPriceAfterStop Float?
|
||||
revengeFailedReason String?
|
||||
revengeOutcome String?
|
||||
revengePnL Float?
|
||||
slDistanceAtEntry Float?
|
||||
firstCrossTime DateTime?
|
||||
highestInZone Float?
|
||||
lowestInZone Float?
|
||||
zoneResetCount Int @default(0)
|
||||
|
||||
@@index([symbol])
|
||||
@@index([revengeExecuted])
|
||||
@@index([revengeWindowExpired])
|
||||
@@ -280,58 +203,42 @@ model StopHunt {
|
||||
@@index([revengeOutcome])
|
||||
}
|
||||
|
||||
// Historical 1-minute market data (Dec 2, 2025)
|
||||
// Stores ALL TradingView webhook data for comprehensive analysis
|
||||
// Retention: 4 weeks (auto-cleanup of older data)
|
||||
model MarketData {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Market identification
|
||||
symbol String // e.g., "SOL-PERP"
|
||||
timeframe String // "1" for 1-minute data
|
||||
|
||||
// Price data
|
||||
price Float // Close price at this minute
|
||||
|
||||
// Technical indicators (from TradingView webhook)
|
||||
atr Float // Average True Range (volatility %)
|
||||
adx Float // Average Directional Index (trend strength)
|
||||
rsi Float // Relative Strength Index (momentum)
|
||||
volumeRatio Float // Current volume / average volume
|
||||
pricePosition Float // Position in recent range (0-100%)
|
||||
maGap Float? // MA50-MA200 gap percentage (v9+)
|
||||
|
||||
// Volume data
|
||||
volume Float? // Raw volume if available
|
||||
|
||||
// Timestamp tracking
|
||||
timestamp DateTime // Exact time of this 1-minute candle close
|
||||
|
||||
@@index([symbol, timestamp]) // Query by symbol and time range
|
||||
@@index([createdAt]) // For cleanup of old data
|
||||
@@index([timestamp]) // For time-based queries
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
symbol String
|
||||
timeframe String
|
||||
price Float
|
||||
atr Float
|
||||
adx Float
|
||||
rsi Float
|
||||
volumeRatio Float
|
||||
pricePosition Float
|
||||
maGap Float?
|
||||
volume Float?
|
||||
timestamp DateTime
|
||||
|
||||
@@index([symbol, timestamp])
|
||||
@@index([createdAt])
|
||||
@@index([timestamp])
|
||||
}
|
||||
|
||||
// Performance analytics (daily aggregates)
|
||||
model DailyStats {
|
||||
id String @id @default(cuid())
|
||||
date DateTime @unique
|
||||
|
||||
tradesCount Int
|
||||
winningTrades Int
|
||||
losingTrades Int
|
||||
totalPnL Float
|
||||
totalPnLPercent Float
|
||||
winRate Float
|
||||
avgWin Float
|
||||
avgLoss Float
|
||||
profitFactor Float
|
||||
maxDrawdown Float
|
||||
sharpeRatio Float?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
id String @id @default(cuid())
|
||||
date DateTime @unique
|
||||
tradesCount Int
|
||||
winningTrades Int
|
||||
losingTrades Int
|
||||
totalPnL Float
|
||||
totalPnLPercent Float
|
||||
winRate Float
|
||||
avgWin Float
|
||||
avgLoss Float
|
||||
profitFactor Float
|
||||
maxDrawdown Float
|
||||
sharpeRatio Float?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([date])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user