commit - mock buy
This commit is contained in:
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.
102
backend/app/api/portfolio.py
Normal file
102
backend/app/api/portfolio.py
Normal file
@@ -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"}
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
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 "=========================================="
|
||||
@@ -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
|
||||
|
||||
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
|
||||
112
src/App.js
112
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 (
|
||||
<div className="app-container">
|
||||
{/* HEADER */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
{/* Logo only, no text */}
|
||||
<span style={{ fontSize: '2.5rem' }}>📊</span>
|
||||
<h1 style={{ margin: 0 }}>Stock Scanner Pro</h1>
|
||||
</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>
|
||||
@@ -79,14 +102,14 @@ function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="nav-container">
|
||||
<button className={`nav-btn ${view === "dashboard" ? "active" : ""}`} onClick={() => setView("dashboard")} title="Dashboard" style={{ padding: '8px 12px' }}>🏠</button>
|
||||
<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>
|
||||
<button className={`nav-btn ${view === "watchlists" ? "active" : ""}`} onClick={() => setView("watchlists")}>📋 Watchlists</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">
|
||||
@@ -95,6 +118,7 @@ function Dashboard() {
|
||||
<Scanner
|
||||
onSelectStock={setSelectedStock}
|
||||
onSignal={setSignal}
|
||||
onBuyClick={onBuyClick}
|
||||
/>
|
||||
<div className="glass-card" style={{ marginTop: "20px" }}>
|
||||
<h2>TradingView Chart</h2>
|
||||
@@ -103,14 +127,60 @@ function Dashboard() {
|
||||
{signal && <Popup signal={signal} />}
|
||||
</>
|
||||
)}
|
||||
{view === "dashboard" && <MainDashboard userProfile={userProfile} />}
|
||||
{view === "ribbon" && <Ribbon />}
|
||||
{view === "smartmoney" && <SmartMoney />}
|
||||
{view === "marketintel" && <MarketIntel />}
|
||||
{view === "trend" && <TrendScanner />}
|
||||
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} />}
|
||||
{view === "dashboard" && <MainDashboard userProfile={userProfile} onBuyClick={onBuyClick} />}
|
||||
{view === "ribbon" && <Ribbon onBuyClick={onBuyClick} />}
|
||||
{view === "smartmoney" && <SmartMoney onBuyClick={onBuyClick} />}
|
||||
{view === "marketintel" && <MarketIntel onBuyClick={onBuyClick} />}
|
||||
{view === "trend" && <TrendScanner onBuyClick={onBuyClick} />}
|
||||
{view === "watchlists" && <WatchlistManager userProfile={userProfile} onProfileUpdated={fetchProfile} onBuyClick={onBuyClick} />}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +162,13 @@ function MainDashboard({ userProfile }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onBuyClick(result.symbol, result.currentPrice); }}
|
||||
style={{ background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', color: '#1a1a2e', padding: '6px 12px', borderRadius: '8px', border: 'none', fontWeight: 'bold', cursor: 'pointer' }}
|
||||
>
|
||||
Buy
|
||||
</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">
|
||||
@@ -143,11 +178,95 @@ function MainDashboard({ userProfile }) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button
|
||||
className="action-btn"
|
||||
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
|
||||
onClick={() => onBuyClick(s.symbol, s.price)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>
|
||||
Chart
|
||||
</button>
|
||||
|
||||
@@ -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 && (
|
||||
<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>
|
||||
@@ -104,6 +104,12 @@ function Scanner({ onSelectStock, onSignal }) {
|
||||
<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>
|
||||
{s?.sector && (
|
||||
<button
|
||||
onClick={() => removeStockFromSector(s?.symbol, s.sector)}
|
||||
@@ -168,6 +174,13 @@ function Scanner({ onSelectStock, onSignal }) {
|
||||
<td>{s?.ai_score}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button
|
||||
className="action-btn"
|
||||
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
|
||||
onClick={() => onBuyClick(s?.symbol, s?.price)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button className="action-btn" onClick={() => { openChart(s?.symbol); onSelectStock(s?.symbol); }}>
|
||||
Chart
|
||||
</button>
|
||||
|
||||
@@ -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() {
|
||||
<td>{s.reason}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button
|
||||
className="action-btn"
|
||||
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
|
||||
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
|
||||
{s.sector && (
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function TrendScanner() {
|
||||
function TrendScanner({ onBuyClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,6 +81,13 @@ function TrendScanner() {
|
||||
<td>{s.trend_reason}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button
|
||||
className="action-btn"
|
||||
style={{ background: 'var(--accent-green)', color: '#1a1a2e', padding: '4px 8px', fontSize: '0.8rem' }}
|
||||
onClick={() => onBuyClick(s.symbol, s.price || s.current_price || 0)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button className="action-btn" onClick={() => openChart(s.symbol)}>Chart</button>
|
||||
{s.sector && (
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function WatchlistManager({ userProfile, onProfileUpdated }) {
|
||||
function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick }) {
|
||||
const [sectorMap, setSectorMap] = useState({});
|
||||
const [newSectorName, setNewSectorName] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -116,7 +116,7 @@ function WatchlistManager({ userProfile, onProfileUpdated }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in" style={{ display: 'flex', gap: '20px', marginTop: '20px', alignItems: 'flex-start' }}>
|
||||
<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' }}>
|
||||
@@ -256,16 +256,24 @@ function WatchlistManager({ userProfile, onProfileUpdated }) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bottom Row: + Personal */}
|
||||
{/* Bottom Row: Actions */}
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
{selectedSector !== "Personal" && (
|
||||
<button
|
||||
onClick={() => { addStockToSector(symbol, "Personal"); }}
|
||||
title="Add to Personal"
|
||||
style={{ width: '100%', 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' }}
|
||||
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' }}
|
||||
>
|
||||
+ Add to Personal
|
||||
+ Personal
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onBuyClick(symbol, 0)}
|
||||
style={{ flex: 1, background: 'var(--accent-green)', color: '#1a1a2e', border: 'none', borderRadius: '4px', cursor: 'pointer', padding: '6px', fontSize: '0.85rem', fontWeight: 'bold' }}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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