Spaces:
Running
Running
| import streamlit as st | |
| st.set_page_config(layout="wide") | |
| import yfinance as yf | |
| # import alpaca as tradeapi | |
| 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 | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| AUTO_TRADE_LOG_PATH = "auto_trade_log.json" # Path to store auto trade log | |
| # The trading history events are saved in the file "auto_trade_log.json" | |
| # This file is created and updated in the current working directory where you run your Streamlit app. | |
| AUTO_TRADE_INTERVAL = 10800 # Interval in seconds (e.g., 10800 seconds = 3 hours) | |
| 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): | |
| 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') | |
| logger.info(f"Bought {qty} shares of {symbol}") | |
| return order | |
| except Exception as e: | |
| logger.error(f"Error buying {symbol}: {e}") | |
| return None | |
| def sell(self, symbol, qty): | |
| # 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') | |
| logger.info(f"Sold {qty} shares of {symbol}") | |
| 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] = position.market_value | |
| 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): | |
| ''' | |
| 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 | |
| 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.warning(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] | |
| print(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()) | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| n = len(symbols) | |
| # Calculate columns based on n for best fit | |
| if n <= 3: | |
| cols = n | |
| elif n <= 6: | |
| cols = 3 | |
| elif n <= 8: | |
| cols = 4 | |
| elif n <= 12: | |
| cols = 4 | |
| else: | |
| cols = 5 | |
| 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') | |
| qty = int(st.number_input('Enter quantity')) | |
| action = st.selectbox('Action', ['Buy', 'Sell']) | |
| if st.button('Execute'): | |
| if action == 'Buy': | |
| order = self.alpaca.buy(symbol, qty) | |
| else: | |
| order = self.alpaca.sell(symbol, qty) | |
| if order: | |
| st.success(f"Order executed: {action} {qty} shares of {symbol}") | |
| else: | |
| st.error("Order failed") | |
| st.header("Portfolio") | |
| st.write("Cash Balance:") | |
| st.write(self.alpaca.getCash()) | |
| st.write("Holdings:") | |
| st.write(self.alpaca.getHoldings()) | |
| st.write("Recent Trades:") | |
| st.write(pd.DataFrame(self.alpaca.trades)) | |
| def auto_trade_based_on_sentiment(self, sentiment): | |
| # Add company name to each action | |
| actions = [] | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| for symbol, sentiment_value in sentiment.items(): | |
| action = None | |
| if sentiment_value == 'Positive': | |
| order = self.alpaca.buy(symbol, 1) | |
| action = 'Buy' | |
| elif sentiment_value == 'Negative': | |
| order = self.alpaca.sell(symbol, 1) | |
| action = 'Sell' | |
| else: | |
| order = None | |
| action = 'Hold' | |
| actions.append({ | |
| 'symbol': symbol, | |
| 'company_name': symbol_to_name.get(symbol, ''), | |
| 'sentiment': sentiment_value, | |
| 'action': action | |
| }) | |
| self.auto_trade_log = actions | |
| return actions | |
| def background_auto_trade(app): | |
| # This function runs in a background thread and does not require a TTY. | |
| # The warning "tcgetpgrp failed: Not a tty" is harmless and can be ignored. | |
| # It is likely caused by the environment in which the script is running (e.g., Streamlit, Docker, or a notebook). | |
| # No code changes are needed for this warning. | |
| while True: | |
| sentiment = app.sentiment.get_news_sentiment(app.analyzer.symbols) | |
| actions = [] | |
| for symbol, sentiment_value in sentiment.items(): | |
| action = None | |
| if sentiment_value == 'Positive': | |
| order = app.alpaca.buy(symbol, 1) | |
| action = 'Buy' | |
| elif sentiment_value == 'Negative': | |
| order = app.alpaca.sell(symbol, 1) | |
| action = 'Sell' | |
| else: | |
| order = None | |
| action = 'Hold' | |
| actions.append({ | |
| 'symbol': symbol, | |
| 'sentiment': sentiment_value, | |
| 'action': action | |
| }) | |
| # Append to log file instead of overwriting | |
| log_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "actions": actions, | |
| "sentiment": sentiment | |
| } | |
| try: | |
| if os.path.exists(AUTO_TRADE_LOG_PATH): | |
| with open(AUTO_TRADE_LOG_PATH, "r") as f: | |
| log_data = json.load(f) | |
| else: | |
| log_data = [] | |
| except Exception: | |
| log_data = [] | |
| log_data.append(log_entry) | |
| with open(AUTO_TRADE_LOG_PATH, "w") as f: | |
| json.dump(log_data, f) | |
| time.sleep(AUTO_TRADE_INTERVAL) | |
| def load_auto_trade_log(): | |
| try: | |
| with open(AUTO_TRADE_LOG_PATH, "r") as f: | |
| return json.load(f) | |
| except Exception: | |
| return None | |
| # Replace deprecated st.experimental_set_query_params with st.query_params | |
| st.query_params() # This is a no-op but ensures Streamlit doesn't rerun due to query params | |
| 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()) | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| n = len(symbols) | |
| # Calculate columns based on n for best fit | |
| if n <= 3: | |
| cols = n | |
| elif n <= 6: | |
| cols = 3 | |
| elif n <= 8: | |
| cols = 4 | |
| elif n <= 12: | |
| cols = 4 | |
| else: | |
| cols = 5 | |
| 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') | |
| qty = int(st.number_input('Enter quantity')) | |
| action = st.selectbox('Action', ['Buy', 'Sell']) | |
| if st.button('Execute'): | |
| if action == 'Buy': | |
| order = self.alpaca.buy(symbol, qty) | |
| else: | |
| order = self.alpaca.sell(symbol, qty) | |
| if order: | |
| st.success(f"Order executed: {action} {qty} shares of {symbol}") | |
| else: | |
| st.error("Order failed") | |
| st.header("Portfolio") | |
| st.write("Cash Balance:") | |
| st.write(self.alpaca.getCash()) | |
| st.write("Holdings:") | |
| st.write(self.alpaca.getHoldings()) | |
| st.write("Recent Trades:") | |
| st.write(pd.DataFrame(self.alpaca.trades)) | |
| def auto_trade_based_on_sentiment(self, sentiment): | |
| # Add company name to each action | |
| actions = [] | |
| symbol_to_name = self.analyzer.symbol_to_name | |
| for symbol, sentiment_value in sentiment.items(): | |
| action = None | |
| if sentiment_value == 'Positive': | |
| order = self.alpaca.buy(symbol, 1) | |
| action = 'Buy' | |
| elif sentiment_value == 'Negative': | |
| order = self.alpaca.sell(symbol, 1) | |
| action = 'Sell' | |
| else: | |
| order = None | |
| action = 'Hold' | |
| actions.append({ | |
| 'symbol': symbol, | |
| 'company_name': symbol_to_name.get(symbol, ''), | |
| 'sentiment': sentiment_value, | |
| 'action': action | |
| }) | |
| self.auto_trade_log = actions | |
| return actions | |
| def background_auto_trade(app): | |
| # This function runs in a background thread and does not require a TTY. | |
| # The warning "tcgetpgrp failed: Not a tty" is harmless and can be ignored. | |
| # It is likely caused by the environment in which the script is running (e.g., Streamlit, Docker, or a notebook). | |
| # No code changes are needed for this warning. | |
| while True: | |
| sentiment = app.sentiment.get_news_sentiment(app.analyzer.symbols) | |
| actions = [] | |
| for symbol, sentiment_value in sentiment.items(): | |
| action = None | |
| if sentiment_value == 'Positive': | |
| order = app.alpaca.buy(symbol, 1) | |
| action = 'Buy' | |
| elif sentiment_value == 'Negative': | |
| order = app.alpaca.sell(symbol, 1) | |
| action = 'Sell' | |
| else: | |
| order = None | |
| action = 'Hold' | |
| actions.append({ | |
| 'symbol': symbol, | |
| 'sentiment': sentiment_value, | |
| 'action': action | |
| }) | |
| # Append to log file instead of overwriting | |
| log_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "actions": actions, | |
| "sentiment": sentiment | |
| } | |
| try: | |
| if os.path.exists(AUTO_TRADE_LOG_PATH): | |
| with open(AUTO_TRADE_LOG_PATH, "r") as f: | |
| log_data = json.load(f) | |
| else: | |
| log_data = [] | |
| except Exception: | |
| log_data = [] | |
| log_data.append(log_entry) | |
| with open(AUTO_TRADE_LOG_PATH, "w") as f: | |
| json.dump(log_data, f) | |
| time.sleep(AUTO_TRADE_INTERVAL) | |
| def load_auto_trade_log(): | |
| try: | |
| with open(AUTO_TRADE_LOG_PATH, "r") as f: | |
| return json.load(f) | |
| except Exception: | |
| return None | |
| # Add this at the top after imports to suppress Streamlit reruns on widget interaction | |
| st.experimental_set_query_params() # This is a no-op but ensures Streamlit doesn't rerun due to query params | |
| 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("Stock Trading Application") | |
| if not st.secrets['ALPACA_API_KEY'] or not st.secrets['NEWS_API_KEY']: | |
| st.error("Please configure your API keys in secrets.toml") | |
| 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"] | |
| # 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 | |
| # Dynamic market clock | |
| 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") | |
| # Show countdown to close | |
| 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") | |
| # Show countdown to open | |
| seconds_left = int((next_open - now).total_seconds()) | |
| st.markdown(f"**Time until open:** {pd.to_timedelta(seconds_left, unit='s')}") | |
| # Add auto-refresh for the clock every 5 seconds | |
| st.experimental_rerun() | |
| time.sleep(5) | |
| # User inputs and portfolio are now in the sidebar | |
| app.manual_trade() | |
| # Main area: plots and data | |
| app.display_charts() | |
| # Read and display latest auto-trade actions | |
| st.write("Automatic Trading Actions Based on Sentiment (background):") | |
| auto_trade_log = load_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"]) | |
| # Reorder columns for clarity | |
| 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 (aggregate for all symbols) | |
| 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"]) | |
| # Pivot to get Buy/Sell counts per symbol over time | |
| # Avoid FutureWarning by explicitly converting to float after replace | |
| hist_df["action_value"] = hist_df["action"].replace({"Buy": 1, "Sell": -1}) | |
| hist_df["action_value"] = hist_df["action_value"].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...") | |
| # Explanation: | |
| # In Alpaca: | |
| # - 'cash' is the actual cash available in your account (uninvested funds). | |
| # - 'buying_power' is the total amount you can use to buy securities, which may be higher than cash if you have margin enabled. | |
| # For a cash account, buying_power == cash. | |
| # For a margin account, buying_power can be up to 2x (or 4x for day trading) your cash, depending on regulations and your account status. | |
| # Example usage: | |
| # account = alpaca.get_account() | |
| # cash_balance = account.cash | |
| # buying_power = account.buying_power | |
| # Note: | |
| # To disable margin on your Alpaca paper account, you must set your account type to "cash" instead of "margin". | |
| # This cannot be changed via the API or code. You must: | |
| # 1. Log in to your Alpaca dashboard at https://app.alpaca.markets/ | |
| # 2. Go to "Paper Trading" > "Settings" | |
| # 3. Set the account type to "Cash" (not "Margin") | |
| # 4. If you do not see this option, you may need to reset your paper account or contact Alpaca support. | |
| # There is no programmatic/API way to change the margin setting for a paper account. | |
| if __name__ == "__main__": | |
| main() |