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
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { useState } from 'react';
|
||||
import { useBacktestLaunch, useBacktestResult } from '../../api/tradingHooks';
|
||||
import { Card, LoadingSpinner } from '../../components/ui';
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
} from 'recharts';
|
||||
|
||||
const RISK_TIERS = ['conservative', 'moderate', 'aggressive'];
|
||||
|
||||
function fmtUsd(v: number | null | undefined) {
|
||||
if (v == null) return '—';
|
||||
return `$${v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function fmtPct(v: number | null | undefined) {
|
||||
if (v == null) return '—';
|
||||
return `${(v * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export function BacktestPanel() {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [initialCapital, setInitialCapital] = useState('100000');
|
||||
const [riskTier, setRiskTier] = useState('moderate');
|
||||
const [backtestId, setBacktestId] = useState<string | undefined>(undefined);
|
||||
|
||||
const launch = useBacktestLaunch();
|
||||
const { data: result, isLoading: resultLoading } = useBacktestResult(backtestId);
|
||||
|
||||
function handleLaunch() {
|
||||
if (!startDate || !endDate) return;
|
||||
launch.mutate(
|
||||
{
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
initial_capital: Number(initialCapital),
|
||||
risk_tier: riskTier,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => setBacktestId(data.id),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const equityCurve = (result?.equity_curve ?? []).map((pt) => ({
|
||||
date: new Date(pt.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
|
||||
value: pt.portfolio_value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Configuration Form */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Backtest Configuration</h2>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-gray-500">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full rounded-md border border-surface-700 bg-surface-950 px-2 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-gray-500">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-full rounded-md border border-surface-700 bg-surface-950 px-2 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-gray-500">Initial Capital</label>
|
||||
<input
|
||||
type="number"
|
||||
value={initialCapital}
|
||||
onChange={(e) => setInitialCapital(e.target.value)}
|
||||
className="w-full rounded-md border border-surface-700 bg-surface-950 px-2 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-gray-500">Risk Tier</label>
|
||||
<select
|
||||
value={riskTier}
|
||||
onChange={(e) => setRiskTier(e.target.value)}
|
||||
className="w-full rounded-md border border-surface-700 bg-surface-950 px-2 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
|
||||
>
|
||||
{RISK_TIERS.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLaunch}
|
||||
disabled={!startDate || !endDate || launch.isPending}
|
||||
className="mt-3 rounded-md bg-brand-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{launch.isPending ? 'Launching…' : 'Run Backtest'}
|
||||
</button>
|
||||
{launch.isError && (
|
||||
<p className="mt-2 text-xs text-red-400">Failed to launch backtest</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Results */}
|
||||
{resultLoading && <LoadingSpinner />}
|
||||
{result && (
|
||||
<>
|
||||
{/* Summary Metrics */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Backtest Results</h2>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
||||
<MetricCard label="Total Return" value={fmtPct(result.total_return)} />
|
||||
<MetricCard label="Sharpe Ratio" value={result.sharpe_ratio?.toFixed(2) ?? '—'} />
|
||||
<MetricCard label="Max Drawdown" value={fmtPct(result.max_drawdown)} />
|
||||
<MetricCard label="Win Rate" value={fmtPct(result.win_rate)} />
|
||||
<MetricCard label="Profit Factor" value={result.profit_factor?.toFixed(2) ?? '—'} />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
|
||||
<span>Trades: {result.trade_count ?? 0}</span>
|
||||
<span>Status: {result.status}</span>
|
||||
<span>Capital: {fmtUsd(result.initial_capital)}</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Equity Curve */}
|
||||
{equityCurve.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Equity Curve</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={equityCurve}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis dataKey="date" tick={{ fill: '#6b7280', fontSize: 11 }} />
|
||||
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: 8 }}
|
||||
labelStyle={{ color: '#9ca3af' }}
|
||||
formatter={(value: number) => [fmtUsd(value), 'Portfolio Value']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Portfolio Value"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-bold text-gray-100">{value}</div>
|
||||
<div className="text-xs text-gray-500">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user