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,71 @@
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
export interface PlanBiasSelectorProps {
bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
isEditing: boolean;
onChange: (bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL') => void;
}
export function PlanBiasSelector({ bias, isEditing, onChange }: PlanBiasSelectorProps): JSX.Element {
const biasOptions: Array<{ value: 'BULLISH' | 'BEARISH' | 'NEUTRAL'; label: string; icon: JSX.Element; color: 'green' | 'slate' | 'red' }> = [
{ value: 'BULLISH', label: 'Bullish', icon: <TrendingUp className="h-5 w-5" aria-hidden="true" />, color: 'green' },
{ value: 'NEUTRAL', label: 'Neutral', icon: <Minus className="h-5 w-5" aria-hidden="true" />, color: 'slate' },
{ value: 'BEARISH', label: 'Bearish', icon: <TrendingDown className="h-5 w-5" aria-hidden="true" />, color: 'red' },
];
const selectedOption = biasOptions.find((opt) => opt.value === bias);
if (!isEditing && selectedOption) {
const colorClasses = {
green: 'border-green-500/50 bg-green-500/10 text-green-300',
slate: 'border-slate-500/50 bg-slate-500/10 text-slate-300',
red: 'border-red-500/50 bg-red-500/10 text-red-300',
};
return (
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Market Bias</label>
<div className={`inline-flex items-center gap-2 rounded-lg border px-4 py-2 ${colorClasses[selectedOption.color]}`}>
{selectedOption.icon}
<span className="font-semibold">{selectedOption.label}</span>
</div>
</div>
);
}
return (
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Market Bias</label>
<div className="flex gap-2">
{biasOptions.map((option) => {
const isSelected = bias === option.value;
const baseClasses = 'flex-1 inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2 font-medium transition-colors cursor-pointer';
const colorClasses = {
green: isSelected
? 'border-green-500 bg-green-500/20 text-green-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-green-500/50 hover:text-green-400',
slate: isSelected
? 'border-slate-500 bg-slate-500/20 text-slate-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-500/50 hover:text-slate-300',
red: isSelected
? 'border-red-500 bg-red-500/20 text-red-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-red-500/50 hover:text-red-400',
};
return (
<button
key={option.value}
onClick={() => onChange(option.value)}
className={`${baseClasses} ${colorClasses[option.color]}`}
aria-pressed={isSelected}
>
{option.icon}
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,77 @@
import { CalendarDays, Edit2, Save, Sparkles, RefreshCw } from 'lucide-react';
export interface PlanHeaderProps {
planDate: string;
isEditing: boolean;
generating: boolean;
onEdit: () => void;
onSave: () => void;
onGenerateAI: () => void;
onReset: () => void;
}
export function PlanHeader({
planDate,
isEditing,
generating,
onEdit,
onSave,
onGenerateAI,
onReset,
}: PlanHeaderProps): JSX.Element {
return (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500/20 text-blue-400">
<CalendarDays className="h-6 w-6" aria-hidden="true" />
</div>
<div>
<h3 className="text-lg font-semibold text-white">Daily Trading Plan</h3>
<p className="text-sm text-slate-400">{planDate}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={onGenerateAI}
disabled={generating}
className="inline-flex items-center gap-2 rounded-lg border border-purple-500/50 bg-purple-500/10 px-4 py-2 text-sm font-medium text-purple-300 hover:bg-purple-500/20 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
aria-label="Generate AI plan"
>
<Sparkles className="h-4 w-4" aria-hidden="true" />
{generating ? 'Generating...' : 'AI Plan'}
</button>
<button
onClick={onReset}
disabled={generating}
className="inline-flex items-center gap-2 rounded-lg border border-slate-600 bg-slate-800 px-4 py-2 text-sm font-medium text-slate-300 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
aria-label="Reset plan"
>
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Reset
</button>
{isEditing ? (
<button
onClick={onSave}
className="inline-flex items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 transition-colors"
aria-label="Save changes"
>
<Save className="h-4 w-4" aria-hidden="true" />
Save
</button>
) : (
<button
onClick={onEdit}
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
aria-label="Edit plan"
>
<Edit2 className="h-4 w-4" aria-hidden="true" />
Edit
</button>
)}
</div>
</div>
);
}
@@ -0,0 +1,125 @@
import { Plus, X } from 'lucide-react';
import { formatCurrency } from '@/utils/indicators';
export interface PlanKeyLevelsEditorProps {
support: number[];
resistance: number[];
isEditing: boolean;
onAddSupport: () => void;
onAddResistance: () => void;
onRemoveSupport: (index: number) => void;
onRemoveResistance: (index: number) => void;
onUpdateSupport: (index: number, value: number) => void;
onUpdateResistance: (index: number, value: number) => void;
}
export function PlanKeyLevelsEditor({
support,
resistance,
isEditing,
onAddSupport,
onAddResistance,
onRemoveSupport,
onRemoveResistance,
onUpdateSupport,
onUpdateResistance,
}: PlanKeyLevelsEditorProps): JSX.Element {
const renderLevelList = (
title: string,
levels: number[],
color: 'green' | 'red',
onAdd: () => void,
onRemove: (index: number) => void,
onUpdate: (index: number, value: number) => void
) => {
const colorClasses = {
green: {
badge: 'bg-green-500/20 text-green-300 border-green-500/30',
button: 'text-green-400 hover:text-green-300',
input: 'border-green-500/30 focus:border-green-500',
},
red: {
badge: 'bg-red-500/20 text-red-300 border-red-500/30',
button: 'text-red-400 hover:text-red-300',
input: 'border-red-500/30 focus:border-red-500',
},
};
const classes = colorClasses[color];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-slate-300">{title}</h5>
{isEditing && (
<button
onClick={onAdd}
className={`inline-flex items-center gap-1 text-sm ${classes.button}`}
aria-label={`Add ${title.toLowerCase()} level`}
>
<Plus className="h-4 w-4" aria-hidden="true" />
Add
</button>
)}
</div>
<div className="space-y-2">
{levels.length === 0 ? (
<p className="text-sm text-slate-500 italic">No levels defined</p>
) : (
levels.map((level, index) => (
<div key={`${level}-${index}`} className="flex items-center gap-2">
{isEditing ? (
<>
<input
type="number"
value={level}
onChange={(e) => onUpdate(index, parseFloat(e.target.value) || 0)}
step="0.01"
className={`flex-1 rounded-lg border bg-slate-800 px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-offset-0 ${classes.input}`}
/>
<button
onClick={() => onRemove(index)}
className="p-2 text-slate-400 hover:text-red-400 transition-colors"
aria-label={`Remove ${title.toLowerCase()} level`}
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
</>
) : (
<div className={`flex-1 rounded-lg border px-3 py-2 ${classes.badge}`}>
{formatCurrency(level)}
</div>
)}
</div>
))
)}
</div>
</div>
);
};
return (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-slate-300">Key Levels</h4>
<div className="grid gap-6 sm:grid-cols-2">
{renderLevelList(
'Support Levels',
support,
'green',
onAddSupport,
onRemoveSupport,
onUpdateSupport
)}
{renderLevelList(
'Resistance Levels',
resistance,
'red',
onAddResistance,
onRemoveResistance,
onUpdateResistance
)}
</div>
</div>
);
}
@@ -0,0 +1,127 @@
import { DollarSign, Target, AlertTriangle } from 'lucide-react';
export interface PlanRiskParametersProps {
dailyTarget: number;
maxLoss: number;
maxTrades: number;
entryZoneMin: number;
entryZoneMax: number;
targetPrice: number;
stopLoss: number;
isEditing: boolean;
onChange: (field: string, value: number) => void;
}
export function PlanRiskParameters({
dailyTarget,
maxLoss,
maxTrades,
entryZoneMin,
entryZoneMax,
targetPrice,
stopLoss,
isEditing,
onChange,
}: PlanRiskParametersProps): JSX.Element {
const renderField = (
label: string,
value: number,
field: string,
icon: JSX.Element,
prefix: string = '$',
step: number = 1
) => {
if (!isEditing) {
return (
<div className="flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-800/50 p-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-700 text-blue-400">
{icon}
</div>
<div className="flex-1">
<p className="text-xs text-slate-400">{label}</p>
<p className="text-lg font-semibold text-white">
{prefix}{value.toFixed(2)}
</p>
</div>
</div>
);
}
return (
<div className="space-y-2">
<label htmlFor={field} className="text-sm font-medium text-slate-400">
{label}
</label>
<div className="flex items-center gap-2">
<span className="text-slate-400">{prefix}</span>
<input
id={field}
type="number"
value={value}
onChange={(e) => onChange(field, parseFloat(e.target.value) || 0)}
step={step}
className="flex-1 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"
/>
</div>
</div>
);
};
return (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-slate-300">Risk Parameters</h4>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{renderField(
'Daily Target',
dailyTarget,
'dailyTarget',
<Target className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Max Loss',
maxLoss,
'maxLoss',
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Max Trades',
maxTrades,
'maxTrades',
<DollarSign className="h-5 w-5" aria-hidden="true" />,
'',
1
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{renderField(
'Entry Zone Min',
entryZoneMin,
'entryZoneMin',
<DollarSign className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Entry Zone Max',
entryZoneMax,
'entryZoneMax',
<DollarSign className="h-5 w-5" aria-hidden="true" />
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{renderField(
'Target Price',
targetPrice,
'targetPrice',
<Target className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Stop Loss',
stopLoss,
'stopLoss',
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
)}
</div>
</div>
);
}
@@ -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 };
@@ -0,0 +1,41 @@
import type { PositionMetrics } from '@/services/api';
import type { StrategyMode } from '@/components/StrategyModeSelector';
import type { SwingPosition } from '@/components/MultiDayPositionTracker';
import type { NewsEvent } from '@/components/NewsEventTracker';
import type { Trade as AdvancedMetricsTrade } from '@/components/AdvancedMetricsDashboard';
import type { TradeDatasetSource } from '@/hooks/useAdvancedTradeMetrics';
export interface TradingPlan {
date: string;
bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
strategyMode: StrategyMode;
dailyTarget: number;
maxLoss: number;
entryZone: { min: number; max: number };
targetPrice: number;
stopLoss: number;
keyLevels: {
support: number[];
resistance: number[];
};
tradingNotes: string;
maxTrades: number;
actualTrades: number;
actualPnL: number;
planFollowed: boolean;
contextMetrics: PositionMetrics | null;
// Phase 3: Swing Trading Features
swingPositions?: SwingPosition[];
newsEvents?: NewsEvent[];
trendConfirmed?: boolean;
}
export interface DailyTradingPlanProps {
currentPrice: number;
onPlanUpdate?: (plan: TradingPlan) => void;
openRouterReady?: boolean;
openRouterMessage?: string;
advancedTrades?: AdvancedMetricsTrade[];
advancedTradesSource?: TradeDatasetSource;
}
@@ -0,0 +1,98 @@
import { useState, useCallback } from 'react';
import { aiApi } from '@/services/api';
import type { TradingPlan } from './types';
export interface UsePlanGenerationReturn {
generating: boolean;
error: string | null;
generatePlan: (currentPrice: number, currentPlan: TradingPlan) => Promise<TradingPlan | null>;
clearError: () => void;
}
/**
* Hook for AI-powered trading plan generation
*/
export function usePlanGeneration(
openRouterReady?: boolean
): UsePlanGenerationReturn {
const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const clearError = useCallback(() => {
setError(null);
}, []);
const generatePlan = useCallback(
async (currentPrice: number, currentPlan: TradingPlan): Promise<TradingPlan | null> => {
if (openRouterReady === false) {
setError(
'AI plan generation requires an OpenRouter API key. Please set OPENROUTER_API_KEY on the backend and restart.'
);
return null;
}
setGenerating(true);
setError(null);
try {
const aiPlanResponse = await aiApi.generateTradingPlan({
symbol: 'XAUUSD',
timeframe: '1m',
limit: 400,
current_price: currentPrice,
user_capital: 100000,
risk_tolerance: 'MODERATE',
use_indicator_preferences: true,
});
if (!aiPlanResponse) {
throw new Error('No response from AI plan generation API');
}
const generatedPlan: TradingPlan = {
...currentPlan,
date: new Date().toDateString(),
bias: aiPlanResponse.market_bias || 'NEUTRAL',
dailyTarget: aiPlanResponse.daily_target ?? currentPlan.dailyTarget,
maxLoss: aiPlanResponse.max_loss ?? currentPlan.maxLoss,
entryZone: {
min: aiPlanResponse.entry_zone_min ?? currentPrice - 10,
max: aiPlanResponse.entry_zone_max ?? currentPrice + 10,
},
targetPrice: aiPlanResponse.target_price ?? currentPrice + 20,
stopLoss: aiPlanResponse.stop_loss ?? currentPrice - 15,
keyLevels: {
support: aiPlanResponse.support_levels?.slice(0, 3) || currentPlan.keyLevels.support,
resistance:
aiPlanResponse.resistance_levels?.slice(0, 3) || currentPlan.keyLevels.resistance,
},
tradingNotes: aiPlanResponse.trading_notes || aiPlanResponse.reasoning || '',
maxTrades: aiPlanResponse.max_trades ?? currentPlan.maxTrades,
contextMetrics: aiPlanResponse.context_metrics || currentPlan.contextMetrics,
};
return generatedPlan;
} catch (err) {
if (err instanceof Error && err.message.includes('OPENROUTER_API_KEY')) {
setError(
'AI plan generation requires an OpenRouter API key. Set OPENROUTER_API_KEY on the backend and restart the server.'
);
} else {
const message = err instanceof Error ? err.message : 'Failed to generate AI plan';
setError(message);
}
return null;
} finally {
setGenerating(false);
}
},
[openRouterReady]
);
return {
generating,
error,
generatePlan,
clearError,
};
}