import { useState, useEffect, useCallback } from 'react';
export interface TradeActivity {
id: string;
conditionId: string;
title: string;
slug: string;
side: string; // 'BUY' | 'SELL'
outcome: string; // 'Yes' | 'No'
size: number;
price: number;
amount: number;
fee: number;
realizedPnl: number;
type: string; // 'TRADE' | 'REDEEM' | ...
createdAt: string;
icon?: string;
}
export interface DailyPnl {
date: string;
pnl: number;
trades: number;
volume: number;
}
export interface PortfolioSummary {
totalTrades: number;
totalBought: number;
totalSold: number;
totalRedeemed: number;
totalRealizedPnl: number;
currentValue: number;
totalPnl: number;
totalFees: number;
uniqueMarkets: number;
firstTrade: string;
lastTrade: string;
}
export interface PortfolioStats {
summary: PortfolioSummary;
dailyPnl: DailyPnl[];
winRate: number;
closedTrades: number;
winningTrades: number;
losingTrades: number;
avgWin: number;
avgLoss: number;
profitFactor: number | null;
}
export function usePortfolioActivity(address: string | undefined, limit = 50) {
const [trades, setTrades] = useState<TradeActivity[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [offset, setOffset] = useState(0);
const fetchPage = useCallback(async (off: number) => {
if (!address) return;
setLoading(true);
try {
const res = await fetch(
`/api/portfolio/activity?address=${address}&limit=${limit}&offset=${off}`
);
const d = await res.json();
if (d.success) {
// Data API /activity uses usdcSize/timestamp and has no id/amount/createdAt —
// normalize to TradeActivity so rows show real $ amounts and times (raw
// passthrough rendered $0 everywhere with duplicate React keys).
const raw: Record<string, unknown>[] = d.data ?? [];
const num = (v: unknown) => Number(v) || 0;
const str = (v: unknown) => (typeof v === 'string' ? v : '');
const items: TradeActivity[] = raw.map((r) => ({
id: `${str(r.transactionHash)}-${str(r.asset) || str(r.conditionId)}`,
conditionId: str(r.conditionId),
title: str(r.title),
slug: str(r.slug),
side: str(r.side),
outcome: str(r.outcome),
size: num(r.size),
price: num(r.price),
amount: num(r.usdcSize),
fee: num(r.fee),
realizedPnl: num(r.realizedPnl),
type: str(r.type) || 'TRADE',
createdAt: r.timestamp ? new Date(num(r.timestamp) * 1000).toISOString() : '',
icon: str(r.icon),
}));
if (off === 0) {
setTrades(items);
} else {
setTrades(prev => [...prev, ...items]);
}
setHasMore(items.length >= limit);
}
} catch { /* ignore */ }
finally { setLoading(false); }
}, [address, limit]);
useEffect(() => {
if (!address) {
const t = setTimeout(() => { setTrades([]); setOffset(0); }, 0);
return () => { clearTimeout(t); };
}
const t = setTimeout(() => { setOffset(0); fetchPage(0); }, 0);
return () => { clearTimeout(t); };
}, [address, fetchPage]);
const loadMore = useCallback(() => {
const next = offset + limit;
setOffset(next);
fetchPage(next);
}, [offset, limit, fetchPage]);
return { trades, loading, hasMore, loadMore, refresh: () => fetchPage(0) };
}
export function usePortfolioStats(address: string | undefined) {
const [stats, setStats] = useState<PortfolioStats | null>(null);
const [loading, setLoading] = useState(false);
const fetch_ = useCallback(async () => {
if (!address) return;
setLoading(true);
try {
const res = await fetch(`/api/portfolio/summary?address=${address}`);
const d = await res.json();
if (d.success) {
setStats(d.data ?? null);
}
} catch { /* ignore */ }
finally { setLoading(false); }
}, [address]);
useEffect(() => {
if (!address) {
const t = setTimeout(() => { setStats(null); }, 0);
return () => { clearTimeout(t); };
}
const t = setTimeout(fetch_, 0);
return () => { clearTimeout(t); };
}, [address, fetch_]);
return { stats, loading, refresh: fetch_ };
}
📜 Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...