Compare commits
3 Commits
user_auth_
...
user_speci
| Author | SHA1 | Date | |
|---|---|---|---|
| f33011b219 | |||
| e4ad2233b8 | |||
| 53ad1abb78 |
21
Dockerfile.frontend
Normal file
21
Dockerfile.frontend
Normal 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;"]
|
||||
14
backend/Dockerfile.backend
Normal file
14
backend/Dockerfile.backend
Normal 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.
BIN
backend/app/api/__pycache__/portfolio.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/portfolio.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,9 @@ class ProfileUpdateRequest(BaseModel):
|
||||
gender: str = None
|
||||
password: str = None # Base64 encoded RSA-encrypted new password
|
||||
|
||||
class SectorMapUpdateRequest(BaseModel):
|
||||
sector_map: dict
|
||||
|
||||
@router.get("/public-key")
|
||||
def get_public_key():
|
||||
return {"public_key": public_key.decode("utf-8")}
|
||||
@@ -84,9 +87,16 @@ def get_me(current_user: User = Depends(get_current_user)):
|
||||
"display_name": current_user.display_name,
|
||||
"email_id": current_user.email_id,
|
||||
"mobile_no": current_user.mobile_no,
|
||||
"gender": current_user.gender
|
||||
"gender": current_user.gender,
|
||||
"sector_map": current_user.sector_map
|
||||
}
|
||||
|
||||
@router.put("/update-sectors")
|
||||
def update_sectors(req: SectorMapUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
current_user.sector_map = req.sector_map
|
||||
db.commit()
|
||||
return {"message": "Sectors updated successfully"}
|
||||
|
||||
@router.put("/update-profile")
|
||||
def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
if req.display_name is not None:
|
||||
|
||||
144
backend/app/api/portfolio.py
Normal file
144
backend/app/api/portfolio.py
Normal 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}
|
||||
@@ -4,24 +4,56 @@ from app.services.stock_engine import (
|
||||
get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel
|
||||
)
|
||||
|
||||
import requests
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/scan")
|
||||
def scan(current_user = Depends(get_current_user)):
|
||||
return get_scan()
|
||||
return get_scan(current_user.sector_map or {})
|
||||
|
||||
@router.get("/ribbon")
|
||||
def ribbon(current_user = Depends(get_current_user)):
|
||||
return get_ribbon()
|
||||
return get_ribbon(current_user.sector_map or {})
|
||||
|
||||
@router.get("/smartmoney")
|
||||
def smartmoney(current_user = Depends(get_current_user)):
|
||||
return get_smartmoney()
|
||||
return get_smartmoney(current_user.sector_map or {})
|
||||
|
||||
@router.get("/trend")
|
||||
def trend(current_user = Depends(get_current_user)):
|
||||
return get_trend()
|
||||
return get_trend(current_user.sector_map or {})
|
||||
|
||||
@router.get("/marketintel")
|
||||
def marketintel(current_user = Depends(get_current_user)):
|
||||
return get_marketintel()
|
||||
|
||||
@router.get("/search")
|
||||
def search(q: str, current_user = Depends(get_current_user)):
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
url = f"https://query2.finance.yahoo.com/v1/finance/search?q={q}"esCount=10&newsCount=0"
|
||||
res = requests.get(url, headers=headers)
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
quotes = data.get("quotes", [])
|
||||
# Filter for Indian stocks (.NS or .BO)
|
||||
indian_stocks = [q for q in quotes if q.get("symbol", "").endswith((".NS", ".BO"))][:10]
|
||||
|
||||
if indian_stocks:
|
||||
import yfinance as yf
|
||||
|
||||
symbols = [q["symbol"] for q in indian_stocks]
|
||||
tickers = yf.Tickers(" ".join(symbols))
|
||||
|
||||
for stock in indian_stocks:
|
||||
try:
|
||||
info = tickers.tickers[stock["symbol"]].info
|
||||
stock["currentPrice"] = info.get("currentPrice") or info.get("regularMarketPrice")
|
||||
stock["marketCap"] = info.get("marketCap")
|
||||
except Exception as e:
|
||||
print(f"Error fetching {stock['symbol']}: {e}")
|
||||
stock["currentPrice"] = None
|
||||
stock["marketCap"] = None
|
||||
|
||||
return indian_stocks
|
||||
return []
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
DATABASE_URL = "postgresql://postgres:M%40triXPostgr3s%406202@103.125.129.116:5333/stock_scanner"
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@192.168.0.111:5432/stock_scanner")
|
||||
SECRET_KEY = "stock-scanner-super-secret-key-12345"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
import datetime
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
@@ -11,3 +12,14 @@ class User(Base):
|
||||
email_id = Column(String, nullable=True)
|
||||
mobile_no = Column(String, nullable=True)
|
||||
gender = Column(String, nullable=True)
|
||||
sector_map = Column(JSON, nullable=True)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
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
|
||||
|
||||
# Initialize the database tables
|
||||
@@ -20,6 +20,7 @@ app.add_middleware(
|
||||
# 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():
|
||||
|
||||
Binary file not shown.
@@ -425,12 +425,12 @@ def analyze(stock):
|
||||
# =========================================
|
||||
# ✅ API - FAST SCAN
|
||||
# =========================================
|
||||
def get_scan():
|
||||
def get_scan(user_sector_map):
|
||||
|
||||
sector_output = {}
|
||||
all_stocks = []
|
||||
|
||||
for sector, stocks in sector_map.items():
|
||||
for sector, stocks in user_sector_map.items():
|
||||
|
||||
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||
data = [x for x in data if x]
|
||||
@@ -536,26 +536,28 @@ def analyze_ribbon(stock):
|
||||
return None
|
||||
|
||||
|
||||
def get_ribbon():
|
||||
def get_ribbon(user_sector_map):
|
||||
|
||||
results = []
|
||||
|
||||
for stocks in sector_map.values():
|
||||
for sector, stocks in user_sector_map.items():
|
||||
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
|
||||
data = [x for x in data if x]
|
||||
for x in data:
|
||||
x["sector"] = sector
|
||||
results.extend(data)
|
||||
|
||||
return results
|
||||
|
||||
def get_smartmoney():
|
||||
def get_smartmoney(user_sector_map):
|
||||
|
||||
results = []
|
||||
|
||||
for sector, stocks in sector_map.items():
|
||||
|
||||
for sector, stocks in user_sector_map.items():
|
||||
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||
data = [x for x in data if x]
|
||||
|
||||
for x in data:
|
||||
x["sector"] = sector
|
||||
results.extend(data)
|
||||
|
||||
|
||||
@@ -575,15 +577,15 @@ def get_smartmoney():
|
||||
"all": results
|
||||
}
|
||||
|
||||
def get_trend():
|
||||
def get_trend(user_sector_map):
|
||||
|
||||
results = []
|
||||
|
||||
for sector, stocks in sector_map.items():
|
||||
|
||||
for sector, stocks in user_sector_map.items():
|
||||
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
|
||||
data = [x for x in data if x]
|
||||
|
||||
for x in data:
|
||||
x["sector"] = sector
|
||||
results.extend(data)
|
||||
|
||||
results.sort(
|
||||
|
||||
114
backend/requirements.txt
Normal file
114
backend/requirements.txt
Normal 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
50
build_and_push.sh
Executable 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
19
docker-compose.yml
Normal 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
10
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://127.0.0.1:8000",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
|
||||
18
run_backend.sh
Executable file
18
run_backend.sh
Executable 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
|
||||
220
src/App.js
220
src/App.js
@@ -9,22 +9,30 @@ import Popup from "./components/Popup";
|
||||
import AboutScanner from "./components/AboutScanner";
|
||||
import MarketIntel from "./components/MarketIntel";
|
||||
import TrendScanner from "./components/TrendScanner";
|
||||
import WatchlistManager from "./components/WatchlistManager";
|
||||
import Login from "./components/Login";
|
||||
import Signup from "./components/Signup";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import Profile from "./components/Profile";
|
||||
import MainDashboard from "./components/MainDashboard";
|
||||
import "./index.css";
|
||||
|
||||
function Dashboard() {
|
||||
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
|
||||
const [signal, setSignal] = useState(null);
|
||||
const [view, setView] = useState("scanner");
|
||||
const [view, setView] = useState("dashboard");
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [scannerLoaded, setScannerLoaded] = useState(false);
|
||||
const [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("http://127.0.0.1:8000/api/auth/me")
|
||||
axios.get("/api/auth/me")
|
||||
.then(res => setUserProfile(res.data))
|
||||
.catch(err => {
|
||||
if (err.response?.status === 401) handleLogout();
|
||||
@@ -40,31 +48,75 @@ function Dashboard() {
|
||||
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 (
|
||||
<div className="app-container">
|
||||
{/* HEADER */}
|
||||
<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 && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<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)}
|
||||
>
|
||||
<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() : '?'}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '55px', right: '0', width: '220px', zIndex: 100, padding: '10px' }}>
|
||||
<button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
|
||||
<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', padding: '8px' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
|
||||
⚙️ Profile Settings
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
@@ -73,19 +125,25 @@ function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="nav-container">
|
||||
<button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")}>📊 Scanner</button>
|
||||
<button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")}>📈 Ribbon Strategy</button>
|
||||
<button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")}>🏦 Smart Money</button>
|
||||
<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 className="nav-container" style={{ gap: '8px' }}>
|
||||
<button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '6px 12px' }}>🏠</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 === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")} style={{ padding: '6px 12px', fontSize: '0.9rem' }}>📈 Ribbon</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 === "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 className="animate-fade-in">
|
||||
{view === "scanner" && (
|
||||
<>
|
||||
<AboutScanner />
|
||||
<Scanner onSelectStock={setSelectedStock} onSignal={setSignal} />
|
||||
<Scanner
|
||||
onSelectStock={setSelectedStock}
|
||||
onSignal={setSignal}
|
||||
onBuyClick={onBuyClick}
|
||||
onResearchClick={onResearchClick}
|
||||
/>
|
||||
<div className="glass-card" style={{ marginTop: "20px" }}>
|
||||
<h2>TradingView Chart</h2>
|
||||
<Chart symbol={selectedStock} />
|
||||
@@ -93,12 +151,132 @@ function Dashboard() {
|
||||
{signal && <Popup signal={signal} />}
|
||||
</>
|
||||
)}
|
||||
{view === "ribbon" && <Ribbon />}
|
||||
{view === "smartmoney" && <SmartMoney />}
|
||||
{view === "marketintel" && <MarketIntel />}
|
||||
{view === "trend" && <TrendScanner />}
|
||||
{view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||
{view === "ribbon" && <Ribbon onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||
{view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} onResearchClick={onResearchClick} />}
|
||||
{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} />}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +1,90 @@
|
||||
import React from "react";
|
||||
|
||||
function AboutScanner() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#111827",
|
||||
padding: "20px",
|
||||
borderRadius: "10px",
|
||||
marginBottom: "20px",
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
<h2>🧠 How The Scanner Works</h2>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>📈 Trend:</b> Price above EMA20 indicates bullish trend.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>⚡ Momentum:</b> RSI measures strength of buying momentum.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🚀 Breakout:</b> Detects stocks breaking recent resistance levels.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🔥 Volume Ratio:</b> Compares current volume with average volume.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>💪 Relative Strength (RS):</b> Measures stock performance vs NIFTY.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🏦 Smart Money Score:</b> Estimates institutional accumulation probability.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🤖 AI Score:</b> Combined score using trend, RSI, breakout, volume and RS.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>✅ Confidence:</b> Probability of a quality setup based on AI Score.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🎯 Fund Candidate:</b> High-volume breakout stocks showing potential accumulation.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🏆 Trade Of The Day:</b> Highest ranked stock across all sectors.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>🏅 Score Guide</h3>
|
||||
|
||||
<ul>
|
||||
<li>A+ (90-100) = Exceptional</li>
|
||||
<li>A (80-89) = Strong</li>
|
||||
<li>B (70-79) = Good</li>
|
||||
<li>C (60-69) = Average</li>
|
||||
<li>D (<60) = Avoid</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
function AboutScanner({ isLoaded }) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
setIsCollapsed(true);
|
||||
}
|
||||
}, [isLoaded]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#111827",
|
||||
padding: "20px",
|
||||
borderRadius: "10px",
|
||||
marginBottom: "20px",
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<li>
|
||||
<b>📈 Trend:</b> Price above EMA20 indicates bullish trend.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>⚡ Momentum:</b> RSI measures strength of buying momentum.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🚀 Breakout:</b> Detects stocks breaking recent resistance levels.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🔥 Volume Ratio:</b> Compares current volume with average volume.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>💪 Relative Strength (RS):</b> Measures stock performance vs NIFTY.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🏦 Smart Money Score:</b> Estimates institutional accumulation probability.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🤖 AI Score:</b> Combined score using trend, RSI, breakout, volume and RS.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>✅ Confidence:</b> Probability of a quality setup based on AI Score.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🎯 Fund Candidate:</b> High-volume breakout stocks showing potential accumulation.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>🏆 Trade Of The Day:</b> Highest ranked stock across all sectors.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>🏅 Score Guide</h3>
|
||||
|
||||
<ul>
|
||||
<li>A+ (90-100) = Exceptional</li>
|
||||
<li>A (80-89) = Strong</li>
|
||||
<li>B (70-79) = Good</li>
|
||||
<li>C (60-69) = Average</li>
|
||||
<li>D (<60) = Avoid</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AboutScanner;
|
||||
@@ -1,163 +1,163 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function Backtest() {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
// ✅ Fetch backtest data
|
||||
const loadBacktest = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/backtest");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error("Backtest Error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBacktest();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#0d1117",
|
||||
color: "#c9d1d9",
|
||||
padding: 20,
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ color: "#58a6ff" }}>
|
||||
📊 Backtest Dashboard (Strategy Performance)
|
||||
</h1>
|
||||
|
||||
{/* ✅ EXPLANATION PANEL */}
|
||||
<div
|
||||
style={{
|
||||
background: "#161b22",
|
||||
border: "1px solid #30363d",
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ color: "#58a6ff" }}>📘 How to Read Backtest</h2>
|
||||
|
||||
<p>
|
||||
✅ <b>Trades</b> = Total opportunities generated
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Wins</b> = Profitable trades
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Loss</b> = Losing trades
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Win Rate</b> = Success % of strategy
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Profit</b> = Net gain (simulated)
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
💡 <b>Best Strategy Rules:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Win Rate ≥ 60% ✅</li>
|
||||
<li>Trades ≥ 10 ✅</li>
|
||||
<li>Profit positive ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* ✅ SECTORS */}
|
||||
{Object.entries(data).map(([sector, stocks]) => (
|
||||
<div
|
||||
key={sector}
|
||||
style={{
|
||||
background: "#161b22",
|
||||
border: "1px solid #30363d",
|
||||
padding: 15,
|
||||
marginBottom: 20,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ color: "#58a6ff" }}>
|
||||
{sector}
|
||||
</h2>
|
||||
|
||||
<table
|
||||
width="100%"
|
||||
style={{
|
||||
borderCollapse: "collapse",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid #30363d" }}>
|
||||
<th>Stock</th>
|
||||
<th>Trades</th>
|
||||
<th>Wins</th>
|
||||
<th>Loss</th>
|
||||
<th>Win %</th>
|
||||
<th>Profit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{stocks.map((s, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
style={{
|
||||
borderBottom: "1px solid #30363d",
|
||||
}}
|
||||
>
|
||||
<td>{s.symbol}</td>
|
||||
|
||||
<td>{s.trades}</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
{s.wins}
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#f85149" }}>
|
||||
{s.loss}
|
||||
</td>
|
||||
|
||||
{/* ✅ WIN RATE COLOR */}
|
||||
<td
|
||||
style={{
|
||||
color:
|
||||
s.win_rate >= 70
|
||||
? "#3fb950"
|
||||
: s.win_rate >= 60
|
||||
? "#d29922"
|
||||
: "#f85149",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{s.win_rate}%
|
||||
</td>
|
||||
|
||||
{/* ✅ PROFIT COLOR */}
|
||||
<td
|
||||
style={{
|
||||
color:
|
||||
s.profit > 0 ? "#3fb950" : "#f85149",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{s.profit}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function Backtest() {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
// ✅ Fetch backtest data
|
||||
const loadBacktest = async () => {
|
||||
try {
|
||||
const res = await axios.get("/api/backtest");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error("Backtest Error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBacktest();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#0d1117",
|
||||
color: "#c9d1d9",
|
||||
padding: 20,
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ color: "#58a6ff" }}>
|
||||
📊 Backtest Dashboard (Strategy Performance)
|
||||
</h1>
|
||||
|
||||
{/* ✅ EXPLANATION PANEL */}
|
||||
<div
|
||||
style={{
|
||||
background: "#161b22",
|
||||
border: "1px solid #30363d",
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ color: "#58a6ff" }}>📘 How to Read Backtest</h2>
|
||||
|
||||
<p>
|
||||
✅ <b>Trades</b> = Total opportunities generated
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Wins</b> = Profitable trades
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Loss</b> = Losing trades
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Win Rate</b> = Success % of strategy
|
||||
</p>
|
||||
<p>
|
||||
✅ <b>Profit</b> = Net gain (simulated)
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
💡 <b>Best Strategy Rules:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Win Rate ≥ 60% ✅</li>
|
||||
<li>Trades ≥ 10 ✅</li>
|
||||
<li>Profit positive ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* ✅ SECTORS */}
|
||||
{Object.entries(data).map(([sector, stocks]) => (
|
||||
<div
|
||||
key={sector}
|
||||
style={{
|
||||
background: "#161b22",
|
||||
border: "1px solid #30363d",
|
||||
padding: 15,
|
||||
marginBottom: 20,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ color: "#58a6ff" }}>
|
||||
{sector}
|
||||
</h2>
|
||||
|
||||
<table
|
||||
width="100%"
|
||||
style={{
|
||||
borderCollapse: "collapse",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid #30363d" }}>
|
||||
<th>Stock</th>
|
||||
<th>Trades</th>
|
||||
<th>Wins</th>
|
||||
<th>Loss</th>
|
||||
<th>Win %</th>
|
||||
<th>Profit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{stocks.map((s, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
style={{
|
||||
borderBottom: "1px solid #30363d",
|
||||
}}
|
||||
>
|
||||
<td>{s.symbol}</td>
|
||||
|
||||
<td>{s.trades}</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
{s.wins}
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#f85149" }}>
|
||||
{s.loss}
|
||||
</td>
|
||||
|
||||
{/* ✅ WIN RATE COLOR */}
|
||||
<td
|
||||
style={{
|
||||
color:
|
||||
s.win_rate >= 70
|
||||
? "#3fb950"
|
||||
: s.win_rate >= 60
|
||||
? "#d29922"
|
||||
: "#f85149",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{s.win_rate}%
|
||||
</td>
|
||||
|
||||
{/* ✅ PROFIT COLOR */}
|
||||
<td
|
||||
style={{
|
||||
color:
|
||||
s.profit > 0 ? "#3fb950" : "#f85149",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{s.profit}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Backtest;
|
||||
296
src/components/Chart.js
vendored
296
src/components/Chart.js
vendored
@@ -1,149 +1,149 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts";
|
||||
import axios from "axios";
|
||||
|
||||
function Chart({ symbol }) {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !symbol) return;
|
||||
|
||||
ref.current.innerHTML = "";
|
||||
|
||||
try {
|
||||
const chart = createChart(ref.current, {
|
||||
width: 800,
|
||||
height: 500,
|
||||
layout: {
|
||||
background: { color: "#111" },
|
||||
textColor: "#DDD",
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: "#333" },
|
||||
horzLines: { color: "#333" },
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ MAIN CANDLE SERIES
|
||||
const candleSeries = chart.addSeries(CandlestickSeries);
|
||||
|
||||
// ✅ EMA LINE
|
||||
const emaSeries = chart.addSeries(LineSeries, {
|
||||
color: "yellow",
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
||||
// ✅ RSI LINE (drawn on same chart for simplicity)
|
||||
const rsiSeries = chart.addSeries(LineSeries, {
|
||||
color: "cyan",
|
||||
lineWidth: 1,
|
||||
});
|
||||
|
||||
// ✅ EMA CALCULATION
|
||||
const calculateEMA = (data, period = 20) => {
|
||||
const k = 2 / (period + 1);
|
||||
let ema = data[0].close;
|
||||
return data.map((d) => {
|
||||
ema = d.close * k + ema * (1 - k);
|
||||
return { time: d.time, value: ema };
|
||||
});
|
||||
};
|
||||
|
||||
// ✅ RSI CALCULATION
|
||||
const calculateRSI = (data, period = 14) => {
|
||||
let gains = 0;
|
||||
let losses = 0;
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const diff = data[i].close - data[i - 1].close;
|
||||
|
||||
if (diff > 0) gains += diff;
|
||||
else losses -= diff;
|
||||
|
||||
if (i >= period) {
|
||||
const rs = gains / (losses || 1);
|
||||
const rsi = 100 - 100 / (1 + rs);
|
||||
|
||||
result.push({ time: data[i].time, value: rsi });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// ✅ LOAD DATA FUNCTION
|
||||
const loadData = () => {
|
||||
axios
|
||||
.get(`http://localhost:8000/history?symbol=${symbol}`)
|
||||
.then((res) => {
|
||||
if (!res.data || res.data.length === 0) return;
|
||||
|
||||
const formatted = res.data.map((d) => ({
|
||||
time: Math.floor(d.time),
|
||||
open: Number(d.open),
|
||||
high: Number(d.high),
|
||||
low: Number(d.low),
|
||||
close: Number(d.close),
|
||||
}));
|
||||
|
||||
// ✅ SET CANDLES
|
||||
candleSeries.setData(formatted);
|
||||
|
||||
// ✅ EMA
|
||||
const emaData = calculateEMA(formatted);
|
||||
emaSeries.setData(emaData);
|
||||
|
||||
// ✅ RSI
|
||||
const rsiData = calculateRSI(formatted);
|
||||
rsiSeries.setData(rsiData);
|
||||
|
||||
// ✅ ENTRY / SL / TARGET
|
||||
const lastPrice = formatted[formatted.length - 1].close;
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice,
|
||||
color: "blue",
|
||||
lineWidth: 2,
|
||||
title: "Entry",
|
||||
});
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice * 0.97,
|
||||
color: "red",
|
||||
title: "SL",
|
||||
});
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice * 1.05,
|
||||
color: "green",
|
||||
title: "Target",
|
||||
});
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
// ✅ INITIAL LOAD
|
||||
loadData();
|
||||
|
||||
// ✅ LIVE UPDATE (every 1 min)
|
||||
const interval = setInterval(loadData, 60000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
chart.remove();
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error("Chart crash:", err);
|
||||
}
|
||||
|
||||
}, [symbol]);
|
||||
|
||||
return <div ref={ref}></div>;
|
||||
}
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { createChart, CandlestickSeries, LineSeries } from "lightweight-charts";
|
||||
import axios from "axios";
|
||||
|
||||
function Chart({ symbol }) {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !symbol) return;
|
||||
|
||||
ref.current.innerHTML = "";
|
||||
|
||||
try {
|
||||
const chart = createChart(ref.current, {
|
||||
width: 800,
|
||||
height: 500,
|
||||
layout: {
|
||||
background: { color: "#111" },
|
||||
textColor: "#DDD",
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: "#333" },
|
||||
horzLines: { color: "#333" },
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ MAIN CANDLE SERIES
|
||||
const candleSeries = chart.addSeries(CandlestickSeries);
|
||||
|
||||
// ✅ EMA LINE
|
||||
const emaSeries = chart.addSeries(LineSeries, {
|
||||
color: "yellow",
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
||||
// ✅ RSI LINE (drawn on same chart for simplicity)
|
||||
const rsiSeries = chart.addSeries(LineSeries, {
|
||||
color: "cyan",
|
||||
lineWidth: 1,
|
||||
});
|
||||
|
||||
// ✅ EMA CALCULATION
|
||||
const calculateEMA = (data, period = 20) => {
|
||||
const k = 2 / (period + 1);
|
||||
let ema = data[0].close;
|
||||
return data.map((d) => {
|
||||
ema = d.close * k + ema * (1 - k);
|
||||
return { time: d.time, value: ema };
|
||||
});
|
||||
};
|
||||
|
||||
// ✅ RSI CALCULATION
|
||||
const calculateRSI = (data, period = 14) => {
|
||||
let gains = 0;
|
||||
let losses = 0;
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const diff = data[i].close - data[i - 1].close;
|
||||
|
||||
if (diff > 0) gains += diff;
|
||||
else losses -= diff;
|
||||
|
||||
if (i >= period) {
|
||||
const rs = gains / (losses || 1);
|
||||
const rsi = 100 - 100 / (1 + rs);
|
||||
|
||||
result.push({ time: data[i].time, value: rsi });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// ✅ LOAD DATA FUNCTION
|
||||
const loadData = () => {
|
||||
axios
|
||||
.get(`http://localhost:8000/history?symbol=${symbol}`)
|
||||
.then((res) => {
|
||||
if (!res.data || res.data.length === 0) return;
|
||||
|
||||
const formatted = res.data.map((d) => ({
|
||||
time: Math.floor(d.time),
|
||||
open: Number(d.open),
|
||||
high: Number(d.high),
|
||||
low: Number(d.low),
|
||||
close: Number(d.close),
|
||||
}));
|
||||
|
||||
// ✅ SET CANDLES
|
||||
candleSeries.setData(formatted);
|
||||
|
||||
// ✅ EMA
|
||||
const emaData = calculateEMA(formatted);
|
||||
emaSeries.setData(emaData);
|
||||
|
||||
// ✅ RSI
|
||||
const rsiData = calculateRSI(formatted);
|
||||
rsiSeries.setData(rsiData);
|
||||
|
||||
// ✅ ENTRY / SL / TARGET
|
||||
const lastPrice = formatted[formatted.length - 1].close;
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice,
|
||||
color: "blue",
|
||||
lineWidth: 2,
|
||||
title: "Entry",
|
||||
});
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice * 0.97,
|
||||
color: "red",
|
||||
title: "SL",
|
||||
});
|
||||
|
||||
candleSeries.createPriceLine({
|
||||
price: lastPrice * 1.05,
|
||||
color: "green",
|
||||
title: "Target",
|
||||
});
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
// ✅ INITIAL LOAD
|
||||
loadData();
|
||||
|
||||
// ✅ LIVE UPDATE (every 1 min)
|
||||
const interval = setInterval(loadData, 60000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
chart.remove();
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error("Chart crash:", err);
|
||||
}
|
||||
|
||||
}, [symbol]);
|
||||
|
||||
return <div ref={ref}></div>;
|
||||
}
|
||||
|
||||
export default Chart;
|
||||
@@ -1,21 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
function ChartPage({ symbol }) {
|
||||
const tvSymbol = symbol || "NSE:RELIANCE";
|
||||
|
||||
const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "600px", marginTop: "20px" }}>
|
||||
<iframe
|
||||
title="TradingView Chart"
|
||||
src={url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import React from "react";
|
||||
|
||||
function ChartPage({ symbol }) {
|
||||
const tvSymbol = symbol || "NSE:RELIANCE";
|
||||
|
||||
const url = `https://www.tradingview.com/chart/?symbol=${tvSymbol}`;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "600px", marginTop: "20px" }}>
|
||||
<iframe
|
||||
title="TradingView Chart"
|
||||
src={url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChartPage;
|
||||
@@ -15,13 +15,13 @@ function Login() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
||||
const resKey = await axios.get("/api/auth/public-key");
|
||||
const publicKeyPem = resKey.data.public_key;
|
||||
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
||||
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
||||
const encryptedBase64 = forge.util.encode64(encrypted);
|
||||
|
||||
const res = await axios.post("http://127.0.0.1:8000/api/auth/login", {
|
||||
const res = await axios.post("/api/auth/login", {
|
||||
username,
|
||||
password: encryptedBase64
|
||||
});
|
||||
|
||||
@@ -1,114 +1,114 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function LongTerm() {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/longterm");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "#0d1117",
|
||||
color: "#c9d1d9",
|
||||
padding: 20,
|
||||
minHeight: "100vh"
|
||||
}}>
|
||||
|
||||
<h1 style={{ color: "#58a6ff" }}>
|
||||
📈 Long-Term Investment Dashboard
|
||||
</h1>
|
||||
|
||||
{/* ✅ EXPLANATION */}
|
||||
<div style={{
|
||||
background: "#161b22",
|
||||
padding: 15,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20
|
||||
}}>
|
||||
<h3>📘 Strategy</h3>
|
||||
|
||||
<ul>
|
||||
<li>✅ Golden Cross = 50 EMA > 200 EMA</li>
|
||||
<li>✅ Support = Price above 200 EMA</li>
|
||||
<li>✅ Momentum = Breakout + Volume</li>
|
||||
</ul>
|
||||
|
||||
<p>💡 Only strong trend reversal stocks are shown</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(data).map(([sector, info]) => {
|
||||
|
||||
|
||||
const filtered = info.stocks.filter(
|
||||
s => s.golden_cross || s.support_strength
|
||||
);
|
||||
|
||||
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={sector} style={{
|
||||
background: "#161b22",
|
||||
padding: 15,
|
||||
marginBottom: 20,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
|
||||
<h2 style={{ color: "#3fb950" }}>
|
||||
{sector} ✅ Long-Term Strong
|
||||
</h2>
|
||||
|
||||
<table width="100%" style={{ textAlign: "center" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stock</th>
|
||||
<th>Score</th>
|
||||
<th>RSI</th>
|
||||
<th>Golden Cross</th>
|
||||
<th>Support</th>
|
||||
<th>Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{filtered.map((s, i) => (
|
||||
<tr key={i}>
|
||||
<td>{s.symbol}</td>
|
||||
<td>{s.score}</td>
|
||||
<td>{s.rsi}</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
✅
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
✅
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
{s.signal}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LongTerm;
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function LongTerm() {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("/api/longterm");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "#0d1117",
|
||||
color: "#c9d1d9",
|
||||
padding: 20,
|
||||
minHeight: "100vh"
|
||||
}}>
|
||||
|
||||
<h1 style={{ color: "#58a6ff" }}>
|
||||
📈 Long-Term Investment Dashboard
|
||||
</h1>
|
||||
|
||||
{/* ✅ EXPLANATION */}
|
||||
<div style={{
|
||||
background: "#161b22",
|
||||
padding: 15,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20
|
||||
}}>
|
||||
<h3>📘 Strategy</h3>
|
||||
|
||||
<ul>
|
||||
<li>✅ Golden Cross = 50 EMA > 200 EMA</li>
|
||||
<li>✅ Support = Price above 200 EMA</li>
|
||||
<li>✅ Momentum = Breakout + Volume</li>
|
||||
</ul>
|
||||
|
||||
<p>💡 Only strong trend reversal stocks are shown</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(data).map(([sector, info]) => {
|
||||
|
||||
|
||||
const filtered = info.stocks.filter(
|
||||
s => s.golden_cross || s.support_strength
|
||||
);
|
||||
|
||||
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={sector} style={{
|
||||
background: "#161b22",
|
||||
padding: 15,
|
||||
marginBottom: 20,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
|
||||
<h2 style={{ color: "#3fb950" }}>
|
||||
{sector} ✅ Long-Term Strong
|
||||
</h2>
|
||||
|
||||
<table width="100%" style={{ textAlign: "center" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stock</th>
|
||||
<th>Score</th>
|
||||
<th>RSI</th>
|
||||
<th>Golden Cross</th>
|
||||
<th>Support</th>
|
||||
<th>Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{filtered.map((s, i) => (
|
||||
<tr key={i}>
|
||||
<td>{s.symbol}</td>
|
||||
<td>{s.score}</td>
|
||||
<td>{s.rsi}</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
✅
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
✅
|
||||
</td>
|
||||
|
||||
<td style={{ color: "#3fb950" }}>
|
||||
{s.signal}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LongTerm;
|
||||
|
||||
282
src/components/MainDashboard.js
Normal file
282
src/components/MainDashboard.js
Normal 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;
|
||||
@@ -10,7 +10,7 @@ function MarketIntel() {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/marketintel");
|
||||
const res = await axios.get("/api/scanner/marketintel");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
function Popup({ signal }) {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const popup = document.getElementById("popup");
|
||||
if (popup) popup.style.display = "none";
|
||||
}, 4000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="popup" className="popup">
|
||||
<h3>🚨 Trade Signal</h3>
|
||||
<p>Stock: {signal.name}</p>
|
||||
<p>Code: {signal.code}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
function Popup({ signal }) {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const popup = document.getElementById("popup");
|
||||
if (popup) popup.style.display = "none";
|
||||
}, 4000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="popup" className="popup">
|
||||
<h3>🚨 Trade Signal</h3>
|
||||
<p>Stock: {signal.name}</p>
|
||||
<p>Code: {signal.code}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Popup;
|
||||
@@ -43,7 +43,7 @@ function Profile({ userProfile, onProfileUpdated }) {
|
||||
|
||||
if (payload.password) {
|
||||
// Fetch Public Key to encrypt new password
|
||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
||||
const resKey = await axios.get("/api/auth/public-key");
|
||||
const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key);
|
||||
const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP');
|
||||
payload.password = forge.util.encode64(encrypted);
|
||||
@@ -51,7 +51,7 @@ function Profile({ userProfile, onProfileUpdated }) {
|
||||
delete payload.password;
|
||||
}
|
||||
|
||||
await axios.put("http://127.0.0.1:8000/api/auth/update-profile", payload);
|
||||
await axios.put("/api/auth/update-profile", payload);
|
||||
setMessage("Profile updated successfully! ✅");
|
||||
if (onProfileUpdated) onProfileUpdated();
|
||||
setFormData(prev => ({ ...prev, password: "" })); // Clear password field
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function Ribbon() {
|
||||
function Ribbon({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/ribbon");
|
||||
const res = await axios.get("/api/scanner/ribbon");
|
||||
setData(res.data || []);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
@@ -26,7 +26,18 @@ function Ribbon() {
|
||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
||||
};
|
||||
|
||||
if (loading) return <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 (
|
||||
<div className="glass-card animate-fade-in">
|
||||
@@ -65,9 +76,35 @@ function Ribbon() {
|
||||
</span>
|
||||
</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)}
|
||||
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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import AboutScanner from "./AboutScanner";
|
||||
|
||||
function Scanner({ onSelectStock, onSignal }) {
|
||||
function Scanner({ onSelectStock, onSignal, onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [lastBest, setLastBest] = useState(null);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/scan");
|
||||
const res = await axios.get("/api/scanner/scan");
|
||||
const newData = res.data;
|
||||
|
||||
const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null;
|
||||
@@ -33,14 +34,41 @@ function Scanner({ onSelectStock, onSignal }) {
|
||||
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 (
|
||||
<div>
|
||||
<AboutScanner isLoaded={true} />
|
||||
|
||||
{/* Top 5 Trades */}
|
||||
{(data.best_5 || []).length > 0 && (
|
||||
<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">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
@@ -72,9 +100,35 @@ function Scanner({ onSelectStock, onSignal }) {
|
||||
<td>{s.grade}</td>
|
||||
<td>{s.institutional ? <span className="text-orange">🔥 Yes</span> : <span className="text-muted">No</span>}</td>
|
||||
<td>
|
||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||
View Chart
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||
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>
|
||||
</tr>
|
||||
))}
|
||||
@@ -125,9 +179,28 @@ function Scanner({ onSelectStock, onSignal }) {
|
||||
<td className="text-green">₹{s?.target}</td>
|
||||
<td>{s?.ai_score}</td>
|
||||
<td>
|
||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||
Chart
|
||||
</button>
|
||||
<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); }}>
|
||||
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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -15,13 +15,13 @@ function Signup() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key");
|
||||
const resKey = await axios.get("/api/auth/public-key");
|
||||
const publicKeyPem = resKey.data.public_key;
|
||||
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
||||
const encrypted = publicKey.encrypt(password, 'RSA-OAEP');
|
||||
const encryptedBase64 = forge.util.encode64(encrypted);
|
||||
|
||||
await axios.post("http://127.0.0.1:8000/api/auth/signup", {
|
||||
await axios.post("/api/auth/signup", {
|
||||
username,
|
||||
password: encryptedBase64
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function SmartMoney() {
|
||||
function SmartMoney({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -12,7 +12,7 @@ function SmartMoney() {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/smartmoney");
|
||||
const res = await axios.get("/api/scanner/smartmoney");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -23,7 +23,30 @@ function SmartMoney() {
|
||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
||||
};
|
||||
|
||||
if (!data) return <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 (
|
||||
<div className="animate-fade-in">
|
||||
@@ -80,7 +103,33 @@ function SmartMoney() {
|
||||
<td>{s.breakout ? "🔥 YES" : "NO"}</td>
|
||||
<td>{s.reason}</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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function TrendScanner() {
|
||||
function TrendScanner({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -12,7 +12,7 @@ function TrendScanner() {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await axios.get("http://127.0.0.1:8000/api/scanner/trend");
|
||||
const res = await axios.get("/api/scanner/trend");
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -23,7 +23,24 @@ function TrendScanner() {
|
||||
window.open(`https://www.tradingview.com/chart/?symbol=NSE:${symbol}`, "_blank");
|
||||
};
|
||||
|
||||
if (!data) return <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 (
|
||||
<div className="animate-fade-in">
|
||||
@@ -63,7 +80,33 @@ function TrendScanner() {
|
||||
</td>
|
||||
<td>{s.trend_reason}</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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
293
src/components/WatchlistManager.js
Normal file
293
src/components/WatchlistManager.js
Normal 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;
|
||||
@@ -83,9 +83,9 @@ h2 {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 10px 20px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -214,3 +214,54 @@ h2 {
|
||||
.text-orange { color: var(--accent-orange); }
|
||||
.text-blue { color: var(--accent-blue); }
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user