Spaces:
Running
Running
Benjamin Consolvo
commited on
Commit
·
7b415bc
1
Parent(s):
2fd894f
yfinance logging
Browse files
app.py
CHANGED
@@ -126,37 +126,68 @@ class AlpacaTrader:
|
|
126 |
|
127 |
class NewsSentiment:
|
128 |
def __init__(self, API_KEY):
|
129 |
-
'''
|
130 |
-
Hutto, C.J. & Gilbert, E.E. (2014). VADER: A Parsimonious Rule-based Model for Sentiment Analysis of Social Media Text. Eighth International Conference on Weblogs and Social Media (ICWSM-14). Ann Arbor, MI, June 2014.
|
131 |
-
'''
|
132 |
self.newsapi = NewsApiClient(api_key=API_KEY)
|
133 |
self.sia = SentimentIntensityAnalyzer()
|
134 |
|
135 |
-
def
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
sentiment = {}
|
142 |
for symbol in symbols:
|
143 |
try:
|
144 |
-
articles = self.newsapi.get_everything(q=symbol,
|
145 |
-
|
146 |
-
|
147 |
-
page=1)
|
148 |
-
compound_score = 0
|
149 |
-
for article in articles['articles'][:5]: # Check first 5 articles
|
150 |
-
# print(f'article= {article}')
|
151 |
-
score = self.sia.polarity_scores(article['title'])['compound']
|
152 |
-
compound_score += score
|
153 |
-
avg_score = compound_score / 5 if articles['articles'] else 0
|
154 |
-
if avg_score > 0.1:
|
155 |
-
sentiment[symbol] = 'Positive'
|
156 |
-
elif avg_score < -0.1:
|
157 |
-
sentiment[symbol] = 'Negative'
|
158 |
-
else:
|
159 |
-
sentiment[symbol] = 'Neutral'
|
160 |
except Exception as e:
|
161 |
logger.error(f"Error getting news for {symbol}: {e}")
|
162 |
sentiment[symbol] = 'Neutral'
|
@@ -302,51 +333,6 @@ class TradingApp:
|
|
302 |
self.data = self.analyzer.get_historical_data(self.analyzer.symbols)
|
303 |
self.auto_trade_log = [] # Store automatic trade actions
|
304 |
|
305 |
-
def get_newsapi_sentiment_and_headlines(self, symbol):
|
306 |
-
"""Get sentiment and headlines using NewsAPI for a symbol."""
|
307 |
-
sentiment_result = None
|
308 |
-
article_headlines = []
|
309 |
-
try:
|
310 |
-
sentiment_dict = self.sentiment.get_news_sentiment([symbol])
|
311 |
-
sentiment_result = sentiment_dict.get(symbol)
|
312 |
-
articles = self.sentiment.newsapi.get_everything(q=symbol, language='en', sort_by='publishedAt', page=1)
|
313 |
-
article_headlines = [a['title'] for a in articles.get('articles', [])[:5]]
|
314 |
-
except Exception as e:
|
315 |
-
logger.error(f"NewsAPI sentiment/headlines error for {symbol}: {e}")
|
316 |
-
return sentiment_result, article_headlines
|
317 |
-
|
318 |
-
def get_yfinance_sentiment_and_headlines(self, symbol):
|
319 |
-
"""Get sentiment and headlines using yfinance for a symbol."""
|
320 |
-
sentiment_result = None
|
321 |
-
article_headlines = []
|
322 |
-
try:
|
323 |
-
ticker = yf.Ticker(symbol)
|
324 |
-
news_items = ticker.news if hasattr(ticker, "news") else []
|
325 |
-
article_headlines = [item.get('title') for item in news_items[:5] if item.get('title')]
|
326 |
-
# Use VADER on yfinance headlines if available
|
327 |
-
if article_headlines:
|
328 |
-
compound_score = 0
|
329 |
-
for title in article_headlines:
|
330 |
-
score = self.sentiment.sia.polarity_scores(title)['compound']
|
331 |
-
compound_score += score
|
332 |
-
avg_score = compound_score / len(article_headlines)
|
333 |
-
if avg_score > 0.1:
|
334 |
-
sentiment_result = 'Positive'
|
335 |
-
elif avg_score < -0.1:
|
336 |
-
sentiment_result = 'Negative'
|
337 |
-
else:
|
338 |
-
sentiment_result = 'Neutral'
|
339 |
-
except Exception as e:
|
340 |
-
logger.error(f"yfinance sentiment/headlines error for {symbol}: {e}")
|
341 |
-
return sentiment_result, article_headlines
|
342 |
-
|
343 |
-
def get_combined_sentiment_and_headlines(self, symbol):
|
344 |
-
"""Try NewsAPI first, fallback to yfinance if needed."""
|
345 |
-
sentiment_result, article_headlines = self.get_newsapi_sentiment_and_headlines(symbol)
|
346 |
-
if not article_headlines:
|
347 |
-
sentiment_result, article_headlines = self.get_yfinance_sentiment_and_headlines(symbol)
|
348 |
-
return sentiment_result, article_headlines
|
349 |
-
|
350 |
def display_charts(self):
|
351 |
# Dynamically adjust columns based on number of stocks and available width
|
352 |
symbols = list(self.data.keys())
|
@@ -401,7 +387,7 @@ class TradingApp:
|
|
401 |
st.header("Manual Trade")
|
402 |
symbol = st.text_input('Enter stock symbol')
|
403 |
|
404 |
-
# --- Sentiment Check Feature
|
405 |
if "sentiment_result" not in st.session_state:
|
406 |
st.session_state["sentiment_result"] = None
|
407 |
if "article_headlines" not in st.session_state:
|
@@ -409,7 +395,7 @@ class TradingApp:
|
|
409 |
|
410 |
if st.button("Check Sentiment"):
|
411 |
if symbol:
|
412 |
-
sentiment_result, article_headlines = self.
|
413 |
st.session_state["sentiment_result"] = sentiment_result
|
414 |
st.session_state["article_headlines"] = article_headlines
|
415 |
st.session_state["sentiment_symbol"] = symbol
|
@@ -418,7 +404,6 @@ class TradingApp:
|
|
418 |
st.session_state["article_headlines"] = []
|
419 |
st.session_state["sentiment_symbol"] = ""
|
420 |
|
421 |
-
# Always display the last checked sentiment/headlines for the current symbol
|
422 |
sentiment_result = st.session_state.get("sentiment_result")
|
423 |
article_headlines = st.session_state.get("article_headlines", [])
|
424 |
sentiment_symbol = st.session_state.get("sentiment_symbol", "")
|
@@ -515,20 +500,17 @@ class TradingApp:
|
|
515 |
st.button("Refresh Portfolio", on_click=refresh_portfolio)
|
516 |
|
517 |
def auto_trade_based_on_sentiment(self, sentiment):
|
518 |
-
"""Execute trades based on sentiment analysis and return actions taken."""
|
519 |
actions = self._execute_sentiment_trades(sentiment)
|
520 |
self.auto_trade_log = actions
|
521 |
return actions
|
522 |
|
523 |
def _execute_sentiment_trades(self, sentiment):
|
524 |
-
"""Helper method to execute trades based on sentiment.
|
525 |
-
Used by both auto_trade_based_on_sentiment and background_auto_trade."""
|
526 |
actions = []
|
527 |
symbol_to_name = self.analyzer.symbol_to_name
|
528 |
for symbol, sentiment_value in sentiment.items():
|
529 |
-
#
|
530 |
if sentiment_value is None or sentiment_value not in ['Positive', 'Negative', 'Neutral']:
|
531 |
-
sentiment_value, _ = self.
|
532 |
action = None
|
533 |
is_market_open = self.alpaca.get_market_status()
|
534 |
if sentiment_value == 'Positive':
|
@@ -561,37 +543,23 @@ class TradingApp:
|
|
561 |
def background_auto_trade(app):
|
562 |
"""This function runs in a background thread and updates session state with automatic trades."""
|
563 |
while True:
|
564 |
-
start_time = time.time()
|
565 |
-
|
566 |
-
sentiment = app.sentiment.
|
567 |
-
|
568 |
-
# Use the shared method to execute trades
|
569 |
actions = app._execute_sentiment_trades(sentiment)
|
570 |
-
|
571 |
-
# Create log entry
|
572 |
log_entry = {
|
573 |
"timestamp": datetime.now().isoformat(),
|
574 |
"actions": actions,
|
575 |
"sentiment": sentiment
|
576 |
}
|
577 |
-
|
578 |
-
# Update session state - ensure the UI reflects the latest data
|
579 |
if AUTO_TRADE_LOG_KEY not in st.session_state:
|
580 |
st.session_state[AUTO_TRADE_LOG_KEY] = []
|
581 |
-
|
582 |
st.session_state[AUTO_TRADE_LOG_KEY].append(log_entry)
|
583 |
-
|
584 |
-
# Limit size to avoid memory issues (keep last 50 entries)
|
585 |
if len(st.session_state[AUTO_TRADE_LOG_KEY]) > 50:
|
586 |
st.session_state[AUTO_TRADE_LOG_KEY] = st.session_state[AUTO_TRADE_LOG_KEY][-50:]
|
587 |
-
|
588 |
-
# Log the update
|
589 |
logger.info(f"Auto-trade completed. Actions: {actions}")
|
590 |
-
|
591 |
-
# Calculate the time taken for this iteration
|
592 |
elapsed_time = time.time() - start_time
|
593 |
-
sleep_time = max(0, AUTO_TRADE_INTERVAL - elapsed_time)
|
594 |
-
|
595 |
logger.info(f"Sleeping for {sleep_time:.2f} seconds before the next auto-trade.")
|
596 |
time.sleep(sleep_time)
|
597 |
|
|
|
126 |
|
127 |
class NewsSentiment:
|
128 |
def __init__(self, API_KEY):
|
|
|
|
|
|
|
129 |
self.newsapi = NewsApiClient(api_key=API_KEY)
|
130 |
self.sia = SentimentIntensityAnalyzer()
|
131 |
|
132 |
+
def get_sentiment_and_headlines(self, symbol):
|
133 |
+
"""
|
134 |
+
Try NewsAPI first, fallback to yfinance if needed.
|
135 |
+
Returns (sentiment, headlines).
|
136 |
+
"""
|
137 |
+
# Try NewsAPI
|
138 |
+
try:
|
139 |
+
articles = self.newsapi.get_everything(q=symbol, language='en', sort_by='publishedAt', page=1)
|
140 |
+
headlines = [a['title'] for a in articles.get('articles', [])[:5]]
|
141 |
+
if headlines:
|
142 |
+
sentiment = self._calculate_sentiment(headlines)
|
143 |
+
return sentiment, headlines
|
144 |
+
else:
|
145 |
+
logger.warning(f"NewsAPI returned no headlines for {symbol}.")
|
146 |
+
except Exception as e:
|
147 |
+
logger.error(f"NewsAPI error for {symbol}: {e}")
|
148 |
+
# Explicitly log that fallback will be attempted
|
149 |
+
logger.info(f"Falling back to yfinance for {symbol} sentiment and headlines.")
|
150 |
+
|
151 |
+
# Fallback to yfinance
|
152 |
+
try:
|
153 |
+
ticker = yf.Ticker(symbol)
|
154 |
+
news_items = ticker.news if hasattr(ticker, "news") else []
|
155 |
+
headlines = [item.get('title') for item in news_items[:5] if item.get('title')]
|
156 |
+
if headlines:
|
157 |
+
logger.info(f"Using yfinance headlines for {symbol}: {headlines}")
|
158 |
+
sentiment = self._calculate_sentiment(headlines)
|
159 |
+
return sentiment, headlines
|
160 |
+
else:
|
161 |
+
logger.warning(f"yfinance returned no headlines for {symbol}.")
|
162 |
+
except Exception as e:
|
163 |
+
logger.error(f"yfinance error for {symbol}: {e}")
|
164 |
+
|
165 |
+
logger.info(f"No sentiment/headlines available for {symbol} from either NewsAPI or yfinance.")
|
166 |
+
return None, []
|
167 |
+
|
168 |
+
def _calculate_sentiment(self, headlines):
|
169 |
+
if not headlines:
|
170 |
+
return None
|
171 |
+
compound_score = sum(self.sia.polarity_scores(title)['compound'] for title in headlines)
|
172 |
+
avg_score = compound_score / len(headlines)
|
173 |
+
if avg_score > 0.1:
|
174 |
+
return 'Positive'
|
175 |
+
elif avg_score < -0.1:
|
176 |
+
return 'Negative'
|
177 |
+
else:
|
178 |
+
return 'Neutral'
|
179 |
+
|
180 |
+
def get_sentiment_bulk(self, symbols):
|
181 |
+
"""
|
182 |
+
Bulk sentiment for a list of symbols using NewsAPI only (for auto-trade).
|
183 |
+
Returns dict: symbol -> sentiment.
|
184 |
+
"""
|
185 |
sentiment = {}
|
186 |
for symbol in symbols:
|
187 |
try:
|
188 |
+
articles = self.newsapi.get_everything(q=symbol, language='en', sort_by='publishedAt', page=1)
|
189 |
+
headlines = [a['title'] for a in articles.get('articles', [])[:5]]
|
190 |
+
sentiment[symbol] = self._calculate_sentiment(headlines) if headlines else 'Neutral'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
except Exception as e:
|
192 |
logger.error(f"Error getting news for {symbol}: {e}")
|
193 |
sentiment[symbol] = 'Neutral'
|
|
|
333 |
self.data = self.analyzer.get_historical_data(self.analyzer.symbols)
|
334 |
self.auto_trade_log = [] # Store automatic trade actions
|
335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
def display_charts(self):
|
337 |
# Dynamically adjust columns based on number of stocks and available width
|
338 |
symbols = list(self.data.keys())
|
|
|
387 |
st.header("Manual Trade")
|
388 |
symbol = st.text_input('Enter stock symbol')
|
389 |
|
390 |
+
# --- Unified Sentiment Check Feature ---
|
391 |
if "sentiment_result" not in st.session_state:
|
392 |
st.session_state["sentiment_result"] = None
|
393 |
if "article_headlines" not in st.session_state:
|
|
|
395 |
|
396 |
if st.button("Check Sentiment"):
|
397 |
if symbol:
|
398 |
+
sentiment_result, article_headlines = self.sentiment.get_sentiment_and_headlines(symbol)
|
399 |
st.session_state["sentiment_result"] = sentiment_result
|
400 |
st.session_state["article_headlines"] = article_headlines
|
401 |
st.session_state["sentiment_symbol"] = symbol
|
|
|
404 |
st.session_state["article_headlines"] = []
|
405 |
st.session_state["sentiment_symbol"] = ""
|
406 |
|
|
|
407 |
sentiment_result = st.session_state.get("sentiment_result")
|
408 |
article_headlines = st.session_state.get("article_headlines", [])
|
409 |
sentiment_symbol = st.session_state.get("sentiment_symbol", "")
|
|
|
500 |
st.button("Refresh Portfolio", on_click=refresh_portfolio)
|
501 |
|
502 |
def auto_trade_based_on_sentiment(self, sentiment):
|
|
|
503 |
actions = self._execute_sentiment_trades(sentiment)
|
504 |
self.auto_trade_log = actions
|
505 |
return actions
|
506 |
|
507 |
def _execute_sentiment_trades(self, sentiment):
|
|
|
|
|
508 |
actions = []
|
509 |
symbol_to_name = self.analyzer.symbol_to_name
|
510 |
for symbol, sentiment_value in sentiment.items():
|
511 |
+
# If sentiment is missing or invalid, try to get it using fallback
|
512 |
if sentiment_value is None or sentiment_value not in ['Positive', 'Negative', 'Neutral']:
|
513 |
+
sentiment_value, _ = self.sentiment.get_sentiment_and_headlines(symbol)
|
514 |
action = None
|
515 |
is_market_open = self.alpaca.get_market_status()
|
516 |
if sentiment_value == 'Positive':
|
|
|
543 |
def background_auto_trade(app):
|
544 |
"""This function runs in a background thread and updates session state with automatic trades."""
|
545 |
while True:
|
546 |
+
start_time = time.time()
|
547 |
+
# Use NewsAPI only for bulk sentiment (to avoid rate limits and speed)
|
548 |
+
sentiment = app.sentiment.get_sentiment_bulk(app.analyzer.symbols)
|
|
|
|
|
549 |
actions = app._execute_sentiment_trades(sentiment)
|
|
|
|
|
550 |
log_entry = {
|
551 |
"timestamp": datetime.now().isoformat(),
|
552 |
"actions": actions,
|
553 |
"sentiment": sentiment
|
554 |
}
|
|
|
|
|
555 |
if AUTO_TRADE_LOG_KEY not in st.session_state:
|
556 |
st.session_state[AUTO_TRADE_LOG_KEY] = []
|
|
|
557 |
st.session_state[AUTO_TRADE_LOG_KEY].append(log_entry)
|
|
|
|
|
558 |
if len(st.session_state[AUTO_TRADE_LOG_KEY]) > 50:
|
559 |
st.session_state[AUTO_TRADE_LOG_KEY] = st.session_state[AUTO_TRADE_LOG_KEY][-50:]
|
|
|
|
|
560 |
logger.info(f"Auto-trade completed. Actions: {actions}")
|
|
|
|
|
561 |
elapsed_time = time.time() - start_time
|
562 |
+
sleep_time = max(0, AUTO_TRADE_INTERVAL - elapsed_time)
|
|
|
563 |
logger.info(f"Sleeping for {sleep_time:.2f} seconds before the next auto-trade.")
|
564 |
time.sleep(sleep_time)
|
565 |
|