3 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
40 changed files with 2082 additions and 609 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.

View File

@@ -26,6 +26,9 @@ class ProfileUpdateRequest(BaseModel):
gender: str = None gender: str = None
password: str = None # Base64 encoded RSA-encrypted new password password: str = None # Base64 encoded RSA-encrypted new password
class SectorMapUpdateRequest(BaseModel):
sector_map: dict
@router.get("/public-key") @router.get("/public-key")
def get_public_key(): def get_public_key():
return {"public_key": public_key.decode("utf-8")} return {"public_key": public_key.decode("utf-8")}
@@ -84,9 +87,16 @@ def get_me(current_user: User = Depends(get_current_user)):
"display_name": current_user.display_name, "display_name": current_user.display_name,
"email_id": current_user.email_id, "email_id": current_user.email_id,
"mobile_no": current_user.mobile_no, "mobile_no": current_user.mobile_no,
"gender": current_user.gender "gender": current_user.gender,
"sector_map": current_user.sector_map
} }
@router.put("/update-sectors")
def update_sectors(req: SectorMapUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
current_user.sector_map = req.sector_map
db.commit()
return {"message": "Sectors updated successfully"}
@router.put("/update-profile") @router.put("/update-profile")
def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
if req.display_name is not None: if req.display_name is not None:

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

@@ -4,24 +4,56 @@ from app.services.stock_engine import (
get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel
) )
import requests
router = APIRouter() router = APIRouter()
@router.get("/scan") @router.get("/scan")
def scan(current_user = Depends(get_current_user)): def scan(current_user = Depends(get_current_user)):
return get_scan() return get_scan(current_user.sector_map or {})
@router.get("/ribbon") @router.get("/ribbon")
def ribbon(current_user = Depends(get_current_user)): def ribbon(current_user = Depends(get_current_user)):
return get_ribbon() return get_ribbon(current_user.sector_map or {})
@router.get("/smartmoney") @router.get("/smartmoney")
def smartmoney(current_user = Depends(get_current_user)): def smartmoney(current_user = Depends(get_current_user)):
return get_smartmoney() return get_smartmoney(current_user.sector_map or {})
@router.get("/trend") @router.get("/trend")
def trend(current_user = Depends(get_current_user)): def trend(current_user = Depends(get_current_user)):
return get_trend() return get_trend(current_user.sector_map or {})
@router.get("/marketintel") @router.get("/marketintel")
def marketintel(current_user = Depends(get_current_user)): def marketintel(current_user = Depends(get_current_user)):
return get_marketintel() 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 []

View File

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

View File

@@ -1,5 +1,6 @@
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String, JSON, Float, ForeignKey, DateTime
from app.db.database import Base from app.db.database import Base
import datetime
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
@@ -11,3 +12,14 @@ class User(Base):
email_id = Column(String, nullable=True) email_id = Column(String, nullable=True)
mobile_no = Column(String, nullable=True) mobile_no = Column(String, nullable=True)
gender = 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)

View File

@@ -1,6 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api import auth, scanner from app.api import auth, scanner, portfolio
from app.db.database import engine, Base from app.db.database import engine, Base
# Initialize the database tables # Initialize the database tables
@@ -20,6 +20,7 @@ app.add_middleware(
# Include Routers # Include Routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(scanner.router, prefix="/api/scanner", tags=["scanner"]) app.include_router(scanner.router, prefix="/api/scanner", tags=["scanner"])
app.include_router(portfolio.router, prefix="/api/portfolio", tags=["portfolio"])
@app.get("/") @app.get("/")
def read_root(): def read_root():

View File

@@ -425,12 +425,12 @@ def analyze(stock):
# ========================================= # =========================================
# ✅ API - FAST SCAN # ✅ API - FAST SCAN
# ========================================= # =========================================
def get_scan(): def get_scan(user_sector_map):
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]
@@ -536,26 +536,28 @@ def analyze_ribbon(stock):
return None return None
def get_ribbon(): def get_ribbon(user_sector_map):
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
def get_smartmoney(): def get_smartmoney(user_sector_map):
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)
@@ -575,15 +577,15 @@ def get_smartmoney():
"all": results "all": results
} }
def get_trend(): def get_trend(user_sector_map):
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(

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

View File

@@ -22,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",

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

@@ -9,22 +9,30 @@ 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 Login from "./components/Login";
import Signup from "./components/Signup"; import Signup from "./components/Signup";
import ProtectedRoute from "./components/ProtectedRoute"; import ProtectedRoute from "./components/ProtectedRoute";
import Profile from "./components/Profile"; import Profile from "./components/Profile";
import MainDashboard from "./components/MainDashboard";
import "./index.css"; import "./index.css";
function Dashboard() { 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 [userProfile, setUserProfile] = useState(null);
const [dropdownOpen, setDropdownOpen] = useState(false); 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 navigate = useNavigate();
const fetchProfile = () => { const fetchProfile = () => {
axios.get("http://127.0.0.1:8000/api/auth/me") axios.get("/api/auth/me")
.then(res => setUserProfile(res.data)) .then(res => setUserProfile(res.data))
.catch(err => { .catch(err => {
if (err.response?.status === 401) handleLogout(); if (err.response?.status === 401) handleLogout();
@@ -40,31 +48,75 @@ function Dashboard() {
navigate("/login"); 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">
{/* HEADER */} {/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
<h1 style={{ margin: 0 }}>📈 Stock Scanner Pro</h1> <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Logo only, no text */}
<span style={{ fontSize: '2.5rem' }}>📊</span>
</div>
{userProfile && ( {userProfile && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div <div
style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', background: 'var(--glass-bg)', padding: '8px 16px', borderRadius: '30px', border: '1px solid var(--border-color)', backdropFilter: 'blur(10px)' }} 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)} onClick={() => setDropdownOpen(!dropdownOpen)}
> >
<div style={{ width: '35px', height: '35px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontWeight: 'bold', fontSize: '1.2rem', color: 'white' }}> <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() : '?'} {userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div> </div>
<span style={{ fontWeight: 600 }}>{userProfile.display_name || userProfile.username}</span> <span style={{ fontWeight: 600, fontSize: '0.9rem' }}>{userProfile.display_name || userProfile.username}</span>
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}></span> <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}></span>
</div> </div>
{dropdownOpen && ( {dropdownOpen && (
<div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '55px', right: '0', width: '220px', zIndex: 100, padding: '10px' }}> <div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '50px', right: '0', width: '200px', zIndex: 100, padding: '8px' }}>
<button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}> <button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap', padding: '8px' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
Profile Settings Profile Settings
</button> </button>
<button className="nav-btn" style={{ width: '100%', border: 'none', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', whiteSpace: 'nowrap' }} onClick={handleLogout}> <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 🚪 Logout
</button> </button>
</div> </div>
@@ -73,19 +125,25 @@ function Dashboard() {
)} )}
</div> </div>
<div className="nav-container"> <div className="nav-container" style={{ gap: '8px' }}>
<button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")}>📊 Scanner</button> <button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '6px 12px' }}>🏠</button>
<button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")}>📈 Ribbon Strategy</button> <button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📊 Scanner</button>
<button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")}>🏦 Smart Money</button> <button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📈 Ribbon</button>
<button className={`nav-btn ${view === "trend" ? "active" : ""}`} onClick={() => setView("trend")}> Trend Analysis</button> <button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>🏦 Smart Money</button>
<button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")}>📰 Market Intel</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 className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📋 Watchlists</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} />
@@ -93,12 +151,132 @@ function Dashboard() {
{signal && <Popup signal={signal} />} {signal && <Popup signal={signal} />}
</> </>
)} )}
{view === "ribbon" && <Ribbon />} {view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "smartmoney" && <SmartMoney />} {view === "ribbon" && <Ribbon onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "marketintel" && <MarketIntel />} {view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "trend" && <TrendScanner />} {view === "marketintel" && <MarketIntel onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "trend" && <TrendScanner onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />} {view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
</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>
); );
} }

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>
@@ -65,6 +82,8 @@ function AboutScanner() {
<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);

View File

@@ -15,13 +15,13 @@ function Login() {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); const resKey = await axios.get("/api/auth/public-key");
const publicKeyPem = resKey.data.public_key; const publicKeyPem = resKey.data.public_key;
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
const encryptedBase64 = forge.util.encode64(encrypted); const encryptedBase64 = forge.util.encode64(encrypted);
const res = await axios.post("http://127.0.0.1:8000/api/auth/login", { const res = await axios.post("/api/auth/login", {
username, username,
password: encryptedBase64 password: encryptedBase64
}); });

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/api/scanner/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);

View File

@@ -43,7 +43,7 @@ function Profile({ userProfile, onProfileUpdated }) {
if (payload.password) { if (payload.password) {
// Fetch Public Key to encrypt new password // Fetch Public Key to encrypt new password
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); const resKey = await axios.get("/api/auth/public-key");
const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key); const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key);
const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP'); const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP');
payload.password = forge.util.encode64(encrypted); payload.password = forge.util.encode64(encrypted);
@@ -51,7 +51,7 @@ function Profile({ userProfile, onProfileUpdated }) {
delete payload.password; delete payload.password;
} }
await axios.put("http://127.0.0.1:8000/api/auth/update-profile", payload); await axios.put("/api/auth/update-profile", payload);
setMessage("Profile updated successfully! ✅"); setMessage("Profile updated successfully! ✅");
if (onProfileUpdated) onProfileUpdated(); if (onProfileUpdated) onProfileUpdated();
setFormData(prev => ({ ...prev, password: "" })); // Clear password field setFormData(prev => ({ ...prev, password: "" })); // Clear password field

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/api/scanner/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>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<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)}> <button className="action-btn" onClick={() => openChart(s.symbol)}>
Chart Chart
</button> </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/api/scanner/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>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}> <button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
View Chart View Chart
</button> </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>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<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); }}> <button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
Chart Chart
</button> </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>
))} ))}

View File

@@ -15,13 +15,13 @@ function Signup() {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); const resKey = await axios.get("/api/auth/public-key");
const publicKeyPem = resKey.data.public_key; const publicKeyPem = resKey.data.public_key;
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
const encryptedBase64 = forge.util.encode64(encrypted); const encryptedBase64 = forge.util.encode64(encrypted);
await axios.post("http://127.0.0.1:8000/api/auth/signup", { await axios.post("/api/auth/signup", {
username, username,
password: encryptedBase64 password: encryptedBase64
}); });

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/api/scanner/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>
<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> <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/api/scanner/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>
<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> <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;
}
}