diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index cd42198..1d9536b 100644 Binary files a/backend/app/__pycache__/main.cpython-313.pyc and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/portfolio.cpython-313.pyc b/backend/app/api/__pycache__/portfolio.cpython-313.pyc new file mode 100644 index 0000000..4e2d38e Binary files /dev/null and b/backend/app/api/__pycache__/portfolio.cpython-313.pyc differ diff --git a/backend/app/api/portfolio.py b/backend/app/api/portfolio.py new file mode 100644 index 0000000..1dfc880 --- /dev/null +++ b/backend/app/api/portfolio.py @@ -0,0 +1,102 @@ +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"} diff --git a/backend/app/core/__pycache__/config.cpython-313.pyc b/backend/app/core/__pycache__/config.cpython-313.pyc index 8c4cc54..2e103e1 100644 Binary files a/backend/app/core/__pycache__/config.cpython-313.pyc and b/backend/app/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index e0adb94..8932fa1 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,6 +1,6 @@ import os -DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal: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 diff --git a/backend/app/db/__pycache__/models.cpython-313.pyc b/backend/app/db/__pycache__/models.cpython-313.pyc index 628655f..bbd5861 100644 Binary files a/backend/app/db/__pycache__/models.cpython-313.pyc and b/backend/app/db/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 61bf8a4..4869ff5 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -1,5 +1,6 @@ -from sqlalchemy import Column, Integer, String, JSON +from sqlalchemy import Column, Integer, String, JSON, Float, ForeignKey, DateTime from app.db.database import Base +import datetime class User(Base): __tablename__ = "users" @@ -12,3 +13,13 @@ class User(Base): 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) diff --git a/backend/app/main.py b/backend/app/main.py index dfc1fe3..99dca23 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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(): diff --git a/build_and_push.sh b/build_and_push.sh new file mode 100755 index 0000000..3664570 --- /dev/null +++ b/build_and_push.sh @@ -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 "==========================================" diff --git a/docker-compose.yml b/docker-compose.yml index d5ef1d1..797089d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,4 +16,4 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: - - DATABASE_URL=postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5333/stock_scanner + - DATABASE_URL=postgresql://postgres:M%40triXPostgr3s%406202@host.docker.internal:5432/stock_scanner diff --git a/run_backend.sh b/run_backend.sh new file mode 100755 index 0000000..b186354 --- /dev/null +++ b/run_backend.sh @@ -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 diff --git a/src/App.js b/src/App.js index eadafb7..fd2da1b 100644 --- a/src/App.js +++ b/src/App.js @@ -24,6 +24,8 @@ function Dashboard() { const [userProfile, setUserProfile] = useState(null); const [dropdownOpen, setDropdownOpen] = useState(false); const [scannerLoaded, setScannerLoaded] = useState(false); + const [buyModalData, setBuyModalData] = useState(null); + const [buyQuantity, setBuyQuantity] = useState(1); const navigate = useNavigate(); const fetchProfile = () => { @@ -43,34 +45,55 @@ function Dashboard() { navigate("/login"); }; + 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); + }; + return (
{/* HEADER */}
+ {/* Logo only, no text */} 📊 -

Stock Scanner Pro

{userProfile && (
setDropdownOpen(!dropdownOpen)} > -
+
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
- {userProfile.display_name || userProfile.username} + {userProfile.display_name || userProfile.username}
{dropdownOpen && ( -
- -
@@ -79,14 +102,14 @@ function Dashboard() { )}
-
- - - - - - - +
+ + + + + + +
@@ -95,6 +118,7 @@ function Dashboard() {

TradingView Chart

@@ -103,14 +127,60 @@ function Dashboard() { {signal && } )} - {view === "dashboard" && } - {view === "ribbon" && } - {view === "smartmoney" && } - {view === "marketintel" && } - {view === "trend" && } - {view === "watchlists" && } + {view === "dashboard" && } + {view === "ribbon" && } + {view === "smartmoney" && } + {view === "marketintel" && } + {view === "trend" && } + {view === "watchlists" && } {view === "profile" && }
+ + {/* Buy Modal */} + {buyModalData && ( +
+
+

Mock Buy

+

{buyModalData.symbol}

+

Current Price: ₹{buyModalData.price}

+ +
+ + 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' + }} + /> +
+ +
+ Total Investment +

+ ₹{(buyModalData.price * buyQuantity).toFixed(2)} +

+
+ +
+ + +
+
+
+ )}
); } diff --git a/src/components/MainDashboard.js b/src/components/MainDashboard.js index 7300584..54395c2 100644 --- a/src/components/MainDashboard.js +++ b/src/components/MainDashboard.js @@ -1,12 +1,40 @@ import React, { useState, useRef, useEffect } from "react"; import axios from "axios"; -function MainDashboard({ userProfile }) { +function MainDashboard({ userProfile, onBuyClick }) { 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"; @@ -134,13 +162,21 @@ function MainDashboard({ userProfile }) { )}
-
- Chart - - - - - +
+ +
+ Chart + + + + + +
))} @@ -148,6 +184,89 @@ function MainDashboard({ userProfile }) { )}
+ {/* Portfolio Section */} +
+
+

💼 Mock Portfolio

+ As of: {new Date().toLocaleDateString()} +
+ + {loadingPortfolio ? ( +

Loading portfolio...

+ ) : portfolio.length === 0 ? ( +
+

Your mock portfolio is empty.

+

Search for a stock or run a scanner to simulate buying.

+
+ ) : ( + <> + {/* Summary Cards */} +
+
+

Total Invested

+

₹{portfolioSummary.total_invested.toLocaleString()}

+
+
+

Current Value

+

₹{portfolioSummary.total_current_value.toLocaleString()}

+
+
= 0 ? 'var(--accent-green)' : 'var(--accent-red)'}` }}> +

Overall P&L

+

= 0 ? 'var(--accent-green)' : 'var(--accent-red)' }}> + {portfolioSummary.total_pnl >= 0 ? '+' : ''}₹{portfolioSummary.total_pnl.toLocaleString()} + ({portfolioSummary.total_pnl_percent.toFixed(2)}%) +

+
+
+ + {/* Holdings Table */} +
+ + + + + + + + + + + + + + + + {portfolio.map(item => ( + + + + + + + + + + + + ))} + +
SymbolQtyDOPBuy PriceInvestedLTPCurrent ValueP&LAction
openChart(item.symbol)}> + {item.symbol} + {item.quantity} +
{new Date(item.purchase_date).toLocaleDateString()}
+
+ {Math.max(0, Math.floor((new Date() - new Date(item.purchase_date)) / (1000 * 60 * 60 * 24)))} days +
+
₹{item.buy_price.toFixed(2)}₹{item.invested.toLocaleString()}₹{item.current_price.toFixed(2)}₹{item.current_value.toLocaleString()}= 0 ? 'var(--accent-green)' : 'var(--accent-red)', fontWeight: 'bold' }}> + {item.pnl >= 0 ? '+' : ''}₹{item.pnl.toLocaleString()} ({item.pnl_percent.toFixed(2)}%) + + +
+
+ + )} +
+
); } diff --git a/src/components/Ribbon.js b/src/components/Ribbon.js index a0d44eb..e1b1296 100644 --- a/src/components/Ribbon.js +++ b/src/components/Ribbon.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; -function Ribbon() { +function Ribbon({ onBuyClick }) { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -77,6 +77,13 @@ function Ribbon() {
+ diff --git a/src/components/Scanner.js b/src/components/Scanner.js index dba4037..623f57d 100644 --- a/src/components/Scanner.js +++ b/src/components/Scanner.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; import AboutScanner from "./AboutScanner"; -function Scanner({ onSelectStock, onSignal }) { +function Scanner({ onSelectStock, onSignal, onBuyClick }) { const [data, setData] = useState(null); const [lastBest, setLastBest] = useState(null); @@ -68,7 +68,7 @@ function Scanner({ onSelectStock, onSignal }) { {/* Top 5 Trades */} {(data.best_5 || []).length > 0 && (
-

🔥 Top 5 Momentum Picks

+

🔥 Top 10 Momentum Picks

@@ -104,6 +104,12 @@ function Scanner({ onSelectStock, onSignal }) { + {s?.sector && (
{s?.ai_score}
+ diff --git a/src/components/SmartMoney.js b/src/components/SmartMoney.js index c5911e1..5ad2e1e 100644 --- a/src/components/SmartMoney.js +++ b/src/components/SmartMoney.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; -function SmartMoney() { +function SmartMoney({ onBuyClick }) { const [data, setData] = useState(null); useEffect(() => { @@ -104,6 +104,13 @@ function SmartMoney() {
{s.reason}
+ {s.sector && (
{s.trend_reason}
+ {s.sector && (
- {/* Bottom Row: + Personal */} - {selectedSector !== "Personal" && ( + {/* Bottom Row: Actions */} +
+ {selectedSector !== "Personal" && ( + + )} - )} +
))} diff --git a/src/index.css b/src/index.css index 012f9cb..2e6ea97 100644 --- a/src/index.css +++ b/src/index.css @@ -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; + } +}