Files
trumpsignal-frontend/app/[locale]/trades/page.tsx
T
2026-04-21 19:32:53 +08:00

180 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import type { BotTrade, TrumpPost } from '@/types'
import { getTrades, getPosts } from '@/lib/api'
function fmtMoney(n: number, opts: { sign?: boolean; decimals?: number } = {}) {
const { decimals = 2, sign = false } = opts
if (n == null || isNaN(n)) return '—'
const abs = Math.abs(n)
const s = abs.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
if (n < 0) return '-$' + s
if (sign && n > 0) return '+$' + s
return '$' + s
}
function fmtPct(n: number) {
const s = n.toFixed(2) + '%'
return n >= 0 ? '+' + s : s
}
function fmtHold(s: number) {
if (s < 60) return s + 's'
const m = Math.floor(s / 60)
if (m < 60) return m + 'm'
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm'
}
function SourceIcon({ source }: { source: string }) {
if (source === 'x') return <div className="src-ico x" style={{ width: 28, height: 28, fontSize: 12 }}>𝕏</div>
return <div className="src-ico truth" style={{ width: 28, height: 28, fontSize: 12 }}>T</div>
}
export default function TradesPage() {
const [trades, setTrades] = useState<BotTrade[]>([])
const [posts, setPosts] = useState<TrumpPost[]>([])
const [loading, setLoading] = useState(true)
const [assetFilter, setAssetFilter] = useState('all')
const [sideFilter, setSideFilter] = useState('all')
useEffect(() => {
Promise.all([
getTrades(200, 1).catch(() => []),
getPosts(500, 1).catch(() => []),
]).then(([t, p]) => {
setTrades(t)
setPosts(p)
}).finally(() => setLoading(false))
}, [])
const filtered = trades.filter(t => {
if (assetFilter !== 'all' && t.asset !== assetFilter) return false
if (sideFilter !== 'all' && t.side !== sideFilter) return false
return true
})
const totalPnl = filtered.reduce((s, t) => s + t.pnl_usd, 0)
const wins = filtered.filter(t => t.pnl_usd > 0).length
const avgHold = filtered.length > 0 ? Math.round(filtered.reduce((s, t) => s + t.hold_seconds, 0) / filtered.length) : 0
return (
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">Trades</h1>
<p className="page-sub">Every trade your bot executed. Transparent and auditable.</p>
</div>
<div className="row gap-s">
<span className="chip">30 days</span>
</div>
</div>
<div className="kpi-row" style={{ gridTemplateColumns: 'repeat(4, 1fr)' }}>
<div className="kpi">
<div className="label">Total trades</div>
<div className="value">{filtered.length}</div>
<div className="foot"><span>Filtered</span></div>
</div>
<div className="kpi">
<div className="label">Win rate</div>
<div className="value">{filtered.length ? ((wins / filtered.length) * 100).toFixed(1) + '%' : '—'}</div>
<div className="foot"><span>{wins}W · {filtered.length - wins}L</span></div>
</div>
<div className="kpi accent">
<div className="label">Net P&amp;L</div>
<div className="value">{fmtMoney(totalPnl, { sign: true, decimals: 0 })}</div>
<div className="foot"><span>All assets</span></div>
</div>
<div className="kpi">
<div className="label">Avg hold</div>
<div className="value">{avgHold ? fmtHold(avgHold) : '—'}</div>
<div className="foot"><span>Per trade</span></div>
</div>
</div>
<div className="row between" style={{ margin: '20px 0 14px' }}>
<div className="row gap-s">
<div className="nav-tabs">
{(['all', 'BTC', 'ETH'] as const).map(a => (
<button key={a} className={`nav-tab ${assetFilter === a ? 'active' : ''}`} onClick={() => setAssetFilter(a)}>
{a === 'all' ? 'All assets' : a}
</button>
))}
</div>
<div className="nav-tabs">
{(['all', 'long', 'short'] as const).map(s => (
<button key={s} className={`nav-tab ${sideFilter === s ? 'active' : ''}`} onClick={() => setSideFilter(s)}>
{s.charAt(0).toUpperCase() + s.slice(1)}
</button>
))}
</div>
</div>
</div>
{loading && <div style={{ textAlign: 'center', padding: 60, color: 'var(--ink-3)' }}>Loading</div>}
{!loading && (
<div className="card flush" style={{ overflow: 'hidden' }}>
<table className="table">
<thead>
<tr>
<th>Asset</th>
<th>Side</th>
<th>Entry</th>
<th>Exit</th>
<th>Hold</th>
<th>Trigger</th>
<th>P&amp;L</th>
</tr>
</thead>
<tbody>
{filtered.length === 0 && (
<tr><td colSpan={7} style={{ textAlign: 'center', padding: 40, color: 'var(--ink-3)' }}>No trades found</td></tr>
)}
{filtered.map(t => {
const triggerPost = posts.find(p => p.id === t.trigger_post_id)
const roiPct = ((t.exit_price - t.entry_price) / t.entry_price) * 100 * (t.side === 'long' ? 1 : -1)
return (
<tr key={t.id}>
<td>
<div className="row gap-s">
<span className={`asset-dot ${t.asset.toLowerCase()}`} />
<span style={{ fontWeight: 500 }}>{t.asset}</span>
</div>
</td>
<td><span className={`side-pill ${t.side}`}>{t.side === 'long' ? '↗ LONG' : '↘ SHORT'}</span></td>
<td className="mono">${t.entry_price.toLocaleString()}</td>
<td className="mono">${t.exit_price.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--ink-2)' }}>{fmtHold(t.hold_seconds)}</td>
<td style={{ maxWidth: 280 }}>
{triggerPost ? (
<div className="row gap-s" style={{ alignItems: 'center' }}>
<SourceIcon source={triggerPost.source} />
<span style={{ fontSize: 12, color: 'var(--ink-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{triggerPost.text.slice(0, 50)}
</span>
</div>
) : (
<span style={{ fontSize: 12, color: 'var(--ink-4)' }}></span>
)}
</td>
<td>
<div className="stack" style={{ alignItems: 'flex-end' }}>
<span className={`delta ${t.pnl_usd >= 0 ? 'up' : 'down'}`} style={{ fontWeight: 600, fontSize: 14 }}>
{fmtMoney(t.pnl_usd, { sign: true })}
</span>
<span className={`delta ${roiPct >= 0 ? 'up' : 'down'}`} style={{ fontSize: 11, opacity: 0.7 }}>{fmtPct(roiPct)}</span>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}