feat(epic-3): implement Trading Intelligence components with mock data

- Add TokenAnalysisPanel.tsx for comprehensive token analysis
  * AI-generated summary with buy/hold/sell/avoid recommendation
  * Contract analysis (verified, renounced, proxy, source code)
  * Holder distribution with top 10% concentration
  * Liquidity analysis with LP lock status and duration
  * Volume trends and trading activity metrics
  * Price history (ATH/ATL, 7d/30d changes, volatility)
  * Social sentiment analysis (Twitter, Telegram, Reddit)

- Add TradingSuggestionPanel.tsx for entry/exit suggestions
  * Entry zone recommendations with reasoning
  * Multiple take-profit targets (3 levels) with confidence scores
  * Stop-loss suggestions with invalidation reasoning
  * Risk/reward ratio calculation and assessment
  * Technical analysis levels (support/resistance)
  * AI reasoning and invalidation conditions

- Add PortfolioPanel.tsx for portfolio tracking
  * Total portfolio value with 24h change
  * Holdings list with current value and P&L tracking
  * Performance analytics (best/worst performers, win rate)
  * Quick actions per token (analyze, alert, view)
  * Manual position entry support

- Add comprehensive mock data for all Epic 3 components
  * MOCK_TOKEN_ANALYSIS with realistic metrics
  * MOCK_TRADING_SUGGESTION with entry/exit levels
  * MOCK_PORTFOLIO with 5 holdings and analytics

Implements Stories 3.1, 3.2, 3.3 from Epic 3: Trading Intelligence
This commit is contained in:
API Test Bot 2026-02-04 02:36:35 +07:00
parent db22cd4a64
commit ea2080619b
4 changed files with 1199 additions and 0 deletions

View file

@ -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 (
<div className={cn("flex flex-col h-full", className)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-primary" />
<div>
<h2 className="font-semibold">Token Analysis</h2>
<p className="text-xs text-muted-foreground">
{analysis.tokenSymbol} <ChainIcon chain={analysis.chain} size="xs" className="inline" />
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* AI Summary */}
<div className="p-4 bg-primary/5 rounded-lg border border-primary/20">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">🤖</div>
<div className="flex-1">
<h3 className="font-semibold text-sm mb-1">AI Summary</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{analysis.aiSummary}
</p>
</div>
</div>
{/* Recommendation */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-primary/10">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Recommendation:</span>
<span className={cn("font-bold text-sm uppercase", getRecommendationColor(analysis.recommendation))}>
{analysis.recommendation}
</span>
</div>
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">Confidence:</span>
<span className="font-semibold text-sm">{analysis.confidence}%</span>
</div>
</div>
</div>
{/* Contract Analysis */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Contract</h3>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded">
{analysis.contract.verified ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
<span className="text-xs">Verified</span>
</div>
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded">
{analysis.contract.renounced ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
<span className="text-xs">Renounced</span>
</div>
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded">
{!analysis.contract.isProxy ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<AlertTriangle className="h-4 w-4 text-yellow-600" />
)}
<span className="text-xs">{analysis.contract.isProxy ? "Proxy" : "Direct"}</span>
</div>
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded">
{analysis.contract.sourceCode ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
<span className="text-xs">Source Code</span>
</div>
</div>
</div>
{/* Holder Distribution */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Holders</h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-muted/50 rounded">
<span className="text-xs text-muted-foreground">Total Holders</span>
<span className="font-semibold text-sm">{analysis.holders.count.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between p-2 bg-muted/50 rounded">
<span className="text-xs text-muted-foreground">Top 10 Holdings</span>
<span className={cn(
"font-semibold text-sm",
analysis.holders.top10Percent > 50 ? "text-red-600" :
analysis.holders.top10Percent > 30 ? "text-yellow-600" :
"text-green-600"
)}>
{analysis.holders.top10Percent}%
</span>
</div>
</div>
</div>
{/* Liquidity */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Droplet className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Liquidity</h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-muted/50 rounded">
<span className="text-xs text-muted-foreground">Total Liquidity</span>
<span className="font-semibold text-sm">{formatCurrency(analysis.liquidity.totalUSD)}</span>
</div>
<div className="flex items-center justify-between p-2 bg-muted/50 rounded">
<span className="text-xs text-muted-foreground">LP Lock Status</span>
<div className="flex items-center gap-1">
{analysis.liquidity.lpLocked ? (
<>
<CheckCircle className="h-3 w-3 text-green-600" />
<span className="font-semibold text-xs text-green-600">
{analysis.liquidity.lpLockDuration ? `${analysis.liquidity.lpLockDuration}d` : "Locked"}
</span>
</>
) : (
<>
<XCircle className="h-3 w-3 text-red-600" />
<span className="font-semibold text-xs text-red-600">Unlocked</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Volume & Price */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<h3 className="font-semibold text-xs text-muted-foreground">Volume 24h</h3>
<div className="flex items-center gap-2">
<span className="font-bold text-lg">{formatCurrency(analysis.volume.volume24h)}</span>
{analysis.volume.trend === "increasing" ? (
<TrendingUp className="h-4 w-4 text-green-600" />
) : analysis.volume.trend === "decreasing" ? (
<TrendingDown className="h-4 w-4 text-red-600" />
) : null}
</div>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-xs text-muted-foreground">Price</h3>
<div className="space-y-1">
<div className="font-bold text-lg">${analysis.price.current.toFixed(8)}</div>
<div className="flex gap-2 text-xs">
<span className={cn(
analysis.price.change7d >= 0 ? "text-green-600" : "text-red-600"
)}>
7d: {analysis.price.change7d >= 0 ? "+" : ""}{analysis.price.change7d.toFixed(1)}%
</span>
<span className={cn(
analysis.price.change30d >= 0 ? "text-green-600" : "text-red-600"
)}>
30d: {analysis.price.change30d >= 0 ? "+" : ""}{analysis.price.change30d.toFixed(1)}%
</span>
</div>
</div>
</div>
</div>
{/* Social Sentiment */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Social Sentiment</h3>
</div>
<div className="p-3 bg-muted/50 rounded">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">
{getSentimentEmoji(analysis.social.sentiment)} {analysis.social.sentiment.charAt(0).toUpperCase() + analysis.social.sentiment.slice(1)}
</span>
<span className="text-xs text-muted-foreground">
Score: {(analysis.social.sentimentScore * 100).toFixed(0)}
</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<div className="text-muted-foreground">Twitter</div>
<div className="font-semibold">{analysis.social.twitterMentions}</div>
</div>
<div>
<div className="text-muted-foreground">Telegram</div>
<div className="font-semibold">{analysis.social.telegramActivity}</div>
</div>
<div>
<div className="text-muted-foreground">Reddit</div>
<div className="font-semibold">{analysis.social.redditDiscussions}</div>
</div>
</div>
</div>
</div>
{/* Last Updated */}
<div className="text-xs text-muted-foreground text-center">
Last updated: {formatTimeAgo(analysis.timestamp)}
</div>
</div>
{/* Footer */}
<div className="border-t p-3">
<Button
variant="default"
className="w-full"
onClick={onViewFullReport}
>
<ExternalLink className="h-4 w-4 mr-2" />
View Full Report
</Button>
</div>
</div>
);
}

View file

@ -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 (
<div className={cn("flex flex-col h-full", className)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-primary" />
<div>
<h2 className="font-semibold">Trading Suggestion</h2>
<p className="text-xs text-muted-foreground">
{suggestion.tokenSymbol} <ChainIcon chain={suggestion.chain} size="xs" className="inline" />
</p>
</div>
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground">Confidence</div>
<div className="font-bold text-sm">{suggestion.overallConfidence}%</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Current Price */}
<div className="p-3 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground mb-1">Current Price</div>
<div className="font-bold text-2xl">{formatPrice(suggestion.currentPrice)}</div>
</div>
{/* Entry Zone */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500" />
<h3 className="font-semibold text-sm">Entry Zone</h3>
</div>
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-lg text-green-600 dark:text-green-400">
{formatPrice(suggestion.entry.min)} - {formatPrice(suggestion.entry.max)}
</span>
</div>
<p className="text-xs text-muted-foreground">{suggestion.entry.reasoning}</p>
</div>
</div>
{/* Targets */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Target className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Take Profit Targets</h3>
</div>
<div className="space-y-2">
{suggestion.targets.map((target) => (
<div
key={target.level}
className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
>
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-sm">🎯 Target {target.level}</span>
<span className="text-xs text-muted-foreground">
Confidence: {target.confidence}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-bold text-blue-600 dark:text-blue-400">
{formatPrice(target.price)}
</span>
<span className="font-semibold text-sm text-green-600 dark:text-green-400">
+{target.percentGain.toFixed(1)}%
</span>
</div>
</div>
))}
</div>
</div>
{/* Stop Loss */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500" />
<h3 className="font-semibold text-sm">Stop Loss</h3>
</div>
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-lg text-red-600 dark:text-red-400">
{formatPrice(suggestion.stopLoss.price)}
</span>
<span className="font-semibold text-sm text-red-600 dark:text-red-400">
{suggestion.stopLoss.percentLoss.toFixed(1)}%
</span>
</div>
<p className="text-xs text-muted-foreground">{suggestion.stopLoss.reasoning}</p>
</div>
</div>
{/* Risk/Reward */}
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Risk/Reward Ratio</span>
<span className={cn("font-bold text-lg", getRiskRewardColor(suggestion.riskReward))}>
1:{suggestion.riskReward.toFixed(1)}
</span>
</div>
<div className="flex items-center gap-2">
<div className={cn(
"px-2 py-1 rounded text-xs font-medium",
suggestion.riskReward >= 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)}
</div>
</div>
</div>
{/* Why? Section */}
<div className="space-y-2">
<button
className="flex items-center gap-2 w-full"
onClick={() => setShowDetails(!showDetails)}
>
<Info className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-sm">Why?</h3>
<div className={cn(
"ml-auto transition-transform",
showDetails && "rotate-180"
)}>
</div>
</button>
{showDetails && (
<div className="space-y-3 pl-6">
<div>
<h4 className="text-xs font-semibold text-muted-foreground mb-1">Reasoning:</h4>
<ul className="space-y-1">
{suggestion.reasoning.map((reason, i) => (
<li key={i} className="text-xs flex items-start gap-2">
<span className="text-green-600 dark:text-green-400"></span>
<span>{reason}</span>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground mb-1">Invalidation Conditions:</h4>
<ul className="space-y-1">
{suggestion.invalidationConditions.map((condition, i) => (
<li key={i} className="text-xs flex items-start gap-2">
<AlertCircle className="h-3 w-3 text-red-600 dark:text-red-400 mt-0.5" />
<span>{condition}</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
</div>
{/* Footer Actions */}
<div className="border-t p-3 space-y-2">
<Button
variant="default"
className="w-full"
onClick={onSetAlerts}
>
Set Alerts for These Levels
</Button>
<Button
variant="outline"
className="w-full"
onClick={onViewChart}
>
View Chart
</Button>
</div>
</div>
);
}

View file

@ -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
// ============================================

View file

@ -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 (
<div className={cn("flex flex-col h-full", className)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2">
<Wallet className="h-5 w-5 text-primary" />
<div>
<h2 className="font-semibold">Portfolio</h2>
<p className="text-xs text-muted-foreground">
{portfolio.holdings.length} tokens
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{/* Total Value */}
<div className="p-4 border-b bg-muted/30">
<div className="text-xs text-muted-foreground mb-1">Total Value</div>
<div className="flex items-baseline gap-2 mb-2">
<span className="font-bold text-3xl">{formatCurrency(portfolio.totalValue)}</span>
</div>
<div className="flex items-center gap-2">
<span className={cn(
"font-semibold text-sm",
portfolio.change24hPercent >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
)}>
{formatPercent(portfolio.change24hPercent)}
</span>
<span className="text-xs text-muted-foreground">
({portfolio.change24h >= 0 ? "+" : ""}{formatCurrency(portfolio.change24h)}) 24h
</span>
{portfolio.change24hPercent >= 0 ? (
<TrendingUp className="h-4 w-4 text-green-600" />
) : (
<TrendingDown className="h-4 w-4 text-red-600" />
)}
</div>
</div>
{/* Holdings List */}
<div className="divide-y">
{portfolio.holdings.map((holding) => (
<div key={`${holding.chain}-${holding.tokenAddress}`} className="p-4 hover:bg-muted/50 transition-colors">
{/* Token Info */}
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<div>
<div className="flex items-center gap-2">
<span className="font-semibold">{holding.symbol}</span>
<ChainIcon chain={holding.chain} size="xs" />
</div>
<div className="text-xs text-muted-foreground">{holding.name}</div>
</div>
</div>
<div className="text-right">
<div className="font-semibold">{formatCurrency(holding.currentValue)}</div>
<div className={cn(
"text-xs font-medium",
holding.change24hPercent >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
)}>
{formatPercent(holding.change24hPercent)}
</div>
</div>
</div>
{/* Amount and Price */}
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
<span>{holding.amount} tokens</span>
<span>${holding.currentPrice.toFixed(6)}</span>
</div>
{/* P&L (if available) */}
{holding.pnl !== undefined && holding.pnlPercent !== undefined && (
<div className="flex items-center justify-between mb-3 p-2 bg-muted/50 rounded">
<span className="text-xs text-muted-foreground">P&L</span>
<div className="flex items-center gap-2">
<span className={cn(
"text-xs font-semibold",
holding.pnl >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
)}>
{holding.pnl >= 0 ? "+" : ""}{formatCurrency(holding.pnl)}
</span>
<span className={cn(
"text-xs font-medium",
holding.pnlPercent >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
)}>
({formatPercent(holding.pnlPercent)})
</span>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1 h-8 text-xs"
onClick={() => onAnalyzeToken?.(holding)}
>
<BarChart3 className="h-3 w-3 mr-1" />
Analyze
</Button>
<Button
variant="outline"
size="sm"
className="flex-1 h-8 text-xs"
onClick={() => onSetAlert?.(holding)}
>
<Bell className="h-3 w-3 mr-1" />
Alert
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0"
onClick={() => onViewToken?.(holding)}
>
<Eye className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
{/* Add Position Button */}
<div className="p-4 border-t">
<Button
variant="outline"
className="w-full"
onClick={onAddPosition}
>
<Plus className="h-4 w-4 mr-2" />
Add Manual Position
</Button>
</div>
{/* Performance Analytics */}
<div className="p-4 border-t bg-muted/30">
<h3 className="font-semibold text-sm mb-3">Performance</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-background rounded">
<span className="text-xs text-muted-foreground">Best Performer</span>
<span className="text-xs font-semibold text-green-600 dark:text-green-400">
{portfolio.analytics.bestPerformer.symbol} (+{portfolio.analytics.bestPerformer.change.toFixed(1)}%)
</span>
</div>
<div className="flex items-center justify-between p-2 bg-background rounded">
<span className="text-xs text-muted-foreground">Worst Performer</span>
<span className="text-xs font-semibold text-red-600 dark:text-red-400">
{portfolio.analytics.worstPerformer.symbol} ({portfolio.analytics.worstPerformer.change.toFixed(1)}%)
</span>
</div>
<div className="flex items-center justify-between p-2 bg-background rounded">
<span className="text-xs text-muted-foreground">Win Rate</span>
<span className="text-xs font-semibold">{portfolio.analytics.winRate}%</span>
</div>
</div>
</div>
</div>
</div>
);
}