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 && (