fix (ornith-35): watchlistRepository double-encoding bug — single JSON.stringify, 13/13 tests pass

This commit is contained in:
Investor Flow Build
2026-06-30 17:54:01 -04:00
parent 97607e0bd4
commit 1007ab4ed5
62 changed files with 11617 additions and 34 deletions
+233
View File
@@ -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;
}
}
+414
View File
@@ -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

+404
View File
@@ -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>
);
}
+65
View File
@@ -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) }),
}));
+127
View File
@@ -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 });
}
});
+76
View File
@@ -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);
},
}));
+84
View File
@@ -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));
+178
View File
@@ -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) });
},
}));
+168
View File
@@ -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;
}