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 */}
+
+
+
+
+
+ {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 */}
+
+
+
+
+
+ {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}%
+
+
+
+
+
+ );
+}