feat: Add Phase 4 advanced metrics and components

- Add advanced metrics dashboard with trade analytics
- Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.)
- Add strategy mode selector and trend confirmation
- Add risk automation panel and slippage correlation analysis
- Add daily trading plan enhancements with modal components
- Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics)
- Add broker service integration and trading API
- Add test setup and vitest configuration
- Include parquet data files for live market data
- Add comprehensive documentation in docs/ folder
This commit is contained in:
Krikorios
2025-11-27 10:23:58 +02:00
parent b5e2b02cb8
commit 48e60d015f
2019 changed files with 39793 additions and 257 deletions
@@ -0,0 +1,580 @@
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useLocalStorage } from '@/hooks';
import { AlertModal, ConfirmModal } from '@/components/shared/Modal';
import { PlanHeader } from './PlanHeader';
import { PlanBiasSelector } from './PlanBiasSelector';
import { PlanRiskParameters } from './PlanRiskParameters';
import { PlanKeyLevelsEditor } from './PlanKeyLevelsEditor';
import { usePlanGeneration } from './usePlanGeneration';
import StrategyModeSelector from '@/components/StrategyModeSelector';
import TrendConfirmation from '@/components/TrendConfirmation';
import MultiDayPositionTracker from '@/components/MultiDayPositionTracker';
import NewsEventTracker from '@/components/NewsEventTracker';
import type { TradingPlan, DailyTradingPlanProps } from './types';
import type { StrategyMode } from '@/components/StrategyModeSelector';
import { STRATEGY_PRESETS } from '@/components/StrategyModeSelector';
import AdvancedMetricsDashboard, { type Trade as AdvancedMetricsTrade, type VolatilityBucket } from '@/components/AdvancedMetricsDashboard';
const createDefaultPlan = (currentPrice: number, strategyMode: StrategyMode = 'SWING'): TradingPlan => {
const preset = STRATEGY_PRESETS[strategyMode];
const riskAmount = 10000 * (preset.riskPerTrade / 100); // Assume $10k account
const stopLossDiff = currentPrice * (preset.stopLossPercent / 100);
const takeProfitDiff = currentPrice * (preset.takeProfitPercent / 100);
return {
date: new Date().toDateString(),
strategyMode,
bias: 'NEUTRAL',
dailyTarget: Math.round(riskAmount * 2), // 2x risk as daily target
maxLoss: Math.round(riskAmount),
entryZone: {
min: currentPrice - (currentPrice * (preset.stopLossPercent / 200)),
max: currentPrice + (currentPrice * (preset.stopLossPercent / 200))
},
targetPrice: currentPrice + takeProfitDiff,
stopLoss: currentPrice - stopLossDiff,
keyLevels: {
support: [
currentPrice - (currentPrice * (preset.stopLossPercent / 50)),
currentPrice - (currentPrice * (preset.stopLossPercent / 25))
],
resistance: [
currentPrice + (currentPrice * (preset.takeProfitPercent / 50)),
currentPrice + (currentPrice * (preset.takeProfitPercent / 25))
],
},
tradingNotes: '',
maxTrades: preset.maxDailyTrades,
actualTrades: 0,
actualPnL: 0,
planFollowed: true,
contextMetrics: null,
};
};
const SIGNAL_LABELS: Record<AdvancedMetricsTrade['signalType'], string> = {
RSI_CROSSOVER: 'RSI Crossover',
MA_CROSSOVER: 'MA Crossover',
BB_BREAKOUT: 'Bollinger Breakout',
MACD: 'MACD Signal',
SUPPORT_BOUNCE: 'Support Bounce',
TREND_CONFIRMATION: 'Trend Confirmation',
NEWS_TRIGGERED: 'News Triggered',
};
export default function DailyTradingPlan({
currentPrice,
onPlanUpdate,
openRouterReady,
openRouterMessage,
advancedTrades = [],
advancedTradesSource = 'sample',
}: DailyTradingPlanProps): JSX.Element {
const [isEditing, setIsEditing] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [showAdvancedMetrics, setShowAdvancedMetrics] = useState(false);
const [analyticsFocus, setAnalyticsFocus] = useState<{
timeframe: string | null;
signalType: AdvancedMetricsTrade['signalType'] | null;
volatility: string | null;
}>({ timeframe: null, signalType: null, volatility: null });
const safeAdvancedTrades = advancedTrades ?? [];
const advancedAnalyticsSummary = useMemo(() => {
if (!safeAdvancedTrades.length) return null;
const totalTrades = safeAdvancedTrades.length;
const wins = safeAdvancedTrades.filter((trade) => trade.profitable).length;
const winRate = (wins / totalTrades) * 100;
const timeframePnL = safeAdvancedTrades.reduce<Record<string, number>>((acc, trade) => {
acc[trade.timeframe] = (acc[trade.timeframe] ?? 0) + trade.pnl;
return acc;
}, {});
const bestTimeframeEntry = Object.entries(timeframePnL).sort((a, b) => b[1] - a[1])[0];
const signalPnL = safeAdvancedTrades.reduce<Record<string, number>>((acc, trade) => {
acc[trade.signalType] = (acc[trade.signalType] ?? 0) + trade.pnl;
return acc;
}, {});
const bestSignalEntry = Object.entries(signalPnL).sort((a, b) => b[1] - a[1])[0];
const slippageImpact = safeAdvancedTrades.reduce((sum, trade) => {
const gross = trade.grossPnL ?? trade.pnl;
if (!gross) return sum;
const impact = trade.slippage ? (trade.slippage / Math.abs(gross)) * 100 : 0;
return sum + impact;
}, 0) / totalTrades;
return {
totalTrades,
winRate,
bestTimeframe: bestTimeframeEntry?.[0] ?? null,
bestSignal: (bestSignalEntry?.[0] as AdvancedMetricsTrade['signalType']) ?? null,
slippageImpact,
};
}, [safeAdvancedTrades]);
const [plan, setPlan] = useLocalStorage<TradingPlan>(
'daily-trading-plan',
createDefaultPlan(currentPrice)
);
const { generating, error, generatePlan } = usePlanGeneration(openRouterReady);
// Ensure plan resets when the stored date is stale (runs after render to avoid blocking updates)
useEffect(() => {
const today = new Date().toDateString();
if (plan.date !== today) {
setPlan((prev) => {
const next = createDefaultPlan(currentPrice, prev.strategyMode);
return {
...next,
bias: prev.bias,
tradingNotes: prev.tradingNotes,
};
});
}
}, [plan.date, currentPrice, setPlan]);
const handleEdit = useCallback(() => {
setIsEditing(true);
}, []);
const handleSave = useCallback(() => {
setIsEditing(false);
if (onPlanUpdate) {
onPlanUpdate(plan);
}
}, [plan, onPlanUpdate]);
const handleConfirmReset = useCallback(() => {
setPlan(createDefaultPlan(currentPrice));
setIsEditing(false);
setShowResetConfirm(false);
}, [currentPrice, setPlan]);
const handleGenerateAI = useCallback(async () => {
const generatedPlan = await generatePlan(currentPrice, plan);
if (generatedPlan) {
setPlan(generatedPlan);
setSuccessMessage(
`AI Plan Generated!\n\nBias: ${generatedPlan.bias}\nTarget: $${generatedPlan.dailyTarget}\n\nReview and edit the plan as needed.`
);
setShowSuccessAlert(true);
if (onPlanUpdate) {
onPlanUpdate(generatedPlan);
}
}
}, [currentPrice, plan, generatePlan, setPlan, onPlanUpdate]);
const handleFieldChange = useCallback(
(field: string, value: number) => {
setPlan((prev) => {
if (field === 'entryZoneMin') {
return { ...prev, entryZone: { ...prev.entryZone, min: value } };
}
if (field === 'entryZoneMax') {
return { ...prev, entryZone: { ...prev.entryZone, max: value } };
}
return { ...prev, [field]: value };
});
},
[setPlan]
);
const handleBiasChange = useCallback(
(bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL') => {
setPlan((prev) => ({ ...prev, bias }));
},
[setPlan]
);
const handleNotesChange = useCallback(
(notes: string) => {
setPlan((prev) => ({ ...prev, tradingNotes: notes }));
},
[setPlan]
);
const handleAdvancedTimeframeSelect = useCallback((timeframe: string) => {
setAnalyticsFocus((prev) => ({
...prev,
timeframe: prev.timeframe === timeframe ? null : timeframe,
}));
}, []);
const handleAdvancedSignalSelect = useCallback((signalType: string) => {
const typedSignal = signalType as AdvancedMetricsTrade['signalType'];
setAnalyticsFocus((prev) => ({
...prev,
signalType: prev.signalType === typedSignal ? null : typedSignal,
}));
}, []);
const handleAdvancedVolatilitySelect = useCallback((bucket: VolatilityBucket) => {
setAnalyticsFocus((prev) => ({
...prev,
volatility: prev.volatility === bucket.range ? null : bucket.range,
}));
}, []);
const handleStrategyModeChange = useCallback(
(mode: StrategyMode) => {
const newPlan = createDefaultPlan(currentPrice, mode);
setPlan((prev) => ({
...newPlan,
bias: prev.bias,
tradingNotes: prev.tradingNotes,
}));
},
[currentPrice, setPlan]
);
// Key levels handlers
const handleAddSupport = useCallback(() => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: [...prev.keyLevels.support, currentPrice - 10],
},
}));
}, [currentPrice, setPlan]);
const handleAddResistance = useCallback(() => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: [...prev.keyLevels.resistance, currentPrice + 10],
},
}));
}, [currentPrice, setPlan]);
const handleRemoveSupport = useCallback(
(index: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.filter((_, i) => i !== index),
},
}));
},
[setPlan]
);
const handleRemoveResistance = useCallback(
(index: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.filter((_, i) => i !== index),
},
}));
},
[setPlan]
);
const handleUpdateSupport = useCallback(
(index: number, value: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.map((level, i) => (i === index ? value : level)),
},
}));
},
[setPlan]
);
const handleUpdateResistance = useCallback(
(index: number, value: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.map((level, i) => (i === index ? value : level)),
},
}));
},
[setPlan]
);
return (
<div className="space-y-6 rounded-2xl border border-slate-700 bg-slate-900 p-6">
<PlanHeader
planDate={plan.date}
isEditing={isEditing}
generating={generating}
onEdit={handleEdit}
onSave={handleSave}
onGenerateAI={handleGenerateAI}
onReset={() => setShowResetConfirm(true)}
/>
{/* Strategy Mode Info Banner */}
<div className="rounded-lg border border-blue-500/30 bg-blue-500/10 px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{STRATEGY_PRESETS[plan.strategyMode].emoji}</span>
<div>
<p className="font-semibold text-blue-300">
{plan.strategyMode} Mode Active
</p>
<p className="text-xs text-blue-200">
Max {STRATEGY_PRESETS[plan.strategyMode].maxDailyTrades} trades
R:R 1:{STRATEGY_PRESETS[plan.strategyMode].r2rRatio.toFixed(1)}
Stop: {STRATEGY_PRESETS[plan.strategyMode].stopLossPercent}%
</p>
</div>
</div>
</div>
</div>
{(error || (openRouterReady === false && openRouterMessage)) && (
<div
className={`rounded-lg border px-4 py-3 ${
openRouterReady === false
? 'border-amber-500/60 bg-amber-500/10 text-amber-100'
: 'border-red-500/60 bg-red-500/10 text-red-100'
}`}
role="alert"
>
<p className="text-sm">{error || openRouterMessage}</p>
</div>
)}
<PlanBiasSelector bias={plan.bias} isEditing={isEditing} onChange={handleBiasChange} />
<div className="hidden sm:block">
<StrategyModeSelector
defaultMode={plan.strategyMode}
onModeChange={handleStrategyModeChange}
variant="compact"
/>
</div>
<div className="sm:hidden">
<StrategyModeSelector
defaultMode={plan.strategyMode}
onModeChange={handleStrategyModeChange}
variant="full"
/>
</div>
<PlanRiskParameters
dailyTarget={plan.dailyTarget}
maxLoss={plan.maxLoss}
maxTrades={plan.maxTrades}
entryZoneMin={plan.entryZone.min}
entryZoneMax={plan.entryZone.max}
targetPrice={plan.targetPrice}
stopLoss={plan.stopLoss}
isEditing={isEditing}
onChange={handleFieldChange}
/>
<PlanKeyLevelsEditor
support={plan.keyLevels.support}
resistance={plan.keyLevels.resistance}
isEditing={isEditing}
onAddSupport={handleAddSupport}
onAddResistance={handleAddResistance}
onRemoveSupport={handleRemoveSupport}
onRemoveResistance={handleRemoveResistance}
onUpdateSupport={handleUpdateSupport}
onUpdateResistance={handleUpdateResistance}
/>
{/* Phase 3: Swing Trading Features - Show for SWING and HYBRID modes */}
{(plan.strategyMode === 'SWING' || plan.strategyMode === 'HYBRID') && (
<div className="space-y-6">
{/* Trend Confirmation for Swing Entry */}
<TrendConfirmation
ema8={2035.50}
ema21={2033.20}
ema55={2031.80}
ema200={2030.00}
macdLine={0.45}
macdSignal={0.32}
rsi={58.5}
timeframe={plan.strategyMode === 'SWING' ? '4h' : '1h'}
onTrendUpdate={(trend) => {
setPlan(prev => ({ ...prev, trendConfirmed: trend.strength !== 'WEAK' }));
}}
/>
{/* Multi-Day Position Tracker */}
{plan.swingPositions && plan.swingPositions.length > 0 && (
<MultiDayPositionTracker
positions={plan.swingPositions}
onMetricsUpdate={(metrics) => {
console.log('Position metrics updated:', metrics);
}}
/>
)}
{/* News Event Monitor */}
{plan.newsEvents && plan.newsEvents.length > 0 && (
<NewsEventTracker
events={plan.newsEvents}
onEventAlert={(event) => {
console.log('News event alert:', event.title);
}}
/>
)}
</div>
)}
{/* Phase 4 Advanced Metrics Snapshot */}
<div className="space-y-4 rounded-2xl border border-emerald-500/30 bg-emerald-500/5 p-4">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-emerald-300">Phase 4 · Execution intel</p>
<p className="text-lg font-semibold text-white">Advanced Metrics Summary</p>
<p className="text-xs text-emerald-200">
{advancedTradesSource === 'live'
? 'Powered by your closed trades.'
: 'Showing curated sample data until you close a trade.'}
</p>
</div>
<div className="text-right text-sm text-emerald-200">
<p>{advancedAnalyticsSummary ? `${advancedAnalyticsSummary.totalTrades} trades` : 'No trades yet'}</p>
<p className="font-semibold text-emerald-300">
{advancedAnalyticsSummary ? `${advancedAnalyticsSummary.winRate.toFixed(1)}% win rate` : 'Run a session to unlock insights'}
</p>
</div>
</div>
{advancedAnalyticsSummary ? (
<>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Best timeframe</p>
<p className="text-lg font-semibold text-white">
{advancedAnalyticsSummary.bestTimeframe ?? '—'}
</p>
<p className="text-xs text-emerald-200/70">Highest net P&L</p>
</div>
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Best signal</p>
<p className="text-lg font-semibold text-white">
{advancedAnalyticsSummary.bestSignal
? SIGNAL_LABELS[advancedAnalyticsSummary.bestSignal]
: '—'}
</p>
<p className="text-xs text-emerald-200/70">Highest risk / reward</p>
</div>
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Avg slippage impact</p>
<p className="text-lg font-semibold text-white">
{Number.isFinite(advancedAnalyticsSummary.slippageImpact)
? `${advancedAnalyticsSummary.slippageImpact.toFixed(1)}%`
: '—'}
</p>
<p className="text-xs text-emerald-200/70">Cost of fills vs. gross</p>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2 text-xs text-emerald-200">
{analyticsFocus.timeframe && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Timeframe: {analyticsFocus.timeframe}
</span>
)}
{analyticsFocus.signalType && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Signal: {SIGNAL_LABELS[analyticsFocus.signalType]}
</span>
)}
{analyticsFocus.volatility && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Volatility: {analyticsFocus.volatility}
</span>
)}
{!analyticsFocus.timeframe && !analyticsFocus.signalType && !analyticsFocus.volatility && (
<span className="text-emerald-200/70">No filter focus selected</span>
)}
</div>
<button
type="button"
onClick={() => setShowAdvancedMetrics((prev) => !prev)}
className="inline-flex items-center gap-2 rounded-full border border-emerald-400/60 px-4 py-1 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/10"
>
{showAdvancedMetrics ? 'Hide dashboard' : 'Open dashboard'}
</button>
</div>
{showAdvancedMetrics && (
<div className="rounded-2xl border border-slate-700 bg-slate-950/40 p-2">
<AdvancedMetricsDashboard
trades={safeAdvancedTrades}
onTimeframeSelect={handleAdvancedTimeframeSelect}
onSignalTypeSelect={handleAdvancedSignalSelect}
onVolatilityRangeSelect={handleAdvancedVolatilitySelect}
/>
</div>
)}
</>
) : (
<div className="rounded-xl border border-dashed border-emerald-400/40 bg-slate-900/40 p-4 text-sm text-emerald-200">
Close at least one trade to unlock the Advanced Metrics Dashboard summary.
</div>
)}
</div>
{/* Trading Notes */}
<div className="space-y-2">
<label htmlFor="trading-notes" className="text-sm font-medium text-slate-400">
Trading Notes
</label>
{isEditing ? (
<textarea
id="trading-notes"
value={plan.tradingNotes}
onChange={(e) => handleNotesChange(e.target.value)}
rows={4}
className="w-full rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
placeholder="Enter your trading notes, observations, and strategies..."
/>
) : (
<div className="rounded-lg border border-slate-700 bg-slate-800/50 px-4 py-3 text-slate-300">
{plan.tradingNotes || <span className="italic text-slate-500">No notes</span>}
</div>
)}
</div>
{/* Modals */}
<ConfirmModal
isOpen={showResetConfirm}
onClose={() => setShowResetConfirm(false)}
onConfirm={handleConfirmReset}
title="Reset Plan"
message="Create a new plan for today? This will clear the current plan."
variant="warning"
/>
<AlertModal
isOpen={showSuccessAlert}
onClose={() => setShowSuccessAlert(false)}
title="Success"
message={successMessage}
variant="success"
/>
</div>
);
}
// Re-export types
export type { TradingPlan, DailyTradingPlanProps };