4ffde8cc06
- 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
126 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|