diff --git a/surfsense_browser_extension/sidepanel/analysis/TokenAnalysisPanel.tsx b/surfsense_browser_extension/sidepanel/analysis/TokenAnalysisPanel.tsx new file mode 100644 index 000000000..ee5d6ef10 --- /dev/null +++ b/surfsense_browser_extension/sidepanel/analysis/TokenAnalysisPanel.tsx @@ -0,0 +1,385 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + TrendingUp, + TrendingDown, + CheckCircle, + XCircle, + AlertTriangle, + RefreshCw, + ExternalLink, + Shield, + Users, + Droplet, + BarChart3, + MessageSquare, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { ChainIcon } from "../components/shared/ChainIcon"; + +export interface TokenAnalysisData { + tokenAddress: string; + tokenSymbol: string; + tokenName: string; + chain: string; + timestamp: Date; + + contract: { + verified: boolean; + renounced: boolean; + isProxy: boolean; + sourceCode: boolean; + }; + + holders: { + count: number; + top10Percent: number; + distribution: { address: string; percent: number }[]; + }; + + liquidity: { + totalUSD: number; + lpLocked: boolean; + lpLockDuration?: number; + liquidityMcapRatio: number; + }; + + volume: { + volume24h: number; + trend: "increasing" | "decreasing" | "stable"; + volumeLiquidityRatio: number; + }; + + price: { + current: number; + ath: number; + atl: number; + change7d: number; + change30d: number; + volatility: number; + }; + + social: { + twitterMentions: number; + telegramActivity: number; + redditDiscussions: number; + sentimentScore: number; // -1 to 1 + sentiment: "positive" | "negative" | "neutral"; + }; + + aiSummary: string; + recommendation: "buy" | "hold" | "sell" | "avoid"; + confidence: number; // 0-100 +} + +export interface TokenAnalysisPanelProps { + /** Token analysis data */ + analysis: TokenAnalysisData; + /** Callback when refresh is clicked */ + onRefresh?: () => void; + /** Callback when "View Full Report" is clicked */ + onViewFullReport?: () => void; + /** Whether data is loading */ + isLoading?: boolean; + /** Additional class names */ + className?: string; +} + +/** + * TokenAnalysisPanel - Comprehensive token analysis display + * + * Features: + * - AI-generated summary with recommendation + * - Contract analysis (verified, renounced, proxy) + * - Holder distribution analysis + * - Liquidity analysis with LP lock status + * - Volume trends and trading activity + * - Price history and volatility + * - Social sentiment analysis + */ +export function TokenAnalysisPanel({ + analysis, + onRefresh, + onViewFullReport, + isLoading = false, + className, +}: TokenAnalysisPanelProps) { + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async () => { + setIsRefreshing(true); + await onRefresh?.(); + setTimeout(() => setIsRefreshing(false), 1000); + }; + + const formatTimeAgo = (date: Date) => { + const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + return `${Math.floor(minutes / 60)}h ago`; + }; + + const formatCurrency = (value: number) => { + if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`; + return `$${value.toFixed(0)}`; + }; + + const getRecommendationColor = (rec: string) => { + switch (rec) { + case "buy": return "text-green-600 dark:text-green-400"; + case "hold": return "text-yellow-600 dark:text-yellow-400"; + case "sell": return "text-orange-600 dark:text-orange-400"; + case "avoid": return "text-red-600 dark:text-red-400"; + default: return "text-muted-foreground"; + } + }; + + const getSentimentEmoji = (sentiment: string) => { + switch (sentiment) { + case "positive": return "😊"; + case "negative": return "😟"; + case "neutral": return "😐"; + default: return "🤔"; + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Token Analysis

+

+ {analysis.tokenSymbol} • +

+
+
+ +
+ + {/* Content */} +
+ {/* AI Summary */} +
+
+
🤖
+
+

AI Summary

+

+ {analysis.aiSummary} +

+
+
+ + {/* Recommendation */} +
+
+ Recommendation: + + {analysis.recommendation} + +
+
+ Confidence: + {analysis.confidence}% +
+
+
+ + {/* Contract Analysis */} +
+
+ +

Contract

+
+
+
+ {analysis.contract.verified ? ( + + ) : ( + + )} + Verified +
+
+ {analysis.contract.renounced ? ( + + ) : ( + + )} + Renounced +
+
+ {!analysis.contract.isProxy ? ( + + ) : ( + + )} + {analysis.contract.isProxy ? "Proxy" : "Direct"} +
+
+ {analysis.contract.sourceCode ? ( + + ) : ( + + )} + Source Code +
+
+
+ + {/* Holder Distribution */} +
+
+ +

Holders

+
+
+
+ Total Holders + {analysis.holders.count.toLocaleString()} +
+
+ Top 10 Holdings + 50 ? "text-red-600" : + analysis.holders.top10Percent > 30 ? "text-yellow-600" : + "text-green-600" + )}> + {analysis.holders.top10Percent}% + +
+
+
+ + {/* Liquidity */} +
+
+ +

Liquidity

+
+
+
+ Total Liquidity + {formatCurrency(analysis.liquidity.totalUSD)} +
+
+ LP Lock Status +
+ {analysis.liquidity.lpLocked ? ( + <> + + + {analysis.liquidity.lpLockDuration ? `${analysis.liquidity.lpLockDuration}d` : "Locked"} + + + ) : ( + <> + + Unlocked + + )} +
+
+
+
+ + {/* Volume & Price */} +
+
+

Volume 24h

+
+ {formatCurrency(analysis.volume.volume24h)} + {analysis.volume.trend === "increasing" ? ( + + ) : analysis.volume.trend === "decreasing" ? ( + + ) : null} +
+
+
+

Price

+
+
${analysis.price.current.toFixed(8)}
+
+ = 0 ? "text-green-600" : "text-red-600" + )}> + 7d: {analysis.price.change7d >= 0 ? "+" : ""}{analysis.price.change7d.toFixed(1)}% + + = 0 ? "text-green-600" : "text-red-600" + )}> + 30d: {analysis.price.change30d >= 0 ? "+" : ""}{analysis.price.change30d.toFixed(1)}% + +
+
+
+
+ + {/* Social Sentiment */} +
+
+ +

Social Sentiment

+
+
+
+ + {getSentimentEmoji(analysis.social.sentiment)} {analysis.social.sentiment.charAt(0).toUpperCase() + analysis.social.sentiment.slice(1)} + + + Score: {(analysis.social.sentimentScore * 100).toFixed(0)} + +
+
+
+
Twitter
+
{analysis.social.twitterMentions}
+
+
+
Telegram
+
{analysis.social.telegramActivity}
+
+
+
Reddit
+
{analysis.social.redditDiscussions}
+
+
+
+
+ + {/* Last Updated */} +
+ Last updated: {formatTimeAgo(analysis.timestamp)} +
+
+ + {/* Footer */} +
+ +
+
+ ); +} + diff --git a/surfsense_browser_extension/sidepanel/analysis/TradingSuggestionPanel.tsx b/surfsense_browser_extension/sidepanel/analysis/TradingSuggestionPanel.tsx new file mode 100644 index 000000000..f1408ad8f --- /dev/null +++ b/surfsense_browser_extension/sidepanel/analysis/TradingSuggestionPanel.tsx @@ -0,0 +1,282 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + TrendingUp, + TrendingDown, + Target, + AlertCircle, + Info, + DollarSign, + Percent, + Clock, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { ChainIcon } from "../components/shared/ChainIcon"; + +export interface TradingSuggestion { + tokenAddress: string; + tokenSymbol: string; + tokenName: string; + chain: string; + currentPrice: number; + timestamp: Date; + + entry: { + min: number; + max: number; + reasoning: string; + }; + + targets: { + level: number; + price: number; + percentGain: number; + confidence: number; + }[]; + + stopLoss: { + price: number; + percentLoss: number; + reasoning: string; + }; + + riskReward: number; + overallConfidence: number; + + technicalLevels: { + support: number[]; + resistance: number[]; + }; + + reasoning: string[]; + invalidationConditions: string[]; +} + +export interface TradingSuggestionPanelProps { + /** Trading suggestion data */ + suggestion: TradingSuggestion; + /** Callback when "Set Alerts" is clicked */ + onSetAlerts?: () => void; + /** Callback when "View Chart" is clicked */ + onViewChart?: () => void; + /** Additional class names */ + className?: string; +} + +/** + * TradingSuggestionPanel - AI-powered entry/exit suggestions + * + * Features: + * - Entry zone recommendations + * - Multiple take-profit targets + * - Stop-loss suggestions + * - Risk/reward ratio calculation + * - Technical analysis levels + * - AI reasoning and invalidation conditions + */ +export function TradingSuggestionPanel({ + suggestion, + onSetAlerts, + onViewChart, + className, +}: TradingSuggestionPanelProps) { + const [showDetails, setShowDetails] = useState(false); + + const formatPrice = (price: number) => { + if (price < 0.01) return `$${price.toFixed(8)}`; + if (price < 1) return `$${price.toFixed(6)}`; + return `$${price.toFixed(4)}`; + }; + + const getRiskRewardColor = (ratio: number) => { + if (ratio >= 3) return "text-green-600 dark:text-green-400"; + if (ratio >= 2) return "text-yellow-600 dark:text-yellow-400"; + return "text-red-600 dark:text-red-400"; + }; + + const getRiskRewardLabel = (ratio: number) => { + if (ratio >= 3) return "Excellent"; + if (ratio >= 2) return "Good"; + if (ratio >= 1.5) return "Fair"; + return "Poor"; + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Trading Suggestion

+

+ {suggestion.tokenSymbol} • +

+
+
+
+
Confidence
+
{suggestion.overallConfidence}%
+
+
+ + {/* Content */} +
+ {/* Current Price */} +
+
Current Price
+
{formatPrice(suggestion.currentPrice)}
+
+ + {/* Entry Zone */} +
+
+
+

Entry Zone

+
+
+
+ + {formatPrice(suggestion.entry.min)} - {formatPrice(suggestion.entry.max)} + +
+

{suggestion.entry.reasoning}

+
+
+ + {/* Targets */} +
+
+ +

Take Profit Targets

+
+
+ {suggestion.targets.map((target) => ( +
+
+ 🎯 Target {target.level} + + Confidence: {target.confidence}% + +
+
+ + {formatPrice(target.price)} + + + +{target.percentGain.toFixed(1)}% + +
+
+ ))} +
+
+ + {/* Stop Loss */} +
+
+
+

Stop Loss

+
+
+
+ + {formatPrice(suggestion.stopLoss.price)} + + + {suggestion.stopLoss.percentLoss.toFixed(1)}% + +
+

{suggestion.stopLoss.reasoning}

+
+
+ + {/* Risk/Reward */} +
+
+ Risk/Reward Ratio + + 1:{suggestion.riskReward.toFixed(1)} + +
+
+
= 3 ? "bg-green-500/20 text-green-600 dark:text-green-400" : + suggestion.riskReward >= 2 ? "bg-yellow-500/20 text-yellow-600 dark:text-yellow-400" : + "bg-red-500/20 text-red-600 dark:text-red-400" + )}> + {getRiskRewardLabel(suggestion.riskReward)} +
+
+
+ + {/* Why? Section */} +
+ + + {showDetails && ( +
+
+

Reasoning:

+
    + {suggestion.reasoning.map((reason, i) => ( +
  • + + {reason} +
  • + ))} +
+
+ +
+

Invalidation Conditions:

+
    + {suggestion.invalidationConditions.map((condition, i) => ( +
  • + + {condition} +
  • + ))} +
+
+
+ )} +
+
+ + {/* Footer Actions */} +
+ + +
+
+ ); +} diff --git a/surfsense_browser_extension/sidepanel/mock/mockData.ts b/surfsense_browser_extension/sidepanel/mock/mockData.ts index 30be13668..36f9b6950 100644 --- a/surfsense_browser_extension/sidepanel/mock/mockData.ts +++ b/surfsense_browser_extension/sidepanel/mock/mockData.ts @@ -8,6 +8,9 @@ import type { WatchlistToken, WatchlistAlert } from "../crypto/WatchlistPanel"; import type { SafetyFactor } from "../crypto/SafetyScoreDisplay"; import type { AlertConfig } from "../crypto/AlertConfigModal"; import type { WhaleTransaction } from "../whale/WhaleActivityFeed"; +import type { TokenAnalysisData } from "../analysis/TokenAnalysisPanel"; +import type { TradingSuggestion } from "../analysis/TradingSuggestionPanel"; +import type { PortfolioData } from "../portfolio/PortfolioPanel"; // ============================================ // MOCK TOKEN DATA (DexScreener) @@ -302,6 +305,248 @@ export const MOCK_WHALE_TRANSACTIONS: WhaleTransaction[] = [ }, ]; +// ============================================ +// MOCK TOKEN ANALYSIS +// ============================================ + +export const MOCK_TOKEN_ANALYSIS: TokenAnalysisData = { + tokenAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + tokenSymbol: "BULLA", + tokenName: "Bulla Token", + chain: "solana", + timestamp: new Date(), + + contract: { + verified: true, + renounced: true, + isProxy: false, + sourceCode: true, + }, + + holders: { + count: 1234, + top10Percent: 35, + distribution: [ + { address: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", percent: 8.5 }, + { address: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", percent: 6.2 }, + { address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", percent: 5.8 }, + ], + }, + + liquidity: { + totalUSD: 50000, + lpLocked: true, + lpLockDuration: 90, + liquidityMcapRatio: 0.15, + }, + + volume: { + volume24h: 100000, + trend: "increasing", + volumeLiquidityRatio: 2.0, + }, + + price: { + current: 0.0001234, + ath: 0.0005, + atl: 0.00001, + change7d: 15.5, + change30d: 45.2, + volatility: 12.5, + }, + + social: { + twitterMentions: 500, + telegramActivity: 1200, + redditDiscussions: 45, + sentimentScore: 0.75, + sentiment: "positive", + }, + + aiSummary: "BULLA shows strong holder distribution with verified contract and renounced ownership. Volume increasing 200% in 24h with locked liquidity for 90 days. Social sentiment is highly positive with growing community engagement. Moderate risk profile with good upside potential.", + recommendation: "buy", + confidence: 75, +}; + +// ============================================ +// MOCK TRADING SUGGESTION +// ============================================ + +export const MOCK_TRADING_SUGGESTION: TradingSuggestion = { + tokenAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + tokenSymbol: "BULLA", + tokenName: "Bulla Token", + chain: "solana", + currentPrice: 0.0001234, + timestamp: new Date(), + + entry: { + min: 0.0001100, + max: 0.0001250, + reasoning: "Strong support zone at 0.00011 with high volume. Current price offers good risk/reward entry.", + }, + + targets: [ + { + level: 1, + price: 0.0001800, + percentGain: 45.8, + confidence: 85, + }, + { + level: 2, + price: 0.0002500, + percentGain: 102.6, + confidence: 70, + }, + { + level: 3, + price: 0.0003500, + percentGain: 183.7, + confidence: 50, + }, + ], + + stopLoss: { + price: 0.0000950, + percentLoss: -23.0, + reasoning: "Below key support level. Invalidates bullish structure if broken.", + }, + + riskReward: 3.2, + overallConfidence: 78, + + technicalLevels: { + support: [0.0001100, 0.0000950, 0.0000800], + resistance: [0.0001800, 0.0002500, 0.0003500], + }, + + reasoning: [ + "Strong accumulation pattern forming on 4H chart", + "Volume profile shows increasing buyer interest", + "RSI showing bullish divergence at support", + "Whale wallets accumulating over past 48 hours", + "Social sentiment turning positive with growing community", + ], + + invalidationConditions: [ + "Break below 0.000095 with high volume", + "Sudden large holder dumping (>5% supply)", + "Liquidity removal or unlock event", + "Negative news or security concerns", + ], +}; + +// ============================================ +// MOCK PORTFOLIO DATA +// ============================================ + +export const MOCK_PORTFOLIO: PortfolioData = { + wallets: [ + { + address: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + chain: "solana", + type: "phantom", + }, + { + address: "0x6982508145454Ce325dDbE47a25d4ec3d2311933", + chain: "ethereum", + type: "metamask", + }, + ], + + totalValue: 12450.50, + change24h: 850.25, + change24hPercent: 7.33, + + holdings: [ + { + tokenAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + chain: "solana", + symbol: "BULLA", + name: "Bulla Token", + amount: "8,100,000", + currentPrice: 0.0001234, + currentValue: 1000.00, + change24h: 150.00, + change24hPercent: 17.65, + entryPrice: 0.0001000, + pnl: 189.54, + pnlPercent: 23.4, + }, + { + tokenAddress: "So11111111111111111111111111111111111111112", + chain: "solana", + symbol: "SOL", + name: "Solana", + amount: "50", + currentPrice: 100.50, + currentValue: 5025.00, + change24h: 250.00, + change24hPercent: 5.24, + entryPrice: 95.00, + pnl: 275.00, + pnlPercent: 5.79, + }, + { + tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + chain: "solana", + symbol: "BONK", + name: "Bonk", + amount: "2,300,000,000", + currentPrice: 0.00001500, + currentValue: 3450.00, + change24h: -125.00, + change24hPercent: -3.50, + entryPrice: 0.00001200, + pnl: 690.00, + pnlPercent: 25.0, + }, + { + tokenAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933", + chain: "ethereum", + symbol: "PEPE", + name: "Pepe", + amount: "23,000,000,000", + currentPrice: 0.00000012, + currentValue: 2760.00, + change24h: 180.00, + change24hPercent: 6.98, + entryPrice: 0.00000010, + pnl: 460.00, + pnlPercent: 20.0, + }, + { + tokenAddress: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + chain: "solana", + symbol: "WIF", + name: "dogwifhat", + amount: "100", + currentPrice: 2.15, + currentValue: 215.50, + change24h: -15.50, + change24hPercent: -6.71, + entryPrice: 2.50, + pnl: -35.00, + pnlPercent: -14.0, + }, + ], + + analytics: { + bestPerformer: { + symbol: "BONK", + change: 25.0, + }, + worstPerformer: { + symbol: "WIF", + change: -14.0, + }, + winRate: 80, + avgHoldTime: 14, + totalTrades: 25, + }, +}; + // ============================================ // MOCK ALERT CONFIGS // ============================================ diff --git a/surfsense_browser_extension/sidepanel/portfolio/PortfolioPanel.tsx b/surfsense_browser_extension/sidepanel/portfolio/PortfolioPanel.tsx new file mode 100644 index 000000000..b5af89a0c --- /dev/null +++ b/surfsense_browser_extension/sidepanel/portfolio/PortfolioPanel.tsx @@ -0,0 +1,287 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + TrendingUp, + TrendingDown, + Wallet, + Plus, + BarChart3, + ExternalLink, + RefreshCw, + Star, + Bell, + Eye, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { ChainIcon } from "../components/shared/ChainIcon"; + +export interface PortfolioHolding { + tokenAddress: string; + chain: string; + symbol: string; + name: string; + amount: string; + currentPrice: number; + currentValue: number; + change24h: number; + change24hPercent: number; + entryPrice?: number; + pnl?: number; + pnlPercent?: number; +} + +export interface PortfolioAnalytics { + bestPerformer: { symbol: string; change: number }; + worstPerformer: { symbol: string; change: number }; + winRate: number; + avgHoldTime: number; + totalTrades: number; +} + +export interface PortfolioData { + wallets: { + address: string; + chain: string; + type: "metamask" | "phantom" | "coinbase"; + }[]; + + totalValue: number; + change24h: number; + change24hPercent: number; + + holdings: PortfolioHolding[]; + analytics: PortfolioAnalytics; +} + +export interface PortfolioPanelProps { + /** Portfolio data */ + portfolio: PortfolioData; + /** Callback when refresh is clicked */ + onRefresh?: () => void; + /** Callback when "Analyze" is clicked for a token */ + onAnalyzeToken?: (holding: PortfolioHolding) => void; + /** Callback when "Set Alert" is clicked for a token */ + onSetAlert?: (holding: PortfolioHolding) => void; + /** Callback when "View on DexScreener" is clicked */ + onViewToken?: (holding: PortfolioHolding) => void; + /** Callback when "Add Manual Position" is clicked */ + onAddPosition?: () => void; + /** Whether data is loading */ + isLoading?: boolean; + /** Additional class names */ + className?: string; +} + +/** + * PortfolioPanel - Portfolio tracker with holdings and P&L + * + * Features: + * - Total portfolio value and 24h change + * - List of holdings with current value and P&L + * - Performance analytics (best/worst performers, win rate) + * - Quick actions per token (analyze, alert, view) + * - Manual position entry + */ +export function PortfolioPanel({ + portfolio, + onRefresh, + onAnalyzeToken, + onSetAlert, + onViewToken, + onAddPosition, + isLoading = false, + className, +}: PortfolioPanelProps) { + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async () => { + setIsRefreshing(true); + await onRefresh?.(); + setTimeout(() => setIsRefreshing(false), 1000); + }; + + const formatCurrency = (value: number) => { + if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`; + return `$${value.toFixed(2)}`; + }; + + const formatPercent = (value: number) => { + const sign = value >= 0 ? "+" : ""; + return `${sign}${value.toFixed(2)}%`; + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Portfolio

+

+ {portfolio.holdings.length} tokens +

+
+
+ +
+ + {/* Content */} +
+ {/* Total Value */} +
+
Total Value
+
+ {formatCurrency(portfolio.totalValue)} +
+
+ = 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" + )}> + {formatPercent(portfolio.change24hPercent)} + + + ({portfolio.change24h >= 0 ? "+" : ""}{formatCurrency(portfolio.change24h)}) 24h + + {portfolio.change24hPercent >= 0 ? ( + + ) : ( + + )} +
+
+ + {/* Holdings List */} +
+ {portfolio.holdings.map((holding) => ( +
+ {/* Token Info */} +
+
+
+
+ {holding.symbol} + +
+
{holding.name}
+
+
+
+
{formatCurrency(holding.currentValue)}
+
= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" + )}> + {formatPercent(holding.change24hPercent)} +
+
+
+ + {/* Amount and Price */} +
+ {holding.amount} tokens + ${holding.currentPrice.toFixed(6)} +
+ + {/* P&L (if available) */} + {holding.pnl !== undefined && holding.pnlPercent !== undefined && ( +
+ P&L +
+ = 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" + )}> + {holding.pnl >= 0 ? "+" : ""}{formatCurrency(holding.pnl)} + + = 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" + )}> + ({formatPercent(holding.pnlPercent)}) + +
+
+ )} + + {/* Action Buttons */} +
+ + + +
+
+ ))} +
+ + {/* Add Position Button */} +
+ +
+ + {/* Performance Analytics */} +
+

Performance

+
+
+ Best Performer + + {portfolio.analytics.bestPerformer.symbol} (+{portfolio.analytics.bestPerformer.change.toFixed(1)}%) + +
+
+ Worst Performer + + {portfolio.analytics.worstPerformer.symbol} ({portfolio.analytics.worstPerformer.change.toFixed(1)}%) + +
+
+ Win Rate + {portfolio.analytics.winRate}% +
+
+
+
+
+ ); +}