From be36d6aa860d0b0c8f386c6ae512f821a7481ce1 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Sat, 15 Nov 2025 18:29:33 +0100 Subject: [PATCH] feat: Add live position monitor to analytics dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FEATURE: Real-time position monitoring with auto-refresh every 3 seconds Implementation: - New LivePosition interface for real-time trade data - Auto-refresh hook fetches from /api/trading/positions every 3s - Displays when Position Manager has active trades - Shows: P&L (realized + unrealized), current price, TP/SL status, position age Live Display Includes: - Header: Symbol, direction (LONG/SHORT), leverage, age, price checks - Real-time P&L: Profit %, account P&L %, color-coded green/red - Price Info: Entry, current, position size (with % after TP1), total P&L - Exit Targets: TP1 (✓ when hit), TP2/Runner, SL (@ B/E when moved) - P&L Breakdown: Realized, unrealized, peak P&L Technical: - Added NEXT_PUBLIC_API_SECRET_KEY to .env for frontend auth - Positions endpoint requires Bearer token authorization - Updates every 3s via useEffect interval - Only shows when monitoring.isActive && positions.length > 0 User Experience: - Live pulsing green dot indicator - Auto-updates without page refresh - Position size shows % remaining after TP1 hit - SL shows '@ B/E' badge when moved to breakeven - Color-coded P&L (green profit, red loss) Files: - app/analytics/page.tsx: Live position monitor section + auto-refresh - .env: Added NEXT_PUBLIC_API_SECRET_KEY User Request: 'i would like to see a live status on the analytics page about an open position' --- .env | 6 ++ app/analytics/page.tsx | 189 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/.env b/.env index adbc2f2..6442f1e 100644 --- a/.env +++ b/.env @@ -23,8 +23,14 @@ DRIFT_PROGRAM_ID=dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH # API secret key for authenticating n8n webhook requests # Generate with: openssl rand -hex 32 # Or: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +# API authentication secret for endpoints +# Generate with: openssl rand -hex 32 API_SECRET_KEY=2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb +# Public API key for frontend (must start with NEXT_PUBLIC_) +# This is safe to expose to the browser - it's for authenticated endpoints only +NEXT_PUBLIC_API_SECRET_KEY=2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb + # ================================ # REQUIRED - SOLANA RPC ENDPOINT # ================================ diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 8bcd0e3..4e5986f 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -90,11 +90,46 @@ interface VersionComparison { descriptions: Record } +interface LivePosition { + id: string + symbol: string + direction: 'long' | 'short' + entryPrice: number + currentPrice: number + entryTime: string + positionSize: number + currentSize: number + leverage: number + stopLoss: number + takeProfit1: number + takeProfit2: number + tp1Hit: boolean + slMovedToBreakeven: boolean + realizedPnL: number + unrealizedPnL: number + peakPnL: number + profitPercent: number + accountPnL: number + priceChecks: number + ageMinutes: number +} + +interface LivePositionsData { + success: boolean + monitoring: { + isActive: boolean + tradeCount: number + symbols: string[] + } + positions: LivePosition[] +} + export default function AnalyticsPage() { const [stats, setStats] = useState(null) const [positions, setPositions] = useState(null) const [lastTrade, setLastTrade] = useState(null) const [versionComparison, setVersionComparison] = useState(null) + const [livePositions, setLivePositions] = useState(null) const [loading, setLoading] = useState(true) const [selectedDays, setSelectedDays] = useState(30) @@ -102,6 +137,17 @@ export default function AnalyticsPage() { loadData() }, [selectedDays]) + // Auto-refresh live positions every 3 seconds + useEffect(() => { + loadLivePositions() // Initial load + + const interval = setInterval(() => { + loadLivePositions() + }, 3000) + + return () => clearInterval(interval) + }, []) + const loadData = async () => { setLoading(true) try { @@ -127,6 +173,20 @@ export default function AnalyticsPage() { setLoading(false) } + const loadLivePositions = async () => { + try { + const res = await fetch('/api/trading/positions', { + headers: { + 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_SECRET_KEY || 'your-secret-key-here'}` + } + }) + const data = await res.json() + setLivePositions(data) + } catch (error) { + console.error('Failed to load live positions:', error) + } + } + const clearManuallyClosed = async () => { if (!confirm('Clear all open trades from database? Use this if you manually closed positions in Drift UI.')) { return @@ -207,6 +267,135 @@ export default function AnalyticsPage() {
+ {/* Live Position Monitor */} + {livePositions && livePositions.monitoring.isActive && livePositions.positions.length > 0 && ( +
+
+
+
+
+

🔴 LIVE Position Monitor

+
+ (updates every 3s) +
+
+ + {livePositions.positions.map((pos) => { + const isProfitable = pos.profitPercent >= 0 + const totalPnL = pos.realizedPnL + pos.unrealizedPnL + + return ( +
+ {/* Header */} +
+
+
{pos.direction === 'long' ? '📈' : '📉'}
+
+
+

{pos.symbol}

+ + {pos.direction.toUpperCase()} + + + {pos.leverage}x + +
+
+ Open for {pos.ageMinutes}m • {pos.priceChecks} price checks +
+
+
+ +
+
+ {isProfitable ? '+' : ''}{pos.profitPercent.toFixed(2)}% +
+
+ {isProfitable ? '+' : ''}{pos.accountPnL.toFixed(2)}% account +
+
+
+ + {/* Price Info */} +
+
+
Entry Price
+
${pos.entryPrice.toFixed(2)}
+
+
+
Current Price
+
+ ${pos.currentPrice.toFixed(2)} +
+
+
+
Position Size
+
+ ${pos.currentSize.toFixed(2)} + {pos.tp1Hit && pos.currentSize < pos.positionSize && ( + ({((pos.currentSize/pos.positionSize)*100).toFixed(0)}%) + )} +
+
+
+
Total P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${totalPnL.toFixed(2)} +
+
+
+ + {/* Exit Targets */} +
+
+
+
TP1 Target
+ {pos.tp1Hit && ✓ HIT} +
+
${pos.takeProfit1.toFixed(2)}
+
+
+
TP2 / Runner
+
${pos.takeProfit2.toFixed(2)}
+
+
+
+
Stop Loss
+ {pos.slMovedToBreakeven && @ B/E} +
+
${pos.stopLoss.toFixed(2)}
+
+
+ + {/* P&L Breakdown */} +
+
+
+
Realized P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${pos.realizedPnL.toFixed(2)} +
+
+
+
Unrealized P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${pos.unrealizedPnL.toFixed(2)} +
+
+
+
Peak P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${pos.peakPnL.toFixed(2)} +
+
+
+
+
+ ) + })} +
+ )} + {/* Position Summary */} {positions && (