Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 75 additions & 33 deletions src/apps/insights/components/SignalCard/SignalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@

import { forwardRef, useState } from 'react';
import { motion } from 'framer-motion';
import {
Clock,
TrendingUp,
Target,
XCircle,
RefreshCw,
CheckCircle2,
ChevronUp,
import {
Clock,
TrendingUp,
Target,
XCircle,
RefreshCw,
CheckCircle2,
ChevronUp,
ChevronDown,
ArrowUpCircle
ArrowUpCircle,
Copy,
Check
} from 'lucide-react';
import { Badge } from '../ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
Expand All @@ -35,15 +37,37 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
({ signal, leverage, sparklineData, logoMap, animateOnMount = true }, ref) => {
const applyLeverage = (pnl: number) => pnl * leverage;
const [timelineOpen, setTimelineOpen] = useState(false);

const [copied, setCopied] = useState(false);

const copyStrategy = async () => {
const strategy = {
orderSide: signal.order_side,
ticker: signal.ticker,
exchange: "BINANCE",
entryPrice: signal.entry_price,
stopLoss: signal.stop_loss,
tp1: signal.tp1,
tp2: signal.tp2,
tp3: signal.tp3
};

try {
await navigator.clipboard.writeText(JSON.stringify(strategy));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy strategy:', err);
}
};

const getTimeSinceCreated = () => {
const now = new Date();
const created = new Date(signal.created_at);
const diffMs = now.getTime() - created.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);

if (diffDays > 0) {
const remainingHours = diffHours % 24;
return remainingHours > 0 ? `${diffDays}d ${remainingHours}h` : `${diffDays}d`;
Expand Down Expand Up @@ -75,10 +99,10 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
{(() => {
const symbol = normalizeSymbol(signal.ticker);
const logoUrl = logoMap[symbol];

return logoUrl ? (
<>
<img
<img
src={logoUrl}
alt={signal.ticker.replace('.P', '')}
className="w-6 h-6 md:w-8 md:h-8 object-contain"
Expand Down Expand Up @@ -109,9 +133,29 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
</Badge>
</div>
</div>
<div className="text-right text-muted-foreground text-sm flex items-center gap-2">
<Clock className="w-4 h-4" />
{getTimeSinceCreated()}
<div className="text-right flex items-center gap-3">
{isOpen && (
<button
onClick={copyStrategy}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-white/20 transition-all duration-200 text-xs font-medium text-white"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5" />
Copied!
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
Copy Strategy
</>
)}
</button>
)}
<div className="text-muted-foreground text-sm flex items-center gap-2">
<Clock className="w-4 h-4" />
{getTimeSinceCreated()}
</div>
</div>
</div>

Expand Down Expand Up @@ -139,11 +183,11 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
<p className="text-[10px] md:text-xs text-muted-foreground mb-1">Stop Loss</p>
{(() => {
const isTrailing = hasTrailingStop(signal) || (
signal.order_side === 'buy'
? signal.stop_loss > signal.entry_price
signal.order_side === 'buy'
? signal.stop_loss > signal.entry_price
: signal.stop_loss < signal.entry_price
);

return isTrailing ? (
<p className="text-sm md:text-lg font-semibold text-green-400 flex items-center gap-1">
<span className="text-xs">{signal.order_side === 'buy' ? '↑' : '↓'}</span>
Expand Down Expand Up @@ -182,19 +226,19 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
].filter(({ price }) => price != null).map(({ key, price, hit }) => {
const isClosed = ['completed', 'stopped', 'closed'].includes(signal.status || 'active');
const priceReachedTP = signal.current_price && (
signal.order_side === 'buy'
? signal.current_price >= price
signal.order_side === 'buy'
? signal.current_price >= price
: signal.current_price <= price
);
const isHit = hit || (isClosed && priceReachedTP);

const tpPercent = signal.order_side?.toLowerCase() === 'sell'
? ((signal.entry_price - price) / signal.entry_price) * 100
: ((price - signal.entry_price) / signal.entry_price) * 100;
const lockedInPercent = isHit ? (tpPercent * 0.3333) : null;
const displayedLockedIn = lockedInPercent !== null ? applyLeverage(lockedInPercent).toFixed(2) : null;
const displayedPotential = applyLeverage(tpPercent * 0.3333).toFixed(2);

return (
<Tooltip key={key}>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -245,7 +289,7 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
<ChevronDown className="w-4 h-4 ml-auto" />
)}
</button>

{timelineOpen && (
<div className="space-y-2">
{timeline.map((event, idx) => {
Expand All @@ -256,10 +300,10 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
RefreshCw,
CheckCircle2,
}[event.icon];

return (
<div
key={idx}
<div
key={idx}
className="flex items-start gap-3 p-3 rounded-lg bg-slate-900/50 border border-slate-800/50"
>
{IconComponent && <IconComponent className={`w-5 h-5 ${event.iconColor} mt-0.5`} />}
Expand Down Expand Up @@ -299,9 +343,8 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
<>
<div>
<p className="text-sm text-muted-foreground mb-1">Unrealized P/L</p>
<p className={`text-xl font-bold flex items-center gap-1 ${
(signal.profit_loss_percent || 0) >= 0 ? 'text-[hsl(142,76%,58%)]' : 'text-[hsl(348,83%,58%)]'
}`}>
<p className={`text-xl font-bold flex items-center gap-1 ${(signal.profit_loss_percent || 0) >= 0 ? 'text-[hsl(142,76%,58%)]' : 'text-[hsl(348,83%,58%)]'
}`}>
{applyLeverage(signal.profit_loss_percent || 0) >= 0 ? '+' : ''}{applyLeverage(signal.profit_loss_percent || 0).toFixed(2).replace('.', ',')}%
{(signal.profit_loss_percent || 0) >= 0 && <ArrowUpCircle className="w-4 h-4" />}
</p>
Expand All @@ -317,9 +360,8 @@ export const SignalCard = forwardRef<HTMLDivElement, SignalCardProps>(
<>
<div>
<p className="text-sm text-muted-foreground mb-1">Realized P/L</p>
<p className={`text-xl font-bold flex items-center gap-1 ${
(signal.realized_pnl_percent || 0) >= 0 ? 'text-[hsl(142,76%,58%)]' : 'text-[hsl(348,83%,58%)]'
}`}>
<p className={`text-xl font-bold flex items-center gap-1 ${(signal.realized_pnl_percent || 0) >= 0 ? 'text-[hsl(142,76%,58%)]' : 'text-[hsl(348,83%,58%)]'
}`}>
{applyLeverage(signal.realized_pnl_percent || 0) >= 0 ? '+' : ''}{applyLeverage(signal.realized_pnl_percent || 0).toFixed(2).replace('.', ',')}%
{(signal.realized_pnl_percent || 0) >= 0 && <ArrowUpCircle className="w-4 h-4" />}
</p>
Expand Down
13 changes: 7 additions & 6 deletions src/apps/insights/hooks/useTradingSignals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ export const useTradingSignals = (options: UseTradingSignalsOptions = {}) => {
if (!isRefresh) {
setLoading(true);
}

const result = await getTradingSignals();
console.log('🔍 [useTradingSignals] API response:', result);

if (result.error) {
throw result.error;
}

// Firebase function returns { signals: [...] }
const signalsArray = (result.signals || result.data || []) as TradingSignal[];
console.log(`✅ [useTradingSignals] Loaded ${signalsArray.length} signals:`, signalsArray);

// Log first signal structure for debugging
if (signalsArray.length > 0) {
const firstSignal = signalsArray[0];
Expand All @@ -51,10 +52,10 @@ export const useTradingSignals = (options: UseTradingSignalsOptions = {}) => {
entry_price: firstSignal.entry_price,
});
}

setSignals(signalsArray);
setError(null);

// Mark initial load as complete
if (isInitialLoad) {
setIsInitialLoad(false);
Expand All @@ -66,7 +67,7 @@ export const useTradingSignals = (options: UseTradingSignalsOptions = {}) => {
} finally {
setLoading(false);
}
}, [enabled]);
}, [enabled, isInitialLoad]);

useEffect(() => {
if (!enabled) {
Expand Down
25 changes: 13 additions & 12 deletions src/apps/insights/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const App = () => {
enabled: Boolean(eoaAddress),
pollIntervalMs: SUBSCRIPTION_POLL_INTERVAL,
});

const [isAwaitingSubscription, setIsAwaitingSubscription] = useState(false);
const [showManageMenu, setShowManageMenu] = useState(false);
const manageMenuRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -161,7 +162,7 @@ const App = () => {
const consentDate = new Date(consent.timestamp);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

if (consentDate > oneYearAgo) {
setConsentGiven(true);
} else {
Expand All @@ -187,22 +188,22 @@ const App = () => {

if (signals.length > 0 && !loading) {
const openSignals = signals.filter(s => s.status === 'active');

// Only fetch sparklines for signals we haven't fetched yet
const signalsNeedingSparklines = openSignals.filter(signal => {
const alreadyFetched = fetchedSparklineIdsRef.current.has(signal.id);
const hasData = sparklineDataMap[signal.id] && sparklineDataMap[signal.id].length > 0;
const isLoading = sparklineLoading[signal.id];

if (alreadyFetched || hasData || isLoading) {
return false;
}

// Mark as fetched to prevent duplicate requests
fetchedSparklineIdsRef.current.add(signal.id);
return true;
});

if (signalsNeedingSparklines.length > 0) {
console.log(`📊 [Sparkline] Initial fetch for ${signalsNeedingSparklines.length} new signals`);
fetchSparklines(signalsNeedingSparklines);
Expand Down Expand Up @@ -258,15 +259,15 @@ const App = () => {
}
return value;
}, [openSignals]);

const closedTotalPnL = useMemo(() => {
const closedPnL = calculateTotalPnL(closedSignals);
const openRealizedPnL = calculateOpenRealizedPnL();
const value = closedPnL + openRealizedPnL;
console.log(`💰 [PnL] closedTotalPnL: ${value}% (closed: ${closedPnL}%, open realized: ${openRealizedPnL}%)`);
return value;
}, [closedSignals, openSignals]);

const floatingPnL = useMemo(() => {
const value = openTotalPnL + closedTotalPnL;
console.log(`💰 [PnL] floatingPnL: ${value}% (open: ${openTotalPnL}%, closed: ${closedTotalPnL}%)`);
Expand Down Expand Up @@ -305,7 +306,7 @@ const App = () => {
};

const handleRefreshSubscription = useCallback(() => {
refetchSubscription().catch(() => {});
refetchSubscription().catch(() => { });
}, [refetchSubscription]);

const handleSubscribeClick = useCallback(() => {
Expand All @@ -324,7 +325,7 @@ const App = () => {

setIsAwaitingSubscription(true);
startPolling();
refetchSubscription().catch(() => {});
refetchSubscription().catch(() => { });

if (!isNativeApp) {
const confirmed = window.confirm(
Expand Down Expand Up @@ -642,9 +643,9 @@ const App = () => {
</div>
) : (
feedEvents.map((event) => (
<FeedEventCard
key={event.id}
event={event}
<FeedEventCard
key={event.id}
event={event}
leverage={leverage}
animateOnMount={isInitialLoad}
/>
Expand Down