Benjamin Consolvo commited on
Commit
7b415bc
·
1 Parent(s): 2fd894f

yfinance logging

Browse files
Files changed (1) hide show
  1. app.py +64 -96
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 get_news_sentiment(self, symbols):
136
- '''
137
- The News API has a rate limit of 100 requests per day for free accounts. If you exceed this limit, you'll get a rateLimited error. Example error message:
138
- ERROR:__main__:Error getting news for APLD: {'status': 'error', 'code': 'rateLimited', 'message': 'You have made too many requests recently. Developer accounts are limited to 100 requests over a 24 hour period (50 requests available every 12 hours). Please upgrade to a paid plan if you need more requests.'}
139
-
140
- '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  sentiment = {}
142
  for symbol in symbols:
143
  try:
144
- articles = self.newsapi.get_everything(q=symbol,
145
- language='en',
146
- sort_by='publishedAt', # <-- fixed argument name
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 (fixed with session state) ---
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.get_combined_sentiment_and_headlines(symbol)
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
- # Use refactored sentiment logic for each symbol
530
  if sentiment_value is None or sentiment_value not in ['Positive', 'Negative', 'Neutral']:
531
- sentiment_value, _ = self.get_combined_sentiment_and_headlines(symbol)
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() # Record the start time of the iteration
565
-
566
- sentiment = app.sentiment.get_news_sentiment(app.analyzer.symbols)
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) # Ensure non-negative sleep 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