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>
);
}