Karan6124 commited on
Commit
11e8872
·
1 Parent(s): 2ab5afb

feat: add candlestick charts, technical indicators, live toasts, and AI personalization

Browse files

- schema.py: Support optional trading_style and risk_tolerance variables in get_ai_insight mutation, and tailor Gemini agent prompt
- StockChart.tsx: Implement calculations and toggle controls for SMA (20), EMA (20), RSI (14) indicator sub-panel, and Line/Candlestick chart representations
- AIAnalyst.tsx: Add UI selectors for Trading Style and Risk Profile parameters
- App.tsx: Add live glassmorphic Toast notification system, check price alerts in WebSocket ticks loop, and format open/high/low/close candlestick data points
- App.css: Style toast stack containers, control buttons, overlays, and keyframes animations

backend/app/graphql/schema.py CHANGED
@@ -457,7 +457,13 @@ class Mutation:
457
  return PaymentOrderType(order_id= order_id, amount= amount * 100, currency= "INR")
458
 
459
  @strawberry.mutation
460
- async def get_ai_insight(self, info: Info, ticker: str) -> GeminiInsightType:
 
 
 
 
 
 
461
  """
462
  Deducts 1 credit, executes the local ONNX ML prediction, and calls
463
  the Gemini ReAct agent loop to return market insights.
@@ -471,8 +477,12 @@ class Mutation:
471
  raise Exception("Insufficient credits. Please recharge via Razorpay.")
472
 
473
  # 2. Call the Gemini ReAct agent loop
 
 
474
  prompt = (
475
  f"Analyze stock ticker {ticker.upper()}. "
 
 
476
  "Retrieve its latest technical indicators and quantitative ML prediction using your tools. "
477
  "Check if the user has any active price alerts or watchlists set up for it. "
478
  "Perform a holistic qualitative and quantitative analysis of this stock."
 
457
  return PaymentOrderType(order_id= order_id, amount= amount * 100, currency= "INR")
458
 
459
  @strawberry.mutation
460
+ async def get_ai_insight(
461
+ self,
462
+ info: Info,
463
+ ticker: str,
464
+ trading_style: Optional[str] = None,
465
+ risk_tolerance: Optional[str] = None
466
+ ) -> GeminiInsightType:
467
  """
468
  Deducts 1 credit, executes the local ONNX ML prediction, and calls
469
  the Gemini ReAct agent loop to return market insights.
 
477
  raise Exception("Insufficient credits. Please recharge via Razorpay.")
478
 
479
  # 2. Call the Gemini ReAct agent loop
480
+ style_label = (trading_style or "swing_trading").replace("_", " ")
481
+ risk_label = risk_tolerance or "moderate"
482
  prompt = (
483
  f"Analyze stock ticker {ticker.upper()}. "
484
+ f"The user's trading style is {style_label} and their risk profile is {risk_label}. "
485
+ "Tailor your analysis, support/resistance levels, entry/exit targets, and strategy report to match this style and risk profile. "
486
  "Retrieve its latest technical indicators and quantitative ML prediction using your tools. "
487
  "Check if the user has any active price alerts or watchlists set up for it. "
488
  "Perform a holistic qualitative and quantitative analysis of this stock."
frontend/src/App.css CHANGED
@@ -1795,3 +1795,87 @@
1795
  .pricing-full-card.lifetime-deal-card {
1796
  animation: pulse-cyan-glow 3s infinite ease-in-out;
1797
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1795
  .pricing-full-card.lifetime-deal-card {
1796
  animation: pulse-cyan-glow 3s infinite ease-in-out;
1797
  }
1798
+
1799
+ /* Toast Notification System Styles */
1800
+ .toast-container {
1801
+ position: fixed;
1802
+ top: 24px;
1803
+ right: 24px;
1804
+ z-index: 9999;
1805
+ display: flex;
1806
+ flex-direction: column;
1807
+ gap: 12px;
1808
+ pointer-events: none;
1809
+ }
1810
+
1811
+ .toast-notification {
1812
+ pointer-events: auto;
1813
+ min-width: 320px;
1814
+ max-width: 420px;
1815
+ display: flex;
1816
+ align-items: center;
1817
+ justify-content: space-between;
1818
+ gap: 14px;
1819
+ padding: 14px 18px;
1820
+ border-radius: 12px;
1821
+ font-family: inherit;
1822
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
1823
+ animation: slide-in-toast 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1824
+ backdrop-filter: blur(16px);
1825
+ -webkit-backdrop-filter: blur(16px);
1826
+ }
1827
+
1828
+ .toast-success {
1829
+ background: rgba(16, 185, 129, 0.08);
1830
+ border: 1px solid rgba(16, 185, 129, 0.3);
1831
+ color: #10b981;
1832
+ }
1833
+
1834
+ .toast-alert {
1835
+ background: rgba(239, 68, 68, 0.08);
1836
+ border: 1px solid rgba(239, 68, 68, 0.3);
1837
+ color: #ef4444;
1838
+ animation: slide-in-toast 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards, shake-toast 0.4s ease-in-out 0.3s;
1839
+ }
1840
+
1841
+ @keyframes slide-in-toast {
1842
+ from {
1843
+ transform: translateX(120%) scale(0.9);
1844
+ opacity: 0;
1845
+ }
1846
+ to {
1847
+ transform: translateX(0) scale(1);
1848
+ opacity: 1;
1849
+ }
1850
+ }
1851
+
1852
+ @keyframes shake-toast {
1853
+ 0%, 100% { transform: translateX(0); }
1854
+ 25% { transform: translateX(-4px); }
1855
+ 75% { transform: translateX(4px); }
1856
+ }
1857
+
1858
+ /* Indicators Controls Styles */
1859
+ .control-toggle-btn {
1860
+ background: none;
1861
+ border: 1px solid var(--border-glass) !important;
1862
+ color: var(--text-secondary);
1863
+ border-radius: 6px;
1864
+ cursor: pointer;
1865
+ display: flex;
1866
+ align-items: center;
1867
+ gap: 6px;
1868
+ font-size: 12px;
1869
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1870
+ outline: none;
1871
+ }
1872
+
1873
+ .control-toggle-btn:hover {
1874
+ background: rgba(255, 255, 255, 0.03) !important;
1875
+ border-color: rgba(255, 255, 255, 0.1) !important;
1876
+ color: var(--text-primary) !important;
1877
+ }
1878
+
1879
+ .control-toggle-btn.active:hover {
1880
+ opacity: 0.9;
1881
+ }
frontend/src/App.tsx CHANGED
@@ -1,7 +1,8 @@
1
- import { useState, useEffect } from 'react';
2
  import LandingPage from './pages/LandingPage';
3
  import Dashboard from './pages/Dashboard';
4
  import UpgradePage from './pages/UpgradePage';
 
5
  import './App.css';
6
 
7
  // Declare global types for external scripts
@@ -56,6 +57,20 @@ export default function App() {
56
  const [alerts, setAlerts] = useState<any[]>([]);
57
  const [chartRange, setChartRange] = useState<string>('1d');
58
  const [indices, setIndices] = useState<any[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  // AI Insights State
61
  const [insight, setInsight] = useState<any>(null);
@@ -224,9 +239,18 @@ export default function App() {
224
  } else {
225
  timeLabel = dateObj.toLocaleDateString([], { month: 'short', day: 'numeric' });
226
  }
 
 
 
 
227
  return {
228
  time: timeLabel,
229
- price: parseFloat(h.close.toFixed(2)),
 
 
 
 
 
230
  };
231
  });
232
  setChartData(formatted);
@@ -287,20 +311,60 @@ export default function App() {
287
  if (msg.type === 'next' && msg.id === 'stock-tick-sub') {
288
  const tick = msg.payload.data?.streamStockTicks;
289
  if (!tick) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  const tickTime = new Date(tick.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
292
 
293
  setChartData((prevData) => {
294
- // If we already have the timestamp, update price. Else append.
295
  const updated = [...prevData];
296
  const last = updated[updated.length - 1];
297
  const newPrice = parseFloat(tick.price.toFixed(2));
298
 
299
  if (last && last.time === tickTime) {
300
- updated[updated.length - 1] = { time: tickTime, price: newPrice };
 
 
 
 
 
 
 
 
 
 
 
 
301
  return updated;
302
  } else {
303
- const nextList = [...updated, { time: tickTime, price: newPrice }];
 
 
 
 
 
 
 
 
 
 
 
304
  // Cap chart view to last 35 data points
305
  if (nextList.length > 35) nextList.shift();
306
  return nextList;
@@ -551,31 +615,51 @@ export default function App() {
551
  }
552
 
553
  return (
554
- <Dashboard
555
- user={user}
556
- watchlist={watchlist}
557
- watchlistQuotes={watchlistQuotes}
558
- activeTicker={activeTicker}
559
- chartData={chartData}
560
- activeStats={activeStats}
561
- alerts={alerts}
562
- insight={insight}
563
- savedStrategies={savedStrategies}
564
- loadingInsight={loadingInsight}
565
- insightError={insightError}
566
- chartRange={chartRange}
567
- onRangeChange={setChartRange}
568
- onSelectTicker={setActiveTicker}
569
- onAddTicker={handleAddWatchlist}
570
- onRemoveTicker={handleRemoveWatchlist}
571
- onCreateAlert={handleCreateAlert}
572
- onDeactivateAlert={handleDeactivateAlert}
573
- onTriggerInsight={triggerAIInsight}
574
- onOpenRecharge={() => setCurrentView('upgrade')}
575
- onLogout={handleLogout}
576
- onLogoClick={() => setCurrentView('landing')}
577
- onAvatarUpload={(newUrl) => setUser((prev: any) => prev ? { ...prev, pictureUrl: newUrl } : null)}
578
- indices={indices}
579
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  );
581
  }
 
1
+ import { useState, useEffect, useRef } from 'react';
2
  import LandingPage from './pages/LandingPage';
3
  import Dashboard from './pages/Dashboard';
4
  import UpgradePage from './pages/UpgradePage';
5
+ import { X } from 'lucide-react';
6
  import './App.css';
7
 
8
  // Declare global types for external scripts
 
57
  const [alerts, setAlerts] = useState<any[]>([]);
58
  const [chartRange, setChartRange] = useState<string>('1d');
59
  const [indices, setIndices] = useState<any[]>([]);
60
+ const [toasts, setToasts] = useState<Array<{ id: string; message: string; type: 'success' | 'alert' }>>([]);
61
+
62
+ const alertsRef = useRef<any[]>([]);
63
+ useEffect(() => {
64
+ alertsRef.current = alerts;
65
+ }, [alerts]);
66
+
67
+ const addToast = (message: string, type: 'success' | 'alert' = 'success') => {
68
+ const id = Math.random().toString(36).substring(2, 9);
69
+ setToasts(prev => [...prev, { id, message, type }]);
70
+ setTimeout(() => {
71
+ setToasts(prev => prev.filter(t => t.id !== id));
72
+ }, 6000);
73
+ };
74
 
75
  // AI Insights State
76
  const [insight, setInsight] = useState<any>(null);
 
239
  } else {
240
  timeLabel = dateObj.toLocaleDateString([], { month: 'short', day: 'numeric' });
241
  }
242
+ const closeVal = parseFloat(h.close.toFixed(2));
243
+ const openVal = parseFloat((h.open !== null && h.open !== undefined ? h.open : h.close).toFixed(2));
244
+ const highVal = parseFloat((h.high !== null && h.high !== undefined ? h.high : h.close).toFixed(2));
245
+ const lowVal = parseFloat((h.low !== null && h.low !== undefined ? h.low : h.close).toFixed(2));
246
  return {
247
  time: timeLabel,
248
+ price: closeVal,
249
+ open: openVal,
250
+ high: highVal,
251
+ low: lowVal,
252
+ close: closeVal,
253
+ range: [openVal, closeVal],
254
  };
255
  });
256
  setChartData(formatted);
 
311
  if (msg.type === 'next' && msg.id === 'stock-tick-sub') {
312
  const tick = msg.payload.data?.streamStockTicks;
313
  if (!tick) return;
314
+
315
+ // Evaluate active alerts for this ticker
316
+ const currentPrice = tick.price;
317
+ alertsRef.current.forEach((alert) => {
318
+ if (alert.ticker === tick.ticker && alert.isActive) {
319
+ const isAboveTriggered = alert.condition === 'above' && currentPrice >= alert.targetPrice;
320
+ const isBelowTriggered = alert.condition === 'below' && currentPrice <= alert.targetPrice;
321
+
322
+ if (isAboveTriggered || isBelowTriggered) {
323
+ addToast(
324
+ `🔔 Alert Triggered: ${tick.ticker} crossed ${alert.condition.toUpperCase()} $${alert.targetPrice.toFixed(2)} (Current: $${currentPrice.toFixed(2)})`,
325
+ 'alert'
326
+ );
327
+ // Call API to deactivate
328
+ handleDeactivateAlert(alert.id);
329
+ }
330
+ }
331
+ });
332
 
333
  const tickTime = new Date(tick.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
334
 
335
  setChartData((prevData) => {
 
336
  const updated = [...prevData];
337
  const last = updated[updated.length - 1];
338
  const newPrice = parseFloat(tick.price.toFixed(2));
339
 
340
  if (last && last.time === tickTime) {
341
+ const openVal = last.open !== undefined ? last.open : newPrice;
342
+ const highVal = Math.max(last.high !== undefined ? last.high : newPrice, newPrice);
343
+ const lowVal = Math.min(last.low !== undefined ? last.low : newPrice, newPrice);
344
+
345
+ updated[updated.length - 1] = {
346
+ time: tickTime,
347
+ price: newPrice,
348
+ open: openVal,
349
+ high: highVal,
350
+ low: lowVal,
351
+ close: newPrice,
352
+ range: [openVal, newPrice]
353
+ };
354
  return updated;
355
  } else {
356
+ const nextList = [
357
+ ...updated,
358
+ {
359
+ time: tickTime,
360
+ price: newPrice,
361
+ open: newPrice,
362
+ high: newPrice,
363
+ low: newPrice,
364
+ close: newPrice,
365
+ range: [newPrice, newPrice]
366
+ }
367
+ ];
368
  // Cap chart view to last 35 data points
369
  if (nextList.length > 35) nextList.shift();
370
  return nextList;
 
615
  }
616
 
617
  return (
618
+ <>
619
+ <Dashboard
620
+ user={user}
621
+ watchlist={watchlist}
622
+ watchlistQuotes={watchlistQuotes}
623
+ activeTicker={activeTicker}
624
+ chartData={chartData}
625
+ activeStats={activeStats}
626
+ alerts={alerts}
627
+ insight={insight}
628
+ savedStrategies={savedStrategies}
629
+ loadingInsight={loadingInsight}
630
+ insightError={insightError}
631
+ chartRange={chartRange}
632
+ onRangeChange={setChartRange}
633
+ onSelectTicker={setActiveTicker}
634
+ onAddTicker={handleAddWatchlist}
635
+ onRemoveTicker={handleRemoveWatchlist}
636
+ onCreateAlert={handleCreateAlert}
637
+ onDeactivateAlert={handleDeactivateAlert}
638
+ onTriggerInsight={triggerAIInsight}
639
+ onOpenRecharge={() => setCurrentView('upgrade')}
640
+ onLogout={handleLogout}
641
+ onLogoClick={() => setCurrentView('landing')}
642
+ onAvatarUpload={(newUrl) => setUser((prev: any) => prev ? { ...prev, pictureUrl: newUrl } : null)}
643
+ indices={indices}
644
+ />
645
+
646
+ {/* Toast Notification Stack */}
647
+ <div className="toast-container">
648
+ {toasts.map((toast) => (
649
+ <div
650
+ key={toast.id}
651
+ className={`toast-notification ${toast.type === 'alert' ? 'toast-alert' : 'toast-success'}`}
652
+ >
653
+ <span style={{ fontSize: '13px', fontWeight: 600, flex: 1, textAlign: 'left' }}>{toast.message}</span>
654
+ <button
655
+ onClick={() => setToasts(prev => prev.filter(t => t.id !== toast.id))}
656
+ style={{ background: 'none', border: 'none', color: 'rgba(255,255,255,0.6)', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: '2px', outline: 'none' }}
657
+ >
658
+ <X size={14} />
659
+ </button>
660
+ </div>
661
+ ))}
662
+ </div>
663
+ </>
664
  );
665
  }
frontend/src/components/AIAnalyst.tsx CHANGED
@@ -14,7 +14,7 @@ interface AIAnalystProps {
14
  savedStrategies: any[];
15
  loadingInsight: boolean;
16
  insightError: string | null;
17
- onTriggerInsight: () => void;
18
  }
19
 
20
  export default function AIAnalyst({
@@ -27,6 +27,8 @@ export default function AIAnalyst({
27
  }: AIAnalystProps) {
28
  const [activeTab, setActiveTab] = useState<'analysis' | 'history'>('analysis');
29
  const [selectedHistoryItem, setSelectedHistoryItem] = useState<any | null>(null);
 
 
30
 
31
  // Custom parser to format **bold** words in neon-cyan and support paragraph newlines
32
  const formatReason = (text: string) => {
@@ -129,11 +131,40 @@ export default function AIAnalyst({
129
  {activeTab === 'analysis' ? (
130
  <>
131
  {!insight && !loadingInsight && (
132
- <div className="insight-cta">
133
- <p className="insight-cta-description">
134
  Analyze indicators, watchlists, alerts, and model prediction in a single ReAct workflow.
135
  </p>
136
- <button className="insight-btn" onClick={onTriggerInsight}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  Generate strategy for {activeTicker}
138
  </button>
139
  </div>
 
14
  savedStrategies: any[];
15
  loadingInsight: boolean;
16
  insightError: string | null;
17
+ onTriggerInsight: (tradingStyle: string, riskTolerance: string) => void;
18
  }
19
 
20
  export default function AIAnalyst({
 
27
  }: AIAnalystProps) {
28
  const [activeTab, setActiveTab] = useState<'analysis' | 'history'>('analysis');
29
  const [selectedHistoryItem, setSelectedHistoryItem] = useState<any | null>(null);
30
+ const [tradingStyle, setTradingStyle] = useState('swing_trading');
31
+ const [riskTolerance, setRiskTolerance] = useState('moderate');
32
 
33
  // Custom parser to format **bold** words in neon-cyan and support paragraph newlines
34
  const formatReason = (text: string) => {
 
131
  {activeTab === 'analysis' ? (
132
  <>
133
  {!insight && !loadingInsight && (
134
+ <div className="insight-cta" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px' }}>
135
+ <p className="insight-cta-description" style={{ margin: 0 }}>
136
  Analyze indicators, watchlists, alerts, and model prediction in a single ReAct workflow.
137
  </p>
138
+
139
+ {/* Style & Risk selectors */}
140
+ <div className="ai-param-selectors" style={{ display: 'flex', gap: '16px', justifyContent: 'center', width: '100%', maxWidth: '400px' }}>
141
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '6px', textAlign: 'left' }}>
142
+ <label style={{ fontSize: '11px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Trading Style</label>
143
+ <select
144
+ value={tradingStyle}
145
+ onChange={(e) => setTradingStyle(e.target.value)}
146
+ style={{ width: '100%', padding: '10px 14px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '10px', color: 'var(--text-primary)', outline: 'none', cursor: 'pointer', fontSize: '12px', transition: 'all 0.2s ease' }}
147
+ >
148
+ <option value="day_trading">Day Trading</option>
149
+ <option value="swing_trading">Swing Trading</option>
150
+ <option value="investing">Long-term Investing</option>
151
+ </select>
152
+ </div>
153
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '6px', textAlign: 'left' }}>
154
+ <label style={{ fontSize: '11px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Risk Profile</label>
155
+ <select
156
+ value={riskTolerance}
157
+ onChange={(e) => setRiskTolerance(e.target.value)}
158
+ style={{ width: '100%', padding: '10px 14px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '10px', color: 'var(--text-primary)', outline: 'none', cursor: 'pointer', fontSize: '12px', transition: 'all 0.2s ease' }}
159
+ >
160
+ <option value="conservative">Conservative</option>
161
+ <option value="moderate">Moderate</option>
162
+ <option value="aggressive">Aggressive</option>
163
+ </select>
164
+ </div>
165
+ </div>
166
+
167
+ <button className="insight-btn" onClick={() => onTriggerInsight(tradingStyle, riskTolerance)} style={{ marginTop: '4px' }}>
168
  Generate strategy for {activeTicker}
169
  </button>
170
  </div>
frontend/src/components/StockChart.tsx CHANGED
@@ -1,9 +1,15 @@
1
-
2
- import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip } from 'recharts';
 
3
 
4
  interface ChartDataPoint {
5
  time: string;
6
  price: number;
 
 
 
 
 
7
  }
8
 
9
  interface StockChartProps {
@@ -14,9 +20,59 @@ interface StockChartProps {
14
  onRangeChange: (range: string) => void;
15
  }
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  export default function StockChart({ activeTicker, chartData, activeStats, chartRange, onRangeChange }: StockChartProps) {
18
  const currentPrice = chartData.length > 0 ? chartData[chartData.length - 1].price : null;
19
 
 
 
 
 
 
 
20
  const ranges = [
21
  { label: '1D', value: '1d' },
22
  { label: '5D', value: '5d' },
@@ -28,6 +84,69 @@ export default function StockChart({ activeTicker, chartData, activeStats, chart
28
  { label: 'MAX', value: 'max' }
29
  ];
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  return (
32
  <div className="glass-panel chart-panel">
33
  <div className="chart-header">
@@ -88,46 +207,140 @@ export default function StockChart({ activeTicker, chartData, activeStats, chart
88
  </div>
89
  )}
90
 
91
- <div className="chart-container">
92
- {chartData.length > 0 ? (
93
- <ResponsiveContainer width="100%" height="100%">
94
- <AreaChart data={chartData} margin={{ top: 10, right: 5, left: -20, bottom: 0 }}>
95
- <defs>
96
- <linearGradient id="chartGlow" x1="0" y1="0" x2="0" y2="1">
97
- <stop offset="5%" stopColor="#00f2fe" stopOpacity={0.2}/>
98
- <stop offset="95%" stopColor="#00f2fe" stopOpacity={0.0}/>
99
- </linearGradient>
100
- </defs>
101
- <XAxis
102
- dataKey="time"
103
- stroke="#475569"
104
- fontSize={11}
105
- tickLine={false}
106
- />
107
- <YAxis
108
- stroke="#475569"
109
- fontSize={11}
110
- domain={['auto', 'auto']}
111
- tickLine={false}
112
- />
113
- <Tooltip
114
- contentStyle={{
115
- background: '#0d101b',
116
- borderColor: '#2e303a',
117
- borderRadius: '8px',
118
- color: '#fff'
119
- }}
120
- />
121
- <Area
122
- type="monotone"
123
- dataKey="price"
124
- stroke="#00f2fe"
125
- strokeWidth={2}
126
- fillOpacity={1}
127
- fill="url(#chartGlow)"
128
- />
129
- </AreaChart>
130
- </ResponsiveContainer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  ) : (
132
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: '8px', padding: '20px', textAlign: 'center' }}>
133
  <span style={{ color: 'var(--text-secondary)', fontSize: '13px', fontWeight: 600 }}>
@@ -141,6 +354,45 @@ export default function StockChart({ activeTicker, chartData, activeStats, chart
141
  </span>
142
  </div>
143
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
  </div>
146
  );
 
1
+ import { useState } from 'react';
2
+ import { ResponsiveContainer, ComposedChart, Area, Bar, Line, XAxis, YAxis, Tooltip, ReferenceLine, LineChart } from 'recharts';
3
+ import { AreaChart as AreaIcon, BarChart2 as CandleIcon, Activity, Eye, EyeOff } from 'lucide-react';
4
 
5
  interface ChartDataPoint {
6
  time: string;
7
  price: number;
8
+ open?: number;
9
+ high?: number;
10
+ low?: number;
11
+ close?: number;
12
+ range?: [number, number];
13
  }
14
 
15
  interface StockChartProps {
 
20
  onRangeChange: (range: string) => void;
21
  }
22
 
23
+ // Custom SVG component to draw wicks and body for candlestick bars
24
+ const CandlestickBar = (props: any) => {
25
+ const { x, y, width, height, open, close, high, low } = props;
26
+ if (open === undefined || close === undefined || high === undefined || low === undefined) return null;
27
+
28
+ const isUp = close >= open;
29
+ const color = isUp ? '#10b981' : '#ef4444'; // green or red
30
+
31
+ const bodyHigh = Math.max(open, close);
32
+ const bodyLow = Math.min(open, close);
33
+
34
+ const bodyHeight = Math.abs(open - close);
35
+ const scale = bodyHeight > 0 ? height / bodyHeight : 1;
36
+
37
+ const yHigh = y - (high - bodyHigh) * scale;
38
+ const yLow = (y + height) + (bodyLow - low) * scale;
39
+
40
+ const cx = x + width / 2;
41
+
42
+ return (
43
+ <g>
44
+ {/* Wick / Shadow */}
45
+ <line
46
+ x1={cx}
47
+ y1={yHigh}
48
+ x2={cx}
49
+ y2={yLow}
50
+ stroke={color}
51
+ strokeWidth={1.5}
52
+ />
53
+ {/* Candle Body */}
54
+ <rect
55
+ x={x}
56
+ y={y}
57
+ width={width}
58
+ height={Math.max(height, 2)} // Ensure at least 2px height for flat candles
59
+ fill={color}
60
+ stroke={color}
61
+ strokeWidth={1}
62
+ />
63
+ </g>
64
+ );
65
+ };
66
+
67
  export default function StockChart({ activeTicker, chartData, activeStats, chartRange, onRangeChange }: StockChartProps) {
68
  const currentPrice = chartData.length > 0 ? chartData[chartData.length - 1].price : null;
69
 
70
+ // Chart States
71
+ const [chartType, setChartType] = useState<'line' | 'candle'>('line');
72
+ const [showSMA, setShowSMA] = useState<boolean>(false);
73
+ const [showEMA, setShowEMA] = useState<boolean>(false);
74
+ const [showRSI, setShowRSI] = useState<boolean>(false);
75
+
76
  const ranges = [
77
  { label: '1D', value: '1d' },
78
  { label: '5D', value: '5d' },
 
84
  { label: 'MAX', value: 'max' }
85
  ];
86
 
87
+ // Helper: compute SMA 20
88
+ const computeSMA = (data: any[], period: number = 20) => {
89
+ return data.map((d, i) => {
90
+ if (i < period - 1) return { ...d, sma: null };
91
+ const slice = data.slice(i - period + 1, i + 1);
92
+ const sum = slice.reduce((acc, curr) => acc + curr.price, 0);
93
+ return { ...d, sma: parseFloat((sum / period).toFixed(2)) };
94
+ });
95
+ };
96
+
97
+ // Helper: compute EMA 20
98
+ const computeEMA = (data: any[], period: number = 20) => {
99
+ const k = 2 / (period + 1);
100
+ let prevEma = data.length > 0 ? data[0].price : 0;
101
+ return data.map((d, i) => {
102
+ if (i === 0) return { ...d, ema: prevEma };
103
+ const ema = d.price * k + prevEma * (1 - k);
104
+ prevEma = ema;
105
+ return { ...d, ema: parseFloat(ema.toFixed(2)) };
106
+ });
107
+ };
108
+
109
+ // Helper: compute RSI 14
110
+ const computeRSI = (data: any[], period: number = 14) => {
111
+ if (data.length === 0) return [];
112
+
113
+ let gains = 0;
114
+ let losses = 0;
115
+
116
+ // Initial gains/losses
117
+ for (let i = 1; i <= period && i < data.length; i++) {
118
+ const diff = data[i].price - data[i - 1].price;
119
+ if (diff > 0) gains += diff;
120
+ else losses -= diff;
121
+ }
122
+
123
+ let avgGain = gains / period;
124
+ let avgLoss = losses / period;
125
+
126
+ return data.map((d, i) => {
127
+ if (i <= period) return { ...d, rsi: 50 }; // default neutral
128
+
129
+ const diff = d.price - data[i - 1].price;
130
+ const gain = diff > 0 ? diff : 0;
131
+ const loss = diff < 0 ? -diff : 0;
132
+
133
+ avgGain = (avgGain * (period - 1) + gain) / period;
134
+ avgLoss = (avgLoss * (period - 1) + loss) / period;
135
+
136
+ if (avgLoss === 0) return { ...d, rsi: 100 };
137
+ const rs = avgGain / avgLoss;
138
+ const rsi = 100 - 100 / (1 + rs);
139
+ return { ...d, rsi: parseFloat(rsi.toFixed(2)) };
140
+ });
141
+ };
142
+
143
+ // Pipeline processed chart data
144
+ let processedData = [...chartData];
145
+ if (showSMA) processedData = computeSMA(processedData, 20);
146
+ if (showEMA) processedData = computeEMA(processedData, 20);
147
+
148
+ const rsiData = showRSI ? computeRSI(chartData, 14) : [];
149
+
150
  return (
151
  <div className="glass-panel chart-panel">
152
  <div className="chart-header">
 
207
  </div>
208
  )}
209
 
210
+ {/* Technical Indicator Controls row */}
211
+ <div className="chart-indicator-controls" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 16px', background: 'rgba(255,255,255,0.01)', borderBottom: '1px solid var(--border-glass)', gap: '12px', flexWrap: 'wrap' }}>
212
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
213
+ <span style={{ fontSize: '11px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginRight: '6px' }}>Chart Type</span>
214
+ <button
215
+ onClick={() => setChartType('line')}
216
+ className={`control-toggle-btn ${chartType === 'line' ? 'active' : ''}`}
217
+ title="Line / Area View"
218
+ style={{ padding: '6px 12px', background: chartType === 'line' ? 'rgba(0, 242, 254, 0.1)' : 'none', border: '1px solid var(--border-glass)', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', color: chartType === 'line' ? 'var(--neon-cyan)' : 'var(--text-secondary)', fontSize: '12px', transition: 'all 0.2s ease', outline: 'none' }}
219
+ >
220
+ <AreaIcon size={14} />
221
+ Line
222
+ </button>
223
+ <button
224
+ onClick={() => setChartType('candle')}
225
+ className={`control-toggle-btn ${chartType === 'candle' ? 'active' : ''}`}
226
+ title="Candlestick View"
227
+ style={{ padding: '6px 12px', background: chartType === 'candle' ? 'rgba(16, 185, 129, 0.1)' : 'none', border: '1px solid var(--border-glass)', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', color: chartType === 'candle' ? '#10b981' : 'var(--text-secondary)', fontSize: '12px', transition: 'all 0.2s ease', outline: 'none' }}
228
+ >
229
+ <CandleIcon size={14} />
230
+ Candle
231
+ </button>
232
+ </div>
233
+
234
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
235
+ <span style={{ fontSize: '11px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginRight: '6px' }}>Technical Overlays</span>
236
+ <button
237
+ onClick={() => setShowSMA(!showSMA)}
238
+ className={`control-toggle-btn ${showSMA ? 'active' : ''}`}
239
+ style={{ padding: '6px 12px', background: showSMA ? 'rgba(161, 84, 255, 0.1)' : 'none', border: '1px solid var(--border-glass)', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', color: showSMA ? 'var(--neon-violet)' : 'var(--text-secondary)', fontSize: '12px', transition: 'all 0.2s ease', outline: 'none' }}
240
+ >
241
+ {showSMA ? <Eye size={13} /> : <EyeOff size={13} />}
242
+ SMA 20
243
+ </button>
244
+ <button
245
+ onClick={() => setShowEMA(!showEMA)}
246
+ className={`control-toggle-btn ${showEMA ? 'active' : ''}`}
247
+ style={{ padding: '6px 12px', background: showEMA ? 'rgba(251, 146, 60, 0.1)' : 'none', border: '1px solid var(--border-glass)', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', color: showEMA ? '#fb923c' : 'var(--text-secondary)', fontSize: '12px', transition: 'all 0.2s ease', outline: 'none' }}
248
+ >
249
+ {showEMA ? <Eye size={13} /> : <EyeOff size={13} />}
250
+ EMA 20
251
+ </button>
252
+ <button
253
+ onClick={() => setShowRSI(!showRSI)}
254
+ className={`control-toggle-btn ${showRSI ? 'active' : ''}`}
255
+ style={{ padding: '6px 12px', background: showRSI ? 'rgba(0, 242, 254, 0.1)' : 'none', border: '1px solid var(--border-glass)', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', color: showRSI ? 'var(--neon-cyan)' : 'var(--text-secondary)', fontSize: '12px', transition: 'all 0.2s ease', outline: 'none' }}
256
+ >
257
+ <Activity size={13} />
258
+ RSI 14
259
+ </button>
260
+ </div>
261
+ </div>
262
+
263
+ {/* Main Chart Area */}
264
+ <div className="chart-container" style={{ display: 'flex', flexDirection: 'column', gap: '16px', height: showRSI ? 'calc(100% - 190px)' : 'calc(100% - 70px)', padding: '16px' }}>
265
+ {processedData.length > 0 ? (
266
+ <div style={{ flex: 1, width: '100%', minHeight: '220px' }}>
267
+ <ResponsiveContainer width="100%" height="100%">
268
+ <ComposedChart data={processedData} margin={{ top: 10, right: 5, left: -20, bottom: 0 }}>
269
+ <defs>
270
+ <linearGradient id="chartGlow" x1="0" y1="0" x2="0" y2="1">
271
+ <stop offset="5%" stopColor="#00f2fe" stopOpacity={0.2}/>
272
+ <stop offset="95%" stopColor="#00f2fe" stopOpacity={0.0}/>
273
+ </linearGradient>
274
+ </defs>
275
+
276
+ <XAxis
277
+ dataKey="time"
278
+ stroke="#475569"
279
+ fontSize={11}
280
+ tickLine={false}
281
+ />
282
+
283
+ <YAxis
284
+ stroke="#475569"
285
+ fontSize={11}
286
+ domain={['auto', 'auto']}
287
+ tickLine={false}
288
+ />
289
+
290
+ <Tooltip
291
+ contentStyle={{
292
+ background: '#0d101b',
293
+ borderColor: '#2e303a',
294
+ borderRadius: '8px',
295
+ color: '#fff',
296
+ fontSize: '12px'
297
+ }}
298
+ />
299
+
300
+ {/* Primary Chart Type Representation */}
301
+ {chartType === 'line' ? (
302
+ <Area
303
+ type="monotone"
304
+ dataKey="price"
305
+ stroke="#00f2fe"
306
+ strokeWidth={2}
307
+ fillOpacity={1}
308
+ fill="url(#chartGlow)"
309
+ name="Price"
310
+ />
311
+ ) : (
312
+ <Bar
313
+ dataKey="range"
314
+ shape={<CandlestickBar />}
315
+ name="Candlestick"
316
+ />
317
+ )}
318
+
319
+ {/* Moving Average Indicators */}
320
+ {showSMA && (
321
+ <Line
322
+ type="monotone"
323
+ dataKey="sma"
324
+ stroke="var(--neon-violet)"
325
+ strokeWidth={1.5}
326
+ strokeDasharray="4 4"
327
+ dot={false}
328
+ name="SMA 20"
329
+ />
330
+ )}
331
+ {showEMA && (
332
+ <Line
333
+ type="monotone"
334
+ dataKey="ema"
335
+ stroke="#fb923c" // Orange
336
+ strokeWidth={1.5}
337
+ dot={false}
338
+ name="EMA 20"
339
+ />
340
+ )}
341
+ </ComposedChart>
342
+ </ResponsiveContainer>
343
+ </div>
344
  ) : (
345
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: '8px', padding: '20px', textAlign: 'center' }}>
346
  <span style={{ color: 'var(--text-secondary)', fontSize: '13px', fontWeight: 600 }}>
 
354
  </span>
355
  </div>
356
  )}
357
+
358
+ {/* Technical Indicator panel: RSI 14 (Auxiliary Chart below main price chart) */}
359
+ {showRSI && rsiData.length > 0 && (
360
+ <div style={{ height: '100px', width: '100%', borderTop: '1px dashed var(--border-glass)', paddingTop: '10px' }}>
361
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '4px', paddingLeft: '10px' }}>
362
+ <span style={{ fontWeight: 700, color: 'var(--neon-cyan)' }}>RSI (14)</span>
363
+ <span style={{ color: 'var(--text-muted)' }}>Overbought &gt;70 | Oversold &lt;30</span>
364
+ </div>
365
+ <ResponsiveContainer width="100%" height="100%">
366
+ <LineChart data={rsiData} margin={{ top: 5, right: 5, left: -20, bottom: 5 }}>
367
+ <XAxis dataKey="time" hide />
368
+ <YAxis stroke="#475569" fontSize={9} domain={[0, 100]} tickLine={false} ticks={[30, 50, 70]} />
369
+ <Tooltip
370
+ contentStyle={{
371
+ background: '#0d101b',
372
+ borderColor: '#2e303a',
373
+ borderRadius: '8px',
374
+ color: '#fff',
375
+ fontSize: '11px'
376
+ }}
377
+ />
378
+
379
+ {/* Baseline references */}
380
+ <ReferenceLine y={70} stroke="#ef4444" strokeWidth={1} strokeDasharray="3 3" label={{ value: 'OB', fill: '#ef4444', fontSize: 8, position: 'insideTopLeft' }} />
381
+ <ReferenceLine y={50} stroke="#475569" strokeWidth={0.5} strokeDasharray="3 3" />
382
+ <ReferenceLine y={30} stroke="#10b981" strokeWidth={1} strokeDasharray="3 3" label={{ value: 'OS', fill: '#10b981', fontSize: 8, position: 'insideBottomLeft' }} />
383
+
384
+ <Line
385
+ type="monotone"
386
+ dataKey="rsi"
387
+ stroke="var(--neon-cyan)"
388
+ strokeWidth={1.5}
389
+ dot={false}
390
+ name="RSI 14"
391
+ />
392
+ </LineChart>
393
+ </ResponsiveContainer>
394
+ </div>
395
+ )}
396
  </div>
397
  </div>
398
  );