done
This commit is contained in:
+172
-10
@@ -1,17 +1,179 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import TradesTable from '@/components/trades/TradesTable'
|
||||
import { getTrades } from '@/lib/api'
|
||||
'use client'
|
||||
|
||||
export const revalidate = 10
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { BotTrade, TrumpPost } from '@/types'
|
||||
import { getTrades, getPosts } from '@/lib/api'
|
||||
|
||||
export default async function TradesPage() {
|
||||
const t = await getTranslations('trades')
|
||||
const trades = await getTrades(100, 1).catch(() => [])
|
||||
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="max-w-[1400px] mx-auto px-6 py-6 pt-20">
|
||||
<h1 className="text-[22px] font-medium text-white mb-5">{t('title')}</h1>
|
||||
<TradesTable trades={trades} />
|
||||
<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&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&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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user