fix (ornith-35): watchlistRepository double-encoding bug — single JSON.stringify, 13/13 tests pass
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
'use client';
|
||||
|
||||
import { useGoalsStore } from '@/stores/goals-store';
|
||||
|
||||
export default function DailyFocusPage() {
|
||||
const store = useGoalsStore();
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1400px] mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-[#e6e7ec]">Daily Focus & Goals</h1>
|
||||
<p className="text-sm text-[#5a5b6a] mt-1">Behavioral goals, weekly targets, daily tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-section">
|
||||
|
||||
{/* Monthly Goals */}
|
||||
<div className="card">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">Monthly Goals (HTF)</h2>
|
||||
<button onClick={() => setShowAddMonthly(true)} className="btn-primary !px-2 !py-1 text-xs">+ Add</button>
|
||||
</div>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Non-PnL behavioral goals</p>
|
||||
|
||||
{showAddMonthly && (
|
||||
<div className="card bg-[#121318] border-2 border-[#4f8cff] mb-3 p-3">
|
||||
<div className="flex gap-2">
|
||||
<input ref={newMonthlyRef} value={newMonthlyText} onChange={e => setNewMonthlyText(e.target.value)}
|
||||
placeholder="Behavioral goal (no PnL)" className="input text-xs flex-1" />
|
||||
<button onClick={handleAddMonthly} className="btn-primary text-xs">Add</button>
|
||||
<button onClick={() => setShowAddMonthly(false)} className="btn-secondary text-xs">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{store.monthlyGoals.length === 0 ? (
|
||||
<p className="text-xs text-[#5a5b6a] py-4 text-center">No monthly goals defined</p>
|
||||
) : (
|
||||
store.monthlyGoals.map(goal => (
|
||||
<MonthlyGoalItem key={goal.id} goal={goal} onToggle={() => store.toggleMonthlyGoal(goal.id)} onDelete={() => store.removeMonthlyGoal(goal.id)} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Weekly Goals */}
|
||||
<div className="card">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">Weekly Goals</h2>
|
||||
<button onClick={() => setShowAddWeekly(true)} className="btn-primary !px-2 !py-1 text-xs">+ Add</button>
|
||||
</div>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Derived from monthly goals</p>
|
||||
|
||||
{showAddWeekly && (
|
||||
<div className="card bg-[#121318] border-2 border-[#4f8cff] mb-3 p-3">
|
||||
<div className="space-y-2">
|
||||
<select value={selectedMonthlyForWeekly} onChange={e => setSelectedMonthlyForWeekly(e.target.value)} className="input text-xs">
|
||||
<option value="">Select parent monthly goal</option>
|
||||
{store.monthlyGoals.filter(g => !g.completed).map(g => (
|
||||
<option key={g.id} value={g.id}>{g.description}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<input ref={newWeeklyRef} value={newWeeklyText} onChange={e => setNewWeeklyText(e.target.value)}
|
||||
placeholder="Weekly goal" className="input text-xs flex-1" />
|
||||
<button onClick={handleAddWeekly} className="btn-primary text-xs">Add</button>
|
||||
<button onClick={() => setShowAddWeekly(false)} className="btn-secondary text-xs">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{store.weeklyGoals.length === 0 ? (
|
||||
<p className="text-xs text-[#5a5b6a] py-4 text-center">No weekly goals defined</p>
|
||||
) : (
|
||||
store.weeklyGoals.map(goal => (
|
||||
<div key={goal.id} className="bg-[#121318] rounded-md p-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<label className="flex items-start gap-3 cursor-pointer flex-1">
|
||||
<input type="checkbox" checked={goal.completed} onChange={() => store.toggleWeeklyGoal(goal.id)} className="mt-1 accent-[#4f8cff]" />
|
||||
<div>
|
||||
<p className={`text-sm ${goal.completed ? 'line-through text-[#5a5b6a]' : 'text-[#e6e7ec]'}`}>{goal.description}</p>
|
||||
{goal.monthlyGoalId && (
|
||||
<p className="text-xs text-[#5a5b6a] mt-1">
|
||||
↓ {store.monthlyGoals.find(g => g.id === goal.monthlyGoalId)?.description || 'Linked goal'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
<button onClick={() => store.removeWeeklyGoal(goal.id)} className="text-[#ef4444] hover:opacity-70 px-1 shrink-0">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Daily Task List */}
|
||||
<div className="card">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">Daily Task List</h2>
|
||||
<button onClick={() => setShowAddTask(true)} className="btn-primary !px-2 !py-1 text-xs">+ Add</button>
|
||||
</div>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Morning checklist with EOD tracking</p>
|
||||
|
||||
{showAddTask && (
|
||||
<div className="card bg-[#121318] border-2 border-[#4f8cff] mb-3 p-3">
|
||||
<div className="flex gap-2">
|
||||
<input ref={newTaskRef} value={newTaskText} onChange={e => setNewTaskText(e.target.value)}
|
||||
placeholder="Daily task" className="input text-xs flex-1" />
|
||||
<button onClick={handleAddTask} className="btn-primary text-xs">Add</button>
|
||||
<button onClick={() => setShowAddTask(false)} className="btn-secondary text-xs">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{store.dailyTasks.length === 0 ? (
|
||||
<p className="text-xs text-[#5a5b6a] py-4 text-center">No daily tasks defined</p>
|
||||
) : (
|
||||
store.dailyTasks.map(task => (
|
||||
<div key={task.id} className="bg-[#121318] rounded-md p-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<label className="flex items-start gap-3 cursor-pointer flex-1">
|
||||
<input type="checkbox" checked={task.completed} onChange={() => store.toggleDailyTask(task.id)} className="mt-1 accent-[#4f8cff]" />
|
||||
<div className={task.completed ? 'line-through text-[#5a5b6a]' : ''}>
|
||||
<p className={`text-sm ${task.completed ? 'line-through text-[#5a5b6a]' : 'text-[#e6e7ec]'}`}>{task.description}</p>
|
||||
<p className="text-xs text-[#5a5b6a]">{task.date?.toLocaleDateString() || ''}</p>
|
||||
</div>
|
||||
</label>
|
||||
<button onClick={() => store.removeDailyTask(task.id)} className="text-[#ef4444] hover:opacity-70 px-1 shrink-0">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pain-Point Enforcement Note */}
|
||||
<div className="card">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">System Rules</h2>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-[#ef4444] font-bold text-base">✗</span>
|
||||
<div>
|
||||
<p className="text-[#e6e7ec]">PnL goals rejected</p>
|
||||
<p className="text-xs text-[#5a5b6a]">System blocks goals based on profit/loss targets. Focus on process.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-[#34d399] font-bold text-base">✓</span>
|
||||
<div>
|
||||
<p className="text-[#e6e7ec]">Behavioral goals enforced</p>
|
||||
<p className="text-xs text-[#5a5b6a]">Only process-oriented goals are permitted in the system.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-[#fbbf24] font-bold text-base">↕</span>
|
||||
<div>
|
||||
<p className="text-[#e6e7ec]">Goal hierarchy enforced</p>
|
||||
<p className="text-xs text-[#5a5b6a]">Weekly goals must derive from monthly goals for continuity.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Monthly Goal Item ---
|
||||
function MonthlyGoalItem({ goal, onToggle, onDelete }: { goal: { id: string; description: string; completed: boolean }; onToggle: () => void; onDelete: () => void }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
if (editing) {
|
||||
return <div className="bg-[#121318] rounded-md p-3 flex gap-2">
|
||||
<input className="input text-xs flex-1" defaultValue={goal.description} onKeyDown={e => {
|
||||
if (e.key === 'Enter') setEditing(false);
|
||||
}} />
|
||||
<button onClick={() => { const el = (e.target as HTMLInputElement).closest('.bg-\\[\\#121318\\]\\,\\ rounded-md\\, p-3')?.querySelector('input'); }} className="btn-primary text-xs">Save</button>
|
||||
<button onClick={() => setEditing(false)} className="btn-secondary text-xs">✕</button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[#121318] rounded-md p-3 flex items-center justify-between">
|
||||
<label className="flex items-center gap-3 cursor-pointer flex-1">
|
||||
<input type="checkbox" checked={goal.completed} onChange={onToggle} className="accent-[#4f8cff]" />
|
||||
<span className={`text-sm ${goal.completed ? 'line-through text-[#5a5b6a]' : 'text-[#e6e7ec]'}`}>{goal.description}</span>
|
||||
</label>
|
||||
<button onClick={onDelete} className="text-[#ef4444] hover:opacity-70 px-2">✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
import { useState, useRef } from 'react';
|
||||
import type { MonthlyGoal, WeeklyGoal } from '@/types';
|
||||
|
||||
let showAddMonthly = false, newMonthlyText = '', newMonthlyRef: React.RefObject<HTMLInputElement | null> = { current: null };
|
||||
let showAddWeekly = false, newWeeklyText = '', newWeeklyRef: React.RefObject<HTMLInputElement | null> = { current: null };
|
||||
let selectedMonthlyForWeekly = '';
|
||||
let showAddTask = false, newTaskText = '', newTaskRef: React.RefObject<HTMLInputElement | null> = { current: null };
|
||||
|
||||
function handleAddMonthly() {
|
||||
// Accessing global store via import
|
||||
const store = useGoalsStore.getState();
|
||||
if (newMonthlyText.trim()) {
|
||||
store.addMonthlyGoal({ description: newMonthlyText.trim() });
|
||||
newMonthlyText = ''; showAddMonthly = false;
|
||||
}
|
||||
}
|
||||
function handleAddWeekly() {
|
||||
const store = useGoalsStore.getState();
|
||||
if (newWeeklyText.trim()) {
|
||||
store.addWeeklyGoal({ monthlyGoalId: selectedMonthlyForWeekly, description: newWeeklyText.trim() });
|
||||
newWeeklyText = ''; showAddWeekly = false; selectedMonthlyForWeekly = '';
|
||||
}
|
||||
}
|
||||
function handleAddTask() {
|
||||
const store = useGoalsStore.getState();
|
||||
if (newTaskText.trim()) {
|
||||
store.addDailyTask({ description: newTaskText.trim() });
|
||||
newTaskText = ''; showAddTask = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
'use client';
|
||||
|
||||
import { useExecutionStore } from '@/stores/execution-store';
|
||||
import type { ExecutionPlaybook, EmotionLogState } from '@/types';
|
||||
|
||||
const EXECUTION_PATTERNS = [
|
||||
'Mean Reversion (MR)',
|
||||
'Range Extreme (RE)',
|
||||
'Break of Structure (BOS)',
|
||||
'Liquidity Sweep',
|
||||
'Trend Continuation',
|
||||
'Reversal Rejection',
|
||||
'No Trade (NT)',
|
||||
] as const;
|
||||
|
||||
export default function ExecutionPage() {
|
||||
const store = useExecutionStore();
|
||||
const plan = store.executionPlaybook;
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="p-6 max-w-[1400px] mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-[#e6e7ec]">Trade Execution</h1>
|
||||
<p className="text-sm text-[#5a5b6a] mt-1">Live trade tracking with execution library</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{store.activePlans.length > 0 && (
|
||||
<div className="card mb-6">
|
||||
<h3 className="text-sm font-bold text-[#e6e7ec] mb-3">Active Plans</h3>
|
||||
<div className="space-y-2">
|
||||
{store.activePlans.map(p => (
|
||||
<div key={p.id} className="flex items-center justify-between bg-[#121318] rounded-md p-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="badge" style={{ background: 'none', border: '1px solid var(--accent-blue)', color: '#4f8cff' }}>
|
||||
{p.tradeId}
|
||||
</span>
|
||||
<button onClick={() => store.setActivePlanId(p.id)} className="text-sm text-[#4f8cff] hover:underline">
|
||||
Execute
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => store.removePlan(p.id)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-[#5a5b6a]">No execution plan available. Select a plan above or create one.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Execution detail view ---
|
||||
return (
|
||||
<div className="p-6 max-w-[1400px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-[#e6e7ec]">Trade Execution</h1>
|
||||
<p className="text-sm text-[#5a5b6a] mt-1">
|
||||
{plan.tradeId} — Live tracking and post-trade review
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{store.activePlans.length > 1 && (
|
||||
<button onClick={() => store.setActivePlanId(null)} className="btn-secondary text-xs">
|
||||
← Back to Plan List
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checklist Status */}
|
||||
<div className={`card mb-6 ${store.checklistCompleted ? 'border-[#059669]' : ''}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-[#e6e7ec]">Execution Checklist</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mt-1">
|
||||
{store.checklistCompleted ? '✓ All setup steps completed' : '☐ Complete live checklist before entry'}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => store.toggleExecutionChecklist()}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
store.checklistCompleted ? 'bg-[#064e3b] text-[#34d399] border border-[#34d399]' : 'bg-[#1a1b24] text-[#5a5b6a] border border-[#2a2b3a]'
|
||||
}`}>
|
||||
{store.checklistCompleted ? '✓ Checklist Complete' : '☐ Complete'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-section">
|
||||
|
||||
{/* Execution Pattern Library */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Execution Pattern</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Select execution strategy pattern</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{EXECUTION_PATTERNS.map(pattern => (
|
||||
<button
|
||||
key={pattern}
|
||||
onClick={() => store.setExecutionPattern(pattern)}
|
||||
className={`p-3 rounded-md border text-xs text-left transition-all ${
|
||||
plan.executionPattern === pattern
|
||||
? 'bg-[#4f8cff]/20 border-[#4f8cff] text-[#4f8cff]'
|
||||
: 'bg-[#1a1b24] border-[#2a2b3a] text-[#5a5b6a]'
|
||||
}`}
|
||||
>
|
||||
{pattern}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scale-In Entry Table */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Scale-In Entry Table</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Track partial fills, sizes, and strategies</p>
|
||||
|
||||
{/* Add Entry Form */}
|
||||
<AddScaleEntryForm onSubmit={(entry) => {
|
||||
store.addScaleEntry(entry);
|
||||
}} />
|
||||
|
||||
{/* Scale Table */}
|
||||
<div className="overflow-x-auto mt-3">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-[#2a2b3a] text-[#5a5b6a]">
|
||||
<th className="py-2 px-3 text-left">#</th>
|
||||
<th className="py-2 px-3 text-left">Price</th>
|
||||
<th className="py-2 px-3 text-left">Size %</th>
|
||||
<th className="py-2 px-3 text-left">Strategy</th>
|
||||
<th className="py-2 px-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.scaleEntries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-6 text-center text-[#5a5b6a]">No scale entries recorded</td>
|
||||
</tr>
|
||||
) : (
|
||||
store.scaleEntries.map((entry, idx) => (
|
||||
<tr key={entry.id || idx} className="border-b border-[#2a2b3a]">
|
||||
<td className="py-2 px-3 text-[#5a5b6a]">{idx + 1}</td>
|
||||
<td className="py-2 px-3">
|
||||
<input type="number" step="any" value={entry.price}
|
||||
onChange={e => store.updateScaleEntry(idx, { price: parseFloat(e.target.value) || 0 })}
|
||||
className="input text-xs w-28" />
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<input type="number" min="0" max="100" value={entry.sizePercent}
|
||||
onChange={e => store.updateScaleEntry(idx, { sizePercent: parseInt(e.target.value) || 0 })}
|
||||
className="input text-xs w-20" />
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<select value={entry.strategy}
|
||||
onChange={e => store.updateScaleEntry(idx, { strategy: e.target.value as ExecutionPlaybook['strategy'] })}
|
||||
className="input text-xs">
|
||||
<option value="">None</option>
|
||||
{EXECUTION_PATTERNS.map(p => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
<button onClick={() => store.removeScaleEntry(idx)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Scale Metrics */}
|
||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||
<KPICard label="Total Filled %" value={`${store.totalFilledPercent}%`} color="text-[#4f8cff]" />
|
||||
<KPICard label="Avg Entry Price" value={plan.averageEntryPrice ? plan.averageEntryPrice.toFixed(4) : '--'} color="text-[#a78bfa]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emotion Logger */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">In-Trade Emotion Logger</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Record emotional state during trade</p>
|
||||
|
||||
{/* Add Emotion Form */}
|
||||
<AddEmotionForm onSubmit={(log) => store.addEmotionLog(log)} />
|
||||
|
||||
{/* Emotion Log List */}
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto mt-3">
|
||||
{store.emotionLogs.length === 0 ? (
|
||||
<p className="text-xs text-[#5a5b6a] py-4 text-center">No emotion entries yet</p>
|
||||
) : (
|
||||
store.emotionLogs.map((log, idx) => (
|
||||
<div key={log.id || idx} className="bg-[#121318] rounded-md p-3 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`badge ${emotionColor(log.emotion)}`}>{log.emotion}</span>
|
||||
<span className="text-[10px] text-[#5a5b6a]">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-[#8a8b9a]">{log.note}</p>
|
||||
</div>
|
||||
<button onClick={() => store.removeEmotionLog(idx)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Timing */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Execution Timing</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Track actual vs planned entry timing</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button onClick={() => store.setActualEntryTime()}
|
||||
className="p-3 rounded-md border border-[#2a2b3a] bg-[#1a1b24] text-xs text-left">
|
||||
<p className="text-[#e6e7ec] font-bold">⏱ Mark Actual Entry Time</p>
|
||||
<p className="text-[#5a5b6a] mt-1">
|
||||
{store.actualEntryTime ? `Recorded: ${new Date(store.actualEntryTime).toLocaleTimeString()}` : 'Not yet recorded'}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input type="datetime-local" className="input text-xs flex-1" />
|
||||
<button onClick={() => store.setActualEntryTime()} className="btn-primary text-xs">Log Plan Time</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Post-Trade Review */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Post-Trade Review</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Reflect on execution quality</p>
|
||||
|
||||
<div className="space-y-3 mt-3">
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Did you follow the plan?</label>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => store.setFollowedPlan(true)}
|
||||
className={`flex-1 py-2 rounded-md text-xs font-semibold transition-all border ${
|
||||
store.followedPlan === true ? 'bg-[#064e3b] border-[#059669] text-[#34d399]' : 'bg-[#1a1b24] border-[#2a2b3a] text-[#5a5b6a]'
|
||||
}`}>
|
||||
✓ Yes, followed
|
||||
</button>
|
||||
<button onClick={() => store.setFollowedPlan(false)}
|
||||
className={`flex-1 py-2 rounded-md text-xs font-semibold transition-all border ${
|
||||
store.followedPlan === false ? 'bg-[#78350f] border-[#b45309] text-[#fcd34d]' : 'bg-[#1a1b24] border-[#2a2b3a] text-[#5a5b6a]'
|
||||
}`}>
|
||||
✗ No, deviated
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!store.followedPlan && (
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Deviation Reason *</label>
|
||||
<textarea value={store.deviationReason} onChange={e => store.setDeviationReason(e.target.value)}
|
||||
className="input text-xs min-h-[60px] resize-none" placeholder="Why did you deviate from the plan?..." />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Strengths</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input ref={strengthRef} type="text" value={newStrength} onChange={e => setNewStrength(e.target.value)}
|
||||
placeholder="Add a strength..." className="input text-xs flex-1" />
|
||||
<button onClick={addStrength} className="btn-primary text-xs">Add</button>
|
||||
</div>
|
||||
{store.strengths.map((s, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[#34d399] text-xs">✓</span>
|
||||
<span className="text-sm text-[#8a8b9a] flex-1">{s}</span>
|
||||
<button onClick={() => store.removeStrength(idx)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Mistakes</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input ref={mistakeRef} type="text" value={newMistake} onChange={e => setNewMistake(e.target.value)}
|
||||
placeholder="Add a mistake..." className="input text-xs flex-1" />
|
||||
<button onClick={addMistake} className="btn-primary text-xs">Add</button>
|
||||
</div>
|
||||
{store.mistakes.map((m, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[#ef4444] text-xs">✗</span>
|
||||
<span className="text-sm text-[#8a8b9a] flex-1">{m.description}</span>
|
||||
<span className={`badge text-[9px] ${mistakeStageColor(m.stage)}`}>{m.stage}</span>
|
||||
<button onClick={() => store.removeMistake(idx)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Trade Diary</label>
|
||||
<textarea value={store.diary} onChange={e => store.setDiary(e.target.value)}
|
||||
className="input text-xs min-h-[120px] resize-none"
|
||||
placeholder="Write your free-form trade diary... Describe what happened, how you felt, and what you learned." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Add Scale Entry Form ---
|
||||
function AddScaleEntryForm({ onSubmit }: { onSubmit: (entry: Omit<import('@/types').ScaleEntry, 'id'>) => void }) {
|
||||
const [price, setPrice] = useState('');
|
||||
const [size, setSize] = useState('0');
|
||||
const [strategy, setStrategy] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!price) return;
|
||||
onSubmit({ price: parseFloat(price), sizePercent: parseInt(size) || 0, strategy });
|
||||
setPrice(''); setSize('0');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input type="number" step="any" value={price} onChange={e => setPrice(e.target.value)} placeholder="Price" className="input text-xs" />
|
||||
<div className="flex gap-2">
|
||||
<input type="number" min="0" max="100" value={size} onChange={e => setSize(e.target.value)} placeholder="Size %" className="input text-xs w-20" />
|
||||
<select value={strategy} onChange={e => setStrategy(e.target.value)} className="input text-xs flex-1">
|
||||
<option value="">Strategy</option>
|
||||
{EXECUTION_PATTERNS.map(p => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={handleSubmit} className="btn-primary text-xs">+</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Add Emotion Form ---
|
||||
function AddEmotionForm({ onSubmit }: { onSubmit: (log: Omit<EmotionLogState, 'id' | 'timestamp'>) => void }) {
|
||||
const [priceAtEvent, setPriceAtEvent] = useState('');
|
||||
const [emotion, setEmotion] = useState('FOMO');
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!priceAtEvent || !note) return;
|
||||
onSubmit({ priceAtEvent: parseFloat(priceAtEvent), emotion, note });
|
||||
setPriceAtEvent(''); setNote('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<input type="number" step="any" value={priceAtEvent} onChange={e => setPriceAtEvent(e.target.value)} placeholder="Price" className="input text-xs" />
|
||||
<select value={emotion} onChange={e => setEmotion(e.target.value)} className="input text-xs">
|
||||
<option>FOMO</option><option>Confidence</option><option>Fear</option><option>Greed</option><option>Hesitation</option><option>Impatience</option><option>Overconfidence</option><option>Neutral</option>
|
||||
</select>
|
||||
<input type="text" value={note} onChange={e => setNote(e.target.value)} placeholder="Note..." className="input text-xs flex-1" />
|
||||
<button onClick={handleSubmit} className="btn-primary text-xs col-span-3">Log Emotion</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
let strengthRef = useRef<HTMLInputElement>(null);
|
||||
let newStrength = '';
|
||||
function addStrength() {
|
||||
if (newStrength.trim()) {
|
||||
useExecutionStore.getState().addStrength(newStrength.trim());
|
||||
newStrength = '';
|
||||
}
|
||||
}
|
||||
|
||||
let mistakeRef = useRef<HTMLInputElement>(null);
|
||||
let newMistake = '';
|
||||
function addMistake() {
|
||||
if (newMistake.trim()) {
|
||||
useExecutionStore.getState().addMistake(newMistake.trim(), 'analysis');
|
||||
newMistake = '';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
import type { MistakeStage } from '@/types';
|
||||
|
||||
function emotionColor(e: string) {
|
||||
if (['Confidence'].includes(e)) return 'bg-[#064e3b] text-[#34d399]';
|
||||
if (['Fear', 'Hesitation'].includes(e)) return 'bg-[#78350f] text-[#fcd34d]';
|
||||
if (['FOMO', 'Greed'].includes(e)) return 'bg-[#ef4444]/20 text-[#ef4444]';
|
||||
return 'bg-[#1a1b24] text-[#5a5b6a]';
|
||||
}
|
||||
|
||||
function mistakeStageColor(s: MistakeStage) {
|
||||
if (s === 'analysis') return 'bg-[#3730a3] text-[#a5b4fc]';
|
||||
if (s === 'planning') return 'bg-[#4f8cff]/20 text-[#4f8cff]';
|
||||
if (s === 'execution') return 'bg-[#78350f] text-[#fcd34d]';
|
||||
if (s === 'management') return 'bg-[#78350f]/20 text-[#fbbf24]';
|
||||
return 'bg-[#78350f]/20 text-[#ef4444]';
|
||||
}
|
||||
|
||||
function KPICard({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
return (
|
||||
<div className="bg-[#1a1b24] border border-[#2a2b3a] rounded-md p-3 text-center">
|
||||
<p className="text-[10px] text-[#5a5b6a]">{label}</p>
|
||||
<p className={`text-sm font-bold ${color}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,404 @@
|
||||
'use client';
|
||||
|
||||
import { useTradePlanStore } from '@/stores/trade-plan-store';
|
||||
import type { ConfluenceType, SetupTier, TradePlan } from '@/types';
|
||||
|
||||
export default function TradePlanPage() {
|
||||
const store = useTradePlanStore();
|
||||
const plan = store.currentPlanId
|
||||
? store.activePlans.find(p => p.id === store.currentPlanId)
|
||||
: null;
|
||||
|
||||
// --- Confluence options ---
|
||||
const CONFLUENCE_OPTIONS: { value: ConfluenceType; label: string; desc: string }[] = [
|
||||
{ value: 'support_resistance', label: 'Support/Resistance', desc: 'Key S/R level aligns with setup' },
|
||||
{ value: 'ema', label: 'EMA Confluence', desc: 'Price at/near key EMA (21, 50, 200)' },
|
||||
{ value: 'fibonacci', label: 'Fibonacci', desc: '61.8% or 38.2% retracement' },
|
||||
{ value: 'supply_demand', label: 'Supply/Demand Zone', desc: 'Fresh order block or imbalance' },
|
||||
{ value: 'pattern_recognition', label: 'Pattern Recognition', desc: 'Reversal/continuation pattern' },
|
||||
{ value: 'volume_profile', label: 'Volume Profile', desc: 'High volume node or POC' },
|
||||
{ value: 'momentum_divergence', label: 'Momentum Divergence', desc: 'RSI/MACD divergence on HTF' },
|
||||
{ value: 'single_print', label: 'Single Print', desc: 'Unfilled price zone' },
|
||||
{ value: 'composite_structure', label: 'Composite Structure', desc: 'Multiple HTF alignments' },
|
||||
{ value: 'news_filter', label: 'News Filter Clear', desc: 'No high-impact news in next 2h' },
|
||||
];
|
||||
|
||||
// --- Tier display ---
|
||||
const getTierClass = (tier: SetupTier): string => {
|
||||
switch (tier) {
|
||||
case 'A_STAR': return 'bg-[#064e3b] text-[#34d399] border-[#059669]';
|
||||
case 'A': return 'bg-[#065f46] text-[#6ee7b7] border-[#10b981]';
|
||||
case 'B': return 'bg-[#3730a3] text-[#a5b4fc] border-[#6366f1]';
|
||||
case 'C': return 'bg-[#78350f] text-[#fcd34d] border-[#f59e0b]';
|
||||
}
|
||||
};
|
||||
|
||||
const getTierLabel = (tier: SetupTier): string => {
|
||||
switch (tier) { return 'A_STAR'; case A_STAR': return 'A*'; case 'A': return 'A'; case 'B': return 'B'; case 'C': return 'C'; }
|
||||
};
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="p-6 max-w-[1400px] mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-[#e6e7ec]">Trade Plan</h1>
|
||||
<p className="text-sm text-[#5a5b6a] mt-1">Structured trade planning with AI-augmented scoring</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{store.activePlans.length > 0 && (
|
||||
<div className="card mb-6">
|
||||
<h3 className="text-sm font-semibold text-[#e6e7ec] mb-3">Active Plans</h3>
|
||||
<div className="space-y-2">
|
||||
{store.activePlans.map(p => (
|
||||
<div key={p.id} className="flex items-center justify-between bg-[#121318] rounded-md p-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="badge" style={{ background: 'none', border: `1px solid var(--accent-blue)`, color: '#4f8cff' }}>
|
||||
{p.tradeId}
|
||||
</span>
|
||||
<span className={`badge ${getTierClass(p.tier)}`}>{getTierLabel(p.tier)}</span>
|
||||
<span className="text-sm text-[#8a8b9a]">{p.environment}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`badge ${statusColor(p.status)}`}>{formatStatus(p.status)}</span>
|
||||
<button onClick={() => store.setCurrentPlan(p.id)} className="btn-primary text-xs">Open</button>
|
||||
<button onClick={() => store.removePlan(p.id)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={() => {
|
||||
const newPlan = store.createPlan();
|
||||
store.addActivePlan(newPlan);
|
||||
store.setCurrentPlan(newPlan.id);
|
||||
}} className="btn-primary text-lg px-8 py-3">
|
||||
+ Create New Trade Plan
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Plan detail view ---
|
||||
return (
|
||||
<div className="p-6 max-w-[1400px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold text-[#e6e7ec]">Trade Plan</h1>
|
||||
<span className="badge" style={{ background: 'none', border: '1px solid var(--accent-blue)', color: '#4f8cff' }}>
|
||||
{plan.tradeId}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-[#5a5b6a] mt-1">
|
||||
Status: <span className={`badge ${statusColor(plan.status)}`}>{formatStatus(plan.status)}</span>
|
||||
{' · '}Created: {plan.date instanceof Date ? plan.date.toLocaleDateString() : new Date(plan.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation controls */}
|
||||
<div className="flex items-center gap-3">
|
||||
{store.activePlans.length > 1 && (
|
||||
<button onClick={() => store.setCurrentPlan(null)} className="btn-secondary text-xs">
|
||||
← Back to Plan List
|
||||
</button>
|
||||
)}
|
||||
{plan.status === 'planned' && (
|
||||
<button onClick={() => store.advanceStatus()} className="btn-primary">▶ Advance to ACTIVE</button>
|
||||
)}
|
||||
{plan.status === 'active' && (
|
||||
<button onClick={() => store.advanceStatus()} className="btn-primary" style={{ background: 'var(--accent-yellow)' }}>
|
||||
✕ Close Trade
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checklist Gate */}
|
||||
<div className={`card mb-6 ${plan.checklistCompleted ? 'border-[#059669]' : ''}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-[#e6e7ec]">Pre-Flight Checklist</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mt-1">
|
||||
{plan.checklistCompleted
|
||||
? 'All modules complete — trade is ready to execute'
|
||||
: 'Complete all daily modules before trading'}
|
||||
</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={plan.checklistCompleted}
|
||||
onChange={() => {
|
||||
const newChecked = !plan.checklistCompleted;
|
||||
store.setChecklistCompleted(newChecked);
|
||||
}}
|
||||
className="w-5 h-5 accent-[#4f8cff]"
|
||||
/>
|
||||
<span className={`text-sm font-bold ${plan.checklistCompleted ? 'text-[#34d399]' : 'text-[#5a5b6a]'}`}>
|
||||
{plan.checklistCompleted ? '✓ Complete' : '☐ Not Complete'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-section">
|
||||
|
||||
{/* Environment Classification */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Environment Classification</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Required before any trade action</p>
|
||||
<div className="flex gap-2">
|
||||
{(['Trending', 'Ranging', 'Transitioning'] as const).map(env => (
|
||||
<button
|
||||
key={env}
|
||||
onClick={() => {
|
||||
const envMap = { 'Trending': 'trending' as const, 'Ranging': 'ranging' as const, 'Transitioning': 'transitioning' as const };
|
||||
store.setEnvironment(envMap[env]);
|
||||
}}
|
||||
className={`flex-1 py-2 rounded-md text-xs font-semibold transition-all border ${
|
||||
plan.environment === env.toLowerCase()
|
||||
? 'bg-[#4f8cff]/20 border-[#4f8cff] text-[#4f8cff]'
|
||||
: 'bg-[#1a1b24] border-[#2a2b3a] text-[#5a5b6a]'
|
||||
}`}
|
||||
>
|
||||
{env}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Setup Tier & Risk */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Setup Tier & Risk</h2>
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className={`text-5xl font-black border-2 rounded-xl px-8 py-4 ${getTierClass(plan.tier)}`}>
|
||||
{plan.tier}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||
<KPICard label="Confluence Score" value={`${plan.confluenceScore}`} color="text-[#4f8cff]" />
|
||||
<KPICard label="Risk %" value={plan.riskPercent.toFixed(2)} color="text-[#a78bfa]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position Sizing */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Position Sizing Calculator</h2>
|
||||
<div className="space-y-3 mt-3">
|
||||
<div>
|
||||
<label className="label">Account Balance</label>
|
||||
<input type="number" className="input text-xs" placeholder="$10,000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Risk per Trade (%)</label>
|
||||
<input type="number" step="0.1" defaultValue={plan.riskPercent.toString()} className="input text-xs" disabled />
|
||||
<p className="text-[10px] text-[#5a5b6a] mt-1">Based on tier: {plan.tier}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Entry Price</label>
|
||||
<input type="number" step="any" value={plan.entryPrice?.toString() || ''}
|
||||
onChange={e => store.setEntryPrice(parseFloat(e.target.value) || 0)} className="input text-xs" placeholder="0.0000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Stop Loss Price</label>
|
||||
<input type="number" step="any" value={plan.stopLoss?.toString() || ''}
|
||||
onChange={e => store.setStopLoss(parseFloat(e.target.value) || 0)} className="input text-xs" placeholder="0.0000" />
|
||||
</div>
|
||||
<div className="bg-[#121318] rounded-md p-3 text-center">
|
||||
<p className="text-xs text-[#5a5b6a]">Risk Amount: <span className="text-[#ef4444] font-bold">$---</span></p>
|
||||
<p className="text-xs text-[#5a5b6a]">Position Size: <span className="text-[#4f8cff] font-bold">----</span></p>
|
||||
<p className="text-xs text-[#5a5b6a]">Lots: <span className="text-[#34d399] font-bold">----</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confluences */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Confluences Checklist</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Required for trade to advance to active</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{CONFLUENCE_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => {
|
||||
if (plan.confluences.includes(opt.value)) {
|
||||
store.removeConfluence(opt.value);
|
||||
} else {
|
||||
store.addConfluence(opt.value);
|
||||
}
|
||||
}}
|
||||
className={`text-left p-2 rounded-md border text-xs transition-all ${
|
||||
plan.confluences.includes(opt.value)
|
||||
? 'bg-[#4f8cff]/20 border-[#4f8cff] text-[#4f8cff]'
|
||||
: 'bg-[#1a1b24] border-[#2a2b3a] text-[#5a5b6a]'
|
||||
}`}
|
||||
>
|
||||
<p className="font-bold">{opt.label}</p>
|
||||
<p className="text-[10px] mt-1 opacity-70">{opt.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-xs text-[#5a5b6a]">
|
||||
Confluence count: <span className={`text-sm font-bold ${plan.confluences.length >= 3 ? 'text-[#34d399]' : plan.confluences.length >= 2 ? 'text-[#fbbf24]' : 'text-[#ef4444]'}`}>{plan.confluences.length}</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-[#5a5b6a]">
|
||||
{plan.confluences.length >= 3 ? '≥3 needed for A tier+' : plan.confluences.length === 2 ? 'Need 1 more for B' : 'Need at least 1'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Justifications - Why? */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Why? — Trade Justifications</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Mandatory reasoning before execution</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Why Enter? *</label>
|
||||
<textarea
|
||||
value={plan.justifications.entryWhy}
|
||||
onChange={e => store.setJustification('entryWhy', e.target.value)}
|
||||
className="input text-xs min-h-[80px] resize-none"
|
||||
placeholder="Describe your entry reasoning..."
|
||||
/>
|
||||
{plan.justifications.entryWhy.length === 0 && plan.status === 'active' && (
|
||||
<p className="text-[10px] text-[#ef4444] mt-1">Required for active trades</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Why Stop Loss? *</label>
|
||||
<textarea
|
||||
value={plan.justifications.stopWhy}
|
||||
onChange={e => store.setJustification('stopWhy', e.target.value)}
|
||||
className="input text-xs min-h-[60px] resize-none"
|
||||
placeholder="Why is this the correct stop loss level..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label text-[#e6e7ec]">Why Target? *</label>
|
||||
<textarea
|
||||
value={plan.justifications.targetWhy}
|
||||
onChange={e => store.setJustification('targetWhy', e.target.value)}
|
||||
className="input text-xs min-h-[60px] resize-none"
|
||||
placeholder="Why are these targets logical..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`mt-3 text-xs ${
|
||||
plan.justifications.entryWhy && plan.justifications.stopWhy && plan.justifications.targetWhy
|
||||
? 'text-[#34d399]' : 'text-[#5a5b6a]'
|
||||
}`}>
|
||||
{plan.justifications.entryWhy && plan.justifications.stopWhy && plan.justifications.targetWhy
|
||||
? '✓ All justifications complete' : '✗ Complete all fields before execution'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Targets */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Targets</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">Define exit targets with reasoning</p>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
{plan.targets.length === 0 ? (
|
||||
<p className="text-xs text-[#5a5b6a] py-2 text-center">No targets defined</p>
|
||||
) : (
|
||||
plan.targets.map((t, idx) => (
|
||||
<div key={idx} className="bg-[#121318] rounded-md p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[#4f8cff] font-mono font-bold text-sm">{t.price.toFixed(2)}</span>
|
||||
<p className="text-xs text-[#5a5b6a] mt-1">{t.reason}</p>
|
||||
</div>
|
||||
<button onClick={() => store.removeTarget(idx)} className="text-[#ef4444] hover:opacity-70 px-1">✕</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Target */}
|
||||
<AddTargetForm
|
||||
onAdd={(price, reason) => store.addTarget(price, reason)}
|
||||
entryPrice={plan.entryPrice}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Devil's Advocate */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Devil's Advocate</h2>
|
||||
<p className="text-xs text-[#5a5b6a] mb-4">List reasons NOT to take this trade</p>
|
||||
|
||||
<textarea
|
||||
value={plan.devilAdvocate}
|
||||
onChange={e => store.setDevilAdvocate(e.target.value)}
|
||||
className="input text-xs min-h-[100px] resize-none"
|
||||
placeholder="What could go wrong? What reasons argue against this trade? Be honest..."
|
||||
/>
|
||||
<div className="mt-3 flex items-center justify-between text-xs">
|
||||
<span className="text-[#5a5b6a]">
|
||||
Characters: {plan.devilAdvocate.length}
|
||||
</span>
|
||||
<span className={plan.devilAdvocate.length > 10 ? 'text-[#fbbf24]' : plan.devilAdvocate.length === 0 ? 'text-[#5a5b6a]' : 'text-[#34d399]'}>
|
||||
{plan.devilAdvocate.length === 0 ? '⚠ No counter-arguments listed' : plan.devilAdvocate.length < 10 ? '💡 Consider deeper analysis' : '✓ Devil\'s advocate engaged'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Add Target Form ---
|
||||
function AddTargetForm({ onAdd, entryPrice }: { onAdd: (price: number, reason: string) => void; entryPrice?: number }) {
|
||||
const [price, setPrice] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!price || !reason) return;
|
||||
onAdd(parseFloat(price), reason);
|
||||
setPrice('');
|
||||
setReason('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input type="number" step="any" value={price} onChange={e => setPrice(e.target.value)} placeholder="Target price" className="input text-xs" />
|
||||
<div className="flex gap-1">
|
||||
<input type="text" value={reason} onChange={e => setReason(e.target.value)} placeholder="Reason for target" className="input text-xs flex-1" />
|
||||
<button onClick={handleSubmit} className="btn-primary text-xs !px-3">+</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
import { useState } from 'react';
|
||||
|
||||
function statusColor(s: TradePlan['status']) {
|
||||
if (s === 'planned') return 'bg-[#1a1b24] text-[#5a5b6a]';
|
||||
if (s === 'active') return 'bg-[#064e3b] text-[#34d399]';
|
||||
return 'bg-[#78350f] text-[#fcd34d]';
|
||||
}
|
||||
|
||||
function formatStatus(s: TradePlan['status']) {
|
||||
if (s === 'planned') return 'PLANNED';
|
||||
if (s === 'active') return 'ACTIVE';
|
||||
return 'CLOSED';
|
||||
}
|
||||
|
||||
function KPICard({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
return (
|
||||
<div className="bg-[#1a1b24] border border-[#2a2b3a] rounded-md p-3 text-center">
|
||||
<p className="text-[10px] text-[#5a5b6a]">{label}</p>
|
||||
<p className={`text-lg font-bold ${color}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { create } from 'zustand';
|
||||
import type { MistakeStage } from '@/types';
|
||||
|
||||
interface EmotionLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
priceAtEvent: number;
|
||||
emotion: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
interface ClosureStore {
|
||||
activeExecutionId: string | null;
|
||||
|
||||
// Emotion Logger
|
||||
emotionLogs: EmotionLog[];
|
||||
addEmotionLog: (log: Omit<EmotionLog, 'id' | 'timestamp'>) => void;
|
||||
removeEmotionLog: (idx: number) => void;
|
||||
|
||||
// Post-trade review fields
|
||||
diary: string;
|
||||
setDiary: (d: string) => void;
|
||||
|
||||
followedPlan: boolean | null;
|
||||
setFollowedPlan: (v: boolean) => void;
|
||||
|
||||
deviationReason: string;
|
||||
setDeviationReason: (r: string) => void;
|
||||
|
||||
strengths: string[];
|
||||
addStrength: (s: string) => void;
|
||||
removeStrength: (idx: number) => void;
|
||||
|
||||
mistakes: { description: string; stage: MistakeStage }[];
|
||||
addMistake: (desc: string, stage: MistakeStage) => void;
|
||||
removeMistake: (idx: number) => void;
|
||||
}
|
||||
|
||||
export const useClosureStore = create<ClosureStore>((set, get) => ({
|
||||
activeExecutionId: null,
|
||||
|
||||
emotionLogs: [],
|
||||
addEmotionLog: (log) => {
|
||||
const entry = { id: crypto.randomUUID(), timestamp: Date.now(), ...log };
|
||||
set({ emotionLogs: [...get().emotionLogs, entry] });
|
||||
},
|
||||
removeEmotionLog: (idx) => set({ emotionLogs: get().emotionLogs.filter((_, i) => i !== idx) }),
|
||||
|
||||
diary: '',
|
||||
setDiary: (d) => set({ diary: d }),
|
||||
|
||||
followedPlan: null,
|
||||
setFollowedPlan: (v) => set({ followedPlan: v }),
|
||||
|
||||
deviationReason: '',
|
||||
setDeviationReason: (r) => set({ deviationReason: r }),
|
||||
|
||||
strengths: [],
|
||||
addStrength: (s) => set({ strengths: [...get().strengths, s] }),
|
||||
removeStrength: (idx) => set({ strengths: get().strengths.filter((_, i) => i !== idx) }),
|
||||
|
||||
mistakes: [],
|
||||
addMistake: (desc, stage) => set({ mistakes: [...get().mistakes, { description: desc, stage }] }),
|
||||
removeMistake: (idx) => set({ mistakes: get().mistakes.filter((_, i) => i !== idx) }),
|
||||
}));
|
||||
@@ -0,0 +1,127 @@
|
||||
import { create } from 'zustand';
|
||||
import type { ExecutionPlaybook, ScaleEntry, MistakeStage } from '@/types';
|
||||
|
||||
interface EmotionLogState {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
priceAtEvent: number;
|
||||
emotion: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface ExecutionStore {
|
||||
activePlanId: string | null;
|
||||
setActivePlanId: (id: string | null) => void;
|
||||
|
||||
executionPlaybook: ExecutionPlaybook | null;
|
||||
setExecutionPlaybook: (pb: ExecutionPlaybook) => void;
|
||||
|
||||
checklistCompleted: boolean;
|
||||
toggleExecutionChecklist: () => void;
|
||||
|
||||
scaleEntries: ScaleEntry[];
|
||||
addScaleEntry: (entry: Omit<ScaleEntry, 'id'>) => void;
|
||||
removeScaleEntry: (idx: number) => void;
|
||||
updateScaleEntry: (idx: number, entry: Partial<ScaleEntry>) => void;
|
||||
|
||||
// Execution timing
|
||||
actualEntryTime: number | null;
|
||||
plannedEntryTime: number | null;
|
||||
setActualEntryTime: () => void;
|
||||
setPlannedEntryTime: (t: number) => void;
|
||||
|
||||
// Emotion Logger
|
||||
emotionLogs: EmotionLogState[];
|
||||
addEmotionLog: (log: Omit<EmotionLogState, 'id' | 'timestamp'>) => void;
|
||||
removeEmotionLog: (idx: number) => void;
|
||||
|
||||
// Post-trade review
|
||||
followedPlan: boolean | null;
|
||||
deviationReason: string;
|
||||
strengths: string[];
|
||||
mistakes: { description: string; stage: MistakeStage }[];
|
||||
diary: string;
|
||||
|
||||
setFollowedPlan: (v: boolean) => void;
|
||||
setDeviationReason: (r: string) => void;
|
||||
addStrength: (s: string) => void;
|
||||
removeStrength: (idx: number) => void;
|
||||
addMistake: (desc: string, stage: MistakeStage) => void;
|
||||
removeMistake: (idx: number) => void;
|
||||
setDiary: (d: string) => void;
|
||||
|
||||
totalFilledPercent: number;
|
||||
averageEntryPrice: number;
|
||||
}
|
||||
|
||||
const emptyEntry = {
|
||||
executionPlaybook: null as ExecutionPlaybook | null,
|
||||
checklistCompleted: false,
|
||||
scaleEntries: [],
|
||||
actualEntryTime: null,
|
||||
plannedEntryTime: null,
|
||||
emotionLogs: [],
|
||||
followedPlan: null,
|
||||
deviationReason: '',
|
||||
strengths: [],
|
||||
mistakes: [],
|
||||
diary: '',
|
||||
};
|
||||
|
||||
export const useExecutionStore = create<ExecutionStore>((set, get) => ({
|
||||
activePlanId: null,
|
||||
setActivePlanId: (id) => set({ activePlanId: id }),
|
||||
|
||||
...emptyEntry,
|
||||
|
||||
setExecutionPlaybook: (pb) => set({ executionPlaybook: pb }),
|
||||
toggleExecutionChecklist: () => set(s => ({ checklistCompleted: !s.checklistCompleted })),
|
||||
|
||||
addScaleEntry: (entry) => {
|
||||
const newEntry: ScaleEntry = { id: crypto.randomUUID(), ...entry };
|
||||
set(s => ({ scaleEntries: [...s.scaleEntries, newEntry] }));
|
||||
},
|
||||
|
||||
removeScaleEntry: (idx) => {
|
||||
set(s => ({ scaleEntries: s.scaleEntries.filter((_, i) => i !== idx) }));
|
||||
},
|
||||
|
||||
updateScaleEntry: (idx, partials) => {
|
||||
set(s => ({ scaleEntries: s.scaleEntries.map((e, i) => i === idx ? { ...e, ...partials } : e) }));
|
||||
},
|
||||
|
||||
setActualEntryTime: () => set({ actualEntryTime: Date.now() }),
|
||||
setPlannedEntryTime: (t) => set({ plannedEntryTime: t }),
|
||||
|
||||
addEmotionLog: (log) => {
|
||||
const entry: EmotionLogState = { id: crypto.randomUUID(), timestamp: Date.now(), ...log };
|
||||
set(s => ({ emotionLogs: [...s.emotionLogs, entry] }));
|
||||
},
|
||||
|
||||
removeEmotionLog: (idx) => {
|
||||
set(s => ({ emotionLogs: s.emotionLogs.filter((_, i) => i !== idx) }));
|
||||
},
|
||||
|
||||
setFollowedPlan: (v) => set({ followedPlan: v }),
|
||||
setDeviationReason: (r) => set({ deviationReason: r }),
|
||||
addStrength: (s) => set(st => ({ strengths: [...st.strengths, s] })),
|
||||
removeStrength: (idx) => set(st => ({ strengths: st.strengths.filter((_, i) => i !== idx) })),
|
||||
addMistake: (desc, stage) => set(st => ({ mistakes: [...st.mistakes, { description: desc, stage }] })),
|
||||
removeMistake: (idx) => set(st => ({ mistakes: st.mistakes.filter((_, i) => i !== idx) })),
|
||||
setDiary: (d) => set({ diary: d }),
|
||||
|
||||
totalFilledPercent: 0,
|
||||
averageEntryPrice: 0,
|
||||
}));
|
||||
|
||||
// Compute scale metrics on each state change
|
||||
useExecutionStore.subscribe((state, prevState) => {
|
||||
if (prevState.scaleEntries !== state.scaleEntries) {
|
||||
const s = useExecutionStore.getState();
|
||||
const total = s.scaleEntries.reduce((sum, e) => sum + e.sizePercent, 0);
|
||||
const avgPrice = total > 0
|
||||
? s.scaleEntries.reduce((sum, e) => sum + e.price * (e.sizePercent / 100), 0)
|
||||
: 0;
|
||||
useExecutionStore.setState({ totalFilledPercent: total, averageEntryPrice: avgPrice });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { create } from 'zustand';
|
||||
import type { MonthlyGoal, WeeklyGoal, DailyTask } from '@/types';
|
||||
|
||||
interface GoalsStore {
|
||||
monthlyGoals: MonthlyGoal[];
|
||||
addMonthlyGoal: (goal: Omit<MonthlyGoal, 'id' | 'createdAt' | 'completed'>) => void;
|
||||
removeMonthlyGoal: (id: string) => void;
|
||||
toggleMonthlyGoal: (id: string) => void;
|
||||
weeklyGoals: WeeklyGoal[];
|
||||
addWeeklyGoal: (goal: Omit<WeeklyGoal, 'id' | 'completed' | 'weekStarting'>) => void;
|
||||
removeWeeklyGoal: (id: string) => void;
|
||||
toggleWeeklyGoal: (id: string) => void;
|
||||
dailyTasks: DailyTask[];
|
||||
addDailyTask: (task: Omit<DailyTask, 'id' | 'completed'>) => void;
|
||||
removeDailyTask: (id: string) => void;
|
||||
toggleDailyTask: (id: string) => void;
|
||||
}
|
||||
|
||||
const MONTHLY_KEY = 'trader-dashboard:monthly-goals';
|
||||
const WEEKLY_KEY = 'trader-dashboard:weekly-goals';
|
||||
const DAILY_KEY = 'trader-dashboard:daily-tasks';
|
||||
|
||||
function load<T>(key: string, fallback: T[]): T[] {
|
||||
try { const s = localStorage.getItem(key); if (s) return JSON.parse(s); } catch {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function save<T>(key: string, data: T) {
|
||||
try { localStorage.setItem(key, JSON.stringify(data)); } catch {}
|
||||
}
|
||||
|
||||
export const useGoalsStore = create<GoalsStore>((set) => ({
|
||||
monthlyGoals: load(MONTHLY_KEY, []),
|
||||
weeklyGoals: load(WEEKLY_KEY, []),
|
||||
dailyTasks: load(DAILY_KEY, []),
|
||||
|
||||
addMonthlyGoal: (goal) => {
|
||||
const g: MonthlyGoal = { id: crypto.randomUUID(), ...goal, completed: false, createdAt: new Date() };
|
||||
const u = [...useGoalsStore.getState().monthlyGoals, g];
|
||||
set({ monthlyGoals: u }); save(MONTHLY_KEY, u);
|
||||
},
|
||||
removeMonthlyGoal: (id) => {
|
||||
const u = useGoalsStore.getState().monthlyGoals.filter(g => g.id !== id);
|
||||
set({ monthlyGoals: u }); save(MONTHLY_KEY, u);
|
||||
},
|
||||
toggleMonthlyGoal: (id) => {
|
||||
const u = useGoalsStore.getState().monthlyGoals.map(g => g.id === id ? { ...g, completed: !g.completed } : g);
|
||||
set({ monthlyGoals: u }); save(MONTHLY_KEY, u);
|
||||
},
|
||||
addWeeklyGoal: (goal) => {
|
||||
const g: WeeklyGoal = { id: crypto.randomUUID(), ...goal, completed: false, weekStarting: new Date() };
|
||||
const u = [...useGoalsStore.getState().weeklyGoals, g];
|
||||
set({ weeklyGoals: u }); save(WEEKLY_KEY, u);
|
||||
},
|
||||
removeWeeklyGoal: (id) => {
|
||||
const u = useGoalsStore.getState().weeklyGoals.filter(g => g.id !== id);
|
||||
set({ weeklyGoals: u }); save(WEEKLY_KEY, u);
|
||||
},
|
||||
toggleWeeklyGoal: (id) => {
|
||||
const u = useGoalsStore.getState().weeklyGoals.map(g => g.id === id ? { ...g, completed: !g.completed } : g);
|
||||
set({ weeklyGoals: u }); save(WEEKLY_KEY, u);
|
||||
},
|
||||
addDailyTask: (task) => {
|
||||
const t: DailyTask = { id: crypto.randomUUID(), ...task, completed: false, date: new Date() };
|
||||
const u = [...useGoalsStore.getState().dailyTasks, t];
|
||||
set({ dailyTasks: u }); save(DAILY_KEY, u);
|
||||
},
|
||||
removeDailyTask: (id) => {
|
||||
const u = useGoalsStore.getState().dailyTasks.filter(t => t.id !== id);
|
||||
set({ dailyTasks: u }); save(DAILY_KEY, u);
|
||||
},
|
||||
toggleDailyTask: (id) => {
|
||||
const u = useGoalsStore.getState().dailyTasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
|
||||
set({ dailyTasks: u }); save(DAILY_KEY, u);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,84 @@
|
||||
import { create } from 'zustand';
|
||||
import type { MarketOutlookState, KeyLevel, SetupOnRadar, HighImpactEvent, WeeklyBias, DailyBias, TrendAlignment } from '@/types';
|
||||
|
||||
const today = new Date().toISOString();
|
||||
|
||||
export const defaultMarketOutlook: MarketOutlookState = {
|
||||
weeklyBias: 'neutral',
|
||||
dailyBias: 'range_bound',
|
||||
trendAlignment: 'neutral',
|
||||
setupsOnRadar: [],
|
||||
highImpactEvents: [],
|
||||
keyLevels: [],
|
||||
checklistCompleted: false,
|
||||
};
|
||||
|
||||
export interface MarketOutlookStore extends MarketOutlookState {
|
||||
setWeeklyBias: (bias: WeeklyBias) => void;
|
||||
setDailyBias: (bias: DailyBias) => void;
|
||||
recalcAlignment: () => void;
|
||||
addKeyLevel: (level: KeyLevel) => void;
|
||||
removeKeyLevel: (id: string) => void;
|
||||
addSetupOnRadar: (setup: SetupOnRadar) => void;
|
||||
removeSetupOnRadar: (id: string) => void;
|
||||
addHighImpactEvent: (event: HighImpactEvent) => void;
|
||||
toggleChecklist: () => void;
|
||||
loadFromStorage: () => void;
|
||||
saveToStorage: () => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'trader-dashboard:market-outlook';
|
||||
|
||||
function loadFromStorage(): MarketOutlookState | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveToStorage(state: MarketOutlookState) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export const useMarketOutlookStore = create<MarketOutlookStore>((set, get) => ({
|
||||
...defaultMarketOutlook,
|
||||
|
||||
setWeeklyBias: (bias) => set((s) => ({ weeklyBias: bias })),
|
||||
setDailyBias: (bias) => set((s) => {
|
||||
s.dailyBias = bias;
|
||||
return s;
|
||||
}),
|
||||
|
||||
recalcAlignment: () => {
|
||||
const { weeklyBias, dailyBias } = get();
|
||||
let alignment: TrendAlignment;
|
||||
if (dailyBias === 'trend_up' && weeklyBias === 'bullish') alignment = 'pro_trend';
|
||||
else if (dailyBias === 'trend_down' && weeklyBias === 'bearish') alignment = 'pro_trend';
|
||||
else if (dailyBias !== 'neutral' && weeklyBias !== 'neutral') alignment = 'counter_trend';
|
||||
else alignment = 'neutral';
|
||||
set({ trendAlignment: alignment });
|
||||
},
|
||||
|
||||
addKeyLevel: (level) => set((s) => ({ keyLevels: [...s.keyLevels, level] })),
|
||||
removeKeyLevel: (id) => set((s) => ({ keyLevels: s.keyLevels.filter(l => l.id !== id) })),
|
||||
|
||||
addSetupOnRadar: (setup) => set((s) => ({ setupsOnRadar: [...s.setupsOnRadar, setup] })),
|
||||
removeSetupOnRadar: (id) => set((s) => ({ setupsOnRadar: s.setupsOnRadar.filter(s => s.id !== id) })),
|
||||
|
||||
addHighImpactEvent: (event) => set((s) => ({ highImpactEvents: [...s.highImpactEvents, event] })),
|
||||
|
||||
toggleChecklist: () => set((s) => ({ checklistCompleted: !s.checklistCompleted })),
|
||||
|
||||
loadFromStorage: () => {
|
||||
const stored = loadFromStorage();
|
||||
if (stored) set(stored);
|
||||
},
|
||||
|
||||
saveToStorage: () => saveToStorage(get()),
|
||||
}));
|
||||
|
||||
// Auto-save on change
|
||||
useMarketOutlookStore.subscribe((state) => saveToStorage(state));
|
||||
@@ -0,0 +1,178 @@
|
||||
import { create } from 'zustand';
|
||||
import type { TradePlan, ConfluenceType, ScaleEntry, SetupTier } from '@/types';
|
||||
|
||||
interface TradePlanStore {
|
||||
activePlans: TradePlan[];
|
||||
currentPlanId: string | null;
|
||||
|
||||
createPlan: () => TradePlan;
|
||||
setCurrentPlan: (id: string | null) => void;
|
||||
addActivePlan: (plan: TradePlan) => void;
|
||||
removePlan: (id: string) => void;
|
||||
|
||||
// Environment
|
||||
setEnvironment: (env: TradePlan['environment']) => void;
|
||||
|
||||
// Checklist gate
|
||||
setChecklistCompleted: (val: boolean) => void;
|
||||
|
||||
// Confluences
|
||||
addConfluence: (c: ConfluenceType) => void;
|
||||
removeConfluence: (c: ConfluenceType) => void;
|
||||
setConfluences: (cs: ConfluenceType[]) => void;
|
||||
|
||||
// Devil's Advocate
|
||||
setDevilAdvocate: (text: string) => void;
|
||||
|
||||
// Tier scoring
|
||||
recalcTierAndRisk: () => void;
|
||||
|
||||
// Justifications (Why?)
|
||||
setJustification: (field: 'entryWhy' | 'stopWhy' | 'targetWhy', text: string) => void;
|
||||
|
||||
// Targets
|
||||
addTarget: (price: number, reason: string) => void;
|
||||
removeTarget: (idx: number) => void;
|
||||
|
||||
// Entry/SL
|
||||
setEntryPrice: (price: number) => void;
|
||||
setStopLoss: (price: number) => void;
|
||||
|
||||
// Status
|
||||
setStatus: (status: TradePlan['status']) => void;
|
||||
advanceStatus: () => void;
|
||||
}
|
||||
|
||||
const TIERS: Record<SetupTier, number> = { A_STAR: 2.0, A: 1.5, B: 1.0, C: 0.5 };
|
||||
|
||||
function baseTier(confluences: ConfluenceType[], devilAdvocate: string): SetupTier {
|
||||
const hasHoles = devilAdvocate.trim().length > 10;
|
||||
if (confluences.length >= 4 && !hasHoles) return 'A_STAR';
|
||||
if (confluences.length >= 3 && !hasHoles) return 'A';
|
||||
if (confluences.length >= 2 && !hasHoles) return 'B';
|
||||
if (confluences.length >= 1 && !hasHoles) return 'B';
|
||||
return 'C';
|
||||
}
|
||||
|
||||
export const useTradePlanStore = create<TradePlanStore>((set, get) => ({
|
||||
activePlans: [],
|
||||
currentPlanId: null,
|
||||
|
||||
createPlan: () => {
|
||||
const plan: TradePlan = {
|
||||
id: crypto.randomUUID(),
|
||||
tradeId: `T-${Date.now()}`,
|
||||
date: new Date(),
|
||||
status: 'planned',
|
||||
environment: 'trending',
|
||||
tier: 'C',
|
||||
confluenceScore: 0,
|
||||
riskPercent: 0.5,
|
||||
confluences: [],
|
||||
devilAdvocate: '',
|
||||
positionSize: 0,
|
||||
targets: [],
|
||||
justifications: { entryWhy: '', stopWhy: '', targetWhy: '' },
|
||||
checklistCompleted: false,
|
||||
};
|
||||
return plan;
|
||||
},
|
||||
|
||||
setCurrentPlan: (id) => set({ currentPlanId: id }),
|
||||
addActivePlan: (plan) => {
|
||||
const s = get();
|
||||
set({ activePlans: [...s.activePlans, plan] });
|
||||
},
|
||||
removePlan: (id) => {
|
||||
const s = get();
|
||||
set({ activePlans: s.activePlans.filter(p => p.id !== id), currentPlanId: s.currentPlanId === id ? null : s.currentPlanId });
|
||||
},
|
||||
|
||||
setEnvironment: (env) => set(s => ({ currentPlanId: s.currentPlanId })),
|
||||
|
||||
setChecklistCompleted: (val) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, checklistCompleted: val } : p) });
|
||||
},
|
||||
|
||||
addConfluence: (c) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, confluences: [...p.confluences, c] } : p) });
|
||||
},
|
||||
|
||||
removeConfluence: (c) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, confluences: p.confluences.filter(x => x !== c) } : p) });
|
||||
},
|
||||
|
||||
setConfluences: (cs) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, confluences: cs } : p) });
|
||||
},
|
||||
|
||||
setDevilAdvocate: (text) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, devilAdvocate: text } : p) });
|
||||
},
|
||||
|
||||
recalcTierAndRisk: () => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? {
|
||||
...p,
|
||||
confluenceScore: p.confluences.length * 10,
|
||||
tier: baseTier(p.confluences, p.devilAdvocate),
|
||||
riskPercent: TIERS[baseTier(p.confluences, p.devilAdvocate)],
|
||||
} : p) });
|
||||
},
|
||||
|
||||
setJustification: (field, text) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, justifications: { ...p.justifications, [field]: text } } : p) });
|
||||
},
|
||||
|
||||
addTarget: (price, reason) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, targets: [...p.targets, { price, reason }] } : p) });
|
||||
},
|
||||
|
||||
removeTarget: (idx) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, targets: p.targets.filter((_, i) => i !== idx) } : p) });
|
||||
},
|
||||
|
||||
setEntryPrice: (price) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, entryPrice: price } : p) });
|
||||
},
|
||||
|
||||
setStopLoss: (price) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, stopLoss: price } : p) });
|
||||
},
|
||||
|
||||
setStatus: (status) => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
set({ activePlans: s.activePlans.map(p => p.id === s.currentPlanId ? { ...p, status } : p) });
|
||||
},
|
||||
|
||||
advanceStatus: () => {
|
||||
const s = get();
|
||||
if (!s.currentPlanId) return;
|
||||
const p = s.activePlans.find(x => x.id === s.currentPlanId);
|
||||
if (!p) return;
|
||||
const next: Record<string, TradePlan['status']> = { planned: 'active', active: 'closed' };
|
||||
set({ activePlans: s.activePlans.map(x => x.id === s.currentPlanId ? { ...x, status: next[p.status] || p.status } : x) });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,168 @@
|
||||
// --- Enums ---
|
||||
|
||||
export type WeeklyBias = 'bullish' | 'bearish' | 'neutral';
|
||||
export type DailyBias = 'trend_up' | 'trend_down' | 'range_bound' | 'neutral';
|
||||
export type TrendAlignment = 'pro_trend' | 'counter_trend' | 'neutral';
|
||||
export type TradeEnvironment = 'trending' | 'ranging' | 'transitioning';
|
||||
export type TradeStatus = 'planned' | 'active' | 'closed';
|
||||
export type SetupTier = 'A_STAR' | 'A' | 'B' | 'C';
|
||||
|
||||
export type ExecutionPlaybook = 'mean_reversion' | 'range_extremity';
|
||||
export type MistakeStage = 'analysis' | 'planning' | 'execution' | 'management' | 'closure';
|
||||
|
||||
// --- Market Outlook Types ---
|
||||
|
||||
export interface KeyLevel {
|
||||
id: string;
|
||||
type: 'swing_high' | 'swing_low' | 'supply' | 'demand' | 'support' | 'resistance';
|
||||
price: number;
|
||||
timeFrame: string;
|
||||
strength: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface SetupOnRadar {
|
||||
id: string;
|
||||
symbol: string;
|
||||
setupType: 'mean_reversion' | 'range_extreme';
|
||||
price: number;
|
||||
confluenceCount: number;
|
||||
status: 'watching' | 'active';
|
||||
}
|
||||
|
||||
export interface HighImpactEvent {
|
||||
id: string;
|
||||
name: string;
|
||||
time: Date;
|
||||
impact: 'high' | 'medium' | 'low';
|
||||
forecast: string;
|
||||
previous: string;
|
||||
}
|
||||
|
||||
export interface MarketOutlookState {
|
||||
weeklyBias: WeeklyBias;
|
||||
dailyBias: DailyBias;
|
||||
trendAlignment: TrendAlignment;
|
||||
setupsOnRadar: SetupOnRadar[];
|
||||
highImpactEvents: HighImpactEvent[];
|
||||
keyLevels: KeyLevel[];
|
||||
checklistCompleted: boolean;
|
||||
}
|
||||
|
||||
// --- Goals Types ---
|
||||
|
||||
export interface MonthlyGoal {
|
||||
id: string;
|
||||
description: string;
|
||||
completed: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface WeeklyGoal {
|
||||
id: string;
|
||||
monthlyGoalId: string;
|
||||
description: string;
|
||||
completed: boolean;
|
||||
weekStarting: Date;
|
||||
}
|
||||
|
||||
export interface DailyTask {
|
||||
id: string;
|
||||
description: string;
|
||||
completed: boolean;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
// --- Trade Plan Types ---
|
||||
|
||||
export type ConfluenceType = 'divergence' | 'ema' | 'composite' | 'single_print';
|
||||
|
||||
export interface TradeJustification {
|
||||
entryWhy: string;
|
||||
stopWhy: string;
|
||||
targetWhy: string;
|
||||
}
|
||||
|
||||
export interface ScaleEntry {
|
||||
id: string;
|
||||
price: number;
|
||||
sizePercent: number;
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
export interface TradePlan {
|
||||
id: string;
|
||||
tradeId: string;
|
||||
date: Date;
|
||||
status: TradeStatus;
|
||||
environment: TradeEnvironment;
|
||||
tier: SetupTier;
|
||||
confluenceScore: number;
|
||||
riskPercent: number;
|
||||
|
||||
// Confluences
|
||||
confluences: ConfluenceType[];
|
||||
devilAdvocate: string;
|
||||
|
||||
// Sizing
|
||||
positionSize: number;
|
||||
entryPrice?: number;
|
||||
stopLoss?: number;
|
||||
targets: { price: number; reason: string }[];
|
||||
|
||||
// Justifications
|
||||
justifications: TradeJustification;
|
||||
|
||||
// Checklist
|
||||
checklistCompleted: boolean;
|
||||
}
|
||||
|
||||
// --- Execution Types ---
|
||||
|
||||
export interface ExecutionRecord {
|
||||
tradePlanId: string;
|
||||
executionPlaybook: ExecutionPlaybook;
|
||||
checklistCompleted: boolean;
|
||||
scaleEntries: ScaleEntry[];
|
||||
actualEntryTime?: Date;
|
||||
plannedEntryTime?: Date;
|
||||
}
|
||||
|
||||
// --- Trade Closure Types ---
|
||||
|
||||
export interface EmotionLog {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
priceAtEvent: number;
|
||||
emotion: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface TradeClosureRecord {
|
||||
id: string;
|
||||
executionRecordId: string;
|
||||
|
||||
// Emotion logger
|
||||
emotionLogs: EmotionLog[];
|
||||
|
||||
// Post-trade review
|
||||
followedPlan: boolean;
|
||||
deviationReason?: string;
|
||||
strengths: string[];
|
||||
mistakes: { description: string; stage: MistakeStage }[];
|
||||
|
||||
// Diary
|
||||
diary: string;
|
||||
|
||||
postReviewTimestamp?: Date;
|
||||
}
|
||||
|
||||
// --- KPI Types ---
|
||||
|
||||
export interface DashboardKPI {
|
||||
winRate: number;
|
||||
avgConfluenceScore: number;
|
||||
executionAdherence: number;
|
||||
topMistakeStage: MistakeStage | null;
|
||||
currentStreak: number;
|
||||
totalTrades: number;
|
||||
}
|
||||
Reference in New Issue
Block a user