feat: trading bot MVP — ICT Order Block + Liquidity Sweep strategy

Full-stack trading bot with:
- FastAPI backend with ICT strategy (Order Block + Liquidity Sweep detection)
- Backtester engine with rolling window, spread simulation, and performance metrics
- Hybrid market data service (yfinance + TwelveData with rate limiting + SQLite cache)
- Simulated exchange for paper trading
- React/TypeScript frontend with TradingView lightweight-charts v5
- Live dashboard with candlestick chart, OHLC legend, trade markers
- Backtest page with configurable parameters, equity curve, and trade table
- WebSocket support for real-time updates
- Bot runner with asyncio loop for automated trading

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 23:25:51 +01:00
commit 4df8d53b1a
58 changed files with 7484 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { useState } from 'react'
import { Play, Square, Wifi, WifiOff } from 'lucide-react'
import type { BotStatus } from '../../lib/api'
import { startBot, stopBot } from '../../lib/api'
interface Props {
status: BotStatus | null
wsConnected: boolean
onStatusChange: () => void
}
export default function BotStatusCard({ status, wsConnected, onStatusChange }: Props) {
const [loading, setLoading] = useState(false)
const handleToggle = async () => {
setLoading(true)
try {
if (status?.running) {
await stopBot()
} else {
await startBot()
}
onStatusChange()
} finally {
setLoading(false)
}
}
return (
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-[#64748b] uppercase tracking-wider">
Bot Status
</h2>
<div className="flex items-center gap-1.5 text-xs">
{wsConnected ? (
<><Wifi size={12} className="text-[#26a69a]" /><span className="text-[#26a69a]">Live</span></>
) : (
<><WifiOff size={12} className="text-[#64748b]" /><span className="text-[#64748b]">Offline</span></>
)}
</div>
</div>
<div className="flex items-center gap-3">
<div className={`w-2.5 h-2.5 rounded-full ${status?.running ? 'bg-[#26a69a] animate-pulse' : 'bg-[#64748b]'}`} />
<span className="text-lg font-bold">
{status?.running ? 'Running' : 'Stopped'}
</span>
</div>
{status?.running && (
<div className="mt-2 text-xs text-[#64748b] space-y-0.5">
<div>{status.instrument} · {status.granularity}</div>
{status.started_at && (
<div>Depuis {new Date(status.started_at).toLocaleTimeString('fr-FR')}</div>
)}
</div>
)}
<button
onClick={handleToggle}
disabled={loading}
className={`mt-4 w-full flex items-center justify-center gap-2 py-2 rounded-lg text-sm font-medium transition-colors ${
status?.running
? 'bg-[#ef535020] text-[#ef5350] hover:bg-[#ef535040] border border-[#ef535033]'
: 'bg-[#6366f1] text-white hover:bg-[#4f46e5]'
} disabled:opacity-50`}
>
{status?.running ? (
<><Square size={14} /> Arrêter</>
) : (
<><Play size={14} /> Démarrer</>
)}
</button>
</div>
)
}

View File

@@ -0,0 +1,46 @@
import type { Trade } from '../../lib/api'
interface Props {
trades: Trade[]
}
export default function PnLSummary({ trades }: Props) {
const closed = trades.filter((t) => t.status === 'closed' && t.pnl !== null)
const totalPnl = closed.reduce((acc, t) => acc + (t.pnl ?? 0), 0)
const winners = closed.filter((t) => (t.pnl ?? 0) > 0)
const winRate = closed.length > 0 ? (winners.length / closed.length) * 100 : 0
const stats = [
{
label: 'PnL Total',
value: `${totalPnl >= 0 ? '+' : ''}${totalPnl.toFixed(2)}`,
color: totalPnl >= 0 ? 'text-[#26a69a]' : 'text-[#ef5350]',
},
{
label: 'Win Rate',
value: `${winRate.toFixed(1)}%`,
color: winRate >= 50 ? 'text-[#26a69a]' : 'text-[#ef5350]',
},
{
label: 'Trades',
value: closed.length.toString(),
color: 'text-white',
},
{
label: 'Ouverts',
value: trades.filter((t) => t.status === 'open').length.toString(),
color: 'text-[#6366f1]',
},
]
return (
<div className="grid grid-cols-2 gap-3">
{stats.map((s) => (
<div key={s.label} className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<div className="text-xs text-[#64748b] mb-1">{s.label}</div>
<div className={`text-xl font-bold ${s.color}`}>{s.value}</div>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,92 @@
import type { Trade } from '../../lib/api'
interface Props {
trades: Trade[]
}
export default function TradeList({ trades }: Props) {
if (trades.length === 0) {
return (
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-6 text-center text-[#64748b] text-sm">
Aucun trade pour le moment
</div>
)
}
return (
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#2a2d3e]">
<h3 className="text-sm font-semibold text-[#64748b] uppercase tracking-wider">
Trades récents
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-[#64748b] border-b border-[#2a2d3e]">
<th className="text-left px-4 py-2">Instrument</th>
<th className="text-left px-4 py-2">Direction</th>
<th className="text-right px-4 py-2">Entrée</th>
<th className="text-right px-4 py-2">Sortie</th>
<th className="text-right px-4 py-2">PnL</th>
<th className="text-left px-4 py-2">Statut</th>
<th className="text-left px-4 py-2">Date</th>
</tr>
</thead>
<tbody>
{trades.map((t) => (
<tr
key={t.id}
className="border-b border-[#2a2d3e] hover:bg-[#2a2d3e20] transition-colors"
>
<td className="px-4 py-2.5 font-mono text-xs">{t.instrument}</td>
<td className="px-4 py-2.5">
<span
className={`px-1.5 py-0.5 rounded text-xs font-semibold ${
t.direction === 'buy'
? 'bg-[#26a69a20] text-[#26a69a]'
: 'bg-[#ef535020] text-[#ef5350]'
}`}
>
{t.direction.toUpperCase()}
</span>
</td>
<td className="px-4 py-2.5 text-right font-mono text-xs">
{t.entry_price.toFixed(5)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-xs text-[#64748b]">
{t.exit_price ? t.exit_price.toFixed(5) : '—'}
</td>
<td className="px-4 py-2.5 text-right font-mono text-xs">
{t.pnl !== null ? (
<span className={t.pnl >= 0 ? 'text-[#26a69a]' : 'text-[#ef5350]'}>
{t.pnl >= 0 ? '+' : ''}{t.pnl.toFixed(2)}
</span>
) : (
<span className="text-[#64748b]"></span>
)}
</td>
<td className="px-4 py-2.5">
<span
className={`px-1.5 py-0.5 rounded text-xs ${
t.status === 'open'
? 'bg-[#6366f120] text-[#6366f1]'
: t.status === 'closed'
? 'bg-[#2a2d3e] text-[#64748b]'
: 'bg-[#ef535020] text-[#ef5350]'
}`}
>
{t.status}
</span>
</td>
<td className="px-4 py-2.5 text-xs text-[#64748b]">
{new Date(t.opened_at).toLocaleDateString('fr-FR')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}