Antisentiment / app.py
Akshay Ukey
Fix performance issues: lazy DB init, minimal CDN, cached resources
4642708
import streamlit as st
from streamlit_lightweight_charts import renderLightweightCharts
from urllib.request import urlopen, Request
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
from datetime import datetime, timedelta
import pytz
from database import *
# NLTK VADER for sentiment analysis
import nltk
import ssl
try:
_create_unverified_https_context = ssl._create_unverified_context
except AttributeError:
pass
else:
ssl._create_default_https_context = _create_unverified_https_context
from nltk.sentiment.vader import SentimentIntensityAnalyzer
# Cache NLTK download and VADER analyzer
@st.cache_resource
def setup_nltk():
"""Download NLTK data once and return analyzer"""
nltk.download('vader_lexicon', quiet=True)
return SentimentIntensityAnalyzer()
@st.cache_resource
def get_vader_analyzer():
"""Get cached VADER sentiment analyzer"""
return setup_nltk()
@st.cache_resource
def get_timezone_objects():
"""Get cached timezone objects"""
return {
'eastern': pytz.timezone('US/Eastern'),
'ist': pytz.timezone('Asia/Kolkata'),
'utc': pytz.utc
}
# Cache static HTML/CSS content for performance
@st.cache_data
def get_minimal_cdn():
"""Get minimal CDN resources without external dependencies"""
return """
<!-- Minimal styling without external CDN dependencies -->
<style>
/* Use system fonts instead of Google Fonts */
body, .stApp {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Basic icon fallbacks */
.ph::before {
content: "โ—";
font-size: 1.2em;
}
</style>
"""
@st.cache_data
def get_professional_header():
"""Get cached professional header HTML"""
return """
<div class="professional-header">
<div class="header-container">
<div class="header-left">
<div class="logo-section">
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13H11V3H3V13ZM3 21H11V15H3V21ZM13 21H21V11H13V21ZM13 3V9H21V3H13Z" fill="currentColor"/>
</svg>
<div class="brand-text">
<h1 class="brand-title">TradingView Pro</h1>
<p class="brand-subtitle">Professional Chart Analysis Platform</p>
</div>
</div>
</div>
<div class="header-right">
<div class="status-indicators">
<div class="status-item">
<i class="ph ph-chart-line"></i>
<span>TradingView Charts</span>
</div>
<div class="status-item">
<i class="ph ph-database"></i>
<span>Historical Data</span>
</div>
<div class="status-item">
<i class="ph ph-clock"></i>
<span>IST</span>
</div>
</div>
</div>
</div>
</div>
"""
@st.cache_data
def get_professional_footer():
"""Get cached professional footer HTML"""
return f"""
<div class="professional-footer">
<div class="footer-container">
<div class="footer-content">
<div class="footer-section">
<h4 class="footer-title">TradingView Features</h4>
<div class="footer-item">
<i class="ph ph-chart-line"></i>
<span>Lightweight Charts</span>
</div>
<div class="footer-item">
<i class="ph ph-crosshair"></i>
<span>Crosshair Everywhere</span>
</div>
<div class="footer-item">
<i class="ph ph-timer"></i>
<span>Real-time Updates</span>
</div>
</div>
<div class="footer-section">
<h4 class="footer-title">Data Management</h4>
<div class="footer-item">
<i class="ph ph-database"></i>
<span>SQLite Database</span>
</div>
<div class="footer-item">
<i class="ph ph-clock"></i>
<span>IST Timezone</span>
</div>
<div class="footer-item">
<i class="ph ph-shield-check"></i>
<span>Safe Testing</span>
</div>
</div>
<div class="footer-section">
<h4 class="footer-title">Technology</h4>
<div class="footer-item">
<i class="ph ph-code"></i>
<span>Python & Streamlit</span>
</div>
<div class="footer-item">
<i class="ph ph-brain"></i>
<span>NLTK VADER</span>
</div>
<div class="footer-item">
<i class="ph ph-chart-bar"></i>
<span>TradingView Charts</span>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="footer-copyright">
<p>&copy; 2024 TradingView Pro. Professional Chart Analysis Platform.</p>
</div>
<div class="footer-links">
<a href="#" class="footer-link">
<i class="ph ph-github-logo"></i>
</a>
<a href="#" class="footer-link">
<i class="ph ph-linkedin-logo"></i>
</a>
<a href="#" class="footer-link">
<i class="ph ph-envelope"></i>
</a>
</div>
</div>
</div>
</div>
"""
@st.cache_data
def get_professional_css():
"""Get cached professional CSS"""
return """
<style>
/* Professional Design System */
:root {
--bg-primary: #0a0e1a;
--bg-secondary: #151b2e;
--bg-tertiary: #1e2538;
--accent-primary: #6366f1;
--accent-secondary: #8b5cf6;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--border: #2d3548;
--glass-bg: rgba(30, 37, 56, 0.8);
--glass-border: rgba(255, 255, 255, 0.1);
}
/* Global Styles */
* {
box-sizing: border-box;
}
body, .stApp {
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
}
/* Professional Header */
.professional-header {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 1rem;
}
.logo-icon {
width: 40px;
height: 40px;
color: var(--accent-primary);
filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.3));
}
.brand-title {
font-family: 'Manrope', sans-serif;
font-size: 1.5rem;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.brand-subtitle {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0;
font-weight: 400;
}
.status-indicators {
display: flex;
gap: 2rem;
}
.status-item {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
}
.status-item i {
color: var(--accent-primary);
font-size: 1.1rem;
}
/* Enhanced Input Section */
.input-section {
margin: 2rem 0;
padding: 0 2rem;
}
.input-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 300px;
gap: 2rem;
align-items: end;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 1rem;
top: 2.5rem;
color: var(--text-secondary);
z-index: 2;
}
.input-label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Enhanced Streamlit Input Styling */
.stTextInput > div > div > input {
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
border-radius: 12px !important;
color: var(--text-primary) !important;
font-size: 1rem !important;
font-family: 'Inter', sans-serif !important;
backdrop-filter: blur(10px) !important;
transition: all 0.3s ease !important;
padding: 1rem !important;
}
.stTextInput > div > div > input:focus {
outline: none !important;
border-color: var(--accent-primary) !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important;
background: rgba(30, 37, 56, 0.9) !important;
}
.stSelectbox > div > div > select {
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
border-radius: 12px !important;
color: var(--text-primary) !important;
font-size: 1rem !important;
font-family: 'Inter', sans-serif !important;
backdrop-filter: blur(10px) !important;
transition: all 0.3s ease !important;
padding: 1rem !important;
}
.stSelectbox > div > div > select:focus {
outline: none !important;
border-color: var(--accent-primary) !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important;
}
.stTextInput label,
.stSelectbox label {
color: var(--text-secondary) !important;
font-size: 0.875rem !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
/* Popular Tickers Dropdown Styling */
.stSelectbox[data-testid="ticker_dropdown"] > div > div > select {
background: var(--accent-primary) !important;
border: 1px solid var(--accent-primary) !important;
border-radius: 8px !important;
color: white !important;
font-size: 0.875rem !important;
font-weight: 600 !important;
padding: 0.75rem !important;
transition: all 0.3s ease !important;
}
.stSelectbox[data-testid="ticker_dropdown"] > div > div > select:hover {
background: var(--accent-secondary) !important;
border-color: var(--accent-secondary) !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3) !important;
}
.stSelectbox[data-testid="ticker_dropdown"] label {
color: var(--accent-primary) !important;
font-size: 0.75rem !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.1em !important;
}
/* Chart Containers */
.charts-section {
margin: 2rem 0;
padding: 0 2rem;
}
.charts-header {
max-width: 1400px;
margin: 0 auto 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.charts-title {
font-family: 'Manrope', sans-serif;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.chart-controls {
display: flex;
gap: 0.5rem;
}
.chart-control-btn {
padding: 0.5rem 1rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 8px;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.chart-control-btn:hover,
.chart-control-btn.active {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
.chart-container {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
overflow: hidden;
margin-bottom: 2rem;
}
.chart-header {
padding: 1.5rem;
border-bottom: 1px solid var(--glass-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-title {
font-family: 'Manrope', sans-serif;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.chart-actions {
display: flex;
gap: 0.5rem;
}
.chart-action-btn {
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.chart-action-btn:hover {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
.chart-content {
padding: 1rem;
}
/* Data Table Section */
.data-table-section {
margin: 2rem 0;
padding: 0 2rem;
}
.table-header {
max-width: 1400px;
margin: 0 auto 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-family: 'Manrope', sans-serif;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.table-actions {
display: flex;
gap: 0.5rem;
}
.table-action-btn {
padding: 0.5rem 1rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 8px;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.table-action-btn:hover {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
/* Professional Footer */
.professional-footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
margin-top: 4rem;
padding: 3rem 0 1rem;
}
.footer-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.footer-title {
font-family: 'Manrope', sans-serif;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.footer-item {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
.footer-item i {
color: var(--accent-primary);
font-size: 1rem;
}
.footer-bottom {
border-top: 1px solid var(--border);
padding-top: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-copyright {
color: var(--text-secondary);
font-size: 0.875rem;
}
.footer-links {
display: flex;
gap: 1rem;
}
.footer-link {
width: 40px;
height: 40px;
border-radius: 8px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition: all 0.3s ease;
}
.footer-link:hover {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
transform: translateY(-2px);
}
/* Enhanced Streamlit Components */
.stDataFrame {
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
border-radius: 12px !important;
backdrop-filter: blur(10px) !important;
}
.stAlert {
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
border-radius: 12px !important;
backdrop-filter: blur(10px) !important;
}
.stButton > button {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
}
.stButton > button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3) !important;
}
/* Hide Streamlit branding */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
.stDeployButton {display: none;}
/* Responsive Design */
@media (max-width: 768px) {
.header-container {
padding: 0 1rem;
flex-direction: column;
gap: 1rem;
}
.input-container {
grid-template-columns: 1fr;
gap: 1rem;
}
.charts-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.footer-bottom {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}
</style>
"""
st.set_page_config(
page_title="TradingView Test - Stock Sentiment Analyzer",
layout="wide",
initial_sidebar_state="collapsed"
)
# Load cached static content - using minimal CDN for faster loads
st.markdown(get_minimal_cdn(), unsafe_allow_html=True)
# Database setup
CACHE_MINUTES = 15 # Cache data for 15 minutes
# FinViz news fetching functions
finviz_url = 'https://finviz.com/quote.ashx?t='
def get_news(ticker):
url = finviz_url + ticker
req = Request(url=url,headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0'})
# Minimal delay for better performance (was 1-3 seconds)
time.sleep(0.1) # Minimal 0.1s delay for rate limiting
try:
response = urlopen(req, timeout=15) # Add 15 second timeout
html = BeautifulSoup(response, 'html.parser')
news_table = html.find(id='news-table')
return news_table
except Exception as e:
print(f"Error fetching news from FinViz: {e}")
return None
def parse_news(news_table):
parsed_news = []
timezones = get_timezone_objects()
eastern = timezones['eastern']
ist = timezones['ist']
today_string = datetime.now(eastern).strftime('%Y-%m-%d')
last_date = today_string # Track last seen date for time-only entries
for x in news_table.find_all('tr'):
try:
text = x.a.get_text()
date_scrape = x.td.text.split()
if len(date_scrape) == 1:
time = date_scrape[0]
date = last_date # Use last seen date
else:
date = date_scrape[0]
time = date_scrape[1]
last_date = date # Update last seen date
parsed_news.append([date, time, text])
except:
pass
columns = ['date', 'time', 'headline']
parsed_news_df = pd.DataFrame(parsed_news, columns=columns)
parsed_news_df['date'] = parsed_news_df['date'].replace("Today", today_string)
parsed_news_df['datetime'] = pd.to_datetime(parsed_news_df['date'] + ' ' + parsed_news_df['time'], format='mixed')
parsed_news_df['datetime'] = parsed_news_df['datetime'].dt.tz_localize(eastern).dt.tz_convert(ist)
return parsed_news_df
def score_news(parsed_news_df):
vader = get_vader_analyzer()
scores = parsed_news_df['headline'].apply(vader.polarity_scores).tolist()
scores_df = pd.DataFrame(scores)
parsed_and_scored_news = parsed_news_df.join(scores_df, rsuffix='_right')
parsed_and_scored_news = parsed_and_scored_news.set_index('datetime')
parsed_and_scored_news = parsed_and_scored_news.drop(columns=['date', 'time'])
parsed_and_scored_news = parsed_and_scored_news.rename(columns={"compound": "sentiment_score"})
return parsed_and_scored_news
# Future article fixer integration
from future_article_fixer import check_future_articles, fix_future_articles, ignore_future_article
def create_tradingview_chart_data(data, chart_type):
"""Prepare data for streamlit-lightweight-charts with manual IST offset"""
# Ensure IST timezone awareness
ist = pytz.timezone('Asia/Kolkata')
utc = pytz.utc
# Make the data index timezone-aware if it isn't already
if data.index.tz is None:
data.index = data.index.tz_localize(ist)
elif str(data.index.tz) != 'Asia/Kolkata':
data.index = data.index.tz_convert(ist)
# Forward-fill gaps (keep in IST for now)
if len(data) > 0:
freq = 'h' if chart_type == 'hourly' else 'D'
complete_range = pd.date_range(
start=data.index.min(),
end=data.index.max(),
freq=freq,
tz=ist # Explicitly set timezone
)
filled_data = data.reindex(complete_range)
filled_data['sentiment_score'] = filled_data['sentiment_score'].ffill()
filled_data = filled_data.dropna()
else:
filled_data = data
# Convert to TradingView format with manual IST offset
chart_data = []
IST_OFFSET_SECONDS = 19800 # 5 hours 30 minutes in seconds
for timestamp, row in filled_data.iterrows():
if chart_type == 'hourly':
# Convert IST to UTC, then add IST offset to display in IST
timestamp_utc = timestamp.astimezone(utc)
# Add IST offset so chart displays IST time correctly
time_value = int(timestamp_utc.timestamp()) + IST_OFFSET_SECONDS
else:
# Daily: Use date string (no timezone needed)
time_value = timestamp.strftime('%Y-%m-%d')
chart_data.append({
"time": time_value,
"value": float(row['sentiment_score'])
})
return chart_data
def plot_hourly_sentiment(data, ticker, title_suffix=""):
"""Create hourly chart using streamlit-lightweight-charts"""
mean_scores = data.resample('h').mean(numeric_only=True)
if len(mean_scores) == 0:
st.warning("No hourly data available")
return
chart_data = create_tradingview_chart_data(mean_scores, 'hourly')
# Use streamlit-lightweight-charts package
chart_options = [{
"chart": {
"height": 400,
"layout": {
"background": {"color": "#0e1117"},
"textColor": "#fafafa"
},
"grid": {
"vertLines": {"color": "#262730"},
"horzLines": {"color": "#262730"}
},
"crosshair": {
"mode": 0 # Normal mode
},
"timeScale": {
"timeVisible": True,
"secondsVisible": False,
"hoursVisible": True,
"minutesVisible": True,
"borderVisible": True
}
},
"series": [{
"type": "Histogram",
"data": chart_data,
"options": {
"color": "#00d4ff",
"priceFormat": {
"type": "price",
"precision": 3,
"minMove": 0.001
}
}
}]
}]
renderLightweightCharts(chart_options, key=f"hourly_{ticker}")
def plot_daily_sentiment(data, ticker, title_suffix=""):
"""Create daily chart using streamlit-lightweight-charts"""
mean_scores = data.resample('d').mean(numeric_only=True)
if len(mean_scores) == 0:
st.warning("No daily data available")
return
chart_data = create_tradingview_chart_data(mean_scores, 'daily')
# Use streamlit-lightweight-charts package
chart_options = [{
"chart": {
"height": 400,
"layout": {
"background": {"color": "#0e1117"},
"textColor": "#fafafa"
},
"grid": {
"vertLines": {"color": "#262730"},
"horzLines": {"color": "#262730"}
},
"crosshair": {
"mode": 0 # Normal mode
},
"timeScale": {
"timeVisible": True,
"secondsVisible": False,
"hoursVisible": True,
"minutesVisible": True,
"borderVisible": True
}
},
"series": [{
"type": "Histogram",
"data": chart_data,
"options": {
"color": "#00d4ff",
"priceFormat": {
"type": "price",
"precision": 3,
"minMove": 0.001
}
}
}]
}]
renderLightweightCharts(chart_options, key=f"daily_{ticker}")
# Import future article fixer
from future_article_fixer import check_future_articles, fix_future_articles, ignore_future_article
# Lazy database initialization - only init when actually needed
def ensure_database_initialized():
"""Initialize database only when needed to avoid slow page loads"""
try:
init_database()
return True
except Exception as e:
print(f"Database initialization failed: {e}")
return False
# Load cached header
st.markdown(get_professional_header(), unsafe_allow_html=True)
# Future article check - moved to expandable section for performance
with st.expander("๐Ÿ”ง Database Maintenance", expanded=False):
st.write("Check for and fix future-dated articles")
if st.button("๐Ÿ” Check for Future-Dated Articles", type="secondary"):
with st.spinner("Checking for future-dated articles..."):
# Ensure database is initialized before checking future articles
ensure_database_initialized()
future_summary, future_count, future_articles_detail = check_future_articles()
if future_count > 0:
st.warning(f"โš ๏ธ **Future-Dated Articles Detected**")
st.text(future_summary)
col1, col2 = st.columns(2)
with col1:
if st.button("๐Ÿ”ง Fix Future-Dated Articles", type="primary"):
with st.spinner("Fixing future-dated articles by fetching correct timestamps from FinViz..."):
result = fix_future_articles()
if result['fixed'] > 0:
st.success(f"โœ… **Fixed {result['fixed']} articles!**")
if result['not_found'] > 0:
st.warning(f"โš ๏ธ **{result['not_found']} articles not found on FinViz**")
st.info("These articles may have been removed from FinViz. You can ignore them individually below.")
if result['errors'] > 0:
st.error(f"โŒ {result['errors']} tickers had errors during processing")
st.info("๐Ÿ”„ **Please refresh the page to see updated data**")
st.stop()
# Show individual articles with ignore option
if future_count > 0:
st.subheader("๐Ÿ“‹ Individual Articles")
for ticker, article_id, headline, article_dt in future_articles_detail:
col_a, col_b = st.columns([4, 1])
with col_a:
st.text(f"{ticker}: {headline[:80]}...")
st.caption(f"Timestamp: {article_dt}")
with col_b:
if st.button(f"๐Ÿ—‘๏ธ Ignore", key=f"ignore_{article_id}"):
ignore_future_article(article_id)
st.success(f"โœ… **Article deleted** - Warning will no longer appear")
st.info("๐Ÿ”„ **Please refresh the page**")
st.stop()
else:
st.success("โœ… No future-dated articles found!")
st.info("๐Ÿ”ฌ **Testing Environment**: This is a local test version with TradingView Lightweight Charts. The main app uses Plotly and is safe.")
# Enhanced Input Section (Streamlit-only, no duplicates)
input_section = """
<div class="input-section">
<div class="input-container">
<div class="input-wrapper">
<div class="input-icon">
<i class="ph ph-magnifying-glass"></i>
</div>
<div class="input-content">
<label class="input-label">Enter Stock Ticker</label>
</div>
</div>
<div class="view-selector-wrapper">
<label class="input-label">View Mode</label>
</div>
</div>
</div>
"""
st.markdown(input_section, unsafe_allow_html=True)
# Create columns for the actual Streamlit inputs (styled but functional)
col1, col2 = st.columns([3, 1])
with col1:
# Popular ticker options
popular_tickers = ['BTC', 'ETH', 'SOL', 'AMZN', 'GOOGL', 'TSLA', 'MSTR', 'AAPL']
# Create a custom input with dropdown suggestions
ticker_input_container = st.container()
with ticker_input_container:
col_input, col_dropdown = st.columns([2, 1])
with col_input:
ticker = st.text_input('Enter Stock Ticker', '', key='ticker_input', placeholder='e.g., BTC, AAPL, TSLA').upper()
with col_dropdown:
selected_ticker = st.selectbox(
'Popular Tickers',
['Select...'] + popular_tickers,
key='ticker_dropdown',
help='Choose from popular tickers'
)
# If user selects from dropdown, update the text input
if selected_ticker != 'Select...':
ticker = selected_ticker
with col2:
view_mode = st.selectbox('View', [
'Historical (7 days)',
'Historical (30 days)',
'Historical (60 days)',
'Historical (90 days)',
'Historical (6 months)',
'Historical (1 year)'
], index=1, key='view_selector')
if ticker:
try:
st.subheader(f"๐Ÿ“ˆ TradingView Test for {ticker}")
# Check if we can fetch new data and fetch if needed
# Ensure database is initialized before any database operations
ensure_database_initialized()
can_fetch, message = can_fetch_new_data(ticker)
# Show cache status
if can_fetch:
st.info(f"โšก {message}")
else:
st.warning(f"๐Ÿ• {message}")
# Fetch and store current data if allowed
if can_fetch:
with st.status("๐Ÿ”„ Fetching fresh data...", expanded=False) as status:
try:
# Step 1: Connect to database
status.update(label="๐Ÿ”— Connecting to database...", state="running")
# Step 2: Fetch news from FinViz
status.update(label="๐Ÿ“ฐ Fetching news from FinViz...", state="running")
news_table = get_news(ticker)
if news_table:
# Step 3: Parse news data
status.update(label="๐Ÿ“Š Parsing news data...", state="running")
parsed_news_df = parse_news(news_table)
# Step 4: Analyze sentiment
status.update(label="๐Ÿง  Analyzing sentiment...", state="running")
parsed_and_scored_news = score_news(parsed_news_df)
# Step 5: Store in database
status.update(label="๐Ÿ’พ Storing data in database...", state="running")
store_sentiment_data(ticker, parsed_and_scored_news)
update_cache_metadata(ticker, len(parsed_and_scored_news))
# Success
status.update(label=f"โœ… Successfully fetched and stored {len(parsed_and_scored_news)} articles", state="complete")
st.success(f"โœ… Fetched and stored {len(parsed_and_scored_news)} articles")
else:
status.update(label="โš ๏ธ No news found for this ticker", state="error")
st.warning("โš ๏ธ No news found for this ticker")
except Exception as e:
status.update(label=f"โŒ Error: {str(e)}", state="error")
st.error(f"โŒ Error fetching news: {str(e)}")
# Get historical data (including newly fetched data)
# Parse days from view_mode
if '7' in view_mode:
days = 7
elif '30' in view_mode:
days = 30
elif '60' in view_mode:
days = 60
elif '90' in view_mode:
days = 90
elif '6 months' in view_mode:
days = 180 # 6 months โ‰ˆ 180 days
elif '1 year' in view_mode:
days = 365 # 1 year = 365 days
else:
days = 30 # default fallback
# Get historical data (cached, so should be fast)
try:
historical_df = get_historical_data(ticker, days=days)
if len(historical_df) > 0:
st.success(f"โœ… Retrieved {len(historical_df)} historical articles")
else:
st.warning("โš ๏ธ No historical data found")
except Exception as e:
st.error(f"โŒ Database connection failed: {str(e)}")
st.info("๐Ÿ’ก The app will continue with limited functionality. Database may be temporarily unavailable.")
historical_df = pd.DataFrame() # Empty DataFrame to prevent crashes
if len(historical_df) > 0:
# Enhanced Chart Containers
chart_section = """
<div class="charts-section">
<div class="charts-header">
<h3 class="charts-title">
<i class="ph ph-chart-line"></i>
TradingView Professional Charts
</h3>
<div class="chart-controls">
<button class="chart-control-btn active" data-chart="hourly">
<i class="ph ph-clock"></i>
Hourly
</button>
<button class="chart-control-btn" data-chart="daily">
<i class="ph ph-calendar"></i>
Daily
</button>
</div>
</div>
</div>
"""
st.markdown(chart_section, unsafe_allow_html=True)
# Charts with professional wrappers
col1, col2 = st.columns(2)
with col1:
chart_wrapper = """
<div class="chart-container">
<div class="chart-header">
<h4 class="chart-title">
<i class="ph ph-clock"></i>
Hourly Sentiment Trend
</h4>
<div class="chart-actions">
<button class="chart-action-btn" title="Export Chart">
<i class="ph ph-download"></i>
</button>
<button class="chart-action-btn" title="Full Screen">
<i class="ph ph-arrows-out"></i>
</button>
</div>
</div>
<div class="chart-content">
"""
st.markdown(chart_wrapper, unsafe_allow_html=True)
plot_hourly_sentiment(historical_df, ticker, f" ({days} Days)")
st.markdown("</div></div>", unsafe_allow_html=True)
with col2:
chart_wrapper = """
<div class="chart-container">
<div class="chart-header">
<h4 class="chart-title">
<i class="ph ph-calendar"></i>
Daily Sentiment Trend
</h4>
<div class="chart-actions">
<button class="chart-action-btn" title="Export Chart">
<i class="ph ph-download"></i>
</button>
<button class="chart-action-btn" title="Full Screen">
<i class="ph ph-arrows-out"></i>
</button>
</div>
</div>
<div class="chart-content">
"""
st.markdown(chart_wrapper, unsafe_allow_html=True)
plot_daily_sentiment(historical_df, ticker, f" ({days} Days)")
st.markdown("</div></div>", unsafe_allow_html=True)
# Enhanced Data Table
table_section = """
<div class="data-table-section">
<div class="table-header">
<h3 class="table-title">
<i class="ph ph-table"></i>
Sample Data
</h3>
<div class="table-actions">
<button class="table-action-btn" title="Export CSV">
<i class="ph ph-download"></i>
Export
</button>
<button class="table-action-btn" title="Refresh Data">
<i class="ph ph-arrow-clockwise"></i>
Refresh
</button>
</div>
</div>
</div>
"""
st.markdown(table_section, unsafe_allow_html=True)
# Display the table with custom styling
st.dataframe(
historical_df[['headline', 'sentiment_score']].head(10),
column_config={
"headline": st.column_config.TextColumn(
"Headline",
width="large",
),
"sentiment_score": st.column_config.NumberColumn(
"Sentiment Score",
format="%.3f",
),
},
hide_index=True,
use_container_width=True
)
else:
st.warning(f"No historical data available for {ticker}. Try running the main app first to collect data.")
except Exception as e:
st.error(f"Error: {str(e)}")
else:
st.info("๐Ÿ‘† Enter a stock ticker above to test TradingView charts")
st.write("""
### Testing Features:
- **TradingView Lightweight Charts**: Professional charting library
- **Forward-fill gaps**: Continuous data for crosshair functionality
- **Crosshair everywhere**: Shows sentiment values at any cursor position
- **Dark theme**: Matches your app's styling
### How to test:
1. Enter a stock ticker (e.g., "BTC")
2. Charts will render using TradingView Lightweight Charts
3. Move cursor anywhere on chart - crosshair should show values
4. Compare with main app's Plotly charts
""")
# Load cached footer and CSS
st.markdown(get_professional_footer(), unsafe_allow_html=True)
st.markdown(get_professional_css(), unsafe_allow_html=True)