Research View
This commit is contained in:
Binary file not shown.
@@ -100,3 +100,45 @@ def delete_portfolio_item(item_id: int, db: Session = Depends(get_db), current_u
|
||||
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}
|
||||
|
||||
108
src/App.js
108
src/App.js
@@ -25,6 +25,9 @@ function Dashboard() {
|
||||
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();
|
||||
|
||||
@@ -45,6 +48,22 @@ 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 {
|
||||
@@ -66,6 +85,10 @@ function Dashboard() {
|
||||
setBuyQuantity(1);
|
||||
};
|
||||
|
||||
const onResearchClick = (symbol) => {
|
||||
setResearchModalData({ symbol });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
{/* HEADER */}
|
||||
@@ -119,6 +142,7 @@ function Dashboard() {
|
||||
onSelectStock={setSelectedStock}
|
||||
onSignal={setSignal}
|
||||
onBuyClick={onBuyClick}
|
||||
onResearchClick={onResearchClick}
|
||||
/>
|
||||
<div className="glass-card" style={{ marginTop: "20px" }}>
|
||||
<h2>TradingView Chart</h2>
|
||||
@@ -127,12 +151,12 @@ function Dashboard() {
|
||||
{signal && <Popup signal={signal} />}
|
||||
</>
|
||||
)}
|
||||
{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 === "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>
|
||||
|
||||
@@ -181,6 +205,78 @@ function Dashboard() {
|
||||
</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,7 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function MainDashboard({ userProfile, onBuyClick }) {
|
||||
function MainDashboard({ userProfile, onBuyClick, onResearchClick }) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
@@ -164,11 +164,19 @@ function MainDashboard({ userProfile, onBuyClick }) {
|
||||
</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); }}
|
||||
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>
|
||||
<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">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function Ribbon({ onBuyClick }) {
|
||||
function Ribbon({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -78,12 +78,17 @@ function Ribbon({ onBuyClick }) {
|
||||
<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)}
|
||||
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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import AboutScanner from "./AboutScanner";
|
||||
|
||||
function Scanner({ onSelectStock, onSignal, onBuyClick }) {
|
||||
function Scanner({ onSelectStock, onSignal, onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [lastBest, setLastBest] = useState(null);
|
||||
|
||||
@@ -110,6 +110,12 @@ function Scanner({ onSelectStock, onSignal, onBuyClick }) {
|
||||
>
|
||||
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)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function SmartMoney({ onBuyClick }) {
|
||||
function SmartMoney({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,12 +105,17 @@ function SmartMoney({ onBuyClick }) {
|
||||
<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)}
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function TrendScanner({ onBuyClick }) {
|
||||
function TrendScanner({ onBuyClick, onResearchClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,12 +82,17 @@ function TrendScanner({ onBuyClick }) {
|
||||
<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)}
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick }) {
|
||||
function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick, onResearchClick }) {
|
||||
const [sectorMap, setSectorMap] = useState({});
|
||||
const [newSectorName, setNewSectorName] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -269,10 +269,16 @@ function WatchlistManager({ userProfile, onProfileUpdated, onBuyClick }) {
|
||||
)}
|
||||
<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' }}
|
||||
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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user