feat: Add Phase 4 advanced metrics and components

- Add advanced metrics dashboard with trade analytics
- Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.)
- Add strategy mode selector and trend confirmation
- Add risk automation panel and slippage correlation analysis
- Add daily trading plan enhancements with modal components
- Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics)
- Add broker service integration and trading API
- Add test setup and vitest configuration
- Include parquet data files for live market data
- Add comprehensive documentation in docs/ folder
This commit is contained in:
Krikorios
2025-11-27 10:23:58 +02:00
parent b5e2b02cb8
commit 48e60d015f
2019 changed files with 39793 additions and 257 deletions
+215
View File
@@ -0,0 +1,215 @@
// INSTRUCTIONS: Replace handleBuy, handleSell, and handleReset in App.tsx with these versions
// Also add loadPortfolioFromBackend and call it in useEffect
import { executeTradeAPI, getPortfolioAPI, resetSimulationAPI, convertBackendPortfolio } from './services/tradingAPI'
// Add this state at the top of App component
const [isLoadingTrade, setIsLoadingTrade] = useState(false)
// Add this function to load portfolio on mount
const loadPortfolioFromBackend = useCallback(async () => {
try {
const backendPortfolio = await getPortfolioAPI()
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
setPortfolio(converted)
console.log('✅ Portfolio loaded from backend:', converted)
} catch (error) {
console.error('Failed to load portfolio from backend:', error)
// Fall back to default portfolio
setPortfolio(recalcPortfolio(createInitialPortfolio(), currentPrice))
}
}, [currentPrice])
// Add this useEffect to load on mount
useEffect(() => {
if (syncToBackend) {
loadPortfolioFromBackend()
}
}, []) // Only run once on mount
// REPLACE handleBuy with this version
const handleBuy = useCallback(async (quantity: number) => {
if (quantity <= 0 || Number.isNaN(quantity)) return
if (!syncToBackend) {
// Original in-memory logic (keep for backward compatibility)
setPortfolio((prev) => {
const cost = quantity * currentPrice
if (cost > prev.cash) {
alert('Insufficient cash for this order')
return prev
}
const existing = prev.position
const totalQuantity = existing ? existing.quantity + quantity : quantity
const avgPrice = existing ? ((existing.avgPrice * existing.quantity + currentPrice * quantity) / totalQuantity) : currentPrice
const trade: Trade = {
id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
timestamp: Date.now(),
action: 'BUY',
quantity,
price: currentPrice,
total: Number(cost.toFixed(2)),
}
const updated: Portfolio = {
...prev,
cash: Number((prev.cash - cost).toFixed(2)),
position: {
symbol: TRADING_SYMBOL,
quantity: Number(totalQuantity.toFixed(4)),
avgPrice: Number(avgPrice.toFixed(2)),
currentPrice,
unrealizedPnl: 0,
unrealizedPnlPercent: 0,
},
trades: [trade, ...prev.trades].slice(0, 200),
}
return recalcPortfolio(updated, currentPrice)
})
return
}
// NEW: Backend-persisted logic
setIsLoadingTrade(true)
try {
const response = await executeTradeAPI({
action: 'BUY',
quantity,
price: currentPrice,
symbol: TRADING_SYMBOL
})
// Reload portfolio from backend to ensure sync
const backendPortfolio = await getPortfolioAPI()
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
setPortfolio(converted)
console.log('✅ BUY trade executed and synced:', response.trade)
} catch (error: any) {
console.error('❌ Trade execution failed:', error)
if (error.response?.data?.detail) {
alert(`Trade failed: ${error.response.data.detail}`)
} else {
alert('Trade execution failed. Please try again.')
}
} finally {
setIsLoadingTrade(false)
}
}, [currentPrice, syncToBackend])
// REPLACE handleSell with this version
const handleSell = useCallback(async (quantity: number, reason = 'Manual exit') => {
if (!syncToBackend) {
// Original in-memory logic (keep for backward compatibility)
setPortfolio((prev) => {
const position = prev.position
if (!position) {
alert('No open position to close')
return prev
}
const size = Math.min(quantity, position.quantity)
if (size <= 0) return prev
const proceeds = size * currentPrice
const pnl = (currentPrice - position.avgPrice) * size
const trade: Trade = {
id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
timestamp: Date.now(),
action: 'SELL',
quantity: size,
price: currentPrice,
total: Number(proceeds.toFixed(2)),
pnl: Number(pnl.toFixed(2)),
}
const remainingQty = Number((position.quantity - size).toFixed(4))
const nextPosition = remainingQty > 0.0001
? { ...position, quantity: remainingQty, currentPrice }
: null
const updated: Portfolio = {
...prev,
cash: Number((prev.cash + proceeds).toFixed(2)),
position: nextPosition,
trades: [trade, ...prev.trades].slice(0, 200),
}
if (reason.startsWith('Auto')) {
console.info(reason)
}
return recalcPortfolio(updated, currentPrice)
})
if (!reason.startsWith('Manual')) {
setAiAnalysis(null)
}
return
}
// NEW: Backend-persisted logic
setIsLoadingTrade(true)
try {
// Check if we have a position
const currentPortfolio = await getPortfolioAPI()
if (!currentPortfolio.position) {
alert('No open position to close')
return
}
const size = Math.min(quantity, currentPortfolio.position.quantity)
const response = await executeTradeAPI({
action: 'SELL',
quantity: size,
price: currentPrice,
symbol: TRADING_SYMBOL,
notes: reason
})
// Reload portfolio from backend to ensure sync
const backendPortfolio = await getPortfolioAPI()
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
setPortfolio(converted)
console.log('✅ SELL trade executed and synced:', response.trade)
if (reason.startsWith('Auto')) {
console.info(reason)
}
} catch (error: any) {
console.error('❌ Trade execution failed:', error)
if (error.response?.data?.detail) {
alert(`Trade failed: ${error.response.data.detail}`)
} else {
alert('Trade execution failed. Please try again.')
}
} finally {
setIsLoadingTrade(false)
}
if (!reason.startsWith('Manual')) {
setAiAnalysis(null)
}
}, [currentPrice, syncToBackend])
// REPLACE handleReset with this version
const handleReset = useCallback(async () => {
if (!syncToBackend) {
// Original in-memory logic
setPortfolio(recalcPortfolio(createInitialPortfolio(), currentPrice))
setAiAnalysis(null)
return
}
// NEW: Backend-persisted logic
setIsLoadingTrade(true)
try {
const response = await resetSimulationAPI()
const converted = convertBackendPortfolio(response.portfolio, currentPrice)
setPortfolio(converted)
setAiAnalysis(null)
console.log('✅ Simulation reset and synced')
} catch (error) {
console.error('❌ Reset failed:', error)
alert('Failed to reset simulation. Please try again.')
} finally {
setIsLoadingTrade(false)
}
}, [currentPrice, syncToBackend])
// OPTIONAL: Add loading indicator in your Trade panel
// Show a spinner or disable buttons when isLoadingTrade is true
+428
View File
@@ -0,0 +1,428 @@
# Phase 1 Refactoring - Testing Checklist
## Build & Compilation Tests
### TypeScript Compilation
- [ ] Run `npm run build` - should complete without errors
- [ ] No TypeScript errors in IDE
- [ ] No unused imports warnings
- [ ] All paths resolve correctly (`@/` aliases work)
### Development Server
- [ ] Run `npm run dev` - server starts successfully
- [ ] No console errors on page load
- [ ] Hot reload works after file changes
---
## DailyTradingPlan Component Tests
### Basic Functionality
- [ ] Component renders without errors
- [ ] Plan date displays current date
- [ ] Default plan values load correctly
- [ ] Component layout looks correct (no broken styles)
### Edit Mode
- [ ] Click "Edit" button - enters edit mode
- [ ] Input fields become editable
- [ ] Can modify bias (Bullish/Neutral/Bearish)
- [ ] Can change daily target value
- [ ] Can change max loss value
- [ ] Can change max trades value
- [ ] Can modify entry zone min/max
- [ ] Can modify target price
- [ ] Can modify stop loss
- [ ] Can edit trading notes
- [ ] Click "Save" - exits edit mode
- [ ] Changes persist after save
### Key Levels Management
- [ ] Support levels display correctly
- [ ] Resistance levels display correctly
- [ ] Click "Add" on Support - new level added
- [ ] Click "Add" on Resistance - new level added
- [ ] Can edit individual support levels
- [ ] Can edit individual resistance levels
- [ ] Click "X" removes support level
- [ ] Click "X" removes resistance level
- [ ] Key levels save correctly
### AI Plan Generation
- [ ] Click "AI Plan" button
- [ ] Button shows "Generating..." during load
- [ ] Success modal appears after generation
- [ ] Modal shows generated bias and target
- [ ] Plan updates with AI-generated values
- [ ] Support/resistance levels update
- [ ] Trading notes populate (if provided)
- [ ] Context metrics saved (if available)
- [ ] Can close success modal
- [ ] Error shows if OpenRouter API key missing
- [ ] Error displays clearly in UI (not just console)
### Reset Functionality
- [ ] Click "Reset" button
- [ ] Confirmation modal appears
- [ ] Modal title says "Reset Plan"
- [ ] Modal explains action clearly
- [ ] Click "Cancel" - closes modal, no changes
- [ ] Click "Reset" again, then "Confirm" - plan resets
- [ ] Plan returns to default values
- [ ] Date updates to today
- [ ] Edit mode turns off after reset
### LocalStorage Persistence
- [ ] Make changes to plan
- [ ] Refresh page
- [ ] Changes persist after reload
- [ ] Plan date remains same
- [ ] All fields retain values
- [ ] Clear browser localStorage
- [ ] Refresh page
- [ ] Default plan loads
- [ ] Old date triggers new plan creation
- [ ] Change system date to tomorrow
- [ ] Reload page
- [ ] New plan created for new date
---
## Modal Components Tests
### ConfirmModal
- [ ] Modal opens on trigger
- [ ] Title displays correctly
- [ ] Message displays correctly
- [ ] "Cancel" button present
- [ ] "Confirm" button present
- [ ] Danger variant shows red button
- [ ] Warning variant shows amber button
- [ ] Info variant shows blue button
- [ ] Click "Cancel" - closes without action
- [ ] Click "Confirm" - executes callback and closes
- [ ] Press Escape key - closes modal
- [ ] Click outside modal - closes modal
- [ ] Focus trapped inside modal
- [ ] Tab navigation works
- [ ] Screen reader announces modal (test with screen reader)
### AlertModal
- [ ] Modal opens on trigger
- [ ] Title displays correctly
- [ ] Message displays correctly (supports multi-line)
- [ ] "OK" button present
- [ ] Success variant shows green button
- [ ] Error variant shows red button
- [ ] Info variant shows blue button
- [ ] Warning variant shows amber button
- [ ] Click "OK" - closes modal
- [ ] Press Escape - closes modal
- [ ] Click outside - closes modal
- [ ] Focus management works
### Modal Accessibility
- [ ] Keyboard navigation works (Tab, Shift+Tab)
- [ ] Escape key closes modal
- [ ] Focus returns to trigger element on close
- [ ] Body scroll disabled when modal open
- [ ] Body scroll restored when modal closes
- [ ] ARIA attributes present (`aria-modal`, `role="dialog"`)
- [ ] Modal has accessible title (`aria-labelledby`)
---
## Hooks Tests
### useLocalStorage Hook
Create a test component to verify:
```typescript
function TestComponent() {
const [value, setValue, removeValue] = useLocalStorage<number>('test-key', 0);
return (
<div>
<p>Value: {value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
<button onClick={removeValue}>Remove</button>
</div>
);
}
```
- [ ] Initial value loads correctly (0)
- [ ] Click "Increment" - value increases
- [ ] Refresh page - value persists
- [ ] Click "Remove" - value resets to default
- [ ] Check localStorage in DevTools - key exists
- [ ] After remove - key deleted from localStorage
- [ ] Invalid JSON in localStorage handled gracefully
- [ ] quota exceeded error handled gracefully
- [ ] Works with complex objects (not just primitives)
- [ ] Function setValue works with callback `setValue(prev => prev + 1)`
### useApi Hook
Create a test component:
```typescript
function TestComponent() {
const { data, loading, error, execute } = useApi(
async () => {
const res = await fetch('/api/test');
return res.json();
}
);
return (
<div>
<button onClick={execute}>Load</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && <pre>{JSON.stringify(data)}</pre>}
</div>
);
}
```
- [ ] Initial state: loading=false, error=null, data=null
- [ ] Click "Load" - loading becomes true
- [ ] After success - loading=false, data populated
- [ ] After error - loading=false, error populated
- [ ] Unmount component during load - no console warnings
- [ ] Request cancelled on unmount
- [ ] Multiple rapid clicks handled correctly
- [ ] onSuccess callback fires
- [ ] onError callback fires
- [ ] Reset function works
---
## Formatting Utilities Tests
Test in browser console:
```javascript
import { formatCurrency, formatPercent, formatNumber, formatPriceChange } from '@/utils/indicators';
// Currency
console.assert(formatCurrency(1234.56) === '$1,234.56');
console.assert(formatCurrency(null) === '—');
console.assert(formatCurrency(undefined) === '—');
console.assert(formatCurrency(NaN) === '—');
// Percent
console.assert(formatPercent(5.25) === '+5.25%');
console.assert(formatPercent(-2.5) === '-2.50%');
console.assert(formatPercent(0) === '0.00%');
console.assert(formatPercent(null) === '—');
// Number
console.assert(formatNumber(1234.567) === '1,234.57');
console.assert(formatNumber(1234.567, { maximumFractionDigits: 0 }) === '1,235');
// Price change
const change1 = formatPriceChange(5.25);
console.assert(change1.text === '+5.25');
console.assert(change1.color === 'text-green-400');
const change2 = formatPriceChange(-2.5);
console.assert(change2.text === '-2.50');
console.assert(change2.color === 'text-red-400');
```
Checklist:
- [ ] All formatCurrency assertions pass
- [ ] All formatPercent assertions pass
- [ ] All formatNumber assertions pass
- [ ] All formatPriceChange assertions pass
- [ ] Functions handle edge cases (null, undefined, NaN)
- [ ] No TypeScript errors when using functions
- [ ] Custom placeholders work
- [ ] Custom options work
---
## App.tsx Integration Tests
### Component Import
- [ ] App imports DailyTradingPlan from new location
- [ ] No import errors in App.tsx
- [ ] App compiles successfully
- [ ] App runs without errors
### Prep Tab
- [ ] Click "Prep" tab
- [ ] DailyTradingPlan component visible
- [ ] DailyMarketSummary component visible
- [ ] DailyChecklistPanel component visible
- [ ] HabitTracker component visible
- [ ] AlertsPanel component visible
- [ ] NewsFeed component visible
- [ ] All components render without errors
- [ ] No layout issues
### State Synchronization
- [ ] Generate AI plan in DailyTradingPlan
- [ ] onPlanUpdate callback fires (if implemented)
- [ ] Parent component receives updated plan
- [ ] Other components can access plan data (if needed)
---
## Browser Compatibility Tests
Test in multiple browsers:
### Chrome/Edge
- [ ] All functionality works
- [ ] Modals display correctly
- [ ] No console errors
- [ ] localStorage works
### Firefox
- [ ] All functionality works
- [ ] Modals display correctly
- [ ] No console errors
- [ ] localStorage works
### Safari
- [ ] All functionality works
- [ ] Modals display correctly
- [ ] No console errors
- [ ] localStorage works
---
## Performance Tests
### Bundle Size
- [ ] Run `npm run build`
- [ ] Check bundle size in dist folder
- [ ] No significant increase from refactoring
- [ ] Code splitting working correctly
### Runtime Performance
- [ ] Open Chrome DevTools Performance tab
- [ ] Record page load
- [ ] No long tasks (>50ms)
- [ ] No layout thrashing
- [ ] Component renders efficiently
### Memory Leaks
- [ ] Open Chrome DevTools Memory tab
- [ ] Take heap snapshot
- [ ] Open/close DailyTradingPlan multiple times
- [ ] Take another heap snapshot
- [ ] Compare - no significant increase
- [ ] Listeners properly cleaned up
---
## Accessibility Tests
### Keyboard Navigation
- [ ] Tab through all interactive elements
- [ ] Focus visible on all elements
- [ ] Can open modal with keyboard
- [ ] Can navigate modal with keyboard
- [ ] Can close modal with keyboard (Escape)
- [ ] Focus trap works in modals
- [ ] Skip to content link works (if implemented)
### Screen Reader
Test with NVDA (Windows) or VoiceOver (Mac):
- [ ] Component announces correctly
- [ ] Buttons have clear labels
- [ ] Form fields have labels
- [ ] Error messages announced
- [ ] Modal role announced
- [ ] Modal title announced
- [ ] Status messages announced (loading, success)
### Color Contrast
Use browser DevTools accessibility checker:
- [ ] All text meets WCAG AA contrast ratio (4.5:1)
- [ ] Buttons meet contrast requirements
- [ ] Error messages readable
- [ ] Success messages readable
---
## Edge Cases & Error Handling
### Network Errors
- [ ] Disconnect network
- [ ] Try AI plan generation
- [ ] Error displayed to user (not just console)
- [ ] Can retry after network restored
- [ ] Other features still work
### API Errors
- [ ] API returns 500 error
- [ ] Error caught and displayed
- [ ] User-friendly error message shown
- [ ] Can recover from error
### Invalid Data
- [ ] Enter negative numbers in plan fields
- [ ] Enter non-numeric values
- [ ] Enter extremely large numbers
- [ ] Application handles gracefully
- [ ] Validation prevents invalid state
### LocalStorage Quota
- [ ] Fill localStorage to quota
- [ ] Try to save plan
- [ ] Error handled gracefully
- [ ] User notified of issue
- [ ] Application doesn't crash
---
## Regression Tests
Ensure existing functionality still works:
- [ ] LiveMarketPanel displays correctly
- [ ] MultiChartSSEPanel works
- [ ] Trade controls function
- [ ] Portfolio tracker updates
- [ ] All tabs navigate correctly
- [ ] Settings panel accessible
- [ ] AI Coach works
- [ ] ML Patterns display
- [ ] No features broken by refactoring
---
## Documentation Tests
- [ ] WEEK1_2_REFACTORING_SUMMARY.md is complete and accurate
- [ ] REFACTORING_QUICK_START.md provides clear examples
- [ ] Code comments are clear and helpful
- [ ] TypeScript interfaces documented
- [ ] Complex functions have JSDoc comments
---
## Cleanup Verification
- [ ] Old DailyTradingPlan.tsx file can be removed
- [ ] No unused imports in codebase
- [ ] No commented-out code left behind
- [ ] No debug console.logs in production code
- [ ] No TODO comments left unresolved
---
## Sign-off
Once all tests pass:
- [ ] Phase 1 refactoring complete
- [ ] No regressions introduced
- [ ] All new features working
- [ ] Documentation updated
- [ ] Ready for Phase 2
**Tested by:** _______________
**Date:** _______________
**Notes:** _______________________________________________
+393
View File
@@ -0,0 +1,393 @@
# Frontend Refactoring Quick Start Guide
## 🚀 What Changed?
Your frontend has been refactored to improve maintainability, type safety, and code organization. Here's what you need to know.
---
## 📦 New Shared Utilities
### 1. useLocalStorage Hook
**Import:**
```typescript
import { useLocalStorage } from '@/hooks';
```
**Usage:**
```typescript
// Instead of managing localStorage manually...
const [value, setValue, removeValue] = useLocalStorage<MyType>('storage-key', defaultValue);
// Works just like useState, but persists automatically!
setValue({ ...value, updated: true });
```
**Features:**
- Type-safe
- Automatic JSON serialization
- Error handling built-in
- Returns remove function as third element
---
### 2. useApi Hook
**Import:**
```typescript
import { useApi } from '@/hooks';
```
**Usage:**
```typescript
// Manages loading, error, and data states automatically
const { data, loading, error, execute } = useApi(
(id: number) => apiService.getData(id),
{
onSuccess: (data) => console.log('Success!', data),
onError: (error) => console.error('Error:', error)
}
);
// Later in your component...
<button onClick={() => execute(123)}>Load Data</button>
{loading && <Spinner />}
{error && <ErrorMessage>{error}</ErrorMessage>}
{data && <DataDisplay data={data} />}
```
**Features:**
- Automatic request cancellation on unmount
- Prevents memory leaks
- Consistent loading/error patterns
---
### 3. Formatting Utilities
**Import:**
```typescript
import { formatCurrency, formatPercent, formatNumber, formatPriceChange } from '@/utils/indicators';
```
**Usage:**
```typescript
// Currency formatting (handles null/undefined)
formatCurrency(1234.56) // "$1,234.56"
formatCurrency(null) // "—"
// Percentage with sign
formatPercent(5.25) // "+5.25%"
formatPercent(-2.5) // "-2.50%"
// Number formatting
formatNumber(1234.567) // "1,234.57"
// Price change with color
const { text, color } = formatPriceChange(5.25);
// { text: "+5.25", color: "text-green-400" }
```
**Replace these patterns:**
```typescript
// ❌ Old way
const formatNumber = (value?: number | null) => {
if (value === undefined || value === null) return '—';
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(value);
};
// ✅ New way
import { formatNumber } from '@/utils/indicators';
```
---
### 4. Modal Components
**Import:**
```typescript
import { Modal, ConfirmModal, AlertModal } from '@/components/shared/Modal';
```
**Replace `confirm()`:**
```typescript
// ❌ Old way
if (confirm('Are you sure you want to delete this?')) {
handleDelete();
}
// ✅ New way
const [showConfirm, setShowConfirm] = useState(false);
<ConfirmModal
isOpen={showConfirm}
onClose={() => setShowConfirm(false)}
onConfirm={handleDelete}
title="Delete Item"
message="Are you sure you want to delete this?"
variant="danger"
/>
```
**Replace `alert()`:**
```typescript
// ❌ Old way
alert('Success! Your changes have been saved.');
// ✅ New way
const [showAlert, setShowAlert] = useState(false);
<AlertModal
isOpen={showAlert}
onClose={() => setShowAlert(false)}
title="Success"
message="Your changes have been saved."
variant="success"
/>
```
**Custom Modal:**
```typescript
<Modal isOpen={open} onClose={() => setOpen(false)} title="Custom Dialog" size="lg">
<p>Your custom content here</p>
<div className="flex gap-2 mt-4">
<button onClick={handleAction}>Action</button>
<button onClick={() => setOpen(false)}>Cancel</button>
</div>
</Modal>
```
**Modal Props:**
- `size`: 'sm' | 'md' | 'lg' | 'xl'
- `showCloseButton`: boolean (default: true)
- `closeOnEscape`: boolean (default: true)
- `closeOnBackdropClick`: boolean (default: true)
---
## 🏗️ New Component Structure
### DailyTradingPlan Refactored
**Location changed:**
```typescript
// ❌ Old import
import DailyTradingPlan from './components/DailyTradingPlan';
// ✅ New import
import DailyTradingPlan from './components/features/trading/DailyTradingPlan';
```
**Props unchanged** - no breaking changes!
**Internal structure:**
```
DailyTradingPlan/
├── index.tsx # Main container
├── types.ts # TypeScript interfaces
├── usePlanGeneration.ts # AI logic hook
├── PlanHeader.tsx # Sub-component
├── PlanBiasSelector.tsx # Sub-component
├── PlanRiskParameters.tsx # Sub-component
└── PlanKeyLevelsEditor.tsx # Sub-component
```
---
## 🎨 Patterns to Follow
### 1. Component Organization
```
components/
├── features/ # Feature-specific components
│ ├── trading/
│ ├── analytics/
│ ├── ai/
│ └── journal/
├── shared/ # Reusable UI components
│ ├── Modal.tsx
│ ├── Button.tsx
│ └── Input.tsx
└── layout/ # Layout components
└── DashboardLayout.tsx
```
### 2. Component File Structure
For complex components, create a directory:
```
MyComponent/
├── index.tsx # Main container
├── types.ts # TypeScript interfaces
├── useMyLogic.ts # Custom hooks
├── SubComponentA.tsx # Sub-component
└── SubComponentB.tsx # Sub-component
```
Export from `index.tsx`:
```typescript
export default function MyComponent() { ... }
export type { MyComponentProps } from './types';
```
### 3. Custom Hooks Pattern
```typescript
// useMyFeature.ts
import { useState, useCallback } from 'react';
export interface UseMyFeatureReturn {
data: MyData | null;
loading: boolean;
error: string | null;
execute: () => Promise<void>;
reset: () => void;
}
export function useMyFeature(): UseMyFeatureReturn {
const [data, setData] = useState<MyData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetchData();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}, []);
const reset = useCallback(() => {
setData(null);
setError(null);
}, []);
return { data, loading, error, execute, reset };
}
```
### 4. TypeScript Best Practices
```typescript
// ✅ Do: Explicit interfaces
interface MyComponentProps {
value: number;
onChange: (value: number) => void;
label?: string;
}
// ✅ Do: Explicit return types
function MyComponent({ value, onChange, label }: MyComponentProps): JSX.Element {
const handleChange = useCallback((newValue: number): void => {
onChange(newValue);
}, [onChange]);
return <div>...</div>;
}
// ❌ Don't: Use 'any'
const [data, setData] = useState<any>(null); // Bad!
// ✅ Do: Use proper types
interface MyData {
id: number;
name: string;
}
const [data, setData] = useState<MyData | null>(null); // Good!
```
---
## 🔧 Migration Checklist
When refactoring a component:
- [ ] Move to appropriate feature directory
- [ ] Extract types to `types.ts`
- [ ] Extract business logic to custom hooks
- [ ] Split into sub-components (if > 250 lines)
- [ ] Replace `localStorage` patterns with `useLocalStorage`
- [ ] Replace direct API calls with `useApi`
- [ ] Replace `alert`/`confirm` with Modal components
- [ ] Use formatting utilities from `@/utils/indicators`
- [ ] Remove all `any` types
- [ ] Add explicit return types
- [ ] Wrap callbacks in `useCallback`
- [ ] Wrap expensive calculations in `useMemo`
- [ ] Add ARIA attributes for accessibility
- [ ] Update imports in parent components
---
## 🧪 Testing Your Changes
### Quick Smoke Test:
```bash
cd frontend
npm run build
```
Should compile without TypeScript errors!
### Runtime Test:
```bash
npm run dev
```
Check:
1. Component loads without errors
2. State updates work
3. localStorage persists
4. Modals open/close correctly
5. No console warnings
---
## 📚 Additional Resources
- [React Hooks Documentation](https://react.dev/reference/react)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
---
## ❓ FAQ
**Q: Can I still use the old DailyTradingPlan import?**
A: No, update imports to the new location. The old file will be removed.
**Q: Do I need to refactor all components at once?**
A: No! Refactor incrementally. Start with components you're actively working on.
**Q: What if I need localStorage outside a component?**
A: Create a separate utility function or use the hook in the nearest parent component.
**Q: Should I use ConfirmModal for every confirmation?**
A: Yes! It provides better UX and accessibility than native `confirm()`.
**Q: Can I customize Modal appearance?**
A: Yes! The Modal component uses Tailwind classes. Extend or override as needed.
---
## 🆘 Getting Help
If you encounter issues:
1. Check TypeScript errors in your IDE
2. Review the summary document: `WEEK1_2_REFACTORING_SUMMARY.md`
3. Look at refactored DailyTradingPlan as an example
4. Ask the team!
---
**Last Updated:** Week 1-2 Refactoring
**Next Update:** Week 3-4 (TradingJournal & AITradingCoach refactoring)
+313
View File
@@ -0,0 +1,313 @@
import { useEffect, useState, useCallback } from 'react'
import { BarChart3, Activity, BookOpen, Settings, Brain, LogOut } from 'lucide-react'
// Components
import LiveMarketPanel from './components/LiveMarketPanel'
import MultiChartSSEPanel from './components/MultiChartSSEPanel'
import NotificationCenter from './components/NotificationCenter'
import UserProfileSetup from './components/UserProfileSetup'
import HabitTracker from './components/HabitTracker'
import DailyChecklistPanel from './components/DailyChecklistPanel'
import AIAnalysisPanel from './components/AIAnalysisPanel'
import DailyTradingPlan from './components/DailyTradingPlan'
import RiskManagement from './components/RiskManagement'
import TradingJournal from './components/TradingJournal'
import DailyMarketSummary from './components/DailyMarketSummary'
import NewsFeed from './components/NewsFeed'
import AlertsPanel from './components/AlertsPanel'
import SettingsPanel from './components/SettingsPanel'
import PromptTemplatesPanel from './components/PromptTemplatesPanel'
import EquityPerformancePanel from './components/EquityPerformancePanel'
import AdvancedAnalytics from './components/AdvancedAnalytics'
// Types
type MainView = 'Dashboard' | 'Trade' | 'Journal' | 'AICoach' | 'Settings'
interface NavItem {
id: MainView
label: string
icon: React.ReactNode
description: string
}
// Navigation configuration
const NAV_ITEMS: NavItem[] = [
{
id: 'Dashboard',
label: 'Dashboard',
icon: <BarChart3 className="w-5 h-5" />,
description: 'Market overview & morning prep'
},
{
id: 'Trade',
label: 'Trade',
icon: <Activity className="w-5 h-5" />,
description: 'Live execution & analysis'
},
{
id: 'Journal',
label: 'Journal',
icon: <BookOpen className="w-5 h-5" />,
description: 'Trading journal & analytics'
},
{
id: 'AICoach',
label: 'AI Coach',
icon: <Brain className="w-5 h-5" />,
description: 'AI insights & coaching'
},
{
id: 'Settings',
label: 'Settings',
icon: <Settings className="w-5 h-5" />,
description: 'Preferences & configuration'
},
]
/**
* Clean sticky navigation bar
*/
function NavigationBar({
active,
onNavigate
}: {
active: MainView
onNavigate: (view: MainView) => void
}) {
return (
<nav className="sticky top-0 z-40 border-b border-slate-800 bg-slate-950/95 backdrop-blur">
<div className="max-w-7xl mx-auto px-6 py-3 flex items-center justify-between">
{/* Branding */}
<div className="flex items-center gap-3">
<div className="rounded-lg bg-amber-500/10 p-2">
<BarChart3 className="w-5 h-5 text-amber-500" />
</div>
<div>
<h1 className="text-sm font-bold text-white">Market Simulator</h1>
<p className="text-xs text-slate-400">AI-Powered Trading</p>
</div>
</div>
{/* Nav items */}
<div className="flex items-center gap-1">
{NAV_ITEMS.map((item) => (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
title={item.description}
className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-sm font-medium ${
active === item.id
? 'bg-amber-500/20 text-amber-200 border border-amber-500/40'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
{item.icon}
<span className="hidden md:inline">{item.label}</span>
</button>
))}
</div>
{/* Right side actions */}
<div className="flex items-center gap-2">
<NotificationCenter />
<button
type="button"
className="p-2 rounded-lg hover:bg-slate-800 text-slate-400 transition"
title="Logout"
>
<LogOut className="w-4 h-4" />
</button>
</div>
</div>
</nav>
)
}
/**
* Dashboard view - Morning prep and market overview
*/
function DashboardView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Good Morning, Trader</h2>
<p className="text-sm text-slate-300">
Review your trading plan for today, check the market conditions, and prepare your checklist.
</p>
</div>
<div className="grid gap-6 xl:grid-cols-[1.4fr,1fr]">
<div className="space-y-6">
<DailyTradingPlan currentPrice={4084.99} onPlanUpdate={() => {}} />
<DailyMarketSummary currentPrice={4084.99} />
</div>
<div className="space-y-6">
<DailyChecklistPanel checklistType="morning" />
<HabitTracker />
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<AlertsPanel />
<NewsFeed />
</div>
</div>
)
}
/**
* Trade view - Live execution cockpit
*/
function TradeView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Live Trading Cockpit</h2>
<p className="text-sm text-slate-300">
Execute trades, monitor risk, review AI analysis, and track your portfolio in real-time.
</p>
</div>
<div className="grid gap-6 xl:grid-cols-[1.4fr,1fr]">
{/* Left side - Analysis & Execution */}
<div className="space-y-6">
<LiveMarketPanel />
<MultiChartSSEPanel />
<div className="grid gap-6 lg:grid-cols-2">
{/* Trade controls would go here */}
<RiskManagement
currentPrice={4084.99}
cash={100000}
position={null}
trades={[]}
/>
</div>
<AIAnalysisPanel analysis={null} isLoading={false} />
</div>
{/* Right side - Portfolio & Automation */}
<div className="space-y-6">
{/* Portfolio tracker would go here */}
</div>
</div>
</div>
)
}
/**
* Journal view - Trading journal and analytics
*/
function JournalView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Trading Journal & Analysis</h2>
<p className="text-sm text-slate-300">
Review your trades, track performance, identify patterns, and improve your trading strategy.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<TradingJournal />
<EquityPerformancePanel />
</div>
<AdvancedAnalytics portfolio={{ cash: 100000, initialCapital: 100000, totalValue: 100000, totalPnl: 0, totalPnlPercent: 0, position: null, trades: [] }} trades={[]} />
</div>
)
}
/**
* AI Coach view - Consolidated AI features and analysis
*/
function AICoachView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Analysis & Insights</h2>
<p className="text-sm text-slate-300">
Get AI-powered analysis and insights to improve your trading decisions.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<AIAnalysisPanel analysis={null} isLoading={false} />
</div>
</div>
)
}
/**
* Settings view - Configuration and preferences
*/
function SettingsView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Settings & Configuration</h2>
<p className="text-sm text-slate-300">
Configure your preferences, manage prompts, and customize your trading experience.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<SettingsPanel />
<PromptTemplatesPanel />
</div>
</div>
)
}
/**
* Main App Component
*/
export default function App() {
const [activeView, setActiveView] = useState<MainView>('Dashboard')
const [showProfileSetup, setShowProfileSetup] = useState(false)
// Check backend status on mount
useEffect(() => {
// Status check removed - not critical for UI refactoring
return () => {}
}, [])
// Render active view
const renderActiveView = useCallback(() => {
switch (activeView) {
case 'Dashboard':
return <DashboardView />
case 'Trade':
return <TradeView />
case 'Journal':
return <JournalView />
case 'AICoach':
return <AICoachView />
case 'Settings':
return <SettingsView />
default:
return null
}
}, [activeView])
return (
<div className="min-h-screen bg-slate-950 text-slate-100">
<NavigationBar active={activeView} onNavigate={setActiveView} />
{/* Main content */}
<main className="mx-auto max-w-7xl px-6 py-8">
{renderActiveView()}
</main>
{/* Profile setup modal */}
{showProfileSetup && (
<UserProfileSetup
onClose={() => setShowProfileSetup(false)}
onSaved={() => {
setShowProfileSetup(false)
setActiveView('Dashboard')
}}
/>
)}
</div>
)
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { vi } from 'vitest'
// Mock heavy or path-aliased components so App can render in isolation
const { mockComponent } = vi.hoisted(() => ({
mockComponent: (name = 'div') => ({ default: () => React.createElement(name) }),
}))
vi.mock('./components/FxSymbolSelector', () => mockComponent('div'))
vi.mock('./components/NotificationCenter', () => mockComponent('div'))
vi.mock('./components/AIAnalysisPanel', () => mockComponent('div'))
vi.mock('./components/RiskManagement', () => mockComponent('div'))
vi.mock('./components/RiskAutomationPanel', () => mockComponent('div'))
vi.mock('./components/BrokerBridgePanel', () => mockComponent('div'))
vi.mock('./components/ManualTradeLogger', () => mockComponent('div'))
vi.mock('./components/PortfolioTracker', () => mockComponent('div'))
vi.mock('./components/TradeControls', () => mockComponent('div'))
// Mock lazy-loaded panels
vi.mock('./components/LiveMarketPanel', () => mockComponent('div'))
vi.mock('./components/AccountPositionsPanel', () => mockComponent('div'))
vi.mock('./components/SettingsPanel', () => mockComponent('div'))
vi.mock('./components/PromptTemplatesPanel', () => mockComponent('div'))
vi.mock('./components/UserProfileSetup', () => mockComponent('div'))
vi.mock('./components/HabitTracker', () => mockComponent('div'))
vi.mock('./components/DailyChecklistPanel', () => mockComponent('div'))
vi.mock('./components/DailyTradingPlan', () => mockComponent('div'))
vi.mock('./components/NewsFeed', () => mockComponent('div'))
vi.mock('./components/AlertsPanel', () => mockComponent('div'))
vi.mock('./components/DailyMarketSummary', () => mockComponent('div'))
vi.mock('./components/TradingJournal', () => mockComponent('div'))
vi.mock('./components/AdvancedAnalytics', () => mockComponent('div'))
vi.mock('./components/EconomicCalendar', () => mockComponent('div'))
vi.mock('./components/AITradingCoach', () => mockComponent('div'))
vi.mock('./components/EquityPerformancePanel', () => mockComponent('div'))
vi.mock('./components/EquityCurveChart', () => mockComponent('div'))
vi.mock('./components/BrokerPositionsPanel', () => mockComponent('div'))
vi.mock('./components/DecisionLogPanel', () => mockComponent('div'))
vi.mock('./components/FxQuotesPanel', () => ({
...mockComponent('div'),
DEFAULT_FX_SYMBOLS: ['XAUUSD', 'EURUSD', 'GBPUSD'],
}))
import App from './App'
describe('App', () => {
it('renders the main heading', () => {
render(<App />)
const heading = screen.getByRole('heading', { name: /Assistant Market Simulator/i })
expect(heading).toBeInTheDocument()
})
})
+287 -257
View File
@@ -1,18 +1,13 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback } from 'react'
import { BarChart3, Activity, BookOpen, Settings, Brain, LogOut } from 'lucide-react'
// Components
import LiveMarketPanel from './components/LiveMarketPanel'
import MultiChartSSEPanel from './components/MultiChartSSEPanel'
import AccountPositionsPanel from './components/AccountPositionsPanel'
import SettingsPanel from './components/SettingsPanel'
import PromptTemplatesPanel from './components/PromptTemplatesPanel'
import { statusApi } from './services/api'
// Phase 1: Daily Helper Components
import NotificationCenter from './components/NotificationCenter'
import UserProfileSetup from './components/UserProfileSetup'
import HabitTracker from './components/HabitTracker'
import DailyChecklistPanel from './components/DailyChecklistPanel'
// Analysis & Decision Components
import AIAnalysisPanel from './components/AIAnalysisPanel'
import DailyTradingPlan from './components/DailyTradingPlan'
import RiskManagement from './components/RiskManagement'
@@ -20,264 +15,299 @@ import TradingJournal from './components/TradingJournal'
import DailyMarketSummary from './components/DailyMarketSummary'
import NewsFeed from './components/NewsFeed'
import AlertsPanel from './components/AlertsPanel'
import SettingsPanel from './components/SettingsPanel'
import PromptTemplatesPanel from './components/PromptTemplatesPanel'
import EquityPerformancePanel from './components/EquityPerformancePanel'
import AdvancedAnalytics from './components/AdvancedAnalytics'
import ManualTradeLogger from './components/ManualTradeLogger'
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
// Types
type MainView = 'Dashboard' | 'Trade' | 'Journal' | 'AICoach' | 'Settings'
interface NavItem {
id: MainView
label: string
icon: React.ReactNode
description: string
}
// Navigation configuration
const NAV_ITEMS: NavItem[] = [
{
id: 'Dashboard',
label: 'Dashboard',
icon: <BarChart3 className="w-5 h-5" />,
description: 'Market overview & morning prep'
},
{
id: 'Trade',
label: 'Trade',
icon: <Activity className="w-5 h-5" />,
description: 'Live execution & analysis'
},
{
id: 'Journal',
label: 'Journal',
icon: <BookOpen className="w-5 h-5" />,
description: 'Trading journal & analytics'
},
{
id: 'AICoach',
label: 'AI Coach',
icon: <Brain className="w-5 h-5" />,
description: 'AI insights & coaching'
},
{
id: 'Settings',
label: 'Settings',
icon: <Settings className="w-5 h-5" />,
description: 'Preferences & configuration'
},
]
/**
* Clean sticky navigation bar
*/
function NavigationBar({
active,
onNavigate
}: {
active: MainView
onNavigate: (view: MainView) => void
}) {
return (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
{tabs.map(t => (
<button key={t} className={`btn ${active === t ? 'bg-blue-600 text-white' : 'bg-dark-surface text-gray-300'}`} onClick={() => onChange(t)}>
{t}
</button>
))}
</div>
<nav className="sticky top-0 z-40 border-b border-slate-800 bg-slate-950/95 backdrop-blur">
<div className="max-w-7xl mx-auto px-6 py-3 flex items-center justify-between">
{/* Branding */}
<div className="flex items-center gap-3">
<div className="rounded-lg bg-amber-500/10 p-2">
<BarChart3 className="w-5 h-5 text-amber-500" />
</div>
<div>
<h1 className="text-sm font-bold text-white">Market Simulator</h1>
<p className="text-xs text-slate-400">AI-Powered Trading</p>
</div>
</div>
{/* Nav items */}
<div className="flex items-center gap-1">
{NAV_ITEMS.map((item) => (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
title={item.description}
className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-sm font-medium ${
active === item.id
? 'bg-amber-500/20 text-amber-200 border border-amber-500/40'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
{item.icon}
<span className="hidden md:inline">{item.label}</span>
</button>
))}
</div>
{/* Right side actions */}
<div className="flex items-center gap-2">
<NotificationCenter />
<button
type="button"
className="p-2 rounded-lg hover:bg-slate-800 text-slate-400 transition"
title="Logout"
>
<LogOut className="w-4 h-4" />
</button>
</div>
</div>
</nav>
)
}
export default function App() {
const [activeTab, setActiveTab] = useState<'Analysis Hub' | 'Daily Prep' | 'Journal & Review' | 'Live Charts' | 'Account' | 'Settings' | 'Prompts'>('Analysis Hub')
const [backendStatus, setBackendStatus] = useState<any>(null)
const [showProfileSetup, setShowProfileSetup] = useState(false)
// Trading state for logged trades
const [loggedTrades, setLoggedTrades] = useState<any[]>([])
const [currentPrice, setCurrentPrice] = useState<number>(4084.99)
const [aiAnalysis, setAiAnalysis] = useState<any>(null)
const [isAnalyzing, setIsAnalyzing] = useState(false)
useEffect(() => {
let mounted = true
;(async () => {
try {
const s = await statusApi.getStatus()
if (mounted) setBackendStatus(s)
} catch (e) {
// ignore
}
})()
return () => { mounted = false }
}, [])
// Simulate price updates (in real app, this would come from WebSocket/SSE)
useEffect(() => {
const interval = setInterval(() => {
setCurrentPrice(prev => {
const change = (Math.random() - 0.5) * 8 // Realistic tick size for gold at ~$4000 level
return Number((prev + change).toFixed(2))
})
}, 3000)
return () => clearInterval(interval)
}, [])
const tabs = ['Analysis Hub', 'Daily Prep', 'Journal & Review', 'Live Charts', 'Account', 'Settings', 'Prompts']
// Load logged trades from localStorage
useEffect(() => {
const stored = localStorage.getItem('logged-trades')
if (stored) {
try {
setLoggedTrades(JSON.parse(stored))
} catch (e) {
console.error('Failed to load logged trades:', e)
}
}
}, [])
// Handle new trade logged
const handleTradeLogged = (trade: any) => {
setLoggedTrades([...loggedTrades, trade])
}
const handleAIAnalysis = async () => {
setIsAnalyzing(true)
// Simulate AI analysis
setTimeout(() => {
const mockAnalysis = {
recommendation: Math.random() > 0.5 ? 'BUY' : 'SELL',
confidence: Math.floor(Math.random() * 30 + 60),
riskLevel: 'MEDIUM',
reasoning: 'Based on technical analysis and market sentiment, the current market conditions suggest...',
supportResistance: {
support: [currentPrice - 20, currentPrice - 40],
resistance: [currentPrice + 20, currentPrice + 40]
}
}
setAiAnalysis(mockAnalysis)
setIsAnalyzing(false)
}, 2000)
}
/**
* Dashboard view - Morning prep and market overview
*/
function DashboardView() {
return (
<div className="min-h-screen bg-dark-bg p-6">
<div className="max-w-[1400px] mx-auto">
<header className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gold-500">Assistant Market Simulator</h1>
<p className="text-gray-400 text-sm">AI-Powered Trading with Daily Helper</p>
</div>
<div className="flex items-center gap-4">
<NotificationCenter />
<div className="text-sm text-gray-400">
{backendStatus ? (
<span>API: {backendStatus.app?.name} v{backendStatus.app?.version}</span>
) : (
<span>Checking API</span>
)}
</div>
</div>
</div>
</header>
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Good Morning, Trader</h2>
<p className="text-sm text-slate-300">
Review your trading plan for today, check the market conditions, and prepare your checklist.
</p>
</div>
<Tabs tabs={tabs} active={activeTab} onChange={(t) => setActiveTab(t as any)} />
<div className="grid gap-6 xl:grid-cols-[1.4fr,1fr]">
<div className="space-y-6">
<DailyTradingPlan currentPrice={4084.99} onPlanUpdate={() => {}} />
<DailyMarketSummary currentPrice={4084.99} />
</div>
<div className="space-y-6">
<DailyChecklistPanel checklistType="morning" />
<HabitTracker />
</div>
</div>
{/* ANALYSIS HUB - Pre-Trade Analysis & Trade Logging */}
{activeTab === 'Analysis Hub' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">🎯 Analysis Hub</h3>
<p className="text-sm text-gray-300">
<strong>Workflow:</strong> Analyze Plan on platform (MT5/TradingView) Execute there Log trade here Monitor & Journal
</p>
</div>
{/* Analysis Tools */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(450px, 1fr))', gap: 16 }}>
{/* AI Analysis - Get recommendation BEFORE trading */}
<div>
<AIAnalysisPanel analysis={aiAnalysis} isLoading={isAnalyzing} />
<button
onClick={handleAIAnalysis}
className="btn-primary w-full mt-4"
disabled={isAnalyzing}
>
{isAnalyzing ? 'Analyzing...' : '🤖 Get AI Analysis'}
</button>
</div>
{/* Risk Calculator - Calculate position size BEFORE trading */}
<RiskManagement
currentPrice={currentPrice}
cash={100000}
position={null}
trades={loggedTrades}
/>
</div>
{/* Trade Logger - Log trades from external platform */}
<ManualTradeLogger onTradeLogged={handleTradeLogged} />
{/* Current Price Reference */}
<div className="card">
<h3 className="text-lg font-semibold mb-4">📊 Current Market Price</h3>
<div className="grid grid-cols-3 gap-4">
<div className="bg-dark-bg rounded-lg p-4 text-center">
<div className="text-sm text-gray-400 mb-1">XAU/USD</div>
<div className="text-3xl font-bold text-gold-500">${currentPrice.toFixed(2)}</div>
<div className="text-xs text-green-500 mt-1">Live Price</div>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="text-xs text-gray-400">24h High</div>
<div className="text-xl font-semibold text-green-500">${(currentPrice + 15).toFixed(2)}</div>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="text-xs text-gray-400">24h Low</div>
<div className="text-xl font-semibold text-red-500">${(currentPrice - 12).toFixed(2)}</div>
</div>
</div>
</div>
</div>
)}
{/* DAILY PREP - Morning Routine */}
{activeTab === 'Daily Prep' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">🌅 Daily Preparation</h3>
<p className="text-sm text-gray-300">
Start your day here: Review market, check news, create trading plan
</p>
</div>
{/* Pre-Market Section */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<DailyMarketSummary currentPrice={currentPrice} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<button
onClick={() => setShowProfileSetup(true)}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition-colors"
>
Setup Profile
</button>
<AlertsPanel />
</div>
</div>
{/* Daily Workflow */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', gap: 16 }}>
<DailyChecklistPanel checklistType="morning" />
<NewsFeed />
</div>
{/* Trading Plan */}
<DailyTradingPlan currentPrice={currentPrice} />
</div>
)}
{/* JOURNAL & REVIEW - Post-Trade Analysis */}
{activeTab === 'Journal & Review' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-purple-500/10 border border-purple-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">📖 Journal & Review</h3>
<p className="text-sm text-gray-300">
Document trades, track performance, identify patterns, improve strategy
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<TradingJournal />
<HabitTracker />
</div>
<AdvancedAnalytics
portfolio={{ cash: 100000, initialCapital: 100000, totalValue: 100000, totalPnl: 0, totalPnlPercent: 0, position: null, trades: loggedTrades }}
trades={loggedTrades}
/>
</div>
)}
{/* LIVE CHARTS - Technical Analysis */}
{activeTab === 'Live Charts' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-orange-500/10 border border-orange-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">📈 Live Charts</h3>
<p className="text-sm text-gray-300">
Technical analysis with live streaming charts
</p>
</div>
<LiveMarketPanel />
<MultiChartSSEPanel />
</div>
)}
{activeTab === 'Account' && <AccountPositionsPanel />}
{activeTab === 'Settings' && <SettingsPanel />}
{activeTab === 'Prompts' && <PromptTemplatesPanel />}
{showProfileSetup && (
<UserProfileSetup
onClose={() => setShowProfileSetup(false)}
onSaved={() => {
// Profile saved successfully
}}
/>
)}
{activeTab === 'Settings' && <SettingsPanel />}
{activeTab === 'Prompts' && <PromptTemplatesPanel />}
<div className="grid gap-6 lg:grid-cols-2">
<AlertsPanel />
<NewsFeed />
</div>
</div>
)
}
/**
* Trade view - Live execution cockpit
*/
function TradeView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Live Trading Cockpit</h2>
<p className="text-sm text-slate-300">
Execute trades, monitor risk, review AI analysis, and track your portfolio in real-time.
</p>
</div>
<div className="grid gap-6 xl:grid-cols-[1.4fr,1fr]">
{/* Left side - Analysis & Execution */}
<div className="space-y-6">
<LiveMarketPanel />
<MultiChartSSEPanel />
<div className="grid gap-6 lg:grid-cols-2">
{/* Trade controls would go here */}
<RiskManagement
currentPrice={4084.99}
cash={100000}
position={null}
trades={[]}
/>
</div>
<AIAnalysisPanel analysis={null} isLoading={false} />
</div>
{/* Right side - Portfolio & Automation */}
<div className="space-y-6">
{/* Portfolio tracker would go here */}
</div>
</div>
</div>
)
}
/**
* Journal view - Trading journal and analytics
*/
function JournalView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Trading Journal & Analysis</h2>
<p className="text-sm text-slate-300">
Review your trades, track performance, identify patterns, and improve your trading strategy.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<TradingJournal />
<EquityPerformancePanel />
</div>
<AdvancedAnalytics portfolio={{ cash: 100000, initialCapital: 100000, totalValue: 100000, totalPnl: 0, totalPnlPercent: 0, position: null, trades: [] }} trades={[]} />
</div>
)
}
/**
* AI Coach view - Consolidated AI features and analysis
*/
function AICoachView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Analysis & Insights</h2>
<p className="text-sm text-slate-300">
Get AI-powered analysis and insights to improve your trading decisions.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<AIAnalysisPanel analysis={null} isLoading={false} />
</div>
</div>
)
}
/**
* Settings view - Configuration and preferences
*/
function SettingsView() {
return (
<div className="space-y-6">
<div className="rounded-lg border border-slate-800 bg-slate-900/50 p-4">
<h2 className="font-semibold text-white mb-2">Settings & Configuration</h2>
<p className="text-sm text-slate-300">
Configure your preferences, manage prompts, and customize your trading experience.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<SettingsPanel />
<PromptTemplatesPanel />
</div>
</div>
)
}
/**
* Main App Component
*/
export default function App() {
const [activeView, setActiveView] = useState<MainView>('Dashboard')
const [showProfileSetup, setShowProfileSetup] = useState(false)
// Check backend status on mount
useEffect(() => {
// Status check removed - not critical for UI refactoring
return () => {}
}, [])
// Render active view
const renderActiveView = useCallback(() => {
switch (activeView) {
case 'Dashboard':
return <DashboardView />
case 'Trade':
return <TradeView />
case 'Journal':
return <JournalView />
case 'AICoach':
return <AICoachView />
case 'Settings':
return <SettingsView />
default:
return null
}
}, [activeView])
return (
<div className="min-h-screen bg-slate-950 text-slate-100">
<NavigationBar active={activeView} onNavigate={setActiveView} />
{/* Main content */}
<main className="mx-auto max-w-7xl px-6 py-8">
{renderActiveView()}
</main>
{/* Profile setup modal */}
{showProfileSetup && (
<UserProfileSetup
onClose={() => setShowProfileSetup(false)}
onSaved={() => {
setShowProfileSetup(false)
setActiveView('Dashboard')
}}
/>
)}
</div>
)
}
+283
View File
@@ -0,0 +1,283 @@
import { useEffect, useState } from 'react'
import LiveMarketPanel from './components/LiveMarketPanel'
import MultiChartSSEPanel from './components/MultiChartSSEPanel'
import AccountPositionsPanel from './components/AccountPositionsPanel'
import SettingsPanel from './components/SettingsPanel'
import PromptTemplatesPanel from './components/PromptTemplatesPanel'
import { statusApi } from './services/api'
// Phase 1: Daily Helper Components
import NotificationCenter from './components/NotificationCenter'
import UserProfileSetup from './components/UserProfileSetup'
import HabitTracker from './components/HabitTracker'
import DailyChecklistPanel from './components/DailyChecklistPanel'
// Analysis & Decision Components
import AIAnalysisPanel from './components/AIAnalysisPanel'
import DailyTradingPlan from './components/DailyTradingPlan'
import RiskManagement from './components/RiskManagement'
import TradingJournal from './components/TradingJournal'
import DailyMarketSummary from './components/DailyMarketSummary'
import NewsFeed from './components/NewsFeed'
import AlertsPanel from './components/AlertsPanel'
import AdvancedAnalytics from './components/AdvancedAnalytics'
import ManualTradeLogger from './components/ManualTradeLogger'
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
return (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
{tabs.map(t => (
<button key={t} className={`btn ${active === t ? 'bg-blue-600 text-white' : 'bg-dark-surface text-gray-300'}`} onClick={() => onChange(t)}>
{t}
</button>
))}
</div>
)
}
export default function App() {
const [activeTab, setActiveTab] = useState<'Analysis Hub' | 'Daily Prep' | 'Journal & Review' | 'Live Charts' | 'Account' | 'Settings' | 'Prompts'>('Analysis Hub')
const [backendStatus, setBackendStatus] = useState<any>(null)
const [showProfileSetup, setShowProfileSetup] = useState(false)
// Trading state for logged trades
const [loggedTrades, setLoggedTrades] = useState<any[]>([])
const [currentPrice, setCurrentPrice] = useState<number>(4084.99)
const [aiAnalysis, setAiAnalysis] = useState<any>(null)
const [isAnalyzing, setIsAnalyzing] = useState(false)
useEffect(() => {
let mounted = true
;(async () => {
try {
const s = await statusApi.getStatus()
if (mounted) setBackendStatus(s)
} catch (e) {
// ignore
}
})()
return () => { mounted = false }
}, [])
// Simulate price updates (in real app, this would come from WebSocket/SSE)
useEffect(() => {
const interval = setInterval(() => {
setCurrentPrice(prev => {
const change = (Math.random() - 0.5) * 8 // Realistic tick size for gold at ~$4000 level
return Number((prev + change).toFixed(2))
})
}, 3000)
return () => clearInterval(interval)
}, [])
const tabs = ['Analysis Hub', 'Daily Prep', 'Journal & Review', 'Live Charts', 'Account', 'Settings', 'Prompts']
// Load logged trades from localStorage
useEffect(() => {
const stored = localStorage.getItem('logged-trades')
if (stored) {
try {
setLoggedTrades(JSON.parse(stored))
} catch (e) {
console.error('Failed to load logged trades:', e)
}
}
}, [])
// Handle new trade logged
const handleTradeLogged = (trade: any) => {
setLoggedTrades([...loggedTrades, trade])
}
const handleAIAnalysis = async () => {
setIsAnalyzing(true)
// Simulate AI analysis
setTimeout(() => {
const mockAnalysis = {
recommendation: Math.random() > 0.5 ? 'BUY' : 'SELL',
confidence: Math.floor(Math.random() * 30 + 60),
riskLevel: 'MEDIUM',
reasoning: 'Based on technical analysis and market sentiment, the current market conditions suggest...',
supportResistance: {
support: [currentPrice - 20, currentPrice - 40],
resistance: [currentPrice + 20, currentPrice + 40]
}
}
setAiAnalysis(mockAnalysis)
setIsAnalyzing(false)
}, 2000)
}
return (
<div className="min-h-screen bg-dark-bg p-6">
<div className="max-w-[1400px] mx-auto">
<header className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gold-500">Assistant Market Simulator</h1>
<p className="text-gray-400 text-sm">AI-Powered Trading with Daily Helper</p>
</div>
<div className="flex items-center gap-4">
<NotificationCenter />
<div className="text-sm text-gray-400">
{backendStatus ? (
<span>API: {backendStatus.app?.name} v{backendStatus.app?.version}</span>
) : (
<span>Checking API…</span>
)}
</div>
</div>
</div>
</header>
<Tabs tabs={tabs} active={activeTab} onChange={(t) => setActiveTab(t as any)} />
{/* ANALYSIS HUB - Pre-Trade Analysis & Trade Logging */}
{activeTab === 'Analysis Hub' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">🎯 Analysis Hub</h3>
<p className="text-sm text-gray-300">
<strong>Workflow:</strong> Analyze → Plan on platform (MT5/TradingView) → Execute there → Log trade here → Monitor & Journal
</p>
</div>
{/* Analysis Tools */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(450px, 1fr))', gap: 16 }}>
{/* AI Analysis - Get recommendation BEFORE trading */}
<div>
<AIAnalysisPanel analysis={aiAnalysis} isLoading={isAnalyzing} />
<button
onClick={handleAIAnalysis}
className="btn-primary w-full mt-4"
disabled={isAnalyzing}
>
{isAnalyzing ? 'Analyzing...' : '🤖 Get AI Analysis'}
</button>
</div>
{/* Risk Calculator - Calculate position size BEFORE trading */}
<RiskManagement
currentPrice={currentPrice}
cash={100000}
position={null}
trades={loggedTrades}
/>
</div>
{/* Trade Logger - Log trades from external platform */}
<ManualTradeLogger onTradeLogged={handleTradeLogged} />
{/* Current Price Reference */}
<div className="card">
<h3 className="text-lg font-semibold mb-4">📊 Current Market Price</h3>
<div className="grid grid-cols-3 gap-4">
<div className="bg-dark-bg rounded-lg p-4 text-center">
<div className="text-sm text-gray-400 mb-1">XAU/USD</div>
<div className="text-3xl font-bold text-gold-500">${currentPrice.toFixed(2)}</div>
<div className="text-xs text-green-500 mt-1">Live Price</div>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="text-xs text-gray-400">24h High</div>
<div className="text-xl font-semibold text-green-500">${(currentPrice + 15).toFixed(2)}</div>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="text-xs text-gray-400">24h Low</div>
<div className="text-xl font-semibold text-red-500">${(currentPrice - 12).toFixed(2)}</div>
</div>
</div>
</div>
</div>
)}
{/* DAILY PREP - Morning Routine */}
{activeTab === 'Daily Prep' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">🌅 Daily Preparation</h3>
<p className="text-sm text-gray-300">
Start your day here: Review market, check news, create trading plan
</p>
</div>
{/* Pre-Market Section */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<DailyMarketSummary currentPrice={currentPrice} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<button
onClick={() => setShowProfileSetup(true)}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition-colors"
>
⚙️ Setup Profile
</button>
<AlertsPanel />
</div>
</div>
{/* Daily Workflow */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', gap: 16 }}>
<DailyChecklistPanel checklistType="morning" />
<NewsFeed />
</div>
{/* Trading Plan */}
<DailyTradingPlan currentPrice={currentPrice} />
</div>
)}
{/* JOURNAL & REVIEW - Post-Trade Analysis */}
{activeTab === 'Journal & Review' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-purple-500/10 border border-purple-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">📖 Journal & Review</h3>
<p className="text-sm text-gray-300">
Document trades, track performance, identify patterns, improve strategy
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<TradingJournal />
<HabitTracker />
</div>
<AdvancedAnalytics
portfolio={{ cash: 100000, initialCapital: 100000, totalValue: 100000, totalPnl: 0, totalPnlPercent: 0, position: null, trades: loggedTrades }}
trades={loggedTrades}
/>
</div>
)}
{/* LIVE CHARTS - Technical Analysis */}
{activeTab === 'Live Charts' && (
<div style={{ display: 'grid', gap: 16 }}>
<div className="bg-orange-500/10 border border-orange-500/30 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-2">📈 Live Charts</h3>
<p className="text-sm text-gray-300">
Technical analysis with live streaming charts
</p>
</div>
<LiveMarketPanel />
<MultiChartSSEPanel />
</div>
)}
{activeTab === 'Account' && <AccountPositionsPanel />}
{activeTab === 'Settings' && <SettingsPanel />}
{activeTab === 'Prompts' && <PromptTemplatesPanel />}
{showProfileSetup && (
<UserProfileSetup
onClose={() => setShowProfileSetup(false)}
onSaved={() => {
// Profile saved successfully
}}
/>
)}
{activeTab === 'Settings' && <SettingsPanel />}
{activeTab === 'Prompts' && <PromptTemplatesPanel />}
</div>
</div>
)
}
@@ -0,0 +1,240 @@
import { useState } from 'react';
import { BarChart3, PieChart, Activity } from 'lucide-react';
import PerformanceByTimeframe from './PerformanceByTimeframe';
import EntryTypeAnalysis from './EntryTypeAnalysis';
import SlippageCorrelationAnalysis from './SlippageCorrelationAnalysis';
import type { TimeframeMetrics } from './PerformanceByTimeframe';
import type { EntryTypeMetrics } from './EntryTypeAnalysis';
import type { VolatilityBucket } from './SlippageCorrelationAnalysis';
/**
* Advanced Metrics Dashboard
* Unified dashboard showing:
* - Performance by timeframe
* - Entry type effectiveness
* - Slippage correlation analysis
*/
export interface Trade {
id: string;
timeframe: string;
signalType: 'RSI_CROSSOVER' | 'MA_CROSSOVER' | 'BB_BREAKOUT' | 'MACD' | 'SUPPORT_BOUNCE' | 'TREND_CONFIRMATION' | 'NEWS_TRIGGERED';
entry: number;
exit: number;
quantity: number;
profitable: boolean;
pnl: number;
grossPnL?: number;
slippage: number;
volatility?: number;
volume?: number;
confidence?: number;
timestamp?: string;
}
export interface AdvancedMetricsDashboardProps {
trades: Trade[];
onTimeframeSelect?: (timeframe: string) => void;
onSignalTypeSelect?: (signalType: string) => void;
onVolatilityRangeSelect?: (range: VolatilityBucket) => void;
}
type DashboardTab = 'timeframe' | 'entries' | 'slippage';
export default function AdvancedMetricsDashboard({
trades,
onTimeframeSelect,
onSignalTypeSelect,
onVolatilityRangeSelect,
}: AdvancedMetricsDashboardProps) {
const [activeTab, setActiveTab] = useState<DashboardTab>('timeframe');
const [selectedTimeframe, setSelectedTimeframe] = useState<string | null>(null);
const [selectedSignalType, setSelectedSignalType] = useState<string | null>(null);
// Filter trades based on selection
const filteredTrades = trades.filter(trade => {
if (selectedTimeframe && trade.timeframe !== selectedTimeframe) return false;
if (selectedSignalType && trade.signalType !== selectedSignalType) return false;
return true;
});
// Overall metrics
const overallMetrics = {
totalTrades: trades.length,
totalProfit: trades.reduce((sum, t) => sum + t.pnl, 0),
winRate: trades.length > 0 ? (trades.filter(t => t.profitable).length / trades.length) * 100 : 0,
totalSlippage: trades.reduce((sum, t) => sum + t.slippage, 0),
};
const handleTimeframeSelect = (timeframe: string) => {
setSelectedTimeframe(selectedTimeframe === timeframe ? null : timeframe);
onTimeframeSelect?.(timeframe);
};
const handleSignalTypeSelect = (signalType: string) => {
setSelectedSignalType(selectedSignalType === signalType ? null : signalType);
onSignalTypeSelect?.(signalType);
};
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 overflow-hidden">
{/* Header */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<h2 className="text-lg font-bold text-slate-200 mb-3">Advanced Metrics Dashboard</h2>
{/* Overall Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total Trades</p>
<p className="text-lg font-bold text-slate-300">{overallMetrics.totalTrades}</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Win Rate</p>
<p className={`text-lg font-bold ${overallMetrics.winRate >= 55 ? 'text-emerald-400' : 'text-amber-400'}`}>
{overallMetrics.winRate.toFixed(1)}%
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total P&L</p>
<p className={`text-lg font-bold ${overallMetrics.totalProfit >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${overallMetrics.totalProfit.toFixed(2)}
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Slippage Cost</p>
<p className="text-lg font-bold text-red-400">${overallMetrics.totalSlippage.toFixed(2)}</p>
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-2">
<button
onClick={() => setActiveTab('timeframe')}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold transition-colors ${
activeTab === 'timeframe'
? 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
: 'bg-slate-700/30 text-slate-400 hover:bg-slate-700/50'
}`}
>
<BarChart3 className="w-4 h-4" />
Timeframes
</button>
<button
onClick={() => setActiveTab('entries')}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold transition-colors ${
activeTab === 'entries'
? 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
: 'bg-slate-700/30 text-slate-400 hover:bg-slate-700/50'
}`}
>
<PieChart className="w-4 h-4" />
Entry Types
</button>
<button
onClick={() => setActiveTab('slippage')}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold transition-colors ${
activeTab === 'slippage'
? 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
: 'bg-slate-700/30 text-slate-400 hover:bg-slate-700/50'
}`}
>
<Activity className="w-4 h-4" />
Slippage
</button>
</div>
{/* Active Filters */}
{(selectedTimeframe || selectedSignalType) && (
<div className="mt-3 flex flex-wrap gap-2">
{selectedTimeframe && (
<button
onClick={() => setSelectedTimeframe(null)}
className="inline-flex items-center gap-1 text-xs bg-blue-500/20 text-blue-300 px-2 py-1 rounded border border-blue-500/30 hover:bg-blue-500/30 transition-colors"
>
Timeframe: {selectedTimeframe}
<span className="font-bold">×</span>
</button>
)}
{selectedSignalType && (
<button
onClick={() => setSelectedSignalType(null)}
className="inline-flex items-center gap-1 text-xs bg-purple-500/20 text-purple-300 px-2 py-1 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors"
>
Signal: {selectedSignalType}
<span className="font-bold">×</span>
</button>
)}
</div>
)}
</div>
{/* Content */}
<div className="p-4">
{activeTab === 'timeframe' && (
<PerformanceByTimeframe
trades={filteredTrades.map(t => ({
id: t.id,
timeframe: t.timeframe,
entry: t.entry,
exit: t.exit,
quantity: t.quantity,
profitable: t.profitable,
pnl: t.pnl,
}))}
onTimeframeSelect={handleTimeframeSelect}
/>
)}
{activeTab === 'entries' && (
<EntryTypeAnalysis
trades={filteredTrades.map(t => ({
id: t.id,
signalType: t.signalType,
entry: t.entry,
exit: t.exit,
quantity: t.quantity,
profitable: t.profitable,
pnl: t.pnl,
confidence: t.confidence,
}))}
onSignalTypeSelect={handleSignalTypeSelect}
/>
)}
{activeTab === 'slippage' && (
<SlippageCorrelationAnalysis
trades={filteredTrades.map(t => ({
id: t.id,
entry: t.entry,
exit: t.exit,
slippage: t.slippage,
volatility: t.volatility,
volume: t.volume,
profitable: t.profitable,
pnl: t.pnl,
grossPnL: t.grossPnL || (t.pnl + t.slippage),
timestamp: t.timestamp,
}))}
onVolatilityRangeSelect={onVolatilityRangeSelect}
/>
)}
</div>
{/* Empty State */}
{trades.length === 0 && (
<div className="p-8 text-center text-slate-400">
<BarChart3 className="w-12 h-12 mx-auto mb-3 text-slate-600" />
<p className="text-lg font-semibold mb-1">No Trading Data</p>
<p className="text-sm">Start trading to see advanced metrics and analysis</p>
</div>
)}
</div>
);
}
export type { TimeframeMetrics, EntryTypeMetrics, VolatilityBucket };
@@ -0,0 +1,240 @@
import { useEffect, useMemo, useState } from 'react'
import { PlugZap, Radio, Loader2, ShieldAlert, ExternalLink } from 'lucide-react'
import { BrokerProvider, BrokerPosition } from '../services/broker'
interface BrokerBridgePanelProps {
providers: BrokerProvider[]
loadingProviders?: boolean
activeProvider: BrokerProvider | null
connectionState: 'disconnected' | 'connecting' | 'connected'
lastHeartbeat?: string | null
balance?: number | null
positions?: BrokerPosition[]
onConnect: (providerId: string, apiKey: string, accountId: string, demo: boolean) => Promise<void>
onDisconnect: () => Promise<void>
onSync: () => Promise<void>
}
export default function BrokerBridgePanel({
providers,
loadingProviders = false,
activeProvider,
connectionState,
lastHeartbeat,
balance,
positions = [],
onConnect,
onDisconnect,
onSync,
}: BrokerBridgePanelProps) {
const [selectedProvider, setSelectedProvider] = useState<string>('')
const [apiKey, setApiKey] = useState('')
const [accountId, setAccountId] = useState('')
const [demoMode, setDemoMode] = useState(true)
const [error, setError] = useState<string | null>(null)
const isBusy = connectionState === 'connecting'
const isConnected = connectionState === 'connected'
const selectedDetails = useMemo(() => providers.find(p => p.id === selectedProvider), [providers, selectedProvider])
useEffect(() => {
if (providers.length && !selectedProvider) {
setSelectedProvider(providers[0].id)
}
}, [providers, selectedProvider])
useEffect(() => {
if (selectedDetails && selectedDetails.supportsDemo === false) {
setDemoMode(false)
}
}, [selectedDetails])
const handleConnect = async () => {
setError(null)
try {
await onConnect(selectedProvider, apiKey.trim(), accountId.trim(), demoMode)
setApiKey('')
setAccountId('')
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to connect')
}
}
const handleDisconnect = async () => {
setError(null)
try {
await onDisconnect()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to disconnect')
}
}
const handleSync = async () => {
setError(null)
try {
await onSync()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to sync positions')
}
}
return (
<div className="card space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PlugZap className="w-5 h-5 text-amber-400" />
<h3 className="text-lg font-semibold">Broker bridge</h3>
</div>
{isConnected && activeProvider && (
<span className="text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-300">
{activeProvider.name}
</span>
)}
</div>
<div className="grid grid-cols-1 gap-3">
<label className="text-sm font-medium text-gray-300">
Provider
<select
className="input mt-1"
value={selectedProvider}
onChange={e => setSelectedProvider(e.target.value)}
disabled={isBusy || isConnected || loadingProviders}
>
{providers.length === 0 ? (
<option value="">{loadingProviders ? 'Loading…' : 'No providers'}</option>
) : (
providers.map(provider => (
<option key={provider.id} value={provider.id}>
{provider.name}
</option>
))
)}
</select>
</label>
{selectedDetails && (
<div className="text-xs text-gray-400 space-y-2 bg-dark-bg border border-dark-border rounded-lg p-3">
<p className="text-gray-200 font-semibold flex items-center gap-2">
<Radio className="w-3 h-3" /> {selectedDetails.description}
</p>
<div className="flex flex-wrap gap-2">
{Object.entries(selectedDetails.features).map(([feature, enabled]) => (
<span
key={feature}
className={`px-2 py-0.5 rounded-full border text-[10px] uppercase tracking-wide ${
enabled ? 'border-green-500/40 text-green-300' : 'border-gray-600 text-gray-500'
}`}
>
{feature.replace(/([A-Z])/g, ' $1').trim()}
</span>
))}
</div>
<a
className="inline-flex items-center gap-1 text-blue-300 hover:text-blue-200"
href={selectedDetails.docsUrl}
target="_blank"
rel="noreferrer"
>
Docs <ExternalLink className="w-3 h-3" />
</a>
</div>
)}
<div className="flex items-center justify-between bg-dark-bg border border-dark-border rounded-lg p-3 text-sm">
<div>
<p className="text-gray-300 font-semibold">Demo / paper mode</p>
<p className="text-xs text-gray-500">Disable to push live orders (when supported)</p>
</div>
<button
className={`px-3 py-1 rounded-full text-xs font-semibold ${demoMode ? 'bg-blue-500/20 text-blue-300' : 'bg-red-500/10 text-red-300'}`}
onClick={() => setDemoMode(prev => !prev)}
disabled={isBusy || isConnected || selectedDetails?.supportsDemo === false}
>
{selectedDetails?.supportsDemo === false ? 'Live only' : demoMode ? 'Demo' : 'Live'}
</button>
</div>
<label className="text-sm font-medium text-gray-300">
API key
<input
type="password"
className="input mt-1"
placeholder="demo-***"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
disabled={isBusy || isConnected}
/>
</label>
<label className="text-sm font-medium text-gray-300">
Account id
<input
type="text"
className="input mt-1"
placeholder="123-456"
value={accountId}
onChange={e => setAccountId(e.target.value)}
disabled={isBusy || isConnected}
/>
</label>
</div>
{error && (
<div className="flex items-center gap-2 text-xs text-red-400">
<ShieldAlert className="w-4 h-4" />
{error}
</div>
)}
<div className="flex flex-wrap gap-3">
{!isConnected ? (
<button
className="btn-primary flex-1"
onClick={handleConnect}
disabled={isBusy || !apiKey || !accountId || !selectedProvider}
>
{isBusy ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" /> Connecting
</span>
) : (
'Connect'
)}
</button>
) : (
<button className="btn-secondary flex-1" onClick={handleDisconnect} disabled={isBusy}>
Disconnect
</button>
)}
<button className="btn-muted" onClick={handleSync} disabled={!isConnected || isBusy}>
Sync positions
</button>
</div>
{isConnected && (
<div className="bg-dark-bg border border-dark-border rounded-lg p-3 text-xs text-gray-400 space-y-1">
{balance !== null && balance !== undefined && (
<p>
Balance <span className="text-gray-200 font-semibold">${balance.toLocaleString()}</span>
</p>
)}
{lastHeartbeat && <p>Heartbeat {new Date(lastHeartbeat).toLocaleTimeString()}</p>}
{positions.length > 0 && (
<div className="space-y-1 pt-2 border-t border-dark-border">
<p className="uppercase text-[10px] tracking-wide text-gray-500">Latest synced positions</p>
{positions.slice(0, 3).map(position => (
<div key={`${position.symbol}-${position.ticket ?? position.avgPrice}`} className="flex justify-between">
<span>{position.symbol}</span>
<span className="text-gray-300">{position.quantity} @ {position.avgPrice.toFixed(2)}</span>
</div>
))}
</div>
)}
</div>
)}
</div>
)
}
@@ -0,0 +1,330 @@
import { useMemo } from 'react';
import { PieChart, CheckCircle, AlertCircle } from 'lucide-react';
/**
* Entry Type Analysis
* Analyzes profitability by entry signal type
* Shows which signals are most reliable and profitable
*/
export type SignalType = 'RSI_CROSSOVER' | 'MA_CROSSOVER' | 'BB_BREAKOUT' | 'MACD' | 'SUPPORT_BOUNCE' | 'TREND_CONFIRMATION' | 'NEWS_TRIGGERED';
export interface EntryTypeMetrics {
signalType: SignalType;
displayName: string;
tradesCount: number;
winRate: number; // %
profitableCount: number;
lossCount: number;
avgProfit: number;
avgLoss: number;
totalPnL: number;
profitFactor: number;
consistency: number; // 0-100, how consistent is this signal
reliability: number; // 0-100, confidence in this signal
}
export interface EntryTypeAnalysisProps {
trades: Array<{
id: string;
signalType: SignalType;
entry: number;
exit: number;
quantity: number;
profitable: boolean;
pnl: number;
confidence?: number; // 0-100
}>;
onSignalTypeSelect?: (signalType: SignalType) => void;
}
const SIGNAL_DISPLAY_NAMES: Record<SignalType, string> = {
RSI_CROSSOVER: 'RSI Crossover',
MA_CROSSOVER: 'MA Crossover',
BB_BREAKOUT: 'Bollinger Breakout',
MACD: 'MACD Signal',
SUPPORT_BOUNCE: 'Support Bounce',
TREND_CONFIRMATION: 'Trend Confirmed',
NEWS_TRIGGERED: 'News Event',
};
export default function EntryTypeAnalysis({
trades,
onSignalTypeSelect,
}: EntryTypeAnalysisProps) {
const signalMetrics = useMemo(() => {
if (trades.length === 0) return [];
// Group by signal type
const grouped = trades.reduce((acc, trade) => {
if (!acc[trade.signalType]) {
acc[trade.signalType] = [];
}
acc[trade.signalType].push(trade);
return acc;
}, {} as Record<SignalType, typeof trades>);
// Calculate metrics for each signal type
return Object.entries(grouped).map(([signalType, typesTrades]) => {
const wins = typesTrades.filter(t => t.profitable);
const losses = typesTrades.filter(t => !t.profitable);
const winPnLs = wins.map(t => t.pnl);
const lossPnLs = losses.map(t => t.pnl);
const avgWin = winPnLs.length > 0 ? winPnLs.reduce((a, b) => a + b, 0) / winPnLs.length : 0;
const avgLoss = lossPnLs.length > 0 ? Math.abs(lossPnLs.reduce((a, b) => a + b, 0) / lossPnLs.length) : 0;
const profitFactor = avgLoss > 0 ? avgWin / avgLoss : (avgWin > 0 ? Infinity : 0);
const winRate = (wins.length / typesTrades.length) * 100;
// Consistency = how close results are to average (lower variance = higher consistency)
const pnls = typesTrades.map(t => t.pnl);
const avgPnL = pnls.reduce((a, b) => a + b, 0) / pnls.length;
const variance = pnls.reduce((sum, pnl) => sum + Math.pow(pnl - avgPnL, 2), 0) / pnls.length;
const stdDev = Math.sqrt(variance);
const consistency = Math.max(0, 100 - (stdDev / (Math.abs(avgPnL) + 1)) * 100);
// Reliability = average confidence of trades with this signal type
const avgConfidence = typesTrades.reduce((sum, t) => sum + (t.confidence || 50), 0) / typesTrades.length;
return {
signalType: signalType as SignalType,
displayName: SIGNAL_DISPLAY_NAMES[signalType as SignalType] || signalType,
tradesCount: typesTrades.length,
winRate: winRate,
profitableCount: wins.length,
lossCount: losses.length,
avgProfit: avgWin,
avgLoss: avgLoss,
totalPnL: typesTrades.reduce((sum, t) => sum + t.pnl, 0),
profitFactor: profitFactor,
consistency: consistency,
reliability: avgConfidence,
} as EntryTypeMetrics;
}).sort((a, b) => b.profitFactor - a.profitFactor); // Sort by profit factor
}, [trades]);
const bestSignal = useMemo(() => {
return signalMetrics.length > 0 ? signalMetrics[0] : null;
}, [signalMetrics]);
const overallMetrics = useMemo(() => {
if (signalMetrics.length === 0) {
return {
totalTrades: 0,
avgWinRate: 0,
avgProfitFactor: 0,
diversityScore: 0,
};
}
const totalTrades = signalMetrics.reduce((sum, m) => sum + m.tradesCount, 0);
const totalWins = signalMetrics.reduce((sum, m) => sum + m.profitableCount, 0);
const avgProfitFactor = signalMetrics.reduce((sum, m) => sum + m.profitFactor, 0) / signalMetrics.length;
const diversityScore = Math.min(100, (signalMetrics.length / 7) * 100); // Max 7 signal types
return {
totalTrades,
avgWinRate: (totalWins / totalTrades) * 100,
avgProfitFactor,
diversityScore,
};
}, [signalMetrics]);
if (trades.length === 0) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 p-6 text-center">
<PieChart className="w-8 h-8 text-slate-600 mx-auto mb-2" />
<p className="text-slate-400">No trading data available</p>
<p className="text-xs text-slate-600 mt-1">Entry type analysis will appear here</p>
</div>
);
}
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 space-y-4 overflow-hidden">
{/* Header Summary */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<h3 className="font-semibold text-slate-200 mb-3">Entry Type Analysis</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Signal Types Used</p>
<p className="text-lg font-bold text-slate-300">{signalMetrics.length}</p>
<p className="text-xs text-slate-600">
{(signalMetrics.length / 7 * 100).toFixed(0)}% utilized
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Avg Win Rate</p>
<p className={`text-lg font-bold ${overallMetrics.avgWinRate >= 55 ? 'text-emerald-400' : 'text-amber-400'}`}>
{overallMetrics.avgWinRate.toFixed(1)}%
</p>
<p className="text-xs text-slate-600">
All signals combined
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Avg Profit Factor</p>
<p className={`text-lg font-bold ${overallMetrics.avgProfitFactor >= 1.5 ? 'text-emerald-400' : 'text-amber-400'}`}>
{overallMetrics.avgProfitFactor.toFixed(2)}
</p>
<p className="text-xs text-slate-600">
{overallMetrics.avgProfitFactor >= 1.5 ? 'Strong' : 'Moderate'}
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Signal Diversity</p>
<p className="text-lg font-bold text-slate-300">{overallMetrics.diversityScore.toFixed(0)}%</p>
<p className="text-xs text-slate-600">
{signalMetrics.length} of 7 types
</p>
</div>
</div>
</div>
{/* Signal Types List */}
<div className="px-4 pb-4 space-y-2">
{signalMetrics.map((signal) => {
const isBest = bestSignal?.signalType === signal.signalType;
const winRateColor =
signal.winRate >= 60 ? 'text-emerald-400' :
signal.winRate >= 50 ? 'text-amber-400' :
'text-red-400';
const profitFactorColor =
signal.profitFactor >= 2 ? 'text-emerald-400' :
signal.profitFactor >= 1.5 ? 'text-amber-400' :
signal.profitFactor >= 1 ? 'text-blue-400' :
'text-red-400';
const consistencyColor =
signal.consistency >= 70 ? 'text-emerald-400' :
signal.consistency >= 50 ? 'text-amber-400' :
'text-red-400';
return (
<div
key={signal.signalType}
onClick={() => onSignalTypeSelect?.(signal.signalType)}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
isBest
? 'border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10'
: 'border-slate-700 bg-slate-800/30 hover:bg-slate-800/50'
}`}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
signal.profitFactor >= 1.5 ? 'bg-emerald-500/20' : 'bg-slate-700/30'
}`}>
{signal.totalPnL >= 0 ? (
<CheckCircle className={`w-4 h-4 ${isBest ? 'text-emerald-400' : 'text-slate-400'}`} />
) : (
<AlertCircle className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<p className="text-sm font-semibold text-slate-200">{signal.displayName}</p>
<p className="text-xs text-slate-500">{signal.tradesCount} trades</p>
</div>
{isBest && (
<span className="ml-auto mr-2 text-xs bg-emerald-500/20 text-emerald-300 px-2 py-1 rounded font-semibold">
Best Signal
</span>
)}
</div>
<div className="text-right">
<p className={`text-sm font-bold font-mono ${
signal.totalPnL >= 0 ? 'text-emerald-400' : 'text-red-400'
}`}>
{signal.totalPnL >= 0 ? '+' : ''}{signal.totalPnL.toFixed(2)}
</p>
<p className={`text-xs font-mono ${winRateColor}`}>
{signal.winRate.toFixed(1)}%
</p>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-4 gap-2 text-xs">
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Win Rate</p>
<p className={`font-bold ${winRateColor}`}>{signal.winRate.toFixed(1)}%</p>
<p className="text-slate-600">{signal.profitableCount}W {signal.lossCount}L</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Profit Factor</p>
<p className={`font-bold ${profitFactorColor}`}>{signal.profitFactor.toFixed(2)}</p>
<p className="text-slate-600">Win/Loss ratio</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Consistency</p>
<p className={`font-bold ${consistencyColor}`}>{signal.consistency.toFixed(0)}%</p>
<p className="text-slate-600">Result spread</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Reliability</p>
<p className="font-bold text-blue-400">{signal.reliability.toFixed(0)}%</p>
<p className="text-slate-600">Confidence avg</p>
</div>
</div>
{/* Metrics Comparison */}
<div className="mt-2 space-y-1">
<div className="text-xs">
<div className="flex justify-between mb-0.5">
<span className="text-slate-500">Win Rate</span>
<span className="text-slate-400">{signal.winRate.toFixed(1)}%</span>
</div>
<div className="h-1 bg-slate-700/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${winRateColor}`}
style={{ width: `${Math.min(signal.winRate, 100)}%` }}
/>
</div>
</div>
<div className="text-xs">
<div className="flex justify-between mb-0.5">
<span className="text-slate-500">Consistency</span>
<span className="text-slate-400">{signal.consistency.toFixed(0)}%</span>
</div>
<div className="h-1 bg-slate-700/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${consistencyColor}`}
style={{ width: `${Math.min(signal.consistency, 100)}%` }}
/>
</div>
</div>
</div>
</div>
);
})}
</div>
{/* Footer: Recommendation */}
{bestSignal && (
<div className="px-4 py-3 bg-emerald-500/10 border-t border-slate-700 text-xs text-emerald-200">
<p className="font-semibold mb-1">💡 Recommendation:</p>
<p>
<span className="font-mono font-bold">{bestSignal.displayName}</span> is your most reliable signal
({bestSignal.winRate.toFixed(1)}% win rate, {bestSignal.profitFactor.toFixed(2)} profit factor).
Prioritize these trades with {bestSignal.reliability.toFixed(0)}% confidence.
</p>
</div>
)}
</div>
);
}
@@ -0,0 +1,370 @@
import { useMemo, useState } from 'react';
import { Calendar, TrendingUp, TrendingDown, Target } from 'lucide-react';
/**
* Multi-day position tracking for swing trades
* Tracks entries, profit tiers, duration, and multi-day targets
*/
export interface SwingPosition {
id: string;
entryDate: string;
entryPrice: number;
quantity: number;
currentPrice?: number;
direction: 'LONG' | 'SHORT';
// Profit targets
target1Price?: number;
target1Closed?: boolean;
target2Price?: number;
target2Closed?: boolean;
target3Price?: number;
target3Closed?: boolean;
// Stop loss
stopLoss?: number;
// Notes
notes?: string;
}
export interface PositionMetrics {
totalPositions: number;
activePositions: number;
completedPositions: number;
avgHoldDays: number;
winRate: number; // %
totalProfit: number;
avgProfitPerTrade: number;
}
export interface MultiDayTrackerProps {
positions: SwingPosition[];
onPositionUpdate?: (position: SwingPosition) => void;
onMetricsUpdate?: (metrics: PositionMetrics) => void;
}
export default function MultiDayPositionTracker({
positions,
onMetricsUpdate,
}: MultiDayTrackerProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
// Calculate position metrics
const metrics = useMemo(() => {
if (positions.length === 0) {
return {
totalPositions: 0,
activePositions: 0,
completedPositions: 0,
avgHoldDays: 0,
winRate: 0,
totalProfit: 0,
avgProfitPerTrade: 0,
};
}
const active = positions.filter(p => !p.target3Closed).length;
const completed = positions.length - active;
// Calculate hold days
const holdDays = positions
.map(p => {
const entry = new Date(p.entryDate);
const now = new Date();
return (now.getTime() - entry.getTime()) / (1000 * 60 * 60 * 24);
})
.reduce((a, b) => a + b, 0) / positions.length;
// Calculate profits
let totalProfit = 0;
let winningTrades = 0;
positions.forEach(p => {
const exitPrice = p.target3Closed && p.target3Price ? p.target3Price : p.currentPrice || p.entryPrice;
const tradeProfit =
p.direction === 'LONG'
? (exitPrice - p.entryPrice) * p.quantity
: (p.entryPrice - exitPrice) * p.quantity;
totalProfit += tradeProfit;
if (tradeProfit > 0) winningTrades += 1;
});
const metrics: PositionMetrics = {
totalPositions: positions.length,
activePositions: active,
completedPositions: completed,
avgHoldDays: holdDays,
winRate: (winningTrades / positions.length) * 100,
totalProfit,
avgProfitPerTrade: totalProfit / positions.length,
};
onMetricsUpdate?.(metrics);
return metrics;
}, [positions, onMetricsUpdate]);
// Calculate individual position metrics
const getPositionMetrics = (position: SwingPosition) => {
const entryDate = new Date(position.entryDate);
const now = new Date();
const holdDays = (now.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24);
const exitPrice = position.currentPrice || position.entryPrice;
const profit = position.direction === 'LONG'
? (exitPrice - position.entryPrice) * position.quantity
: (position.entryPrice - exitPrice) * position.quantity;
const profitPercent = position.direction === 'LONG'
? ((exitPrice - position.entryPrice) / position.entryPrice) * 100
: ((position.entryPrice - exitPrice) / position.entryPrice) * 100;
return { holdDays: Math.round(holdDays * 10) / 10, profit, profitPercent };
};
// Format date to readable format
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
};
if (positions.length === 0) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 p-6 text-center">
<Calendar className="w-8 h-8 text-slate-600 mx-auto mb-2" />
<p className="text-slate-400">No active swing positions</p>
<p className="text-xs text-slate-600 mt-1">Multi-day positions will appear here</p>
</div>
);
}
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 space-y-4 overflow-hidden">
{/* Header Summary */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<h3 className="font-semibold text-slate-200 mb-3">Multi-Day Position Tracker</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total Positions</p>
<p className="text-lg font-bold text-slate-300">{metrics.totalPositions}</p>
<p className="text-xs text-slate-600">
{metrics.activePositions} active
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Avg Hold Time</p>
<p className="text-lg font-bold text-slate-300">{metrics.avgHoldDays.toFixed(1)}d</p>
<p className="text-xs text-slate-600">
{metrics.avgHoldDays > 3 ? 'Strong swing' : metrics.avgHoldDays > 1 ? 'Good swing' : 'Quick swing'}
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Win Rate</p>
<p className={`text-lg font-bold ${metrics.winRate >= 60 ? 'text-emerald-400' : metrics.winRate >= 40 ? 'text-amber-400' : 'text-red-400'}`}>
{metrics.winRate.toFixed(1)}%
</p>
<p className="text-xs text-slate-600">
{metrics.completedPositions} closed
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total P&L</p>
<p className={`text-lg font-bold ${metrics.totalProfit >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${Math.abs(metrics.totalProfit).toFixed(2)}
</p>
<p className="text-xs text-slate-600">
${(metrics.avgProfitPerTrade).toFixed(2)}/trade
</p>
</div>
</div>
</div>
{/* Positions List */}
<div className="px-4 pb-4 space-y-3">
{positions.map((position) => {
const { holdDays, profit, profitPercent } = getPositionMetrics(position);
const isExpanded = expandedId === position.id;
const isLong = position.direction === 'LONG';
const currentPrice_ = position.currentPrice || position.entryPrice;
// Determine status
let status: 'active' | 'partial' | 'completed' = 'active';
let closedCount = 0;
if (position.target1Closed) closedCount++;
if (position.target2Closed) closedCount++;
if (position.target3Closed) closedCount++;
if (closedCount === 3) status = 'completed';
else if (closedCount > 0) status = 'partial';
return (
<div
key={position.id}
className={`rounded-lg border transition-all cursor-pointer ${
isExpanded
? 'border-blue-500/40 bg-blue-500/5'
: status === 'completed'
? 'border-slate-700 bg-slate-800/30'
: 'border-slate-700 bg-slate-800/50 hover:bg-slate-800/70'
}`}
onClick={() => setExpandedId(isExpanded ? null : position.id)}
>
{/* Main Row */}
<div className="p-3 flex items-center justify-between gap-2">
{/* Left: Entry & Direction */}
<div className="flex items-center gap-3 min-w-0">
<div className={`p-2 rounded-lg ${isLong ? 'bg-emerald-500/20' : 'bg-red-500/20'}`}>
{isLong ? (
<TrendingUp className="w-4 h-4 text-emerald-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div className="min-w-0">
<p className="text-sm font-mono font-semibold text-slate-200">
{position.quantity} oz @ ${position.entryPrice.toFixed(2)}
</p>
<p className="text-xs text-slate-500">
{formatDate(position.entryDate)} {holdDays}d
</p>
</div>
</div>
{/* Right: Current P&L */}
<div className="text-right">
<p className={`text-sm font-bold font-mono ${profit >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{profit >= 0 ? '+' : ''}{profit.toFixed(2)}
</p>
<p className={`text-xs font-mono ${profitPercent >= 0 ? 'text-emerald-300' : 'text-red-300'}`}>
{profitPercent >= 0 ? '+' : ''}{profitPercent.toFixed(2)}%
</p>
</div>
</div>
{/* Expanded Details */}
{isExpanded && (
<>
<div className="border-t border-slate-700/50 p-3 space-y-3">
{/* Current Price & Movement */}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="bg-slate-700/30 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Current Price</p>
<p className="font-mono font-semibold text-slate-200">
${currentPrice_.toFixed(2)}
</p>
<p className={`font-mono text-xs ${
(isLong && currentPrice_ > position.entryPrice) || (!isLong && currentPrice_ < position.entryPrice)
? 'text-emerald-400'
: 'text-red-400'
}`}>
{isLong ? currentPrice_ - position.entryPrice >= 0 ? '+' : '' : currentPrice_ - position.entryPrice <= 0 ? '+' : ''}
${(isLong ? currentPrice_ - position.entryPrice : position.entryPrice - currentPrice_).toFixed(2)}
</p>
</div>
<div className="bg-slate-700/30 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Stop Loss</p>
<p className="font-mono font-semibold text-slate-200">
${(position.stopLoss || position.entryPrice * 0.98).toFixed(2)}
</p>
<p className={`font-mono text-xs ${
(isLong && currentPrice_ > (position.stopLoss || position.entryPrice * 0.98)) ||
(!isLong && currentPrice_ < (position.stopLoss || position.entryPrice * 0.98))
? 'text-emerald-400'
: 'text-red-400'
}`}>
{(isLong ? currentPrice_ - (position.stopLoss || position.entryPrice * 0.98) :
(position.stopLoss || position.entryPrice * 0.98) - currentPrice_).toFixed(2)} buffer
</p>
</div>
</div>
{/* Profit Targets */}
<div className="space-y-2">
<p className="text-xs font-semibold text-slate-400">Profit Targets</p>
<div className="space-y-1.5">
{[1, 2, 3].map((tier) => {
const targetPrice = tier === 1 ? position.target1Price :
tier === 2 ? position.target2Price :
position.target3Price;
const isClosed = tier === 1 ? position.target1Closed :
tier === 2 ? position.target2Closed :
position.target3Closed;
if (!targetPrice) return null;
const targetProfit = isLong
? (targetPrice - position.entryPrice) * (position.quantity / 3)
: (position.entryPrice - targetPrice) * (position.quantity / 3);
return (
<div
key={`target-${tier}`}
className={`text-xs rounded px-2 py-1.5 flex items-center justify-between ${
isClosed
? 'bg-emerald-500/10 border border-emerald-500/30'
: 'bg-slate-700/30 border border-slate-600/30'
}`}
>
<div className="flex items-center gap-2">
<Target className={`w-3 h-3 ${isClosed ? 'text-emerald-400' : 'text-slate-500'}`} />
<span className={`font-mono ${isClosed ? 'text-emerald-300 line-through' : 'text-slate-300'}`}>
T{tier}: ${targetPrice.toFixed(2)}
</span>
</div>
<span className={`font-mono ${isClosed ? 'text-emerald-400' : 'text-slate-500'}`}>
{isClosed ? '✓ Closed' : `+$${(targetProfit).toFixed(2)}`}
</span>
</div>
);
})}
</div>
</div>
{/* Notes */}
{position.notes && (
<div className="bg-slate-700/20 rounded px-2 py-1.5 text-xs text-slate-300 italic">
"{position.notes}"
</div>
)}
{/* Status */}
<div className="bg-slate-700/30 rounded px-2 py-1.5 text-xs">
<div className="flex items-center justify-between mb-1">
<span className="text-slate-500">Position Status</span>
<span className={`font-semibold ${
status === 'completed' ? 'text-emerald-400' :
status === 'partial' ? 'text-amber-400' :
'text-blue-400'
}`}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</div>
<div className="h-1 bg-slate-600/50 rounded-full overflow-hidden">
<div
className={`h-full ${
status === 'completed' ? 'bg-emerald-500' :
status === 'partial' ? 'bg-amber-500' :
'bg-blue-500'
}`}
style={{ width: `${(closedCount / 3) * 100}%` }}
/>
</div>
</div>
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,336 @@
import { useMemo, useState } from 'react';
import { AlertTriangle, TrendingUp, TrendingDown, Clock, X } from 'lucide-react';
/**
* News Event Tracker for swing trading
* Alerts on important economic events, earnings, and market-moving news
* Helps avoid bad entry/exit times and position yourself before major moves
*/
export type EventImpact = 'HIGH' | 'MEDIUM' | 'LOW';
export type EventCategory = 'ECONOMIC' | 'EARNINGS' | 'FED' | 'GEOPOLITICAL' | 'SUPPLY_DEMAND';
export interface NewsEvent {
id: string;
title: string;
category: EventCategory;
impact: EventImpact;
scheduledTime: string; // ISO datetime
status: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
forecast?: number;
actual?: number;
previous?: number;
sentiment?: 'BULLISH' | 'BEARISH' | 'NEUTRAL'; // For sentiment analysis
description?: string;
recommendation?: string; // Action for trader
}
export interface NewsEventTrackerProps {
events: NewsEvent[];
currentTime?: string; // ISO datetime (defaults to now)
onEventAlert?: (event: NewsEvent) => void;
showCompleted?: boolean;
}
export default function NewsEventTracker({
events,
currentTime = new Date().toISOString(),
onEventAlert,
showCompleted = false,
}: NewsEventTrackerProps) {
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());
const [expandedId, setExpandedId] = useState<string | null>(null);
// Calculate time until event
const getTimeUntil = (scheduledTime: string) => {
const now = new Date(currentTime);
const eventTime = new Date(scheduledTime);
const diffMs = eventTime.getTime() - now.getTime();
const diffMins = Math.round(diffMs / 60000);
if (diffMins < 0) {
const pastMins = Math.abs(diffMins);
if (pastMins < 60) return `${pastMins}m ago`;
const pastHours = Math.round(pastMins / 60);
if (pastHours < 24) return `${pastHours}h ago`;
const pastDays = Math.round(pastHours / 24);
return `${pastDays}d ago`;
}
if (diffMins === 0) return 'Now';
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.round(diffMins / 60);
if (hours < 24) return `${hours}h`;
const days = Math.round(hours / 24);
return `${days}d`;
};
// Update event status based on time
const getEventStatus = (event: NewsEvent) => {
const now = new Date(currentTime);
const eventTime = new Date(event.scheduledTime);
const diffMs = eventTime.getTime() - now.getTime();
if (diffMs < 0 && Math.abs(diffMs) > 3600000) {
return 'COMPLETED';
} else if (diffMs < 0 && diffMs > -3600000) {
return 'IN_PROGRESS';
} else {
return 'UPCOMING';
}
};
// Filter and sort events
const filteredEvents = useMemo(() => {
return events
.filter(e => !dismissedIds.has(e.id))
.filter(e => showCompleted || getEventStatus(e) !== 'COMPLETED')
.sort((a, b) => {
const aTime = new Date(a.scheduledTime).getTime();
const bTime = new Date(b.scheduledTime).getTime();
return aTime - bTime;
});
}, [events, dismissedIds, showCompleted, currentTime]);
// Upcoming high-impact events
const upcomingHighImpact = useMemo(() => {
return filteredEvents.filter(
e => getEventStatus(e) === 'UPCOMING' && e.impact === 'HIGH'
);
}, [filteredEvents]);
const handleDismiss = (id: string) => {
setDismissedIds(prev => new Set(prev).add(id));
};
const getCategoryIcon = (category: EventCategory) => {
const iconClass = 'w-4 h-4';
switch (category) {
case 'ECONOMIC':
return <TrendingUp className={`${iconClass} text-blue-400`} />;
case 'EARNINGS':
return <TrendingDown className={`${iconClass} text-purple-400`} />;
case 'FED':
return <AlertTriangle className={`${iconClass} text-red-400`} />;
case 'GEOPOLITICAL':
return <AlertTriangle className={`${iconClass} text-orange-400`} />;
case 'SUPPLY_DEMAND':
return <TrendingUp className={`${iconClass} text-amber-400`} />;
default:
return <Clock className={`${iconClass} text-slate-400`} />;
}
};
const getImpactColor = (impact: EventImpact) => {
switch (impact) {
case 'HIGH':
return 'bg-red-500/15 border-red-500/30 text-red-200';
case 'MEDIUM':
return 'bg-amber-500/15 border-amber-500/30 text-amber-200';
case 'LOW':
return 'bg-blue-500/15 border-blue-500/30 text-blue-200';
}
};
const getSentimentColor = (sentiment?: string) => {
switch (sentiment) {
case 'BULLISH':
return 'text-emerald-400';
case 'BEARISH':
return 'text-red-400';
default:
return 'text-slate-400';
}
};
// Alert for upcoming high-impact events
if (upcomingHighImpact.length > 0 && onEventAlert) {
upcomingHighImpact.forEach(e => {
const timeUntil = getTimeUntil(e.scheduledTime);
if (timeUntil === '1h' || timeUntil === '5m' || timeUntil === '15m') {
onEventAlert(e);
}
});
}
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 overflow-hidden">
{/* Header */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-5 h-5 text-amber-500" />
<h3 className="font-semibold text-slate-200">News Event Monitor</h3>
{upcomingHighImpact.length > 0 && (
<span className="ml-auto bg-red-500/20 text-red-300 text-xs px-2 py-1 rounded-full font-semibold">
{upcomingHighImpact.length} High Impact Soon
</span>
)}
</div>
<p className="text-xs text-slate-500">
{filteredEvents.length} events tracked Avoid trading during high-impact events
</p>
</div>
{/* Events List */}
{filteredEvents.length === 0 ? (
<div className="p-8 text-center text-slate-400">
<Clock className="w-8 h-8 mx-auto mb-2 text-slate-600" />
<p>No upcoming news events</p>
</div>
) : (
<div className="divide-y divide-slate-700/50">
{filteredEvents.map(event => {
const isExpanded = expandedId === event.id;
const timeUntil = getTimeUntil(event.scheduledTime);
const status = getEventStatus(event);
const eventDate = new Date(event.scheduledTime);
const formattedTime = eventDate.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
});
return (
<div
key={event.id}
className={`p-3 transition-colors cursor-pointer hover:bg-slate-800/30 ${
isExpanded ? 'bg-slate-800/40' : ''
}`}
onClick={() => setExpandedId(isExpanded ? null : event.id)}
>
{/* Main Row */}
<div className="flex items-start gap-3 justify-between mb-2">
{/* Left: Category and Title */}
<div className="flex items-start gap-3 flex-1 min-w-0">
<div className="mt-1">{getCategoryIcon(event.category)}</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-slate-200 truncate">
{event.title}
</p>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded border ${getImpactColor(event.impact)}`}>
{event.impact} Impact
</span>
<span className={`text-xs px-2 py-0.5 rounded ${
status === 'UPCOMING' ? 'bg-blue-500/20 text-blue-300' :
status === 'IN_PROGRESS' ? 'bg-orange-500/20 text-orange-300' :
'bg-slate-700/30 text-slate-400'
}`}>
{status === 'UPCOMING' ? `${timeUntil}` : status === 'IN_PROGRESS' ? 'In Progress' : 'Completed'}
</span>
</div>
</div>
</div>
{/* Right: Time & Action */}
<div className="text-right flex items-start gap-2">
<div className="text-xs text-slate-500">
<p className="font-mono">{formattedTime}</p>
<p className="text-slate-600">{eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDismiss(event.id);
}}
className="p-1 hover:bg-slate-700/50 rounded transition-colors"
title="Dismiss"
>
<X className="w-4 h-4 text-slate-500 hover:text-slate-300" />
</button>
</div>
</div>
{/* Expanded Details */}
{isExpanded && (
<div className="mt-3 pt-3 border-t border-slate-700/30 space-y-2">
{/* Category and Sentiment */}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Category</p>
<p className="text-slate-300 font-semibold capitalize">
{event.category.replace(/_/g, ' ')}
</p>
</div>
{event.sentiment && (
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Sentiment</p>
<p className={`font-semibold capitalize ${getSentimentColor(event.sentiment)}`}>
{event.sentiment}
</p>
</div>
)}
</div>
{/* Forecast vs Actual */}
{(event.forecast !== undefined || event.actual !== undefined || event.previous !== undefined) && (
<div className="bg-slate-800/40 rounded px-2 py-1.5 text-xs">
<p className="text-slate-500 mb-1">Data</p>
<div className="grid grid-cols-3 gap-2 font-mono text-slate-300">
{event.forecast !== undefined && (
<div>
<p className="text-slate-600 text-xs mb-0.5">Forecast</p>
<p className="font-semibold">{event.forecast}</p>
</div>
)}
{event.actual !== undefined && (
<div>
<p className="text-slate-600 text-xs mb-0.5">Actual</p>
<p className={`font-semibold ${
(event.forecast !== undefined && event.actual > event.forecast)
? 'text-emerald-400'
: event.forecast !== undefined && event.actual < event.forecast
? 'text-red-400'
: 'text-slate-300'
}`}>
{event.actual}
</p>
</div>
)}
{event.previous !== undefined && (
<div>
<p className="text-slate-600 text-xs mb-0.5">Previous</p>
<p className="font-semibold">{event.previous}</p>
</div>
)}
</div>
</div>
)}
{/* Description */}
{event.description && (
<div className="bg-slate-800/40 rounded px-2 py-1.5 text-xs">
<p className="text-slate-500 mb-1">Details</p>
<p className="text-slate-300">{event.description}</p>
</div>
)}
{/* Recommendation */}
{event.recommendation && (
<div className={`rounded px-2 py-1.5 text-xs border ${
event.impact === 'HIGH'
? 'bg-red-500/10 border-red-500/30 text-red-200'
: 'bg-amber-500/10 border-amber-500/30 text-amber-200'
}`}>
<p className="font-semibold mb-1">Recommendation:</p>
<p>{event.recommendation}</p>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* Footer: Event Count */}
{filteredEvents.length > 0 && (
<div className="px-4 py-2 bg-slate-800/30 border-t border-slate-700/50 text-xs text-slate-500">
{filteredEvents.filter(e => getEventStatus(e) === 'UPCOMING').length} upcoming {' '}
{filteredEvents.filter(e => getEventStatus(e) === 'COMPLETED').length} completed
</div>
)}
</div>
);
}
@@ -0,0 +1,292 @@
import { useMemo } from 'react';
import { BarChart3, TrendingUp, TrendingDown } from 'lucide-react';
/**
* Performance by Timeframe Analysis
* Analyzes which timeframes are most profitable for your trading
* Helps optimize strategy by identifying best trading windows
*/
export interface TimeframeMetrics {
timeframe: string;
tradesCount: number;
winRate: number; // %
avgProfit: number;
avgLoss: number;
profitFactor: number; // avg win / avg loss
totalPnL: number;
profitableTradesCount: number;
looseTrades: number;
bestTrade: number;
worstTrade: number;
}
export interface PerformanceByTimeframeProps {
trades: Array<{
id: string;
timeframe: string;
entry: number;
exit: number;
quantity: number;
profitable: boolean;
pnl: number;
}>;
onTimeframeSelect?: (timeframe: string) => void;
}
export default function PerformanceByTimeframe({
trades,
onTimeframeSelect,
}: PerformanceByTimeframeProps) {
const timeframeMetrics = useMemo(() => {
if (trades.length === 0) return [];
// Group by timeframe
const grouped = trades.reduce((acc, trade) => {
if (!acc[trade.timeframe]) {
acc[trade.timeframe] = [];
}
acc[trade.timeframe].push(trade);
return acc;
}, {} as Record<string, typeof trades>);
// Calculate metrics for each timeframe
return Object.entries(grouped).map(([tf, tfTrades]) => {
const wins = tfTrades.filter(t => t.profitable);
const losses = tfTrades.filter(t => !t.profitable);
const winPnLs = wins.map(t => t.pnl);
const lossPnLs = losses.map(t => t.pnl);
const avgWin = winPnLs.length > 0 ? winPnLs.reduce((a, b) => a + b, 0) / winPnLs.length : 0;
const avgLoss = lossPnLs.length > 0 ? Math.abs(lossPnLs.reduce((a, b) => a + b, 0) / lossPnLs.length) : 0;
const profitFactor = avgLoss > 0 ? avgWin / avgLoss : (avgWin > 0 ? Infinity : 0);
return {
timeframe: tf,
tradesCount: tfTrades.length,
winRate: (wins.length / tfTrades.length) * 100,
avgProfit: avgWin,
avgLoss: avgLoss,
profitFactor: profitFactor,
totalPnL: tfTrades.reduce((sum, t) => sum + t.pnl, 0),
profitableTradesCount: wins.length,
looseTrades: losses.length,
bestTrade: Math.max(...tfTrades.map(t => t.pnl)),
worstTrade: Math.min(...tfTrades.map(t => t.pnl)),
} as TimeframeMetrics;
}).sort((a, b) => b.tradesCount - a.tradesCount); // Sort by trade count
}, [trades]);
// Find best and worst timeframes
const bestTimeframe = useMemo(() => {
return timeframeMetrics.length > 0
? timeframeMetrics.reduce((best, current) =>
current.profitFactor > best.profitFactor ? current : best
)
: null;
}, [timeframeMetrics]);
const overallMetrics = useMemo(() => {
if (timeframeMetrics.length === 0) {
return {
totalTrades: 0,
overallWinRate: 0,
overallPnL: 0,
avgProfitFactor: 0,
};
}
const totalTrades = timeframeMetrics.reduce((sum, m) => sum + m.tradesCount, 0);
const totalWins = timeframeMetrics.reduce((sum, m) => sum + m.profitableTradesCount, 0);
const totalPnL = timeframeMetrics.reduce((sum, m) => sum + m.totalPnL, 0);
const avgProfitFactor = timeframeMetrics.reduce((sum, m) => sum + m.profitFactor, 0) / timeframeMetrics.length;
return {
totalTrades,
overallWinRate: (totalWins / totalTrades) * 100,
overallPnL: totalPnL,
avgProfitFactor,
};
}, [timeframeMetrics]);
if (trades.length === 0) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 p-6 text-center">
<BarChart3 className="w-8 h-8 text-slate-600 mx-auto mb-2" />
<p className="text-slate-400">No trading data available</p>
<p className="text-xs text-slate-600 mt-1">Trade history will appear here</p>
</div>
);
}
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 space-y-4 overflow-hidden">
{/* Header Summary */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<h3 className="font-semibold text-slate-200 mb-3">Performance by Timeframe</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total Trades</p>
<p className="text-lg font-bold text-slate-300">{overallMetrics.totalTrades}</p>
<p className="text-xs text-slate-600">
{timeframeMetrics.length} timeframes
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Overall Win Rate</p>
<p className={`text-lg font-bold ${overallMetrics.overallWinRate >= 60 ? 'text-emerald-400' : overallMetrics.overallWinRate >= 50 ? 'text-amber-400' : 'text-red-400'}`}>
{overallMetrics.overallWinRate.toFixed(1)}%
</p>
<p className="text-xs text-slate-600">
{Math.round(overallMetrics.overallWinRate / 10)} / 10
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total P&L</p>
<p className={`text-lg font-bold ${overallMetrics.overallPnL >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${Math.abs(overallMetrics.overallPnL).toFixed(2)}
</p>
<p className="text-xs text-slate-600">
{overallMetrics.overallPnL >= 0 ? '+' : '-'}{((overallMetrics.overallPnL / overallMetrics.totalTrades) || 0).toFixed(2)}/trade
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Avg Profit Factor</p>
<p className={`text-lg font-bold ${overallMetrics.avgProfitFactor >= 1.5 ? 'text-emerald-400' : overallMetrics.avgProfitFactor >= 1 ? 'text-amber-400' : 'text-red-400'}`}>
{overallMetrics.avgProfitFactor.toFixed(2)}
</p>
<p className="text-xs text-slate-600">
{overallMetrics.avgProfitFactor >= 1.5 ? 'Excellent' : overallMetrics.avgProfitFactor >= 1 ? 'Good' : 'Poor'}
</p>
</div>
</div>
</div>
{/* Timeframe List */}
<div className="px-4 pb-4 space-y-2">
{timeframeMetrics.map((tf) => {
const isBest = bestTimeframe?.timeframe === tf.timeframe;
const winRateColor =
tf.winRate >= 65 ? 'text-emerald-400' :
tf.winRate >= 55 ? 'text-amber-400' :
'text-red-400';
const profitFactorColor =
tf.profitFactor >= 2 ? 'text-emerald-400' :
tf.profitFactor >= 1.5 ? 'text-amber-400' :
tf.profitFactor >= 1 ? 'text-blue-400' :
'text-red-400';
return (
<div
key={tf.timeframe}
onClick={() => onTimeframeSelect?.(tf.timeframe)}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
isBest
? 'border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10'
: 'border-slate-700 bg-slate-800/30 hover:bg-slate-800/50'
}`}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
tf.profitFactor >= 1.5 ? 'bg-emerald-500/20' : 'bg-slate-700/30'
}`}>
{tf.totalPnL >= 0 ? (
<TrendingUp className={`w-4 h-4 ${isBest ? 'text-emerald-400' : 'text-slate-400'}`} />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<p className="text-sm font-semibold text-slate-200">{tf.timeframe}</p>
<p className="text-xs text-slate-500">{tf.tradesCount} trades</p>
</div>
{isBest && (
<span className="ml-auto mr-2 text-xs bg-emerald-500/20 text-emerald-300 px-2 py-1 rounded font-semibold">
Best
</span>
)}
</div>
<div className="text-right">
<p className={`text-sm font-bold font-mono ${
tf.totalPnL >= 0 ? 'text-emerald-400' : 'text-red-400'
}`}>
{tf.totalPnL >= 0 ? '+' : ''}{tf.totalPnL.toFixed(2)}
</p>
<p className={`text-xs font-mono ${winRateColor}`}>
{tf.winRate.toFixed(1)}%
</p>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-4 gap-2 text-xs">
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Win Rate</p>
<p className={`font-bold ${winRateColor}`}>{tf.winRate.toFixed(1)}%</p>
<p className="text-slate-600">{tf.profitableTradesCount}W {tf.looseTrades}L</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Avg Win</p>
<p className="font-bold text-emerald-400">${tf.avgProfit.toFixed(2)}</p>
<p className="text-slate-600">Best: ${tf.bestTrade.toFixed(2)}</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Avg Loss</p>
<p className="font-bold text-red-400">${tf.avgLoss.toFixed(2)}</p>
<p className="text-slate-600">Worst: ${tf.worstTrade.toFixed(2)}</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Profit Factor</p>
<p className={`font-bold ${profitFactorColor}`}>{tf.profitFactor.toFixed(2)}</p>
<p className="text-slate-600">
{tf.profitFactor >= 2 ? '⭐ Excellent' : tf.profitFactor >= 1.5 ? '✓ Good' : '↗ Poor'}
</p>
</div>
</div>
{/* Win Rate Bar */}
<div className="mt-2">
<div className="h-1.5 bg-slate-700/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
tf.winRate >= 65 ? 'bg-emerald-500' :
tf.winRate >= 55 ? 'bg-amber-500' :
'bg-red-500'
}`}
style={{ width: `${Math.min(tf.winRate, 100)}%` }}
/>
</div>
</div>
</div>
);
})}
</div>
{/* Footer: Recommendation */}
{bestTimeframe && (
<div className="px-4 py-3 bg-emerald-500/10 border-t border-slate-700 text-xs text-emerald-200">
<p className="font-semibold mb-1">💡 Recommendation:</p>
<p>
Focus more trades on <span className="font-mono font-bold">{bestTimeframe.timeframe}</span> timeframe -
it shows the best profit factor ({bestTimeframe.profitFactor.toFixed(2)}) with {bestTimeframe.winRate.toFixed(1)}% win rate.
</p>
</div>
)}
</div>
);
}
@@ -0,0 +1,323 @@
import { useEffect, useState } from 'react'
import { Shield, Zap, Activity, AlertTriangle, RefreshCcw } from 'lucide-react'
import { type PositionMetrics } from '@/services/api'
export type AutomationEventType = 'STOP_LOSS' | 'TAKE_PROFIT' | 'TRAILING' | 'INFO'
export interface AutomationEvent {
id: string
timestamp: number
type: AutomationEventType
description: string
}
interface RiskAutomationPanelProps {
currentPrice: number
stopLoss: number | null
takeProfit: number | null
trailingPercent: number | null
autoCloseEnabled: boolean
events: AutomationEvent[]
onToggleAutoClose: (enabled: boolean) => void
onClearGuards: () => void
onUpdateTrailing: (percent: number | null) => void
metrics?: PositionMetrics | null
metricsLoading?: boolean
metricsError?: string | null
onRefreshMetrics?: () => Promise<PositionMetrics | null> | void
onApplyGuardSuggestions?: (suggestions: { stopLoss?: number | null; takeProfit?: number | null; trailingPercent?: number | null }) => void
variant?: 'default' | 'embedded'
}
export default function RiskAutomationPanel({
currentPrice,
stopLoss,
takeProfit,
trailingPercent,
autoCloseEnabled,
events,
onToggleAutoClose,
onClearGuards,
onUpdateTrailing,
metrics,
metricsLoading = false,
metricsError,
onRefreshMetrics,
onApplyGuardSuggestions,
variant = 'default',
}: RiskAutomationPanelProps) {
const [localTrailing, setLocalTrailing] = useState<number>(trailingPercent ?? 1.5)
useEffect(() => {
if (typeof trailingPercent === 'number') {
setLocalTrailing(trailingPercent)
}
}, [trailingPercent])
const trailingActive = typeof trailingPercent === 'number'
const formatNumber = (value?: number | null, maximumFractionDigits = 2) => {
if (value === undefined || value === null || Number.isNaN(value)) return '—'
return new Intl.NumberFormat('en-US', { maximumFractionDigits }).format(value)
}
const formatCurrency = (value?: number | null) => {
if (value === undefined || value === null || Number.isNaN(value)) return '—'
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }).format(value)
}
const formatPercent = (value?: number | null) => {
if (value === undefined || value === null || Number.isNaN(value)) return '—'
const signed = value > 0 ? '+' : ''
return `${signed}${formatNumber(value)}%`
}
const snapshot = metrics ?? null
const suggestedStopLoss = snapshot?.support_levels?.[0] ?? null
const suggestedTakeProfit = snapshot?.resistance_levels?.[0] ?? null
const suggestedTrailingPercent = snapshot?.atr14 && snapshot.current_price
? Number(((snapshot.atr14 / snapshot.current_price) * 100).toFixed(1))
: null
const canApplySuggestions = Boolean(
onApplyGuardSuggestions && (
typeof suggestedStopLoss === 'number' ||
typeof suggestedTakeProfit === 'number' ||
typeof suggestedTrailingPercent === 'number'
),
)
const handleRefreshClick = () => {
void onRefreshMetrics?.()
}
const handleApplySuggestions = () => {
onApplyGuardSuggestions?.({
stopLoss: typeof suggestedStopLoss === 'number' ? suggestedStopLoss : undefined,
takeProfit: typeof suggestedTakeProfit === 'number' ? suggestedTakeProfit : undefined,
trailingPercent: typeof suggestedTrailingPercent === 'number' ? Math.max(0.5, suggestedTrailingPercent) : undefined,
})
}
const isEmbedded = variant === 'embedded'
return (
<div className={isEmbedded ? 'space-y-4' : 'card space-y-4'}>
<div className="bg-dark-bg/70 border border-dark-border rounded-lg p-4 space-y-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs uppercase tracking-wide text-gray-500">Position metrics snapshot</p>
{snapshot ? (
<p className="text-sm text-gray-300">
{snapshot.symbol} · {snapshot.timeframe} ·{' '}
{new Date(snapshot.timestamp * 1000).toLocaleTimeString()}
</p>
) : (
<p className="text-sm text-gray-500">Metrics unavailable</p>
)}
</div>
<div className="flex items-center gap-2">
{metricsLoading && <span className="text-xs text-gray-500">Refreshing</span>}
<button
onClick={handleRefreshClick}
className="btn text-xs flex items-center gap-1 disabled:opacity-60"
type="button"
disabled={!onRefreshMetrics}
>
<RefreshCcw className="w-3 h-3" /> Refresh
</button>
</div>
</div>
{metricsError && <div className="text-xs text-red-400">{metricsError}</div>}
{snapshot ? (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-3 text-sm text-gray-200">
<div>
<div className="text-xs uppercase text-gray-500">Current price</div>
<div className="font-semibold">{formatCurrency(snapshot.current_price)}</div>
</div>
<div>
<div className="text-xs uppercase text-gray-500">Change</div>
<div
className={`font-semibold ${snapshot.change && snapshot.change < 0 ? 'text-red-400' : snapshot.change && snapshot.change > 0 ? 'text-green-400' : 'text-gray-200'}`}
>
{formatNumber(snapshot.change)} ({formatPercent(snapshot.change_percent)})
</div>
</div>
<div>
<div className="text-xs uppercase text-gray-500">ATR(14)</div>
<div className="font-semibold">{formatNumber(snapshot.atr14)}</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 text-sm text-gray-200">
<div>
<div className="text-xs uppercase text-gray-500">RSI(14)</div>
<div className="font-semibold">{formatNumber(snapshot.rsi14)}</div>
</div>
<div>
<div className="text-xs uppercase text-gray-500">Volatility 30</div>
<div className="font-semibold">{formatPercent(snapshot.volatility30)}</div>
</div>
<div>
<div className="text-xs uppercase text-gray-500">Momentum 12</div>
<div className="font-semibold">{formatNumber(snapshot.momentum12)}</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 text-xs text-gray-400">
<div>
<div className="uppercase tracking-wide text-gray-500 mb-1">Support levels</div>
<ul className="space-y-1">
{snapshot.support_levels.length ? (
snapshot.support_levels.map(level => (
<li key={`support-${level}`}>{formatCurrency(level)}</li>
))
) : (
<li>None detected</li>
)}
</ul>
</div>
<div>
<div className="uppercase tracking-wide text-gray-500 mb-1">Resistance levels</div>
<ul className="space-y-1">
{snapshot.resistance_levels.length ? (
snapshot.resistance_levels.map(level => (
<li key={`resistance-${level}`}>{formatCurrency(level)}</li>
))
) : (
<li>None detected</li>
)}
</ul>
</div>
</div>
<div className="text-xs text-gray-500">Bars analysed: {snapshot.bars_analyzed}</div>
{canApplySuggestions && (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-t border-dark-border pt-3 text-xs text-gray-400">
<div>
Suggested guard rails · SL {formatCurrency(suggestedStopLoss)} · TP {formatCurrency(suggestedTakeProfit)} ·
Trailing {formatPercent(suggestedTrailingPercent)}
</div>
<button className="btn-primary text-xs" onClick={handleApplySuggestions} type="button">
Apply guard suggestions
</button>
</div>
)}
</div>
) : (
<div className="text-xs text-gray-500">Snapshot unavailable. Refresh to load the latest metrics.</div>
)}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-blue-400" />
<h3 className="text-lg font-semibold">Automation Guards</h3>
</div>
<button
onClick={onClearGuards}
className="text-xs text-gray-400 hover:text-white"
>
Clear targets
</button>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p className="text-xs uppercase tracking-wide text-gray-400">Stop Loss</p>
{stopLoss ? (
<p className="text-lg font-semibold text-red-400">${stopLoss.toFixed(2)}</p>
) : (
<p className="text-gray-500">Not armed</p>
)}
</div>
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p className="text-xs uppercase tracking-wide text-gray-400">Take Profit</p>
{takeProfit ? (
<p className="text-lg font-semibold text-green-400">${takeProfit.toFixed(2)}</p>
) : (
<p className="text-gray-500">Not armed</p>
)}
</div>
</div>
<div className="flex items-center justify-between bg-dark-bg rounded-lg p-3 border border-dark-border">
<div>
<p className="text-xs text-gray-400">Auto close</p>
<p className="text-sm font-medium text-gray-200">
{autoCloseEnabled ? 'Triggers will flatten positions' : 'Manual acknowledgement required'}
</p>
</div>
<button
onClick={() => onToggleAutoClose(!autoCloseEnabled)}
className={`px-3 py-1 rounded-full text-xs font-semibold ${
autoCloseEnabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-700 text-gray-300'
}`}
>
{autoCloseEnabled ? 'Enabled' : 'Disabled'}
</button>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-yellow-400" />
<span>Trailing stop (%)</span>
</div>
{trailingActive && (
<button
onClick={() => onUpdateTrailing(null)}
className="text-xs text-gray-400 hover:text-white"
>
Disable
</button>
)}
</div>
<div className="flex items-center gap-2">
<input
type="range"
min="0.5"
max="5"
step="0.1"
value={localTrailing}
onChange={(e) => setLocalTrailing(Number(e.target.value))}
className="flex-1"
/>
<span className="w-12 text-sm text-right">{localTrailing.toFixed(1)}%</span>
</div>
<button
onClick={() => onUpdateTrailing(localTrailing)}
className="btn-primary text-xs"
>
{trailingActive ? 'Update trailing stop' : 'Enable trailing stop'}
</button>
</div>
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
<div className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-gray-400" />
<p className="text-sm font-semibold">Latest event</p>
</div>
{events.length === 0 ? (
<p className="text-xs text-gray-500">Awaiting first automation event...</p>
) : (
<div className="text-xs text-gray-300 space-y-1">
{events.slice(-3).reverse().map(event => (
<div key={event.id} className="flex items-start gap-2">
<AlertTriangle className="w-3 h-3 mt-0.5 text-amber-400" />
<div>
<p className="font-medium">{event.description}</p>
<p className="text-[10px] text-gray-500">
{new Date(event.timestamp).toLocaleTimeString()}
</p>
</div>
</div>
))}
</div>
)}
</div>
<p className="text-xs text-gray-500 text-center">
Current price {currentPrice.toFixed(2)} automation guards will arm broker actions as soon as the backend is wired.
</p>
</div>
)
}
@@ -0,0 +1,336 @@
import { useMemo } from 'react';
import { Activity, TrendingUp, TrendingDown } from 'lucide-react';
/**
* Slippage Correlation Analysis
* Analyzes slippage costs and their correlation with:
* - Market volatility (ATR)
* - Volume levels
* - Time of day
* - Market conditions
*/
export interface VolatilityBucket {
range: string;
minVolatility: number;
maxVolatility: number;
tradesCount: number;
avgSlippage: number;
winRate: number;
profitability: number; // avg P&L after slippage
slippageImpact: number; // % of profit lost to slippage
}
export interface SlippageCorrelationProps {
trades: Array<{
id: string;
entry: number;
exit: number;
slippage: number; // Cost in dollars
volatility?: number; // ATR or similar
volume?: number; // Trade volume
profitable: boolean;
pnl: number;
grossPnL: number; // Before slippage
timestamp?: string;
}>;
onVolatilityRangeSelect?: (range: VolatilityBucket) => void;
}
export default function SlippageCorrelationAnalysis({
trades,
onVolatilityRangeSelect,
}: SlippageCorrelationProps) {
const volatilityAnalysis = useMemo(() => {
if (trades.length === 0) return [];
// Define volatility buckets
const buckets: VolatilityBucket[] = [
{ range: 'Very Low', minVolatility: 0, maxVolatility: 0.5, tradesCount: 0, avgSlippage: 0, winRate: 0, profitability: 0, slippageImpact: 0 },
{ range: 'Low', minVolatility: 0.5, maxVolatility: 1.0, tradesCount: 0, avgSlippage: 0, winRate: 0, profitability: 0, slippageImpact: 0 },
{ range: 'Medium', minVolatility: 1.0, maxVolatility: 1.5, tradesCount: 0, avgSlippage: 0, winRate: 0, profitability: 0, slippageImpact: 0 },
{ range: 'High', minVolatility: 1.5, maxVolatility: 2.5, tradesCount: 0, avgSlippage: 0, winRate: 0, profitability: 0, slippageImpact: 0 },
{ range: 'Very High', minVolatility: 2.5, maxVolatility: 100, tradesCount: 0, avgSlippage: 0, winRate: 0, profitability: 0, slippageImpact: 0 },
];
// Assign trades to buckets
trades.forEach(trade => {
const volatility = trade.volatility || 1.0;
const bucket = buckets.find(b => volatility >= b.minVolatility && volatility < b.maxVolatility);
if (bucket) {
bucket.tradesCount += 1;
bucket.avgSlippage += trade.slippage;
bucket.profitability += trade.pnl;
if (trade.profitable) bucket.winRate += 1;
// Calculate slippage impact
const grossPnL = trade.grossPnL || (trade.pnl + trade.slippage);
bucket.slippageImpact += grossPnL > 0 ? (trade.slippage / grossPnL) * 100 : 0;
}
});
// Finalize calculations
return buckets
.filter(b => b.tradesCount > 0)
.map(bucket => ({
...bucket,
avgSlippage: bucket.avgSlippage / bucket.tradesCount,
winRate: (bucket.winRate / bucket.tradesCount) * 100,
profitability: bucket.profitability / bucket.tradesCount,
slippageImpact: bucket.slippageImpact / bucket.tradesCount,
}));
}, [trades]);
const slippageMetrics = useMemo(() => {
if (trades.length === 0) {
return {
totalTrades: 0,
avgSlippage: 0,
totalSlippageCost: 0,
maxSlippage: 0,
minSlippage: 0,
slippageVariance: 0,
profitBeforeSlippage: 0,
profitAfterSlippage: 0,
slippageImpactPercent: 0,
};
}
const totalSlippage = trades.reduce((sum, t) => sum + t.slippage, 0);
const avgSlippage = totalSlippage / trades.length;
const slippages = trades.map(t => t.slippage);
const maxSlippage = Math.max(...slippages);
const minSlippage = Math.min(...slippages);
// Calculate variance
const slippageVariance = slippages.reduce((sum, s) => sum + Math.pow(s - avgSlippage, 2), 0) / slippages.length;
const profitBeforeSlippage = trades.reduce((sum, t) => sum + (t.grossPnL || (t.pnl + t.slippage)), 0);
const profitAfterSlippage = trades.reduce((sum, t) => sum + t.pnl, 0);
const slippageImpactPercent = profitBeforeSlippage > 0 ? (totalSlippage / profitBeforeSlippage) * 100 : 0;
return {
totalTrades: trades.length,
avgSlippage,
totalSlippageCost: totalSlippage,
maxSlippage,
minSlippage,
slippageVariance: Math.sqrt(slippageVariance),
profitBeforeSlippage,
profitAfterSlippage,
slippageImpactPercent,
};
}, [trades]);
const bestVolatilityBucket = useMemo(() => {
return volatilityAnalysis.length > 0
? volatilityAnalysis.reduce((best, current) =>
current.profitability > best.profitability ? current : best
)
: null;
}, [volatilityAnalysis]);
if (trades.length === 0) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 p-6 text-center">
<Activity className="w-8 h-8 text-slate-600 mx-auto mb-2" />
<p className="text-slate-400">No trading data available</p>
<p className="text-xs text-slate-600 mt-1">Slippage analysis will appear here</p>
</div>
);
}
return (
<div className="rounded-lg border border-slate-700 bg-slate-900 space-y-4 overflow-hidden">
{/* Header Summary */}
<div className="bg-slate-800/50 p-4 border-b border-slate-700">
<h3 className="font-semibold text-slate-200 mb-3">Slippage & Correlation Analysis</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Total Slippage Cost</p>
<p className="text-lg font-bold text-red-400">${slippageMetrics.totalSlippageCost.toFixed(2)}</p>
<p className="text-xs text-slate-600">
${slippageMetrics.avgSlippage.toFixed(2)}/trade avg
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Profit Before Slippage</p>
<p className={`text-lg font-bold ${slippageMetrics.profitBeforeSlippage >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${slippageMetrics.profitBeforeSlippage.toFixed(2)}
</p>
<p className="text-xs text-slate-600">
Gross P&L
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Profit After Slippage</p>
<p className={`text-lg font-bold ${slippageMetrics.profitAfterSlippage >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${slippageMetrics.profitAfterSlippage.toFixed(2)}
</p>
<p className="text-xs text-slate-600">
Net P&L
</p>
</div>
<div className="bg-slate-800/50 rounded px-3 py-2">
<p className="text-xs text-slate-500">Slippage Impact</p>
<p className={`text-lg font-bold ${slippageMetrics.slippageImpactPercent <= 5 ? 'text-emerald-400' : slippageMetrics.slippageImpactPercent <= 10 ? 'text-amber-400' : 'text-red-400'}`}>
{slippageMetrics.slippageImpactPercent.toFixed(1)}%
</p>
<p className="text-xs text-slate-600">
Of gross profit
</p>
</div>
</div>
</div>
{/* Volatility Buckets */}
<div className="px-4 pb-4 space-y-2">
<p className="text-xs text-slate-500 font-semibold mb-2">Performance by Market Volatility</p>
{volatilityAnalysis.map((bucket) => {
const isBest = bestVolatilityBucket?.range === bucket.range;
const profitColor = bucket.profitability >= 0 ? 'text-emerald-400' : 'text-red-400';
const slippageImpactColor =
bucket.slippageImpact <= 5 ? 'text-emerald-400' :
bucket.slippageImpact <= 10 ? 'text-amber-400' :
'text-red-400';
return (
<div
key={bucket.range}
onClick={() => onVolatilityRangeSelect?.(bucket)}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
isBest
? 'border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10'
: 'border-slate-700 bg-slate-800/30 hover:bg-slate-800/50'
}`}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
bucket.profitability >= 0 ? 'bg-emerald-500/20' : 'bg-red-500/20'
}`}>
{bucket.profitability >= 0 ? (
<TrendingUp className="w-4 h-4 text-emerald-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<p className="text-sm font-semibold text-slate-200">{bucket.range} Volatility</p>
<p className="text-xs text-slate-500">{bucket.tradesCount} trades</p>
</div>
{isBest && (
<span className="ml-auto mr-2 text-xs bg-emerald-500/20 text-emerald-300 px-2 py-1 rounded font-semibold">
Best
</span>
)}
</div>
<div className="text-right">
<p className={`text-sm font-bold font-mono ${profitColor}`}>
{bucket.profitability >= 0 ? '+' : ''}{bucket.profitability.toFixed(2)}
</p>
<p className="text-xs font-mono text-slate-500">
per trade
</p>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-4 gap-2 text-xs">
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Win Rate</p>
<p className="font-bold text-emerald-400">{bucket.winRate.toFixed(1)}%</p>
<p className="text-slate-600">Profitable</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Avg Slippage</p>
<p className="font-bold text-red-400">${bucket.avgSlippage.toFixed(2)}</p>
<p className="text-slate-600">Per trade</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Slippage Impact</p>
<p className={`font-bold ${slippageImpactColor}`}>{bucket.slippageImpact.toFixed(1)}%</p>
<p className="text-slate-600">Of profit</p>
</div>
<div className="bg-slate-800/40 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Profitability</p>
<p className={`font-bold ${profitColor}`}>{bucket.profitability.toFixed(2)}</p>
<p className="text-slate-600">Net/trade</p>
</div>
</div>
{/* Metrics Bars */}
<div className="mt-2 space-y-1">
<div className="text-xs">
<div className="flex justify-between mb-0.5">
<span className="text-slate-500">Slippage Impact</span>
<span className="text-slate-400">{bucket.slippageImpact.toFixed(1)}%</span>
</div>
<div className="h-1 bg-slate-700/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${slippageImpactColor}`}
style={{ width: `${Math.min(bucket.slippageImpact, 100)}%` }}
/>
</div>
</div>
</div>
</div>
);
})}
</div>
{/* Statistics */}
<div className="px-4 py-3 bg-slate-800/30 border-t border-slate-700 space-y-2 text-xs">
<div className="grid grid-cols-2 gap-2">
<div>
<p className="text-slate-500 mb-1">Slippage Range</p>
<p className="font-mono text-slate-300">
${slippageMetrics.minSlippage.toFixed(2)} - ${slippageMetrics.maxSlippage.toFixed(2)}
</p>
</div>
<div>
<p className="text-slate-500 mb-1">Slippage Consistency</p>
<p className="font-mono text-slate-300">
±${slippageMetrics.slippageVariance.toFixed(2)} (StdDev)
</p>
</div>
</div>
</div>
{/* Recommendation */}
<div className={`px-4 py-3 border-t border-slate-700 text-xs ${
slippageMetrics.slippageImpactPercent <= 5
? 'bg-emerald-500/10 text-emerald-200'
: slippageMetrics.slippageImpactPercent <= 10
? 'bg-amber-500/10 text-amber-200'
: 'bg-red-500/10 text-red-200'
}`}>
<p className="font-semibold mb-1">
{slippageMetrics.slippageImpactPercent <= 5 ? '✓' : slippageMetrics.slippageImpactPercent <= 10 ? '⚠' : '✗'} Slippage Analysis:
</p>
<p>
{slippageMetrics.slippageImpactPercent <= 5
? `Excellent slippage management (${slippageMetrics.slippageImpactPercent.toFixed(1)}% impact). Continue current execution strategy.`
: slippageMetrics.slippageImpactPercent <= 10
? `Moderate slippage (${slippageMetrics.slippageImpactPercent.toFixed(1)}% impact). Consider using limit orders or trading lower volatility periods.`
: `High slippage costs (${slippageMetrics.slippageImpactPercent.toFixed(1)}% impact). Trade only in best volatility conditions and focus on larger moves.`
}
{bestVolatilityBucket && ` Best results in <span class="font-mono">${bestVolatilityBucket.range}</span> volatility conditions.`}
</p>
</div>
</div>
);
}
@@ -0,0 +1,295 @@
import { useState, useCallback, useEffect } from 'react';
import { Zap, TrendingUp, Layers } from 'lucide-react';
export type StrategyMode = 'SCALP' | 'SWING' | 'HYBRID';
export interface StrategyPreset {
mode: StrategyMode;
riskPerTrade: number; // % of capital per trade
stopLossPercent: number; // % stop loss
takeProfitPercent: number; // % take profit
timeFrame: string; // '1m', '5m', '4h', 'daily'
maxHoldMinutes: number; // Max hold time
maxDailyTrades: number; // Max trades per day
r2rRatio: number; // Risk to Reward ratio
description: string;
emoji: string;
}
export const STRATEGY_PRESETS: Record<StrategyMode, StrategyPreset> = {
SCALP: {
mode: 'SCALP',
riskPerTrade: 0.25,
stopLossPercent: 0.5,
takeProfitPercent: 1,
timeFrame: '1m',
maxHoldMinutes: 5,
maxDailyTrades: 20,
r2rRatio: 1,
description: 'Quick profits from micro price moves. High frequency, tight stops.',
emoji: '⚡',
},
SWING: {
mode: 'SWING',
riskPerTrade: 2,
stopLossPercent: 2,
takeProfitPercent: 8,
timeFrame: 'daily',
maxHoldMinutes: 24 * 60, // 1 day minimum
maxDailyTrades: 3,
r2rRatio: 3,
description: 'Trend capture over days. Lower frequency, larger targets.',
emoji: '📈',
},
HYBRID: {
mode: 'HYBRID',
riskPerTrade: 1.25, // Average of both
stopLossPercent: 1.25,
takeProfitPercent: 4.5,
timeFrame: 'mixed', // Both 5m and daily
maxHoldMinutes: 120, // 2 hours balance
maxDailyTrades: 10,
r2rRatio: 2,
description: '70% swing + 30% scalp. Best of both: trend capture + daily income.',
emoji: '🎯',
},
};
interface StrategyModeSelectorProps {
onModeChange?: (mode: StrategyMode, preset: StrategyPreset) => void;
defaultMode?: StrategyMode;
variant?: 'full' | 'compact';
}
export default function StrategyModeSelector({
onModeChange,
defaultMode = 'SWING',
variant = 'full',
}: StrategyModeSelectorProps) {
const [selectedMode, setSelectedMode] = useState<StrategyMode>(defaultMode);
const [showDetails, setShowDetails] = useState(false);
// Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('trading-strategy-mode');
if (saved && (saved === 'SCALP' || saved === 'SWING' || saved === 'HYBRID')) {
setSelectedMode(saved);
}
}, []);
const handleModeChange = useCallback(
(mode: StrategyMode) => {
setSelectedMode(mode);
localStorage.setItem('trading-strategy-mode', mode);
const preset = STRATEGY_PRESETS[mode];
onModeChange?.(mode, preset);
},
[onModeChange]
);
const preset = STRATEGY_PRESETS[selectedMode];
if (variant === 'compact') {
return (
<div className="flex gap-2">
{(Object.keys(STRATEGY_PRESETS) as StrategyMode[]).map((mode) => (
<button
key={mode}
onClick={() => handleModeChange(mode)}
className={`px-3 py-1 text-xs font-semibold rounded transition ${
selectedMode === mode
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{STRATEGY_PRESETS[mode].emoji} {mode}
</button>
))}
</div>
);
}
return (
<div className="card space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Layers className="w-5 h-5 text-blue-500" />
Trading Strategy Mode
</h3>
<button
onClick={() => setShowDetails(!showDetails)}
className="text-xs text-blue-400 hover:text-blue-300 underline"
>
{showDetails ? 'Hide' : 'Show'} Details
</button>
</div>
{/* Mode Selector Buttons */}
<div className="grid grid-cols-3 gap-2">
{(Object.keys(STRATEGY_PRESETS) as StrategyMode[]).map((mode) => {
const modePreset = STRATEGY_PRESETS[mode];
const isSelected = selectedMode === mode;
return (
<button
key={mode}
onClick={() => handleModeChange(mode)}
className={`p-4 rounded-lg border-2 transition text-center ${
isSelected
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 bg-gray-900/50 hover:border-gray-600'
}`}
>
<div className="text-2xl mb-2">{modePreset.emoji}</div>
<div className="font-semibold text-sm">{mode}</div>
<div className="text-xs text-gray-400 mt-1">
{mode === 'SCALP' && 'Quick Moves'}
{mode === 'SWING' && 'Trend Capture'}
{mode === 'HYBRID' && 'Balanced'}
</div>
</button>
);
})}
</div>
{/* Current Mode Info */}
<div className="p-3 bg-blue-500/10 rounded-lg border border-blue-500/30">
<p className="text-sm text-gray-300 mb-2">{preset.description}</p>
</div>
{/* Detailed Parameters (Toggle) */}
{showDetails && (
<div className="p-4 bg-dark-bg rounded-lg border border-dark-border space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
{/* Scalp Parameters */}
<div className="col-span-2">
<h4 className="font-semibold text-xs text-gray-400 mb-2 uppercase">
Risk Management
</h4>
</div>
<div>
<span className="text-gray-400">Risk per Trade:</span>
<div className="font-medium text-blue-400">{preset.riskPerTrade}%</div>
</div>
<div>
<span className="text-gray-400">Stop Loss:</span>
<div className="font-medium text-red-400">{preset.stopLossPercent}%</div>
</div>
<div>
<span className="text-gray-400">Take Profit:</span>
<div className="font-medium text-green-400">{preset.takeProfitPercent}%</div>
</div>
<div>
<span className="text-gray-400">R:R Ratio:</span>
<div className="font-medium text-yellow-400">1:{preset.r2rRatio.toFixed(1)}</div>
</div>
{/* Time Parameters */}
<div className="col-span-2 border-t border-dark-border pt-3 mt-3">
<h4 className="font-semibold text-xs text-gray-400 mb-2 uppercase">
Time & Frequency
</h4>
</div>
<div>
<span className="text-gray-400">Time Frame:</span>
<div className="font-medium text-purple-400">{preset.timeFrame}</div>
</div>
<div>
<span className="text-gray-400">Max Hold Time:</span>
<div className="font-medium text-purple-400">
{preset.maxHoldMinutes >= 60
? `${(preset.maxHoldMinutes / 60).toFixed(1)}h`
: `${preset.maxHoldMinutes}m`}
</div>
</div>
<div className="col-span-2">
<span className="text-gray-400">Max Daily Trades:</span>
<div className="font-medium text-purple-400">{preset.maxDailyTrades} trades</div>
</div>
</div>
{/* Strategy Tips */}
<div className="border-t border-dark-border pt-3 mt-3">
<h4 className="font-semibold text-xs text-gray-400 mb-2 uppercase">
💡 Strategy Tips
</h4>
<ul className="text-xs text-gray-400 space-y-1 list-disc list-inside">
{selectedMode === 'SCALP' && (
<>
<li>Use 1-5 min charts for entry signals</li>
<li>Close 50% at 0.5% profit, let 50% run to 1%</li>
<li>Avoid holding through market chop</li>
<li>Speed is critical - execute fast</li>
<li>Max 5-20 trades per day depending on volatility</li>
</>
)}
{selectedMode === 'SWING' && (
<>
<li>Confirm trends with EMA alignment</li>
<li>Use support/resistance for entries</li>
<li>Partial profit taking at 1:2, 1:3 levels</li>
<li>Use trailing stops to protect gains</li>
<li>Hold 1-5 days for trend capture</li>
</>
)}
{selectedMode === 'HYBRID' && (
<>
<li>Allocate 70% capital to swing trades</li>
<li>Allocate 30% capital to scalping</li>
<li>Scalping provides daily income buffer</li>
<li>Swings capture larger trends</li>
<li>Balance reduces psychological stress</li>
</>
)}
</ul>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2 pt-2">
<button
onClick={() => handleModeChange('SCALP')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition ${
selectedMode === 'SCALP'
? 'bg-yellow-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
<Zap className="w-4 h-4 inline mr-1" />
Scalping
</button>
<button
onClick={() => handleModeChange('SWING')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition ${
selectedMode === 'SWING'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
<TrendingUp className="w-4 h-4 inline mr-1" />
Swing
</button>
<button
onClick={() => handleModeChange('HYBRID')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition ${
selectedMode === 'HYBRID'
? 'bg-purple-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
<Layers className="w-4 h-4 inline mr-1" />
Hybrid
</button>
</div>
</div>
);
}
@@ -0,0 +1,329 @@
import { useMemo } from 'react';
import { TrendingUp, TrendingDown, AlertCircle, CheckCircle } from 'lucide-react';
/**
* Trend Analysis using EMA alignment, MACD, and RSI confirmation
* For swing trading - ensures strong directional bias before entry
*/
export interface TrendStrength {
direction: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
strength: 'WEAK' | 'MODERATE' | 'STRONG' | 'VERY_STRONG';
confidence: number; // 0-100%
score: number; // 0-100
}
export interface TrendConfirmationProps {
// Price and indicators
ema8: number;
ema21: number;
ema55: number;
ema200: number;
macdLine: number;
macdSignal: number;
rsi: number;
// Optional context
timeframe?: string;
onTrendUpdate?: (trend: TrendStrength) => void;
}
export default function TrendConfirmation({
ema8,
ema21,
ema55,
ema200,
macdLine,
macdSignal,
rsi,
timeframe = '4h',
onTrendUpdate,
}: TrendConfirmationProps) {
const trendAnalysis = useMemo(() => {
const analysis: TrendStrength = {
direction: 'NEUTRAL',
strength: 'WEAK',
confidence: 0,
score: 0,
};
let bullishScore = 0;
let bearishScore = 0;
const maxScore = 100;
// ===== EMA ALIGNMENT (40 points total) =====
// Short-term trend: EMA8 > EMA21
if (ema8 > ema21) {
bullishScore += 10;
} else if (ema8 < ema21) {
bearishScore += 10;
}
// Medium-term trend: EMA21 > EMA55
if (ema21 > ema55) {
bullishScore += 15;
} else if (ema21 < ema55) {
bearishScore += 15;
}
// Long-term trend: EMA55 > EMA200 (most important for swing)
if (ema55 > ema200) {
bullishScore += 15;
} else if (ema55 < ema200) {
bearishScore += 15;
}
// ===== MACD CONFIRMATION (35 points total) =====
// MACD line above signal line (bullish)
if (macdLine > macdSignal) {
bullishScore += 20;
} else if (macdLine < macdSignal) {
bearishScore += 20;
}
// MACD histogram distance (15 points)
const macdDiff = Math.abs(macdLine - macdSignal);
if (macdDiff > 0.5) {
if (macdLine > macdSignal) {
bullishScore += 15;
} else {
bearishScore += 15;
}
} else {
// Weak crossover, neutral region
bullishScore += 7;
bearishScore += 7;
}
// ===== RSI CONFIRMATION (25 points total) =====
// RSI > 50 (bullish bias)
if (rsi > 60) {
bullishScore += 15; // Strong bullish
} else if (rsi > 50) {
bullishScore += 10; // Mild bullish
}
// RSI < 50 (bearish bias)
if (rsi < 40) {
bearishScore += 15; // Strong bearish
} else if (rsi < 50) {
bearishScore += 10; // Mild bearish
}
// Avoid extremes for swing trading
if (rsi > 80 || rsi < 20) {
// Potential reversal zone - reduce confidence
if (rsi > 80) bearishScore += 5;
if (rsi < 20) bullishScore += 5;
}
// ===== DETERMINE DIRECTION AND STRENGTH =====
const normalizedBullish = (bullishScore / maxScore) * 100;
const normalizedBearish = (bearishScore / maxScore) * 100;
if (normalizedBullish > normalizedBearish + 10) {
analysis.direction = 'BULLISH';
analysis.score = normalizedBullish;
analysis.confidence = Math.min(normalizedBullish, 100);
if (normalizedBullish > 80) {
analysis.strength = 'VERY_STRONG';
} else if (normalizedBullish > 65) {
analysis.strength = 'STRONG';
} else if (normalizedBullish > 50) {
analysis.strength = 'MODERATE';
} else {
analysis.strength = 'WEAK';
}
} else if (normalizedBearish > normalizedBullish + 10) {
analysis.direction = 'BEARISH';
analysis.score = normalizedBearish;
analysis.confidence = Math.min(normalizedBearish, 100);
if (normalizedBearish > 80) {
analysis.strength = 'VERY_STRONG';
} else if (normalizedBearish > 65) {
analysis.strength = 'STRONG';
} else if (normalizedBearish > 50) {
analysis.strength = 'MODERATE';
} else {
analysis.strength = 'WEAK';
}
} else {
analysis.direction = 'NEUTRAL';
analysis.strength = 'WEAK';
analysis.confidence = 0;
analysis.score = Math.max(normalizedBullish, normalizedBearish);
}
return analysis;
}, [ema8, ema21, ema55, ema200, macdLine, macdSignal, rsi]);
// Trigger callback on update
if (onTrendUpdate) {
onTrendUpdate(trendAnalysis);
}
// Color and icon based on trend
const isBullish = trendAnalysis.direction === 'BULLISH';
const isBearish = trendAnalysis.direction === 'BEARISH';
const isNeutral = trendAnalysis.direction === 'NEUTRAL';
const bgColor = isBullish ? 'bg-emerald-500/10' : isBearish ? 'bg-red-500/10' : 'bg-slate-700/30';
const borderColor = isBullish ? 'border-emerald-500/40' : isBearish ? 'border-red-500/40' : 'border-slate-600';
const textColor = isBullish ? 'text-emerald-300' : isBearish ? 'text-red-300' : 'text-slate-400';
const strengthColors = {
VERY_STRONG: isBullish ? 'text-emerald-500' : isBearish ? 'text-red-500' : 'text-slate-500',
STRONG: isBullish ? 'text-emerald-400' : isBearish ? 'text-red-400' : 'text-slate-400',
MODERATE: isBullish ? 'text-emerald-300' : isBearish ? 'text-red-300' : 'text-slate-400',
WEAK: 'text-slate-500',
};
const getStrengthBar = () => {
const width = Math.max(trendAnalysis.confidence, 15); // Minimum visual width
return `${width}%`;
};
return (
<div className={`rounded-lg border ${borderColor} ${bgColor} p-4 space-y-3`}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isBullish && <TrendingUp className="w-5 h-5 text-emerald-400" />}
{isBearish && <TrendingDown className="w-5 h-5 text-red-400" />}
{isNeutral && <AlertCircle className="w-5 h-5 text-slate-500" />}
<h3 className={`font-semibold ${textColor}`}>Trend Confirmation</h3>
</div>
<span className="text-xs font-mono text-slate-500">{timeframe}</span>
</div>
{/* Trend Direction & Strength */}
<div className="bg-slate-800/50 rounded px-3 py-2 flex items-center justify-between">
<div>
<p className={`font-bold text-lg ${textColor}`}>
{trendAnalysis.direction}
</p>
<p className={`text-xs ${strengthColors[trendAnalysis.strength]}`}>
{trendAnalysis.strength} ({trendAnalysis.confidence.toFixed(0)}% confidence)
</p>
</div>
<CheckCircle className={`w-6 h-6 ${
trendAnalysis.strength === 'VERY_STRONG' ? (isBullish ? 'text-emerald-500' : 'text-red-500') :
trendAnalysis.strength === 'STRONG' ? (isBullish ? 'text-emerald-400' : 'text-red-400') :
trendAnalysis.strength === 'MODERATE' ? 'text-amber-400' : 'text-slate-500'
}`} />
</div>
{/* Confidence Bar */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-400">Trend Strength</span>
<span className="text-xs font-mono text-slate-500">{trendAnalysis.score.toFixed(0)}/100</span>
</div>
<div className="h-2 bg-slate-700/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${
isBullish ? 'bg-emerald-500' : isBearish ? 'bg-red-500' : 'bg-slate-500'
}`}
style={{ width: getStrengthBar() }}
/>
</div>
</div>
{/* EMA Alignment Details */}
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="bg-slate-800/30 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Short Term</p>
<p className={`font-mono font-semibold ${ema8 > ema21 ? 'text-emerald-400' : ema8 < ema21 ? 'text-red-400' : 'text-slate-400'}`}>
EMA8 {ema8 > ema21 ? '↑' : ema8 < ema21 ? '↓' : '='}
</p>
<p className="text-slate-600 text-xs">{ema8.toFixed(2)}</p>
</div>
<div className="bg-slate-800/30 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Medium Term</p>
<p className={`font-mono font-semibold ${ema21 > ema55 ? 'text-emerald-400' : ema21 < ema55 ? 'text-red-400' : 'text-slate-400'}`}>
EMA21 {ema21 > ema55 ? '↑' : ema21 < ema55 ? '↓' : '='}
</p>
<p className="text-slate-600 text-xs">{ema21.toFixed(2)}</p>
</div>
<div className="bg-slate-800/30 rounded px-2 py-1.5">
<p className="text-slate-500 mb-0.5">Long Term</p>
<p className={`font-mono font-semibold ${ema55 > ema200 ? 'text-emerald-400' : ema55 < ema200 ? 'text-red-400' : 'text-slate-400'}`}>
EMA55 {ema55 > ema200 ? '↑' : ema55 < ema200 ? '↓' : '='}
</p>
<p className="text-slate-600 text-xs">{ema55.toFixed(2)}</p>
</div>
</div>
{/* MACD Status */}
<div className="bg-slate-800/30 rounded px-3 py-2 flex items-center justify-between">
<div className="text-xs">
<p className="text-slate-500 mb-1">MACD Signal</p>
<p className={`font-mono ${macdLine > macdSignal ? 'text-emerald-400' : macdLine < macdSignal ? 'text-red-400' : 'text-slate-400'}`}>
{macdLine > macdSignal ? '✓ Bullish Alignment' : macdLine < macdSignal ? '✗ Bearish Alignment' : '— Neutral Alignment'}
</p>
</div>
<span className="text-xs text-slate-600 font-mono">{Math.abs(macdLine - macdSignal).toFixed(4)}</span>
</div>
{/* RSI Status */}
<div className="bg-slate-800/30 rounded px-3 py-2 flex items-center justify-between">
<div className="text-xs">
<p className="text-slate-500 mb-1">RSI Condition</p>
<p className={`font-mono ${
rsi > 60 ? 'text-emerald-400' :
rsi > 50 ? 'text-emerald-300' :
rsi < 40 ? 'text-red-400' :
rsi < 50 ? 'text-red-300' :
'text-slate-400'
}`}>
RSI {rsi.toFixed(1)} {
rsi > 70 ? '(Overbought - Caution)' :
rsi > 60 ? '(Bullish)' :
rsi > 40 ? '(Neutral)' :
rsi < 30 ? '(Oversold - Caution)' :
'(Bearish)'
}
</p>
</div>
<span className="text-xs text-slate-600 font-mono">{rsi.toFixed(1)}%</span>
</div>
{/* Recommendation */}
<div className={`text-xs rounded px-3 py-2 ${
trendAnalysis.strength === 'VERY_STRONG'
? isBullish
? 'bg-emerald-500/15 text-emerald-200 border border-emerald-500/30'
: 'bg-red-500/15 text-red-200 border border-red-500/30'
: trendAnalysis.strength === 'STRONG'
? 'bg-amber-500/15 text-amber-200 border border-amber-500/30'
: 'bg-slate-700/30 text-slate-300 border border-slate-600'
}`}>
{trendAnalysis.strength === 'VERY_STRONG' && (
<p>
<span className="font-semibold"> Strong Entry Signal:</span> Trend is {trendAnalysis.direction.toLowerCase()} with excellent confirmation.
All indicators aligned. Ideal for swing entry.
</p>
)}
{trendAnalysis.strength === 'STRONG' && (
<p>
<span className="font-semibold"> Good Entry Signal:</span> Trend is {trendAnalysis.direction.toLowerCase()} with strong confirmation.
Ready for swing entry.
</p>
)}
{trendAnalysis.strength === 'MODERATE' && (
<p>
<span className="font-semibold"> Moderate Signal:</span> Trend is {trendAnalysis.direction.toLowerCase()} but confirmation mixed.
Wait for stronger alignment or reduce position size.
</p>
)}
{trendAnalysis.strength === 'WEAK' && (
<p>
<span className="font-semibold"> Weak Signal:</span> Trend is unclear. Wait for clearer confirmation before entering.
</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,71 @@
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
export interface PlanBiasSelectorProps {
bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
isEditing: boolean;
onChange: (bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL') => void;
}
export function PlanBiasSelector({ bias, isEditing, onChange }: PlanBiasSelectorProps): JSX.Element {
const biasOptions: Array<{ value: 'BULLISH' | 'BEARISH' | 'NEUTRAL'; label: string; icon: JSX.Element; color: 'green' | 'slate' | 'red' }> = [
{ value: 'BULLISH', label: 'Bullish', icon: <TrendingUp className="h-5 w-5" aria-hidden="true" />, color: 'green' },
{ value: 'NEUTRAL', label: 'Neutral', icon: <Minus className="h-5 w-5" aria-hidden="true" />, color: 'slate' },
{ value: 'BEARISH', label: 'Bearish', icon: <TrendingDown className="h-5 w-5" aria-hidden="true" />, color: 'red' },
];
const selectedOption = biasOptions.find((opt) => opt.value === bias);
if (!isEditing && selectedOption) {
const colorClasses = {
green: 'border-green-500/50 bg-green-500/10 text-green-300',
slate: 'border-slate-500/50 bg-slate-500/10 text-slate-300',
red: 'border-red-500/50 bg-red-500/10 text-red-300',
};
return (
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Market Bias</label>
<div className={`inline-flex items-center gap-2 rounded-lg border px-4 py-2 ${colorClasses[selectedOption.color]}`}>
{selectedOption.icon}
<span className="font-semibold">{selectedOption.label}</span>
</div>
</div>
);
}
return (
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Market Bias</label>
<div className="flex gap-2">
{biasOptions.map((option) => {
const isSelected = bias === option.value;
const baseClasses = 'flex-1 inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2 font-medium transition-colors cursor-pointer';
const colorClasses = {
green: isSelected
? 'border-green-500 bg-green-500/20 text-green-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-green-500/50 hover:text-green-400',
slate: isSelected
? 'border-slate-500 bg-slate-500/20 text-slate-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-500/50 hover:text-slate-300',
red: isSelected
? 'border-red-500 bg-red-500/20 text-red-300'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-red-500/50 hover:text-red-400',
};
return (
<button
key={option.value}
onClick={() => onChange(option.value)}
className={`${baseClasses} ${colorClasses[option.color]}`}
aria-pressed={isSelected}
>
{option.icon}
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,77 @@
import { CalendarDays, Edit2, Save, Sparkles, RefreshCw } from 'lucide-react';
export interface PlanHeaderProps {
planDate: string;
isEditing: boolean;
generating: boolean;
onEdit: () => void;
onSave: () => void;
onGenerateAI: () => void;
onReset: () => void;
}
export function PlanHeader({
planDate,
isEditing,
generating,
onEdit,
onSave,
onGenerateAI,
onReset,
}: PlanHeaderProps): JSX.Element {
return (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-500/20 text-blue-400">
<CalendarDays className="h-6 w-6" aria-hidden="true" />
</div>
<div>
<h3 className="text-lg font-semibold text-white">Daily Trading Plan</h3>
<p className="text-sm text-slate-400">{planDate}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={onGenerateAI}
disabled={generating}
className="inline-flex items-center gap-2 rounded-lg border border-purple-500/50 bg-purple-500/10 px-4 py-2 text-sm font-medium text-purple-300 hover:bg-purple-500/20 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
aria-label="Generate AI plan"
>
<Sparkles className="h-4 w-4" aria-hidden="true" />
{generating ? 'Generating...' : 'AI Plan'}
</button>
<button
onClick={onReset}
disabled={generating}
className="inline-flex items-center gap-2 rounded-lg border border-slate-600 bg-slate-800 px-4 py-2 text-sm font-medium text-slate-300 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
aria-label="Reset plan"
>
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Reset
</button>
{isEditing ? (
<button
onClick={onSave}
className="inline-flex items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 transition-colors"
aria-label="Save changes"
>
<Save className="h-4 w-4" aria-hidden="true" />
Save
</button>
) : (
<button
onClick={onEdit}
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
aria-label="Edit plan"
>
<Edit2 className="h-4 w-4" aria-hidden="true" />
Edit
</button>
)}
</div>
</div>
);
}
@@ -0,0 +1,125 @@
import { Plus, X } from 'lucide-react';
import { formatCurrency } from '@/utils/indicators';
export interface PlanKeyLevelsEditorProps {
support: number[];
resistance: number[];
isEditing: boolean;
onAddSupport: () => void;
onAddResistance: () => void;
onRemoveSupport: (index: number) => void;
onRemoveResistance: (index: number) => void;
onUpdateSupport: (index: number, value: number) => void;
onUpdateResistance: (index: number, value: number) => void;
}
export function PlanKeyLevelsEditor({
support,
resistance,
isEditing,
onAddSupport,
onAddResistance,
onRemoveSupport,
onRemoveResistance,
onUpdateSupport,
onUpdateResistance,
}: PlanKeyLevelsEditorProps): JSX.Element {
const renderLevelList = (
title: string,
levels: number[],
color: 'green' | 'red',
onAdd: () => void,
onRemove: (index: number) => void,
onUpdate: (index: number, value: number) => void
) => {
const colorClasses = {
green: {
badge: 'bg-green-500/20 text-green-300 border-green-500/30',
button: 'text-green-400 hover:text-green-300',
input: 'border-green-500/30 focus:border-green-500',
},
red: {
badge: 'bg-red-500/20 text-red-300 border-red-500/30',
button: 'text-red-400 hover:text-red-300',
input: 'border-red-500/30 focus:border-red-500',
},
};
const classes = colorClasses[color];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-slate-300">{title}</h5>
{isEditing && (
<button
onClick={onAdd}
className={`inline-flex items-center gap-1 text-sm ${classes.button}`}
aria-label={`Add ${title.toLowerCase()} level`}
>
<Plus className="h-4 w-4" aria-hidden="true" />
Add
</button>
)}
</div>
<div className="space-y-2">
{levels.length === 0 ? (
<p className="text-sm text-slate-500 italic">No levels defined</p>
) : (
levels.map((level, index) => (
<div key={`${level}-${index}`} className="flex items-center gap-2">
{isEditing ? (
<>
<input
type="number"
value={level}
onChange={(e) => onUpdate(index, parseFloat(e.target.value) || 0)}
step="0.01"
className={`flex-1 rounded-lg border bg-slate-800 px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-offset-0 ${classes.input}`}
/>
<button
onClick={() => onRemove(index)}
className="p-2 text-slate-400 hover:text-red-400 transition-colors"
aria-label={`Remove ${title.toLowerCase()} level`}
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
</>
) : (
<div className={`flex-1 rounded-lg border px-3 py-2 ${classes.badge}`}>
{formatCurrency(level)}
</div>
)}
</div>
))
)}
</div>
</div>
);
};
return (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-slate-300">Key Levels</h4>
<div className="grid gap-6 sm:grid-cols-2">
{renderLevelList(
'Support Levels',
support,
'green',
onAddSupport,
onRemoveSupport,
onUpdateSupport
)}
{renderLevelList(
'Resistance Levels',
resistance,
'red',
onAddResistance,
onRemoveResistance,
onUpdateResistance
)}
</div>
</div>
);
}
@@ -0,0 +1,127 @@
import { DollarSign, Target, AlertTriangle } from 'lucide-react';
export interface PlanRiskParametersProps {
dailyTarget: number;
maxLoss: number;
maxTrades: number;
entryZoneMin: number;
entryZoneMax: number;
targetPrice: number;
stopLoss: number;
isEditing: boolean;
onChange: (field: string, value: number) => void;
}
export function PlanRiskParameters({
dailyTarget,
maxLoss,
maxTrades,
entryZoneMin,
entryZoneMax,
targetPrice,
stopLoss,
isEditing,
onChange,
}: PlanRiskParametersProps): JSX.Element {
const renderField = (
label: string,
value: number,
field: string,
icon: JSX.Element,
prefix: string = '$',
step: number = 1
) => {
if (!isEditing) {
return (
<div className="flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-800/50 p-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-700 text-blue-400">
{icon}
</div>
<div className="flex-1">
<p className="text-xs text-slate-400">{label}</p>
<p className="text-lg font-semibold text-white">
{prefix}{value.toFixed(2)}
</p>
</div>
</div>
);
}
return (
<div className="space-y-2">
<label htmlFor={field} className="text-sm font-medium text-slate-400">
{label}
</label>
<div className="flex items-center gap-2">
<span className="text-slate-400">{prefix}</span>
<input
id={field}
type="number"
value={value}
onChange={(e) => onChange(field, parseFloat(e.target.value) || 0)}
step={step}
className="flex-1 rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
/>
</div>
</div>
);
};
return (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-slate-300">Risk Parameters</h4>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{renderField(
'Daily Target',
dailyTarget,
'dailyTarget',
<Target className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Max Loss',
maxLoss,
'maxLoss',
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Max Trades',
maxTrades,
'maxTrades',
<DollarSign className="h-5 w-5" aria-hidden="true" />,
'',
1
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{renderField(
'Entry Zone Min',
entryZoneMin,
'entryZoneMin',
<DollarSign className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Entry Zone Max',
entryZoneMax,
'entryZoneMax',
<DollarSign className="h-5 w-5" aria-hidden="true" />
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{renderField(
'Target Price',
targetPrice,
'targetPrice',
<Target className="h-5 w-5" aria-hidden="true" />
)}
{renderField(
'Stop Loss',
stopLoss,
'stopLoss',
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
)}
</div>
</div>
);
}
@@ -0,0 +1,580 @@
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useLocalStorage } from '@/hooks';
import { AlertModal, ConfirmModal } from '@/components/shared/Modal';
import { PlanHeader } from './PlanHeader';
import { PlanBiasSelector } from './PlanBiasSelector';
import { PlanRiskParameters } from './PlanRiskParameters';
import { PlanKeyLevelsEditor } from './PlanKeyLevelsEditor';
import { usePlanGeneration } from './usePlanGeneration';
import StrategyModeSelector from '@/components/StrategyModeSelector';
import TrendConfirmation from '@/components/TrendConfirmation';
import MultiDayPositionTracker from '@/components/MultiDayPositionTracker';
import NewsEventTracker from '@/components/NewsEventTracker';
import type { TradingPlan, DailyTradingPlanProps } from './types';
import type { StrategyMode } from '@/components/StrategyModeSelector';
import { STRATEGY_PRESETS } from '@/components/StrategyModeSelector';
import AdvancedMetricsDashboard, { type Trade as AdvancedMetricsTrade, type VolatilityBucket } from '@/components/AdvancedMetricsDashboard';
const createDefaultPlan = (currentPrice: number, strategyMode: StrategyMode = 'SWING'): TradingPlan => {
const preset = STRATEGY_PRESETS[strategyMode];
const riskAmount = 10000 * (preset.riskPerTrade / 100); // Assume $10k account
const stopLossDiff = currentPrice * (preset.stopLossPercent / 100);
const takeProfitDiff = currentPrice * (preset.takeProfitPercent / 100);
return {
date: new Date().toDateString(),
strategyMode,
bias: 'NEUTRAL',
dailyTarget: Math.round(riskAmount * 2), // 2x risk as daily target
maxLoss: Math.round(riskAmount),
entryZone: {
min: currentPrice - (currentPrice * (preset.stopLossPercent / 200)),
max: currentPrice + (currentPrice * (preset.stopLossPercent / 200))
},
targetPrice: currentPrice + takeProfitDiff,
stopLoss: currentPrice - stopLossDiff,
keyLevels: {
support: [
currentPrice - (currentPrice * (preset.stopLossPercent / 50)),
currentPrice - (currentPrice * (preset.stopLossPercent / 25))
],
resistance: [
currentPrice + (currentPrice * (preset.takeProfitPercent / 50)),
currentPrice + (currentPrice * (preset.takeProfitPercent / 25))
],
},
tradingNotes: '',
maxTrades: preset.maxDailyTrades,
actualTrades: 0,
actualPnL: 0,
planFollowed: true,
contextMetrics: null,
};
};
const SIGNAL_LABELS: Record<AdvancedMetricsTrade['signalType'], string> = {
RSI_CROSSOVER: 'RSI Crossover',
MA_CROSSOVER: 'MA Crossover',
BB_BREAKOUT: 'Bollinger Breakout',
MACD: 'MACD Signal',
SUPPORT_BOUNCE: 'Support Bounce',
TREND_CONFIRMATION: 'Trend Confirmation',
NEWS_TRIGGERED: 'News Triggered',
};
export default function DailyTradingPlan({
currentPrice,
onPlanUpdate,
openRouterReady,
openRouterMessage,
advancedTrades = [],
advancedTradesSource = 'sample',
}: DailyTradingPlanProps): JSX.Element {
const [isEditing, setIsEditing] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [showAdvancedMetrics, setShowAdvancedMetrics] = useState(false);
const [analyticsFocus, setAnalyticsFocus] = useState<{
timeframe: string | null;
signalType: AdvancedMetricsTrade['signalType'] | null;
volatility: string | null;
}>({ timeframe: null, signalType: null, volatility: null });
const safeAdvancedTrades = advancedTrades ?? [];
const advancedAnalyticsSummary = useMemo(() => {
if (!safeAdvancedTrades.length) return null;
const totalTrades = safeAdvancedTrades.length;
const wins = safeAdvancedTrades.filter((trade) => trade.profitable).length;
const winRate = (wins / totalTrades) * 100;
const timeframePnL = safeAdvancedTrades.reduce<Record<string, number>>((acc, trade) => {
acc[trade.timeframe] = (acc[trade.timeframe] ?? 0) + trade.pnl;
return acc;
}, {});
const bestTimeframeEntry = Object.entries(timeframePnL).sort((a, b) => b[1] - a[1])[0];
const signalPnL = safeAdvancedTrades.reduce<Record<string, number>>((acc, trade) => {
acc[trade.signalType] = (acc[trade.signalType] ?? 0) + trade.pnl;
return acc;
}, {});
const bestSignalEntry = Object.entries(signalPnL).sort((a, b) => b[1] - a[1])[0];
const slippageImpact = safeAdvancedTrades.reduce((sum, trade) => {
const gross = trade.grossPnL ?? trade.pnl;
if (!gross) return sum;
const impact = trade.slippage ? (trade.slippage / Math.abs(gross)) * 100 : 0;
return sum + impact;
}, 0) / totalTrades;
return {
totalTrades,
winRate,
bestTimeframe: bestTimeframeEntry?.[0] ?? null,
bestSignal: (bestSignalEntry?.[0] as AdvancedMetricsTrade['signalType']) ?? null,
slippageImpact,
};
}, [safeAdvancedTrades]);
const [plan, setPlan] = useLocalStorage<TradingPlan>(
'daily-trading-plan',
createDefaultPlan(currentPrice)
);
const { generating, error, generatePlan } = usePlanGeneration(openRouterReady);
// Ensure plan resets when the stored date is stale (runs after render to avoid blocking updates)
useEffect(() => {
const today = new Date().toDateString();
if (plan.date !== today) {
setPlan((prev) => {
const next = createDefaultPlan(currentPrice, prev.strategyMode);
return {
...next,
bias: prev.bias,
tradingNotes: prev.tradingNotes,
};
});
}
}, [plan.date, currentPrice, setPlan]);
const handleEdit = useCallback(() => {
setIsEditing(true);
}, []);
const handleSave = useCallback(() => {
setIsEditing(false);
if (onPlanUpdate) {
onPlanUpdate(plan);
}
}, [plan, onPlanUpdate]);
const handleConfirmReset = useCallback(() => {
setPlan(createDefaultPlan(currentPrice));
setIsEditing(false);
setShowResetConfirm(false);
}, [currentPrice, setPlan]);
const handleGenerateAI = useCallback(async () => {
const generatedPlan = await generatePlan(currentPrice, plan);
if (generatedPlan) {
setPlan(generatedPlan);
setSuccessMessage(
`AI Plan Generated!\n\nBias: ${generatedPlan.bias}\nTarget: $${generatedPlan.dailyTarget}\n\nReview and edit the plan as needed.`
);
setShowSuccessAlert(true);
if (onPlanUpdate) {
onPlanUpdate(generatedPlan);
}
}
}, [currentPrice, plan, generatePlan, setPlan, onPlanUpdate]);
const handleFieldChange = useCallback(
(field: string, value: number) => {
setPlan((prev) => {
if (field === 'entryZoneMin') {
return { ...prev, entryZone: { ...prev.entryZone, min: value } };
}
if (field === 'entryZoneMax') {
return { ...prev, entryZone: { ...prev.entryZone, max: value } };
}
return { ...prev, [field]: value };
});
},
[setPlan]
);
const handleBiasChange = useCallback(
(bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL') => {
setPlan((prev) => ({ ...prev, bias }));
},
[setPlan]
);
const handleNotesChange = useCallback(
(notes: string) => {
setPlan((prev) => ({ ...prev, tradingNotes: notes }));
},
[setPlan]
);
const handleAdvancedTimeframeSelect = useCallback((timeframe: string) => {
setAnalyticsFocus((prev) => ({
...prev,
timeframe: prev.timeframe === timeframe ? null : timeframe,
}));
}, []);
const handleAdvancedSignalSelect = useCallback((signalType: string) => {
const typedSignal = signalType as AdvancedMetricsTrade['signalType'];
setAnalyticsFocus((prev) => ({
...prev,
signalType: prev.signalType === typedSignal ? null : typedSignal,
}));
}, []);
const handleAdvancedVolatilitySelect = useCallback((bucket: VolatilityBucket) => {
setAnalyticsFocus((prev) => ({
...prev,
volatility: prev.volatility === bucket.range ? null : bucket.range,
}));
}, []);
const handleStrategyModeChange = useCallback(
(mode: StrategyMode) => {
const newPlan = createDefaultPlan(currentPrice, mode);
setPlan((prev) => ({
...newPlan,
bias: prev.bias,
tradingNotes: prev.tradingNotes,
}));
},
[currentPrice, setPlan]
);
// Key levels handlers
const handleAddSupport = useCallback(() => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: [...prev.keyLevels.support, currentPrice - 10],
},
}));
}, [currentPrice, setPlan]);
const handleAddResistance = useCallback(() => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: [...prev.keyLevels.resistance, currentPrice + 10],
},
}));
}, [currentPrice, setPlan]);
const handleRemoveSupport = useCallback(
(index: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.filter((_, i) => i !== index),
},
}));
},
[setPlan]
);
const handleRemoveResistance = useCallback(
(index: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.filter((_, i) => i !== index),
},
}));
},
[setPlan]
);
const handleUpdateSupport = useCallback(
(index: number, value: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.map((level, i) => (i === index ? value : level)),
},
}));
},
[setPlan]
);
const handleUpdateResistance = useCallback(
(index: number, value: number) => {
setPlan((prev) => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.map((level, i) => (i === index ? value : level)),
},
}));
},
[setPlan]
);
return (
<div className="space-y-6 rounded-2xl border border-slate-700 bg-slate-900 p-6">
<PlanHeader
planDate={plan.date}
isEditing={isEditing}
generating={generating}
onEdit={handleEdit}
onSave={handleSave}
onGenerateAI={handleGenerateAI}
onReset={() => setShowResetConfirm(true)}
/>
{/* Strategy Mode Info Banner */}
<div className="rounded-lg border border-blue-500/30 bg-blue-500/10 px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{STRATEGY_PRESETS[plan.strategyMode].emoji}</span>
<div>
<p className="font-semibold text-blue-300">
{plan.strategyMode} Mode Active
</p>
<p className="text-xs text-blue-200">
Max {STRATEGY_PRESETS[plan.strategyMode].maxDailyTrades} trades
R:R 1:{STRATEGY_PRESETS[plan.strategyMode].r2rRatio.toFixed(1)}
Stop: {STRATEGY_PRESETS[plan.strategyMode].stopLossPercent}%
</p>
</div>
</div>
</div>
</div>
{(error || (openRouterReady === false && openRouterMessage)) && (
<div
className={`rounded-lg border px-4 py-3 ${
openRouterReady === false
? 'border-amber-500/60 bg-amber-500/10 text-amber-100'
: 'border-red-500/60 bg-red-500/10 text-red-100'
}`}
role="alert"
>
<p className="text-sm">{error || openRouterMessage}</p>
</div>
)}
<PlanBiasSelector bias={plan.bias} isEditing={isEditing} onChange={handleBiasChange} />
<div className="hidden sm:block">
<StrategyModeSelector
defaultMode={plan.strategyMode}
onModeChange={handleStrategyModeChange}
variant="compact"
/>
</div>
<div className="sm:hidden">
<StrategyModeSelector
defaultMode={plan.strategyMode}
onModeChange={handleStrategyModeChange}
variant="full"
/>
</div>
<PlanRiskParameters
dailyTarget={plan.dailyTarget}
maxLoss={plan.maxLoss}
maxTrades={plan.maxTrades}
entryZoneMin={plan.entryZone.min}
entryZoneMax={plan.entryZone.max}
targetPrice={plan.targetPrice}
stopLoss={plan.stopLoss}
isEditing={isEditing}
onChange={handleFieldChange}
/>
<PlanKeyLevelsEditor
support={plan.keyLevels.support}
resistance={plan.keyLevels.resistance}
isEditing={isEditing}
onAddSupport={handleAddSupport}
onAddResistance={handleAddResistance}
onRemoveSupport={handleRemoveSupport}
onRemoveResistance={handleRemoveResistance}
onUpdateSupport={handleUpdateSupport}
onUpdateResistance={handleUpdateResistance}
/>
{/* Phase 3: Swing Trading Features - Show for SWING and HYBRID modes */}
{(plan.strategyMode === 'SWING' || plan.strategyMode === 'HYBRID') && (
<div className="space-y-6">
{/* Trend Confirmation for Swing Entry */}
<TrendConfirmation
ema8={2035.50}
ema21={2033.20}
ema55={2031.80}
ema200={2030.00}
macdLine={0.45}
macdSignal={0.32}
rsi={58.5}
timeframe={plan.strategyMode === 'SWING' ? '4h' : '1h'}
onTrendUpdate={(trend) => {
setPlan(prev => ({ ...prev, trendConfirmed: trend.strength !== 'WEAK' }));
}}
/>
{/* Multi-Day Position Tracker */}
{plan.swingPositions && plan.swingPositions.length > 0 && (
<MultiDayPositionTracker
positions={plan.swingPositions}
onMetricsUpdate={(metrics) => {
console.log('Position metrics updated:', metrics);
}}
/>
)}
{/* News Event Monitor */}
{plan.newsEvents && plan.newsEvents.length > 0 && (
<NewsEventTracker
events={plan.newsEvents}
onEventAlert={(event) => {
console.log('News event alert:', event.title);
}}
/>
)}
</div>
)}
{/* Phase 4 Advanced Metrics Snapshot */}
<div className="space-y-4 rounded-2xl border border-emerald-500/30 bg-emerald-500/5 p-4">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-emerald-300">Phase 4 · Execution intel</p>
<p className="text-lg font-semibold text-white">Advanced Metrics Summary</p>
<p className="text-xs text-emerald-200">
{advancedTradesSource === 'live'
? 'Powered by your closed trades.'
: 'Showing curated sample data until you close a trade.'}
</p>
</div>
<div className="text-right text-sm text-emerald-200">
<p>{advancedAnalyticsSummary ? `${advancedAnalyticsSummary.totalTrades} trades` : 'No trades yet'}</p>
<p className="font-semibold text-emerald-300">
{advancedAnalyticsSummary ? `${advancedAnalyticsSummary.winRate.toFixed(1)}% win rate` : 'Run a session to unlock insights'}
</p>
</div>
</div>
{advancedAnalyticsSummary ? (
<>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Best timeframe</p>
<p className="text-lg font-semibold text-white">
{advancedAnalyticsSummary.bestTimeframe ?? '—'}
</p>
<p className="text-xs text-emerald-200/70">Highest net P&L</p>
</div>
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Best signal</p>
<p className="text-lg font-semibold text-white">
{advancedAnalyticsSummary.bestSignal
? SIGNAL_LABELS[advancedAnalyticsSummary.bestSignal]
: '—'}
</p>
<p className="text-xs text-emerald-200/70">Highest risk / reward</p>
</div>
<div className="rounded-xl border border-emerald-400/30 bg-emerald-400/5 p-3">
<p className="text-xs text-emerald-200/80">Avg slippage impact</p>
<p className="text-lg font-semibold text-white">
{Number.isFinite(advancedAnalyticsSummary.slippageImpact)
? `${advancedAnalyticsSummary.slippageImpact.toFixed(1)}%`
: '—'}
</p>
<p className="text-xs text-emerald-200/70">Cost of fills vs. gross</p>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2 text-xs text-emerald-200">
{analyticsFocus.timeframe && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Timeframe: {analyticsFocus.timeframe}
</span>
)}
{analyticsFocus.signalType && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Signal: {SIGNAL_LABELS[analyticsFocus.signalType]}
</span>
)}
{analyticsFocus.volatility && (
<span className="rounded-full border border-emerald-400/60 bg-emerald-400/10 px-3 py-1">
Volatility: {analyticsFocus.volatility}
</span>
)}
{!analyticsFocus.timeframe && !analyticsFocus.signalType && !analyticsFocus.volatility && (
<span className="text-emerald-200/70">No filter focus selected</span>
)}
</div>
<button
type="button"
onClick={() => setShowAdvancedMetrics((prev) => !prev)}
className="inline-flex items-center gap-2 rounded-full border border-emerald-400/60 px-4 py-1 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/10"
>
{showAdvancedMetrics ? 'Hide dashboard' : 'Open dashboard'}
</button>
</div>
{showAdvancedMetrics && (
<div className="rounded-2xl border border-slate-700 bg-slate-950/40 p-2">
<AdvancedMetricsDashboard
trades={safeAdvancedTrades}
onTimeframeSelect={handleAdvancedTimeframeSelect}
onSignalTypeSelect={handleAdvancedSignalSelect}
onVolatilityRangeSelect={handleAdvancedVolatilitySelect}
/>
</div>
)}
</>
) : (
<div className="rounded-xl border border-dashed border-emerald-400/40 bg-slate-900/40 p-4 text-sm text-emerald-200">
Close at least one trade to unlock the Advanced Metrics Dashboard summary.
</div>
)}
</div>
{/* Trading Notes */}
<div className="space-y-2">
<label htmlFor="trading-notes" className="text-sm font-medium text-slate-400">
Trading Notes
</label>
{isEditing ? (
<textarea
id="trading-notes"
value={plan.tradingNotes}
onChange={(e) => handleNotesChange(e.target.value)}
rows={4}
className="w-full rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
placeholder="Enter your trading notes, observations, and strategies..."
/>
) : (
<div className="rounded-lg border border-slate-700 bg-slate-800/50 px-4 py-3 text-slate-300">
{plan.tradingNotes || <span className="italic text-slate-500">No notes</span>}
</div>
)}
</div>
{/* Modals */}
<ConfirmModal
isOpen={showResetConfirm}
onClose={() => setShowResetConfirm(false)}
onConfirm={handleConfirmReset}
title="Reset Plan"
message="Create a new plan for today? This will clear the current plan."
variant="warning"
/>
<AlertModal
isOpen={showSuccessAlert}
onClose={() => setShowSuccessAlert(false)}
title="Success"
message={successMessage}
variant="success"
/>
</div>
);
}
// Re-export types
export type { TradingPlan, DailyTradingPlanProps };
@@ -0,0 +1,41 @@
import type { PositionMetrics } from '@/services/api';
import type { StrategyMode } from '@/components/StrategyModeSelector';
import type { SwingPosition } from '@/components/MultiDayPositionTracker';
import type { NewsEvent } from '@/components/NewsEventTracker';
import type { Trade as AdvancedMetricsTrade } from '@/components/AdvancedMetricsDashboard';
import type { TradeDatasetSource } from '@/hooks/useAdvancedTradeMetrics';
export interface TradingPlan {
date: string;
bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
strategyMode: StrategyMode;
dailyTarget: number;
maxLoss: number;
entryZone: { min: number; max: number };
targetPrice: number;
stopLoss: number;
keyLevels: {
support: number[];
resistance: number[];
};
tradingNotes: string;
maxTrades: number;
actualTrades: number;
actualPnL: number;
planFollowed: boolean;
contextMetrics: PositionMetrics | null;
// Phase 3: Swing Trading Features
swingPositions?: SwingPosition[];
newsEvents?: NewsEvent[];
trendConfirmed?: boolean;
}
export interface DailyTradingPlanProps {
currentPrice: number;
onPlanUpdate?: (plan: TradingPlan) => void;
openRouterReady?: boolean;
openRouterMessage?: string;
advancedTrades?: AdvancedMetricsTrade[];
advancedTradesSource?: TradeDatasetSource;
}
@@ -0,0 +1,98 @@
import { useState, useCallback } from 'react';
import { aiApi } from '@/services/api';
import type { TradingPlan } from './types';
export interface UsePlanGenerationReturn {
generating: boolean;
error: string | null;
generatePlan: (currentPrice: number, currentPlan: TradingPlan) => Promise<TradingPlan | null>;
clearError: () => void;
}
/**
* Hook for AI-powered trading plan generation
*/
export function usePlanGeneration(
openRouterReady?: boolean
): UsePlanGenerationReturn {
const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const clearError = useCallback(() => {
setError(null);
}, []);
const generatePlan = useCallback(
async (currentPrice: number, currentPlan: TradingPlan): Promise<TradingPlan | null> => {
if (openRouterReady === false) {
setError(
'AI plan generation requires an OpenRouter API key. Please set OPENROUTER_API_KEY on the backend and restart.'
);
return null;
}
setGenerating(true);
setError(null);
try {
const aiPlanResponse = await aiApi.generateTradingPlan({
symbol: 'XAUUSD',
timeframe: '1m',
limit: 400,
current_price: currentPrice,
user_capital: 100000,
risk_tolerance: 'MODERATE',
use_indicator_preferences: true,
});
if (!aiPlanResponse) {
throw new Error('No response from AI plan generation API');
}
const generatedPlan: TradingPlan = {
...currentPlan,
date: new Date().toDateString(),
bias: aiPlanResponse.market_bias || 'NEUTRAL',
dailyTarget: aiPlanResponse.daily_target ?? currentPlan.dailyTarget,
maxLoss: aiPlanResponse.max_loss ?? currentPlan.maxLoss,
entryZone: {
min: aiPlanResponse.entry_zone_min ?? currentPrice - 10,
max: aiPlanResponse.entry_zone_max ?? currentPrice + 10,
},
targetPrice: aiPlanResponse.target_price ?? currentPrice + 20,
stopLoss: aiPlanResponse.stop_loss ?? currentPrice - 15,
keyLevels: {
support: aiPlanResponse.support_levels?.slice(0, 3) || currentPlan.keyLevels.support,
resistance:
aiPlanResponse.resistance_levels?.slice(0, 3) || currentPlan.keyLevels.resistance,
},
tradingNotes: aiPlanResponse.trading_notes || aiPlanResponse.reasoning || '',
maxTrades: aiPlanResponse.max_trades ?? currentPlan.maxTrades,
contextMetrics: aiPlanResponse.context_metrics || currentPlan.contextMetrics,
};
return generatedPlan;
} catch (err) {
if (err instanceof Error && err.message.includes('OPENROUTER_API_KEY')) {
setError(
'AI plan generation requires an OpenRouter API key. Set OPENROUTER_API_KEY on the backend and restart the server.'
);
} else {
const message = err instanceof Error ? err.message : 'Failed to generate AI plan';
setError(message);
}
return null;
} finally {
setGenerating(false);
}
},
[openRouterReady]
);
return {
generating,
error,
generatePlan,
clearError,
};
}
+244
View File
@@ -0,0 +1,244 @@
import { useEffect, useRef, ReactNode } from 'react';
import { X } from 'lucide-react';
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
showCloseButton?: boolean;
closeOnEscape?: boolean;
closeOnBackdropClick?: boolean;
}
/**
* Accessible Modal component with focus trap and keyboard handling
*
* @example
* <Modal isOpen={open} onClose={() => setOpen(false)} title="Confirm Action">
* <p>Are you sure you want to proceed?</p>
* <div className="flex gap-2 mt-4">
* <button onClick={handleConfirm}>Confirm</button>
* <button onClick={() => setOpen(false)}>Cancel</button>
* </div>
* </Modal>
*/
export function Modal({
isOpen,
onClose,
title,
children,
size = 'md',
showCloseButton = true,
closeOnEscape = true,
closeOnBackdropClick = true,
}: ModalProps): JSX.Element | null {
const modalRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
useEffect(() => {
if (!isOpen) return;
// Store the element that had focus before the modal opened
previousActiveElement.current = document.activeElement as HTMLElement;
// Focus the modal
modalRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
// Restore focus to the previously focused element
previousActiveElement.current?.focus();
};
}, [isOpen]);
useEffect(() => {
if (!isOpen || !closeOnEscape) return;
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, closeOnEscape, onClose]);
if (!isOpen) return null;
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (closeOnBackdropClick && event.target === event.currentTarget) {
onClose();
}
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
onClick={handleBackdropClick}
aria-modal="true"
role="dialog"
aria-labelledby={title ? 'modal-title' : undefined}
>
<div
ref={modalRef}
className={`relative w-full ${sizeClasses[size]} bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col`}
tabIndex={-1}
>
{/* Header */}
{(title || showCloseButton) && (
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-700">
{title && (
<h2 id="modal-title" className="text-xl font-semibold text-white">
{title}
</h2>
)}
{showCloseButton && (
<button
onClick={onClose}
className="ml-auto p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
aria-label="Close modal"
>
<X className="w-5 h-5" aria-hidden="true" />
</button>
)}
</div>
)}
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{children}
</div>
</div>
</div>
);
}
export interface ConfirmModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'danger' | 'warning' | 'info';
}
/**
* Confirmation dialog modal (replaces window.confirm)
*
* @example
* <ConfirmModal
* isOpen={showConfirm}
* onClose={() => setShowConfirm(false)}
* onConfirm={handleDelete}
* title="Delete Item"
* message="Are you sure you want to delete this item? This action cannot be undone."
* variant="danger"
* />
*/
export function ConfirmModal({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'info',
}: ConfirmModalProps): JSX.Element {
const handleConfirm = () => {
onConfirm();
onClose();
};
const variantStyles = {
danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
warning: 'bg-amber-600 hover:bg-amber-700 focus:ring-amber-500',
info: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500',
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="sm">
<p className="text-slate-300 mb-6">{message}</p>
<div className="flex gap-3 justify-end">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg bg-slate-800 text-slate-200 hover:bg-slate-700 transition-colors"
>
{cancelText}
</button>
<button
onClick={handleConfirm}
className={`px-4 py-2 rounded-lg text-white transition-colors focus:outline-none focus:ring-2 ${variantStyles[variant]}`}
>
{confirmText}
</button>
</div>
</Modal>
);
}
export interface AlertModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
message: string;
okText?: string;
variant?: 'success' | 'error' | 'info' | 'warning';
}
/**
* Alert dialog modal (replaces window.alert)
*
* @example
* <AlertModal
* isOpen={showAlert}
* onClose={() => setShowAlert(false)}
* title="Success"
* message="Your changes have been saved successfully."
* variant="success"
* />
*/
export function AlertModal({
isOpen,
onClose,
title,
message,
okText = 'OK',
variant = 'info',
}: AlertModalProps): JSX.Element {
const variantStyles = {
success: 'bg-green-600 hover:bg-green-700',
error: 'bg-red-600 hover:bg-red-700',
warning: 'bg-amber-600 hover:bg-amber-700',
info: 'bg-blue-600 hover:bg-blue-700',
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="sm">
<p className="text-slate-300 mb-6 whitespace-pre-wrap">{message}</p>
<div className="flex justify-end">
<button
onClick={onClose}
className={`px-4 py-2 rounded-lg text-white transition-colors ${variantStyles[variant]}`}
>
{okText}
</button>
</div>
</Modal>
);
}
+9
View File
@@ -0,0 +1,9 @@
/**
* Custom React Hooks
*/
export { useLocalStorage } from './useLocalStorage';
export { useApi } from './useApi';
export type { UseApiState, UseApiReturn } from './useApi';
export { useAdvancedTradeMetrics } from './useAdvancedTradeMetrics';
export type { TradeDatasetSource } from './useAdvancedTradeMetrics';
@@ -0,0 +1,201 @@
import { useMemo } from 'react';
import type { Trade as PortfolioTrade } from '@/types';
import type { Trade as AdvancedMetricsTrade } from '@/components/AdvancedMetricsDashboard';
export type TradeDatasetSource = 'live' | 'sample';
const TIMEFRAMES = ['1m', '3m', '5m', '15m', '1h', '4h', 'Daily'];
const SIGNAL_TYPES: AdvancedMetricsTrade['signalType'][] = [
'RSI_CROSSOVER',
'MA_CROSSOVER',
'BB_BREAKOUT',
'MACD',
'SUPPORT_BOUNCE',
'TREND_CONFIRMATION',
'NEWS_TRIGGERED',
];
const now = Date.now();
const sampleTimestamp = (minutesAgo: number) => new Date(now - minutesAgo * 60 * 1000).toISOString();
const SAMPLE_TRADES: AdvancedMetricsTrade[] = [
{
id: 'sample-1',
timeframe: '1m',
signalType: 'RSI_CROSSOVER',
entry: 2031.5,
exit: 2033.6,
quantity: 3,
profitable: true,
pnl: 160.5,
grossPnL: 162.9,
slippage: 2.4,
volatility: 0.8,
volume: 6100.8,
confidence: 62,
timestamp: sampleTimestamp(45),
},
{
id: 'sample-2',
timeframe: '5m',
signalType: 'MA_CROSSOVER',
entry: 2030.2,
exit: 2034.1,
quantity: 2,
profitable: true,
pnl: 285.8,
grossPnL: 287.6,
slippage: 1.8,
volatility: 1.05,
volume: 4068.2,
confidence: 68,
timestamp: sampleTimestamp(90),
},
{
id: 'sample-3',
timeframe: '15m',
signalType: 'TREND_CONFIRMATION',
entry: 2028.4,
exit: 2035.0,
quantity: 1.8,
profitable: true,
pnl: 420.2,
grossPnL: 421.5,
slippage: 1.3,
volatility: 1.4,
volume: 3663,
confidence: 78,
timestamp: sampleTimestamp(150),
},
{
id: 'sample-4',
timeframe: '1h',
signalType: 'MACD',
entry: 2036.1,
exit: 2033.0,
quantity: 1.5,
profitable: false,
pnl: -95.3,
grossPnL: -93.1,
slippage: 2.2,
volatility: 1.9,
volume: 3049.5,
confidence: 54,
timestamp: sampleTimestamp(210),
},
{
id: 'sample-5',
timeframe: '4h',
signalType: 'SUPPORT_BOUNCE',
entry: 2026.0,
exit: 2033.8,
quantity: 2.1,
profitable: true,
pnl: 310.6,
grossPnL: 312.2,
slippage: 1.6,
volatility: 2.2,
volume: 4270.98,
confidence: 73,
timestamp: sampleTimestamp(300),
},
{
id: 'sample-6',
timeframe: 'Daily',
signalType: 'NEWS_TRIGGERED',
entry: 2038.4,
exit: 2034.5,
quantity: 1.2,
profitable: false,
pnl: -140.6,
grossPnL: -137.5,
slippage: 3.1,
volatility: 2.8,
volume: 2441.4,
confidence: 48,
timestamp: sampleTimestamp(420),
},
{
id: 'sample-7',
timeframe: '15m',
signalType: 'BB_BREAKOUT',
entry: 2031.8,
exit: 2034.0,
quantity: 2.6,
profitable: true,
pnl: 85.3,
grossPnL: 86.1,
slippage: 0.8,
volatility: 0.95,
volume: 5288.4,
confidence: 57,
timestamp: sampleTimestamp(510),
},
{
id: 'sample-8',
timeframe: '5m',
signalType: 'TREND_CONFIRMATION',
entry: 2029.7,
exit: 2034.8,
quantity: 2.4,
profitable: true,
pnl: 210.5,
grossPnL: 211.7,
slippage: 1.2,
volatility: 1.3,
volume: 4883.52,
confidence: 81,
timestamp: sampleTimestamp(600),
},
];
const toAdvancedTrade = (trade: PortfolioTrade, index: number): AdvancedMetricsTrade => {
const seed = trade.timestamp ?? index;
const timeframe = TIMEFRAMES[Math.abs(seed + index) % TIMEFRAMES.length];
const signalType = SIGNAL_TYPES[Math.abs(seed * 3 + index) % SIGNAL_TYPES.length];
const quantity = Math.max(trade.quantity, 0.0001);
const pnl = trade.pnl ?? 0;
const exit = trade.price;
const entry = exit - pnl / quantity;
const volatility = Number((0.45 + ((seed % 12) * 0.2)).toFixed(2));
const slippage = Number((Math.abs(exit) * 0.0001 * (1 + (seed % 5) * 0.15) + 0.08).toFixed(2));
const grossPnL = Number((pnl + slippage).toFixed(2));
const confidenceBase = pnl >= 0 ? 65 : 55;
const confidence = Math.max(45, Math.min(95, confidenceBase + ((seed % 7) - 3) * 4));
const volume = Number((quantity * exit).toFixed(2));
return {
id: trade.id,
timeframe,
signalType,
entry: Number(entry.toFixed(2)) || Number(exit.toFixed(2)),
exit: Number(exit.toFixed(2)),
quantity: Number(quantity.toFixed(4)),
profitable: pnl >= 0,
pnl: Number(pnl.toFixed(2)),
grossPnL,
slippage,
volatility,
volume,
confidence,
timestamp: new Date(trade.timestamp ?? Date.now()).toISOString(),
};
};
export const useAdvancedTradeMetrics = (trades?: PortfolioTrade[] | null) => {
return useMemo<{ trades: AdvancedMetricsTrade[]; source: TradeDatasetSource }>(() => {
if (!trades || trades.length === 0) {
return { trades: SAMPLE_TRADES, source: 'sample' };
}
const executed = trades.filter((trade) => trade.action === 'SELL' && typeof trade.pnl === 'number');
if (executed.length === 0) {
return { trades: SAMPLE_TRADES, source: 'sample' };
}
return {
trades: executed.map(toAdvancedTrade),
source: 'live',
};
}, [trades]);
};
+118
View File
@@ -0,0 +1,118 @@
import { useState, useCallback, useRef, useEffect } from 'react';
export interface UseApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export interface UseApiReturn<T, Args extends any[]> extends UseApiState<T> {
execute: (...args: Args) => Promise<T | null>;
reset: () => void;
}
/**
* Custom hook for managing async API calls with loading and error states
*
* @param apiCall - The async function to execute
* @param options - Configuration options
* @returns Object containing data, loading, error, execute function, and reset function
*
* @example
* const { data, loading, error, execute } = useApi(
* (id: number) => api.getUser(id),
* { executeOnMount: false }
* );
*
* // Later...
* await execute(123);
*/
export function useApi<T, Args extends any[] = []>(
apiCall: (...args: Args) => Promise<T>,
options: {
executeOnMount?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: string) => void;
} = {}
): UseApiReturn<T, Args> {
const { executeOnMount = false, onSuccess, onError } = options;
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: false,
error: null,
});
const abortControllerRef = useRef<AbortController | null>(null);
const mountedRef = useRef(true);
const execute = useCallback(
async (...args: Args): Promise<T | null> => {
// Cancel any pending request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const result = await apiCall(...args);
if (!mountedRef.current) return null;
setState({ data: result, loading: false, error: null });
if (onSuccess) {
onSuccess(result);
}
return result;
} catch (err) {
if (!mountedRef.current) return null;
// Don't treat abort as an error
if (err instanceof Error && err.name === 'AbortError') {
return null;
}
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
setState({ data: null, loading: false, error: errorMessage });
if (onError) {
onError(errorMessage);
}
return null;
}
},
[apiCall, onSuccess, onError]
);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
useEffect(() => {
mountedRef.current = true;
if (executeOnMount) {
execute(...([] as unknown as Args));
}
return () => {
mountedRef.current = false;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [execute, executeOnMount]);
return {
...state,
execute,
reset,
};
}
+48
View File
@@ -0,0 +1,48 @@
import { useState, useEffect, useCallback } from 'react';
/**
* Custom hook for syncing state with localStorage
* Handles JSON serialization/deserialization and error cases
*
* @param key - localStorage key
* @param defaultValue - default value if key doesn't exist or parsing fails
* @returns [value, setValue, removeValue]
*/
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return defaultValue;
try {
const stored = localStorage.getItem(key);
if (stored === null) return defaultValue;
return JSON.parse(stored) as T;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
useEffect(() => {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing localStorage key "${key}":`, error);
}
}, [key, value]);
const removeValue = useCallback(() => {
try {
localStorage.removeItem(key);
setValue(defaultValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, defaultValue]);
return [value, setValue, removeValue];
}
+113
View File
@@ -0,0 +1,113 @@
import api from './api';
export type BrokerAction = 'BUY' | 'SELL';
export interface BrokerProvider {
id: string;
name: string;
description: string;
docsUrl: string;
latencyMs: number;
features: Record<string, boolean>;
supportsDemo: boolean;
}
export interface BrokerCredentials {
apiKey: string;
accountId: string;
demo?: boolean;
}
export interface BrokerOrder {
action: BrokerAction;
symbol: string;
quantity: number;
price: number;
type?: 'MARKET' | 'LIMIT' | 'STOP' | string;
stopLoss?: number | null;
takeProfit?: number | null;
}
export interface BrokerPosition {
symbol: string;
quantity: number;
avgPrice: number;
lastPrice?: number | null;
pnl?: number | null;
ticket?: number;
}
export interface BrokerSession {
provider: BrokerProvider | null;
account_id?: string;
balance?: number | null;
positions?: BrokerPosition[];
last_heartbeat?: string;
demo?: boolean;
}
const mapProvider = (provider: any): BrokerProvider => ({
id: provider.id,
name: provider.name,
description: provider.description,
docsUrl: provider.docs_url,
latencyMs: provider.latency_ms,
features: provider.features || {},
supportsDemo: provider.supports_demo ?? true,
});
const mapSession = (session: any | null): BrokerSession | null => {
if (!session) return null;
return {
provider: session.provider ? mapProvider(session.provider) : null,
account_id: session.account_id,
balance: session.balance,
positions: session.positions || [],
last_heartbeat: session.last_heartbeat,
demo: session.demo,
};
};
export const brokerService = {
async listProviders(): Promise<BrokerProvider[]> {
const response = await api.get('/brokers/providers');
return (response.data || []).map(mapProvider);
},
async getSession(): Promise<BrokerSession | null> {
const response = await api.get('/brokers/session');
return mapSession(response.data);
},
async connect(providerId: string, credentials: BrokerCredentials): Promise<BrokerSession> {
const response = await api.post('/brokers/connect', {
provider_id: providerId,
api_key: credentials.apiKey,
account_id: credentials.accountId,
demo: credentials.demo ?? true,
});
const session = mapSession(response.data);
if (!session) {
throw new Error('Unable to create broker session');
}
return session;
},
async disconnect(): Promise<void> {
await api.post('/brokers/disconnect');
},
async placeOrder(order: BrokerOrder): Promise<{ remote_id: string; filled: boolean }> {
const response = await api.post('/brokers/orders', order);
return response.data;
},
async syncPositions(): Promise<{ positions: BrokerPosition[]; balance?: number; lastHeartbeat?: string }> {
const response = await api.post('/brokers/sync');
return {
positions: response.data?.positions || [],
balance: response.data?.balance,
lastHeartbeat: response.data?.lastHeartbeat,
};
},
};
+154
View File
@@ -0,0 +1,154 @@
// Portfolio Persistence API Service
// Add this to your App.tsx or create a separate service file
import axios from 'axios'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001'
export interface TradeRequest {
action: 'BUY' | 'SELL'
quantity: number
price: number
symbol?: string
notes?: string
stop_loss?: number
take_profit?: number
}
export interface TradeResponse {
trade: {
id: number
action: string
quantity: number
price: number
total: number
pnl: number | null
timestamp: number
}
portfolio: {
cash: number
initial_capital: number
position: {
symbol: string
quantity: number
avg_price: number
current_price: number
unrealized_pnl: number
unrealized_pnl_percent: number
} | null
trades: Array<{
id: number
action: string
quantity: number
price: number
total: number
pnl: number | null
timestamp: number
}>
equity_history: Array<{
time: number
equity: number
}>
total_pnl: number
total_pnl_percent: number
}
}
export interface PortfolioState {
cash: number
initial_capital: number
position: {
symbol: string
quantity: number
avg_price: number
current_price: number
unrealized_pnl: number
unrealized_pnl_percent: number
} | null
trades: Array<{
id: number
action: string
quantity: number
price: number
total: number
pnl: number | null
timestamp: number
}>
equity_history: Array<{
time: number
equity: number
}>
total_pnl: number
total_pnl_percent: number
}
export interface TradingStats {
total_trades: number
winning_trades: number
losing_trades: number
win_rate: number
total_pnl: number
total_pnl_percent: number
total_profit: number
total_loss: number
profit_factor: number
current_capital: number
initial_capital: number
}
// API functions
export async function executeTradeAPI(trade: TradeRequest): Promise<TradeResponse> {
const response = await axios.post(`${API_URL}/api/trading/execute`, trade)
return response.data
}
export async function getPortfolioAPI(): Promise<PortfolioState> {
const response = await axios.get(`${API_URL}/api/trading/portfolio`)
return response.data
}
export async function resetSimulationAPI(): Promise<{ message: string; portfolio: PortfolioState }> {
const response = await axios.post(`${API_URL}/api/trading/reset`)
return response.data
}
export async function getTradeHistoryAPI(limit = 100): Promise<Array<any>> {
const response = await axios.get(`${API_URL}/api/trading/history`, {
params: { limit }
})
return response.data
}
export async function getTradingStatsAPI(): Promise<TradingStats> {
const response = await axios.get(`${API_URL}/api/trading/stats`)
return response.data
}
// Utility to convert backend portfolio to frontend Portfolio type
export function convertBackendPortfolio(backendPortfolio: PortfolioState, currentPrice: number) {
return {
cash: backendPortfolio.cash,
initialCapital: backendPortfolio.initial_capital,
totalValue: backendPortfolio.cash + (backendPortfolio.position?.quantity || 0) * currentPrice,
totalPnl: backendPortfolio.total_pnl,
totalPnlPercent: backendPortfolio.total_pnl_percent,
position: backendPortfolio.position ? {
symbol: backendPortfolio.position.symbol,
quantity: backendPortfolio.position.quantity,
avgPrice: backendPortfolio.position.avg_price,
currentPrice: backendPortfolio.position.current_price,
unrealizedPnl: backendPortfolio.position.unrealized_pnl,
unrealizedPnlPercent: backendPortfolio.position.unrealized_pnl_percent
} : null,
trades: backendPortfolio.trades.map(t => ({
id: t.id.toString(),
timestamp: t.timestamp * 1000, // Convert to milliseconds
action: t.action as 'BUY' | 'SELL',
quantity: t.quantity,
price: t.price,
total: t.total,
pnl: t.pnl || undefined
}))
}
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.ts'
}
})