diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..65799df --- /dev/null +++ b/.env.local.example @@ -0,0 +1,11 @@ +# Copy to .env.local and fill in your values. + +# Backend REST API base URL (no trailing slash) +# Dev: http://localhost:8000 +# Production: https://api.yourdomain.com +NEXT_PUBLIC_API_URL=https://api.yourdomain.com + +# Backend WebSocket base URL +# Dev: ws://localhost:8000 +# Production: wss://api.yourdomain.com +NEXT_PUBLIC_WS_URL=wss://api.yourdomain.com diff --git a/app/[locale]/DashboardClient.tsx b/app/[locale]/DashboardClient.tsx index b7ba785..a48d19c 100644 --- a/app/[locale]/DashboardClient.tsx +++ b/app/[locale]/DashboardClient.tsx @@ -7,8 +7,7 @@ import { useDashboardStore } from '@/store/dashboard' import { usePriceSocket } from '@/lib/useRealtimeData' import { getPrices, getUserPublic } from '@/lib/api' import ChartPanel from '@/components/dashboard/ChartPanel' -import BotPanel from '@/components/dashboard/BotPanel' -import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo } from '@/components/dashboard/PostCards' +import PostRow, { SignalPill, SourceIcon, fmtPct, timeAgo, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards' interface Props { initialPosts: TrumpPost[] @@ -47,7 +46,7 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
@realDonaldTrump
-
{timeAgo(post.published_at)} ago · {new Date(post.published_at).toLocaleString()}
+
·
@@ -63,9 +62,9 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void }) {/* AI confidence */} -
+
AI confidence - {post.ai_confidence}% + {post.ai_confidence}%
@@ -74,19 +73,8 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void }) {/* AI reasoning */} {post.ai_reasoning && ( <> -
AI reasoning
-
+
AI reasoning
+
{post.ai_reasoning}
@@ -144,26 +132,35 @@ function SelectHint() { // ── Main dashboard ───────────────────────────────────────────────────────────── export default function DashboardClient({ initialPosts, initialPerformance }: Props) { - const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setHlApiKeySet, isSubscribed } = useDashboardStore() + const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness } = useDashboardStore() function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') } const { address, isConnected } = useAccount() const [posts, setPosts] = useState(initialPosts) const [candles, setCandles] = useState([]) const [selectedPostId, setSelectedPostId] = useState(null) + // For 1D: show all posts from selected day + const [selectedDayPosts, setSelectedDayPosts] = useState(null) useEffect(() => { if (!isConnected || !address) { setSubscribed(false) setHlApiKeySet(false) + setBotReadiness('unknown') return } + // Clear account-scoped bot state immediately when the connected wallet + // changes so a previous wallet's status never leaks into the next one. + setSubscribed(false) + setHlApiKeySet(false) + setBotReadiness('unknown') getUserPublic(address.toLowerCase()) .then((user) => { setSubscribed(user.active) setHlApiKeySet(user.hl_api_key_set) + setBotReadiness(user.hl_api_key_set ? 'saved' : 'unknown') }) .catch(() => {}) - }, [address, isConnected, setSubscribed, setHlApiKeySet]) + }, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness]) usePriceSocket({ onPrice: (a, price) => setLivePrice(a, price), @@ -187,9 +184,13 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr : 0 const totalPosts = posts.length + const todayKey = new Date().toISOString().slice(0, 10) + const signalsToday = posts.filter(p => p.published_at?.slice(0, 10) === todayKey).length const actionablePosts = posts.filter(p => p.signal === 'buy' || p.signal === 'short').length const winRate = initialPerformance?.win_rate ?? 0 const netPnl = initialPerformance?.net_pnl_usd ?? 0 + const hasPriceData = candles.length > 0 + const hasPerformanceData = Boolean(initialPerformance) return (
@@ -197,7 +198,7 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr

Signal monitor

- {actionablePosts} actionable signals · Auto-trader {isSubscribed ? 'running' : 'standby'} · Model v4-selective + {actionablePosts} actionable signals · Auto-trader {botReadiness === 'ready' ? 'ready' : hlApiKeySet ? 'saved' : isSubscribed ? 'subscribed' : 'standby'} · {hasPriceData ? 'Live market context' : 'Waiting for market data'}

@@ -212,24 +213,24 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
BTC
{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}
- = 0 ? 'up' : 'down'}`}>{fmtPct(priceChange)} + = 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} {timeframe}
Signals today
-
{actionablePosts}
-
Actionable posts
+
{signalsToday}
+
{actionablePosts} actionable total
30d Net P&L
-
{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString(undefined, { maximumFractionDigits: 0 })}
-
Bot performance
+
{netPnl >= 0 ? '+$' : '-$'}{Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}
+
{hasPerformanceData ? 'Bot performance' : 'Performance pending'}
Win rate
{initialPerformance ? (winRate * 100).toFixed(1) + '%' : '—'}
-
{initialPerformance?.total_trades ?? 0} trades
+
{hasPerformanceData ? `${initialPerformance?.total_trades ?? 0} trades` : 'Waiting for trade history'}
@@ -244,7 +245,7 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
{lastCandle ? '$' + Math.round(lastCandle.close).toLocaleString() : '—'}
- = 0 ? 'up' : 'down'}`}>{fmtPct(priceChange)} · {timeframe} + = 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · {timeframe}
@@ -269,7 +270,14 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr posts={posts} candles={candles} externalSelectedId={selectedPostId} - onSelectPost={setSelectedPostId} + onSelectPost={(id) => { + setSelectedDayPosts(null) + setSelectedPostId(id) + }} + onSelectDayPosts={(dayPosts) => { + setSelectedDayPosts(dayPosts) + setSelectedPostId(null) + }} />
@@ -289,26 +297,120 @@ export default function DashboardClient({ initialPosts, initialPerformance }: Pr
{recentPosts.map(p => ( - setSelectedPostId(selectedPostId === p.id ? null : p.id)} - /> + ))}
- {/* Right rail: bot stats + post detail */} + {/* Right rail */}
- + {/* Day-view: all posts from clicked day */} + {selectedDayPosts ? ( +
+
+
+
{selectedDayPosts.length} posts · this candle
+
+ +
+
+ +
+
+ {selectedDayPosts.map(p => { + const expanded = selectedPostId === p.id + const impact = p.price_impact + function fmtImpactPct(v: number | null | undefined) { + if (v == null || isNaN(v)) return '—' + const s = Math.abs(v).toFixed(2) + '%' + return v >= 0 ? '+' + s : '-' + s + } + return ( +
+ {/* collapsed row — always visible */} +
setSelectedPostId(expanded ? null : p.id)} + style={{ padding:12, cursor:'pointer', + background: expanded ? 'var(--bg-sunk)' : 'transparent' }}> +
+
+ + {p.sentiment} +
+ +
+

+ {p.text} +

+
+
+
+
+ {p.ai_confidence}% conf +
+
- {/* Post detail — shown when something is selected, else hint */} - {selectedPost - ? setSelectedPostId(null)} /> - : - } + {/* expanded detail */} + {expanded && ( +
+ {p.ai_reasoning && ( + <> +
AI reasoning
+
+ {p.ai_reasoning} +
+ + )} + {impact && ( + <> +
Price impact · {impact.asset}
+
+ {(['m5','m15','m1h'] as const).map((key) => { + const label = key === 'm5' ? '5m' : key === 'm15' ? '15m' : '1h' + const v = impact[key] + const correct = impact[`correct_${key}` as 'correct_m5'|'correct_m15'|'correct_m1h'] + return ( +
+
{label}
+
=0?'up':'down'}`} style={{ fontSize:13, fontWeight:600 }}> + {fmtImpactPct(v)} +
+ {correct != null && ( +
+ {correct ? '✓' : '✗'} +
+ )} +
+ ) + })} +
+ + )} +
+ )} +
+ ) + })} +
+
+ ) : selectedPost ? ( + setSelectedPostId(null)} /> + ) : ( + + )}
diff --git a/app/[locale]/analytics/page.tsx b/app/[locale]/analytics/page.tsx index eea7cb7..e83c4df 100644 --- a/app/[locale]/analytics/page.tsx +++ b/app/[locale]/analytics/page.tsx @@ -4,6 +4,8 @@ import { useState, useEffect } from 'react' import { getPerformance, getTrades } from '@/lib/api' import type { BotPerformance, BotTrade } from '@/types' +type Period = '7d' | '30d' | '90d' | 'All' + function fmtMoney(n: number) { if (n == null || isNaN(n)) return '—' const abs = Math.abs(n) @@ -20,30 +22,65 @@ function fmtHold(s: number) { return Math.floor(m / 60) + 'h ' + (m % 60) + 'm' } +function inPeriod(iso: string, period: Period) { + if (period === 'All') return true + const days = Number.parseInt(period, 10) + if (Number.isNaN(days)) return true + const time = new Date(iso).getTime() + return time >= Date.now() - days * 24 * 60 * 60 * 1000 +} + +function calcDrawdownPct(trades: BotTrade[]) { + const ordered = [...trades].sort( + (a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime() + ) + let equity = 0 + let peak = 0 + let maxDrawdownPct = 0 + + for (const trade of ordered) { + equity += trade.pnl_usd + peak = Math.max(peak, equity) + if (peak > 0) { + maxDrawdownPct = Math.max(maxDrawdownPct, ((peak - equity) / peak) * 100) + } + } + + return maxDrawdownPct +} + export default function AnalyticsPage() { const [perf, setPerf] = useState(null) const [trades, setTrades] = useState([]) - const [period, setPeriod] = useState('30d') + const [period, setPeriod] = useState('30d') useEffect(() => { Promise.all([ getPerformance().catch(() => null), - getTrades(200, 1).catch(() => []), + getTrades(100, 1).catch(() => []), ]).then(([p, t]) => { setPerf(p) setTrades(t) }) }, []) - const winRate = perf ? perf.win_rate * 100 : 0 - const bestTrade = trades.length ? Math.max(...trades.map(t => t.pnl_usd)) : 0 - const worstTrade = trades.length ? Math.min(...trades.map(t => t.pnl_usd)) : 0 - const avgTrade = trades.length ? trades.reduce((s, t) => s + t.pnl_usd, 0) / trades.length : 0 + const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) + const totalPnl = filteredTrades.reduce((sum, trade) => sum + trade.pnl_usd, 0) + const wins = filteredTrades.filter((trade) => trade.pnl_usd > 0).length + const winRate = filteredTrades.length ? (wins / filteredTrades.length) * 100 : 0 + const bestTrade = filteredTrades.length ? Math.max(...filteredTrades.map(t => t.pnl_usd)) : 0 + const worstTrade = filteredTrades.length ? Math.min(...filteredTrades.map(t => t.pnl_usd)) : 0 + const avgTrade = filteredTrades.length ? filteredTrades.reduce((s, t) => s + t.pnl_usd, 0) / filteredTrades.length : 0 + const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 + const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) + const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length + ? perf.net_pnl_usd + : totalPnl const metrics = [ - { k: 'Max drawdown', v: perf ? perf.max_drawdown_pct.toFixed(1) + '%' : '—', sub: 'Worst peak-to-trough', down: true }, - { k: 'Total trades', v: String(perf?.total_trades ?? '—'), sub: 'Bot executions' }, - { k: 'Avg hold time', v: perf ? fmtHold(perf.avg_hold_seconds) : '—', sub: 'Per trade' }, + { k: 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: 'Worst peak-to-trough', down: true }, + { k: 'Total trades', v: String(filteredTrades.length || '—'), sub: 'Closed bot executions' }, + { k: 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: 'Per trade' }, { k: 'Avg trade P&L', v: fmtMoney(avgTrade), sub: 'Mean per trade', up: avgTrade > 0 }, { k: 'Best trade', v: fmtMoney(bestTrade), sub: 'Largest single win', up: true }, { k: 'Worst trade', v: fmtMoney(worstTrade), sub: 'Largest single loss', down: true }, @@ -57,7 +94,7 @@ export default function AnalyticsPage() {

Deep dive into {period} of signals and trades.

- {['7d', '30d', '90d', 'All'].map(p => ( + {(['7d', '30d', '90d', 'All'] as const).map(p => ( ))}
@@ -69,11 +106,11 @@ export default function AnalyticsPage() {
Performance · {period}
- {perf ? fmtMoney(perf.net_pnl_usd) : '—'} + {filteredTrades.length ? fmtMoney(summaryPnl) : '—'}
- = 0.5 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate - {perf?.total_trades ?? 0} trades + = 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate + {filteredTrades.length} trades
@@ -91,9 +128,13 @@ export default function AnalyticsPage() { {/* No data message */} - {!perf && !trades.length && ( + {!filteredTrades.length && (
-

No trade data yet. The bot will populate analytics once it starts executing.

+

+ {trades.length + ? `No trades closed in the ${period} window yet.` + : 'No trade data yet. The bot will populate analytics once it starts executing.'} +

)} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..ef425f2 --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -0,0 +1,46 @@ +export default function ContactPage() { + return ( +
+
+
+

Contact Us

+

Questions, feedback, or support requests.

+
+
+ +
+
+
+ + +
+
+ +