From e4ad2233b86d3a032b21ac1b2da172d2a8531d5a Mon Sep 17 00:00:00 2001 From: Narayanan Madaswamy Date: Fri, 7 Aug 2026 22:38:27 +0530 Subject: [PATCH] commit - mock buy --- backend/app/__pycache__/main.cpython-313.pyc | Bin 1184 -> 1314 bytes .../api/__pycache__/portfolio.cpython-313.pyc | Bin 0 -> 5419 bytes backend/app/api/portfolio.py | 102 +++++++++++++ .../core/__pycache__/config.cpython-313.pyc | Bin 481 -> 468 bytes backend/app/core/config.py | 2 +- .../app/db/__pycache__/models.cpython-313.pyc | Bin 929 -> 1600 bytes backend/app/db/models.py | 13 +- backend/app/main.py | 3 +- build_and_push.sh | 50 +++++++ docker-compose.yml | 2 +- run_backend.sh | 18 +++ src/App.js | 112 ++++++++++++--- src/components/MainDashboard.js | 135 ++++++++++++++++-- src/components/Ribbon.js | 9 +- src/components/Scanner.js | 17 ++- src/components/SmartMoney.js | 9 +- src/components/TrendScanner.js | 9 +- src/components/WatchlistManager.js | 26 ++-- src/index.css | 51 +++++++ 19 files changed, 511 insertions(+), 47 deletions(-) create mode 100644 backend/app/api/__pycache__/portfolio.cpython-313.pyc create mode 100644 backend/app/api/portfolio.py create mode 100755 build_and_push.sh create mode 100755 run_backend.sh diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index cd42198a04b9a4a9568a2494462df07f65aef6ee..1d9536b217b437abb983ffbdb6face383bb72885 100644 GIT binary patch delta 510 zcmZ3$xrmGJGcPX}0}x#JUz(-FJdsa=v0}i?blVs3a{vCo_NYV#bX^Rebu11)2JA!6Np_ zflM)sVw2Y~`LcpSMRF3eIJ-EI;ioA)S&rFWwul|bxy7GYP@tERq?eLdl9-fOoLa;I z6fNSMT*~ZY1JYIm3gse@z9I=Aaf`zyH$SB`C)KV<703m-x%dK*_`uA_$at4Q`Ywaf zeFmM23_6p4F}KUzU|_z^Aas#IXoBT+Vdaa$$`^EO4tO81y#NvnoZQGF%>ptR05UjT AkpKVy delta 448 zcmZ3)wSbfFGcPX}0}$vX7H1u0n#d=?=rU1VJ4AsYm?4MiK{dvd4goIXkZPJoA^dP6T2i^ zkQ}C#V7_2}ea2v3OJ<-wJVAC zi@i8GF)uH*X!0w@jgvPpMKX#`=3@3$mH_Gi;bJ}@(ZKM4NA!ZY)dg5g;u^puoDtVUwGm zQks)$SEK^ug4|fV9!Pv(W@Kc%%OHK1!RS7N;^a7%cKHX4EbZcr;vK@xG9Q^38Erl@ LGcfUiSzzM;Cf!Ua 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 0000000000000000000000000000000000000000..4e2d38ee3becea5ba1d420377102fa9cc20378a4 GIT binary patch literal 5419 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 ioXGlLn>uKzU;V!uP;ssR literal 0 HcmV?d00001 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 8c4cc5435a97adecfeb0d21d14df303d5bc6cff5..2e103e1646866427de26cbb55dde566fe3ce14a4 100644 GIT binary patch delta 99 zcmaFJe1)0!GcPX}0}zNXmu0=5$XmqdH?cua$k5VA&(O?5&p^-6(9p`%#Mo%!@kd6Q zKy@FO85tRGGjKlO7woU>s+=KrL9_BQe^mp|4PN60t{XhU4V*XR6fQDI7x4g90{~!G B95Mg^ delta 93 zcmcb@{E(UVGcPX}0}u!o6=!{$$Xmo1Iy~cyG@Pl67@hr7T8i6VBxWw20cJx zVmv~KvEFLD)RX!j_%DziDr1cC*qce~S!b4_oov4MzBg~*%=g~zJMl{(c%rHbqU+5c zcc;hog5mZyWv`Ke47`SP3|RMpPv?Ns{ovPm;B^56T?8@8a(ru&E`#j(+?oPP7|kKW zA3+A6m6IH+j1WwCQGrIqbJ0MFi%G{UAtn;xo?UUPH6r$&&nyr=Rk2NnC@CAPa;dgv zwTQH0I##A!wPKj~m(8a2u($VF<>}jmcbgV8GaveoaTHJWcQL`C{-!Uj1lZvW`2Kre zBYI^_3)K0ygfs8tp!e~5TgXaO0H4I8Xwo;bAZPhGlvNDQ5bpS3!efFV8WLpzkE!E# zAn0)|JGmJ>t9{OF&YdG?u2JV!(z3eg+YQ+6W}- z5|Ne7m5bmtSZKDYdAmZShHKWGveN>QCCPcWm8(O!V1dlIRHP$v=!l5mI)$3OOVqjx zg>5>l9NPjW-MbKYqnKxOVGQi{&32CBOY}8#wX^t-s z!^?|@!DJ^rRHwVC{rQ1c#r#aCUA?B}%hVF)M{_tJ-3n=-=cf+nzeF4s82>;UM`&Y!Hol|iH>92V6&!!Hc#5bw S4NF){nX7pz8Vk~0xVk%f4KyevS`TdbwUsYS&hnv>@-Dlqa+-p_c8QDE{cCU-7QkaiHkGWj)AVZA(z>!-tUWCov~EBQ>|O2xL7^u=BqjlED`E#pflU+uvA~8Y zfK+nW { @@ -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; + } +}