Bug Cleanup
This commit is contained in:
+60
-35
@@ -8,7 +8,7 @@ import {
|
||||
import { BacktestingTab } from './components/BacktestingTab';
|
||||
import { OptimizerTab } from './components/OptimizerTab';
|
||||
import { TradeHistory } from './components/TradeHistory';
|
||||
import { motion } from 'motion/react';
|
||||
import { motion as Motion } from 'motion/react';
|
||||
|
||||
import { EquityCurve } from './components/EquityCurve';
|
||||
import { MetricCard } from './components/MetricCard';
|
||||
@@ -115,6 +115,7 @@ export default function App() {
|
||||
const candleSeriesRef = useRef(null);
|
||||
const equitySeriesRef = useRef(null);
|
||||
const markersRef = useRef(null);
|
||||
const abortControllerRef = useRef(null);
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
@@ -194,30 +195,41 @@ export default function App() {
|
||||
const loadData = useCallback(async () => {
|
||||
const shouldLoadDashboardData = ['dashboard', 'forex-stats', 'trade-history'].includes(activeTab);
|
||||
if (!shouldLoadDashboardData) {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const shouldLoadBacktest = showBacktest || activeTab === 'forex-stats';
|
||||
const shouldFetchBacktest = shouldLoadBacktest && !hasSharedBacktest;
|
||||
const datasetQuery = `dataset=${encodeURIComponent(selectedDataset)}`;
|
||||
const fetches = [
|
||||
fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}&${datasetQuery}`),
|
||||
fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}&${datasetQuery}`),
|
||||
fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}&${datasetQuery}`, { signal: controller.signal }),
|
||||
fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}&${datasetQuery}`, { signal: controller.signal }),
|
||||
];
|
||||
if (shouldFetchBacktest) {
|
||||
fetches.push(fetch(`http://localhost:8000/api/backtest?timeframe=${timeframe}&rr=${riskReward}&lookback=${stratParams.lookback}&ob_age=${stratParams.obAge}&atr_mult=${stratParams.atrMult}&sweep=${stratParams.sweep}&sweep_lookback=${stratParams.sweepLookback}&session=${stratParams.session}&${datasetQuery}`));
|
||||
fetches.push(fetch(`http://localhost:8000/api/backtest?timeframe=${timeframe}&rr=${riskReward}&lookback=${stratParams.lookback}&ob_age=${stratParams.obAge}&atr_mult=${stratParams.atrMult}&sweep=${stratParams.sweep}&sweep_lookback=${stratParams.sweepLookback}&session=${stratParams.session}&${datasetQuery}`, { signal: controller.signal }));
|
||||
}
|
||||
|
||||
const responses = await Promise.all(fetches);
|
||||
if (controller.signal.aborted) return;
|
||||
const candleData = await responses[0].json();
|
||||
const indicatorData = await responses[1].json();
|
||||
const backtestPayload = shouldFetchBacktest
|
||||
? await responses[2].json()
|
||||
: (shouldLoadBacktest ? backtestData : null);
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
const candles = candleData.candles.map((candle) => ({
|
||||
time: Math.floor(new Date(candle.time).getTime() / 1000),
|
||||
open: candle.open,
|
||||
@@ -331,9 +343,12 @@ export default function App() {
|
||||
setBacktestData(backtestPayload);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') return;
|
||||
console.error('Failed to load data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [activeTab, backtestData, hasSharedBacktest, indicators, riskReward, selectedDataset, showBacktest, timeframe]);
|
||||
|
||||
@@ -419,8 +434,18 @@ export default function App() {
|
||||
const backtestStats = backtestData?.stats ?? null;
|
||||
const equityCurve = useMemo(() => buildEquityCurve(backtestTrades), [backtestTrades]);
|
||||
const monthlyReturns = useMemo(() => buildMonthlyReturns(backtestTrades), [backtestTrades]);
|
||||
const maxDrawdown = useMemo(() => calculateMaxDrawdown(equityCurve), [equityCurve]);
|
||||
const sharpeRatio = useMemo(() => calculateSharpeRatio(backtestTrades), [backtestTrades]);
|
||||
const maxDrawdown = useMemo(() => {
|
||||
if (backtestStats?.max_drawdown_pct != null) {
|
||||
return -Math.abs(backtestStats.max_drawdown_pct);
|
||||
}
|
||||
return calculateMaxDrawdown(equityCurve);
|
||||
}, [backtestStats, equityCurve]);
|
||||
const sharpeRatio = useMemo(() => {
|
||||
if (backtestStats?.sharpe_ratio != null) {
|
||||
return backtestStats.sharpe_ratio;
|
||||
}
|
||||
return calculateSharpeRatio(backtestTrades);
|
||||
}, [backtestStats, backtestTrades]);
|
||||
const largestWin = useMemo(() => backtestTrades.reduce((best, t) => Math.max(best, t.pnl), 0), [backtestTrades]);
|
||||
const largestLoss = useMemo(() => backtestTrades.reduce((worst, t) => Math.min(worst, t.pnl), 0), [backtestTrades]);
|
||||
const grossProfit = backtestStats ? backtestStats.winners * backtestStats.avg_win : 0;
|
||||
@@ -466,7 +491,7 @@ export default function App() {
|
||||
<div className="max-w-[1440px] mx-auto px-6 py-8">
|
||||
|
||||
{/* Header */}
|
||||
<motion.header
|
||||
<Motion.header
|
||||
className="flex justify-between items-center gap-4 mb-8 flex-wrap"
|
||||
variants={itemVariants}
|
||||
initial="hidden"
|
||||
@@ -495,7 +520,7 @@ export default function App() {
|
||||
<div className="w-2 h-2 bg-[#10b981] animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</motion.header>
|
||||
</Motion.header>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mb-8 border-b border-[#262626] pb-4">
|
||||
@@ -521,7 +546,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Dashboard Tab */}
|
||||
<motion.div
|
||||
<Motion.div
|
||||
className="space-y-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
@@ -529,7 +554,7 @@ export default function App() {
|
||||
style={{ display: activeTab === 'dashboard' ? 'block' : 'none' }}
|
||||
>
|
||||
{/* Toolbar */}
|
||||
<motion.div variants={itemVariants} className="p-5 border border-[#262626] bg-[#0a0a0a]">
|
||||
<Motion.div variants={itemVariants} className="p-5 border border-[#262626] bg-[#0a0a0a]">
|
||||
<div className="flex flex-wrap gap-6 items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] text-[#737373] font-mono uppercase tracking-widest">Timeframe</span>
|
||||
@@ -606,31 +631,31 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Candlestick Chart */}
|
||||
<motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Market Chart</p>
|
||||
<h2 className="text-[20px] font-semibold tracking-tight">Candles with structure and trade markers</h2>
|
||||
</div>
|
||||
<div ref={chartContainerRef} className="h-[480px] border border-[#1a1a1a] overflow-hidden" />
|
||||
</motion.section>
|
||||
</Motion.section>
|
||||
|
||||
{/* Equity Line (lightweight-charts) */}
|
||||
{showBacktest && (
|
||||
<motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.section variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-[11px] text-[#737373] font-mono uppercase tracking-widest mb-1">Equity Curve</p>
|
||||
<h2 className="text-[20px] font-semibold tracking-tight">Strategy balance progression</h2>
|
||||
</div>
|
||||
<div ref={equityChartRef} className="h-[180px] border border-[#1a1a1a] overflow-hidden" />
|
||||
</motion.section>
|
||||
</Motion.section>
|
||||
)}
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Stats Tab */}
|
||||
<motion.div
|
||||
<Motion.div
|
||||
className="space-y-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
@@ -638,7 +663,7 @@ export default function App() {
|
||||
style={{ display: activeTab === 'forex-stats' ? 'block' : 'none' }}
|
||||
>
|
||||
{/* Hero */}
|
||||
<motion.section variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-[1.7fr_0.9fr] gap-6 p-8 border border-[#262626] bg-[#0a0a0a]">
|
||||
<Motion.section variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-[1.7fr_0.9fr] gap-6 p-8 border border-[#262626] bg-[#0a0a0a]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-3 flex-wrap text-[13px] font-mono">
|
||||
</div>
|
||||
@@ -656,10 +681,10 @@ export default function App() {
|
||||
</select>
|
||||
<p className="text-[11px] text-[#525252] font-mono">Switch CSVs here to refresh all metrics and charts.</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
</Motion.section>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<motion.section variants={itemVariants} className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<Motion.section variants={itemVariants} className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{overviewMetrics.map((metric) => (
|
||||
<MetricCard
|
||||
key={metric.label}
|
||||
@@ -671,15 +696,15 @@ export default function App() {
|
||||
neutral={metric.neutral}
|
||||
/>
|
||||
))}
|
||||
</motion.section>
|
||||
</Motion.section>
|
||||
|
||||
{/* Equity Curve (recharts) */}
|
||||
<motion.section variants={itemVariants}>
|
||||
<Motion.section variants={itemVariants}>
|
||||
<EquityCurve data={equityCurve} startingBalance={STARTING_BALANCE} />
|
||||
</motion.section>
|
||||
</Motion.section>
|
||||
|
||||
{/* Distribution + Breakdown */}
|
||||
<motion.div variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Motion.div variants={itemVariants} className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<TradeDistribution
|
||||
wins={backtestStats?.winners ?? 0}
|
||||
losses={backtestStats?.losers ?? 0}
|
||||
@@ -695,25 +720,25 @@ export default function App() {
|
||||
maxDrawdown={maxDrawdown}
|
||||
sharpeRatio={sharpeRatio}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Trade History Tab */}
|
||||
<motion.div
|
||||
<Motion.div
|
||||
className="space-y-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={mounted ? 'visible' : 'hidden'}
|
||||
style={{ display: activeTab === 'trade-history' ? 'block' : 'none' }}
|
||||
>
|
||||
<motion.section variants={itemVariants}>
|
||||
<Motion.section variants={itemVariants}>
|
||||
<TradeHistory trades={backtestTrades} />
|
||||
</motion.section>
|
||||
</motion.div>
|
||||
</Motion.section>
|
||||
</Motion.div>
|
||||
|
||||
{/* Backtesting Tab */}
|
||||
{activeTab === 'backtesting' && (
|
||||
<motion.div
|
||||
<Motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={mounted ? 'visible' : 'hidden'}
|
||||
@@ -724,11 +749,11 @@ export default function App() {
|
||||
onDatasetChange={setSelectedDataset}
|
||||
onBacktestComplete={handleBacktestComplete}
|
||||
/>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'optimizer' && (
|
||||
<motion.div
|
||||
<Motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={mounted ? 'visible' : 'hidden'}
|
||||
@@ -738,7 +763,7 @@ export default function App() {
|
||||
selectedDataset={selectedDataset}
|
||||
onDatasetChange={setSelectedDataset}
|
||||
/>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { motion as Motion } from 'motion/react';
|
||||
import {
|
||||
CandlestickSeries,
|
||||
LineSeries,
|
||||
@@ -618,7 +618,7 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||
<div>
|
||||
@@ -817,29 +817,29 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
|
||||
<ToggleInput label="Use Break-Even" value={useBreakEven} onChange={setUseBreakEven} />
|
||||
<ToggleInput label="Use Partial TP" value={usePartialTp} onChange={setUsePartialTp} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Chart */}
|
||||
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Backtest Chart</p>
|
||||
<h2 className="text-[20px] font-semibold tracking-tight">Trade entries and exits</h2>
|
||||
</div>
|
||||
<div ref={chartContainerRef} className="h-[420px] border border-[#1a1a1a] overflow-hidden" />
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Equity */}
|
||||
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Equity Curve</p>
|
||||
<h2 className="text-[20px] font-semibold tracking-tight">Balance progression</h2>
|
||||
</div>
|
||||
<div ref={equityChartRef} className="h-[160px] border border-[#1a1a1a] overflow-hidden" />
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
|
||||
{/* Results */}
|
||||
{stats && (
|
||||
<motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<Motion.div variants={itemVariants} className="border border-[#262626] bg-[#0a0a0a] p-6">
|
||||
<div className="mb-6">
|
||||
<p className="text-[11px] text-[#525252] font-mono uppercase tracking-widest mb-1">Results</p>
|
||||
<h2 className="text-[20px] font-semibold tracking-tight">Backtest Summary</h2>
|
||||
@@ -992,8 +992,10 @@ export function BacktestingTab({ datasets = [], selectedDataset, onDatasetChange
|
||||
<p className={`text-[24px] font-semibold ${partialTpRealized >= 0 ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>${formatMoney(partialTpRealized)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
import { motion, useInView } from 'motion/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion as Motion } from 'motion/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function MetricCard({ label, value, change, isPositive, isPrimary = false, neutral = false }) {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.3 });
|
||||
const [displayValue, setDisplayValue] = useState('0');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
const displayValue = useMemo(() => {
|
||||
const numericValue = parseFloat(value.replace(/[^0-9.-]/g, ''));
|
||||
if (isNaN(numericValue)) {
|
||||
setDisplayValue(value);
|
||||
return;
|
||||
if (isNaN(numericValue)) return value;
|
||||
if (value.includes('$')) {
|
||||
return `$${numericValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
const duration = 1200;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
const progress = Math.min((Date.now() - startTime) / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const current = numericValue * eased;
|
||||
|
||||
if (value.includes('$')) {
|
||||
setDisplayValue(`$${current.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
} else if (value.includes('%')) {
|
||||
setDisplayValue(`${current.toFixed(1)}%`);
|
||||
} else {
|
||||
setDisplayValue(current % 1 === 0 ? Math.round(current).toString() : current.toFixed(2));
|
||||
}
|
||||
|
||||
if (progress < 1) requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}, [isInView, value]);
|
||||
if (value.includes('%')) {
|
||||
return `${numericValue.toFixed(1)}%`;
|
||||
}
|
||||
return numericValue % 1 === 0 ? Math.round(numericValue).toString() : numericValue.toFixed(2);
|
||||
}, [value]);
|
||||
|
||||
const color = neutral ? 'text-[#fafafa]' : isPositive ? 'text-[#10b981]' : 'text-[#ef4444]';
|
||||
const changeLabel = typeof change === 'number' ? `${change >= 0 ? '+' : ''}${change.toFixed(1)}%` : change;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
<Motion.div
|
||||
className={isPrimary ? 'col-span-2 md:col-span-1' : ''}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
@@ -76,6 +52,6 @@ export function MetricCard({ label, value, change, isPositive, isPrimary = false
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ function CustomTooltip({ active, payload, total }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function TradeDistribution({ wins = 0, losses = 0, avgWin = 0, avgLoss = 0, largestWin = 0, largestLoss = 0 }) {
|
||||
export function TradeDistribution({ wins = 0, losses = 0, avgWin = 0, avgLoss = 0 }) {
|
||||
const total = wins + losses;
|
||||
const data = [
|
||||
{ name: 'Wins', value: wins },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { motion as Motion } from 'motion/react';
|
||||
|
||||
function formatCurrency(value) {
|
||||
const abs = Math.abs(value);
|
||||
@@ -104,7 +104,6 @@ export function TradeHistory({ trades = [] }) {
|
||||
pageSlice.map((trade, index) => {
|
||||
const globalIndex = (safeCurrentPage - 1) * ROWS_PER_PAGE + index;
|
||||
const isWin = trade.pnl > 0;
|
||||
const enterDate = new Date(trade.enter_time);
|
||||
const exitDate = new Date(trade.exit_time);
|
||||
const dateStr = exitDate.toLocaleDateString('en-CA');
|
||||
const timeStr = exitDate.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
@@ -112,7 +111,7 @@ export function TradeHistory({ trades = [] }) {
|
||||
const direction = trade.direction === 'long' ? 'BUY' : 'SELL';
|
||||
|
||||
return (
|
||||
<motion.tr
|
||||
<Motion.tr
|
||||
key={`${trade.enter_time}-${index}`}
|
||||
className="border-b border-[#1a1a1a] hover:bg-[#111111] transition-colors"
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -142,7 +141,7 @@ export function TradeHistory({ trades = [] }) {
|
||||
Closed
|
||||
</span>
|
||||
</td>
|
||||
</motion.tr>
|
||||
</Motion.tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user