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:
77
frontend/src/components/Dashboard/BotStatus.tsx
Normal file
77
frontend/src/components/Dashboard/BotStatus.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
46
frontend/src/components/Dashboard/PnLSummary.tsx
Normal file
46
frontend/src/components/Dashboard/PnLSummary.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
92
frontend/src/components/Dashboard/TradeList.tsx
Normal file
92
frontend/src/components/Dashboard/TradeList.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user