Spaces:
Sleeping
Sleeping
Benjamin Consolvo
commited on
Commit
·
4f4559a
1
Parent(s):
93252b7
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 |
|