Initial commit: Gold Trading Simulator with AI-powered analysis

This commit is contained in:
Krikorios
2025-11-16 00:50:04 +02:00
commit 72c1d3adb7
128 changed files with 16232 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:8000/api
VITE_ALPHA_VANTAGE_API_KEY=your_alpha_vantage_key_here
+17
View File
@@ -0,0 +1,17 @@
{
"env": { "browser": true, "es2020": true },
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"ignorePatterns": ["dist", ".eslintrc.cjs"],
"parser": "@typescript-eslint/parser",
"plugins": ["react-refresh"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
]
}
}
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gold Trading Simulator - XAU/USD</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
{
"name": "gold-trading-simulator-frontend",
"version": "1.0.0",
"type": "module",
"description": "AI-Powered Gold Trading Scenario Simulator",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"lightweight-charts": "^4.1.0",
"axios": "^1.6.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"@tanstack/react-query": "^5.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.3",
"vite": "^5.0.8"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+78
View File
@@ -0,0 +1,78 @@
import { useEffect, useState } from 'react'
import LiveMarketPanel from './components/LiveMarketPanel'
import MultiChartSSEPanel from './components/MultiChartSSEPanel'
import AccountPositionsPanel from './components/AccountPositionsPanel'
import EquityPerformancePanel from './components/EquityPerformancePanel'
import DecisionLogPanel from './components/DecisionLogPanel'
import SettingsPanel from './components/SettingsPanel'
import PromptTemplatesPanel from './components/PromptTemplatesPanel'
import { statusApi } from './services/api'
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<'Live' | 'Account' | 'Equity' | 'Decisions' | 'Settings' | 'Prompts'>('Live')
const [backendStatus, setBackendStatus] = useState<any>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
const s = await statusApi.getStatus()
if (mounted) setBackendStatus(s)
} catch (e) {
// ignore
}
})()
return () => { mounted = false }
}, [])
const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Settings', 'Prompts']
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">Minimal UI wired to new backend endpoints</p>
</div>
<div className="text-sm text-gray-400">
{backendStatus ? (
<span>API: {backendStatus.app?.name} v{backendStatus.app?.version}</span>
) : (
<span>Checking API</span>
)}
</div>
</div>
</header>
<Tabs tabs={tabs} active={activeTab} onChange={(t) => setActiveTab(t as any)} />
{activeTab === 'Live' && (
<div style={{ display: 'grid', gap: 16 }}>
<LiveMarketPanel />
<MultiChartSSEPanel />
</div>
)}
{activeTab === 'Account' && <AccountPositionsPanel />}
{activeTab === 'Equity' && <EquityPerformancePanel />}
{activeTab === 'Decisions' && <DecisionLogPanel />}
{activeTab === 'Settings' && <SettingsPanel />}
{activeTab === 'Prompts' && <PromptTemplatesPanel />}
</div>
</div>
)
}
+160
View File
@@ -0,0 +1,160 @@
import { Brain, TrendingUp, TrendingDown, Minus, AlertTriangle } from 'lucide-react';
import type { AIAnalysis } from '@/types';
interface AIAnalysisPanelProps {
analysis: AIAnalysis | null;
isLoading: boolean;
}
export default function AIAnalysisPanel({ analysis, isLoading }: AIAnalysisPanelProps) {
if (isLoading) {
return (
<div className="card">
<div className="flex items-center gap-3 mb-4">
<Brain className="w-6 h-6 text-blue-500" />
<h3 className="text-lg font-semibold">AI Analysis</h3>
</div>
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span className="ml-3 text-gray-400">Analyzing market conditions...</span>
</div>
</div>
);
}
if (!analysis) {
return (
<div className="card">
<div className="flex items-center gap-3 mb-4">
<Brain className="w-6 h-6 text-blue-500" />
<h3 className="text-lg font-semibold">AI Analysis</h3>
</div>
<p className="text-gray-400 text-center py-8">
Click "AI Analysis" to get intelligent market insights
</p>
</div>
);
}
const getRecommendationIcon = () => {
switch (analysis.recommendation) {
case 'BUY':
return <TrendingUp className="w-6 h-6 text-green-500" />;
case 'SELL':
return <TrendingDown className="w-6 h-6 text-red-500" />;
case 'HOLD':
return <Minus className="w-6 h-6 text-yellow-500" />;
}
};
const getRecommendationColor = () => {
switch (analysis.recommendation) {
case 'BUY':
return 'text-green-500';
case 'SELL':
return 'text-red-500';
case 'HOLD':
return 'text-yellow-500';
}
};
const getRiskColor = () => {
switch (analysis.riskLevel) {
case 'LOW':
return 'bg-green-500/20 text-green-500';
case 'MEDIUM':
return 'bg-yellow-500/20 text-yellow-500';
case 'HIGH':
return 'bg-red-500/20 text-red-500';
}
};
return (
<div className="card">
<div className="flex items-center gap-3 mb-4">
<Brain className="w-6 h-6 text-blue-500" />
<h3 className="text-lg font-semibold">AI Analysis</h3>
</div>
<div className="space-y-4">
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
{getRecommendationIcon()}
<div>
<p className="text-sm text-gray-400">Recommendation</p>
<p className={`text-xl font-bold ${getRecommendationColor()}`}>
{analysis.recommendation}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-gray-400">Confidence</p>
<p className="text-xl font-bold">{analysis.confidence}%</p>
</div>
</div>
<div className="w-full bg-dark-surface rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${analysis.confidence}%` }}
/>
</div>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-gray-400" />
<p className="text-sm font-semibold">Risk Level</p>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getRiskColor()}`}>
{analysis.riskLevel}
</span>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="font-semibold mb-2">Reasoning</h4>
<p className="text-sm text-gray-300 leading-relaxed">{analysis.reasoning}</p>
</div>
{(analysis.supportResistance.support.length > 0 ||
analysis.supportResistance.resistance.length > 0) && (
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="font-semibold mb-3">Key Levels</h4>
<div className="space-y-3">
{analysis.supportResistance.resistance.length > 0 && (
<div>
<p className="text-xs text-gray-400 mb-1">Resistance</p>
<div className="flex flex-wrap gap-2">
{analysis.supportResistance.resistance.map((level, idx) => (
<span
key={idx}
className="px-2 py-1 bg-red-500/20 text-red-500 rounded text-sm"
>
${level.toFixed(2)}
</span>
))}
</div>
</div>
)}
{analysis.supportResistance.support.length > 0 && (
<div>
<p className="text-xs text-gray-400 mb-1">Support</p>
<div className="flex flex-wrap gap-2">
{analysis.supportResistance.support.map((level, idx) => (
<span
key={idx}
className="px-2 py-1 bg-green-500/20 text-green-500 rounded text-sm"
>
${level.toFixed(2)}
</span>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import { accountApi } from '@/services/api'
export default function AccountPositionsPanel() {
const [account, setAccount] = useState<any | null>(null)
const [positions, setPositions] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const [a, p] = await Promise.all([accountApi.getAccount(), accountApi.getPositions()])
if (mounted) { setAccount(a); setPositions(p) }
} catch (e: any) {
setError(e?.message || 'Failed to load account')
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
return (
<div className="card">
<div className="title">Account & Positions</div>
{loading && <div className="text-gray-400">Loading</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && account && (
<div style={{ display: 'grid', gap: 12 }}>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Metric label="Cash" value={fmt(account.cash)} />
<Metric label="Equity" value={fmt(account.equity)} />
<Metric label="Initial" value={fmt(account.initial_capital)} />
</div>
<div>
<div className="text-gray-300 font-semibold mb-2">Positions</div>
{positions.length === 0 ? (
<div className="text-gray-400">No open positions</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-400">
<th>Symbol</th>
<th>Qty</th>
<th>Avg Price</th>
<th>Last Price</th>
<th>Market Value</th>
</tr>
</thead>
<tbody>
{positions.map((p, i) => (
<tr key={i} className="border-t border-dark-border">
<td>{p.symbol}</td>
<td>{p.quantity}</td>
<td>{fmt(p.avg_price)}</td>
<td>{p.last_price ? fmt(p.last_price) : '-'}</td>
<td>{fmt(p.market_value)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)}
</div>
)
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="card" style={{ padding: 12 }}>
<div className="text-gray-400 text-xs">{label}</div>
<div className="text-gray-100 text-lg font-semibold">{value}</div>
</div>
)
}
function fmt(n: number) {
return n.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
@@ -0,0 +1,272 @@
import { useMemo } from 'react';
import {
TrendingUp,
TrendingDown,
Target,
Shield,
BarChart3,
AlertCircle,
} from 'lucide-react';
import type { Trade, Portfolio } from '@/types';
import {
calculateWinRate,
calculateSharpeRatio,
calculateMaxDrawdown,
formatPrice,
formatPercent,
} from '@/utils/indicators';
interface AdvancedAnalyticsProps {
portfolio: Portfolio;
trades: Trade[];
}
export default function AdvancedAnalytics({ portfolio, trades }: AdvancedAnalyticsProps) {
const analytics = useMemo(() => {
if (trades.length === 0) {
return {
winRate: 0,
totalTrades: 0,
winningTrades: 0,
losingTrades: 0,
avgWin: 0,
avgLoss: 0,
largestWin: 0,
largestLoss: 0,
profitFactor: 0,
sharpeRatio: 0,
maxDrawdown: 0,
avgHoldTime: 0,
riskRewardRatio: 0,
};
}
const completedTrades = trades.filter((t) => t.pnl !== undefined);
const winningTrades = completedTrades.filter((t) => t.pnl! > 0);
const losingTrades = completedTrades.filter((t) => t.pnl! <= 0);
const totalWins = winningTrades.reduce((sum, t) => sum + t.pnl!, 0);
const totalLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl!, 0));
const avgWin = winningTrades.length > 0 ? totalWins / winningTrades.length : 0;
const avgLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0;
const largestWin = winningTrades.length > 0 ? Math.max(...winningTrades.map((t) => t.pnl!)) : 0;
const largestLoss = losingTrades.length > 0 ? Math.min(...losingTrades.map((t) => t.pnl!)) : 0;
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
// Calculate returns for Sharpe ratio
const returns = completedTrades.map((t) => (t.pnl! / (t.quantity * t.price)) * 100);
const sharpeRatio = calculateSharpeRatio(returns);
// Calculate equity curve for max drawdown
const equity: number[] = [portfolio.initialCapital];
let currentEquity = portfolio.initialCapital;
for (const trade of completedTrades) {
currentEquity += trade.pnl!;
equity.push(currentEquity);
}
const maxDrawdown = calculateMaxDrawdown(equity);
const riskRewardRatio = avgLoss > 0 ? avgWin / avgLoss : 0;
return {
winRate: calculateWinRate(completedTrades),
totalTrades: completedTrades.length,
winningTrades: winningTrades.length,
losingTrades: losingTrades.length,
avgWin,
avgLoss,
largestWin,
largestLoss,
profitFactor,
sharpeRatio,
maxDrawdown,
avgHoldTime: 0, // Would need timestamp tracking
riskRewardRatio,
};
}, [trades, portfolio]);
const getQualityRating = (value: number, type: string): { color: string; text: string } => {
switch (type) {
case 'winRate':
if (value >= 60) return { color: 'text-green-500', text: 'Excellent' };
if (value >= 50) return { color: 'text-blue-500', text: 'Good' };
if (value >= 40) return { color: 'text-yellow-500', text: 'Average' };
return { color: 'text-red-500', text: 'Poor' };
case 'sharpe':
if (value >= 2) return { color: 'text-green-500', text: 'Excellent' };
if (value >= 1) return { color: 'text-blue-500', text: 'Good' };
if (value >= 0.5) return { color: 'text-yellow-500', text: 'Average' };
return { color: 'text-red-500', text: 'Poor' };
case 'profitFactor':
if (value >= 2) return { color: 'text-green-500', text: 'Excellent' };
if (value >= 1.5) return { color: 'text-blue-500', text: 'Good' };
if (value >= 1) return { color: 'text-yellow-500', text: 'Average' };
return { color: 'text-red-500', text: 'Poor' };
case 'maxDrawdown':
if (value <= 10) return { color: 'text-green-500', text: 'Excellent' };
if (value <= 20) return { color: 'text-blue-500', text: 'Good' };
if (value <= 30) return { color: 'text-yellow-500', text: 'Average' };
return { color: 'text-red-500', text: 'High Risk' };
default:
return { color: 'text-gray-500', text: 'N/A' };
}
};
if (trades.length === 0) {
return (
<div className="card">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
Advanced Analytics
</h3>
<p className="text-center text-gray-400 py-8">
No trades yet. Execute some trades to see detailed analytics.
</p>
</div>
);
}
const winRateQuality = getQualityRating(analytics.winRate, 'winRate');
const sharpeQuality = getQualityRating(analytics.sharpeRatio, 'sharpe');
const profitFactorQuality = getQualityRating(analytics.profitFactor, 'profitFactor');
const drawdownQuality = getQualityRating(analytics.maxDrawdown, 'maxDrawdown');
return (
<div className="card">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-blue-500" />
Advanced Analytics
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Win Rate */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Win Rate</span>
</div>
<p className="text-2xl font-bold">{analytics.winRate}%</p>
<p className={`text-xs ${winRateQuality.color}`}>{winRateQuality.text}</p>
<p className="text-xs text-gray-500 mt-1">
{analytics.winningTrades}W / {analytics.losingTrades}L
</p>
</div>
{/* Profit Factor */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Profit Factor</span>
</div>
<p className="text-2xl font-bold">
{analytics.profitFactor === Infinity
? '∞'
: analytics.profitFactor.toFixed(2)}
</p>
<p className={`text-xs ${profitFactorQuality.color}`}>{profitFactorQuality.text}</p>
<p className="text-xs text-gray-500 mt-1">Wins / Losses ratio</p>
</div>
{/* Sharpe Ratio */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<Shield className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Sharpe Ratio</span>
</div>
<p className="text-2xl font-bold">{analytics.sharpeRatio.toFixed(2)}</p>
<p className={`text-xs ${sharpeQuality.color}`}>{sharpeQuality.text}</p>
<p className="text-xs text-gray-500 mt-1">Risk-adjusted returns</p>
</div>
{/* Max Drawdown */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingDown className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Max Drawdown</span>
</div>
<p className="text-2xl font-bold text-red-500">-{analytics.maxDrawdown}%</p>
<p className={`text-xs ${drawdownQuality.color}`}>{drawdownQuality.text}</p>
<p className="text-xs text-gray-500 mt-1">Largest peak-to-trough</p>
</div>
{/* Average Win */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-green-500" />
<span className="text-sm text-gray-400">Avg Win</span>
</div>
<p className="text-xl font-semibold text-green-500">
{formatPrice(analytics.avgWin)}
</p>
<p className="text-xs text-gray-500 mt-1">
Largest: {formatPrice(analytics.largestWin)}
</p>
</div>
{/* Average Loss */}
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<TrendingDown className="w-4 h-4 text-red-500" />
<span className="text-sm text-gray-400">Avg Loss</span>
</div>
<p className="text-xl font-semibold text-red-500">
{formatPrice(analytics.avgLoss)}
</p>
<p className="text-xs text-gray-500 mt-1">
Largest: {formatPrice(Math.abs(analytics.largestLoss))}
</p>
</div>
{/* Risk/Reward Ratio */}
<div className="bg-dark-bg rounded-lg p-4 col-span-2">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Risk/Reward Ratio</span>
</div>
<p className="text-2xl font-bold">
1:{analytics.riskRewardRatio.toFixed(2)}
</p>
<p className="text-xs text-gray-500 mt-1">
Average win vs average loss per trade
</p>
</div>
</div>
{/* Performance Summary */}
<div className="mt-4 p-4 bg-dark-bg rounded-lg border border-dark-border">
<h4 className="font-semibold mb-2">Performance Summary</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Total Trades:</span>
<span className="font-medium">{analytics.totalTrades}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Net P&L:</span>
<span
className={`font-semibold ${
portfolio.totalPnl >= 0 ? 'text-green-500' : 'text-red-500'
}`}
>
{formatPrice(portfolio.totalPnl)} ({formatPercent(portfolio.totalPnlPercent)})
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Capital Efficiency:</span>
<span className="font-medium">
{((portfolio.totalValue / portfolio.initialCapital - 1) * 100).toFixed(2)}%
</span>
</div>
</div>
</div>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import { useEffect, useState } from 'react';
import {
Bell,
AlertTriangle,
TrendingUp,
TrendingDown,
Zap,
Shield,
Calendar,
X,
} from 'lucide-react';
import type { AlertsData, Alert, AlertType, AlertSeverity } from '@/types';
import { newsApi } from '@/services/api';
export default function AlertsPanel() {
const [alertsData, setAlertsData] = useState<AlertsData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState<'ALL' | AlertSeverity>('ALL');
const loadAlerts = async () => {
try {
setIsLoading(true);
const data = await newsApi.getAlerts(50);
setAlertsData(data);
} catch (error) {
console.error('Error loading alerts:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadAlerts();
// Auto-refresh every minute
const interval = setInterval(loadAlerts, 60000);
return () => clearInterval(interval);
}, []);
const filteredAlerts = alertsData?.alerts.filter(
(alert) => filter === 'ALL' || alert.severity === filter
) || [];
return (
<div className="card">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="relative">
<Bell className="w-6 h-6 text-yellow-500" />
{alertsData && alertsData.critical_count > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
{alertsData.critical_count}
</span>
)}
</div>
<div>
<h3 className="text-lg font-semibold">Alerts</h3>
{alertsData && (
<p className="text-xs text-gray-400">
{alertsData.alerts.length} alerts {alertsData.critical_count} critical
</p>
)}
</div>
</div>
</div>
<div className="mb-4">
<div className="grid grid-cols-5 gap-2 text-sm">
<button
onClick={() => setFilter('ALL')}
className={`px-2 py-1 rounded-md transition-colors ${
filter === 'ALL' ? 'bg-blue-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
All
</button>
<button
onClick={() => setFilter('CRITICAL')}
className={`px-2 py-1 rounded-md transition-colors ${
filter === 'CRITICAL' ? 'bg-red-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Critical
</button>
<button
onClick={() => setFilter('HIGH')}
className={`px-2 py-1 rounded-md transition-colors ${
filter === 'HIGH' ? 'bg-orange-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
High
</button>
<button
onClick={() => setFilter('MEDIUM')}
className={`px-2 py-1 rounded-md transition-colors ${
filter === 'MEDIUM' ? 'bg-yellow-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Medium
</button>
<button
onClick={() => setFilter('LOW')}
className={`px-2 py-1 rounded-md transition-colors ${
filter === 'LOW' ? 'bg-gray-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Low
</button>
</div>
</div>
<div className="space-y-2 max-h-80 overflow-y-auto">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-500"></div>
</div>
) : filteredAlerts.length > 0 ? (
filteredAlerts.map((alert) => (
<AlertCard key={alert.id} alert={alert} />
))
) : (
<p className="text-center text-gray-400 py-8">No alerts to display</p>
)}
</div>
</div>
);
}
function AlertCard({ alert }: { alert: Alert }) {
const getAlertIcon = (type: AlertType) => {
switch (type) {
case 'PRICE_SPIKE':
return <TrendingUp className="w-5 h-5 text-green-500" />;
case 'PRICE_DROP':
return <TrendingDown className="w-5 h-5 text-red-500" />;
case 'NEWS_BREAKING':
return <Zap className="w-5 h-5 text-blue-500" />;
case 'SUPPORT_BREACH':
case 'RESISTANCE_BREACH':
return <Shield className="w-5 h-5 text-orange-500" />;
case 'HIGH_VOLATILITY':
return <AlertTriangle className="w-5 h-5 text-yellow-500" />;
case 'ECONOMIC_EVENT':
return <Calendar className="w-5 h-5 text-purple-500" />;
default:
return <Bell className="w-5 h-5 text-gray-500" />;
}
};
const getSeverityColor = (severity: AlertSeverity) => {
switch (severity) {
case 'CRITICAL':
return 'bg-red-500/20 border-red-500';
case 'HIGH':
return 'bg-orange-500/20 border-orange-500';
case 'MEDIUM':
return 'bg-yellow-500/20 border-yellow-500';
case 'LOW':
return 'bg-blue-500/20 border-blue-500';
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffMs / 60000);
if (diffSecs < 60) {
return `${diffSecs}s ago`;
} else if (diffMins < 60) {
return `${diffMins}m ago`;
} else {
return date.toLocaleTimeString();
}
};
return (
<div
className={`p-3 rounded-md border-l-4 ${getSeverityColor(
alert.severity
)} hover:bg-dark-hover transition-colors`}
>
<div className="flex items-start gap-3">
{getAlertIcon(alert.type)}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-sm">{alert.title}</h4>
<span className="text-xs text-gray-500">{formatTime(alert.timestamp)}</span>
</div>
<p className="text-xs text-gray-400 mb-2">{alert.message}</p>
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded ${getSeverityColor(alert.severity)}`}>
{alert.severity}
</span>
{alert.price && (
<span className="text-xs px-2 py-0.5 rounded bg-dark-bg text-gray-400">
${alert.price.toFixed(2)}
</span>
)}
{alert.change_percent && (
<span
className={`text-xs px-2 py-0.5 rounded ${
alert.change_percent > 0
? 'bg-green-500/20 text-green-500'
: 'bg-red-500/20 text-red-500'
}`}
>
{alert.change_percent > 0 ? '+' : ''}
{alert.change_percent.toFixed(2)}%
</span>
)}
{alert.action_required && (
<span className="text-xs px-2 py-0.5 rounded bg-red-500/20 text-red-500 font-semibold">
ACTION REQUIRED
</span>
)}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,187 @@
import { useState } from 'react';
import { Settings, X } from 'lucide-react';
import type { TabCustomization } from '@/types';
interface ComponentSettingsProps {
customization?: TabCustomization;
onUpdate: (customization: TabCustomization) => void;
availableSettings?: {
refreshRate?: boolean;
autoRefresh?: boolean;
displayMode?: string[];
filters?: Record<string, any>;
theme?: boolean;
};
}
export default function ComponentSettings({
customization = {},
onUpdate,
availableSettings = {},
}: ComponentSettingsProps) {
const [isOpen, setIsOpen] = useState(false);
const [localSettings, setLocalSettings] = useState<TabCustomization>(customization);
const handleSave = () => {
onUpdate(localSettings);
setIsOpen(false);
};
const handleCancel = () => {
setLocalSettings(customization);
setIsOpen(false);
};
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="p-1.5 hover:bg-dark-hover rounded transition-colors opacity-0 group-hover:opacity-100"
title="Component Settings"
>
<Settings className="w-4 h-4 text-gray-400" />
</button>
);
}
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-dark-card rounded-lg shadow-xl max-w-md w-full">
{/* Header */}
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
<h3 className="text-lg font-semibold">Component Settings</h3>
<button onClick={handleCancel} className="text-gray-400 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
{/* Settings */}
<div className="p-4 space-y-4">
{availableSettings.autoRefresh && (
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Auto Refresh</label>
<input
type="checkbox"
checked={localSettings.autoRefresh || false}
onChange={(e) =>
setLocalSettings({ ...localSettings, autoRefresh: e.target.checked })
}
className="rounded"
/>
</div>
)}
{availableSettings.refreshRate && localSettings.autoRefresh && (
<div>
<label className="text-sm font-medium block mb-2">
Refresh Rate (seconds)
</label>
<input
type="number"
min={10}
max={3600}
value={localSettings.refreshRate || 60}
onChange={(e) =>
setLocalSettings({ ...localSettings, refreshRate: Number(e.target.value) })
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg"
/>
</div>
)}
{availableSettings.displayMode && availableSettings.displayMode.length > 0 && (
<div>
<label className="text-sm font-medium block mb-2">Display Mode</label>
<select
value={localSettings.displayMode || 'default'}
onChange={(e) =>
setLocalSettings({ ...localSettings, displayMode: e.target.value })
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg"
>
{availableSettings.displayMode.map((mode) => (
<option key={mode} value={mode}>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</option>
))}
</select>
</div>
)}
{availableSettings.theme && (
<div>
<label className="text-sm font-medium block mb-2">Theme</label>
<select
value={localSettings.theme || 'default'}
onChange={(e) =>
setLocalSettings({
...localSettings,
theme: e.target.value as 'default' | 'compact' | 'detailed',
})
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg"
>
<option value="default">Default</option>
<option value="compact">Compact</option>
<option value="detailed">Detailed</option>
</select>
</div>
)}
{availableSettings.filters && Object.keys(availableSettings.filters).length > 0 && (
<div className="border-t border-gray-700 pt-4">
<label className="text-sm font-medium block mb-2">Filters</label>
{Object.entries(availableSettings.filters).map(([key, options]) => (
<div key={key} className="mb-3">
<label className="text-xs text-gray-400 block mb-1">
{key.charAt(0).toUpperCase() + key.slice(1)}
</label>
{Array.isArray(options) ? (
<select
value={localSettings.filters?.[key] || options[0]}
onChange={(e) =>
setLocalSettings({
...localSettings,
filters: { ...localSettings.filters, [key]: e.target.value },
})
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm"
>
{options.map((opt: string) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
) : (
<input
type="text"
value={localSettings.filters?.[key] || ''}
onChange={(e) =>
setLocalSettings({
...localSettings,
filters: { ...localSettings.filters, [key]: e.target.value },
})
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm"
/>
)}
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-700 flex items-center justify-end gap-2">
<button onClick={handleCancel} className="btn-secondary">
Cancel
</button>
<button onClick={handleSave} className="btn-primary">
Save
</button>
</div>
</div>
</div>
);
}
+416
View File
@@ -0,0 +1,416 @@
import { useState, useEffect } from 'react';
import { CheckCircle2, Circle, Clock, TrendingUp, Calendar, BarChart3, AlertCircle } from 'lucide-react';
interface ChecklistItem {
id: string;
title: string;
description: string;
completed: boolean;
category: 'pre-market' | 'active-trading' | 'post-market';
}
interface DailyChecklistProps {
onComplete?: (completedCount: number, totalCount: number) => void;
}
const DEFAULT_CHECKLIST: ChecklistItem[] = [
// Pre-Market (Before Trading)
{
id: 'review-calendar',
title: 'Check Economic Calendar',
description: 'Review today\'s economic events and their potential impact on gold',
completed: false,
category: 'pre-market',
},
{
id: 'check-news',
title: 'Scan Market News',
description: 'Read overnight news, geopolitical events, Fed statements',
completed: false,
category: 'pre-market',
},
{
id: 'analyze-sentiment',
title: 'Analyze Market Sentiment',
description: 'Check overall market sentiment and gold-specific sentiment',
completed: false,
category: 'pre-market',
},
{
id: 'identify-levels',
title: 'Identify Key Levels',
description: 'Mark support/resistance, pivot points, previous high/low',
completed: false,
category: 'pre-market',
},
{
id: 'set-plan',
title: 'Create Trading Plan',
description: 'Set daily target, max loss, entry/exit criteria',
completed: false,
category: 'pre-market',
},
{
id: 'check-risk',
title: 'Review Risk Parameters',
description: 'Confirm position sizing, stop-loss levels, risk per trade',
completed: false,
category: 'pre-market',
},
{
id: 'mental-prep',
title: 'Mental Preparation',
description: 'Review trading rules, stay disciplined, manage emotions',
completed: false,
category: 'pre-market',
},
// Active Trading
{
id: 'monitor-price',
title: 'Monitor Price Action',
description: 'Watch for entry signals based on your plan',
completed: false,
category: 'active-trading',
},
{
id: 'follow-plan',
title: 'Execute According to Plan',
description: 'Only take trades that match your criteria',
completed: false,
category: 'active-trading',
},
{
id: 'manage-positions',
title: 'Manage Open Positions',
description: 'Adjust stops, take partials, follow exit rules',
completed: false,
category: 'active-trading',
},
{
id: 'track-news-live',
title: 'Track Breaking News',
description: 'Monitor for unexpected events that could impact positions',
completed: false,
category: 'active-trading',
},
{
id: 'record-trades',
title: 'Log Trades in Real-Time',
description: 'Record entry reasons, emotions, and setup quality',
completed: false,
category: 'active-trading',
},
// Post-Market (After Trading)
{
id: 'review-trades',
title: 'Review All Trades',
description: 'Analyze winners and losers, identify patterns',
completed: false,
category: 'post-market',
},
{
id: 'update-journal',
title: 'Complete Trading Journal',
description: 'Document lessons learned, emotional state, market conditions',
completed: false,
category: 'post-market',
},
{
id: 'analyze-performance',
title: 'Analyze Daily Performance',
description: 'Calculate P&L, win rate, risk-reward, adherence to plan',
completed: false,
category: 'post-market',
},
{
id: 'update-levels',
title: 'Update Key Levels',
description: 'Mark new support/resistance for tomorrow',
completed: false,
category: 'post-market',
},
{
id: 'plan-tomorrow',
title: 'Preview Tomorrow',
description: 'Check upcoming economic events and prepare strategy',
completed: false,
category: 'post-market',
},
{
id: 'set-alerts',
title: 'Set Price Alerts',
description: 'Configure alerts for overnight price movements',
completed: false,
category: 'post-market',
},
];
export default function DailyChecklist({ onComplete }: DailyChecklistProps) {
const [checklist, setChecklist] = useState<ChecklistItem[]>(() => {
const stored = localStorage.getItem('daily-trading-checklist');
if (stored) {
try {
return JSON.parse(stored);
} catch {
return DEFAULT_CHECKLIST;
}
}
return DEFAULT_CHECKLIST;
});
const [activeCategory, setActiveCategory] = useState<'pre-market' | 'active-trading' | 'post-market'>('pre-market');
const [showCompleted, setShowCompleted] = useState(true);
// Save to localStorage whenever checklist changes
useEffect(() => {
localStorage.setItem('daily-trading-checklist', JSON.stringify(checklist));
const completed = checklist.filter(item => item.completed).length;
const total = checklist.length;
if (onComplete) {
onComplete(completed, total);
}
}, [checklist, onComplete]);
// Reset checklist at midnight
useEffect(() => {
const checkReset = () => {
const lastReset = localStorage.getItem('checklist-last-reset');
const today = new Date().toDateString();
if (lastReset !== today) {
setChecklist(DEFAULT_CHECKLIST);
localStorage.setItem('checklist-last-reset', today);
}
};
checkReset();
const interval = setInterval(checkReset, 60000); // Check every minute
return () => clearInterval(interval);
}, []);
const handleToggle = (id: string) => {
setChecklist(prev =>
prev.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
};
const handleResetAll = () => {
if (confirm('Reset all checklist items? This will mark all as incomplete.')) {
setChecklist(DEFAULT_CHECKLIST);
}
};
const getCategoryItems = (category: string) => {
return checklist.filter(item => item.category === category);
};
const getCategoryProgress = (category: string) => {
const items = getCategoryItems(category);
const completed = items.filter(item => item.completed).length;
return { completed, total: items.length, percentage: (completed / items.length) * 100 };
};
const categories = [
{
id: 'pre-market',
label: 'Pre-Market',
icon: Calendar,
color: 'text-blue-500',
description: 'Before trading begins'
},
{
id: 'active-trading',
label: 'Active Trading',
icon: TrendingUp,
color: 'text-green-500',
description: 'During market hours'
},
{
id: 'post-market',
label: 'Post-Market',
icon: BarChart3,
color: 'text-purple-500',
description: 'After trading closes'
},
];
const totalProgress = getCategoryProgress('pre-market').completed +
getCategoryProgress('active-trading').completed +
getCategoryProgress('post-market').completed;
const totalItems = checklist.length;
const overallPercentage = (totalProgress / totalItems) * 100;
const currentCategoryData = categories.find(c => c.id === activeCategory)!;
const currentProgress = getCategoryProgress(activeCategory);
const displayItems = getCategoryItems(activeCategory);
const visibleItems = showCompleted ? displayItems : displayItems.filter(item => !item.completed);
return (
<div className="h-full flex flex-col">
{/* Header with overall progress */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<Clock className="w-6 h-6 text-gold-500" />
<div>
<h3 className="text-lg font-semibold">Daily Trading Checklist</h3>
<p className="text-xs text-gray-400">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</p>
</div>
</div>
<button
onClick={handleResetAll}
className="text-xs text-gray-400 hover:text-white transition-colors"
>
Reset All
</button>
</div>
{/* Overall progress bar */}
<div className="mb-2">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-400">Overall Progress</span>
<span className="text-gold-500 font-semibold">
{totalProgress}/{totalItems} ({overallPercentage.toFixed(0)}%)
</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className="bg-gold-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${overallPercentage}%` }}
/>
</div>
</div>
</div>
{/* Category tabs */}
<div className="flex gap-2 mb-4 border-b border-gray-700">
{categories.map(category => {
const progress = getCategoryProgress(category.id);
const Icon = category.icon;
return (
<button
key={category.id}
onClick={() => setActiveCategory(category.id as any)}
className={`flex-1 px-4 py-3 border-b-2 transition-colors ${
activeCategory === category.id
? 'border-gold-500 text-white'
: 'border-transparent text-gray-400 hover:text-white'
}`}
>
<div className="flex items-center justify-center gap-2 mb-1">
<Icon className={`w-4 h-4 ${activeCategory === category.id ? category.color : ''}`} />
<span className="text-sm font-medium">{category.label}</span>
</div>
<div className="text-xs">
{progress.completed}/{progress.total}
</div>
</button>
);
})}
</div>
{/* Category info */}
<div className="mb-4 p-3 bg-dark-hover rounded-lg">
<div className="flex items-center justify-between">
<div>
<h4 className={`font-semibold ${currentCategoryData.color}`}>
{currentCategoryData.label}
</h4>
<p className="text-xs text-gray-400">{currentCategoryData.description}</p>
</div>
<div className="text-right">
<div className="text-lg font-bold text-gold-500">
{currentProgress.percentage.toFixed(0)}%
</div>
<div className="text-xs text-gray-400">
{currentProgress.completed} / {currentProgress.total}
</div>
</div>
</div>
</div>
{/* Show completed toggle */}
<div className="mb-3 flex items-center justify-end">
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<input
type="checkbox"
checked={showCompleted}
onChange={(e) => setShowCompleted(e.target.checked)}
className="rounded"
/>
<span>Show completed</span>
</label>
</div>
{/* Checklist items */}
<div className="flex-1 overflow-y-auto space-y-2">
{visibleItems.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<CheckCircle2 className="w-12 h-12 mx-auto mb-3 text-green-500" />
<p>All tasks completed!</p>
<p className="text-sm">Great job on this section.</p>
</div>
) : (
visibleItems.map(item => (
<button
key={item.id}
onClick={() => handleToggle(item.id)}
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
item.completed
? 'border-green-500/30 bg-green-500/5'
: 'border-gray-700 hover:border-gray-600 bg-dark-hover'
}`}
>
<div className="flex items-start gap-3">
<div className="pt-0.5">
{item.completed ? (
<CheckCircle2 className="w-5 h-5 text-green-500" />
) : (
<Circle className="w-5 h-5 text-gray-500" />
)}
</div>
<div className="flex-1">
<h5 className={`font-medium mb-1 ${
item.completed ? 'text-gray-500 line-through' : 'text-white'
}`}>
{item.title}
</h5>
<p className={`text-sm ${
item.completed ? 'text-gray-600' : 'text-gray-400'
}`}>
{item.description}
</p>
</div>
</div>
</button>
))
)}
</div>
{/* Category completion message */}
{currentProgress.completed === currentProgress.total && currentProgress.total > 0 && (
<div className="mt-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 text-green-500">
<AlertCircle className="w-5 h-5" />
<span className="text-sm font-medium">
{currentCategoryData.label} phase complete!
{activeCategory === 'pre-market' && ' Ready to trade.'}
{activeCategory === 'active-trading' && ' Well managed!'}
{activeCategory === 'post-market' && ' See you tomorrow!'}
</span>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,402 @@
import { useState, useEffect } from 'react';
import { Sun, TrendingUp, TrendingDown, Calendar, AlertCircle, Newspaper, BarChart3 } from 'lucide-react';
interface MarketSummary {
date: string;
sentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
priceAction: {
current: number;
open: number;
high: number;
low: number;
change: number;
changePercent: number;
};
keyLevels: {
resistance: number[];
support: number[];
pivot: number;
};
economicEvents: Array<{
time: string;
event: string;
impact: 'HIGH' | 'MEDIUM' | 'LOW';
forecast?: string;
}>;
newsSummary: {
bullishCount: number;
bearishCount: number;
topHeadlines: string[];
};
aiPrediction: {
direction: 'UP' | 'DOWN' | 'SIDEWAYS';
confidence: number;
keyFactors: string[];
};
}
interface DailyMarketSummaryProps {
currentPrice: number;
}
export default function DailyMarketSummary({ currentPrice }: DailyMarketSummaryProps) {
const [summary, setSummary] = useState<MarketSummary>({
date: new Date().toDateString(),
sentiment: 'NEUTRAL',
priceAction: {
current: currentPrice,
open: currentPrice - 5,
high: currentPrice + 10,
low: currentPrice - 12,
change: 5,
changePercent: 0.25,
},
keyLevels: {
resistance: [currentPrice + 20, currentPrice + 40, currentPrice + 60],
support: [currentPrice - 20, currentPrice - 40, currentPrice - 60],
pivot: currentPrice,
},
economicEvents: [
{ time: '08:30', event: 'US CPI Data', impact: 'HIGH', forecast: '0.3%' },
{ time: '10:00', event: 'Fed Speech', impact: 'HIGH' },
{ time: '14:00', event: 'Gold Inventory', impact: 'MEDIUM' },
],
newsSummary: {
bullishCount: 12,
bearishCount: 8,
topHeadlines: [
'Dollar weakens amid Fed rate cut expectations',
'Geopolitical tensions boost safe-haven demand',
'Central banks continue gold accumulation',
],
},
aiPrediction: {
direction: 'UP',
confidence: 72,
keyFactors: [
'Weakening dollar trend',
'Strong technical support holding',
'Bullish news sentiment',
'Upcoming high-impact economic data',
],
},
});
const [selectedView, setSelectedView] = useState<'overview' | 'levels' | 'events' | 'ai'>('overview');
// Update summary when price changes significantly
useEffect(() => {
setSummary(prev => ({
...prev,
priceAction: {
...prev.priceAction,
current: currentPrice,
change: currentPrice - prev.priceAction.open,
changePercent: ((currentPrice - prev.priceAction.open) / prev.priceAction.open) * 100,
},
}));
}, [currentPrice]);
const getSentimentColor = (sentiment: string) => {
switch (sentiment) {
case 'BULLISH': return 'text-green-500';
case 'BEARISH': return 'text-red-500';
default: return 'text-gray-400';
}
};
const getImpactColor = (impact: string) => {
switch (impact) {
case 'HIGH': return 'bg-red-500/20 text-red-500';
case 'MEDIUM': return 'bg-yellow-500/20 text-yellow-500';
case 'LOW': return 'bg-blue-500/20 text-blue-500';
default: return 'bg-gray-500/20 text-gray-500';
}
};
const views = [
{ id: 'overview', label: 'Overview', icon: Sun },
{ id: 'levels', label: 'Key Levels', icon: BarChart3 },
{ id: 'events', label: 'Events', icon: Calendar },
{ id: 'ai', label: 'AI Forecast', icon: TrendingUp },
];
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="mb-4 pb-4 border-b border-gray-700">
<div className="flex items-center gap-3 mb-3">
<Sun className="w-6 h-6 text-gold-500" />
<div>
<h3 className="text-lg font-semibold">Daily Market Brief</h3>
<p className="text-xs text-gray-400">
{new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
})}
</p>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-dark-hover rounded-lg">
<div className="text-xs text-gray-400 mb-1">Current Price</div>
<div className="text-xl font-bold text-white">
${summary.priceAction.current.toFixed(2)}
</div>
<div className={`text-sm font-medium ${
summary.priceAction.change >= 0 ? 'text-green-500' : 'text-red-500'
}`}>
{summary.priceAction.change >= 0 ? '+' : ''}
${summary.priceAction.change.toFixed(2)} ({summary.priceAction.changePercent.toFixed(2)}%)
</div>
</div>
<div className="p-3 bg-dark-hover rounded-lg">
<div className="text-xs text-gray-400 mb-1">Market Sentiment</div>
<div className={`text-xl font-bold ${getSentimentColor(summary.sentiment)}`}>
{summary.sentiment}
</div>
<div className="text-sm text-gray-400">
{summary.newsSummary.bullishCount} 🟢 / {summary.newsSummary.bearishCount} 🔴
</div>
</div>
</div>
</div>
{/* View Selector */}
<div className="flex gap-1 mb-4 p-1 bg-dark-bg rounded-lg">
{views.map(view => {
const Icon = view.icon;
return (
<button
key={view.id}
onClick={() => setSelectedView(view.id as any)}
className={`flex-1 px-3 py-2 rounded-lg transition-colors text-sm ${
selectedView === view.id
? 'bg-gold-500 text-black font-medium'
: 'text-gray-400 hover:text-white'
}`}
>
<Icon className="w-4 h-4 mx-auto mb-1" />
{view.label}
</button>
);
})}
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto">
{/* Overview */}
{selectedView === 'overview' && (
<div className="space-y-4">
{/* Price Range */}
<div className="p-4 bg-dark-hover rounded-lg">
<h4 className="font-semibold mb-3 flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-blue-500" />
Today's Range
</h4>
<div className="grid grid-cols-3 gap-3 text-sm">
<div>
<div className="text-gray-400 text-xs mb-1">Open</div>
<div className="font-semibold">${summary.priceAction.open.toFixed(2)}</div>
</div>
<div>
<div className="text-gray-400 text-xs mb-1">High</div>
<div className="font-semibold text-green-500">${summary.priceAction.high.toFixed(2)}</div>
</div>
<div>
<div className="text-gray-400 text-xs mb-1">Low</div>
<div className="font-semibold text-red-500">${summary.priceAction.low.toFixed(2)}</div>
</div>
</div>
</div>
{/* Top Headlines */}
<div className="p-4 bg-dark-hover rounded-lg">
<h4 className="font-semibold mb-3 flex items-center gap-2">
<Newspaper className="w-4 h-4 text-blue-500" />
Top Headlines
</h4>
<div className="space-y-2">
{summary.newsSummary.topHeadlines.map((headline, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<div className="w-1.5 h-1.5 rounded-full bg-gold-500 mt-1.5 flex-shrink-0" />
<div className="text-gray-300">{headline}</div>
</div>
))}
</div>
</div>
{/* Quick AI Insight */}
<div className={`p-4 rounded-lg border-2 ${
summary.aiPrediction.direction === 'UP'
? 'bg-green-500/10 border-green-500/30'
: summary.aiPrediction.direction === 'DOWN'
? 'bg-red-500/10 border-red-500/30'
: 'bg-gray-500/10 border-gray-500/30'
}`}>
<div className="flex items-center gap-2 mb-2">
{summary.aiPrediction.direction === 'UP' ? (
<TrendingUp className="w-5 h-5 text-green-500" />
) : (
<TrendingDown className="w-5 h-5 text-red-500" />
)}
<div>
<div className="font-semibold">AI Prediction: {summary.aiPrediction.direction}</div>
<div className="text-xs text-gray-400">
Confidence: {summary.aiPrediction.confidence}%
</div>
</div>
</div>
</div>
</div>
)}
{/* Key Levels */}
{selectedView === 'levels' && (
<div className="space-y-4">
<div className="p-4 bg-dark-hover rounded-lg">
<h4 className="font-semibold mb-3 text-green-500">Resistance Levels</h4>
<div className="space-y-2">
{summary.keyLevels.resistance.map((level, i) => (
<div key={i} className="flex items-center justify-between p-2 bg-green-500/5 rounded border border-green-500/20">
<span className="text-sm text-gray-400">R{i + 1}</span>
<span className="font-semibold">${level.toFixed(2)}</span>
<span className="text-xs text-gray-500">
+{((level - currentPrice) / currentPrice * 100).toFixed(2)}%
</span>
</div>
))}
</div>
</div>
<div className="p-4 bg-dark-hover rounded-lg border-2 border-gold-500/30">
<h4 className="font-semibold mb-2 text-gold-500">Pivot Point</h4>
<div className="text-center">
<div className="text-2xl font-bold">${summary.keyLevels.pivot.toFixed(2)}</div>
<div className="text-xs text-gray-400">Key decision level</div>
</div>
</div>
<div className="p-4 bg-dark-hover rounded-lg">
<h4 className="font-semibold mb-3 text-red-500">Support Levels</h4>
<div className="space-y-2">
{summary.keyLevels.support.map((level, i) => (
<div key={i} className="flex items-center justify-between p-2 bg-red-500/5 rounded border border-red-500/20">
<span className="text-sm text-gray-400">S{i + 1}</span>
<span className="font-semibold">${level.toFixed(2)}</span>
<span className="text-xs text-gray-500">
{((level - currentPrice) / currentPrice * 100).toFixed(2)}%
</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Economic Events */}
{selectedView === 'events' && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-5 h-5 text-blue-500" />
<h4 className="font-semibold">Today's Economic Calendar</h4>
</div>
{summary.economicEvents.map((event, i) => (
<div key={i} className="p-4 bg-dark-hover rounded-lg border border-gray-700">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-12 h-12 rounded-lg bg-blue-500/20 flex items-center justify-center">
<span className="text-sm font-bold text-blue-500">{event.time}</span>
</div>
<div>
<div className="font-semibold">{event.event}</div>
{event.forecast && (
<div className="text-xs text-gray-400">Forecast: {event.forecast}</div>
)}
</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${getImpactColor(event.impact)}`}>
{event.impact}
</span>
</div>
</div>
))}
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg mt-4">
<div className="flex items-center gap-2 text-yellow-500 text-sm">
<AlertCircle className="w-4 h-4" />
<span>Be cautious during high-impact events</span>
</div>
</div>
</div>
)}
{/* AI Forecast */}
{selectedView === 'ai' && (
<div className="space-y-4">
<div className="p-4 bg-gradient-to-br from-purple-500/20 to-blue-500/20 rounded-lg border border-purple-500/30">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-lg bg-purple-500/30 flex items-center justify-center">
{summary.aiPrediction.direction === 'UP' ? (
<TrendingUp className="w-6 h-6 text-green-500" />
) : (
<TrendingDown className="w-6 h-6 text-red-500" />
)}
</div>
<div>
<div className="text-xl font-bold">
{summary.aiPrediction.direction}WARD Bias
</div>
<div className="text-sm text-gray-400">
AI Confidence: {summary.aiPrediction.confidence}%
</div>
</div>
</div>
{/* Confidence Bar */}
<div className="mb-4">
<div className="w-full bg-gray-700 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all ${
summary.aiPrediction.confidence > 70
? 'bg-green-500'
: summary.aiPrediction.confidence > 50
? 'bg-yellow-500'
: 'bg-red-500'
}`}
style={{ width: `${summary.aiPrediction.confidence}%` }}
/>
</div>
</div>
</div>
{/* Key Factors */}
<div className="p-4 bg-dark-hover rounded-lg">
<h4 className="font-semibold mb-3">Key Contributing Factors</h4>
<div className="space-y-2">
{summary.aiPrediction.keyFactors.map((factor, i) => (
<div key={i} className="flex items-start gap-2">
<div className="w-6 h-6 rounded-full bg-gold-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs font-bold text-gold-500">{i + 1}</span>
</div>
<div className="text-sm text-gray-300">{factor}</div>
</div>
))}
</div>
</div>
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<div className="text-xs text-blue-400">
<strong>Disclaimer:</strong> AI predictions are based on historical patterns and current data. Always use proper risk management and make your own trading decisions.
</div>
</div>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,485 @@
import { useState, useEffect } from 'react';
import { Target, DollarSign, TrendingUp, TrendingDown, AlertTriangle, Save, Edit2 } from 'lucide-react';
interface TradingPlan {
date: string;
bias: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
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;
}
interface DailyTradingPlanProps {
currentPrice: number;
onPlanUpdate?: (plan: TradingPlan) => void;
}
export default function DailyTradingPlan({ currentPrice, onPlanUpdate }: DailyTradingPlanProps) {
const [isEditing, setIsEditing] = useState(false);
const [plan, setPlan] = useState<TradingPlan>(() => {
const stored = localStorage.getItem('daily-trading-plan');
const today = new Date().toDateString();
if (stored) {
try {
const parsed = JSON.parse(stored);
// If plan is from today, use it; otherwise create new
if (parsed.date === today) {
return parsed;
}
} catch {
// Fall through to create new plan
}
}
// Create new plan for today
return {
date: today,
bias: 'NEUTRAL',
dailyTarget: 500,
maxLoss: 250,
entryZone: { min: currentPrice - 10, max: currentPrice + 10 },
targetPrice: currentPrice + 20,
stopLoss: currentPrice - 15,
keyLevels: {
support: [currentPrice - 20, currentPrice - 40],
resistance: [currentPrice + 20, currentPrice + 40],
},
tradingNotes: '',
maxTrades: 3,
actualTrades: 0,
actualPnL: 0,
planFollowed: true,
};
});
useEffect(() => {
localStorage.setItem('daily-trading-plan', JSON.stringify(plan));
if (onPlanUpdate) {
onPlanUpdate(plan);
}
}, [plan, onPlanUpdate]);
const handleSave = () => {
setIsEditing(false);
// Trigger save notification could go here
};
const handleReset = () => {
if (confirm('Create a new plan for today? This will clear current plan.')) {
const today = new Date().toDateString();
setPlan({
date: today,
bias: 'NEUTRAL',
dailyTarget: 500,
maxLoss: 250,
entryZone: { min: currentPrice - 10, max: currentPrice + 10 },
targetPrice: currentPrice + 20,
stopLoss: currentPrice - 15,
keyLevels: {
support: [currentPrice - 20, currentPrice - 40],
resistance: [currentPrice + 20, currentPrice + 40],
},
tradingNotes: '',
maxTrades: 3,
actualTrades: 0,
actualPnL: 0,
planFollowed: true,
});
setIsEditing(true);
}
};
const addSupport = () => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: [...prev.keyLevels.support, currentPrice - 10],
},
}));
};
const addResistance = () => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: [...prev.keyLevels.resistance, currentPrice + 10],
},
}));
};
const removeSupport = (index: number) => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.filter((_, i) => i !== index),
},
}));
};
const removeResistance = (index: number) => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.filter((_, i) => i !== index),
},
}));
};
const updateSupport = (index: number, value: number) => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
support: prev.keyLevels.support.map((s, i) => i === index ? value : s),
},
}));
};
const updateResistance = (index: number, value: number) => {
setPlan(prev => ({
...prev,
keyLevels: {
...prev.keyLevels,
resistance: prev.keyLevels.resistance.map((r, i) => i === index ? value : r),
},
}));
};
const targetReached = plan.actualPnL >= plan.dailyTarget;
const maxLossReached = plan.actualPnL <= -plan.maxLoss;
const shouldStopTrading = targetReached || maxLossReached;
return (
<div className="h-full flex flex-col overflow-y-auto">
{/* Header */}
<div className="mb-4 pb-4 border-b border-gray-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<Target className="w-6 h-6 text-gold-500" />
<div>
<h3 className="text-lg font-semibold">Daily Trading Plan</h3>
<p className="text-xs text-gray-400">
{new Date(plan.date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})}
</p>
</div>
</div>
<div className="flex gap-2">
{!isEditing ? (
<>
<button
onClick={() => setIsEditing(true)}
className="btn-secondary text-sm flex items-center gap-2"
>
<Edit2 className="w-4 h-4" />
Edit
</button>
<button
onClick={handleReset}
className="btn-secondary text-sm"
>
New Plan
</button>
</>
) : (
<button
onClick={handleSave}
className="btn-primary text-sm flex items-center gap-2"
>
<Save className="w-4 h-4" />
Save
</button>
)}
</div>
</div>
</div>
{/* Market Bias */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Market Bias</label>
<div className="grid grid-cols-3 gap-2">
{(['BULLISH', 'NEUTRAL', 'BEARISH'] as const).map(bias => (
<button
key={bias}
disabled={!isEditing}
onClick={() => setPlan({ ...plan, bias })}
className={`p-3 rounded-lg border-2 transition-all ${
plan.bias === bias
? bias === 'BULLISH'
? 'border-green-500 bg-green-500/10 text-green-500'
: bias === 'BEARISH'
? 'border-red-500 bg-red-500/10 text-red-500'
: 'border-gray-500 bg-gray-500/10 text-gray-300'
: 'border-gray-700 text-gray-400 hover:border-gray-600'
} ${!isEditing ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
{bias === 'BULLISH' && <TrendingUp className="w-5 h-5 mx-auto mb-1" />}
{bias === 'NEUTRAL' && <div className="w-5 h-0.5 bg-current mx-auto mb-2" />}
{bias === 'BEARISH' && <TrendingDown className="w-5 h-5 mx-auto mb-1" />}
<div className="text-xs font-medium">{bias}</div>
</button>
))}
</div>
</div>
{/* Risk Parameters */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-2">
<DollarSign className="w-4 h-4 inline mr-1" />
Daily Target
</label>
<input
type="number"
disabled={!isEditing}
value={plan.dailyTarget}
onChange={(e) => setPlan({ ...plan, dailyTarget: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg disabled:opacity-60"
placeholder="500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
<AlertTriangle className="w-4 h-4 inline mr-1" />
Max Loss
</label>
<input
type="number"
disabled={!isEditing}
value={plan.maxLoss}
onChange={(e) => setPlan({ ...plan, maxLoss: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg disabled:opacity-60"
placeholder="250"
/>
</div>
</div>
{/* Entry Zone */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Entry Zone</label>
<div className="grid grid-cols-2 gap-2">
<div>
<input
type="number"
disabled={!isEditing}
step="0.01"
value={plan.entryZone.min}
onChange={(e) =>
setPlan({
...plan,
entryZone: { ...plan.entryZone, min: Number(e.target.value) },
})
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm disabled:opacity-60"
placeholder="Min"
/>
<p className="text-xs text-gray-500 mt-1">Minimum entry</p>
</div>
<div>
<input
type="number"
disabled={!isEditing}
step="0.01"
value={plan.entryZone.max}
onChange={(e) =>
setPlan({
...plan,
entryZone: { ...plan.entryZone, max: Number(e.target.value) },
})
}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm disabled:opacity-60"
placeholder="Max"
/>
<p className="text-xs text-gray-500 mt-1">Maximum entry</p>
</div>
</div>
</div>
{/* Target & Stop Loss */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-2 text-green-500">Target Price</label>
<input
type="number"
disabled={!isEditing}
step="0.01"
value={plan.targetPrice}
onChange={(e) => setPlan({ ...plan, targetPrice: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-green-500/30 rounded-lg disabled:opacity-60"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-red-500">Stop Loss</label>
<input
type="number"
disabled={!isEditing}
step="0.01"
value={plan.stopLoss}
onChange={(e) => setPlan({ ...plan, stopLoss: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-red-500/30 rounded-lg disabled:opacity-60"
/>
</div>
</div>
{/* Key Levels */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Support Levels</label>
<div className="space-y-2 mb-2">
{plan.keyLevels.support.map((level, index) => (
<div key={index} className="flex gap-2">
<input
type="number"
disabled={!isEditing}
step="0.01"
value={level}
onChange={(e) => updateSupport(index, Number(e.target.value))}
className="flex-1 px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm disabled:opacity-60"
/>
{isEditing && (
<button
onClick={() => removeSupport(index)}
className="px-3 py-2 bg-red-500/20 text-red-500 rounded-lg hover:bg-red-500/30"
>
</button>
)}
</div>
))}
</div>
{isEditing && (
<button onClick={addSupport} className="btn-secondary text-sm w-full">
+ Add Support
</button>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Resistance Levels</label>
<div className="space-y-2 mb-2">
{plan.keyLevels.resistance.map((level, index) => (
<div key={index} className="flex gap-2">
<input
type="number"
disabled={!isEditing}
step="0.01"
value={level}
onChange={(e) => updateResistance(index, Number(e.target.value))}
className="flex-1 px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm disabled:opacity-60"
/>
{isEditing && (
<button
onClick={() => removeResistance(index)}
className="px-3 py-2 bg-red-500/20 text-red-500 rounded-lg hover:bg-red-500/30"
>
</button>
)}
</div>
))}
</div>
{isEditing && (
<button onClick={addResistance} className="btn-secondary text-sm w-full">
+ Add Resistance
</button>
)}
</div>
{/* Max Trades */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Maximum Trades Today</label>
<input
type="number"
disabled={!isEditing}
min={1}
max={10}
value={plan.maxTrades}
onChange={(e) => setPlan({ ...plan, maxTrades: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg disabled:opacity-60"
/>
</div>
{/* Trading Notes */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Trading Notes & Strategy</label>
<textarea
disabled={!isEditing}
value={plan.tradingNotes}
onChange={(e) => setPlan({ ...plan, tradingNotes: e.target.value })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded-lg disabled:opacity-60"
rows={4}
placeholder="Market conditions, strategy notes, things to watch..."
/>
</div>
{/* Actual Performance (read-only summary) */}
<div className="mt-auto pt-4 border-t border-gray-700">
<h4 className="text-sm font-semibold mb-3">Today's Performance</h4>
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="p-3 bg-dark-hover rounded-lg">
<div className="text-xs text-gray-400 mb-1">Actual P&L</div>
<div className={`text-lg font-bold ${
plan.actualPnL > 0 ? 'text-green-500' : plan.actualPnL < 0 ? 'text-red-500' : 'text-gray-400'
}`}>
${plan.actualPnL.toFixed(2)}
</div>
</div>
<div className="p-3 bg-dark-hover rounded-lg">
<div className="text-xs text-gray-400 mb-1">Trades Taken</div>
<div className={`text-lg font-bold ${
plan.actualTrades >= plan.maxTrades ? 'text-red-500' : 'text-white'
}`}>
{plan.actualTrades} / {plan.maxTrades}
</div>
</div>
</div>
{/* Trading Status Alert */}
{shouldStopTrading && (
<div className={`p-3 rounded-lg border-2 ${
targetReached
? 'bg-green-500/10 border-green-500/30 text-green-500'
: 'bg-red-500/10 border-red-500/30 text-red-500'
}`}>
<div className="flex items-center gap-2 font-semibold">
<AlertTriangle className="w-5 h-5" />
{targetReached ? 'Daily Target Reached!' : 'Max Loss Hit!'}
</div>
<p className="text-sm mt-1">
{targetReached
? 'Consider stopping trading for today.'
: 'Stop trading immediately to preserve capital.'}
</p>
</div>
)}
{plan.actualTrades >= plan.maxTrades && !shouldStopTrading && (
<div className="p-3 bg-yellow-500/10 border-2 border-yellow-500/30 rounded-lg text-yellow-500">
<div className="flex items-center gap-2 font-semibold text-sm">
<AlertTriangle className="w-4 h-4" />
Maximum trades reached
</div>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,401 @@
import { useState } from 'react';
import { Settings, Layout, Grid, Columns, Save, RotateCcw, Eye, EyeOff, GripVertical } from 'lucide-react';
import type { DashboardConfig, TabConfig, LayoutMode, LayoutPreset } from '@/types';
interface DashboardCustomizerProps {
config: DashboardConfig;
onConfigChange: (config: DashboardConfig) => void;
onSavePreset: (name: string, description: string) => void;
onLoadPreset: (presetId: string) => void;
onResetToDefault: () => void;
}
const DEFAULT_PRESETS: LayoutPreset[] = [
{
id: 'trading-focus',
name: 'Trading Focus',
description: 'Optimized for active trading with chart and controls prominent',
mode: 'grid',
tabs: [],
},
{
id: 'analysis-focus',
name: 'Analysis Focus',
description: 'Full-screen analytics and AI insights',
mode: 'tabs',
tabs: [],
},
{
id: 'news-focus',
name: 'News Focus',
description: 'News and market updates at the forefront',
mode: 'split',
tabs: [],
},
{
id: 'balanced',
name: 'Balanced View',
description: 'Equal emphasis on all components',
mode: 'grid',
tabs: [],
},
];
export default function DashboardCustomizer({
config,
onConfigChange,
onSavePreset,
onLoadPreset,
onResetToDefault,
}: DashboardCustomizerProps) {
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'layout' | 'tabs' | 'presets'>('layout');
const [draggedTab, setDraggedTab] = useState<TabConfig | null>(null);
const [presetName, setPresetName] = useState('');
const [presetDescription, setPresetDescription] = useState('');
const handleLayoutModeChange = (mode: LayoutMode) => {
onConfigChange({ ...config, mode });
};
const handleTabToggle = (tabId: string) => {
const updatedTabs = config.tabs.map((tab) =>
tab.id === tabId ? { ...tab, visible: !tab.visible } : tab
);
onConfigChange({ ...config, tabs: updatedTabs });
};
const handleTabPin = (tabId: string) => {
const updatedTabs = config.tabs.map((tab) =>
tab.id === tabId ? { ...tab, pinned: !tab.pinned } : tab
);
onConfigChange({ ...config, tabs: updatedTabs });
};
const handleTabSizeChange = (tabId: string, size: TabConfig['size']) => {
const updatedTabs = config.tabs.map((tab) =>
tab.id === tabId ? { ...tab, size } : tab
);
onConfigChange({ ...config, tabs: updatedTabs });
};
const handleDragStart = (tab: TabConfig) => {
setDraggedTab(tab);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = (targetTab: TabConfig) => {
if (!draggedTab) return;
const updatedTabs = [...config.tabs];
const draggedIndex = updatedTabs.findIndex((t) => t.id === draggedTab.id);
const targetIndex = updatedTabs.findIndex((t) => t.id === targetTab.id);
if (draggedIndex !== -1 && targetIndex !== -1) {
updatedTabs.splice(draggedIndex, 1);
updatedTabs.splice(targetIndex, 0, draggedTab);
// Update order numbers
const reorderedTabs = updatedTabs.map((tab, index) => ({
...tab,
order: index,
}));
onConfigChange({ ...config, tabs: reorderedTabs });
}
setDraggedTab(null);
};
const handleSavePreset = () => {
if (presetName.trim()) {
onSavePreset(presetName, presetDescription);
setPresetName('');
setPresetDescription('');
setActiveTab('presets');
}
};
const allPresets = [...DEFAULT_PRESETS, ...config.customPresets];
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="btn-secondary flex items-center gap-2"
title="Customize Dashboard"
>
<Settings className="w-4 h-4" />
<span>Customize</span>
</button>
);
}
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-dark-card rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-6 border-b border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Settings className="w-6 h-6 text-gold-500" />
<div>
<h2 className="text-xl font-bold">Dashboard Customization</h2>
<p className="text-sm text-gray-400">Personalize your trading interface</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-white transition-colors"
>
</button>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-700">
<button
onClick={() => setActiveTab('layout')}
className={`px-6 py-3 font-medium transition-colors ${
activeTab === 'layout'
? 'text-gold-500 border-b-2 border-gold-500'
: 'text-gray-400 hover:text-white'
}`}
>
<Layout className="w-4 h-4 inline mr-2" />
Layout Mode
</button>
<button
onClick={() => setActiveTab('tabs')}
className={`px-6 py-3 font-medium transition-colors ${
activeTab === 'tabs'
? 'text-gold-500 border-b-2 border-gold-500'
: 'text-gray-400 hover:text-white'
}`}
>
<Columns className="w-4 h-4 inline mr-2" />
Tabs & Order
</button>
<button
onClick={() => setActiveTab('presets')}
className={`px-6 py-3 font-medium transition-colors ${
activeTab === 'presets'
? 'text-gold-500 border-b-2 border-gold-500'
: 'text-gray-400 hover:text-white'
}`}
>
<Grid className="w-4 h-4 inline mr-2" />
Presets
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{activeTab === 'layout' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Choose Layout Mode</h3>
<div className="grid grid-cols-3 gap-4">
<button
onClick={() => handleLayoutModeChange('grid')}
className={`p-4 rounded-lg border-2 transition-all ${
config.mode === 'grid'
? 'border-gold-500 bg-gold-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<Grid className="w-8 h-8 mx-auto mb-2" />
<div className="font-medium">Grid</div>
<div className="text-xs text-gray-400 mt-1">
Multiple panels visible
</div>
</button>
<button
onClick={() => handleLayoutModeChange('tabs')}
className={`p-4 rounded-lg border-2 transition-all ${
config.mode === 'tabs'
? 'border-gold-500 bg-gold-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<Columns className="w-8 h-8 mx-auto mb-2" />
<div className="font-medium">Tabs</div>
<div className="text-xs text-gray-400 mt-1">
One panel at a time
</div>
</button>
<button
onClick={() => handleLayoutModeChange('split')}
className={`p-4 rounded-lg border-2 transition-all ${
config.mode === 'split'
? 'border-gold-500 bg-gold-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<Layout className="w-8 h-8 mx-auto mb-2" />
<div className="font-medium">Split</div>
<div className="text-xs text-gray-400 mt-1">
Two main sections
</div>
</button>
</div>
</div>
</div>
)}
{activeTab === 'tabs' && (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Manage Tabs</h3>
<p className="text-sm text-gray-400">Drag to reorder, toggle visibility</p>
</div>
<div className="space-y-2">
{config.tabs
.sort((a, b) => a.order - b.order)
.map((tab) => (
<div
key={tab.id}
draggable
onDragStart={() => handleDragStart(tab)}
onDragOver={handleDragOver}
onDrop={() => handleDrop(tab)}
className={`p-4 rounded-lg border-2 transition-all cursor-move ${
tab.visible
? 'border-gray-700 bg-dark-hover'
: 'border-gray-800 bg-gray-900/50 opacity-60'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<GripVertical className="w-5 h-5 text-gray-500" />
<div>
<div className="font-medium">{tab.label}</div>
<div className="text-xs text-gray-400">Order: {tab.order + 1}</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Size selector */}
<select
value={tab.size}
onChange={(e) =>
handleTabSizeChange(tab.id, e.target.value as TabConfig['size'])
}
className="px-2 py-1 bg-dark-bg border border-gray-700 rounded text-sm"
disabled={!tab.visible}
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
<option value="full">Full</option>
</select>
{/* Pin button */}
<button
onClick={() => handleTabPin(tab.id)}
className={`p-2 rounded ${
tab.pinned ? 'text-gold-500' : 'text-gray-400 hover:text-white'
}`}
title="Pin tab"
disabled={!tab.visible}
>
📌
</button>
{/* Visibility toggle */}
<button
onClick={() => handleTabToggle(tab.id)}
className="p-2 rounded hover:bg-dark-hover"
title={tab.visible ? 'Hide' : 'Show'}
>
{tab.visible ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'presets' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Quick Presets</h3>
<div className="grid grid-cols-2 gap-4 mb-6">
{allPresets.map((preset) => (
<button
key={preset.id}
onClick={() => onLoadPreset(preset.id)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
config.activePreset === preset.id
? 'border-gold-500 bg-gold-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<div className="font-medium mb-1">{preset.name}</div>
<div className="text-xs text-gray-400">{preset.description}</div>
</button>
))}
</div>
</div>
<div className="border-t border-gray-700 pt-6">
<h3 className="text-lg font-semibold mb-4">Save Current Layout</h3>
<div className="space-y-3">
<input
type="text"
placeholder="Preset name..."
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
className="w-full px-4 py-2 bg-dark-bg border border-gray-700 rounded-lg focus:border-gold-500 focus:outline-none"
/>
<textarea
placeholder="Description (optional)..."
value={presetDescription}
onChange={(e) => setPresetDescription(e.target.value)}
className="w-full px-4 py-2 bg-dark-bg border border-gray-700 rounded-lg focus:border-gold-500 focus:outline-none"
rows={2}
/>
<button
onClick={handleSavePreset}
disabled={!presetName.trim()}
className="btn-primary w-full flex items-center justify-center gap-2"
>
<Save className="w-4 h-4" />
Save as New Preset
</button>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-gray-700 flex items-center justify-between">
<button
onClick={onResetToDefault}
className="btn-secondary flex items-center gap-2 text-red-500 hover:text-red-400"
>
<RotateCcw className="w-4 h-4" />
Reset to Default
</button>
<button onClick={() => setIsOpen(false)} className="btn-primary">
Done
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,62 @@
import { useEffect, useState } from 'react'
import { decisionsApi } from '@/services/api'
export default function DecisionLogPanel() {
const [items, setItems] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const rows = await decisionsApi.getLatest(50)
if (mounted) setItems(rows)
} catch (e: any) {
setError(e?.message || 'Failed to load decisions')
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
return (
<div className="card">
<div className="title">Decision Log</div>
{loading && <div className="text-gray-400">Loading</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div style={{ overflowX: 'auto' }}>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-400">
<th>Time</th>
<th>Symbol</th>
<th>TF</th>
<th>Rec</th>
<th>Conf</th>
<th>Risk</th>
<th>Rationale</th>
</tr>
</thead>
<tbody>
{items.map((d) => (
<tr key={d.id} className="border-t border-dark-border">
<td className="whitespace-nowrap">{new Date(d.time).toLocaleString()}</td>
<td>{d.symbol}</td>
<td>{d.timeframe}</td>
<td>{d.recommendation}</td>
<td>{(Number(d.confidence) * 100).toFixed(0)}%</td>
<td>{d.risk_level}</td>
<td className="max-w-[600px] truncate" title={d.rationale}>{d.rationale}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
@@ -0,0 +1,71 @@
import { useEffect, useState } from 'react'
import { performanceApi } from '@/services/api'
import SimpleLineChart from './SimpleLineChart'
export default function EquityPerformancePanel() {
const [equity, setEquity] = useState<{ time: number; equity: number }[]>([])
const [perf, setPerf] = useState<any | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const [eq, pf] = await Promise.all([
performanceApi.getEquityHistory('XAU/USD', '1m', 300),
performanceApi.getPerformance('XAU/USD', '1m', 300),
])
if (mounted) { setEquity(eq); setPerf(pf) }
} catch (e: any) {
setError(e?.message || 'Failed to load equity/performance')
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
return (
<div className="card">
<div className="title">Equity & Performance</div>
{loading && <div className="text-gray-400">Loading</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div style={{ display: 'grid', gap: 12 }}>
<SimpleLineChart title="Equity Curve" data={equity.map(r => ({ time: r.time, value: r.equity }))} />
{perf?.available ? (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Metric label="Total Return" value={pct(perf.total_return)} />
<Metric label="Sharpe" value={num(perf.sharpe)} />
<Metric label="Sortino" value={num(perf.sortino)} />
<Metric label="Max Drawdown" value={pct(perf.max_drawdown)} />
</div>
) : (
<div className="text-gray-400">Not enough data</div>
)}
</div>
)}
</div>
)
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="card" style={{ padding: 12 }}>
<div className="text-gray-400 text-xs">{label}</div>
<div className="text-gray-100 text-lg font-semibold">{value}</div>
</div>
)
}
function pct(v?: number | null) {
if (v === null || v === undefined) return '-'
return (v * 100).toFixed(2) + '%'
}
function num(v?: number | null) {
if (v === null || v === undefined) return '-'
return String(Number(v).toFixed(2))
}
+78
View File
@@ -0,0 +1,78 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
errorInfo?: ErrorInfo;
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
this.setState({ errorInfo });
}
handleReset = () => {
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="card bg-red-500/10 border-red-500/50">
<div className="flex items-start gap-3">
<AlertTriangle className="w-6 h-6 text-red-500 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-red-500 mb-2">
Something went wrong
</h3>
<p className="text-sm text-gray-400 mb-4">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
<details className="mb-4">
<summary className="text-xs text-gray-500 cursor-pointer hover:text-gray-400">
Error Details
</summary>
<pre className="mt-2 p-2 bg-dark-bg rounded text-xs text-gray-500 overflow-auto max-h-40">
{this.state.error?.stack}
{'\n\n'}
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
<button
onClick={this.handleReset}
className="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 rounded-md text-sm font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" />
Try Again
</button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
+83
View File
@@ -0,0 +1,83 @@
import { useState } from 'react';
import { Download, FileText, Table, BarChart } from 'lucide-react';
import { exportTradesToCSV, exportPortfolioSummary, exportAnalyticsReport } from '@/utils/export';
import type { Portfolio } from '@/types';
interface ExportMenuProps {
portfolio: Portfolio;
analytics: any;
}
export default function ExportMenu({ portfolio, analytics }: ExportMenuProps) {
const [isOpen, setIsOpen] = useState(false);
const handleExportCSV = () => {
exportTradesToCSV(portfolio.trades, portfolio);
setIsOpen(false);
};
const handleExportJSON = () => {
exportPortfolioSummary(portfolio);
setIsOpen(false);
};
const handleExportReport = () => {
exportAnalyticsReport(portfolio, analytics);
setIsOpen(false);
};
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 bg-dark-surface rounded-md hover:bg-dark-hover transition-colors"
>
<Download className="w-4 h-4" />
<span className="text-sm">Export</span>
</button>
{isOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
<div className="absolute top-full mt-2 right-0 w-64 bg-dark-surface border border-dark-border rounded-lg shadow-xl z-50">
<div className="p-2">
<button
onClick={handleExportCSV}
className="w-full flex items-center gap-3 px-3 py-2 hover:bg-dark-hover rounded transition-colors text-left"
>
<Table className="w-4 h-4 text-green-500" />
<div>
<p className="text-sm font-medium">Export to CSV</p>
<p className="text-xs text-gray-400">All trades data</p>
</div>
</button>
<button
onClick={handleExportJSON}
className="w-full flex items-center gap-3 px-3 py-2 hover:bg-dark-hover rounded transition-colors text-left mt-1"
>
<FileText className="w-4 h-4 text-blue-500" />
<div>
<p className="text-sm font-medium">Export to JSON</p>
<p className="text-xs text-gray-400">Portfolio summary</p>
</div>
</button>
<button
onClick={handleExportReport}
className="w-full flex items-center gap-3 px-3 py-2 hover:bg-dark-hover rounded transition-colors text-left mt-1"
>
<BarChart className="w-4 h-4 text-purple-500" />
<div>
<p className="text-sm font-medium">Analytics Report</p>
<p className="text-xs text-gray-400">Full text report</p>
</div>
</button>
</div>
</div>
</>
)}
</div>
);
}
+187
View File
@@ -0,0 +1,187 @@
import { useEffect, useRef, useState } from 'react';
import { createChart, IChartApi, ISeriesApi, CandlestickData } from 'lightweight-charts';
import type { PriceData } from '@/types';
interface GoldChartProps {
data: PriceData[];
smaData?: { time: number; value: number }[];
onCrosshairMove?: (price: number | null) => void;
liveUpdate?: PriceData | null;
}
export default function GoldChart({ data, smaData, onCrosshairMove, liveUpdate }: GoldChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const smaSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
useEffect(() => {
if (!chartContainerRef.current) return;
// Create chart
const chart = createChart(chartContainerRef.current, {
layout: {
background: { color: '#1a1f2e' },
textColor: '#d1d4dc',
},
grid: {
vertLines: { color: '#2a3142' },
horzLines: { color: '#2a3142' },
},
width: chartContainerRef.current.clientWidth,
height: 500,
timeScale: {
timeVisible: true,
secondsVisible: false,
borderColor: '#2a3142',
},
rightPriceScale: {
borderColor: '#2a3142',
},
crosshair: {
mode: 1,
vertLine: {
color: '#758696',
width: 1,
style: 3,
labelBackgroundColor: '#4682B4',
},
horzLine: {
color: '#758696',
width: 1,
style: 3,
labelBackgroundColor: '#4682B4',
},
},
});
chartRef.current = chart;
// Add candlestick series
const candleSeries = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
});
candleSeriesRef.current = candleSeries;
// Add SMA series
const smaSeries = chart.addLineSeries({
color: '#FFD700',
lineWidth: 2,
title: 'SMA',
});
smaSeriesRef.current = smaSeries;
// Handle resize
const handleResize = () => {
if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({
width: chartContainerRef.current.clientWidth,
});
}
};
window.addEventListener('resize', handleResize);
// Handle crosshair move
chart.subscribeCrosshairMove((param) => {
if (param.time) {
const data = param.seriesData.get(candleSeries) as CandlestickData | undefined;
if (data) {
setCurrentPrice(data.close);
onCrosshairMove?.(data.close);
}
} else {
setCurrentPrice(null);
onCrosshairMove?.(null);
}
});
return () => {
window.removeEventListener('resize', handleResize);
chart.remove();
};
}, [onCrosshairMove]);
// Update data
useEffect(() => {
if (candleSeriesRef.current && data.length > 0) {
const formattedData = data.map((d) => ({
time: d.time as any,
open: d.open,
high: d.high,
low: d.low,
close: d.close,
}));
candleSeriesRef.current.setData(formattedData);
setCurrentPrice(data[data.length - 1].close);
}
}, [data]);
// Update SMA
useEffect(() => {
if (smaSeriesRef.current && smaData && smaData.length > 0) {
const formattedSMA = smaData.map((d) => ({
time: d.time as any,
value: d.value,
}));
smaSeriesRef.current.setData(formattedSMA);
}
}, [smaData]);
// Handle live price updates
useEffect(() => {
if (!liveUpdate || !candleSeriesRef.current || data.length === 0) return;
// Get the last candle timestamp from historical data
const lastHistoricalTime = data[data.length - 1].time;
// Only update if the new time is newer than or equal to the last historical time
// This prevents the "Cannot update oldest data" error
if (liveUpdate.time < lastHistoricalTime) {
console.log('Skipping live update: timestamp is older than historical data');
return;
}
const newCandle = {
time: liveUpdate.time as any,
open: liveUpdate.open,
high: liveUpdate.high,
low: liveUpdate.low,
close: liveUpdate.close,
};
// Update the chart with the new candle
candleSeriesRef.current.update(newCandle);
setCurrentPrice(liveUpdate.close);
}, [liveUpdate, data]);
return (
<div className="card">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-xl font-bold text-gold-500">XAU/USD - Gold Trading</h2>
{currentPrice && (
<p className="text-sm text-gray-400">
Current: ${currentPrice.toFixed(2)}
</p>
)}
</div>
<div className="flex gap-2">
<span className="px-3 py-1 bg-dark-bg rounded-md text-sm">
<span className="text-gold-500"></span> SMA(50)
</span>
</div>
</div>
<div ref={chartContainerRef} className="w-full" />
</div>
);
}
+157
View File
@@ -0,0 +1,157 @@
import { useState } from 'react';
import { Settings, Check, X } from 'lucide-react';
interface IndicatorConfig {
id: string;
name: string;
enabled: boolean;
color: string;
params: Record<string, number>;
}
interface IndicatorPanelProps {
indicators: IndicatorConfig[];
onChange: (indicators: IndicatorConfig[]) => void;
}
export default function IndicatorPanel({ indicators, onChange }: IndicatorPanelProps) {
const [isOpen, setIsOpen] = useState(false);
const toggleIndicator = (id: string) => {
const updated = indicators.map((ind) =>
ind.id === id ? { ...ind, enabled: !ind.enabled } : ind
);
onChange(updated);
};
const updateParam = (id: string, param: string, value: number) => {
// Validate input
if (isNaN(value) || value <= 0) {
return; // Don't update with invalid values
}
const updated = indicators.map((ind) =>
ind.id === id ? { ...ind, params: { ...ind.params, [param]: value } } : ind
);
onChange(updated);
};
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 bg-dark-surface rounded-md hover:bg-dark-hover transition-colors"
>
<Settings className="w-4 h-4" />
<span className="text-sm">Indicators</span>
<span className="text-xs bg-blue-600 px-2 py-0.5 rounded-full">
{indicators.filter((i) => i.enabled).length}
</span>
</button>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
{/* Panel */}
<div className="absolute top-full mt-2 right-0 w-80 bg-dark-surface border border-dark-border rounded-lg shadow-xl z-50">
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">Technical Indicators</h3>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-dark-hover rounded"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-3 max-h-96 overflow-y-auto">
{indicators.map((indicator) => (
<div
key={indicator.id}
className="p-3 bg-dark-bg rounded-lg border border-dark-border"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: indicator.color }}
/>
<span className="font-medium text-sm">{indicator.name}</span>
</div>
<button
onClick={() => toggleIndicator(indicator.id)}
className={`
p-1 rounded transition-colors
${
indicator.enabled
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-400'
}
`}
>
{indicator.enabled ? (
<Check className="w-4 h-4" />
) : (
<X className="w-4 h-4" />
)}
</button>
</div>
{indicator.enabled && Object.keys(indicator.params).length > 0 && (
<div className="space-y-2 mt-3 pt-3 border-t border-dark-border">
{Object.entries(indicator.params).map(([param, value]) => (
<div key={param}>
<label className="block text-xs text-gray-400 mb-1">
{param.charAt(0).toUpperCase() + param.slice(1)}
</label>
<input
type="number"
value={value}
onChange={(e) =>
updateParam(indicator.id, param, Number(e.target.value))
}
min="1"
step="1"
className="w-full px-2 py-1 text-sm bg-dark-bg border border-dark-border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
</div>
))}
</div>
)}
</div>
))}
</div>
<div className="mt-4 pt-4 border-t border-dark-border">
<button
onClick={() => {
const allEnabled = indicators.map((ind) => ({ ...ind, enabled: true }));
onChange(allEnabled);
}}
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition-colors"
>
Enable All
</button>
<button
onClick={() => {
const allDisabled = indicators.map((ind) => ({ ...ind, enabled: false }));
onChange(allDisabled);
}}
className="w-full mt-2 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium transition-colors"
>
Disable All
</button>
</div>
</div>
</div>
</>
)}
</div>
);
}
+171
View File
@@ -0,0 +1,171 @@
import { useEffect, useRef } from 'react'
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts'
function buildWsUrl(pathAndQuery: string) {
const api = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
const base = api.replace(/\/$/, '') // trim trailing slash
const wsBase = base.replace(/^http/, 'ws').replace(/\/$/, '')
return wsBase + pathAndQuery
}
type Bar = { time: any; open: number; high: number; low: number; close: number }
function tfToSeconds(tf: string): number {
switch (tf) {
case '1m': return 60
case '5m': return 5 * 60
case '15m': return 15 * 60
case '30m': return 30 * 60
case '1h': return 60 * 60
case '4h': return 4 * 60 * 60
default: return 60
}
}
export default function LiveKlineChart({ symbol, timeframe = '1m' }: { symbol: string; timeframe?: string }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const chartRef = useRef<IChartApi | null>(null)
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
const wsRef = useRef<WebSocket | null>(null)
const bucketSecondsRef = useRef<number>(tfToSeconds(timeframe))
const bucketsRef = useRef<Map<number, Bar>>(new Map())
useEffect(() => {
if (!containerRef.current) return
const chart = createChart(containerRef.current, {
layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
rightPriceScale: { borderColor: '#1f2937' },
timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
height: 320,
width: containerRef.current.clientWidth,
})
chartRef.current = chart
const series = chart.addCandlestickSeries({
upColor: '#16a34a', downColor: '#dc2626', borderUpColor: '#16a34a', borderDownColor: '#dc2626', wickUpColor: '#16a34a', wickDownColor: '#dc2626',
})
seriesRef.current = series
const onResize = () => {
if (!containerRef.current || !chartRef.current) return
chartRef.current.applyOptions({ width: containerRef.current.clientWidth })
}
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
chart.remove()
}
}, [])
// Keep a rolling 1m history to allow instant timeframe switches without refetch
const history1mRef = useRef<Bar[]>([])
// Load initial 1m history when symbol changes
useEffect(() => {
;(async () => {
try {
bucketsRef.current = new Map()
if (seriesRef.current) seriesRef.current.setData([])
const { marketDataApi } = await import('@/services/api')
const rows = await marketDataApi.getOHLCV(symbol, '1m', 1000)
const bars: Bar[] = rows.map(r => ({ time: r.time as any, open: r.open, high: r.high, low: r.low, close: r.close }))
history1mRef.current = bars
// Build current timeframe view from 1m history
const tfSec = tfToSeconds(timeframe)
const map = new Map<number, Bar>()
for (const r of bars) {
const t = r.time as number
const b = Math.floor(t / tfSec) * tfSec
const cur = map.get(b)
if (!cur) {
map.set(b, { time: b as any, open: r.open, high: r.high, low: r.low, close: r.close })
} else {
cur.high = Math.max(cur.high, r.high)
cur.low = Math.min(cur.low, r.low)
cur.close = r.close
}
}
bucketsRef.current = map
if (seriesRef.current) {
seriesRef.current.setData(Array.from(map.values()))
}
} catch {}
})()
}, [symbol])
// On timeframe change, rebuild view from 1m history only (no refetch)
useEffect(() => {
bucketSecondsRef.current = tfToSeconds(timeframe)
const tfSec = bucketSecondsRef.current
const map = new Map<number, Bar>()
for (const r of history1mRef.current) {
const t = r.time as number
const b = Math.floor(t / tfSec) * tfSec
const cur = map.get(b)
if (!cur) {
map.set(b, { time: b as any, open: r.open, high: r.high, low: r.low, close: r.close })
} else {
cur.high = Math.max(cur.high, r.high)
cur.low = Math.min(cur.low, r.low)
cur.close = r.close
}
}
bucketsRef.current = map
if (seriesRef.current) seriesRef.current.setData(Array.from(map.values()))
}, [timeframe])
useEffect(() => {
// Always subscribe to 1m from backend to minimize upstream loads
const path = `/stream/klines?symbols=${encodeURIComponent(symbol)}&timeframe=1m`
const url = buildWsUrl(path)
const ws = new WebSocket(url)
wsRef.current = ws
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data)
if (!msg || msg.symbol.replace('/', '') !== symbol.replace('/', '')) return
const t = Math.floor(Date.parse(msg.close_time) / 1000)
// Update rolling 1m history (append or replace last if same minute)
const last = history1mRef.current[history1mRef.current.length - 1]
if (last && (last.time as number) === t) {
// update last bar
last.high = Math.max(last.high, msg.high)
last.low = Math.min(last.low, msg.low)
last.close = msg.close
} else if (!last || (last.time as number) < t) {
history1mRef.current.push({ time: t as any, open: msg.open, high: msg.high, low: msg.low, close: msg.close })
if (history1mRef.current.length > 2000) history1mRef.current.shift()
}
// Update current timeframe bucket
const bucket = Math.floor(t / bucketSecondsRef.current) * bucketSecondsRef.current
const existing = bucketsRef.current.get(bucket)
const update: Bar = existing
? {
time: bucket as any,
open: existing.open,
high: Math.max(existing.high, msg.high),
low: Math.min(existing.low, msg.low),
close: msg.close,
}
: {
time: bucket as any,
open: msg.open,
high: msg.high,
low: msg.low,
close: msg.close,
}
bucketsRef.current.set(bucket, update)
seriesRef.current?.update(update)
} catch {}
}
ws.onerror = () => {}
return () => { try { ws.close() } catch {} }
}, [symbol])
return (
<div className="card">
<div className="title">{symbol} {timeframe}</div>
<div ref={containerRef} className="chart" />
</div>
)
}
+114
View File
@@ -0,0 +1,114 @@
import { useEffect, useMemo, useState } from 'react'
import SimpleKlineChart from './SimpleKlineChart'
import SymbolTimeframeSelector from './SymbolTimeframeSelector'
import { useSSEMultiplexer } from '@/hooks/useSSEMultiplexer'
function tfToSeconds(tf: string): number {
switch (tf) {
case '1m': return 60
case '5m': return 5 * 60
case '1h': return 60 * 60
case '4h': return 4 * 60 * 60
default: return 60
}
}
type Bar = { time: number; open: number; high: number; low: number; close: number }
function resampleFrom1m(bars1m: Bar[], timeframe: string): Bar[] {
const tfSec = tfToSeconds(timeframe)
const buckets = new Map<number, Bar>()
for (const r of bars1m) {
const b = Math.floor(r.time / tfSec) * tfSec
const cur = buckets.get(b)
if (!cur) {
buckets.set(b, { time: b, open: r.open, high: r.high, low: r.low, close: r.close })
} else {
cur.high = Math.max(cur.high, r.high)
cur.low = Math.min(cur.low, r.low)
cur.close = r.close
}
}
return Array.from(buckets.values()).sort((a, b) => a.time - b.time)
}
export default function LiveMarketPanel() {
const [symbol1, setSymbol1] = useState<string>('BTCUSDT')
const [tf1, setTf1] = useState<string>('1m')
const [symbol2, setSymbol2] = useState<string>('XAUUSD')
const [tf2, setTf2] = useState<string>('1m')
const symbols = useMemo(() => Array.from(new Set([symbol1, symbol2])), [symbol1, symbol2])
const { isConnected, lastBySymbol } = useSSEMultiplexer(symbols, '1m')
const [history1m, setHistory1m] = useState<Record<string, Bar[]>>({})
// Load initial 1m history for selected symbols
useEffect(() => {
let cancelled = false
;(async () => {
const next: Record<string, Bar[]> = { ...history1m }
for (const s of symbols) {
if (next[s]?.length) continue
try {
const rows = await (await import('@/services/api')).marketDataApi.getOHLCV(s, '1m', 1000)
next[s] = rows.map(r => ({ time: r.time as number, open: r.open, high: r.high, low: r.low, close: r.close }))
} catch {
next[s] = []
}
}
if (!cancelled) setHistory1m(next)
})()
return () => { cancelled = true }
}, [symbols.join(',')])
// Apply incoming SSE events to 1m history buffers
useEffect(() => {
if (!Object.keys(lastBySymbol).length) return
setHistory1m(prev => {
const next = { ...prev }
for (const [sym, evt] of Object.entries(lastBySymbol)) {
if (!evt) continue
const t = Math.floor(Date.parse(evt.close_time) / 1000)
const arr = next[sym] ? [...next[sym]] : []
if (arr.length && arr[arr.length - 1].time === t) {
arr[arr.length - 1] = { time: t, open: evt.open, high: evt.high, low: evt.low, close: evt.close }
} else if (!arr.length || arr[arr.length - 1].time < t) {
arr.push({ time: t, open: evt.open, high: evt.high, low: evt.low, close: evt.close })
if (arr.length > 2000) arr.shift()
}
next[sym] = arr
}
return next
})
}, [lastBySymbol])
const data1 = useMemo(() => {
const base = history1m[symbol1] || []
return tf1 === '1m' ? base : resampleFrom1m(base, tf1)
}, [history1m, symbol1, tf1])
const data2 = useMemo(() => {
const base = history1m[symbol2] || []
return tf2 === '1m' ? base : resampleFrom1m(base, tf2)
}, [history1m, symbol2, tf2])
return (
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr', paddingBottom: 16 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '0 0 8px 0' }}>
<SymbolTimeframeSelector symbol={symbol1} timeframe={tf1} onChangeSymbol={setSymbol1} onChangeTimeframe={setTf1} />
{isConnected ? <span style={{ color: '#10b981' }}> SSE</span> : <span style={{ color: '#ef4444' }}> SSE</span>}
</div>
<SimpleKlineChart title={`${symbol1}${tf1} (SSE)`} data={data1.map(b => ({ ...b, time: b.time as any }))} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '0 0 8px 0' }}>
<SymbolTimeframeSelector symbol={symbol2} timeframe={tf2} onChangeSymbol={setSymbol2} onChangeTimeframe={setTf2} />
{isConnected ? <span style={{ color: '#10b981' }}> SSE</span> : <span style={{ color: '#ef4444' }}> SSE</span>}
</div>
<SimpleKlineChart title={`${symbol2}${tf2} (SSE)`} data={data2.map(b => ({ ...b, time: b.time as any }))} />
</div>
</div>
)
}
@@ -0,0 +1,81 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import SimpleKlineChart from './SimpleKlineChart'
import WatchlistPanel, { useWatchlist } from './WatchlistPanel'
import { useSSEMultiplexer } from '@/hooks/useSSEMultiplexer'
import { marketDataApi } from '@/services/api'
type Bar = { time: number; open: number; high: number; low: number; close: number }
export default function MultiChartSSEPanel() {
const { symbols, setSymbols } = useWatchlist()
const { isConnected, lastBySymbol } = useSSEMultiplexer(symbols, '1m')
const [history, setHistory] = useState<Record<string, Bar[]>>({})
const [lastBars, setLastBars] = useState<Record<string, Bar | undefined>>({})
// Load initial 1m history for each symbol
useEffect(() => {
let cancelled = false
;(async () => {
const next: Record<string, Bar[]> = {}
for (const s of symbols) {
try {
const rows = await marketDataApi.getOHLCV(s, '1m', 1000)
next[s] = rows.map(r => ({ time: r.time, open: r.open, high: r.high, low: r.low, close: r.close }))
} catch {
next[s] = []
}
}
if (!cancelled) setHistory(next)
})()
return () => { cancelled = true }
}, [symbols.join(',')])
// Apply incoming SSE events to lastBars and extend history when a new minute closes
useEffect(() => {
const updated: Record<string, Bar | undefined> = {}
const newHistory: Record<string, Bar[]> = { ...history }
for (const [sym, evt] of Object.entries(lastBySymbol)) {
if (!evt) continue
const t = Math.floor(Date.parse(evt.close_time) / 1000)
const bar: Bar = { time: t, open: evt.open, high: evt.high, low: evt.low, close: evt.close }
updated[sym] = bar
// Append or update last element of history if new minute
const arr = newHistory[sym] || []
if (arr.length && arr[arr.length - 1].time === bar.time) {
arr[arr.length - 1] = bar
} else if (!arr.length || arr[arr.length - 1].time < bar.time) {
arr.push(bar)
if (arr.length > 2000) arr.shift()
}
newHistory[sym] = arr
}
if (Object.keys(updated).length) {
setLastBars(prev => ({ ...prev, ...updated }))
setHistory(newHistory)
}
}, [lastBySymbol])
const onWatchlistChange = useCallback((s: string[]) => setSymbols(s), [setSymbols])
return (
<div style={{ display: 'grid', gap: 16 }}>
<WatchlistPanel onChange={onWatchlistChange} />
<div className="card" style={{ padding: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span className="title">Live SSE Watchlist</span>
{isConnected ? <span style={{ color: '#10b981' }}> Connected</span> : <span style={{ color: '#ef4444' }}> Disconnected</span>}
</div>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr', }}>
{symbols.map(sym => (
<SimpleKlineChart
key={sym}
title={`${sym} — 1m (SSE)`}
data={(history[sym] || []).map(b => ({ time: b.time as any, open: b.open, high: b.high, low: b.low, close: b.close }))}
lastBar={lastBars[sym] ? { ...lastBars[sym], time: lastBars[sym]!.time as any } : undefined}
/>
))}
</div>
</div>
</div>
)
}
+243
View File
@@ -0,0 +1,243 @@
import { useEffect, useState } from 'react';
import { Newspaper, TrendingUp, TrendingDown, Minus, ExternalLink, RefreshCw } from 'lucide-react';
import type { NewsFeed as NewsFeedType, NewsArticle, Sentiment } from '@/types';
import { newsApi } from '@/services/api';
export default function NewsFeed() {
const [newsFeed, setNewsFeed] = useState<NewsFeedType | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState<'ALL' | Sentiment>('ALL');
const [autoRefresh, setAutoRefresh] = useState(false);
const loadNews = async () => {
try {
setIsLoading(true);
const data = await newsApi.getNewsFeed(50);
setNewsFeed(data);
} catch (error) {
console.error('Error loading news:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadNews();
}, []);
useEffect(() => {
if (autoRefresh) {
const interval = setInterval(loadNews, 300000); // Refresh every 5 minutes
return () => clearInterval(interval);
}
}, [autoRefresh]);
const getSentimentIcon = (sentiment: Sentiment) => {
switch (sentiment) {
case 'POSITIVE':
return <TrendingUp className="w-4 h-4 text-green-500" />;
case 'NEGATIVE':
return <TrendingDown className="w-4 h-4 text-red-500" />;
case 'NEUTRAL':
return <Minus className="w-4 h-4 text-gray-400" />;
}
};
const getSentimentColor = (sentiment: Sentiment) => {
switch (sentiment) {
case 'POSITIVE':
return 'border-l-green-500 bg-green-500/5';
case 'NEGATIVE':
return 'border-l-red-500 bg-red-500/5';
case 'NEUTRAL':
return 'border-l-gray-500 bg-gray-500/5';
}
};
const getImpactBadge = (impact: string) => {
const colors = {
HIGH: 'bg-red-500/20 text-red-500',
MEDIUM: 'bg-yellow-500/20 text-yellow-500',
LOW: 'bg-blue-500/20 text-blue-500',
};
return colors[impact as keyof typeof colors] || colors.LOW;
};
const filteredArticles = newsFeed?.articles.filter(
(article) => filter === 'ALL' || article.sentiment === filter
) || [];
return (
<div className="card">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Newspaper className="w-6 h-6 text-blue-500" />
<div>
<h3 className="text-lg font-semibold">Market News</h3>
{newsFeed && (
<p className="text-xs text-gray-400">
{newsFeed.total_count} articles Overall: {newsFeed.overall_sentiment}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={loadNews}
disabled={isLoading}
className="p-2 rounded-md hover:bg-dark-hover transition-colors"
title="Refresh news"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="rounded"
/>
<span className="text-gray-400">Auto</span>
</label>
</div>
</div>
{newsFeed && (
<div className="mb-4">
<div className="grid grid-cols-4 gap-2 text-sm">
<button
onClick={() => setFilter('ALL')}
className={`px-3 py-2 rounded-md transition-colors ${
filter === 'ALL' ? 'bg-blue-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
All ({newsFeed.total_count})
</button>
<button
onClick={() => setFilter('POSITIVE')}
className={`px-3 py-2 rounded-md transition-colors ${
filter === 'POSITIVE' ? 'bg-green-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Bullish ({newsFeed.bullish_count})
</button>
<button
onClick={() => setFilter('NEGATIVE')}
className={`px-3 py-2 rounded-md transition-colors ${
filter === 'NEGATIVE' ? 'bg-red-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Bearish ({newsFeed.bearish_count})
</button>
<button
onClick={() => setFilter('NEUTRAL')}
className={`px-3 py-2 rounded-md transition-colors ${
filter === 'NEUTRAL' ? 'bg-gray-600' : 'bg-dark-bg hover:bg-dark-hover'
}`}
>
Neutral ({newsFeed.neutral_count})
</button>
</div>
</div>
)}
<div className="space-y-3 max-h-96 overflow-y-auto">
{isLoading && !newsFeed ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
) : filteredArticles.length > 0 ? (
filteredArticles.map((article) => (
<NewsArticleCard key={article.id} article={article} />
))
) : (
<p className="text-center text-gray-400 py-8">No news articles found</p>
)}
</div>
</div>
);
}
function NewsArticleCard({ article }: { article: NewsArticle }) {
const getSentimentColor = (sentiment: Sentiment) => {
switch (sentiment) {
case 'POSITIVE':
return 'border-l-green-500 bg-green-500/5';
case 'NEGATIVE':
return 'border-l-red-500 bg-red-500/5';
case 'NEUTRAL':
return 'border-l-gray-500 bg-gray-500/5';
}
};
const getSentimentIcon = (sentiment: Sentiment) => {
switch (sentiment) {
case 'POSITIVE':
return <TrendingUp className="w-4 h-4 text-green-500" />;
case 'NEGATIVE':
return <TrendingDown className="w-4 h-4 text-red-500" />;
case 'NEUTRAL':
return <Minus className="w-4 h-4 text-gray-400" />;
}
};
const getImpactBadge = (impact: string) => {
const colors = {
HIGH: 'bg-red-500/20 text-red-500',
MEDIUM: 'bg-yellow-500/20 text-yellow-500',
LOW: 'bg-blue-500/20 text-blue-500',
};
return colors[impact as keyof typeof colors] || colors.LOW;
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
if (diffMins < 60) {
return `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else {
return date.toLocaleDateString();
}
};
return (
<div
className={`p-3 rounded-md border-l-4 ${getSentimentColor(
article.sentiment
)} hover:bg-dark-hover transition-colors cursor-pointer`}
onClick={() => window.open(article.url, '_blank')}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{getSentimentIcon(article.sentiment)}
<span className="text-xs font-medium text-gray-500">{article.source}</span>
<span className={`text-xs px-2 py-0.5 rounded ${getImpactBadge(article.impact_on_gold)}`}>
{article.impact_on_gold}
</span>
<span className="text-xs text-gray-500">{formatDate(article.published_at)}</span>
</div>
<h4 className="font-medium text-sm mb-1 line-clamp-2">{article.title}</h4>
{article.description && (
<p className="text-xs text-gray-400 line-clamp-2">{article.description}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className="text-xs px-2 py-0.5 rounded bg-dark-bg text-gray-400">
{article.category}
</span>
<span className="text-xs text-gray-500">
Relevance: {(article.relevance_score * 100).toFixed(0)}%
</span>
</div>
</div>
<ExternalLink className="w-4 h-4 text-gray-500 flex-shrink-0" />
</div>
</div>
);
}
@@ -0,0 +1,140 @@
import { TrendingUp, TrendingDown, DollarSign, Activity } from 'lucide-react';
import type { Portfolio } from '@/types';
import { formatPrice, formatPercent, formatNumber } from '@/utils/indicators';
interface PortfolioTrackerProps {
portfolio: Portfolio;
}
export default function PortfolioTracker({ portfolio }: PortfolioTrackerProps) {
const isProfitable = portfolio.totalPnl >= 0;
return (
<div className="card">
<h3 className="text-lg font-semibold mb-4">Portfolio</h3>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<DollarSign className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Total Value</span>
</div>
<p className="text-2xl font-bold">{formatPrice(portfolio.totalValue)}</p>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
{isProfitable ? (
<TrendingUp className="w-4 h-4 text-green-500" />
) : (
<TrendingDown className="w-4 h-4 text-red-500" />
)}
<span className="text-sm text-gray-400">Total P&L</span>
</div>
<p
className={`text-2xl font-bold ${
isProfitable ? 'text-green-500' : 'text-red-500'
}`}
>
{formatPrice(portfolio.totalPnl)}
</p>
<p
className={`text-sm ${isProfitable ? 'text-green-500' : 'text-red-500'}`}
>
{formatPercent(portfolio.totalPnlPercent)}
</p>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<DollarSign className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Cash</span>
</div>
<p className="text-xl font-semibold">{formatPrice(portfolio.cash)}</p>
</div>
<div className="bg-dark-bg rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Trades</span>
</div>
<p className="text-xl font-semibold">{portfolio.trades.length}</p>
</div>
</div>
{portfolio.position && (
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<h4 className="font-semibold mb-3">Current Position</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Symbol</span>
<span className="font-medium">{portfolio.position.symbol}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Quantity</span>
<span className="font-medium">{formatNumber(portfolio.position.quantity)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Avg Price</span>
<span className="font-medium">{formatPrice(portfolio.position.avgPrice)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Current Price</span>
<span className="font-medium">{formatPrice(portfolio.position.currentPrice)}</span>
</div>
<div className="flex justify-between border-t border-dark-border pt-2">
<span className="text-gray-400">Unrealized P&L</span>
<span
className={`font-semibold ${
portfolio.position.unrealizedPnl >= 0 ? 'text-green-500' : 'text-red-500'
}`}
>
{formatPrice(portfolio.position.unrealizedPnl)} (
{formatPercent(portfolio.position.unrealizedPnlPercent)})
</span>
</div>
</div>
</div>
)}
{portfolio.trades.length > 0 && (
<div>
<h4 className="font-semibold mb-3">Recent Trades</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{portfolio.trades.slice(-5).reverse().map((trade) => (
<div
key={trade.id}
className="bg-dark-bg rounded-lg p-3 text-sm flex justify-between items-center"
>
<div>
<span
className={`font-semibold ${
trade.action === 'BUY' ? 'text-green-500' : 'text-red-500'
}`}
>
{trade.action}
</span>
<span className="text-gray-400 ml-2">
{formatNumber(trade.quantity)} @ {formatPrice(trade.price)}
</span>
</div>
<div className="text-right">
<p className="font-medium">{formatPrice(trade.total)}</p>
{trade.pnl !== undefined && (
<p
className={`text-xs ${
trade.pnl >= 0 ? 'text-green-500' : 'text-red-500'
}`}
>
{formatPrice(trade.pnl)}
</p>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react'
import { promptsApi } from '@/services/api'
export default function PromptTemplatesPanel() {
const [list, setList] = useState<any[]>([])
const [active, setActive] = useState<any | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const rows = await promptsApi.list()
if (mounted) setList(rows)
if (rows?.[0]) setActive(await promptsApi.get(rows[0].name))
} catch (e: any) {
setError(e?.message || 'Failed to load prompt templates')
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
const load = async (name: string) => {
try { setActive(await promptsApi.get(name)) } catch {}
}
return (
<div className="card" style={{ display: 'grid', gap: 12 }}>
<div className="title">Prompt Templates</div>
{loading && <div className="text-gray-400">Loading</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div style={{ display: 'grid', gap: 12, gridTemplateColumns: '1fr 2fr' }}>
<div className="card">
<div className="text-gray-300 font-semibold mb-2">Templates</div>
<ul className="space-y-1">
{list.map(t => (
<li key={t.name}>
<button className="btn bg-dark-bg w-full text-left" onClick={() => load(t.name)}>
<div className="font-medium">{t.name}</div>
<div className="text-gray-400 text-xs">{t.description}</div>
</button>
</li>
))}
</ul>
</div>
<div className="card">
{active ? (
<div>
<div className="text-gray-300 font-semibold">{active.name}</div>
<div className="text-gray-400 text-sm mb-2">Vars: {active.variables?.join(', ') || '-'}</div>
<pre className="whitespace-pre-wrap text-gray-100 text-sm bg-dark-bg p-3 rounded border border-dark-border">{active.body}</pre>
</div>
) : (
<div className="text-gray-400">Select a template</div>
)}
</div>
</div>
)}
</div>
)
}
+232
View File
@@ -0,0 +1,232 @@
import { useState, useEffect } from 'react';
import { Shield, Calculator, TrendingUp, AlertTriangle } from 'lucide-react';
import { calculatePositionSize, formatPrice } from '@/utils/indicators';
interface RiskManagementProps {
currentPrice: number;
cash: number;
position: any;
trades: any[];
onSetStopLoss?: (price: number) => void;
onSetTakeProfit?: (price: number) => void;
}
export default function RiskManagement({
currentPrice,
cash,
position: _position,
trades,
onSetStopLoss,
onSetTakeProfit,
}: RiskManagementProps) {
const [riskPercent, setRiskPercent] = useState<number>(2);
const [stopLossPercent, setStopLossPercent] = useState<number>(2);
const [takeProfitPercent, setTakeProfitPercent] = useState<number>(4);
const [positionSize, setPositionSize] = useState<number>(0);
const [stopLossPrice, setStopLossPrice] = useState<number>(0);
const [takeProfitPrice, setTakeProfitPrice] = useState<number>(0);
// Calculate position size based on risk
useEffect(() => {
const riskAmount = cash * (riskPercent / 100);
const stopLossDiff = currentPrice * (stopLossPercent / 100);
const maxQuantity = riskAmount / stopLossDiff;
setPositionSize(Number(maxQuantity.toFixed(4)));
setStopLossPrice(Number((currentPrice * (1 - stopLossPercent / 100)).toFixed(2)));
setTakeProfitPrice(Number((currentPrice * (1 + takeProfitPercent / 100)).toFixed(2)));
}, [currentPrice, cash, riskPercent, stopLossPercent, takeProfitPercent]);
// Calculate Kelly Criterion recommendation
const getKellyRecommendation = () => {
if (trades.length < 10) {
return { size: 0, message: 'Need at least 10 trades for Kelly calculation' };
}
const completedTrades = trades.filter((t: any) => t.pnl !== undefined);
const winningTrades = completedTrades.filter((t: any) => t.pnl > 0);
const losingTrades = completedTrades.filter((t: any) => t.pnl <= 0);
if (winningTrades.length === 0 || losingTrades.length === 0) {
return { size: 0, message: 'Need both winning and losing trades' };
}
const winRate = winningTrades.length / completedTrades.length;
const avgWin = winningTrades.reduce((sum: number, t: any) => sum + t.pnl, 0) / winningTrades.length;
const avgLoss = Math.abs(losingTrades.reduce((sum: number, t: any) => sum + t.pnl, 0) / losingTrades.length);
const kellySize = calculatePositionSize(cash, winRate, avgWin, avgLoss);
return {
size: kellySize,
message: `Kelly suggests ${formatPrice(kellySize)} position size`,
};
};
const kellyRec = getKellyRecommendation();
const maxLoss = positionSize * (currentPrice * (stopLossPercent / 100));
const maxProfit = positionSize * (currentPrice * (takeProfitPercent / 100));
const riskRewardRatio = takeProfitPercent / stopLossPercent;
return (
<div className="card">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Shield className="w-5 h-5 text-blue-500" />
Risk Management
</h3>
{/* Risk Parameters */}
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">
Risk per Trade (% of Capital)
</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0.5"
max="5"
step="0.5"
value={riskPercent}
onChange={(e) => setRiskPercent(Number(e.target.value))}
className="flex-1"
/>
<span className="text-sm font-medium w-12">{riskPercent}%</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Max risk: {formatPrice(cash * (riskPercent / 100))}
</p>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Stop Loss (%)</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0.5"
max="10"
step="0.5"
value={stopLossPercent}
onChange={(e) => setStopLossPercent(Number(e.target.value))}
className="flex-1"
/>
<span className="text-sm font-medium w-12">{stopLossPercent}%</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Stop at: {formatPrice(stopLossPrice)}
</p>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Take Profit (%)</label>
<div className="flex items-center gap-2">
<input
type="range"
min="1"
max="20"
step="0.5"
value={takeProfitPercent}
onChange={(e) => setTakeProfitPercent(Number(e.target.value))}
className="flex-1"
/>
<span className="text-sm font-medium w-12">{takeProfitPercent}%</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Target: {formatPrice(takeProfitPrice)}
</p>
</div>
</div>
{/* Recommended Position Size */}
<div className="mt-4 p-4 bg-dark-bg rounded-lg border border-dark-border">
<div className="flex items-center gap-2 mb-3">
<Calculator className="w-4 h-4 text-blue-500" />
<h4 className="font-semibold">Recommended Position</h4>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Position Size:</span>
<span className="font-bold">{positionSize.toFixed(4)} oz</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Cost:</span>
<span className="font-medium">{formatPrice(positionSize * currentPrice)}</span>
</div>
<div className="flex justify-between border-t border-dark-border pt-2">
<span className="text-gray-400">Max Loss:</span>
<span className="font-semibold text-red-500">-{formatPrice(maxLoss)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Max Profit:</span>
<span className="font-semibold text-green-500">+{formatPrice(maxProfit)}</span>
</div>
<div className="flex justify-between border-t border-dark-border pt-2">
<span className="text-gray-400">R:R Ratio:</span>
<span
className={`font-semibold ${
riskRewardRatio >= 2 ? 'text-green-500' : 'text-yellow-500'
}`}
>
1:{riskRewardRatio.toFixed(2)}
</span>
</div>
</div>
</div>
{/* Kelly Criterion */}
{trades.length >= 10 && (
<div className="mt-4 p-4 bg-blue-500/10 rounded-lg border border-blue-500/30">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-4 h-4 text-blue-500" />
<h4 className="font-semibold text-sm">Kelly Criterion</h4>
</div>
<p className="text-xs text-gray-300 mb-2">{kellyRec.message}</p>
{kellyRec.size > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-400">Suggested oz:</span>
<span className="font-medium">
{(kellyRec.size / currentPrice).toFixed(4)} oz
</span>
</div>
)}
</div>
)}
{/* Risk Warning */}
<div className="mt-4 p-3 bg-yellow-500/10 rounded-lg border border-yellow-500/30">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-yellow-500 flex-shrink-0 mt-0.5" />
<div className="text-xs text-gray-300">
<p className="font-medium text-yellow-500 mb-1">Risk Guidelines:</p>
<ul className="space-y-1 list-disc list-inside">
<li>Never risk more than 2% per trade</li>
<li>Maintain R:R ratio of at least 1:2</li>
<li>Always use stop losses</li>
<li>Consider Kelly Criterion for sizing</li>
</ul>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="mt-4 space-y-2">
<button
onClick={() => onSetStopLoss?.(stopLossPrice)}
className="btn w-full bg-red-600 hover:bg-red-700 text-white flex items-center justify-center gap-2"
>
<Shield className="w-4 h-4" />
Set Stop Loss ({formatPrice(stopLossPrice)})
</button>
<button
onClick={() => onSetTakeProfit?.(takeProfitPrice)}
className="btn w-full bg-green-600 hover:bg-green-700 text-white flex items-center justify-center gap-2"
>
<TrendingUp className="w-4 h-4" />
Set Take Profit ({formatPrice(takeProfitPrice)})
</button>
</div>
</div>
);
}
+85
View File
@@ -0,0 +1,85 @@
import { useEffect, useState } from 'react'
import { settingsApi } from '@/services/api'
export default function SettingsPanel() {
const [models, setModels] = useState<any>({})
const [exchanges, setExchanges] = useState<any>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const [m, e] = await Promise.all([settingsApi.getModels(), settingsApi.getExchanges()])
if (mounted) { setModels(m); setExchanges(e) }
} catch (err: any) {
setError(err?.message || 'Failed to load settings')
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
const saveModels = async () => {
setSaving(true)
try { setModels(await settingsApi.putModels(models)) } finally { setSaving(false) }
}
const saveExchanges = async () => {
setSaving(true)
try { setExchanges(await settingsApi.putExchanges(exchanges)) } finally { setSaving(false) }
}
return (
<div className="card" style={{ display: 'grid', gap: 12 }}>
<div className="title">Settings</div>
{loading && <div className="text-gray-400">Loading</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div style={{ display: 'grid', gap: 16 }}>
<section className="card">
<div className="title">Models</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<LabeledInput label="Default Model" value={models.default_model || ''} onChange={v => setModels({ ...models, default_model: v })} />
<LabeledInput label="Temperature" type="number" value={models.temperature ?? 0.3} onChange={v => setModels({ ...models, temperature: Number(v) })} />
<LabeledInput label="Max Tokens" type="number" value={models.max_tokens ?? 800} onChange={v => setModels({ ...models, max_tokens: Number(v) })} />
</div>
<div className="mt-3">
<button className="btn-primary" onClick={saveModels} disabled={saving}>{saving ? 'Saving…' : 'Save Models'}</button>
</div>
</section>
<section className="card">
<div className="title">Exchanges</div>
<Toggle label="Binance Enabled" checked={exchanges?.binance?.enabled ?? true} onChange={v => setExchanges({ ...exchanges, binance: { ...(exchanges.binance || {}), enabled: v } })} />
<Toggle label="Alpha Vantage Enabled" checked={exchanges?.alpha_vantage?.enabled ?? true} onChange={v => setExchanges({ ...exchanges, alpha_vantage: { ...(exchanges.alpha_vantage || {}), enabled: v } })} />
<div className="mt-3">
<button className="btn-primary" onClick={saveExchanges} disabled={saving}>{saving ? 'Saving…' : 'Save Exchanges'}</button>
</div>
</section>
</div>
)}
</div>
)
}
function LabeledInput({ label, value, onChange, type = 'text' }: { label: string; value: any; onChange: (v: string) => void; type?: string }) {
return (
<label className="flex flex-col gap-1">
<span className="text-gray-300 text-sm">{label}</span>
<input className="input" type={type} value={value} onChange={e => onChange(e.target.value)} />
</label>
)
}
function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
return (
<label className="flex items-center gap-2">
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} />
<span className="text-gray-300 text-sm">{label}</span>
</label>
)
}
@@ -0,0 +1,51 @@
import { useEffect, useRef } from 'react'
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts'
type Bar = { time: any; open: number; high: number; low: number; close: number }
export default function SimpleKlineChart({ title, data, lastBar }: { title: string; data: Bar[]; lastBar?: Bar }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const chartRef = useRef<IChartApi | null>(null)
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
useEffect(() => {
if (!containerRef.current) return
const chart = createChart(containerRef.current, {
layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
rightPriceScale: { borderColor: '#1f2937' },
timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
height: 280,
width: containerRef.current.clientWidth,
})
chartRef.current = chart
const series = chart.addCandlestickSeries({
upColor: '#16a34a', downColor: '#dc2626', borderUpColor: '#16a34a', borderDownColor: '#dc2626', wickUpColor: '#16a34a', wickDownColor: '#dc2626',
})
seriesRef.current = series
const onResize = () => {
if (!containerRef.current || !chartRef.current) return
chartRef.current.applyOptions({ width: containerRef.current.clientWidth })
}
window.addEventListener('resize', onResize)
return () => { window.removeEventListener('resize', onResize); chart.remove() }
}, [])
useEffect(() => {
if (!seriesRef.current) return
seriesRef.current.setData(data)
}, [data])
useEffect(() => {
if (!seriesRef.current || !lastBar) return
seriesRef.current.update(lastBar)
}, [lastBar])
return (
<div className="card">
<div className="title">{title}</div>
<div ref={containerRef} className="chart" />
</div>
)
}
@@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react'
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts'
type Point = { time: number; value: number }
export default function SimpleLineChart({ title, data }: { title: string; data: Point[] }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const chartRef = useRef<IChartApi | null>(null)
const seriesRef = useRef<ISeriesApi<'Line'> | null>(null)
useEffect(() => {
if (!containerRef.current) return
const chart = createChart(containerRef.current, {
layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
rightPriceScale: { borderColor: '#1f2937' },
timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
height: 280,
width: containerRef.current.clientWidth,
})
chartRef.current = chart
const series = chart.addLineSeries({ color: '#3b82f6', lineWidth: 2 })
seriesRef.current = series
const onResize = () => {
if (!containerRef.current || !chartRef.current) return
chartRef.current.applyOptions({ width: containerRef.current.clientWidth })
}
window.addEventListener('resize', onResize)
return () => { window.removeEventListener('resize', onResize); chart.remove() }
}, [])
useEffect(() => {
if (!seriesRef.current) return
seriesRef.current.setData(data.map(d => ({ time: d.time as any, value: d.value })))
}, [data])
return (
<div className="card">
<div className="title">{title}</div>
<div ref={containerRef} className="chart" />
</div>
)
}
@@ -0,0 +1,51 @@
import React from 'react'
const SYMBOLS = [
{ label: 'BTC/USDT (Binance)', value: 'BTCUSDT' },
{ label: 'ETH/USDT (Binance)', value: 'ETHUSDT' },
{ label: 'XAU/USD (Gold)', value: 'XAUUSD' },
]
const TIMEFRAMES = [
{ label: '1m', value: '1m' },
{ label: '5m', value: '5m' },
{ label: '1h', value: '1h' },
{ label: '4h', value: '4h' },
]
export default function SymbolTimeframeSelector({
symbol,
timeframe,
onChangeSymbol,
onChangeTimeframe,
}: {
symbol: string
timeframe: string
onChangeSymbol: (v: string) => void
onChangeTimeframe: (v: string) => void
}) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<select
className="input"
value={symbol}
onChange={(e) => onChangeSymbol(e.target.value)}
aria-label="Symbol"
>
{SYMBOLS.map((s) => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
<select
className="input"
value={timeframe}
onChange={(e) => onChangeTimeframe(e.target.value)}
aria-label="Timeframe"
>
{TIMEFRAMES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</div>
)
}
+296
View File
@@ -0,0 +1,296 @@
import { useState, ReactNode } from 'react';
import { X, Maximize2, Minimize2, Pin, PinOff } from 'lucide-react';
import ComponentSettings from './ComponentSettings';
import type { TabConfig, LayoutMode, TabCustomization } from '@/types';
interface TabPanel {
config: TabConfig;
content: ReactNode;
}
interface TabbedContainerProps {
panels: TabPanel[];
mode: LayoutMode;
onTabClose?: (tabId: string) => void;
onTabPin?: (tabId: string) => void;
onCustomizationUpdate?: (tabId: string, customization: TabCustomization) => void;
className?: string;
}
export default function TabbedContainer({
panels,
mode,
onTabClose,
onTabPin,
onCustomizationUpdate,
className = '',
}: TabbedContainerProps) {
const [activeTabId, setActiveTabId] = useState<string>(
panels.find((p) => p.config.visible)?.config.id || panels[0]?.config.id
);
const [expandedTab, setExpandedTab] = useState<string | null>(null);
const visiblePanels = panels
.filter((p) => p.config.visible)
.sort((a, b) => a.config.order - b.config.order);
const activePanel = visiblePanels.find((p) => p.config.id === activeTabId);
const handleExpand = (tabId: string) => {
setExpandedTab(expandedTab === tabId ? null : tabId);
};
// Tabs mode - single panel with tab switcher
if (mode === 'tabs') {
return (
<div className={`flex flex-col h-full ${className}`}>
{/* Tab switcher */}
<div className="flex items-center gap-1 border-b border-gray-700 bg-dark-card px-2 overflow-x-auto">
{visiblePanels.map((panel) => (
<button
key={panel.config.id}
onClick={() => setActiveTabId(panel.config.id)}
className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${
activeTabId === panel.config.id
? 'border-gold-500 text-gold-500 bg-dark-hover'
: 'border-transparent text-gray-400 hover:text-white hover:bg-dark-hover'
}`}
>
{panel.config.pinned && <Pin className="w-3 h-3" />}
<span>{panel.config.label}</span>
{onTabClose && !panel.config.pinned && (
<button
onClick={(e) => {
e.stopPropagation();
onTabClose(panel.config.id);
}}
className="p-0.5 hover:bg-red-500/20 rounded"
>
<X className="w-3 h-3" />
</button>
)}
</button>
))}
</div>
{/* Active panel content */}
<div className="flex-1 overflow-hidden">
{activePanel && (
<div className="h-full">{activePanel.content}</div>
)}
</div>
</div>
);
}
// Split mode - two main sections
if (mode === 'split') {
const leftPanels = visiblePanels.filter((p) => p.config.position !== 'right');
const rightPanels = visiblePanels.filter((p) => p.config.position === 'right');
return (
<div className={`grid grid-cols-2 gap-6 h-full ${className}`}>
{/* Left section */}
<div className="space-y-6 overflow-y-auto">
{leftPanels.map((panel) => (
<PanelCard
key={panel.config.id}
panel={panel}
onExpand={handleExpand}
onPin={onTabPin}
onClose={onTabClose}
onCustomizationUpdate={onCustomizationUpdate}
isExpanded={expandedTab === panel.config.id}
/>
))}
</div>
{/* Right section */}
<div className="space-y-6 overflow-y-auto">
{rightPanels.map((panel) => (
<PanelCard
key={panel.config.id}
panel={panel}
onExpand={handleExpand}
onPin={onTabPin}
onClose={onTabClose}
onCustomizationUpdate={onCustomizationUpdate}
isExpanded={expandedTab === panel.config.id}
/>
))}
</div>
</div>
);
}
// Grid mode - responsive grid layout
const getSizeClass = (size: TabConfig['size']) => {
switch (size) {
case 'small':
return 'col-span-1';
case 'medium':
return 'col-span-1 lg:col-span-2';
case 'large':
return 'col-span-1 lg:col-span-3';
case 'full':
return 'col-span-full';
default:
return 'col-span-1';
}
};
return (
<div className={`grid grid-cols-1 lg:grid-cols-6 gap-6 ${className}`}>
{visiblePanels.map((panel) => (
<div
key={panel.config.id}
className={`${getSizeClass(panel.config.size)} ${
expandedTab === panel.config.id ? 'fixed inset-4 z-40' : ''
}`}
>
<PanelCard
panel={panel}
onExpand={handleExpand}
onPin={onTabPin}
onClose={onTabClose}
onCustomizationUpdate={onCustomizationUpdate}
isExpanded={expandedTab === panel.config.id}
/>
</div>
))}
</div>
);
}
// Individual panel card component
interface PanelCardProps {
panel: TabPanel;
onExpand: (tabId: string) => void;
onPin?: (tabId: string) => void;
onClose?: (tabId: string) => void;
onCustomizationUpdate?: (tabId: string, customization: TabCustomization) => void;
isExpanded: boolean;
}
function PanelCard({ panel, onExpand, onPin, onClose, onCustomizationUpdate, isExpanded }: PanelCardProps) {
const getAvailableSettings = (tabId: string) => {
// Define available settings per tab
switch (tabId) {
case 'news':
return {
autoRefresh: true,
refreshRate: true,
filters: {
sentiment: ['ALL', 'POSITIVE', 'NEGATIVE', 'NEUTRAL'],
impact: ['ALL', 'HIGH', 'MEDIUM', 'LOW'],
},
theme: true,
};
case 'alerts':
return {
autoRefresh: true,
refreshRate: true,
filters: {
severity: ['ALL', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW'],
type: ['ALL', 'PRICE_SPIKE', 'NEWS_BREAKING', 'SUPPORT_BREACH'],
},
};
case 'chart':
return {
displayMode: ['candlestick', 'line', 'area'],
theme: true,
};
case 'analytics':
return {
displayMode: ['detailed', 'compact', 'charts-only'],
theme: true,
};
case 'daily-checklist':
return {
autoRefresh: false,
displayMode: ['all-phases', 'current-phase-only'],
theme: true,
};
case 'trading-journal':
return {
displayMode: ['detailed', 'compact', 'list'],
filters: {
emotion: ['all', 'confident', 'neutral', 'anxious'],
result: ['all', 'winners', 'losers'],
},
};
case 'market-summary':
return {
autoRefresh: true,
refreshRate: true,
displayMode: ['overview', 'levels', 'events', 'ai'],
};
default:
return { theme: true };
}
};
return (
<div
className={`card flex flex-col group ${
isExpanded ? 'h-full' : 'h-auto'
}`}
>
{/* Panel header */}
<div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-700">
<h3 className="text-lg font-semibold flex items-center gap-2">
{panel.config.pinned && <Pin className="w-4 h-4 text-gold-500" />}
{panel.config.label}
</h3>
<div className="flex items-center gap-1">
{onCustomizationUpdate && (
<ComponentSettings
customization={panel.config.customization}
onUpdate={(customization) =>
onCustomizationUpdate(panel.config.id, customization)
}
availableSettings={getAvailableSettings(panel.config.id)}
/>
)}
{onPin && (
<button
onClick={() => onPin(panel.config.id)}
className="p-1.5 hover:bg-dark-hover rounded transition-colors"
title={panel.config.pinned ? 'Unpin' : 'Pin'}
>
{panel.config.pinned ? (
<PinOff className="w-4 h-4" />
) : (
<Pin className="w-4 h-4" />
)}
</button>
)}
<button
onClick={() => onExpand(panel.config.id)}
className="p-1.5 hover:bg-dark-hover rounded transition-colors"
title={isExpanded ? 'Minimize' : 'Expand'}
>
{isExpanded ? (
<Minimize2 className="w-4 h-4" />
) : (
<Maximize2 className="w-4 h-4" />
)}
</button>
{onClose && !panel.config.pinned && (
<button
onClick={() => onClose(panel.config.id)}
className="p-1.5 hover:bg-red-500/20 rounded transition-colors"
title="Close"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Panel content */}
<div className={`flex-1 ${isExpanded ? 'overflow-y-auto' : ''}`}>
{panel.content}
</div>
</div>
);
}
@@ -0,0 +1,54 @@
import { Clock } from 'lucide-react';
interface TimeframeSelectorProps {
selected: string;
onChange: (timeframe: string) => void;
disabled?: boolean;
}
export default function TimeframeSelector({
selected,
onChange,
disabled = false,
}: TimeframeSelectorProps) {
const timeframes = [
{ value: '1min', label: '1M', description: 'Scalping' },
{ value: '5min', label: '5M', description: 'Intraday' },
{ value: '15min', label: '15M', description: 'Intraday' },
{ value: '30min', label: '30M', description: 'Short-term' },
{ value: '60min', label: '1H', description: 'Hourly' },
{ value: '4hour', label: '4H', description: 'Swing' },
{ value: 'daily', label: '1D', description: 'Daily' },
{ value: 'weekly', label: '1W', description: 'Long-term' },
];
return (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">Timeframe:</span>
</div>
<div className="flex gap-1">
{timeframes.map((tf) => (
<button
key={tf.value}
onClick={() => onChange(tf.value)}
disabled={disabled}
className={`
px-3 py-1.5 text-sm font-medium rounded transition-colors
${
selected === tf.value
? 'bg-blue-600 text-white'
: 'bg-dark-bg text-gray-400 hover:bg-dark-hover hover:text-gray-200'
}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
title={tf.description}
>
{tf.label}
</button>
))}
</div>
</div>
);
}
+215
View File
@@ -0,0 +1,215 @@
import { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, RotateCcw, Sparkles } from 'lucide-react';
interface TradeControlsProps {
currentPrice: number;
cash: number;
onBuy: (quantity: number) => void;
onSell: (quantity: number) => void;
onReset: () => void;
onAIAnalysis: () => void;
hasPosition: boolean;
isAnalyzing?: boolean;
}
export default function TradeControls({
currentPrice,
cash,
onBuy,
onSell,
onReset,
onAIAnalysis,
hasPosition,
isAnalyzing = false,
}: TradeControlsProps) {
const [quantity, setQuantity] = useState<string>('1');
const [usdAmount, setUsdAmount] = useState<string>('');
// Initialize USD amount when currentPrice changes
useEffect(() => {
const qty = parseFloat(quantity);
if (!isNaN(qty) && qty > 0 && currentPrice > 0) {
setUsdAmount((qty * currentPrice).toFixed(2));
}
}, [currentPrice]);
const handleQuantityChange = (value: string) => {
// Allow empty string, numbers, and decimal points
if (value === '' || /^\d*\.?\d*$/.test(value)) {
setQuantity(value);
const num = parseFloat(value);
if (!isNaN(num) && num > 0) {
setUsdAmount((num * currentPrice).toFixed(2));
} else {
setUsdAmount('');
}
}
};
const handleUsdChange = (value: string) => {
// Allow empty string, numbers, and decimal points
if (value === '' || /^\d*\.?\d*$/.test(value)) {
setUsdAmount(value);
const num = parseFloat(value);
if (!isNaN(num) && num > 0 && currentPrice > 0) {
setQuantity((num / currentPrice).toFixed(4));
} else {
setQuantity('');
}
}
};
const handleBuy = () => {
const qty = parseFloat(quantity);
if (isNaN(qty) || qty <= 0) {
alert('Please enter a valid quantity greater than 0');
return;
}
const cost = qty * currentPrice;
if (cost > cash) {
alert(`Insufficient funds! You need $${cost.toFixed(2)} but only have $${cash.toFixed(2)}`);
return;
}
onBuy(qty);
setQuantity('1');
setUsdAmount((1 * currentPrice).toFixed(2));
};
const handleSell = () => {
const qty = parseFloat(quantity);
if (isNaN(qty) || qty <= 0) {
alert('Please enter a valid quantity greater than 0');
return;
}
if (!hasPosition) {
alert('You have no position to sell!');
return;
}
onSell(qty);
setQuantity('1');
setUsdAmount((1 * currentPrice).toFixed(2));
};
const maxBuyQuantity = currentPrice > 0 ? cash / currentPrice : 0;
const qty = parseFloat(quantity);
const totalCost = !isNaN(qty) && qty > 0 ? qty * currentPrice : 0;
const canBuy = !isNaN(qty) && qty > 0 && totalCost <= cash && currentPrice > 0;
return (
<div className="card">
<h3 className="text-lg font-semibold mb-4">Trade Controls</h3>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Current Price</label>
<div className="bg-dark-bg rounded-lg p-3 text-xl font-bold text-gold-500">
${currentPrice.toFixed(2)}
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Quantity (oz)</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={quantity}
onChange={(e) => handleQuantityChange(e.target.value)}
className="input flex-1"
placeholder="0.00"
/>
<button
onClick={() => handleQuantityChange(maxBuyQuantity.toFixed(4))}
className="px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded-md text-sm font-medium"
disabled={maxBuyQuantity <= 0}
>
Max
</button>
</div>
<div className="flex gap-1 mb-1">
<button
onClick={() => handleUsdChange((cash * 0.25).toFixed(2))}
className="text-xs px-2 py-1 bg-dark-bg hover:bg-dark-hover rounded"
>
25%
</button>
<button
onClick={() => handleUsdChange((cash * 0.5).toFixed(2))}
className="text-xs px-2 py-1 bg-dark-bg hover:bg-dark-hover rounded"
>
50%
</button>
<button
onClick={() => handleUsdChange((cash * 0.75).toFixed(2))}
className="text-xs px-2 py-1 bg-dark-bg hover:bg-dark-hover rounded"
>
75%
</button>
</div>
<p className="text-xs text-gray-500">
Max: {maxBuyQuantity.toFixed(4)} oz
</p>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">USD Amount</label>
<input
type="number"
value={usdAmount}
onChange={(e) => handleUsdChange(e.target.value)}
className="input w-full"
min="0"
step="0.01"
placeholder="0.00"
/>
<p className="text-xs text-gray-500 mt-1">
Total: ${totalCost.toFixed(2)}
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={handleBuy}
disabled={!canBuy}
className="btn-success flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
title={!canBuy ? (cash === 0 ? 'No funds available' : 'Invalid quantity or insufficient funds') : 'Buy gold (Ctrl+B)'}
>
<TrendingUp className="w-4 h-4" />
Buy
</button>
<button
onClick={handleSell}
disabled={!hasPosition || isNaN(qty) || qty <= 0}
className="btn-danger flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
title={!hasPosition ? 'No position to sell' : 'Sell gold (Ctrl+S)'}
>
<TrendingDown className="w-4 h-4" />
Sell
</button>
</div>
<div className="border-t border-dark-border pt-4 space-y-3">
<button
onClick={onAIAnalysis}
disabled={isAnalyzing}
className="btn-primary w-full flex items-center justify-center gap-2 disabled:opacity-50"
>
<Sparkles className="w-4 h-4" />
{isAnalyzing ? 'Analyzing...' : 'AI Analysis'}
</button>
<button
onClick={onReset}
className="btn w-full bg-gray-700 hover:bg-gray-600 text-white flex items-center justify-center gap-2"
>
<RotateCcw className="w-4 h-4" />
Reset Simulation
</button>
</div>
</div>
</div>
);
}
+458
View File
@@ -0,0 +1,458 @@
import { useState, useEffect } from 'react';
import { BookOpen, Plus, Calendar, TrendingUp, TrendingDown, Smile, Meh, Frown, Search, Filter } from 'lucide-react';
interface JournalEntry {
id: string;
date: string;
timestamp: number;
tradeId?: string;
action: 'BUY' | 'SELL';
price: number;
quantity: number;
pnl?: number;
setupQuality: 1 | 2 | 3 | 4 | 5;
emotionalState: 'confident' | 'neutral' | 'anxious' | 'fearful' | 'greedy';
planFollowed: boolean;
entryReason: string;
exitReason?: string;
marketConditions: string;
lessonsLearned: string;
tags: string[];
}
interface TradingJournalProps {
onAddEntry?: (entry: JournalEntry) => void;
}
export default function TradingJournal({ onAddEntry }: TradingJournalProps) {
const [entries, setEntries] = useState<JournalEntry[]>(() => {
const stored = localStorage.getItem('trading-journal');
if (stored) {
try {
return JSON.parse(stored);
} catch {
return [];
}
}
return [];
});
const [isAddingEntry, setIsAddingEntry] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [filterEmotion, setFilterEmotion] = useState<string>('all');
const [filterPnL, setFilterPnL] = useState<'all' | 'winners' | 'losers'>('all');
const [newEntry, setNewEntry] = useState<Partial<JournalEntry>>({
action: 'BUY',
price: 0,
quantity: 0,
setupQuality: 3,
emotionalState: 'neutral',
planFollowed: true,
entryReason: '',
marketConditions: '',
lessonsLearned: '',
tags: [],
});
useEffect(() => {
localStorage.setItem('trading-journal', JSON.stringify(entries));
}, [entries]);
const handleAddEntry = () => {
const entry: JournalEntry = {
id: Date.now().toString(),
date: new Date().toDateString(),
timestamp: Date.now(),
...newEntry as any,
};
setEntries(prev => [entry, ...prev]);
if (onAddEntry) {
onAddEntry(entry);
}
// Reset form
setNewEntry({
action: 'BUY',
price: 0,
quantity: 0,
setupQuality: 3,
emotionalState: 'neutral',
planFollowed: true,
entryReason: '',
marketConditions: '',
lessonsLearned: '',
tags: [],
});
setIsAddingEntry(false);
};
const filteredEntries = entries.filter(entry => {
// Search filter
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
const matchesSearch =
entry.entryReason.toLowerCase().includes(searchLower) ||
entry.exitReason?.toLowerCase().includes(searchLower) ||
entry.marketConditions.toLowerCase().includes(searchLower) ||
entry.lessonsLearned.toLowerCase().includes(searchLower) ||
entry.tags.some(tag => tag.toLowerCase().includes(searchLower));
if (!matchesSearch) return false;
}
// Emotion filter
if (filterEmotion !== 'all' && entry.emotionalState !== filterEmotion) {
return false;
}
// P&L filter
if (filterPnL === 'winners' && (!entry.pnl || entry.pnl <= 0)) {
return false;
}
if (filterPnL === 'losers' && (!entry.pnl || entry.pnl >= 0)) {
return false;
}
return true;
});
const getEmotionIcon = (emotion: string) => {
switch (emotion) {
case 'confident':
case 'greedy':
return <Smile className="w-5 h-5 text-green-500" />;
case 'neutral':
return <Meh className="w-5 h-5 text-gray-400" />;
case 'anxious':
case 'fearful':
return <Frown className="w-5 h-5 text-red-500" />;
default:
return <Meh className="w-5 h-5 text-gray-400" />;
}
};
const stats = {
totalEntries: entries.length,
avgSetupQuality: entries.length > 0
? (entries.reduce((sum, e) => sum + e.setupQuality, 0) / entries.length).toFixed(1)
: '0',
planFollowedPercent: entries.length > 0
? ((entries.filter(e => e.planFollowed).length / entries.length) * 100).toFixed(0)
: '0',
};
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="mb-4 pb-4 border-b border-gray-700">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<BookOpen className="w-6 h-6 text-gold-500" />
<div>
<h3 className="text-lg font-semibold">Trading Journal</h3>
<p className="text-xs text-gray-400">{stats.totalEntries} entries recorded</p>
</div>
</div>
<button
onClick={() => setIsAddingEntry(!isAddingEntry)}
className="btn-primary text-sm flex items-center gap-2"
>
<Plus className="w-4 h-4" />
New Entry
</button>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-3 gap-3">
<div className="p-2 bg-dark-hover rounded text-center">
<div className="text-xs text-gray-400">Entries</div>
<div className="text-lg font-bold text-gold-500">{stats.totalEntries}</div>
</div>
<div className="p-2 bg-dark-hover rounded text-center">
<div className="text-xs text-gray-400">Avg Setup</div>
<div className="text-lg font-bold text-blue-500">{stats.avgSetupQuality}/5</div>
</div>
<div className="p-2 bg-dark-hover rounded text-center">
<div className="text-xs text-gray-400">Plan Followed</div>
<div className="text-lg font-bold text-green-500">{stats.planFollowedPercent}%</div>
</div>
</div>
</div>
{/* Add Entry Form */}
{isAddingEntry && (
<div className="mb-4 p-4 bg-dark-hover rounded-lg border border-gray-700">
<h4 className="font-semibold mb-3">New Journal Entry</h4>
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<label className="block text-xs text-gray-400 mb-1">Action</label>
<select
value={newEntry.action}
onChange={(e) => setNewEntry({ ...newEntry, action: e.target.value as any })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
>
<option value="BUY">BUY</option>
<option value="SELL">SELL</option>
</select>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Price</label>
<input
type="number"
step="0.01"
value={newEntry.price}
onChange={(e) => setNewEntry({ ...newEntry, price: Number(e.target.value) })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<label className="block text-xs text-gray-400 mb-1">Setup Quality (1-5)</label>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map(rating => (
<button
key={rating}
onClick={() => setNewEntry({ ...newEntry, setupQuality: rating as any })}
className={`flex-1 py-2 rounded border-2 transition-colors ${
(newEntry.setupQuality || 0) >= rating
? 'border-gold-500 bg-gold-500/20 text-gold-500'
: 'border-gray-700 text-gray-500'
}`}
>
</button>
))}
</div>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Emotional State</label>
<select
value={newEntry.emotionalState}
onChange={(e) => setNewEntry({ ...newEntry, emotionalState: e.target.value as any })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
>
<option value="confident">Confident</option>
<option value="neutral">Neutral</option>
<option value="anxious">Anxious</option>
<option value="fearful">Fearful</option>
<option value="greedy">Greedy</option>
</select>
</div>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-400 mb-1">Entry Reason</label>
<textarea
value={newEntry.entryReason}
onChange={(e) => setNewEntry({ ...newEntry, entryReason: e.target.value })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
rows={2}
placeholder="Why did you enter this trade?"
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-400 mb-1">Market Conditions</label>
<textarea
value={newEntry.marketConditions}
onChange={(e) => setNewEntry({ ...newEntry, marketConditions: e.target.value })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
rows={2}
placeholder="Describe market conditions..."
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-400 mb-1">Lessons Learned</label>
<textarea
value={newEntry.lessonsLearned}
onChange={(e) => setNewEntry({ ...newEntry, lessonsLearned: e.target.value })}
className="w-full px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
rows={2}
placeholder="What did you learn?"
/>
</div>
<div className="mb-3">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={newEntry.planFollowed}
onChange={(e) => setNewEntry({ ...newEntry, planFollowed: e.target.checked })}
className="rounded"
/>
<span>I followed my trading plan</span>
</label>
</div>
<div className="flex gap-2">
<button onClick={handleAddEntry} className="btn-primary text-sm flex-1">
Save Entry
</button>
<button
onClick={() => setIsAddingEntry(false)}
className="btn-secondary text-sm"
>
Cancel
</button>
</div>
</div>
)}
{/* Filters */}
<div className="mb-4 space-y-2">
<div className="flex gap-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search entries..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-3 py-2 bg-dark-bg border border-gray-700 rounded-lg text-sm"
/>
</div>
<button className="p-2 bg-dark-bg border border-gray-700 rounded-lg hover:bg-dark-hover">
<Filter className="w-4 h-4" />
</button>
</div>
<div className="flex gap-2">
<select
value={filterEmotion}
onChange={(e) => setFilterEmotion(e.target.value)}
className="flex-1 px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
>
<option value="all">All Emotions</option>
<option value="confident">Confident</option>
<option value="neutral">Neutral</option>
<option value="anxious">Anxious</option>
<option value="fearful">Fearful</option>
<option value="greedy">Greedy</option>
</select>
<select
value={filterPnL}
onChange={(e) => setFilterPnL(e.target.value as any)}
className="flex-1 px-3 py-2 bg-dark-bg border border-gray-700 rounded text-sm"
>
<option value="all">All Trades</option>
<option value="winners">Winners Only</option>
<option value="losers">Losers Only</option>
</select>
</div>
</div>
{/* Journal Entries */}
<div className="flex-1 overflow-y-auto space-y-3">
{filteredEntries.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<BookOpen className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>No journal entries found</p>
<p className="text-sm">Start documenting your trading journey</p>
</div>
) : (
filteredEntries.map(entry => (
<div
key={entry.id}
className="p-4 bg-dark-hover rounded-lg border border-gray-700 hover:border-gray-600 transition-colors"
>
{/* Entry Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
{entry.action === 'BUY' ? (
<TrendingUp className="w-5 h-5 text-green-500" />
) : (
<TrendingDown className="w-5 h-5 text-red-500" />
)}
<div>
<div className="font-semibold">
{entry.action} @ ${entry.price.toFixed(2)}
</div>
<div className="text-xs text-gray-400 flex items-center gap-2">
<Calendar className="w-3 h-3" />
{new Date(entry.timestamp).toLocaleString()}
</div>
</div>
</div>
<div className="text-right">
{entry.pnl !== undefined && (
<div className={`font-bold ${entry.pnl > 0 ? 'text-green-500' : 'text-red-500'}`}>
{entry.pnl > 0 ? '+' : ''}${entry.pnl.toFixed(2)}
</div>
)}
<div className="flex items-center gap-1 justify-end mt-1">
{[...Array(entry.setupQuality)].map((_, i) => (
<span key={i} className="text-gold-500 text-xs"></span>
))}
</div>
</div>
</div>
{/* Emotional State & Plan Followed */}
<div className="flex items-center gap-4 mb-3 text-sm">
<div className="flex items-center gap-2">
{getEmotionIcon(entry.emotionalState)}
<span className="text-gray-400 capitalize">{entry.emotionalState}</span>
</div>
<div className={`flex items-center gap-1 ${entry.planFollowed ? 'text-green-500' : 'text-red-500'}`}>
{entry.planFollowed ? '✓' : '✗'} Plan {entry.planFollowed ? 'Followed' : 'Deviated'}
</div>
</div>
{/* Entry Details */}
{entry.entryReason && (
<div className="mb-2">
<div className="text-xs text-gray-500 font-medium mb-1">Entry Reason:</div>
<div className="text-sm text-gray-300">{entry.entryReason}</div>
</div>
)}
{entry.exitReason && (
<div className="mb-2">
<div className="text-xs text-gray-500 font-medium mb-1">Exit Reason:</div>
<div className="text-sm text-gray-300">{entry.exitReason}</div>
</div>
)}
{entry.marketConditions && (
<div className="mb-2">
<div className="text-xs text-gray-500 font-medium mb-1">Market Conditions:</div>
<div className="text-sm text-gray-300">{entry.marketConditions}</div>
</div>
)}
{entry.lessonsLearned && (
<div className="mb-2">
<div className="text-xs text-gray-500 font-medium mb-1">Lessons Learned:</div>
<div className="text-sm text-gray-300 italic">{entry.lessonsLearned}</div>
</div>
)}
{/* Tags */}
{entry.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{entry.tags.map((tag, i) => (
<span
key={i}
className="px-2 py-1 bg-blue-500/20 text-blue-500 text-xs rounded"
>
{tag}
</span>
))}
</div>
)}
</div>
))
)}
</div>
</div>
);
}
@@ -0,0 +1,51 @@
import React, { useEffect, useMemo, useState } from 'react'
const DEFAULT_LIST = ['BTCUSDT', 'XAUUSD']
const ALL_SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XAUUSD']
const LS_KEY = 'watchlist.symbols'
export function useWatchlist() {
const [symbols, setSymbols] = useState<string[]>(() => {
try {
const raw = localStorage.getItem(LS_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return DEFAULT_LIST
})
useEffect(() => {
try { localStorage.setItem(LS_KEY, JSON.stringify(symbols)) } catch {}
}, [symbols])
const add = (s: string) => setSymbols(prev => Array.from(new Set([...prev, s])))
const remove = (s: string) => setSymbols(prev => prev.filter(x => x !== s))
return { symbols, add, remove, setSymbols }
}
export default function WatchlistPanel({ onChange }: { onChange?: (symbols: string[]) => void }) {
const { symbols, add, remove, setSymbols } = useWatchlist()
const [candidate, setCandidate] = useState<string>('BTCUSDT')
useEffect(() => { onChange?.(symbols) }, [symbols])
const available = useMemo(() => ALL_SYMBOLS.filter(s => !symbols.includes(s)), [symbols])
return (
<div className="card">
<div className="title">Watchlist</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<select className="input" value={candidate} onChange={e => setCandidate(e.target.value)}>
{available.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<button className="btn-primary" onClick={() => add(candidate)} disabled={!candidate}>Add</button>
<button className="btn" onClick={() => setSymbols(DEFAULT_LIST)}>Reset</button>
</div>
<ul style={{ display: 'grid', gap: 6 }}>
{symbols.map(s => (
<li key={s} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{s}</span>
<button className="btn-danger" onClick={() => remove(s)}>Remove</button>
</li>
))}
</ul>
</div>
)
}
+85
View File
@@ -0,0 +1,85 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import type { PriceData } from '@/types';
import { marketDataApi } from '@/services/api';
export interface UseLivePriceOptions {
enabled?: boolean;
pollInterval?: number; // milliseconds, default 10000 (10 seconds)
timeframe?: string; // chart timeframe: 1min, 5min, etc.
onUpdate?: (priceData: PriceData) => void;
onError?: (error: Error) => void;
}
export function useLivePrice(options: UseLivePriceOptions = {}) {
const {
enabled = true,
pollInterval = 10000,
timeframe = '1min',
onUpdate,
onError,
} = options;
const [latestPrice, setLatestPrice] = useState<PriceData | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<Error | null>(null);
const intervalRef = useRef<number | null>(null);
const onUpdateRef = useRef(onUpdate);
const onErrorRef = useRef(onError);
// Keep refs updated
useEffect(() => {
onUpdateRef.current = onUpdate;
onErrorRef.current = onError;
}, [onUpdate, onError]);
const fetchLatestPrice = useCallback(async () => {
try {
const priceData = await marketDataApi.getLivePrice(timeframe);
setLatestPrice(priceData);
setError(null);
setIsConnected(true);
onUpdateRef.current?.(priceData);
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to fetch live price');
setError(error);
setIsConnected(false);
onErrorRef.current?.(error);
console.error('Live price fetch error:', error);
}
}, [timeframe]);
useEffect(() => {
if (!enabled) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setIsConnected(false);
return;
}
// Fetch immediately on mount
fetchLatestPrice();
// Set up polling interval
intervalRef.current = setInterval(fetchLatestPrice, pollInterval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [enabled, pollInterval, fetchLatestPrice]);
const refresh = useCallback(() => {
fetchLatestPrice();
}, [fetchLatestPrice]);
return {
latestPrice,
isConnected,
error,
refresh,
};
}
+78
View File
@@ -0,0 +1,78 @@
import { useEffect, useMemo, useRef, useState } from 'react'
function buildSseUrl(symbols: string[], timeframe = '1m') {
const api = (import.meta as any).env?.VITE_API_URL || 'http://localhost:8000/api'
const base = api.replace(/\/$/, '')
const params = new URLSearchParams({ symbols: symbols.join(','), timeframe })
return `${base}/stream/sse?${params.toString()}`
}
export type KlineEvent = {
symbol: string
timeframe: string
open_time: string
close_time: string
open: number
high: number
low: number
close: number
volume: number
is_closed: boolean
source: string
}
export function useSSEMultiplexer(symbols: string[], timeframe = '1m') {
const [isConnected, setIsConnected] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [lastBySymbol, setLastBySymbol] = useState<Record<string, KlineEvent | undefined>>({})
const esRef = useRef<EventSource | null>(null)
const key = useMemo(() => symbols.slice().sort().join(','), [symbols])
useEffect(() => {
if (!symbols.length) return
// Close previous
if (esRef.current) {
try { esRef.current.close() } catch {}
esRef.current = null
}
const url = buildSseUrl(symbols, timeframe)
const es = new EventSource(url)
esRef.current = es
setIsConnected(false)
setError(null)
const onOpen = () => setIsConnected(true)
const onError = () => {
setIsConnected(false)
setError(new Error('SSE connection error'))
}
const onMessage = (ev: MessageEvent) => {
// We expect named events 'kline'
try {
const data = JSON.parse(ev.data) as KlineEvent
setLastBySymbol(prev => ({ ...prev, [data.symbol.replace('/', '')]: data }))
} catch {}
}
const onKline = (ev: MessageEvent) => onMessage(ev)
es.addEventListener('open', onOpen as any)
es.addEventListener('error', onError as any)
es.addEventListener('kline', onKline as any)
// Some servers send unnamed messages
es.addEventListener('message', onMessage as any)
return () => {
try {
es.removeEventListener('open', onOpen as any)
es.removeEventListener('error', onError as any)
es.removeEventListener('kline', onKline as any)
es.removeEventListener('message', onMessage as any)
es.close()
} catch {}
esRef.current = null
setIsConnected(false)
}
}, [key, timeframe])
return { isConnected, error, lastBySymbol }
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './styles/index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+161
View File
@@ -0,0 +1,161 @@
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
export const marketDataApi = {
getGoldPrice: async (): Promise<any> => {
const response = await api.get('/market/gold/current');
return response.data;
},
getHistoricalData: async (
interval: string = 'daily',
outputSize: string = 'compact'
): Promise<any[]> => {
const response = await api.get('/market/gold/history', {
params: { interval, outputSize },
});
return response.data;
},
getLivePrice: async (interval: string = '1min'): Promise<any> => {
const response = await api.get('/market/gold/live', {
params: { interval },
});
return response.data;
},
// New: Generic OHLCV history for Live charts (BTC/ETH via Binance, XAU via AlphaVantage)
getOHLCV: async (
symbol: string,
timeframe: string = '1m',
limit: number = 500
): Promise<any[]> => {
const response = await api.get('/ohlcv', { params: { symbol, timeframe, limit } });
// Normalize to PriceData
const rows = response.data as { time: number; open: number; high: number; low: number; close: number }[];
return rows.map((r) => ({ time: r.time, open: r.open, high: r.high, low: r.low, close: r.close }));
},
getIndicatorData: async (
indicator: string,
params: Record<string, any>
): Promise<any[]> => {
const response = await api.get(`/indicators/${indicator}`, {
params,
});
return response.data;
},
};
export const tradingApi = {
executeTrade: async (trade: any): Promise<any> => {
const response = await api.post('/trading/execute', trade);
return response.data;
},
getTradeHistory: async (): Promise<any[]> => {
const response = await api.get('/trading/history');
return response.data;
},
getPortfolio: async (): Promise<any> => {
const response = await api.get('/trading/portfolio');
return response.data;
},
resetSimulation: async (): Promise<void> => {
await api.post('/trading/reset');
},
};
export const aiApi = {
analyzeScenario: async (data: {
priceData: any[];
indicators: any[];
currentPrice: number;
}): Promise<any> => {
const response = await api.post('/ai/analyze', data);
return response.data;
},
getTradeRecommendation: async (
priceData: any[],
position: any
): Promise<any> => {
const response = await api.post('/ai/recommend', {
priceData,
position,
});
return response.data;
},
};
export const newsApi = {
getNewsFeed: async (limit: number = 50): Promise<any> => {
const response = await api.get('/news/feed', {
params: { limit },
});
return response.data;
},
getAlerts: async (limit: number = 50): Promise<any> => {
const response = await api.get('/news/alerts', {
params: { limit },
});
return response.data;
},
getEconomicCalendar: async (): Promise<any> => {
const response = await api.get('/news/economic-calendar');
return response.data;
},
clearOldAlerts: async (): Promise<void> => {
await api.post('/news/alerts/clear');
},
};
// New endpoints
export const accountApi = {
getAccount: async (): Promise<any> => (await api.get('/account')).data,
getPositions: async (): Promise<any[]> => (await api.get('/positions')).data,
}
export const performanceApi = {
getEquityHistory: async (symbol = 'XAU/USD', timeframe = '1m', limit = 300): Promise<{ time: number; equity: number }[]> => (
await api.get('/equity-history', { params: { symbol, timeframe, limit } })
).data,
getPerformance: async (symbol = 'XAU/USD', timeframe = '1m', limit = 300): Promise<any> => (
await api.get('/performance', { params: { symbol, timeframe, limit } })
).data,
}
export const statusApi = {
getStatus: async (): Promise<any> => (await api.get('/status')).data,
}
export const settingsApi = {
getModels: async (): Promise<any> => (await api.get('/settings/models')).data,
putModels: async (patch: any): Promise<any> => (await api.put('/settings/models', patch)).data,
getExchanges: async (): Promise<any> => (await api.get('/settings/exchanges')).data,
putExchanges: async (patch: any): Promise<any> => (await api.put('/settings/exchanges', patch)).data,
}
export const promptsApi = {
list: async (): Promise<any[]> => (await api.get('/prompt-templates')).data,
get: async (name: string): Promise<any> => (await api.get(`/prompt-templates/${encodeURIComponent(name)}`)).data,
}
export const decisionsApi = {
getLatest: async (limit = 20): Promise<any[]> => (await api.get('/decisions/latest', { params: { limit } })).data,
}
export default api;
+49
View File
@@ -0,0 +1,49 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-dark-border;
}
body {
@apply bg-dark-bg text-gray-100;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
}
@layer components {
.card {
@apply bg-dark-surface border border-dark-border rounded-lg p-4;
}
.btn {
@apply px-4 py-2 rounded-md font-medium transition-colors duration-200;
}
.btn-primary {
@apply btn bg-blue-600 hover:bg-blue-700 text-white;
}
.btn-success {
@apply btn bg-green-500 hover:bg-green-600 text-white;
}
.btn-danger {
@apply btn bg-red-500 hover:bg-red-600 text-white;
}
.input {
@apply bg-dark-bg border border-dark-border rounded-md px-3 py-2 text-gray-100
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
}
}
+202
View File
@@ -0,0 +1,202 @@
export interface PriceData {
time: number;
open: number;
high: number;
low: number;
close: number;
volume?: number;
}
export interface Trade {
id: string;
timestamp: number;
action: 'BUY' | 'SELL';
quantity: number;
price: number;
total: number;
pnl?: number;
}
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
}
export interface Portfolio {
cash: number;
initialCapital: number;
totalValue: number;
totalPnl: number;
totalPnlPercent: number;
position: Position | null;
trades: Trade[];
}
export interface Indicator {
name: string;
enabled: boolean;
params: Record<string, number>;
data: { time: number; value: number }[];
}
export interface SimulationConfig {
initialCapital: number;
tradeFee: number;
slippage: number;
}
export interface AIAnalysis {
recommendation: 'BUY' | 'SELL' | 'HOLD';
confidence: number;
reasoning: string;
supportResistance: {
support: number[];
resistance: number[];
};
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
high24h: number;
low24h: number;
volume: number;
}
// News and Sentiment Types
export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
export interface NewsArticle {
id: string;
source: string;
title: string;
description?: string;
url: string;
published_at: string;
sentiment: Sentiment;
sentiment_score: number;
impact_on_gold: 'HIGH' | 'MEDIUM' | 'LOW';
relevance_score: number;
category: string;
}
export interface NewsFeed {
articles: NewsArticle[];
total_count: number;
bullish_count: number;
bearish_count: number;
neutral_count: number;
overall_sentiment: Sentiment;
avg_sentiment_score: number;
}
export interface EconomicEvent {
id: string;
title: string;
country: string;
currency: string;
event_date: string;
importance: 'HIGH' | 'MEDIUM' | 'LOW';
forecast?: string;
previous?: string;
actual?: string;
impact_on_gold: string;
}
export interface EconomicCalendar {
events: EconomicEvent[];
upcoming_high_impact: number;
}
// Alert Types
export type AlertType =
| 'PRICE_SPIKE'
| 'PRICE_DROP'
| 'NEWS_BREAKING'
| 'SUPPORT_BREACH'
| 'RESISTANCE_BREACH'
| 'HIGH_VOLATILITY'
| 'ECONOMIC_EVENT';
export type AlertSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
export interface Alert {
id: string;
type: AlertType;
severity: AlertSeverity;
title: string;
message: string;
price?: number;
change_percent?: number;
timestamp: string;
related_news?: string[];
action_required: boolean;
}
export interface AlertsData {
alerts: Alert[];
critical_count: number;
unread_count: number;
}
// Dashboard Customization Types
export type TabId =
| 'chart'
| 'portfolio'
| 'trade-controls'
| 'risk-management'
| 'ai-analysis'
| 'news'
| 'alerts'
| 'analytics'
| 'daily-checklist'
| 'daily-plan'
| 'trading-journal'
| 'market-summary';
export type LayoutMode = 'grid' | 'tabs' | 'split';
export type TabSize = 'small' | 'medium' | 'large' | 'full';
export type TabPosition = 'left' | 'center' | 'right' | 'top' | 'bottom';
export interface TabCustomization {
refreshRate?: number; // in seconds
autoRefresh?: boolean;
displayMode?: string;
filters?: Record<string, any>;
theme?: 'default' | 'compact' | 'detailed';
}
export interface TabConfig {
id: TabId;
label: string;
icon?: string;
visible: boolean;
order: number;
size: TabSize;
position?: TabPosition;
group?: string; // for grouping related tabs
customization?: TabCustomization;
pinned?: boolean;
}
export interface LayoutPreset {
id: string;
name: string;
description: string;
mode: LayoutMode;
tabs: TabConfig[];
}
export interface DashboardConfig {
mode: LayoutMode;
tabs: TabConfig[];
activePreset?: string;
customPresets: LayoutPreset[];
}
+348
View File
@@ -0,0 +1,348 @@
import type { DashboardConfig, TabConfig, LayoutPreset } from '@/types';
const STORAGE_KEY = 'gold-trading-dashboard-config';
export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
{
id: 'chart',
label: 'Price Chart',
visible: true,
order: 0,
size: 'large',
position: 'left',
pinned: true,
customization: {
autoRefresh: true,
refreshRate: 60,
},
},
{
id: 'trade-controls',
label: 'Trade Controls',
visible: true,
order: 1,
size: 'medium',
position: 'center',
pinned: true,
},
{
id: 'portfolio',
label: 'Portfolio',
visible: true,
order: 2,
size: 'medium',
position: 'center',
},
{
id: 'risk-management',
label: 'Risk Management',
visible: true,
order: 3,
size: 'medium',
position: 'center',
},
{
id: 'ai-analysis',
label: 'AI Analysis',
visible: true,
order: 4,
size: 'large',
position: 'left',
},
{
id: 'news',
label: 'News Feed',
visible: true,
order: 5,
size: 'medium',
position: 'right',
customization: {
autoRefresh: true,
refreshRate: 300,
filters: { sentiment: 'ALL' },
},
},
{
id: 'alerts',
label: 'Alerts',
visible: true,
order: 6,
size: 'medium',
position: 'right',
},
{
id: 'analytics',
label: 'Advanced Analytics',
visible: true,
order: 7,
size: 'full',
position: 'bottom',
},
// New Daily Trading Tools
{
id: 'daily-checklist',
label: 'Daily Checklist',
visible: false,
order: 8,
size: 'medium',
position: 'left',
},
{
id: 'daily-plan',
label: 'Trading Plan',
visible: false,
order: 9,
size: 'medium',
position: 'center',
},
{
id: 'trading-journal',
label: 'Trading Journal',
visible: false,
order: 10,
size: 'large',
position: 'right',
},
{
id: 'market-summary',
label: 'Market Brief',
visible: false,
order: 11,
size: 'medium',
position: 'left',
},
];
export const DEFAULT_PRESETS: LayoutPreset[] = [
{
id: 'trading-focus',
name: 'Trading Focus',
description: 'Optimized for active trading with chart and controls prominent',
mode: 'grid',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0 }, // chart
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 1, visible: true }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 2, visible: true }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 3, visible: true }, // risk
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 4, visible: false }, // ai
{ ...DEFAULT_TAB_CONFIGS[5], size: 'small', order: 5, visible: true }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'small', order: 6, visible: true }, // alerts
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 7, visible: false }, // analytics
],
},
{
id: 'analysis-focus',
name: 'Analysis Focus',
description: 'Full-screen analytics and AI insights',
mode: 'tabs',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0, visible: true }, // chart
{ ...DEFAULT_TAB_CONFIGS[4], size: 'full', order: 1, visible: true }, // ai
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 2, visible: true }, // analytics
{ ...DEFAULT_TAB_CONFIGS[3], size: 'full', order: 3, visible: true }, // risk
{ ...DEFAULT_TAB_CONFIGS[2], size: 'full', order: 4, visible: true }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[5], size: 'full', order: 5, visible: true }, // news
{ ...DEFAULT_TAB_CONFIGS[1], size: 'full', order: 6, visible: false }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[6], size: 'full', order: 7, visible: false }, // alerts
],
},
{
id: 'news-focus',
name: 'News Focus',
description: 'News and market updates at the forefront',
mode: 'split',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 0, position: 'left' }, // chart
{ ...DEFAULT_TAB_CONFIGS[5], size: 'large', order: 1, position: 'right' }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 2, position: 'right' }, // alerts
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 3, position: 'left' }, // ai
{ ...DEFAULT_TAB_CONFIGS[1], size: 'small', order: 4, position: 'left', visible: false },
{ ...DEFAULT_TAB_CONFIGS[2], size: 'small', order: 5, position: 'left', visible: false },
{ ...DEFAULT_TAB_CONFIGS[3], size: 'small', order: 6, position: 'left', visible: false },
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 7, visible: false }, // analytics
],
},
{
id: 'balanced',
name: 'Balanced View',
description: 'Equal emphasis on all components',
mode: 'grid',
tabs: DEFAULT_TAB_CONFIGS.map((tab, index) => ({ ...tab, order: index, visible: true })),
},
// Daily Trading Workflow Presets
{
id: 'morning-setup',
name: '🌅 Morning Setup',
description: 'Pre-market routine: checklist, plan, and market brief',
mode: 'grid',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[11], size: 'large', order: 0, visible: true }, // market summary
{ ...DEFAULT_TAB_CONFIGS[8], size: 'medium', order: 1, visible: true }, // checklist
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 2, visible: true }, // trading plan
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 3, visible: true }, // chart
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 4, visible: true }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 5, visible: true }, // alerts
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 6, visible: true }, // ai
{ ...DEFAULT_TAB_CONFIGS[1], size: 'small', order: 7, visible: false }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[2], size: 'small', order: 8, visible: false }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[3], size: 'small', order: 9, visible: false }, // risk
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 10, visible: false }, // analytics
{ ...DEFAULT_TAB_CONFIGS[10], size: 'large', order: 11, visible: false }, // journal
],
},
{
id: 'active-trading',
name: '📈 Active Trading',
description: 'During market hours: chart, controls, and execution',
mode: 'grid',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0, visible: true, pinned: true }, // chart
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 1, visible: true, pinned: true }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 2, visible: true }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 3, visible: true }, // risk
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 4, visible: true }, // trading plan
{ ...DEFAULT_TAB_CONFIGS[8], size: 'small', order: 5, visible: true }, // checklist
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 6, visible: true }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 7, visible: true }, // alerts
{ ...DEFAULT_TAB_CONFIGS[10], size: 'medium', order: 8, visible: false }, // journal (quick access)
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 9, visible: false }, // ai
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 10, visible: false }, // analytics
{ ...DEFAULT_TAB_CONFIGS[11], size: 'medium', order: 11, visible: false }, // market summary
],
},
{
id: 'end-of-day-review',
name: '🌙 End-of-Day Review',
description: 'Post-market analysis: journal, analytics, and planning for tomorrow',
mode: 'tabs',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[10], size: 'full', order: 0, visible: true }, // journal
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 1, visible: true }, // analytics
{ ...DEFAULT_TAB_CONFIGS[8], size: 'full', order: 2, visible: true }, // checklist
{ ...DEFAULT_TAB_CONFIGS[9], size: 'full', order: 3, visible: true }, // trading plan
{ ...DEFAULT_TAB_CONFIGS[2], size: 'full', order: 4, visible: true }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 5, visible: true }, // chart
{ ...DEFAULT_TAB_CONFIGS[11], size: 'full', order: 6, visible: false }, // market summary
{ ...DEFAULT_TAB_CONFIGS[1], size: 'full', order: 7, visible: false }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[3], size: 'full', order: 8, visible: false }, // risk
{ ...DEFAULT_TAB_CONFIGS[4], size: 'full', order: 9, visible: false }, // ai
{ ...DEFAULT_TAB_CONFIGS[5], size: 'full', order: 10, visible: false }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'full', order: 11, visible: false }, // alerts
],
},
{
id: 'daily-trader',
name: '⚡ Complete Daily Trader',
description: 'All daily trading tools visible for comprehensive workflow',
mode: 'grid',
tabs: [
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 0, visible: true, position: 'left' }, // chart
{ ...DEFAULT_TAB_CONFIGS[11], size: 'medium', order: 1, visible: true, position: 'left' }, // market summary
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 2, visible: true, position: 'center' }, // trade controls
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 3, visible: true, position: 'center' }, // trading plan
{ ...DEFAULT_TAB_CONFIGS[8], size: 'medium', order: 4, visible: true, position: 'center' }, // checklist
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 5, visible: true, position: 'center' }, // portfolio
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 6, visible: true, position: 'right' }, // news
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 7, visible: true, position: 'right' }, // alerts
{ ...DEFAULT_TAB_CONFIGS[10], size: 'large', order: 8, visible: true, position: 'right' }, // journal
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 9, visible: false }, // risk
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 10, visible: false }, // ai
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 11, visible: false }, // analytics
],
},
];
export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = {
mode: 'grid',
tabs: DEFAULT_TAB_CONFIGS,
activePreset: 'balanced',
customPresets: [],
};
export function loadDashboardConfig(): DashboardConfig {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const config = JSON.parse(stored);
const storedTabs: TabConfig[] = config.tabs || [];
const defaultTabsById = new Map(DEFAULT_TAB_CONFIGS.map((tab) => [tab.id, tab]));
// Merge stored tab settings with defaults so required tabs always exist
const mergedTabs = DEFAULT_TAB_CONFIGS.map((defaultTab) => {
const storedTab = storedTabs.find((tab) => tab.id === defaultTab.id);
return storedTab ? { ...defaultTab, ...storedTab } : defaultTab;
});
// Preserve any custom tabs user may have added that aren't in defaults
const additionalTabs = storedTabs.filter((tab) => !defaultTabsById.has(tab.id));
return {
...DEFAULT_DASHBOARD_CONFIG,
...config,
tabs: [...mergedTabs, ...additionalTabs],
};
}
} catch (error) {
console.error('Error loading dashboard config:', error);
}
return DEFAULT_DASHBOARD_CONFIG;
}
export function saveDashboardConfig(config: DashboardConfig): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
} catch (error) {
console.error('Error saving dashboard config:', error);
}
}
export function getPresetById(presetId: string): LayoutPreset | undefined {
return DEFAULT_PRESETS.find((p) => p.id === presetId);
}
export function applyPreset(config: DashboardConfig, presetId: string): DashboardConfig {
const preset = getPresetById(presetId);
if (!preset) {
// Check custom presets
const customPreset = config.customPresets.find((p) => p.id === presetId);
if (customPreset) {
return {
...config,
mode: customPreset.mode,
tabs: customPreset.tabs,
activePreset: presetId,
};
}
return config;
}
return {
...config,
mode: preset.mode,
tabs: preset.tabs,
activePreset: presetId,
};
}
export function saveCustomPreset(
config: DashboardConfig,
name: string,
description: string
): DashboardConfig {
const newPreset: LayoutPreset = {
id: `custom-${Date.now()}`,
name,
description,
mode: config.mode,
tabs: [...config.tabs],
};
return {
...config,
customPresets: [...config.customPresets, newPreset],
};
}
export function resetToDefault(): DashboardConfig {
return { ...DEFAULT_DASHBOARD_CONFIG };
}
+144
View File
@@ -0,0 +1,144 @@
import type { Trade, Portfolio } from '@/types';
// Export trades to CSV
export const exportTradesToCSV = (trades: Trade[], portfolio: Portfolio): void => {
const headers = [
'ID',
'Timestamp',
'Action',
'Quantity',
'Price',
'Total',
'P&L',
'Date/Time',
];
const rows = trades.map((trade) => [
trade.id,
trade.timestamp,
trade.action,
trade.quantity.toFixed(4),
trade.price.toFixed(2),
trade.total.toFixed(2),
trade.pnl?.toFixed(2) || 'N/A',
new Date(trade.timestamp).toLocaleString(),
]);
// Add summary row
rows.push([]);
rows.push(['Summary', '', '', '', '', '', '']);
rows.push(['Total Trades', trades.length.toString(), '', '', '', '', '']);
rows.push(['Initial Capital', portfolio.initialCapital.toFixed(2), '', '', '', '', '']);
rows.push(['Current Value', portfolio.totalValue.toFixed(2), '', '', '', '', '']);
rows.push(['Total P&L', portfolio.totalPnl.toFixed(2), '', '', '', '', '']);
rows.push([
'Total P&L %',
portfolio.totalPnlPercent.toFixed(2) + '%',
'',
'',
'',
'',
'',
]);
const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n');
downloadFile(csvContent, 'gold_trades_export.csv', 'text/csv');
};
// Export portfolio summary
export const exportPortfolioSummary = (portfolio: Portfolio): void => {
const summary = {
exportDate: new Date().toISOString(),
initialCapital: portfolio.initialCapital,
currentCash: portfolio.cash,
totalValue: portfolio.totalValue,
totalPnL: portfolio.totalPnl,
totalPnLPercent: portfolio.totalPnlPercent,
totalTrades: portfolio.trades.length,
position: portfolio.position
? {
symbol: portfolio.position.symbol,
quantity: portfolio.position.quantity,
avgPrice: portfolio.position.avgPrice,
currentPrice: portfolio.position.currentPrice,
unrealizedPnL: portfolio.position.unrealizedPnl,
unrealizedPnLPercent: portfolio.position.unrealizedPnlPercent,
}
: null,
};
const jsonContent = JSON.stringify(summary, null, 2);
downloadFile(jsonContent, 'portfolio_summary.json', 'application/json');
};
// Export analytics report
export const exportAnalyticsReport = (
portfolio: Portfolio,
analytics: any
): void => {
const report = `
=================================================
GOLD TRADING SIMULATOR - ANALYTICS REPORT
=================================================
Generated: ${new Date().toLocaleString()}
PORTFOLIO SUMMARY
-------------------------------------------------
Initial Capital: $${portfolio.initialCapital.toFixed(2)}
Current Value: $${portfolio.totalValue.toFixed(2)}
Total P&L: $${portfolio.totalPnl.toFixed(2)} (${portfolio.totalPnlPercent.toFixed(2)}%)
Cash Available: $${portfolio.cash.toFixed(2)}
PERFORMANCE METRICS
-------------------------------------------------
Total Trades: ${analytics.totalTrades}
Win Rate: ${analytics.winRate}%
Winning Trades: ${analytics.winningTrades}
Losing Trades: ${analytics.losingTrades}
Average Win: $${analytics.avgWin.toFixed(2)}
Average Loss: $${analytics.avgLoss.toFixed(2)}
Largest Win: $${analytics.largestWin.toFixed(2)}
Largest Loss: $${Math.abs(analytics.largestLoss).toFixed(2)}
Profit Factor: ${analytics.profitFactor === Infinity ? '∞' : analytics.profitFactor.toFixed(2)}
Risk/Reward Ratio: 1:${analytics.riskRewardRatio.toFixed(2)}
Sharpe Ratio: ${analytics.sharpeRatio.toFixed(2)}
Max Drawdown: ${analytics.maxDrawdown.toFixed(2)}%
${portfolio.position ? `
CURRENT POSITION
-------------------------------------------------
Symbol: ${portfolio.position.symbol}
Quantity: ${portfolio.position.quantity.toFixed(4)} oz
Avg Entry Price: $${portfolio.position.avgPrice.toFixed(2)}
Current Price: $${portfolio.position.currentPrice.toFixed(2)}
Unrealized P&L: $${portfolio.position.unrealizedPnl.toFixed(2)} (${portfolio.position.unrealizedPnlPercent.toFixed(2)}%)
` : ''}
=================================================
END OF REPORT
=================================================
`.trim();
downloadFile(report, 'trading_analytics_report.txt', 'text/plain');
};
// Helper function to download file
const downloadFile = (content: string, filename: string, mimeType: string): void => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Copy to clipboard
export const copyToClipboard = (text: string): Promise<void> => {
return navigator.clipboard.writeText(text);
};
+431
View File
@@ -0,0 +1,431 @@
import type { PriceData } from '@/types';
export const calculateSMA = (
data: PriceData[],
period: number
): { time: number; value: number }[] => {
const result: { time: number; value: number }[] = [];
for (let i = period - 1; i < data.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j].close;
}
const sma = sum / period;
result.push({
time: data[i].time,
value: Number(sma.toFixed(2)),
});
}
return result;
};
export const calculateEMA = (
data: PriceData[],
period: number
): { time: number; value: number }[] => {
const result: { time: number; value: number }[] = [];
const multiplier = 2 / (period + 1);
if (data.length < period) return result;
// Calculate initial SMA for first EMA value
let sum = 0;
for (let i = 0; i < period; i++) {
sum += data[i].close;
}
let ema = sum / period;
result.push({ time: data[period - 1].time, value: Number(ema.toFixed(2)) });
// Calculate EMA for remaining data
for (let i = period; i < data.length; i++) {
ema = (data[i].close - ema) * multiplier + ema;
result.push({ time: data[i].time, value: Number(ema.toFixed(2)) });
}
return result;
};
export const calculateRSI = (
data: PriceData[],
period: number = 14
): { time: number; value: number }[] => {
const result: { time: number; value: number }[] = [];
if (data.length < period + 1) return result;
let gains = 0;
let losses = 0;
// Calculate initial average gain and loss
for (let i = 1; i <= period; i++) {
const change = data[i].close - data[i - 1].close;
if (change > 0) {
gains += change;
} else {
losses += Math.abs(change);
}
}
let avgGain = gains / period;
let avgLoss = losses / period;
for (let i = period; i < data.length; i++) {
const change = data[i].close - data[i - 1].close;
const gain = change > 0 ? change : 0;
const loss = change < 0 ? Math.abs(change) : 0;
avgGain = (avgGain * (period - 1) + gain) / period;
avgLoss = (avgLoss * (period - 1) + loss) / period;
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
const rsi = 100 - 100 / (1 + rs);
result.push({ time: data[i].time, value: Number(rsi.toFixed(2)) });
}
return result;
};
export const formatPrice = (price: number): string => {
return `$${price.toFixed(2)}`;
};
export const formatPercent = (value: number): string => {
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(2)}%`;
};
export const formatNumber = (value: number): string => {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
};
// MACD (Moving Average Convergence Divergence)
export const calculateMACD = (
data: PriceData[],
fastPeriod: number = 12,
slowPeriod: number = 26,
signalPeriod: number = 9
): {
macd: { time: number; value: number }[];
signal: { time: number; value: number }[];
histogram: { time: number; value: number }[];
} => {
const fastEMA = calculateEMA(data, fastPeriod);
const slowEMA = calculateEMA(data, slowPeriod);
const macdLine: { time: number; value: number }[] = [];
// Calculate MACD line (fast EMA - slow EMA)
const startIndex = slowPeriod - 1;
for (let i = 0; i < fastEMA.length; i++) {
if (i + startIndex < slowEMA.length) {
macdLine.push({
time: fastEMA[i + fastPeriod - slowPeriod].time,
value: Number((fastEMA[i + fastPeriod - slowPeriod].value - slowEMA[i].value).toFixed(2)),
});
}
}
// Calculate signal line (EMA of MACD)
const signalLine: { time: number; value: number }[] = [];
if (macdLine.length >= signalPeriod) {
const multiplier = 2 / (signalPeriod + 1);
let ema = macdLine.slice(0, signalPeriod).reduce((sum, d) => sum + d.value, 0) / signalPeriod;
signalLine.push({ time: macdLine[signalPeriod - 1].time, value: Number(ema.toFixed(2)) });
for (let i = signalPeriod; i < macdLine.length; i++) {
ema = (macdLine[i].value - ema) * multiplier + ema;
signalLine.push({ time: macdLine[i].time, value: Number(ema.toFixed(2)) });
}
}
// Calculate histogram (MACD - Signal)
const histogram: { time: number; value: number }[] = [];
for (let i = 0; i < signalLine.length; i++) {
const macdValue = macdLine[i + macdLine.length - signalLine.length].value;
histogram.push({
time: signalLine[i].time,
value: Number((macdValue - signalLine[i].value).toFixed(2)),
});
}
return { macd: macdLine, signal: signalLine, histogram };
};
// Bollinger Bands
export const calculateBollingerBands = (
data: PriceData[],
period: number = 20,
stdDev: number = 2
): {
upper: { time: number; value: number }[];
middle: { time: number; value: number }[];
lower: { time: number; value: number }[];
} => {
const middle = calculateSMA(data, period);
const upper: { time: number; value: number }[] = [];
const lower: { time: number; value: number }[] = [];
for (let i = period - 1; i < data.length; i++) {
const slice = data.slice(i - period + 1, i + 1);
const sma = slice.reduce((sum, d) => sum + d.close, 0) / period;
const variance = slice.reduce((sum, d) => sum + Math.pow(d.close - sma, 2), 0) / period;
const sd = Math.sqrt(variance);
const time = data[i].time;
upper.push({ time, value: Number((sma + stdDev * sd).toFixed(2)) });
lower.push({ time, value: Number((sma - stdDev * sd).toFixed(2)) });
}
return { upper, middle, lower };
};
// ATR (Average True Range)
export const calculateATR = (
data: PriceData[],
period: number = 14
): { time: number; value: number }[] => {
const result: { time: number; value: number }[] = [];
if (data.length < period + 1) return result;
const trueRanges: number[] = [];
// Calculate True Range for each period
for (let i = 1; i < data.length; i++) {
const high = data[i].high;
const low = data[i].low;
const prevClose = data[i - 1].close;
const tr = Math.max(
high - low,
Math.abs(high - prevClose),
Math.abs(low - prevClose)
);
trueRanges.push(tr);
}
// Calculate initial ATR (SMA of TR)
let atr = trueRanges.slice(0, period).reduce((sum, tr) => sum + tr, 0) / period;
result.push({ time: data[period].time, value: Number(atr.toFixed(2)) });
// Calculate remaining ATR using smoothing
for (let i = period; i < trueRanges.length; i++) {
atr = (atr * (period - 1) + trueRanges[i]) / period;
result.push({ time: data[i + 1].time, value: Number(atr.toFixed(2)) });
}
return result;
};
// Fibonacci Retracement Levels
export const calculateFibonacci = (
high: number,
low: number
): { level: string; price: number }[] => {
const diff = high - low;
const levels = [
{ level: '0%', ratio: 0 },
{ level: '23.6%', ratio: 0.236 },
{ level: '38.2%', ratio: 0.382 },
{ level: '50%', ratio: 0.5 },
{ level: '61.8%', ratio: 0.618 },
{ level: '78.6%', ratio: 0.786 },
{ level: '100%', ratio: 1 },
];
return levels.map((l) => ({
level: l.level,
price: Number((high - diff * l.ratio).toFixed(2)),
}));
};
// Stochastic Oscillator
export const calculateStochastic = (
data: PriceData[],
kPeriod: number = 14,
dPeriod: number = 3
): {
k: { time: number; value: number }[];
d: { time: number; value: number }[];
} => {
const kLine: { time: number; value: number }[] = [];
for (let i = kPeriod - 1; i < data.length; i++) {
const slice = data.slice(i - kPeriod + 1, i + 1);
const high = Math.max(...slice.map((d) => d.high));
const low = Math.min(...slice.map((d) => d.low));
const close = data[i].close;
const k = low === high ? 50 : ((close - low) / (high - low)) * 100;
kLine.push({ time: data[i].time, value: Number(k.toFixed(2)) });
}
// Calculate %D (SMA of %K)
const dLine: { time: number; value: number }[] = [];
for (let i = dPeriod - 1; i < kLine.length; i++) {
const sum = kLine.slice(i - dPeriod + 1, i + 1).reduce((s, d) => s + d.value, 0);
dLine.push({ time: kLine[i].time, value: Number((sum / dPeriod).toFixed(2)) });
}
return { k: kLine, d: dLine };
};
// Pivot Points (Standard)
export const calculatePivotPoints = (
high: number,
low: number,
close: number
): {
pivot: number;
r1: number;
r2: number;
r3: number;
s1: number;
s2: number;
s3: number;
} => {
const pivot = (high + low + close) / 3;
const r1 = 2 * pivot - low;
const s1 = 2 * pivot - high;
const r2 = pivot + (high - low);
const s2 = pivot - (high - low);
const r3 = high + 2 * (pivot - low);
const s3 = low - 2 * (high - pivot);
return {
pivot: Number(pivot.toFixed(2)),
r1: Number(r1.toFixed(2)),
r2: Number(r2.toFixed(2)),
r3: Number(r3.toFixed(2)),
s1: Number(s1.toFixed(2)),
s2: Number(s2.toFixed(2)),
s3: Number(s3.toFixed(2)),
};
};
// Volume Weighted Average Price (VWAP)
export const calculateVWAP = (
data: PriceData[]
): { time: number; value: number }[] => {
const result: { time: number; value: number }[] = [];
let cumulativeTPV = 0; // Typical Price * Volume
let cumulativeVolume = 0;
for (let i = 0; i < data.length; i++) {
const typicalPrice = (data[i].high + data[i].low + data[i].close) / 3;
const volume = data[i].volume || 1; // Default to 1 if volume not available
cumulativeTPV += typicalPrice * volume;
cumulativeVolume += volume;
const vwap = cumulativeTPV / cumulativeVolume;
result.push({ time: data[i].time, value: Number(vwap.toFixed(2)) });
}
return result;
};
// Support and Resistance Detection
export const findSupportResistance = (
data: PriceData[],
lookback: number = 20,
threshold: number = 0.02 // 2% threshold
): { support: number[]; resistance: number[] } => {
const support: number[] = [];
const resistance: number[] = [];
for (let i = lookback; i < data.length - lookback; i++) {
const slice = data.slice(i - lookback, i + lookback + 1);
const current = data[i];
// Check if current low is a support (lowest in range)
const isSupport = slice.every((d) => current.low <= d.low * (1 + threshold));
if (isSupport && !support.includes(current.low)) {
support.push(Number(current.low.toFixed(2)));
}
// Check if current high is a resistance (highest in range)
const isResistance = slice.every((d) => current.high >= d.high * (1 - threshold));
if (isResistance && !resistance.includes(current.high)) {
resistance.push(Number(current.high.toFixed(2)));
}
}
// Remove duplicates and sort
return {
support: [...new Set(support)].sort((a, b) => b - a).slice(0, 5),
resistance: [...new Set(resistance)].sort((a, b) => a - b).slice(0, 5),
};
};
// Calculate Win Rate from trades
export const calculateWinRate = (trades: any[]): number => {
if (trades.length === 0) return 0;
const winningTrades = trades.filter((t) => t.pnl && t.pnl > 0).length;
return Number(((winningTrades / trades.length) * 100).toFixed(2));
};
// Calculate Sharpe Ratio (simplified)
export const calculateSharpeRatio = (
returns: number[],
riskFreeRate: number = 0.02
): number => {
if (returns.length === 0) return 0;
const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length;
const stdDev = Math.sqrt(variance);
if (stdDev === 0) return 0;
const excessReturn = avgReturn - riskFreeRate / 252; // Daily risk-free rate
return Number((excessReturn / stdDev).toFixed(2));
};
// Calculate Maximum Drawdown
export const calculateMaxDrawdown = (equity: number[]): number => {
if (equity.length === 0) return 0;
let maxDrawdown = 0;
let peak = equity[0];
for (const value of equity) {
if (value > peak) {
peak = value;
}
const drawdown = ((peak - value) / peak) * 100;
if (drawdown > maxDrawdown) {
maxDrawdown = drawdown;
}
}
return Number(maxDrawdown.toFixed(2));
};
// Position Sizing (Kelly Criterion)
export const calculatePositionSize = (
capital: number,
winRate: number,
avgWin: number,
avgLoss: number
): number => {
if (avgLoss === 0) return 0;
const winLossRatio = Math.abs(avgWin / avgLoss);
const kellyPercent = (winRate - (1 - winRate) / winLossRatio) * 100;
// Use half-Kelly for safety
const safeKelly = kellyPercent / 2;
// Cap at 10% of capital
const maxPercent = Math.min(Math.max(safeKelly, 0), 10);
return Number((capital * (maxPercent / 100)).toFixed(2));
};
+10
View File
@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_ALPHA_VANTAGE_API_KEY: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+18
View File
@@ -0,0 +1,18 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
'dark-bg': '#0b1220',
'dark-surface': '#121a2b',
'dark-border': '#1f2a3a',
'gold-500': '#d4af37',
},
},
},
plugins: [],
}
+33
View File
@@ -0,0 +1,33 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
border: '#2a3142',
dark: {
bg: '#0a0e1a',
surface: '#1a1f2e',
border: '#2a3142',
hover: '#242938',
},
gold: {
500: '#FFD700',
600: '#FFC700',
},
green: {
500: '#10b981',
600: '#059669',
},
red: {
500: '#ef4444',
600: '#dc2626',
},
},
},
},
plugins: [],
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})