mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 22:02:39 +02:00
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:
parent
db22cd4a64
commit
ea2080619b
4 changed files with 1199 additions and 0 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue