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:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user