4 Commits

Author SHA1 Message Date
f33011b219 Research View 2026-08-08 08:03:46 +05:30
e4ad2233b8 commit - mock buy 2026-08-07 22:38:27 +05:30
53ad1abb78 Customize - Stock map list, watch list, home page 2026-08-05 23:51:18 +05:30
076c4ff68f User Auth Done 2026-08-05 22:00:49 +05:30
51 changed files with 2803 additions and 651 deletions

21
Dockerfile.frontend Normal file
View File

@@ -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;"]

View File

@@ -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"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

116
backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,116 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from pydantic import BaseModel
from jose import JWTError, jwt
from app.db.database import get_db
from app.db.models import User
from app.core.security import (
public_key, decrypt_password, verify_password,
get_password_hash, create_access_token
)
from app.core.config import SECRET_KEY, ALGORITHM
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
class AuthRequest(BaseModel):
username: str
password: str # Base64 encoded RSA-encrypted password
class ProfileUpdateRequest(BaseModel):
display_name: str = None
email_id: str = None
mobile_no: str = None
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")}
@router.post("/signup")
def signup(req: AuthRequest, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.username == req.username).first()
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
# Decrypt password
decrypted_password = decrypt_password(req.password)
hashed_password = get_password_hash(decrypted_password)
new_user = User(username=req.username, hashed_password=hashed_password)
db.add(new_user)
db.commit()
return {"message": "User created successfully"}
@router.post("/login")
def login(req: AuthRequest, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.username == req.username).first()
if not db_user:
raise HTTPException(status_code=400, detail="Invalid credentials")
decrypted_password = decrypt_password(req.password)
if not verify_password(decrypted_password, db_user.hashed_password):
raise HTTPException(status_code=400, detail="Invalid credentials")
access_token = create_access_token(data={"sub": db_user.username})
return {"access_token": access_token, "token_type": "bearer"}
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
return user
@router.get("/me")
def get_me(current_user: User = Depends(get_current_user)):
return {
"username": current_user.username,
"display_name": current_user.display_name,
"email_id": current_user.email_id,
"mobile_no": current_user.mobile_no,
"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:
current_user.display_name = req.display_name
if req.email_id is not None:
current_user.email_id = req.email_id
if req.mobile_no is not None:
current_user.mobile_no = req.mobile_no
if req.gender is not None:
current_user.gender = req.gender
if req.password:
decrypted_password = decrypt_password(req.password)
hashed_password = get_password_hash(decrypted_password)
current_user.hashed_password = hashed_password
db.commit()
return {"message": "Profile updated successfully"}

View File

@@ -0,0 +1,144 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
import yfinance as yf
from app.db.database import get_db
from app.db.models import User, PortfolioItem
from app.api.auth import get_current_user
router = APIRouter()
class BuyRequest(BaseModel):
symbol: str
quantity: int
buy_price: float
class SellRequest(BaseModel):
portfolio_item_id: int
@router.post("/buy")
def buy_stock(req: BuyRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
final_buy_price = req.buy_price
# If price wasn't provided (e.g. from watchlist), fetch it live
if final_buy_price <= 0:
try:
ticker = yf.Ticker(req.symbol)
final_buy_price = ticker.fast_info.last_price
except Exception as e:
print(f"Error fetching live price on buy for {req.symbol}: {e}")
raise HTTPException(status_code=400, detail="Could not fetch live price, please try again.")
new_item = PortfolioItem(
user_id=current_user.id,
symbol=req.symbol,
quantity=req.quantity,
buy_price=final_buy_price
)
db.add(new_item)
db.commit()
db.refresh(new_item)
return {"message": "Stock purchased successfully", "item": new_item}
@router.get("/")
def get_portfolio(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
items = db.query(PortfolioItem).filter(PortfolioItem.user_id == current_user.id).all()
portfolio = []
total_invested = 0
total_current_value = 0
for item in items:
# Fetch live price
live_price = item.buy_price # Fallback
try:
ticker = yf.Ticker(item.symbol)
live_price = ticker.fast_info.last_price
except Exception as e:
print(f"Error fetching price for {item.symbol}: {e}")
invested = item.quantity * item.buy_price
current_value = item.quantity * live_price
pnl = current_value - invested
pnl_percent = (pnl / invested) * 100 if invested > 0 else 0
total_invested += invested
total_current_value += current_value
portfolio.append({
"id": item.id,
"symbol": item.symbol,
"quantity": item.quantity,
"buy_price": item.buy_price,
"current_price": round(live_price, 2),
"invested": round(invested, 2),
"current_value": round(current_value, 2),
"pnl": round(pnl, 2),
"pnl_percent": round(pnl_percent, 2),
"purchase_date": item.purchase_date
})
total_pnl = total_current_value - total_invested
total_pnl_percent = (total_pnl / total_invested) * 100 if total_invested > 0 else 0
return {
"items": portfolio,
"summary": {
"total_invested": round(total_invested, 2),
"total_current_value": round(total_current_value, 2),
"total_pnl": round(total_pnl, 2),
"total_pnl_percent": round(total_pnl_percent, 2)
}
}
@router.delete("/{item_id}")
def delete_portfolio_item(item_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
item = db.query(PortfolioItem).filter(PortfolioItem.id == item_id, PortfolioItem.user_id == current_user.id).first()
if not item:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(item)
db.commit()
return {"message": "Stock sold/removed successfully"}
@router.get("/recommendations/{symbol}")
def get_recommendations(symbol: str, current_user: User = Depends(get_current_user)):
try:
ticker = yf.Ticker(symbol)
recs = ticker.recommendations
if recs is None or recs.empty:
return {"error": "No recommendation data available", "confidence": 0}
# Get the most recent month's data (period '0m' is usually index 0)
latest = recs.iloc[0]
strong_buy = int(latest.get("strongBuy", 0))
buy = int(latest.get("buy", 0))
hold = int(latest.get("hold", 0))
sell = int(latest.get("sell", 0))
strong_sell = int(latest.get("strongSell", 0))
total = strong_buy + buy + hold + sell + strong_sell
if total == 0:
return {"error": "No recommendations found", "confidence": 0}
# Calculate confidence score
# Strong Buy = 100, Buy = 75, Hold = 50, Sell = 25, Strong Sell = 0
score = (strong_buy * 100 + buy * 75 + hold * 50 + sell * 25) / total
return {
"symbol": symbol,
"period": str(latest.get("period", "0m")),
"strongBuy": strong_buy,
"buy": buy,
"hold": hold,
"sell": sell,
"strongSell": strong_sell,
"total": total,
"confidence": round(score, 1)
}
except Exception as e:
print(f"Error fetching recommendations for {symbol}: {e}")
return {"error": str(e), "confidence": 0}

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends
from app.api.auth import get_current_user
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(current_user.sector_map or {})
@router.get("/ribbon")
def ribbon(current_user = Depends(get_current_user)):
return get_ribbon(current_user.sector_map or {})
@router.get("/smartmoney")
def smartmoney(current_user = Depends(get_current_user)):
return get_smartmoney(current_user.sector_map or {})
@router.get("/trend")
def trend(current_user = Depends(get_current_user)):
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}&quotesCount=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 []

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@192.168.0.111:5432/stock_scanner")
SECRET_KEY = "stock-scanner-super-secret-key-12345"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60

View File

@@ -0,0 +1,61 @@
import os
import base64
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
import bcrypt
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from fastapi import HTTPException
from app.core.config import SECRET_KEY, ALGORITHM
RSA_PRIVATE_KEY_PATH = "private.pem"
RSA_PUBLIC_KEY_PATH = "public.pem"
if not os.path.exists(RSA_PRIVATE_KEY_PATH) or not os.path.exists(RSA_PUBLIC_KEY_PATH):
key = RSA.generate(2048)
private_key = key.export_key()
with open(RSA_PRIVATE_KEY_PATH, "wb") as file_out:
file_out.write(private_key)
public_key = key.publickey().export_key()
with open(RSA_PUBLIC_KEY_PATH, "wb") as file_out:
file_out.write(public_key)
else:
with open(RSA_PRIVATE_KEY_PATH, "rb") as f:
private_key = f.read()
with open(RSA_PUBLIC_KEY_PATH, "rb") as f:
public_key = f.read()
rsa_private_key = RSA.import_key(private_key)
cipher_rsa = PKCS1_OAEP.new(rsa_private_key)
def decrypt_password(encrypted_b64_password: str) -> str:
try:
encrypted_bytes = base64.b64decode(encrypted_b64_password)
decrypted = cipher_rsa.decrypt(encrypted_bytes)
return decrypted.decode("utf-8")
except Exception as e:
raise HTTPException(status_code=400, detail="Invalid RSA payload or decryption failed")
def verify_password(plain_password, hashed_password):
if isinstance(plain_password, str):
plain_password = plain_password.encode('utf-8')
if isinstance(hashed_password, str):
hashed_password = hashed_password.encode('utf-8')
return bcrypt.checkpw(plain_password, hashed_password)
def get_password_hash(password):
if isinstance(password, str):
password = password.encode('utf-8')
return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8')
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import DATABASE_URL
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

25
backend/app/db/models.py Normal file
View File

@@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, JSON, Float, ForeignKey, DateTime
from app.db.database import Base
import datetime
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
display_name = Column(String, nullable=True)
email_id = Column(String, nullable=True)
mobile_no = Column(String, nullable=True)
gender = Column(String, nullable=True)
sector_map = Column(JSON, nullable=True)
class PortfolioItem(Base):
__tablename__ = "portfolio_items"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
symbol = Column(String, index=True)
quantity = Column(Integer, default=1)
buy_price = Column(Float)
purchase_date = Column(DateTime, default=datetime.datetime.utcnow)

27
backend/app/main.py Normal file
View File

@@ -0,0 +1,27 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api import auth, scanner, portfolio
from app.db.database import engine, Base
# Initialize the database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Stock Scanner Pro API")
# Configure CORS for frontend access
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include Routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(scanner.router, prefix="/api/scanner", tags=["scanner"])
app.include_router(portfolio.router, prefix="/api/portfolio", tags=["portfolio"])
@app.get("/")
def read_root():
return {"message": "Stock Scanner Backend is running."}

View File

@@ -1,20 +1,8 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import yfinance as yf import yfinance as yf
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ✅ PERSONAL # ✅ PERSONAL
# ✅ PERSONAL # ✅ PERSONAL
personal_stocks = [ personal_stocks = [
@@ -437,13 +425,12 @@ def analyze(stock):
# ========================================= # =========================================
# ✅ API - FAST SCAN # ✅ API - FAST SCAN
# ========================================= # =========================================
@app.get("/scan") def get_scan(user_sector_map):
def scan():
sector_output = {} sector_output = {}
all_stocks = [] 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 = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x] data = [x for x in data if x]
@@ -549,28 +536,28 @@ def analyze_ribbon(stock):
return None return None
@app.get("/ribbon") def get_ribbon(user_sector_map):
def ribbon():
results = [] results = []
for stocks in sector_map.values(): for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks)) data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
data = [x for x in data if x] data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data) results.extend(data)
return results return results
@app.get("/smartmoney") def get_smartmoney(user_sector_map):
def smartmoney():
results = [] results = []
for sector, stocks in sector_map.items(): for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks)) data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x] data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data) results.extend(data)
@@ -590,16 +577,15 @@ def smartmoney():
"all": results "all": results
} }
@app.get("/trend") def get_trend(user_sector_map):
def trend():
results = [] results = []
for sector, stocks in sector_map.items(): for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks)) data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x] data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data) results.extend(data)
results.sort( results.sort(
@@ -623,8 +609,7 @@ def trend():
# - Full sector_map (too large) # - Full sector_map (too large)
# - Deep MACD + Supertrend # - Deep MACD + Supertrend
# - 6 month data calls # - 6 month data calls
@app.get("/marketintel") def get_marketintel():
def marketintel():
try: try:
nifty_news = yf.Ticker("^NSEI").news or [] nifty_news = yf.Ticker("^NSEI").news or []
reliance_news = yf.Ticker("RELIANCE.NS").news or [] reliance_news = yf.Ticker("RELIANCE.NS").news or []

27
backend/private.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtqxw/s/eCLosxX61Zvd0PYicYUoKjbn6gsxe+jb4p0hMNK36
YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjwTkNBF8P3bPJ+R+qVXeiqFBIAkRNX
VqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04WLtw5WUtYUbLwO2TcAYghmzk/UTw
p0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDaUsIa3qy6LG7UkGz0LQ3RKu9RqoVO
I8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBRC+yJ3UxZvqIUpR6taToZxPL0aZG9
XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+EQIDAQABAoIBAE2cThW0sxz6HHN8
Ng4dLGHIXMho8LruPSS9N80O9e38pYPsNuknQIjQTmFmOxTQa8jrZtNy/5S0tDTt
BVWNoiteH5W9SK4dCNH9NKDFbR1B2MPKd9z3Ms/lKLJ8ITert8NrM3ZbN+09NEbK
0jHYO8xXNsv/hJxDu+WoCnAZ6k+x1IFDXRfV4MrTBX27t4xtfA5KeXQyPzotUeBn
VjVIoRqkCSIJLr+f6ATa+OHlfBjyTbBQscG1/0JNfDDQFmvOYyyc4zpVn0bAINw7
klvJaXP++DRaZsvNnM1SGpsJ+U86O56ODkX4v2a+H3czhpOA+q0yePHQ/nWr73Nf
7WQYxdECgYEA1OVJZoNPxqVefooY9Tm5mDRXeRVdOormEI8GZkrIAG85w+MD9MMr
2s8FjXDniN4khSClpF9YunB8e77ufu0TgpqhHhXSnn/5JEQ4UO5wiO1EqBvhkFUJ
CXitjYehgwDc5WKvUfPxeMCMvF+rYoHhN1aYIczuN36fhDAXA6pxGyMCgYEA26iy
2RHgu0XNa37Z9KeJCDGNtpj2dYttRAua4UVudxPYn+Q2IdKZ9HPjgWPKgdUjAEup
uuIDhbk1zmQiquvyNMP0HX+p2yG8DX77cdajyvBG5p3BxUX1MxXx4Q6ICVAr4K51
cZg3YWsIsCsCCO7DkgW1kFRlVptUMMmbJjNoXzsCgYAE32qaqg69YTOUedywYC3b
SfdmkhKcMGmrn1pqJPQG7oTH8v44L+9lBq/92MOz4kG7uk+QP45sVf7DZk9XIF39
80QUyDMV5Z/yMI2JbKuutp+HqXu0Lf4S9WwjfSM5OF/V8DhLC+ZO+Tk/ZoEptAdP
mO/KdkJNitxjziX4s4H7OQKBgB77oJ51oxlHMz5iWiPkLbP2KWMEGF9kFzlt2Z7E
yFwLdJa4/dmvdv/ACOsLRFkj0xgLlBlEH/MQuMIv5aPuO++tZBV1GGRMUdYlfxoD
iH7rfVSyE87bm0ZlZgS0pAOMR2Qdt3saWVVoX4VZy6Ou6e8C1yVQgirBJhLrnPK6
dZJZAoGBAK/Nob7Lrxi2kXuJbUOp+Jcir/sPeWE0ZPH9vnAdDkDLrUFbyNWJHHgI
tn1rrPCJeF3+gGdbKcrubHGxJMZGDh3qALs35L6195ZaGys6l+Imp1FxsSFROf/S
Hy2wbjf7fz/gb0U7dfD7ZtVPDvqdFax7NrEdvdcslA0IUiVPqGkC
-----END RSA PRIVATE KEY-----

9
backend/public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtqxw/s/eCLosxX61Zvd0
PYicYUoKjbn6gsxe+jb4p0hMNK36YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjw
TkNBF8P3bPJ+R+qVXeiqFBIAkRNXVqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04
WLtw5WUtYUbLwO2TcAYghmzk/UTwp0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDa
UsIa3qy6LG7UkGz0LQ3RKu9RqoVOI8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBR
C+yJ3UxZvqIUpR6taToZxPL0aZG9XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+
EQIDAQAB
-----END PUBLIC KEY-----

114
backend/requirements.txt Normal file
View File

@@ -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

50
build_and_push.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Configuration
REGISTRY="hub.technobeesolutions.in"
USERNAME="technobee_admin"
PASSWORD='M@tr!x#149@dm!N'
BACKEND_IMAGE="${REGISTRY}/stock-scanner-backend:latest"
FRONTEND_IMAGE="${REGISTRY}/stock-scanner-frontend:latest"
PLATFORM="linux/amd64"
echo "=========================================="
echo " Stock Scanner - Docker Build & Push "
echo "=========================================="
# 1. Login to Docker Registry
echo "[1/4] Logging into Docker Registry ($REGISTRY)..."
# Using --password-stdin is the secure way to pass passwords to docker login
echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
if [ $? -ne 0 ]; then
echo "❌ Docker login failed! Please check your credentials and ensure Docker is running."
exit 1
fi
echo "✅ Login successful."
echo ""
# 2. Build and push backend
echo "[2/4] Building and pushing Backend Image for $PLATFORM..."
docker buildx build --platform "$PLATFORM" -t "$BACKEND_IMAGE" -f backend/Dockerfile.backend ./backend --push
if [ $? -ne 0 ]; then
echo "❌ Backend build/push failed!"
exit 1
fi
echo "✅ Backend image built and pushed successfully."
echo ""
# 3. Build and push frontend
echo "[3/4] Building and pushing Frontend Image for $PLATFORM..."
docker buildx build --platform "$PLATFORM" -t "$FRONTEND_IMAGE" -f Dockerfile.frontend . --push
if [ $? -ne 0 ]; then
echo "❌ Frontend build/push failed!"
exit 1
fi
echo "✅ Frontend image built and pushed successfully."
echo ""
echo "=========================================="
echo "🎉 Build and Push Complete!"
echo "You can now run 'docker compose pull && docker compose up -d' on your target server."
echo "=========================================="

19
docker-compose.yml Normal file
View File

@@ -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:5432/stock_scanner

10
nginx.conf Normal file
View File

@@ -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;
}
}

17
package-lock.json generated
View File

@@ -14,9 +14,10 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1", "axios": "^1.18.1",
"lightweight-charts": "^5.2.0", "lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-router-dom": "^7.18.0", "react-router-dom": "^7.18.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
} }
@@ -13958,9 +13959,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -13980,12 +13981,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.18.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.18.0" "react-router": "7.18.2"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

View File

@@ -9,9 +9,10 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1", "axios": "^1.18.1",
"lightweight-charts": "^5.2.0", "lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-router-dom": "^7.18.0", "react-router-dom": "^7.18.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@@ -21,6 +22,7 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"proxy": "http://127.0.0.1:8000",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",

27
private.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuUUB5HsRK6mvKyjAClLckNxDK69MZTW2Yn9qOJZvw3j0R4sl
IhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+3z+a+Ds/uP3tDYbnfT4cr9VEhgq9
FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0TuShT/xImmqLfxkmvZSP/syPoFytEq
X66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQzFxSBt0xpeyMXHChn8AeocaOySC3
kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK+EZXzWEbugBWOg/NSy9pnzPRGjAY
qknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+SwIDAQABAoIBAAdhNs6Vkl0FYMtc
z615mHySsYBDds0PQykQPzhq0Z/dKQnSgpOlrHlkJ8/dQRxLV0woccUo+9OyfR63
UxyqroSxAWjC32psfu1LgtwLxdPBMH4QXnDPnN9fAXJhDqa8LkrNaYQ/id/ZDj3r
27EOZ1l/FaMMAp302cQF1L1iKFnm/ksN9s4G2+8UwrtUWum5LOeD6x22H8443uRf
Lg4IvMC4nLYCQRUAeaKJoTZnw8KeOmKpF2opfu8L1vQ8ubYoZqBX+gNP6GflFG6j
zgoNWfZvRQnDfofH+ZOPcB7vbVKcXKFZ0kHzfw40Nq49GxZndsK30xCUtdDiW7eE
Mgfv9EUCgYEA1q/HUTKBFpvKGu1BwCVTIdvqI0qScWpYst525CbeI36MCkP6ReCX
kvi7259saMRdAaEhIj8EN3BuDu5k8jjNRqkAnkq0pNHkYD3lzXBc4H3RC0LdmEmn
cUUS16sLSkcciI3+SnGjZUbCjso2r2Sy1d/iPhR7L2QpmlzqtPNZxr0CgYEA3OwK
NJMNN1ARv+aOka+mxn6+MsbbWWB0CLSbhg5WRRnmsefDMBjtWsRr+/ufQO7rC+q1
rv83jiCgJl3u5idS4Hrm/k77iZZzrvJruuR5W5GgvqMTcdut7l4pdqSlvV96D4z7
29/sze3vVoZv4zIeWxj5QDeT7xLwvjoZpUbHDacCgYAsCjsVCQs6HBNFms4WIJIB
LB/HxZBs+6feaYxyGRcQqPEJWhCJLR1q5OOElhujEkUSBH/LiqnOxZ2OKpFCryxN
BnY+Ao00EmqK46e0kQw8cRLlAH58sv9KWSUYYNocDqJn0NkNZGpkaDOZHxpAuKOH
BDphCcqLWjy+kbkEDbeo8QKBgQCD9m7GJsyvLKHdmi+xMFYTnWOpWwVtZuMIzDFW
Kzw2/JjDzifWlB07qbbDBvOCyvQV4zZxeLvLpwtiv5tTWUv1ERTn9W/lKLyjVOUq
9wzSuLNnDGwyB8Hmb9KerwzdiKmVnmZXWXPPMoBTk+xDrw1Y5xsD0+8G0K6DQptN
EXEXYwKBgQDS5DR5IDGvTycguDhU1QGSW0iEzMfBlZftKAucB/KYcQj8A4+AECKM
9SqcTI2Aneou+7PubpCFDSEckSKZkxb6ibw/ynA1EE55wyjuctyHTpqeHJjoK7jS
R5hyVL5iFBwGxv6oiWeKld+ZbDSB+i89km+kARaXjIB2gZmkepAIyA==
-----END RSA PRIVATE KEY-----

9
public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuUUB5HsRK6mvKyjAClLc
kNxDK69MZTW2Yn9qOJZvw3j0R4slIhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+
3z+a+Ds/uP3tDYbnfT4cr9VEhgq9FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0Tu
ShT/xImmqLfxkmvZSP/syPoFytEqX66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQ
zFxSBt0xpeyMXHChn8AeocaOySC3kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK
+EZXzWEbugBWOg/NSy9pnzPRGjAYqknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+
SwIDAQAB
-----END PUBLIC KEY-----

18
run_backend.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
echo "Starting Stock Scanner Backend..."
# Navigate to the backend directory
cd backend || exit 1
# Activate virtual environment if it exists (optional, helps if user uses venv)
if [ -d "venv" ]; then
echo "Activating virtual environment..."
source venv/bin/activate
elif [ -d "../venv" ]; then
echo "Activating virtual environment..."
source ../venv/bin/activate
fi
# Run the uvicorn server with hot reload
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

View File

@@ -1,4 +1,6 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom";
import axios from "axios";
import Chart from "./components/Chart"; import Chart from "./components/Chart";
import Scanner from "./components/Scanner"; import Scanner from "./components/Scanner";
import Ribbon from "./components/Ribbon"; import Ribbon from "./components/Ribbon";
@@ -7,59 +9,141 @@ import Popup from "./components/Popup";
import AboutScanner from "./components/AboutScanner"; import AboutScanner from "./components/AboutScanner";
import MarketIntel from "./components/MarketIntel"; import MarketIntel from "./components/MarketIntel";
import TrendScanner from "./components/TrendScanner"; 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"; import "./index.css";
function App() { function Dashboard() {
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE"); const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
const [signal, setSignal] = useState(null); 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 [buyModalData, setBuyModalData] = useState(null);
const [researchModalData, setResearchModalData] = useState(null);
const [buyModalRecs, setBuyModalRecs] = useState(null);
const [buyModalRecsLoading, setBuyModalRecsLoading] = useState(false);
const [buyQuantity, setBuyQuantity] = useState(1);
const navigate = useNavigate();
const fetchProfile = () => {
axios.get("/api/auth/me")
.then(res => setUserProfile(res.data))
.catch(err => {
if (err.response?.status === 401) handleLogout();
});
};
useEffect(() => {
fetchProfile();
}, []);
const handleLogout = () => {
localStorage.removeItem("token");
navigate("/login");
};
useEffect(() => {
if (researchModalData) {
setBuyModalRecs(null);
setBuyModalRecsLoading(true);
axios.get(`/api/portfolio/recommendations/${researchModalData.symbol}`)
.then(res => {
setBuyModalRecs(res.data);
setBuyModalRecsLoading(false);
})
.catch(err => {
console.error("Failed to fetch recs", err);
setBuyModalRecsLoading(false);
});
}
}, [researchModalData]);
const handleBuySubmit = async () => {
if (!buyModalData || buyQuantity < 1) return;
try {
await axios.post("/api/portfolio/buy", {
symbol: buyModalData.symbol,
quantity: buyQuantity,
buy_price: buyModalData.price
});
setBuyModalData(null);
setBuyQuantity(1);
} catch (err) {
console.error("Error buying stock:", err);
alert("Failed to buy stock");
}
};
const onBuyClick = (symbol, price) => {
setBuyModalData({ symbol, price });
setBuyQuantity(1);
};
const onResearchClick = (symbol) => {
setResearchModalData({ symbol });
};
return ( return (
<div className="app-container"> <div className="app-container">
<h1>📈 Stock Scanner Pro</h1> {/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Logo only, no text */}
<span style={{ fontSize: '2.5rem' }}>📊</span>
</div>
<div className="nav-container"> {userProfile && (
<button <div style={{ position: 'relative' }}>
className={`nav-btn ${view === "scanner" ? "active" : ""}`} <div
onClick={() => setView("scanner")} style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', background: 'var(--glass-bg)', padding: '6px 12px', borderRadius: '30px', border: '1px solid var(--border-color)', backdropFilter: 'blur(10px)' }}
> onClick={() => setDropdownOpen(!dropdownOpen)}
📊 Scanner >
</button> <div style={{ width: '30px', height: '30px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontWeight: 'bold', fontSize: '1rem', color: 'white' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>{userProfile.display_name || userProfile.username}</span>
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}></span>
</div>
<button {dropdownOpen && (
className={`nav-btn ${view === "ribbon" ? "active" : ""}`} <div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '50px', right: '0', width: '200px', zIndex: 100, padding: '8px' }}>
onClick={() => setView("ribbon")} <button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap', padding: '8px' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
> Profile Settings
📈 Ribbon Strategy </button>
</button> <button className="nav-btn" style={{ width: '100%', border: 'none', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', whiteSpace: 'nowrap', padding: '8px' }} onClick={handleLogout}>
🚪 Logout
</button>
</div>
)}
</div>
)}
</div>
<button <div className="nav-container" style={{ gap: '8px' }}>
className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} <button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '6px 12px' }}>🏠</button>
onClick={() => setView("smartmoney")} <button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📊 Scanner</button>
> <button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📈 Ribbon</button>
🏦 Smart Money <button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>🏦 Smart Money</button>
</button> <button className={`nav-btn ${view === "trend" ? "active" : ""}`} onClick={() => setView("trend")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}> Trend</button>
<button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📰 Intel</button>
<button <button className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📋 Watchlists</button>
className={`nav-btn ${view === "trend" ? "active" : ""}`}
onClick={() => setView("trend")}
>
Trend Analysis
</button>
<button
className={`nav-btn ${view === "marketintel" ? "active" : ""}`}
onClick={() => setView("marketintel")}
>
📰 Market Intel
</button>
</div> </div>
<div className="animate-fade-in"> <div className="animate-fade-in">
{view === "scanner" && ( {view === "scanner" && (
<> <>
<AboutScanner /> <Scanner
<Scanner onSelectStock={setSelectedStock} onSignal={setSignal} /> onSelectStock={setSelectedStock}
onSignal={setSignal}
onBuyClick={onBuyClick}
onResearchClick={onResearchClick}
/>
<div className="glass-card" style={{ marginTop: "20px" }}> <div className="glass-card" style={{ marginTop: "20px" }}>
<h2>TradingView Chart</h2> <h2>TradingView Chart</h2>
<Chart symbol={selectedStock} /> <Chart symbol={selectedStock} />
@@ -67,17 +151,146 @@ function App() {
{signal && <Popup signal={signal} />} {signal && <Popup signal={signal} />}
</> </>
)} )}
{view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "ribbon" && <Ribbon />} {view === "ribbon" && <Ribbon onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "smartmoney" && <SmartMoney />} {view === "marketintel" && <MarketIntel onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "trend" && <TrendScanner onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "marketintel" && <MarketIntel />} {view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
{view === "trend" && <TrendScanner />}
</div> </div>
{/* Buy Modal */}
{buyModalData && (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)',
display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999
}}>
<div style={{
background: 'var(--glass-bg)', padding: '30px', borderRadius: '15px',
border: '1px solid var(--border-color)', width: 'min(350px, 90vw)', textAlign: 'center',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)'
}}>
<h2 style={{ marginBottom: '10px' }}>Mock Buy</h2>
<h3 style={{ color: 'var(--accent-green)', margin: '10px 0', fontSize: '1.8rem' }}>{buyModalData.symbol}</h3>
<p style={{ color: 'var(--text-secondary)' }}>Current Price: <strong style={{ color: 'var(--text-color)' }}>{buyModalData.price}</strong></p>
<div style={{ margin: '25px 0', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '15px' }}>
<label style={{ fontWeight: '500' }}>Quantity: </label>
<input
type="number"
value={buyQuantity}
onChange={(e) => setBuyQuantity(parseInt(e.target.value) || 1)}
min="1"
style={{
background: 'var(--search-bg)', border: '1px solid var(--border-color)',
color: 'var(--text-color)', padding: '10px', borderRadius: '8px', width: '100px',
fontSize: '1.1rem', textAlign: 'center'
}}
/>
</div>
<div style={{ margin: '20px 0', padding: '15px', background: 'rgba(0,0,0,0.2)', borderRadius: '10px' }}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Total Investment</span>
<p style={{ fontWeight: 'bold', fontSize: '1.4rem', margin: '5px 0 0 0', color: 'var(--accent-blue)' }}>
{(buyModalData.price * buyQuantity).toFixed(2)}
</p>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '15px', marginTop: '25px' }}>
<button className="nav-btn" style={{ flex: 1, padding: '12px' }} onClick={() => setBuyModalData(null)}>Cancel</button>
<button className="action-btn" onClick={handleBuySubmit} style={{ flex: 1, padding: '12px', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', color: '#1a1a2e', border: 'none', fontWeight: 'bold' }}>Confirm Buy</button>
</div>
</div>
</div>
)}
{/* Research Modal */}
{researchModalData && (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)',
display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999
}}>
<div style={{
background: 'var(--glass-bg)', padding: '30px', borderRadius: '15px',
border: '1px solid var(--border-color)', width: 'min(400px, 90vw)', textAlign: 'center',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)'
}}>
<h2 style={{ marginBottom: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
<span>🔍</span> Analyst Research
</h2>
<h3 style={{ color: 'var(--accent-blue)', margin: '10px 0', fontSize: '1.8rem' }}>{researchModalData.symbol}</h3>
{/* Analyst Recommendations */}
<div style={{ margin: '20px 0', padding: '15px', background: 'var(--bg-secondary)', borderRadius: '10px', border: '1px solid var(--border-color)', textAlign: 'left' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 'bold', color: 'var(--text-primary)' }}>Analyst Consensus</span>
{buyModalRecsLoading && <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Analyzing...</span>}
{buyModalRecs && !buyModalRecs.error && (
<span className={`badge ${buyModalRecs.confidence >= 70 ? 'badge-success' : buyModalRecs.confidence >= 40 ? 'badge-warning' : 'badge-danger'}`} style={{ fontSize: '0.9rem' }}>
{buyModalRecs.confidence}% Confidence
</span>
)}
</div>
{buyModalRecs && buyModalRecs.error && (
<div style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', textAlign: 'center', padding: '10px 0' }}>
No sufficient analyst coverage found.
</div>
)}
{buyModalRecs && !buyModalRecs.error && (
<div style={{ display: 'flex', gap: '8px', fontSize: '0.85rem', textAlign: 'center' }}>
{(buyModalRecs.strongBuy + buyModalRecs.buy) > 0 && (
<div style={{ flex: buyModalRecs.strongBuy + buyModalRecs.buy, background: 'var(--accent-green-bg)', color: 'var(--accent-green)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Buy<br/>({buyModalRecs.strongBuy + buyModalRecs.buy})
</div>
)}
{buyModalRecs.hold > 0 && (
<div style={{ flex: buyModalRecs.hold, background: 'var(--accent-orange-bg)', color: 'var(--accent-orange)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Hold<br/>({buyModalRecs.hold})
</div>
)}
{(buyModalRecs.sell + buyModalRecs.strongSell) > 0 && (
<div style={{ flex: buyModalRecs.sell + buyModalRecs.strongSell, background: 'var(--accent-red-bg)', color: 'var(--accent-red)', padding: '8px 4px', borderRadius: '6px', fontWeight: 'bold' }}>
Sell<br/>({buyModalRecs.sell + buyModalRecs.strongSell})
</div>
)}
</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', marginTop: '25px' }}>
<a
href={`https://trendlyne.com/research-reports/stock/${researchModalData.symbol.replace('.NS', '')}`}
target="_blank"
rel="noreferrer"
className="action-btn"
style={{ padding: '12px', background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border-color)', fontWeight: 'bold', textDecoration: 'none' }}
>
Read Detailed Trendlyne Reports
</a>
<button className="nav-btn" style={{ padding: '12px', margin: 0, justifyContent: 'center' }} onClick={() => setResearchModalData(null)}>Close</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
</Routes>
</Router>
);
}
export default App; export default App;

View File

@@ -1,6 +1,14 @@
import React from "react"; import React, { useState, useEffect } from "react";
function AboutScanner({ isLoaded }) {
const [isCollapsed, setIsCollapsed] = useState(false);
useEffect(() => {
if (isLoaded) {
setIsCollapsed(true);
}
}, [isLoaded]);
function AboutScanner() {
return ( return (
<div <div
style={{ style={{
@@ -11,7 +19,16 @@ function AboutScanner() {
color: "white" color: "white"
}} }}
> >
<h2>🧠 How The Scanner Works</h2> <div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<h2 style={{ margin: 0 }}>🧠 How The Scanner Works</h2>
<span style={{ fontSize: "1.2rem", padding: "5px" }}>{isCollapsed ? "▼" : "▲"}</span>
</div>
{!isCollapsed && (
<div className="animate-fade-in" style={{ marginTop: "15px" }}>
<ul> <ul>
<li> <li>
@@ -64,6 +81,8 @@ function AboutScanner() {
<li>C (60-69) = Average</li> <li>C (60-69) = Average</li>
<li>D (&lt;60) = Avoid</li> <li>D (&lt;60) = Avoid</li>
</ul> </ul>
</div>
)}
</div> </div>
); );
} }

View File

@@ -7,7 +7,7 @@ function Backtest() {
// ✅ Fetch backtest data // ✅ Fetch backtest data
const loadBacktest = async () => { const loadBacktest = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/backtest"); const res = await axios.get("/api/backtest");
setData(res.data); setData(res.data);
} catch (err) { } catch (err) {
console.error("Backtest Error:", err); console.error("Backtest Error:", err);

70
src/components/Login.js Normal file
View File

@@ -0,0 +1,70 @@
import React, { useState } from "react";
import axios from "axios";
import { useNavigate, Link } from "react-router-dom";
import forge from "node-forge";
function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
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("/api/auth/login", {
username,
password: encryptedBase64
});
localStorage.setItem("token", res.data.access_token);
navigate("/");
} catch (err) {
setError(err.response?.data?.detail || "Login failed");
} finally {
setLoading(false);
}
};
return (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-blue">📈 Login</h2>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center' }} disabled={loading}>
{loading ? "Logging in..." : "Login"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Don't have an account? <Link to="/signup" className="text-blue">Sign up</Link></p>
</div>
</div>
);
}
export default Login;

View File

@@ -6,7 +6,7 @@ function LongTerm() {
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/longterm"); const res = await axios.get("/api/longterm");
setData(res.data); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@@ -0,0 +1,282 @@
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
function MainDashboard({ userProfile, onBuyClick, onResearchClick }) {
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [searching, setSearching] = useState(false);
const searchTimeout = useRef(null);
const [portfolio, setPortfolio] = useState([]);
const [portfolioSummary, setPortfolioSummary] = useState(null);
const [loadingPortfolio, setLoadingPortfolio] = useState(true);
useEffect(() => {
fetchPortfolio();
}, []);
const fetchPortfolio = async () => {
try {
const res = await axios.get("/api/portfolio/");
setPortfolio(res.data.items);
setPortfolioSummary(res.data.summary);
} catch (err) {
console.error("Error fetching portfolio:", err);
}
setLoadingPortfolio(false);
};
const sellStock = async (id) => {
try {
await axios.delete(`/api/portfolio/${id}`);
fetchPortfolio(); // refresh
} catch (err) {
console.error("Error selling stock:", err);
}
};
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 (
<div className="animate-fade-in" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '30px' }}>
<div style={{ width: '100%', position: 'relative' }}>
<div style={{ position: 'relative' }}>
<span style={{ position: 'absolute', left: '20px', top: '50%', transform: 'translateY(-50%)', fontSize: '1.5rem', color: 'var(--text-muted)' }}>🔍</span>
<input
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder="Search Indian stocks (e.g. TATAMOTORS, INFY)..."
style={{
width: '100%',
padding: '20px 20px 20px 60px',
borderRadius: '30px',
border: '2px solid var(--accent-blue)',
background: 'rgba(0,0,0,0.4)',
color: 'white',
fontSize: '1.2rem',
boxShadow: '0 8px 32px rgba(59, 130, 246, 0.2)',
outline: 'none',
transition: 'all 0.3s'
}}
/>
{searching && <span style={{ position: 'absolute', right: '25px', top: '50%', transform: 'translateY(-50%)', color: 'var(--accent-blue)' }}>Searching...</span>}
</div>
{/* Search Results Dropdown */}
{searchResults.length > 0 && searchQuery.length >= 2 && (
<div className="glass-card" style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 100, marginTop: '10px', maxHeight: '400px', overflowY: 'auto', padding: '10px', borderRadius: '20px' }}>
{searchResults.map((result, i) => (
<div
key={i}
onClick={() => 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)'}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<strong style={{ color: 'var(--accent-blue)', fontSize: '1.2rem' }}>{result.symbol}</strong>
{result.sector && (
<span style={{ fontSize: '0.8rem', background: 'rgba(59, 130, 246, 0.2)', color: 'var(--accent-blue)', padding: '4px 8px', borderRadius: '6px', border: '1px solid var(--accent-blue)' }}>
{result.sector}
</span>
)}
{result.marketCap && (
<span style={{ fontSize: '0.8rem', background: 'rgba(16, 185, 129, 0.2)', color: 'var(--accent-green)', padding: '4px 8px', borderRadius: '6px', border: '1px solid var(--accent-green)' }}>
{getMarketCapCategory(result.marketCap)}
</span>
)}
</div>
<div className="text-small text-muted" style={{ display: 'flex', gap: '10px', marginTop: '6px', fontSize: '0.9rem' }}>
<span>{result.longname || result.shortname}</span>
{result.currentPrice && (
<>
<span></span>
<span style={{ color: 'var(--text-primary)' }}>Price: {formatPrice(result.currentPrice)}</span>
</>
)}
{result.marketCap && (
<>
<span></span>
<span>{formatMarketCap(result.marketCap)}</span>
</>
)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<button
className="action-btn"
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 10px', fontSize: '0.8rem', fontWeight: 'bold', borderRadius: '8px', border: 'none', cursor: 'pointer' }}
onClick={(e) => { e.stopPropagation(); onBuyClick(result.symbol, result.currentPrice); }}
>
Buy
</button>
<button
className="action-btn"
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', padding: '4px 10px', fontSize: '0.8rem', fontWeight: 'bold', borderRadius: '8px', cursor: 'pointer' }}
onClick={(e) => { e.stopPropagation(); onResearchClick(result.symbol); }}
>
Research
</button>
<div style={{ color: 'var(--accent-green)', display: 'flex', alignItems: 'center', gap: '5px' }}>
<span>Chart</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="13" x2="18" y2="19"></line>
<polyline points="18 5 18 11 12 11"></polyline>
<polyline points="12 19 12 5 6 5"></polyline>
</svg>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Portfolio Section */}
<div style={{ width: '100%', marginTop: '50px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ display: 'flex', alignItems: 'center', gap: '10px' }}><span style={{ fontSize: '1.8rem' }}>💼</span> Mock Portfolio</h2>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>As of: {new Date().toLocaleDateString()}</span>
</div>
{loadingPortfolio ? (
<p>Loading portfolio...</p>
) : portfolio.length === 0 ? (
<div className="glass-card" style={{ textAlign: 'center', padding: '40px' }}>
<p style={{ color: 'var(--text-muted)' }}>Your mock portfolio is empty.</p>
<p style={{ fontSize: '0.9rem', color: 'var(--text-secondary)' }}>Search for a stock or run a scanner to simulate buying.</p>
</div>
) : (
<>
{/* Summary Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px', marginBottom: '30px' }}>
<div className="glass-card" style={{ textAlign: 'center' }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Total Invested</p>
<h3 style={{ margin: 0 }}>{portfolioSummary.total_invested.toLocaleString()}</h3>
</div>
<div className="glass-card" style={{ textAlign: 'center' }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Current Value</p>
<h3 style={{ margin: 0, color: 'var(--accent-blue)' }}>{portfolioSummary.total_current_value.toLocaleString()}</h3>
</div>
<div className="glass-card" style={{ textAlign: 'center', border: `1px solid ${portfolioSummary.total_pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)'}` }}>
<p style={{ color: 'var(--text-secondary)', margin: '0 0 10px 0' }}>Overall P&L</p>
<h3 style={{ margin: 0, color: portfolioSummary.total_pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)' }}>
{portfolioSummary.total_pnl >= 0 ? '+' : ''}{portfolioSummary.total_pnl.toLocaleString()}
<span style={{ fontSize: '1rem', marginLeft: '8px' }}>({portfolioSummary.total_pnl_percent.toFixed(2)}%)</span>
</h3>
</div>
</div>
{/* Holdings Table */}
<div className="glass-card" style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr>
<th>Symbol</th>
<th>Qty</th>
<th>DOP</th>
<th>Buy Price</th>
<th>Invested</th>
<th>LTP</th>
<th>Current Value</th>
<th>P&L</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{portfolio.map(item => (
<tr key={item.id}>
<td style={{ fontWeight: 'bold', color: 'var(--accent-blue)', cursor: 'pointer' }} onClick={() => openChart(item.symbol)}>
{item.symbol}
</td>
<td>{item.quantity}</td>
<td style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
<div>{new Date(item.purchase_date).toLocaleDateString()}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--accent-blue)', marginTop: '2px' }}>
{Math.max(0, Math.floor((new Date() - new Date(item.purchase_date)) / (1000 * 60 * 60 * 24)))} days
</div>
</td>
<td>{item.buy_price.toFixed(2)}</td>
<td>{item.invested.toLocaleString()}</td>
<td>{item.current_price.toFixed(2)}</td>
<td>{item.current_value.toLocaleString()}</td>
<td style={{ color: item.pnl >= 0 ? 'var(--accent-green)' : 'var(--accent-red)', fontWeight: 'bold' }}>
{item.pnl >= 0 ? '+' : ''}{item.pnl.toLocaleString()} ({item.pnl_percent.toFixed(2)}%)
</td>
<td>
<button className="delete-btn" onClick={() => sellStock(item.id)} title="Sell / Remove">Sell</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
);
}
export default MainDashboard;

View File

@@ -10,7 +10,7 @@ function MarketIntel() {
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/marketintel"); const res = await axios.get("/api/scanner/marketintel");
setData(res.data); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

173
src/components/Profile.js Normal file
View File

@@ -0,0 +1,173 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import forge from "node-forge";
function Profile({ userProfile, onProfileUpdated }) {
const [formData, setFormData] = useState({
display_name: "",
email_id: "",
mobile_no: "",
gender: "",
password: ""
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
if (userProfile) {
setFormData({
display_name: userProfile.display_name || "",
email_id: userProfile.email_id || "",
mobile_no: userProfile.mobile_no || "",
gender: userProfile.gender || "",
password: "" // password field is blank initially
});
}
}, [userProfile]);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSave = async (e) => {
e.preventDefault();
setLoading(true);
setMessage("");
setError("");
try {
const payload = { ...formData };
if (payload.password) {
// Fetch Public Key to encrypt new password
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);
} else {
delete payload.password;
}
await axios.put("/api/auth/update-profile", payload);
setMessage("Profile updated successfully! ✅");
if (onProfileUpdated) onProfileUpdated();
setFormData(prev => ({ ...prev, password: "" })); // Clear password field
} catch (err) {
setError(err.response?.data?.detail || "Failed to update profile");
} finally {
setLoading(false);
}
};
if (!userProfile) return <div className="glass-card text-center">Loading Profile...</div>;
return (
<div className="animate-fade-in" style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
<div className="glass-card" style={{ width: '100%', maxWidth: '600px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '30px' }}>
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '2.5rem', fontWeight: 'bold', color: 'white', boxShadow: 'var(--shadow-glow)' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<div>
<h2 style={{ margin: 0, fontSize: '1.8rem' }} className="text-blue">Profile Settings</h2>
<p className="text-muted" style={{ margin: '5px 0 0 0' }}>Manage your personal details and security</p>
</div>
</div>
<form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Username (Read Only)</label>
<input
type="text"
value={userProfile.username}
disabled
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'rgba(0,0,0,0.2)', color: 'var(--text-muted)', cursor: 'not-allowed' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Display Name</label>
<input
type="text" name="display_name" value={formData.display_name} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Enter your name"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Email Address</label>
<input
type="email" name="email_id" value={formData.email_id} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="your.email@example.com"
autoComplete="off"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Mobile Number</label>
<input
type="text" name="mobile_no" value={formData.mobile_no} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="+91 9999999999"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Gender</label>
<select
name="gender" value={formData.gender} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
>
<option value="">Select Gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div style={{ borderTop: '1px solid var(--border-color)', margin: '10px 0' }}></div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-orange" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Change Password (Optional)</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleChange}
style={{ width: '100%', padding: '12px', paddingRight: '45px', borderRadius: '8px', border: '1px solid rgba(245, 158, 11, 0.3)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Leave blank to keep current password"
autoComplete="new-password"
/>
<span
onClick={() => setShowPassword(!showPassword)}
style={{ position: 'absolute', right: '15px', cursor: 'pointer', fontSize: '1.2rem', userSelect: 'none' }}
title={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? "🙈" : "👁️"}
</span>
</div>
</div>
{message && <div style={{ padding: '10px', background: 'var(--accent-green-bg)', color: 'var(--accent-green)', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}>{message}</div>}
{error && <div style={{ padding: '10px', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', borderRadius: '8px', border: '1px solid rgba(239, 68, 68, 0.2)' }}>{error}</div>}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '10px' }}>
<button type="submit" className="nav-btn active" style={{ padding: '12px 30px', fontSize: '1rem' }} disabled={loading}>
{loading ? "Saving..." : "Save Changes"}
</button>
</div>
</form>
</div>
</div>
);
}
export default Profile;

View File

@@ -0,0 +1,12 @@
import React from "react";
import { Navigate } from "react-router-dom";
function ProtectedRoute({ children }) {
const token = localStorage.getItem("token");
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute;

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
function Ribbon() { function Ribbon({ onBuyClick, onResearchClick }) {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/ribbon"); const res = await axios.get("/api/scanner/ribbon");
setData(res.data || []); setData(res.data || []);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
@@ -26,7 +26,18 @@ function Ribbon() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
}; };
if (loading) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Ribbon...</div>; 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 <div className="glass-card" style={{ textAlign: "center" }}>Loading Ribbon (this may take up to 60 seconds for large watchlists)...</div>;
return ( return (
<div className="glass-card animate-fade-in"> <div className="glass-card animate-fade-in">
@@ -65,9 +76,35 @@ function Ribbon() {
</span> </span>
</td> </td>
<td> <td>
<button className="action-btn" onClick={() => openChart(s.symbol)}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
Chart <button
</button> onClick={() => onBuyClick(s.symbol, s.price)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>
Chart
</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import AboutScanner from "./AboutScanner";
function Scanner({ onSelectStock, onSignal }) { function Scanner({ onSelectStock, onSignal, onBuyClick, onResearchClick }) {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [lastBest, setLastBest] = useState(null); const [lastBest, setLastBest] = useState(null);
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/scan"); const res = await axios.get("/api/scanner/scan");
const newData = res.data; const newData = res.data;
const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null; const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null;
@@ -33,14 +34,41 @@ function Scanner({ onSelectStock, onSignal }) {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
}; };
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Scanner Data...</div>; 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 (
<>
<AboutScanner isLoaded={false} />
<div className="glass-card" style={{ textAlign: "center" }}>Loading Scanner Data (this may take up to 60 seconds for large watchlists)...</div>
</>
);
return ( return (
<div> <div>
<AboutScanner isLoaded={true} />
{/* Top 5 Trades */} {/* Top 5 Trades */}
{(data.best_5 || []).length > 0 && ( {(data.best_5 || []).length > 0 && (
<div className="glass-card animate-fade-in" style={{ marginBottom: "30px", borderLeft: "4px solid var(--accent-green)" }}> <div className="glass-card animate-fade-in" style={{ marginBottom: "30px", borderLeft: "4px solid var(--accent-green)" }}>
<h2 className="text-green">🔥 Top 5 Momentum Picks</h2> <h2 className="text-green">🔥 Top 10 Momentum Picks</h2>
<div className="data-table-container"> <div className="data-table-container">
<table className="data-table"> <table className="data-table">
<thead> <thead>
@@ -72,9 +100,35 @@ function Scanner({ onSelectStock, onSignal }) {
<td>{s.grade}</td> <td>{s.grade}</td>
<td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td> <td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td>
<td> <td>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
View Chart <button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
</button> View Chart
</button>
<button
onClick={() => onBuyClick(s?.symbol, s?.price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s?.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
{s?.sector && (
<button
onClick={() => removeStockFromSector(s?.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td> </td>
</tr> </tr>
))} ))}
@@ -125,9 +179,28 @@ function Scanner({ onSelectStock, onSignal }) {
<td className="text-green">{s?.target}</td> <td className="text-green">{s?.target}</td>
<td>{s?.ai_score}</td> <td>{s?.ai_score}</td>
<td> <td>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
Chart <button
</button> className="action-btn"
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
onClick={() => onBuyClick(s?.symbol, s?.price)}
>
Buy
</button>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
Chart
</button>
<button
onClick={() => removeStockFromSector(s?.symbol, sector)}
title={`Remove from ${sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</td> </td>
</tr> </tr>
))} ))}

69
src/components/Signup.js Normal file
View File

@@ -0,0 +1,69 @@
import React, { useState } from "react";
import axios from "axios";
import { useNavigate, Link } from "react-router-dom";
import forge from "node-forge";
function Signup() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleSignup = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
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("/api/auth/signup", {
username,
password: encryptedBase64
});
navigate("/login");
} catch (err) {
setError(err.response?.data?.detail || "Signup failed");
} finally {
setLoading(false);
}
};
return (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-green"> Sign Up</h2>
<form onSubmit={handleSignup} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center', background: 'var(--accent-green)', borderColor: 'var(--accent-green)' }} disabled={loading}>
{loading ? "Signing up..." : "Sign Up"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Already have an account? <Link to="/login" className="text-blue">Login</Link></p>
</div>
</div>
);
}
export default Signup;

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
function SmartMoney() { function SmartMoney({ onBuyClick, onResearchClick }) {
const [data, setData] = useState(null); const [data, setData] = useState(null);
useEffect(() => { useEffect(() => {
@@ -12,7 +12,7 @@ function SmartMoney() {
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/smartmoney"); const res = await axios.get("/api/scanner/smartmoney");
setData(res.data); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -23,7 +23,30 @@ function SmartMoney() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
}; };
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Smart Money Data...</div>; 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 <div className="glass-card" style={{ textAlign: "center" }}>Loading Smart Money Data (this may take up to 60 seconds for large watchlists)...</div>;
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
@@ -80,7 +103,33 @@ function SmartMoney() {
<td>{s.breakout ? "🔥 YES" : "NO"}</td> <td>{s.breakout ? "🔥 YES" : "NO"}</td>
<td>{s.reason}</td> <td>{s.reason}</td>
<td> <td>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
function TrendScanner() { function TrendScanner({ onBuyClick, onResearchClick }) {
const [data, setData] = useState(null); const [data, setData] = useState(null);
useEffect(() => { useEffect(() => {
@@ -12,7 +12,7 @@ function TrendScanner() {
const loadData = async () => { const loadData = async () => {
try { try {
const res = await axios.get("http://127.0.0.1:8000/trend"); const res = await axios.get("/api/scanner/trend");
setData(res.data); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -23,7 +23,24 @@ function TrendScanner() {
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank"); window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
}; };
if (!data) return <div className="glass-card" style={{ textAlign: "center" }}>Loading Trend Data...</div>; 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 <div className="glass-card" style={{ textAlign: "center" }}>Loading Trend Data (this may take up to 60 seconds for large watchlists)...</div>;
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
@@ -63,7 +80,33 @@ function TrendScanner() {
</td> </td>
<td>{s.trend_reason}</td> <td>{s.trend_reason}</td>
<td> <td>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(s.symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
{s.sector && (
<button
onClick={() => removeStockFromSector(s.symbol, s.sector)}
title={`Remove from ${s.sector}`}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -0,0 +1,293 @@
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick, onResearchClick }) {
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 (
<div className="animate-fade-in" style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', marginTop: '20px', alignItems: 'flex-start' }}>
{/* LEFT PANEL: Sector List & Creator */}
<div className="glass-card" style={{ flex: '1', minWidth: '250px' }}>
<h3 className="text-blue" style={{ marginTop: 0 }}>📋 Watchlists</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}>
{Object.keys(sectorMap).map(sector => (
<div
key={sector}
onClick={() => 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'
}}
>
<span style={{ fontWeight: selectedSector === sector ? 'bold' : 'normal', color: selectedSector === sector ? 'var(--accent-blue)' : 'white' }}>
{sector} <span className="text-muted text-small">({sectorMap[sector].length})</span>
</span>
{sector !== "Personal" && (
<button onClick={(e) => { e.stopPropagation(); deleteSector(sector); }} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '5px' }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
)}
</div>
))}
</div>
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '15px' }}>
<p className="text-small text-muted" style={{ margin: '0 0 10px 0' }}>Create new watchlist</p>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text" value={newSectorName} onChange={e => 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' }}
/>
<button onClick={createSector} className="nav-btn active" style={{ padding: '0 15px' }}>Add</button>
</div>
</div>
</div>
{/* RIGHT PANEL: Search & Stock List */}
<div className="glass-card" style={{ flex: '3' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 style={{ margin: 0, color: selectedSector === 'Personal' ? 'var(--accent-green)' : 'white' }}>
{selectedSector} Watchlist
</h2>
{saving && <span className="text-muted">Saving...</span>}
{message && <span className="text-green">{message}</span>}
</div>
{/* Search Bar */}
<div style={{ position: 'relative', marginBottom: '30px' }}>
<input
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder={`Search Indian stocks to add to ${selectedSector}... (e.g. TATAMOTORS)`}
style={{ width: '100%', padding: '15px', borderRadius: '8px', border: '1px solid var(--accent-blue)', background: 'rgba(0,0,0,0.3)', color: 'white', fontSize: '1.1rem' }}
/>
{searching && <span style={{ position: 'absolute', right: '15px', top: '15px' }}>Searching...</span>}
{/* Search Results Dropdown */}
{searchResults.length > 0 && searchQuery.length >= 2 && (
<div className="glass-card" style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 10, marginTop: '5px', maxHeight: '300px', overflowY: 'auto', padding: '5px' }}>
{searchResults.map((result, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', borderBottom: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', marginBottom: '5px', borderRadius: '5px' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<strong style={{ color: 'var(--accent-blue)', fontSize: '1.1rem' }}>{result.symbol}</strong>
{result.sector && (
<span style={{ fontSize: '0.75rem', background: 'rgba(59, 130, 246, 0.2)', color: 'var(--accent-blue)', padding: '2px 6px', borderRadius: '4px', border: '1px solid var(--accent-blue)' }}>
{result.sector}
</span>
)}
{result.marketCap && (
<span style={{ fontSize: '0.75rem', background: 'rgba(16, 185, 129, 0.2)', color: 'var(--accent-green)', padding: '2px 6px', borderRadius: '4px', border: '1px solid var(--accent-green)' }}>
{getMarketCapCategory(result.marketCap)}
</span>
)}
</div>
<div className="text-small text-muted" style={{ display: 'flex', gap: '10px', marginTop: '4px' }}>
<span>{result.longname || result.shortname}</span>
{result.currentPrice && (
<>
<span></span>
<span style={{ color: 'var(--text-primary)' }}>Price: {formatPrice(result.currentPrice)}</span>
</>
)}
{result.marketCap && (
<>
<span></span>
<span>{formatMarketCap(result.marketCap)}</span>
</>
)}
</div>
</div>
<button
onClick={() => { addStockToSector(result.symbol, selectedSector); setSearchResults([]); setSearchQuery(""); }}
style={{ background: 'var(--accent-green)', color: 'white', border: 'none', borderRadius: '50%', width: '35px', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center', cursor: 'pointer', fontSize: '1.2rem', flexShrink: 0, boxShadow: '0 4px 6px rgba(16, 185, 129, 0.3)' }}
title={`Add to ${selectedSector}`}
>
+
</button>
</div>
))}
</div>
)}
</div>
{/* Stock List Grid */}
{sectorMap[selectedSector] && sectorMap[selectedSector].length === 0 ? (
<div className="text-center text-muted" style={{ padding: '40px' }}>No stocks in this watchlist. Search above to add some!</div>
) : (
<div style={{ maxHeight: '60vh', overflowY: 'auto', paddingRight: '10px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '15px' }}>
{(sectorMap[selectedSector] || []).map(symbol => (
<div key={symbol} style={{ display: 'flex', flexDirection: 'column', gap: '10px', background: 'var(--bg-tertiary)', padding: '12px 15px', borderRadius: '8px', border: '1px solid var(--border-color)' }}>
{/* Top Row: Symbol and Trash */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 'bold' }}>{symbol}</span>
<button
onClick={() => removeStockFromSector(symbol, selectedSector)}
title="Remove from this watchlist"
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: '0', display: 'flex', alignItems: 'center' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
{/* Bottom Row: Actions */}
<div style={{ display: 'flex', gap: '10px' }}>
{selectedSector !== "Personal" && (
<button
onClick={() => { addStockToSector(symbol, "Personal"); }}
title="Add to Personal"
style={{ flex: 1, background: 'rgba(16, 185, 129, 0.1)', border: '1px solid var(--accent-green)', color: 'var(--accent-green)', borderRadius: '4px', cursor: 'pointer', padding: '6px', fontSize: '0.85rem' }}
>
+ Personal
</button>
)}
<button
onClick={() => onBuyClick(symbol, 0)}
style={{ background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Buy
</button>
<button
onClick={() => onResearchClick(symbol)}
style={{ background: 'transparent', color: 'var(--accent-blue)', border: '1px solid var(--accent-blue)', borderRadius: '4px', cursor: 'pointer', padding: '6px 12px', fontSize: '0.85rem', fontWeight: 'bold' }}
>
Research
</button>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
export default WatchlistManager;

View File

@@ -83,9 +83,9 @@ h2 {
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-primary); color: var(--text-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 10px 20px; padding: 8px 14px;
border-radius: 8px; border-radius: 8px;
font-size: 0.95rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -214,3 +214,54 @@ h2 {
.text-orange { color: var(--accent-orange); } .text-orange { color: var(--accent-orange); }
.text-blue { color: var(--accent-blue); } .text-blue { color: var(--accent-blue); }
.text-muted { color: var(--text-muted); } .text-muted { color: var(--text-muted); }
/* RESPONSIVE DESIGN */
@media (max-width: 768px) {
.app-container {
padding: 20px 10px;
}
.nav-container {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 10px;
margin-bottom: 25px;
}
.nav-btn {
white-space: nowrap;
flex-shrink: 0;
}
.glass-card {
padding: 16px;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.3rem; }
.data-table th, .data-table td {
padding: 10px 8px;
font-size: 0.85rem;
}
/* Adjust grid columns for extreme small devices */
.stock-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.app-container {
padding: 15px 8px;
}
h1 { font-size: 1.8rem; }
.action-btn {
padding: 4px 8px;
font-size: 0.75rem;
}
}

View File

@@ -1,8 +1,17 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; import App from './App';
import axios from 'axios';
const root = ReactDOM.createRoot(document.getElementById("root")); axios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />