Files
stonks-oracle/frontend/src/pages/trading/TradeHistory.tsx
T
Celes Renata 4ffde8cc06 feat: autonomous trading engine — full implementation
- Database migration 018 with 13 tables for trading engine state
- Trading engine service (services/trading/) with 12 pure computation modules:
  position sizer, stop-loss manager, reserve pool, circuit breaker,
  risk tier controller, correlation matrix, tax lots, trading window,
  gradual entry, notifications, micro-trading, backtester
- Core TradingEngine with pre-trade evaluation pipeline and integration wiring
- FastAPI HTTP service with 14 endpoints (health, config, decisions, metrics, backtest)
- Performance tracker with Sharpe ratio, drawdown, profit factor computation
- 194 Python tests (165 property-based + 29 integration)
- Frontend: 13 TanStack Query hooks, 7 dashboard panels, tabbed Trading Engine page
- Helm chart entry, network policy, nginx proxy, ingress for trading-engine
- Shared infrastructure: enums, Redis keys, TradingConfig in AppConfig
2026-04-15 16:12:22 +00:00

126 lines
3.8 KiB
TypeScript

import { useState } from 'react';
import { useTradingDecisions } from '../../api/tradingHooks';
import type { TradingDecision } from '../../api/tradingHooks';
import { Card, LoadingSpinner } from '../../components/ui';
import { DataTable, type Column } from '../../components/DataTable';
function fmtUsd(v: number | null | undefined) {
if (v == null) return '—';
return `$${v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function pnlColor(v: number | null | undefined) {
if (v == null) return 'text-gray-400';
return v >= 0 ? 'text-green-400' : 'text-red-400';
}
const PAGE_SIZE = 25;
const columns: Column<TradingDecision>[] = [
{
key: 'ticker',
header: 'Ticker',
className: 'font-mono font-semibold text-brand-300',
},
{
key: 'decision',
header: 'Decision',
render: (r) => <span className="capitalize">{r.decision}</span>,
},
{
key: 'computed_share_quantity',
header: 'Shares',
render: (r) => <span>{r.computed_share_quantity ?? '—'}</span>,
},
{
key: 'computed_position_size',
header: 'Position Size',
render: (r) => <span>{fmtUsd(r.computed_position_size)}</span>,
},
{
key: 'risk_tier_at_decision',
header: 'Risk Tier',
render: (r) => <span className="capitalize">{r.risk_tier_at_decision}</span>,
},
{
key: 'portfolio_heat_at_decision',
header: 'Heat',
render: (r) => (
<span className={pnlColor(r.portfolio_heat_at_decision)}>
{r.portfolio_heat_at_decision != null ? `${(r.portfolio_heat_at_decision * 100).toFixed(1)}%` : '—'}
</span>
),
},
{
key: 'is_micro_trade',
header: 'Micro',
render: (r) => <span>{r.is_micro_trade ? 'Yes' : 'No'}</span>,
},
{
key: 'created_at',
header: 'Date',
render: (r) => <span className="text-xs">{new Date(r.created_at).toLocaleString()}</span>,
},
];
export function TradeHistory() {
const [tickerFilter, setTickerFilter] = useState('');
const [page, setPage] = useState(0);
const { data, isLoading } = useTradingDecisions({
ticker: tickerFilter || undefined,
limit: PAGE_SIZE,
offset: page * PAGE_SIZE,
});
if (isLoading) return <LoadingSpinner />;
return (
<div className="space-y-4">
<Card>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-medium text-gray-400">Trade History</h2>
<input
type="text"
placeholder="Filter by ticker…"
value={tickerFilter}
onChange={(e) => {
setTickerFilter(e.target.value.toUpperCase());
setPage(0);
}}
className="w-32 rounded-md border border-surface-700 bg-surface-950 px-2 py-1 text-xs text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none"
aria-label="Filter by ticker"
/>
</div>
<DataTable<TradingDecision>
data={data ?? []}
columns={columns}
keyField="id"
pageSize={PAGE_SIZE}
emptyMessage="No trading decisions found"
/>
{/* Server-side pagination controls */}
<div className="mt-2 flex items-center justify-end gap-2 text-xs text-gray-500">
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="rounded px-2 py-1 hover:bg-surface-800 disabled:opacity-30"
aria-label="Previous page"
>
Prev
</button>
<span>Page {page + 1}</span>
<button
onClick={() => setPage((p) => p + 1)}
disabled={(data?.length ?? 0) < PAGE_SIZE}
className="rounded px-2 py-1 hover:bg-surface-800 disabled:opacity-30"
aria-label="Next page"
>
Next
</button>
</div>
</Card>
</div>
);
}