Spaces:
Running
Running
| import streamlit as st | |
| import yfinance as yf | |
| import alpaca_trade_api as alpaca | |
| from newsapi import NewsApiClient | |
| from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer | |
| from datetime import datetime, timedelta | |
| import streamlit as st | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import logging | |
| import threading | |
| import time | |
| import json | |
| import os | |
| import plotly.graph_objs as go | |
| from sklearn.preprocessing import minmax_scale | |
| from plotly.subplots import make_subplots | |
| # Configure logging with timestamps | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(levelname)s - %(message)s", | |
| datefmt="%Y-%m-%d %H:%M:%S" | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Use session state keys instead of file paths | |
| AUTO_TRADE_LOG_KEY = "auto_trade_log" # Session state key for trade log | |
| AUTO_TRADE_INTERVAL = 10800 # Interval in seconds (e.g., 10800 seconds = 3 hours) | |
| st.set_page_config(layout="wide") | |
| class AlpacaTrader: | |
| def __init__(self, API_KEY, API_SECRET, BASE_URL): | |
| self.alpaca = alpaca.REST(API_KEY, API_SECRET, BASE_URL) | |
| self.cash = 0 | |
| self.holdings = {} | |
| self.trades = [] | |
| def get_market_status(self): | |
| return self.alpaca.get_clock().is_open | |
| def buy(self, symbol, qty, reason=None): | |
| try: | |
| # Ensure at least $1000 in cash before buying | |
| account = self.alpaca.get_account() | |
| cash_balance = float(account.cash) | |
| if cash_balance < 1000: | |
| logger.warning(f"Low cash: (${cash_balance}) to buy {symbol}. Minimum $1000 required.") | |
| return None | |
| order = self.alpaca.submit_order(symbol=symbol, qty=qty, side='buy', type='market', time_in_force='day') | |
| if reason: | |
| logger.info(f"Bought {qty} shares of {symbol} [Reason: {reason}]") | |
| else: | |
| logger.info(f"Bought {qty} shares of {symbol}") | |
| # Record the trade | |
| if order: | |
| self.trades.append({ | |
| 'symbol': symbol, | |
| 'qty': qty, | |
| 'action': 'Buy', | |
| 'time': datetime.now(), | |
| 'reason': reason | |
| }) | |
| return order | |
| except Exception as e: | |
| logger.error(f"Error buying {symbol}: {e}") | |
| return None | |
| def sell(self, symbol, qty, reason=None): | |
| # Check if position exists and has enough quantity before attempting to sell | |
| positions = {p.symbol: float(p.qty) for p in self.alpaca.list_positions()} | |
| if symbol not in positions: | |
| logger.warning(f"No position in {symbol}. Sell not attempted.") | |
| return None | |
| if positions[symbol] < qty: | |
| logger.warning(f"Not enough shares to sell: {qty} requested, {positions[symbol]} available for {symbol}. Sell not attempted.") | |
| return None | |
| try: | |
| order = self.alpaca.submit_order(symbol=symbol, qty=qty, side='sell', type='market', time_in_force='day') | |
| if reason: | |
| logger.info(f"Sold {qty} shares of {symbol} [Reason: {reason}]") | |
| else: | |
| logger.info(f"Sold {qty} shares of {symbol}") | |
| # Record the trade | |
| if order: | |
| self.trades.append({ | |
| 'symbol': symbol, | |
| 'qty': qty, | |
| 'action': 'Sell', | |
| 'time': datetime.now(), | |
| 'reason': reason | |
| }) | |
| return order | |
| except Exception as e: | |
| logger.error(f"Error selling {symbol}: {e}") | |
| return None | |
| def getHoldings(self): | |
| positions = self.alpaca.list_positions() | |
| for position in positions: | |
| self.holdings[position.symbol] = float(position.market_value) | |
| # Return holdings as a dictionary for internal use | |
| return self.holdings | |
| def getCash(self): | |
| return self.alpaca.get_account().cash | |
| def update_portfolio(self, symbol, price, qty, action): | |
| if action == 'buy': | |
| self.cash -= price * qty | |
| if symbol in self.holdings: | |
| self.holdings[symbol] += price * qty | |
| else: | |
| self.holdings[symbol] = price * qty | |
| elif action == 'sell': | |
| self.cash += price * qty | |
| self.holdings[symbol] -= price * qty | |
| if self.holdings[symbol] <= 0: | |
| del self.holdings[symbol] | |
| self.trades.append({'symbol': symbol, 'price': price, 'qty': qty, 'action': action, 'time': datetime.now()}) | |
| class NewsSentiment: | |
| def __init__(self, API_KEY): | |
| ''' | |
| 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. | |
| ''' | |
| self.newsapi = NewsApiClient(api_key=API_KEY) | |
| self.sia = SentimentIntensityAnalyzer() | |
| def get_news_sentiment(self, symbols): | |
| ''' | |
| 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: | |
| 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.'} | |
| ''' | |
| sentiment = {} | |
| for symbol in symbols: | |
| try: | |
| articles = self.newsapi.get_everything(q=symbol, | |
| language='en', | |
| sort_by='publishedAt', # <-- fixed argument name | |
| page=1) | |
| compound_score = 0 | |
| for article in articles['articles'][:5]: # Check first 5 articles | |
| # print(f'article= {article}') | |
| score = self.sia.polarity_scores(article['title'])['compound'] | |
| compound_score += score | |
| avg_score = compound_score / 5 if articles['articles'] else 0 | |
| if avg_score > 0.1: | |
| sentiment[symbol] = 'Positive' | |
| elif avg_score < -0.1: | |
| sentiment[symbol] = 'Negative' | |
| else: | |
| sentiment[symbol] = 'Neutral' | |
| except Exception as e: | |
| logger.error(f"Error getting news for {symbol}: {e}") | |
| sentiment[symbol] = 'Neutral' | |
| return sentiment | |
| class StockAnalyzer: | |
| def __init__(self, alpaca): | |
| self.alpaca = alpaca | |
| self.symbols = self.get_top_volume_stocks() | |
| # Build a symbol->name mapping for use in plots/tables | |
| self.symbol_to_name = self.get_symbol_to_name() | |
| def get_symbol_to_name(self): | |
| # Get mapping from symbol to company name using Alpaca asset info | |
| assets = self.alpaca.alpaca.list_assets(status='active') | |
| return {asset.symbol: asset.name for asset in assets} | |
| def get_bars(self, alp_api, symbols, timeframe='1D'): | |
| bars_data = {} | |
| try: | |
| bars = alp_api.get_bars(list(symbols), timeframe).df | |
| if 'symbol' not in bars.columns: | |
| logger.warning("The 'symbol' column is missing in the bars DataFrame.") | |
| return {symbol: {'bar_data': None} for symbol in symbols} | |
| for symbol in symbols: | |
| symbol_bars = bars[bars['symbol'] == symbol] | |
| if not symbol_bars.empty: | |
| bar_info = symbol_bars.iloc[-1] | |
| # Handle index type for timestamp | |
| if isinstance(bar_info.name, tuple): | |
| timestamp = bar_info.name[1].isoformat() | |
| else: | |
| timestamp = bar_info.name.isoformat() | |
| bars_data[symbol] = { | |
| 'bar_data': { | |
| 'volume': bar_info['volume'], | |
| 'open': bar_info['open'], | |
| 'high': bar_info['high'], | |
| 'low': bar_info['low'], | |
| 'close': bar_info['close'], | |
| 'timestamp': timestamp | |
| } | |
| } | |
| else: | |
| logger.debug(f"No bar data for symbol: {symbol}") | |
| bars_data[symbol] = {'bar_data': None} | |
| except Exception as e: | |
| logger.warning(f"Error fetching bars in batch: {e}") | |
| for symbol in symbols: | |
| bars_data[symbol] = {'bar_data': None} | |
| return bars_data | |
| def assetswithconditions(self,stock_assets): | |
| cond = { | |
| 'class': ['us_equity'], | |
| 'exchange': ['NASDAQ', 'NYSE'], | |
| 'status': ['active'], | |
| 'tradable': [True], | |
| 'marginable': [True], | |
| 'shortable': [True], | |
| 'easy_to_borrow': [True], | |
| 'fractionable': [True] | |
| } | |
| assets_with_conditions = [] | |
| asset_symbol_dict = {} | |
| for asset in stock_assets: | |
| # Skip symbols with '.' or '/' (preferred shares, warrants, etc.) | |
| if '.' in asset.symbol or '/' in asset.symbol: | |
| continue | |
| if (asset.__getattr__('class') in cond['class'] and | |
| asset.exchange in cond['exchange'] and | |
| asset.status in cond['status'] and | |
| asset.tradable in cond['tradable'] and | |
| asset.marginable in cond['marginable'] and | |
| asset.shortable in cond['shortable'] and | |
| asset.easy_to_borrow in cond['easy_to_borrow'] and | |
| asset.fractionable in cond['fractionable'] | |
| ): | |
| assets_with_conditions.append(asset) | |
| asset_no_comma = asset.name.replace(',', '') | |
| asset_first_word = asset_no_comma.split()[0] | |
| asset_symbol_dict[asset.symbol] = asset._raw | |
| asset_symbol_dict[asset.symbol]['firstWord'] = asset_first_word | |
| sorted_dict = dict(sorted(asset_symbol_dict.items())) | |
| # print(f'Length of Alpaca assets with conditions = {len(assets_with_conditions)}') | |
| # print(f'assets_with_conditions = {assets_with_conditions}') | |
| return assets_with_conditions, sorted_dict | |
| def get_top_volume_stocks(self,num_stocks=10): | |
| try: | |
| # Get all tradable assets | |
| assets = self.alpaca.alpaca.list_assets(status='active') | |
| # tradable_assets = {asset.symbol: {} for asset in assets if asset.tradable} | |
| # print(f'tradable_assets = {tradable_assets}') | |
| assets_with_conditions, sorted_dict = self.assetswithconditions(assets) | |
| # print(f'sorted_dict = {sorted_dict}') | |
| # Fetch bar data for all tradable assets | |
| # print(f'sorted_dict.keys()={sorted_dict.keys()}') | |
| tradable_assets = self.get_bars(self.alpaca.alpaca, sorted_dict.keys(), timeframe='1D') | |
| # Extract volume and calculate the top 10 stocks by volume | |
| volume_data = { | |
| symbol: info['bar_data']['volume'] | |
| for symbol, info in tradable_assets.items() | |
| if info['bar_data'] is not None | |
| } | |
| top_volume_stocks = sorted(volume_data, key=volume_data.get, reverse=True)[:num_stocks] | |
| logger.info(f'top_volume_stocks = {top_volume_stocks}') | |
| return top_volume_stocks | |
| except Exception as e: | |
| logger.error(f"Error fetching top volume stocks: {e}") | |
| return [] | |
| def get_historical_data(self, symbols): | |
| data = {} | |
| for symbol in symbols: | |
| try: | |
| # Pull historical data from 2000-01-01 to today, daily interval | |
| ticker = yf.Ticker(symbol) | |
| hist = ticker.history(start='2023-01-01', end=datetime.now().strftime('%Y-%m-%d'), interval='1d') | |
| data[symbol] = hist | |
| except Exception as e: | |
| logger.error(f"Error getting data for {symbol}: {e}") | |
| return data | |
| class TradingApp: | |
| def __init__(self): | |
| self.alpaca = AlpacaTrader(st.secrets['ALPACA_API_KEY'], st.secrets['ALPACA_SECRET_KEY'], 'https://paper-api.alpaca.markets') | |
| self.sentiment = NewsSentiment(st.secrets['NEWS_API_KEY']) | |
| self.analyzer = StockAnalyzer(self.alpaca) | |
| self.data = self.analyzer.get_historical_data(self.analyzer.symbols) | |
| self.auto_trade_log = [] # Store automatic trade actions | |
| def display_charts(self): | |
| # Dynamically adjust columns based on number of stocks and available width | |
| symbols = list(self.data.keys()) | |
| if not symbols: | |
| st.warning("No stock data available to display charts.") | |
| return # Exit the function if no symbols are available | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| n = len(symbols) | |
| # Calculate columns based on n for best fit | |
| cols = 3 | |
| rows = (n + cols - 1) // cols | |
| subplot_titles = [ | |
| f"{symbol} - {symbol_to_name.get(symbol, '')}" for symbol in symbols | |
| ] | |
| fig = make_subplots(rows=rows, cols=cols, subplot_titles=subplot_titles) | |
| for idx, symbol in enumerate(symbols): | |
| df = self.data[symbol] | |
| if not df.empty: | |
| row = idx // cols + 1 | |
| col = idx % cols + 1 | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df.index, | |
| y=df['Close'], | |
| mode='lines', | |
| name=symbol, | |
| hovertemplate=f"%{{x}}<br>{symbol}: %{{y:.2f}}<extra></extra>" | |
| ), | |
| row=row, | |
| col=col | |
| ) | |
| fig.update_layout( | |
| title="Top Volume Stocks - Price Charts (Since 2023)", | |
| height=max(400 * rows, 600), | |
| showlegend=False, | |
| dragmode=False, | |
| ) | |
| # Enable scroll-zoom for each subplot (individual zoom) | |
| fig.update_layout( | |
| xaxis=dict(fixedrange=False), | |
| yaxis=dict(fixedrange=False), | |
| ) | |
| for i in range(1, rows * cols + 1): | |
| fig.layout[f'xaxis{i}'].update(fixedrange=False) | |
| fig.layout[f'yaxis{i}'].update(fixedrange=False) | |
| st.plotly_chart(fig, use_container_width=True, config={"scrollZoom": True}) | |
| def manual_trade(self): | |
| # Move all user inputs to the sidebar | |
| with st.sidebar: | |
| st.header("Manual Trade") | |
| symbol = st.text_input('Enter stock symbol') | |
| # --- Sentiment Check Feature (moved up) --- | |
| sentiment_result = None | |
| article_headlines = [] | |
| if st.button("Check Sentiment"): | |
| if symbol: | |
| try: | |
| # Use NewsSentiment to get sentiment | |
| sentiment_dict = self.sentiment.get_news_sentiment([symbol]) | |
| sentiment_result = sentiment_dict.get(symbol) | |
| # Try NewsAPI headlines first, fallback to yfinance if fails | |
| try: | |
| articles = self.sentiment.newsapi.get_everything(q=symbol, language='en', sort_by='publishedAt', page=1) | |
| article_headlines = [a['title'] for a in articles.get('articles', [])[:5]] | |
| if not article_headlines: | |
| raise Exception("No NewsAPI headlines") | |
| except Exception: | |
| # Fallback to yfinance headlines | |
| try: | |
| ticker = yf.Ticker(symbol) | |
| news_items = ticker.news if hasattr(ticker, "news") else [] | |
| article_headlines = [item.get('title') for item in news_items[:5] if item.get('title')] | |
| except Exception: | |
| article_headlines = [] | |
| except Exception as e: | |
| sentiment_result = None | |
| article_headlines = [] | |
| else: | |
| sentiment_result = None | |
| article_headlines = [] | |
| if sentiment_result is not None: | |
| st.markdown(f"**Sentiment for {symbol.upper()}:** {sentiment_result if sentiment_result in ['Positive', 'Negative', 'Neutral'] else 'No sentiment available'}") | |
| elif sentiment_result is None and st.session_state.get("Check Sentiment"): | |
| st.markdown("**Sentiment:** No sentiment available") | |
| if article_headlines: | |
| st.markdown("**Recent Headlines:**") | |
| for headline in article_headlines: | |
| st.write(f"- {headline}") | |
| elif sentiment_result is not None and not article_headlines: | |
| st.markdown("_No headlines available._") | |
| # Fetch the current stock price dynamically using Alpaca's API | |
| def get_stock_price(symbol): | |
| try: | |
| if not symbol: | |
| return None | |
| last_trade = self.alpaca.alpaca.get_latest_trade(symbol) | |
| return last_trade.price | |
| except Exception as e: | |
| logger.error(f"Error fetching stock price for {symbol}: {e}") | |
| return None | |
| # Update stock price when a new symbol is entered | |
| if symbol: | |
| if "stock_price" not in st.session_state or st.session_state.get("last_symbol") != symbol: | |
| st.session_state["stock_price"] = get_stock_price(symbol) | |
| st.session_state["last_symbol"] = symbol | |
| stock_price = st.session_state.get("stock_price") | |
| # Explicitly display the stock price below the input field | |
| if stock_price is not None: | |
| st.write(f"Current stock price for {symbol.upper()}: ${stock_price:,.2f}") | |
| else: | |
| st.write("Enter a valid stock symbol to see the price.") | |
| # Allow user to enter either quantity or amount | |
| trade_option = st.radio("Trade Option", ["Enter Quantity", "Enter Amount"]) | |
| qty = st.number_input('Enter quantity', min_value=0.0, step=0.01, value=0.0) if trade_option == "Enter Quantity" else None | |
| amount = st.number_input('Enter amount ($)', min_value=0.0, step=0.01, value=0.0) if trade_option == "Enter Amount" else None | |
| # Dynamically calculate the other field | |
| if stock_price: | |
| if trade_option == "Enter Quantity" and qty: | |
| amount = qty * stock_price | |
| st.write(f"Calculated Amount: ${amount:,.2f}") | |
| elif trade_option == "Enter Amount" and amount: | |
| qty = float(amount / stock_price) | |
| st.write(f"Calculated Quantity: {qty:,.2f}") | |
| action = st.selectbox('Action', ['Buy', 'Sell']) | |
| if st.button('Execute'): | |
| if stock_price and qty: | |
| is_market_open = self.alpaca.get_market_status() | |
| if action == 'Buy': | |
| order = self.alpaca.buy(symbol, qty, reason="Manual Trade") | |
| else: | |
| order = self.alpaca.sell(symbol, qty, reason="Manual Trade") | |
| if order: | |
| if not is_market_open: | |
| _, _, next_open, _ = get_market_times(self.alpaca.alpaca) | |
| next_open_time = next_open.strftime('%Y-%m-%d %H:%M:%S') if next_open else "unknown" | |
| st.warning(f"Market is currently closed. The {action.lower()} order for {qty} shares of {symbol} has been submitted and will execute when the market opens at {next_open_time}.") | |
| else: | |
| st.success(f"Order executed: {action} {qty} shares of {symbol}") | |
| else: | |
| st.error("Order failed") | |
| else: | |
| st.error("Please enter a valid stock symbol and trade details.") | |
| def auto_trade_based_on_sentiment(self, sentiment): | |
| """Execute trades based on sentiment analysis and return actions taken.""" | |
| actions = self._execute_sentiment_trades(sentiment) | |
| self.auto_trade_log = actions | |
| return actions | |
| def _execute_sentiment_trades(self, sentiment): | |
| """Helper method to execute trades based on sentiment. | |
| Used by both auto_trade_based_on_sentiment and background_auto_trade.""" | |
| actions = [] | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| for symbol, sentiment_value in sentiment.items(): | |
| action = None | |
| is_market_open = self.alpaca.get_market_status() | |
| if sentiment_value == 'Positive': | |
| order = self.alpaca.buy(symbol, 1, reason="Sentiment: Positive") | |
| action = 'Buy' | |
| elif sentiment_value == 'Negative': | |
| order = self.alpaca.sell(symbol, 1, reason="Sentiment: Negative") | |
| action = 'Sell' | |
| else: | |
| order = None | |
| action = 'Hold' | |
| logger.info(f"Held {symbol}") | |
| if order: | |
| if not is_market_open: | |
| _, _, next_open, _ = get_market_times(self.alpaca.alpaca) | |
| next_open_time = next_open.strftime('%Y-%m-%d %H:%M:%S') if next_open else "unknown" | |
| logger.warning(f"Market is currently closed. The {action.lower()} order for 1 share of {symbol} has been submitted and will execute when the market opens at {next_open_time}.") | |
| else: | |
| logger.info(f"Order executed: {action} 1 share of {symbol}") | |
| actions.append({ | |
| 'symbol': symbol, | |
| 'company_name': symbol_to_name.get(symbol, ''), | |
| 'sentiment': sentiment_value, | |
| 'action': action | |
| }) | |
| return actions | |
| def background_auto_trade(app): | |
| """This function runs in a background thread and updates session state with automatic trades.""" | |
| while True: | |
| start_time = time.time() # Record the start time of the iteration | |
| sentiment = app.sentiment.get_news_sentiment(app.analyzer.symbols) | |
| # Use the shared method to execute trades | |
| actions = app._execute_sentiment_trades(sentiment) | |
| # Create log entry | |
| log_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "actions": actions, | |
| "sentiment": sentiment | |
| } | |
| # Update session state - ensure the UI reflects the latest data | |
| if AUTO_TRADE_LOG_KEY not in st.session_state: | |
| st.session_state[AUTO_TRADE_LOG_KEY] = [] | |
| st.session_state[AUTO_TRADE_LOG_KEY].append(log_entry) | |
| # Limit size to avoid memory issues (keep last 50 entries) | |
| if len(st.session_state[AUTO_TRADE_LOG_KEY]) > 50: | |
| st.session_state[AUTO_TRADE_LOG_KEY] = st.session_state[AUTO_TRADE_LOG_KEY][-50:] | |
| # Log the update | |
| logger.info(f"Auto-trade completed. Actions: {actions}") | |
| # Calculate the time taken for this iteration | |
| elapsed_time = time.time() - start_time | |
| sleep_time = max(0, AUTO_TRADE_INTERVAL - elapsed_time) # Ensure non-negative sleep time | |
| logger.info(f"Sleeping for {sleep_time:.2f} seconds before the next auto-trade.") | |
| time.sleep(sleep_time) | |
| def get_auto_trade_log(): | |
| """Get the auto trade log from session state.""" | |
| if AUTO_TRADE_LOG_KEY not in st.session_state: | |
| st.session_state[AUTO_TRADE_LOG_KEY] = [] | |
| return st.session_state[AUTO_TRADE_LOG_KEY] | |
| def get_market_times(alpaca_api): | |
| try: | |
| clock = alpaca_api.get_clock() | |
| is_open = clock.is_open | |
| now = pd.Timestamp(clock.timestamp).tz_convert('America/New_York') | |
| next_close = pd.Timestamp(clock.next_close).tz_convert('America/New_York') | |
| next_open = pd.Timestamp(clock.next_open).tz_convert('America/New_York') | |
| return is_open, now, next_open, next_close | |
| except Exception as e: | |
| logger.error(f"Error fetching market times: {e}") | |
| return None, None, None, None | |
| def main(): | |
| st.title("Ben's Stock Trading Application") | |
| st.markdown("This is a fun stock trading application that uses Alpaca API for trading and News API for sentiment analysis. Come and trade my money! Well, it's a paper account, so it's not real money. But still, have fun!") | |
| if not st.secrets['ALPACA_API_KEY'] or not st.secrets['NEWS_API_KEY']: | |
| st.error("Please configure your ALPACA_API_KEY and NEWS_API_KEY") | |
| return | |
| # Prevent Streamlit from rerunning the script on every widget interaction | |
| # Use session state to persist objects and only update when necessary | |
| if "app_instance" not in st.session_state: | |
| st.session_state["app_instance"] = TradingApp() | |
| app = st.session_state["app_instance"] | |
| # Create two columns for market status and portfolio holdings | |
| col1, col2 = st.columns([1, 1]) | |
| # Column 1: Portfolio holdings bar chart | |
| with col1: | |
| st.subheader("Portfolio Holdings") | |
| holdings_container = st.empty() # Create a container for dynamic updates | |
| def update_holdings(): | |
| holdings = app.alpaca.getHoldings() | |
| if holdings: | |
| df = pd.DataFrame(list(holdings.items()), columns=['Ticker', 'Market Value']) | |
| fig = go.Figure( | |
| data=[ | |
| go.Bar( | |
| x=df['Ticker'], | |
| y=df['Market Value'], | |
| marker=dict(color=df['Market Value'], colorscale='Viridis'), | |
| ) | |
| ] | |
| ) | |
| fig.update_layout( | |
| xaxis_title="Ticker", | |
| yaxis_title="$ USD", | |
| height=400, | |
| ) | |
| # Use a unique key by appending the current timestamp | |
| holdings_container.plotly_chart(fig, use_container_width=True, key=f"portfolio_holdings_chart_{time.time()}") | |
| else: | |
| holdings_container.info("No holdings to display.") | |
| # Periodically refresh the holdings plot | |
| update_holdings() | |
| st.button("Refresh Holdings", on_click=update_holdings) | |
| # Add an expandable section for detailed holdings | |
| st.subheader("Detailed Holdings") | |
| with st.expander("View Detailed Holdings"): | |
| holdings = app.alpaca.getHoldings() # Use self.alpaca instead of app.alpaca | |
| if holdings: | |
| # Get positions to access both market value and quantity | |
| positions = app.alpaca.alpaca.list_positions() | |
| positions_data = [] | |
| for position in positions: | |
| positions_data.append({ | |
| "Ticker": position.symbol, | |
| "Shares": float(position.qty), | |
| "Amount (USD)": round(float(position.market_value)) | |
| }) | |
| detailed_holdings = pd.DataFrame(positions_data) | |
| st.table(detailed_holdings) | |
| else: | |
| st.info("No holdings to display.") | |
| # Column 2: Market status | |
| with col2: | |
| is_open, now, next_open, next_close = get_market_times(app.alpaca.alpaca) | |
| market_status = "π’ Market is OPEN" if is_open else "π΄ Market is CLOSED" | |
| st.markdown(f"### {market_status}") | |
| if now is not None: | |
| st.markdown(f"**Current time (ET):** {now.strftime('%Y-%m-%d %H:%M:%S')}") | |
| if is_open and next_close is not None: | |
| st.markdown(f"**Market closes at:** {next_close.strftime('%Y-%m-%d %H:%M:%S')} ET") | |
| seconds_left = int((next_close - now).total_seconds()) | |
| st.markdown(f"**Time until close:** {pd.to_timedelta(seconds_left, unit='s')}") | |
| elif not is_open and next_open is not None: | |
| st.markdown(f"**Market opens at:** {next_open.strftime('%Y-%m-%d %H:%M:%S')} ET") | |
| seconds_left = int((next_open - now).total_seconds()) | |
| st.markdown(f"**Time until open:** {pd.to_timedelta(seconds_left, unit='s')}") | |
| # Initialize auto trade log in session state if needed | |
| if AUTO_TRADE_LOG_KEY not in st.session_state: | |
| st.session_state[AUTO_TRADE_LOG_KEY] = [] | |
| # Only start the background thread once | |
| if "auto_trade_thread_started" not in st.session_state: | |
| thread = threading.Thread(target=background_auto_trade, args=(app,), daemon=True) | |
| thread.start() | |
| st.session_state["auto_trade_thread_started"] = True | |
| # Main area: plots and data | |
| app.manual_trade() | |
| app.display_charts() | |
| # Read and display latest auto-trade actions | |
| st.write("Automatic Trading Actions Based on Sentiment (background):") | |
| auto_trade_log = get_auto_trade_log() | |
| if auto_trade_log: | |
| # Show the most recent entry | |
| last_entry = auto_trade_log[-1] | |
| st.write(f"Last checked: {last_entry['timestamp']}") | |
| df = pd.DataFrame(last_entry["actions"]) | |
| if "company_name" in df.columns: | |
| df = df[["symbol", "company_name", "sentiment", "action"]] | |
| st.dataframe(df) | |
| st.write("Sentiment Analysis (latest):") | |
| st.write(last_entry["sentiment"]) | |
| # Plot buy/sell actions over time | |
| st.write("Auto-Trading History (Buy/Sell Actions Over Time):") | |
| history = [] | |
| for entry in auto_trade_log: | |
| ts = entry["timestamp"] | |
| for act in entry["actions"]: | |
| if act["action"] in ("Buy", "Sell"): | |
| history.append({ | |
| "timestamp": ts, | |
| "symbol": act["symbol"], | |
| "action": act["action"] | |
| }) | |
| if history: | |
| hist_df = pd.DataFrame(history) | |
| if not hist_df.empty: | |
| hist_df["timestamp"] = pd.to_datetime(hist_df["timestamp"]) | |
| hist_df["action_value"] = hist_df["action"].replace({"Buy": 1, "Sell": -1}).astype(float) | |
| pivot = hist_df.pivot_table(index="timestamp", columns="symbol", values="action_value", aggfunc="sum") | |
| st.line_chart(pivot.fillna(0)) | |
| else: | |
| st.info("Waiting for first background auto-trade run...") | |
| if __name__ == "__main__": | |
| main() |