diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..70331a5 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,21 @@ +# Build stage +FROM node:18-alpine AS build + +WORKDIR /app +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built assets +COPY --from=build /app/build /usr/share/nginx/html + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/backend/Dockerfile.backend b/backend/Dockerfile.backend new file mode 100644 index 0000000..5131562 --- /dev/null +++ b/backend/Dockerfile.backend @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y git cmake g++ && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/api/__pycache__/auth.cpython-313.pyc b/backend/app/api/__pycache__/auth.cpython-313.pyc index df4a7b4..0532e85 100644 Binary files a/backend/app/api/__pycache__/auth.cpython-313.pyc and b/backend/app/api/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/scanner.cpython-313.pyc b/backend/app/api/__pycache__/scanner.cpython-313.pyc index 75629f8..afffd7e 100644 Binary files a/backend/app/api/__pycache__/scanner.cpython-313.pyc and b/backend/app/api/__pycache__/scanner.cpython-313.pyc differ diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index b94b240..12fc18e 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -26,6 +26,9 @@ class ProfileUpdateRequest(BaseModel): gender: str = None password: str = None # Base64 encoded RSA-encrypted new password +class SectorMapUpdateRequest(BaseModel): + sector_map: dict + @router.get("/public-key") def get_public_key(): return {"public_key": public_key.decode("utf-8")} @@ -84,9 +87,16 @@ def get_me(current_user: User = Depends(get_current_user)): "display_name": current_user.display_name, "email_id": current_user.email_id, "mobile_no": current_user.mobile_no, - "gender": current_user.gender + "gender": current_user.gender, + "sector_map": current_user.sector_map } +@router.put("/update-sectors") +def update_sectors(req: SectorMapUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + current_user.sector_map = req.sector_map + db.commit() + return {"message": "Sectors updated successfully"} + @router.put("/update-profile") def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): if req.display_name is not None: diff --git a/backend/app/api/scanner.py b/backend/app/api/scanner.py index 6283430..8724e67 100644 --- a/backend/app/api/scanner.py +++ b/backend/app/api/scanner.py @@ -4,24 +4,56 @@ from app.services.stock_engine import ( get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel ) +import requests + router = APIRouter() @router.get("/scan") def scan(current_user = Depends(get_current_user)): - return get_scan() + return get_scan(current_user.sector_map or {}) @router.get("/ribbon") def ribbon(current_user = Depends(get_current_user)): - return get_ribbon() + return get_ribbon(current_user.sector_map or {}) @router.get("/smartmoney") def smartmoney(current_user = Depends(get_current_user)): - return get_smartmoney() + return get_smartmoney(current_user.sector_map or {}) @router.get("/trend") def trend(current_user = Depends(get_current_user)): - return get_trend() + return get_trend(current_user.sector_map or {}) @router.get("/marketintel") def marketintel(current_user = Depends(get_current_user)): return get_marketintel() + +@router.get("/search") +def search(q: str, current_user = Depends(get_current_user)): + headers = {'User-Agent': 'Mozilla/5.0'} + url = f"https://query2.finance.yahoo.com/v1/finance/search?q={q}"esCount=10&newsCount=0" + res = requests.get(url, headers=headers) + if res.status_code == 200: + data = res.json() + quotes = data.get("quotes", []) + # Filter for Indian stocks (.NS or .BO) + indian_stocks = [q for q in quotes if q.get("symbol", "").endswith((".NS", ".BO"))][:10] + + if indian_stocks: + import yfinance as yf + + symbols = [q["symbol"] for q in indian_stocks] + tickers = yf.Tickers(" ".join(symbols)) + + for stock in indian_stocks: + try: + info = tickers.tickers[stock["symbol"]].info + stock["currentPrice"] = info.get("currentPrice") or info.get("regularMarketPrice") + stock["marketCap"] = info.get("marketCap") + except Exception as e: + print(f"Error fetching {stock['symbol']}: {e}") + stock["currentPrice"] = None + stock["marketCap"] = None + + return indian_stocks + return [] diff --git a/backend/app/core/__pycache__/config.cpython-313.pyc b/backend/app/core/__pycache__/config.cpython-313.pyc index cab9925..8c4cc54 100644 Binary files a/backend/app/core/__pycache__/config.cpython-313.pyc and b/backend/app/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 3aa511d..e0adb94 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,6 +1,6 @@ import os -DATABASE_URL = "postgresql://postgres:M%40triXPostgr3s%406202@103.125.129.116:5333/stock_scanner" +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5333/stock_scanner") SECRET_KEY = "stock-scanner-super-secret-key-12345" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 diff --git a/backend/app/db/__pycache__/models.cpython-313.pyc b/backend/app/db/__pycache__/models.cpython-313.pyc index 7491bf3..628655f 100644 Binary files a/backend/app/db/__pycache__/models.cpython-313.pyc and b/backend/app/db/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 3ae74a9..61bf8a4 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String +from sqlalchemy import Column, Integer, String, JSON from app.db.database import Base class User(Base): @@ -11,3 +11,4 @@ class User(Base): email_id = Column(String, nullable=True) mobile_no = Column(String, nullable=True) gender = Column(String, nullable=True) + sector_map = Column(JSON, nullable=True) diff --git a/backend/app/services/__pycache__/stock_engine.cpython-313.pyc b/backend/app/services/__pycache__/stock_engine.cpython-313.pyc index 2136e4c..85e78cf 100644 Binary files a/backend/app/services/__pycache__/stock_engine.cpython-313.pyc and b/backend/app/services/__pycache__/stock_engine.cpython-313.pyc differ diff --git a/backend/app/services/stock_engine.py b/backend/app/services/stock_engine.py index 83ed439..bd2ef7e 100644 --- a/backend/app/services/stock_engine.py +++ b/backend/app/services/stock_engine.py @@ -425,12 +425,12 @@ def analyze(stock): # ========================================= # ✅ API - FAST SCAN # ========================================= -def get_scan(): +def get_scan(user_sector_map): sector_output = {} all_stocks = [] - for sector, stocks in sector_map.items(): + for sector, stocks in user_sector_map.items(): data = list(ThreadPoolExecutor(5).map(analyze, stocks)) data = [x for x in data if x] @@ -536,26 +536,28 @@ def analyze_ribbon(stock): return None -def get_ribbon(): +def get_ribbon(user_sector_map): results = [] - for stocks in sector_map.values(): + for sector, stocks in user_sector_map.items(): data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks)) data = [x for x in data if x] + for x in data: + x["sector"] = sector results.extend(data) return results -def get_smartmoney(): +def get_smartmoney(user_sector_map): results = [] - for sector, stocks in sector_map.items(): - + for sector, stocks in user_sector_map.items(): data = list(ThreadPoolExecutor(5).map(analyze, stocks)) data = [x for x in data if x] - + for x in data: + x["sector"] = sector results.extend(data) @@ -575,15 +577,15 @@ def get_smartmoney(): "all": results } -def get_trend(): +def get_trend(user_sector_map): results = [] - for sector, stocks in sector_map.items(): - + for sector, stocks in user_sector_map.items(): data = list(ThreadPoolExecutor(5).map(analyze, stocks)) data = [x for x in data if x] - + for x in data: + x["sector"] = sector results.extend(data) results.sort( diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..7e9fea4 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,114 @@ +alembic==1.18.5 +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.1 +APScheduler==3.11.2 +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 +bcrypt==5.0.0 +beautifulsoup4==4.15.0 +cachetools==7.0.5 +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.7 +click==8.3.1 +cryptography==48.0.0 +curl_cffi==0.16.0 +defusedxml==0.7.1 +dnspython==2.8.0 +-e git+https://github.com/maddy23285/docvision.git@b0a6bea7ad1db9f515c9128964370c6bfdaad9e1#egg=docfingerprint +-e git+https://github.com/maddy23285/docvision.git@b0a6bea7ad1db9f515c9128964370c6bfdaad9e1#egg=docfingerprint_embedding&subdirectory=embedding_service +ecdsa==0.19.2 +email-validator==2.3.0 +fastapi==0.128.0 +filelock==3.17.0 +fonttools==4.62.1 +fpdf2==2.8.7 +fsspec==2025.2.0 +h11==0.16.0 +hf-xet==1.5.2 +httpcore==1.0.9 +httptools==0.7.1 +httpx==0.28.1 +huggingface_hub==0.36.2 +idna==3.11 +ImageHash==4.3.2 +imap-tools==1.13.0 +iniconfig==2.3.0 +Jinja2==3.1.4 +lxml==6.1.1 +Mako==1.3.12 +MarkupSafe==3.0.2 +mpmath==1.3.0 +multitasking==0.0.13 +networkx==3.4.2 +numpy==2.2.3 +ollama==0.6.2 +opencv-python==4.12.0.88 +opencv-python-headless==4.13.0.92 +packaging==26.2 +pandas==3.0.5 +passlib==1.7.4 +pdf2image==1.17.0 +pdfminer.six==20251230 +pdfplumber==0.11.9 +peewee==4.3.0 +pgvector==0.5.0 +pillow==11.1.0 +platformdirs==4.11.0 +pluggy==1.6.0 +protobuf==7.35.1 +psycopg==3.3.4 +psycopg-binary==3.3.4 +psycopg2-binary==2.9.11 +pyasn1==0.6.4 +pycparser==3.0 +pycryptodome==3.23.0 +pycryptodomex==3.21.0 +pydantic==2.12.5 +pydantic-settings==2.14.2 +pydantic_core==2.41.5 +Pygments==2.20.0 +PyJWT==2.11.0 +pypdf==6.13.0 +pypdfium2==5.9.0 +pytesseract==0.3.13 +pytest==8.4.2 +pytest-asyncio==0.26.0 +python-dateutil==2.9.0.post0 +python-docx==1.2.0 +python-dotenv==1.2.1 +python-jose==3.5.0 +python-multipart==0.0.22 +pytz==2026.3.post1 +PyWavelets==1.9.0 +PyYAML==6.0.3 +redis==5.2.0 +regex==2026.7.19 +requests==2.34.2 +rsa==4.9.1 +safetensors==0.8.0 +scipy==1.18.0 +setuptools==75.8.0 +six==1.17.0 +soupsieve==2.9.1 +SQLAlchemy==2.0.46 +starlette==0.50.0 +structlog==25.5.0 +sympy==1.13.1 +tokenizers==0.22.2 +torch==2.6.0 +torchaudio==2.6.0 +torchvision==0.21.0 +tqdm==4.69.0 +transformers==4.57.6 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +tzlocal==5.3.1 +urllib3==2.7.0 +uvicorn==0.40.0 +uvloop==0.22.1 +watchfiles==1.1.1 +websockets==16.0 +yfinance==1.5.2 +zxing-cpp==2.3.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d5ef1d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ + +services: + frontend: + image: hub.technobeesolutions.in/stock-scanner-frontend:latest + restart: always + ports: + - "3055:80" + depends_on: + - backend + + backend: + image: hub.technobeesolutions.in/stock-scanner-backend:latest + restart: always + ports: + - "8055:8000" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - DATABASE_URL=postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5333/stock_scanner diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2a6bbb6 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } +} diff --git a/package.json b/package.json index 1724a3c..0199060 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://127.0.0.1:8000", "eslintConfig": { "extends": [ "react-app", diff --git a/src/App.js b/src/App.js index b10f14d..eadafb7 100644 --- a/src/App.js +++ b/src/App.js @@ -9,22 +9,25 @@ import Popup from "./components/Popup"; import AboutScanner from "./components/AboutScanner"; import MarketIntel from "./components/MarketIntel"; import TrendScanner from "./components/TrendScanner"; +import WatchlistManager from "./components/WatchlistManager"; import Login from "./components/Login"; import Signup from "./components/Signup"; import ProtectedRoute from "./components/ProtectedRoute"; import Profile from "./components/Profile"; +import MainDashboard from "./components/MainDashboard"; import "./index.css"; function Dashboard() { const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE"); const [signal, setSignal] = useState(null); - const [view, setView] = useState("scanner"); + const [view, setView] = useState("dashboard"); const [userProfile, setUserProfile] = useState(null); const [dropdownOpen, setDropdownOpen] = useState(false); + const [scannerLoaded, setScannerLoaded] = useState(false); const navigate = useNavigate(); const fetchProfile = () => { - axios.get("http://127.0.0.1:8000/api/auth/me") + axios.get("/api/auth/me") .then(res => setUserProfile(res.data)) .catch(err => { if (err.response?.status === 401) handleLogout(); @@ -44,7 +47,10 @@ function Dashboard() {
{/* HEADER */}
-

📈 Stock Scanner Pro

+
+ 📊 +

Stock Scanner Pro

+
{userProfile && (
@@ -74,18 +80,22 @@ function Dashboard() {
+ +
{view === "scanner" && ( <> - - +

TradingView Chart

@@ -93,10 +103,12 @@ function Dashboard() { {signal && } )} + {view === "dashboard" && } {view === "ribbon" && } {view === "smartmoney" && } {view === "marketintel" && } {view === "trend" && } + {view === "watchlists" && } {view === "profile" && }
diff --git a/src/components/AboutScanner.js b/src/components/AboutScanner.js index f6b9c3c..e854200 100644 --- a/src/components/AboutScanner.js +++ b/src/components/AboutScanner.js @@ -1,71 +1,90 @@ -import React from "react"; - -function AboutScanner() { - return ( -
-

🧠 How The Scanner Works

- -
    -
  • - 📈 Trend: Price above EMA20 indicates bullish trend. -
  • - -
  • - ⚡ Momentum: RSI measures strength of buying momentum. -
  • - -
  • - 🚀 Breakout: Detects stocks breaking recent resistance levels. -
  • - -
  • - 🔥 Volume Ratio: Compares current volume with average volume. -
  • - -
  • - 💪 Relative Strength (RS): Measures stock performance vs NIFTY. -
  • - -
  • - 🏦 Smart Money Score: Estimates institutional accumulation probability. -
  • - -
  • - 🤖 AI Score: Combined score using trend, RSI, breakout, volume and RS. -
  • - -
  • - ✅ Confidence: Probability of a quality setup based on AI Score. -
  • - -
  • - 🎯 Fund Candidate: High-volume breakout stocks showing potential accumulation. -
  • - -
  • - 🏆 Trade Of The Day: Highest ranked stock across all sectors. -
  • -
- -

🏅 Score Guide

- -
    -
  • A+ (90-100) = Exceptional
  • -
  • A (80-89) = Strong
  • -
  • B (70-79) = Good
  • -
  • C (60-69) = Average
  • -
  • D (<60) = Avoid
  • -
-
- ); -} - +import React, { useState, useEffect } from "react"; + +function AboutScanner({ isLoaded }) { + const [isCollapsed, setIsCollapsed] = useState(false); + + useEffect(() => { + if (isLoaded) { + setIsCollapsed(true); + } + }, [isLoaded]); + + return ( +
+
setIsCollapsed(!isCollapsed)} + > +

🧠 How The Scanner Works

+ {isCollapsed ? "▼" : "▲"} +
+ + {!isCollapsed && ( +
+ +
    +
  • + 📈 Trend: Price above EMA20 indicates bullish trend. +
  • + +
  • + ⚡ Momentum: RSI measures strength of buying momentum. +
  • + +
  • + 🚀 Breakout: Detects stocks breaking recent resistance levels. +
  • + +
  • + 🔥 Volume Ratio: Compares current volume with average volume. +
  • + +
  • + 💪 Relative Strength (RS): Measures stock performance vs NIFTY. +
  • + +
  • + 🏦 Smart Money Score: Estimates institutional accumulation probability. +
  • + +
  • + 🤖 AI Score: Combined score using trend, RSI, breakout, volume and RS. +
  • + +
  • + ✅ Confidence: Probability of a quality setup based on AI Score. +
  • + +
  • + 🎯 Fund Candidate: High-volume breakout stocks showing potential accumulation. +
  • + +
  • + 🏆 Trade Of The Day: Highest ranked stock across all sectors. +
  • +
+ +

🏅 Score Guide

+ +
    +
  • A+ (90-100) = Exceptional
  • +
  • A (80-89) = Strong
  • +
  • B (70-79) = Good
  • +
  • C (60-69) = Average
  • +
  • D (<60) = Avoid
  • +
+
+ )} +
+ ); +} + export default AboutScanner; \ No newline at end of file diff --git a/src/components/Backtest.js b/src/components/Backtest.js index 1dbc9ef..a6dadd9 100644 --- a/src/components/Backtest.js +++ b/src/components/Backtest.js @@ -1,163 +1,163 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; - -function Backtest() { - const [data, setData] = useState({}); - - // ✅ Fetch backtest data - const loadBacktest = async () => { - try { - const res = await axios.get("http://127.0.0.1:8000/backtest"); - setData(res.data); - } catch (err) { - console.error("Backtest Error:", err); - } - }; - - useEffect(() => { - loadBacktest(); - }, []); - - return ( -
-

- 📊 Backtest Dashboard (Strategy Performance) -

- - {/* ✅ EXPLANATION PANEL */} -
-

📘 How to Read Backtest

- -

- ✅ Trades = Total opportunities generated -

-

- ✅ Wins = Profitable trades -

-

- ✅ Loss = Losing trades -

-

- ✅ Win Rate = Success % of strategy -

-

- ✅ Profit = Net gain (simulated) -

- -
- -

- 💡 Best Strategy Rules: -

-
    -
  • Win Rate ≥ 60% ✅
  • -
  • Trades ≥ 10 ✅
  • -
  • Profit positive ✅
  • -
-
- - {/* ✅ SECTORS */} - {Object.entries(data).map(([sector, stocks]) => ( -
-

- {sector} -

- - - - - - - - - - - - - - - {stocks.map((s, i) => ( - - - - - - - - - - {/* ✅ WIN RATE COLOR */} - - - {/* ✅ PROFIT COLOR */} - - - ))} - -
StockTradesWinsLossWin %Profit
{s.symbol}{s.trades} - {s.wins} - - {s.loss} - = 70 - ? "#3fb950" - : s.win_rate >= 60 - ? "#d29922" - : "#f85149", - fontWeight: "bold", - }} - > - {s.win_rate}% - 0 ? "#3fb950" : "#f85149", - fontWeight: "bold", - }} - > - {s.profit} -
-
- ))} -
- ); -} - +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +function Backtest() { + const [data, setData] = useState({}); + + // ✅ Fetch backtest data + const loadBacktest = async () => { + try { + const res = await axios.get("/api/backtest"); + setData(res.data); + } catch (err) { + console.error("Backtest Error:", err); + } + }; + + useEffect(() => { + loadBacktest(); + }, []); + + return ( +
+

+ 📊 Backtest Dashboard (Strategy Performance) +

+ + {/* ✅ EXPLANATION PANEL */} +
+

📘 How to Read Backtest

+ +

+ ✅ Trades = Total opportunities generated +

+

+ ✅ Wins = Profitable trades +

+

+ ✅ Loss = Losing trades +

+

+ ✅ Win Rate = Success % of strategy +

+

+ ✅ Profit = Net gain (simulated) +

+ +
+ +

+ 💡 Best Strategy Rules: +

+
    +
  • Win Rate ≥ 60% ✅
  • +
  • Trades ≥ 10 ✅
  • +
  • Profit positive ✅
  • +
+
+ + {/* ✅ SECTORS */} + {Object.entries(data).map(([sector, stocks]) => ( +
+

+ {sector} +

+ + + + + + + + + + + + + + + {stocks.map((s, i) => ( + + + + + + + + + + {/* ✅ WIN RATE COLOR */} + + + {/* ✅ PROFIT COLOR */} + + + ))} + +
StockTradesWinsLossWin %Profit
{s.symbol}{s.trades} + {s.wins} + + {s.loss} + = 70 + ? "#3fb950" + : s.win_rate >= 60 + ? "#d29922" + : "#f85149", + fontWeight: "bold", + }} + > + {s.win_rate}% + 0 ? "#3fb950" : "#f85149", + fontWeight: "bold", + }} + > + {s.profit} +
+
+ ))} +
+ ); +} + export default Backtest; \ No newline at end of file diff --git a/src/components/Chart.js b/src/components/Chart.js index c1ded22..cb18a9f 100644 --- a/src/components/Chart.js +++ b/src/components/Chart.js @@ -1,149 +1,149 @@ -import React, { useEffect, useRef } from "react"; -import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts"; -import axios from "axios"; - -function Chart({ symbol }) { - const ref = useRef(null); - - useEffect(() => { - if (!ref.current || !symbol) return; - - ref.current.innerHTML = ""; - - try { - const chart = createChart(ref.current, { - width: 800, - height: 500, - layout: { - background: { color: "#111" }, - textColor: "#DDD", - }, - grid: { - vertLines: { color: "#333" }, - horzLines: { color: "#333" }, - }, - }); - - // ✅ MAIN CANDLE SERIES - const candleSeries = chart.addSeries(CandlestickSeries); - - // ✅ EMA LINE - const emaSeries = chart.addSeries(LineSeries, { - color: "yellow", - lineWidth: 2, - }); - - // ✅ RSI LINE (drawn on same chart for simplicity) - const rsiSeries = chart.addSeries(LineSeries, { - color: "cyan", - lineWidth: 1, - }); - - // ✅ EMA CALCULATION - const calculateEMA = (data, period = 20) => { - const k = 2 / (period + 1); - let ema = data[0].close; - return data.map((d) => { - ema = d.close * k + ema * (1 - k); - return { time: d.time, value: ema }; - }); - }; - - // ✅ RSI CALCULATION - const calculateRSI = (data, period = 14) => { - let gains = 0; - let losses = 0; - - const result = []; - - for (let i = 1; i < data.length; i++) { - const diff = data[i].close - data[i - 1].close; - - if (diff > 0) gains += diff; - else losses -= diff; - - if (i >= period) { - const rs = gains / (losses || 1); - const rsi = 100 - 100 / (1 + rs); - - result.push({ time: data[i].time, value: rsi }); - } - } - - return result; - }; - - // ✅ LOAD DATA FUNCTION - const loadData = () => { - axios - .get(`http://localhost:8000/history?symbol=${symbol}`) - .then((res) => { - if (!res.data || res.data.length === 0) return; - - const formatted = res.data.map((d) => ({ - time: Math.floor(d.time), - open: Number(d.open), - high: Number(d.high), - low: Number(d.low), - close: Number(d.close), - })); - - // ✅ SET CANDLES - candleSeries.setData(formatted); - - // ✅ EMA - const emaData = calculateEMA(formatted); - emaSeries.setData(emaData); - - // ✅ RSI - const rsiData = calculateRSI(formatted); - rsiSeries.setData(rsiData); - - // ✅ ENTRY / SL / TARGET - const lastPrice = formatted[formatted.length - 1].close; - - candleSeries.createPriceLine({ - price: lastPrice, - color: "blue", - lineWidth: 2, - title: "Entry", - }); - - candleSeries.createPriceLine({ - price: lastPrice * 0.97, - color: "red", - title: "SL", - }); - - candleSeries.createPriceLine({ - price: lastPrice * 1.05, - color: "green", - title: "Target", - }); - - chart.timeScale().fitContent(); - }) - .catch((err) => console.error(err)); - }; - - // ✅ INITIAL LOAD - loadData(); - - // ✅ LIVE UPDATE (every 1 min) - const interval = setInterval(loadData, 60000); - - return () => { - clearInterval(interval); - chart.remove(); - }; - - } catch (err) { - console.error("Chart crash:", err); - } - - }, [symbol]); - - return
; -} - +import React, { useEffect, useRef } from "react"; +import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts"; +import axios from "axios"; + +function Chart({ symbol }) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current || !symbol) return; + + ref.current.innerHTML = ""; + + try { + const chart = createChart(ref.current, { + width: 800, + height: 500, + layout: { + background: { color: "#111" }, + textColor: "#DDD", + }, + grid: { + vertLines: { color: "#333" }, + horzLines: { color: "#333" }, + }, + }); + + // ✅ MAIN CANDLE SERIES + const candleSeries = chart.addSeries(CandlestickSeries); + + // ✅ EMA LINE + const emaSeries = chart.addSeries(LineSeries, { + color: "yellow", + lineWidth: 2, + }); + + // ✅ RSI LINE (drawn on same chart for simplicity) + const rsiSeries = chart.addSeries(LineSeries, { + color: "cyan", + lineWidth: 1, + }); + + // ✅ EMA CALCULATION + const calculateEMA = (data, period = 20) => { + const k = 2 / (period + 1); + let ema = data[0].close; + return data.map((d) => { + ema = d.close * k + ema * (1 - k); + return { time: d.time, value: ema }; + }); + }; + + // ✅ RSI CALCULATION + const calculateRSI = (data, period = 14) => { + let gains = 0; + let losses = 0; + + const result = []; + + for (let i = 1; i < data.length; i++) { + const diff = data[i].close - data[i - 1].close; + + if (diff > 0) gains += diff; + else losses -= diff; + + if (i >= period) { + const rs = gains / (losses || 1); + const rsi = 100 - 100 / (1 + rs); + + result.push({ time: data[i].time, value: rsi }); + } + } + + return result; + }; + + // ✅ LOAD DATA FUNCTION + const loadData = () => { + axios + .get(`http://localhost:8000/history?symbol=${symbol}`) + .then((res) => { + if (!res.data || res.data.length === 0) return; + + const formatted = res.data.map((d) => ({ + time: Math.floor(d.time), + open: Number(d.open), + high: Number(d.high), + low: Number(d.low), + close: Number(d.close), + })); + + // ✅ SET CANDLES + candleSeries.setData(formatted); + + // ✅ EMA + const emaData = calculateEMA(formatted); + emaSeries.setData(emaData); + + // ✅ RSI + const rsiData = calculateRSI(formatted); + rsiSeries.setData(rsiData); + + // ✅ ENTRY / SL / TARGET + const lastPrice = formatted[formatted.length - 1].close; + + candleSeries.createPriceLine({ + price: lastPrice, + color: "blue", + lineWidth: 2, + title: "Entry", + }); + + candleSeries.createPriceLine({ + price: lastPrice * 0.97, + color: "red", + title: "SL", + }); + + candleSeries.createPriceLine({ + price: lastPrice * 1.05, + color: "green", + title: "Target", + }); + + chart.timeScale().fitContent(); + }) + .catch((err) => console.error(err)); + }; + + // ✅ INITIAL LOAD + loadData(); + + // ✅ LIVE UPDATE (every 1 min) + const interval = setInterval(loadData, 60000); + + return () => { + clearInterval(interval); + chart.remove(); + }; + + } catch (err) { + console.error("Chart crash:", err); + } + + }, [symbol]); + + return
; +} + export default Chart; \ No newline at end of file diff --git a/src/components/ChartPage.js b/src/components/ChartPage.js index 393f90c..255f9e6 100644 --- a/src/components/ChartPage.js +++ b/src/components/ChartPage.js @@ -1,21 +1,21 @@ -import React from "react"; - -function ChartPage({ symbol }) { - const tvSymbol = symbol || "NSE:RELIANCE"; - - const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`; - - return ( -
- -
- ); -} - +import React from "react"; + +function ChartPage({ symbol }) { + const tvSymbol = symbol || "NSE:RELIANCE"; + + const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`; + + return ( +
+ +
+ ); +} + export default ChartPage; \ No newline at end of file diff --git a/src/components/Login.js b/src/components/Login.js index 80a7c87..fa14713 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -15,13 +15,13 @@ function Login() { setLoading(true); setError(""); try { - const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const resKey = await axios.get("/api/auth/public-key"); const publicKeyPem = resKey.data.public_key; const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); const encryptedBase64 = forge.util.encode64(encrypted); - const res = await axios.post("http://127.0.0.1:8000/api/auth/login", { + const res = await axios.post("/api/auth/login", { username, password: encryptedBase64 }); diff --git a/src/components/LongTerm.js b/src/components/LongTerm.js index 97129dc..5272c53 100644 --- a/src/components/LongTerm.js +++ b/src/components/LongTerm.js @@ -1,114 +1,114 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; - -function LongTerm() { - const [data, setData] = useState({}); - - const loadData = async () => { - try { - const res = await axios.get("http://127.0.0.1:8000/longterm"); - setData(res.data); - } catch (err) { - console.error(err); - } - }; - - useEffect(() => { - loadData(); - }, []); - - return ( -
- -

- 📈 Long-Term Investment Dashboard -

- - {/* ✅ EXPLANATION */} -
-

📘 Strategy

- -
    -
  • ✅ Golden Cross = 50 EMA > 200 EMA
  • -
  • ✅ Support = Price above 200 EMA
  • -
  • ✅ Momentum = Breakout + Volume
  • -
- -

💡 Only strong trend reversal stocks are shown

-
- - {Object.entries(data).map(([sector, info]) => { - - -const filtered = info.stocks.filter( - s => s.golden_cross || s.support_strength -); - - - if (filtered.length === 0) return null; - - return ( -
- -

- {sector} ✅ Long-Term Strong -

- - - - - - - - - - - - - - - {filtered.map((s, i) => ( - - - - - - - - - - - - ))} - -
StockScoreRSIGolden CrossSupportSignal
{s.symbol}{s.score}{s.rsi} - ✅ - - ✅ - - {s.signal} -
- -
- ); - })} -
- ); -} - -export default LongTerm; +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +function LongTerm() { + const [data, setData] = useState({}); + + const loadData = async () => { + try { + const res = await axios.get("/api/longterm"); + setData(res.data); + } catch (err) { + console.error(err); + } + }; + + useEffect(() => { + loadData(); + }, []); + + return ( +
+ +

+ 📈 Long-Term Investment Dashboard +

+ + {/* ✅ EXPLANATION */} +
+

📘 Strategy

+ +
    +
  • ✅ Golden Cross = 50 EMA > 200 EMA
  • +
  • ✅ Support = Price above 200 EMA
  • +
  • ✅ Momentum = Breakout + Volume
  • +
+ +

💡 Only strong trend reversal stocks are shown

+
+ + {Object.entries(data).map(([sector, info]) => { + + +const filtered = info.stocks.filter( + s => s.golden_cross || s.support_strength +); + + + if (filtered.length === 0) return null; + + return ( +
+ +

+ {sector} ✅ Long-Term Strong +

+ + + + + + + + + + + + + + + {filtered.map((s, i) => ( + + + + + + + + + + + + ))} + +
StockScoreRSIGolden CrossSupportSignal
{s.symbol}{s.score}{s.rsi} + ✅ + + ✅ + + {s.signal} +
+ +
+ ); + })} +
+ ); +} + +export default LongTerm; diff --git a/src/components/MainDashboard.js b/src/components/MainDashboard.js new file mode 100644 index 0000000..7300584 --- /dev/null +++ b/src/components/MainDashboard.js @@ -0,0 +1,155 @@ +import React, { useState, useRef, useEffect } from "react"; +import axios from "axios"; + +function MainDashboard({ userProfile }) { + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [searching, setSearching] = useState(false); + const searchTimeout = useRef(null); + + const formatMarketCap = (cap) => { + if (!cap) return "N/A"; + if (cap >= 1e12) return "₹" + (cap / 1e12).toFixed(2) + "T"; + if (cap >= 1e9) return "₹" + (cap / 1e9).toFixed(2) + "B"; + if (cap >= 1e7) return "₹" + (cap / 1e7).toFixed(2) + "Cr"; + return "₹" + cap.toLocaleString(); + }; + + const getMarketCapCategory = (cap) => { + if (!cap) return null; + const cr = cap / 1e7; + if (cr >= 20000) return "Large Cap"; + if (cr >= 5000) return "Mid Cap"; + return "Small Cap"; + }; + + const formatPrice = (price) => { + if (!price) return "N/A"; + return "₹" + price.toFixed(2); + }; + + const handleSearch = (e) => { + const query = e.target.value; + setSearchQuery(query); + + if (searchTimeout.current) clearTimeout(searchTimeout.current); + + if (query.trim().length < 2) { + setSearchResults([]); + return; + } + + searchTimeout.current = setTimeout(async () => { + setSearching(true); + try { + const res = await axios.get(`/api/scanner/search?q=${query}`); + setSearchResults(res.data); + } catch (err) { + console.error(err); + } + setSearching(false); + }, 500); + }; + + const openChart = (symbol) => { + window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); + }; + + return ( +
+ +
+
+ 🔍 + + {searching && Searching...} +
+ + {/* Search Results Dropdown */} + {searchResults.length > 0 && searchQuery.length >= 2 && ( +
+ {searchResults.map((result, i) => ( +
openChart(result.symbol)} + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '15px', + borderBottom: '1px solid var(--border-color)', + background: 'var(--bg-tertiary)', + marginBottom: '8px', + borderRadius: '12px', + cursor: 'pointer', + transition: 'background 0.2s' + }} + onMouseOver={(e) => e.currentTarget.style.background = 'rgba(59, 130, 246, 0.1)'} + onMouseOut={(e) => e.currentTarget.style.background = 'var(--bg-tertiary)'} + > +
+
+ {result.symbol} + {result.sector && ( + + {result.sector} + + )} + {result.marketCap && ( + + {getMarketCapCategory(result.marketCap)} + + )} +
+
+ {result.longname || result.shortname} + {result.currentPrice && ( + <> + + Price: {formatPrice(result.currentPrice)} + + )} + {result.marketCap && ( + <> + + {formatMarketCap(result.marketCap)} + + )} +
+
+
+ Chart + + + + + +
+
+ ))} +
+ )} +
+ +
+ ); +} + +export default MainDashboard; diff --git a/src/components/MarketIntel.js b/src/components/MarketIntel.js index 2a0e3f3..52eb37f 100644 --- a/src/components/MarketIntel.js +++ b/src/components/MarketIntel.js @@ -10,7 +10,7 @@ function MarketIntel() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/api/scanner/marketintel"); + const res = await axios.get("/api/scanner/marketintel"); setData(res.data); } catch (err) { console.error(err); diff --git a/src/components/Popup.js b/src/components/Popup.js index 0005458..7ede032 100644 --- a/src/components/Popup.js +++ b/src/components/Popup.js @@ -1,22 +1,22 @@ -import React, { useEffect } from "react"; - -function Popup({ signal }) { - useEffect(() => { - const timer = setTimeout(() => { - const popup = document.getElementById("popup"); - if (popup) popup.style.display = "none"; - }, 4000); - - return () => clearTimeout(timer); - }, []); - - return ( - - ); -} - +import React, { useEffect } from "react"; + +function Popup({ signal }) { + useEffect(() => { + const timer = setTimeout(() => { + const popup = document.getElementById("popup"); + if (popup) popup.style.display = "none"; + }, 4000); + + return () => clearTimeout(timer); + }, []); + + return ( + + ); +} + export default Popup; \ No newline at end of file diff --git a/src/components/Profile.js b/src/components/Profile.js index 238a860..f73a454 100644 --- a/src/components/Profile.js +++ b/src/components/Profile.js @@ -43,7 +43,7 @@ function Profile({ userProfile, onProfileUpdated }) { if (payload.password) { // Fetch Public Key to encrypt new password - const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const resKey = await axios.get("/api/auth/public-key"); const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key); const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP'); payload.password = forge.util.encode64(encrypted); @@ -51,7 +51,7 @@ function Profile({ userProfile, onProfileUpdated }) { delete payload.password; } - await axios.put("http://127.0.0.1:8000/api/auth/update-profile", payload); + await axios.put("/api/auth/update-profile", payload); setMessage("Profile updated successfully! ✅"); if (onProfileUpdated) onProfileUpdated(); setFormData(prev => ({ ...prev, password: "" })); // Clear password field diff --git a/src/components/Ribbon.js b/src/components/Ribbon.js index 266e1ce..a0d44eb 100644 --- a/src/components/Ribbon.js +++ b/src/components/Ribbon.js @@ -7,7 +7,7 @@ function Ribbon() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/api/scanner/ribbon"); + const res = await axios.get("/api/scanner/ribbon"); setData(res.data || []); setLoading(false); } catch (err) { @@ -26,7 +26,18 @@ function Ribbon() { window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); }; - if (loading) return
Loading Ribbon...
; + const removeStockFromSector = async (symbol, sector) => { + try { + await axios.delete("/api/scanner/sectors", { + data: { sector, symbol } + }); + setData(prev => prev.filter(s => s.symbol !== symbol)); + } catch (err) { + console.error("Error removing stock", err); + } + }; + + if (loading) return
Loading Ribbon (this may take up to 60 seconds for large watchlists)...
; return (
@@ -65,9 +76,23 @@ function Ribbon() { - +
+ + {s.sector && ( + + )} +
))} diff --git a/src/components/Scanner.js b/src/components/Scanner.js index 9a988fd..dba4037 100644 --- a/src/components/Scanner.js +++ b/src/components/Scanner.js @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; +import AboutScanner from "./AboutScanner"; function Scanner({ onSelectStock, onSignal }) { const [data, setData] = useState(null); @@ -7,7 +8,7 @@ function Scanner({ onSelectStock, onSignal }) { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/api/scanner/scan"); + const res = await axios.get("/api/scanner/scan"); const newData = res.data; const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null; @@ -33,10 +34,37 @@ function Scanner({ onSelectStock, onSignal }) { window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); }; - if (!data) return
Loading Scanner Data...
; + const removeStockFromSector = async (symbol, sector) => { + try { + await axios.delete("/api/scanner/sectors", { + data: { sector, symbol } + }); + setData(prev => { + const newData = JSON.parse(JSON.stringify(prev)); + if (newData.sectors && newData.sectors[sector]) { + newData.sectors[sector].stocks = newData.sectors[sector].stocks.filter(s => s.symbol !== symbol); + } + if (newData.best_5) { + newData.best_5 = newData.best_5.filter(s => s.symbol !== symbol); + } + return newData; + }); + } catch (err) { + console.error("Error removing stock", err); + } + }; + + if (!data) return ( + <> + +
Loading Scanner Data (this may take up to 60 seconds for large watchlists)...
+ + ); return (
+ + {/* Top 5 Trades */} {(data.best_5 || []).length > 0 && (
@@ -72,9 +100,23 @@ function Scanner({ onSelectStock, onSignal }) { {s.grade} {s.institutional ? 🔥 Yes : No} - +
+ + {s?.sector && ( + + )} +
))} @@ -125,9 +167,21 @@ function Scanner({ onSelectStock, onSignal }) { ₹{s?.target} {s?.ai_score} - +
+ + +
))} diff --git a/src/components/Signup.js b/src/components/Signup.js index dd4f7aa..2bbaab8 100644 --- a/src/components/Signup.js +++ b/src/components/Signup.js @@ -15,13 +15,13 @@ function Signup() { setLoading(true); setError(""); try { - const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const resKey = await axios.get("/api/auth/public-key"); const publicKeyPem = resKey.data.public_key; const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); const encryptedBase64 = forge.util.encode64(encrypted); - await axios.post("http://127.0.0.1:8000/api/auth/signup", { + await axios.post("/api/auth/signup", { username, password: encryptedBase64 }); diff --git a/src/components/SmartMoney.js b/src/components/SmartMoney.js index 19c9e2f..c5911e1 100644 --- a/src/components/SmartMoney.js +++ b/src/components/SmartMoney.js @@ -12,7 +12,7 @@ function SmartMoney() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/api/scanner/smartmoney"); + const res = await axios.get("/api/scanner/smartmoney"); setData(res.data); } catch (err) { console.error(err); @@ -23,7 +23,30 @@ function SmartMoney() { window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); }; - if (!data) return
Loading Smart Money Data...
; + const removeStockFromSector = async (symbol, sector) => { + try { + await axios.delete("/api/scanner/sectors", { + data: { sector, symbol } + }); + setData(prev => { + const newData = JSON.parse(JSON.stringify(prev)); + if (newData.top_10) { + newData.top_10 = newData.top_10.filter(s => s.symbol !== symbol); + } + if (newData.all) { + newData.all = newData.all.filter(s => s.symbol !== symbol); + } + if (newData.trade_of_day && newData.trade_of_day.symbol === symbol) { + newData.trade_of_day = newData.top_10.length > 0 ? newData.top_10[0] : null; + } + return newData; + }); + } catch (err) { + console.error("Error removing stock", err); + } + }; + + if (!data) return
Loading Smart Money Data (this may take up to 60 seconds for large watchlists)...
; return (
@@ -80,7 +103,21 @@ function SmartMoney() { {s.breakout ? "🔥 YES" : "NO"} {s.reason} - +
+ + {s.sector && ( + + )} +
))} diff --git a/src/components/TrendScanner.js b/src/components/TrendScanner.js index d876751..3e7a1d1 100644 --- a/src/components/TrendScanner.js +++ b/src/components/TrendScanner.js @@ -12,7 +12,7 @@ function TrendScanner() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/api/scanner/trend"); + const res = await axios.get("/api/scanner/trend"); setData(res.data); } catch (err) { console.error(err); @@ -23,7 +23,24 @@ function TrendScanner() { window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); }; - if (!data) return
Loading Trend Data...
; + const removeStockFromSector = async (symbol, sector) => { + try { + await axios.delete("/api/scanner/sectors", { + data: { sector, symbol } + }); + setData(prev => { + const newData = JSON.parse(JSON.stringify(prev)); + if (newData.top_20) { + newData.top_20 = newData.top_20.filter(s => s.symbol !== symbol); + } + return newData; + }); + } catch (err) { + console.error("Error removing stock", err); + } + }; + + if (!data) return
Loading Trend Data (this may take up to 60 seconds for large watchlists)...
; return (
@@ -63,7 +80,21 @@ function TrendScanner() { {s.trend_reason} - +
+ + {s.sector && ( + + )} +
))} diff --git a/src/components/WatchlistManager.js b/src/components/WatchlistManager.js new file mode 100644 index 0000000..a229162 --- /dev/null +++ b/src/components/WatchlistManager.js @@ -0,0 +1,279 @@ +import React, { useState, useEffect, useRef } from "react"; +import axios from "axios"; + +function WatchlistManager({ userProfile, onProfileUpdated }) { + const [sectorMap, setSectorMap] = useState({}); + const [newSectorName, setNewSectorName] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [searching, setSearching] = useState(false); + const [selectedSector, setSelectedSector] = useState("Personal"); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState(""); + + const searchTimeout = useRef(null); + + const formatMarketCap = (cap) => { + if (!cap) return "N/A"; + if (cap >= 1e12) return "₹" + (cap / 1e12).toFixed(2) + "T"; + if (cap >= 1e9) return "₹" + (cap / 1e9).toFixed(2) + "B"; + if (cap >= 1e7) return "₹" + (cap / 1e7).toFixed(2) + "Cr"; + return "₹" + cap.toLocaleString(); + }; + + const getMarketCapCategory = (cap) => { + if (!cap) return null; + const cr = cap / 1e7; // Convert to Crores + if (cr >= 20000) return "Large Cap"; + if (cr >= 5000) return "Mid Cap"; + return "Small Cap"; + }; + + const formatPrice = (price) => { + if (!price) return "N/A"; + return "₹" + price.toFixed(2); + }; + + useEffect(() => { + if (userProfile && userProfile.sector_map) { + setSectorMap(userProfile.sector_map); + } + }, [userProfile]); + + const saveSectors = async (updatedMap) => { + setSaving(true); + setMessage(""); + try { + await axios.put("/api/auth/update-sectors", { sector_map: updatedMap }); + setSectorMap(updatedMap); + if (onProfileUpdated) onProfileUpdated(); + setMessage("Sectors saved! ✅"); + setTimeout(() => setMessage(""), 3000); + } catch (err) { + console.error(err); + setMessage("Failed to save sectors."); + } + setSaving(false); + }; + + const handleSearch = (e) => { + const query = e.target.value; + setSearchQuery(query); + + if (searchTimeout.current) clearTimeout(searchTimeout.current); + + if (query.trim().length < 2) { + setSearchResults([]); + return; + } + + searchTimeout.current = setTimeout(async () => { + setSearching(true); + try { + const res = await axios.get(`/api/scanner/search?q=${query}`); + setSearchResults(res.data); + } catch (err) { + console.error(err); + } + setSearching(false); + }, 500); + }; + + const addStockToSector = (symbol, targetSector) => { + const updatedMap = { ...sectorMap }; + if (!updatedMap[targetSector]) updatedMap[targetSector] = []; + if (!updatedMap[targetSector].includes(symbol)) { + updatedMap[targetSector].push(symbol); + saveSectors(updatedMap); + } + }; + + const removeStockFromSector = (symbol, targetSector) => { + const updatedMap = { ...sectorMap }; + if (updatedMap[targetSector]) { + updatedMap[targetSector] = updatedMap[targetSector].filter(s => s !== symbol); + saveSectors(updatedMap); + } + }; + + const createSector = () => { + const name = newSectorName.trim(); + if (name && !sectorMap[name]) { + const updatedMap = { ...sectorMap, [name]: [] }; + saveSectors(updatedMap); + setNewSectorName(""); + } + }; + + const deleteSector = (sector) => { + if (sector === "Personal") return; // Protect Personal + if (window.confirm(`Are you sure you want to delete the ${sector} watchlist?`)) { + const updatedMap = { ...sectorMap }; + delete updatedMap[sector]; + saveSectors(updatedMap); + if (selectedSector === sector) setSelectedSector("Personal"); + } + }; + + return ( +
+ + {/* LEFT PANEL: Sector List & Creator */} +
+

📋 Watchlists

+
+ {Object.keys(sectorMap).map(sector => ( +
setSelectedSector(sector)} + style={{ + padding: '10px 15px', + background: selectedSector === sector ? 'rgba(59, 130, 246, 0.2)' : 'var(--bg-tertiary)', + border: selectedSector === sector ? '1px solid var(--accent-blue)' : '1px solid var(--border-color)', + borderRadius: '8px', cursor: 'pointer', + display: 'flex', justifyContent: 'space-between', alignItems: 'center', + transition: 'all 0.2s' + }} + > + + {sector} ({sectorMap[sector].length}) + + {sector !== "Personal" && ( + + )} +
+ ))} +
+ +
+

Create new watchlist

+
+ setNewSectorName(e.target.value)} + placeholder="E.g., EV Stocks" + style={{ flex: 1, padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }} + /> + +
+
+
+ + {/* RIGHT PANEL: Search & Stock List */} +
+
+

+ {selectedSector} Watchlist +

+ {saving && Saving...} + {message && {message}} +
+ + {/* Search Bar */} +
+ + {searching && Searching...} + + {/* Search Results Dropdown */} + {searchResults.length > 0 && searchQuery.length >= 2 && ( +
+ {searchResults.map((result, i) => ( +
+
+
+ {result.symbol} + {result.sector && ( + + {result.sector} + + )} + {result.marketCap && ( + + {getMarketCapCategory(result.marketCap)} + + )} +
+
+ {result.longname || result.shortname} + {result.currentPrice && ( + <> + + Price: {formatPrice(result.currentPrice)} + + )} + {result.marketCap && ( + <> + + {formatMarketCap(result.marketCap)} + + )} +
+
+ +
+ ))} +
+ )} +
+ + {/* Stock List Grid */} + {sectorMap[selectedSector] && sectorMap[selectedSector].length === 0 ? ( +
No stocks in this watchlist. Search above to add some!
+ ) : ( +
+
+ {(sectorMap[selectedSector] || []).map(symbol => ( +
+ + {/* Top Row: Symbol and Trash */} +
+ {symbol} + +
+ + {/* Bottom Row: + Personal */} + {selectedSector !== "Personal" && ( + + )} +
+ ))} +
+
+ )} +
+
+ ); +} + +export default WatchlistManager; diff --git a/src/index.css b/src/index.css index e0205d5..012f9cb 100644 --- a/src/index.css +++ b/src/index.css @@ -83,9 +83,9 @@ h2 { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); - padding: 10px 20px; + padding: 8px 14px; border-radius: 8px; - font-size: 0.95rem; + font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease;