Spaces:
Running
on
Zero
Running
on
Zero
Upload 9 files
#40
by
DZsoul
- opened
- STOCK-MARKET-APP/app.py +14 -0
- STOCK-MARKET-APP/config.py +58 -0
- STOCK-MARKET-APP/mode.page.txt +49 -0
- STOCK-MARKET-APP/model.py +51 -0
- STOCK-MARKET-APP/plots.py +49 -0
- STOCK-MARKET-APP/requirement.txt +9 -0
- STOCK-MARKET-APP/stock_data_loader.py +19 -0
- STOCK-MARKET-APP/utils.py +324 -0
- STOCK-MARKET-APP/view_page.py +48 -0
STOCK-MARKET-APP/app.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from view_page import StockDashboard
|
| 3 |
+
from model_page import StockModelPage
|
| 4 |
+
|
| 5 |
+
def main():
|
| 6 |
+
st.set_page_config(layout='wide', page_title='Stock Analysis', page_icon=':dollar:')
|
| 7 |
+
page = st.sidebar.radio('Pages', ['View Page', 'Model Page'])
|
| 8 |
+
if page == 'View Page':
|
| 9 |
+
StockDashboard().run()
|
| 10 |
+
elif page == 'Model Page':
|
| 11 |
+
StockModelPage().run()
|
| 12 |
+
|
| 13 |
+
if __name__ == '__main__':
|
| 14 |
+
main()
|
STOCK-MARKET-APP/config.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration settings for the stock forecasting app
|
| 2 |
+
|
| 3 |
+
# Rate limiting settings
|
| 4 |
+
RATE_LIMIT_DELAY = 0.5 # Minimum delay between API calls (seconds)
|
| 5 |
+
MAX_RETRIES = 3 # Maximum number of retries for failed requests
|
| 6 |
+
BASE_RETRY_DELAY = 3 # Base delay for exponential backoff (seconds)
|
| 7 |
+
|
| 8 |
+
# Cache settings
|
| 9 |
+
DEFAULT_CACHE_TTL = 300 # Default cache time-to-live (seconds) - 5 minutes
|
| 10 |
+
MODEL_CACHE_TTL = 600 # Cache TTL for model data (seconds) - 10 minutes
|
| 11 |
+
|
| 12 |
+
# API settings
|
| 13 |
+
YAHOO_FINANCE_TIMEOUT = 10 # Timeout for yfinance requests (seconds)
|
| 14 |
+
|
| 15 |
+
# UI settings
|
| 16 |
+
DEFAULT_TICKERS = ['NVDA', 'AAPL', 'GOOGL', 'MSFT', 'AMZN']
|
| 17 |
+
PERIOD_MAP = {
|
| 18 |
+
'all': 'max',
|
| 19 |
+
'1m': '1mo',
|
| 20 |
+
'6m': '6mo',
|
| 21 |
+
'1y': '1y'
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# Error messages
|
| 25 |
+
ERROR_MESSAGES = {
|
| 26 |
+
'rate_limit': """
|
| 27 |
+
π« **Rate Limit Exceeded**
|
| 28 |
+
|
| 29 |
+
Yahoo Finance has temporarily limited your requests. This happens when too many requests are made in a short time.
|
| 30 |
+
|
| 31 |
+
**What you can do:**
|
| 32 |
+
- Wait 5-10 minutes before trying again
|
| 33 |
+
- Use the cached data if available
|
| 34 |
+
- Try a different stock ticker
|
| 35 |
+
|
| 36 |
+
The app will automatically retry with delays between requests.
|
| 37 |
+
""",
|
| 38 |
+
'network': """
|
| 39 |
+
π **Network Error**
|
| 40 |
+
|
| 41 |
+
There seems to be a connectivity issue.
|
| 42 |
+
|
| 43 |
+
**What you can do:**
|
| 44 |
+
- Check your internet connection
|
| 45 |
+
- Try refreshing the page
|
| 46 |
+
- Wait a moment and try again
|
| 47 |
+
""",
|
| 48 |
+
'no_data': """
|
| 49 |
+
π **No Data Available**
|
| 50 |
+
|
| 51 |
+
No stock data was found for the selected ticker and time period.
|
| 52 |
+
|
| 53 |
+
**What you can do:**
|
| 54 |
+
- Try a different time period
|
| 55 |
+
- Check if the ticker symbol is correct
|
| 56 |
+
- Try a different stock ticker
|
| 57 |
+
"""
|
| 58 |
+
}
|
STOCK-MARKET-APP/mode.page.txt
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from model import Model
|
| 4 |
+
from plots import Plots
|
| 5 |
+
from stock_data_loader import StockDataLoader
|
| 6 |
+
|
| 7 |
+
class StockModelPage:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.tickers = ['NVDA', 'AAPL', 'GOOGL', 'MSFT', 'AMZN']
|
| 10 |
+
self.setup_sidebar()
|
| 11 |
+
|
| 12 |
+
def setup_sidebar(self):
|
| 13 |
+
self.ticker = st.sidebar.selectbox('Choose Stock Ticker', self.tickers)
|
| 14 |
+
self.start_date = st.sidebar.date_input('Start Date', value=pd.to_datetime('2010-01-01'))
|
| 15 |
+
self.end_date = st.sidebar.date_input('End Date', value=pd.to_datetime('today'))
|
| 16 |
+
self.load_button_clicked = st.sidebar.button('Load Data')
|
| 17 |
+
|
| 18 |
+
def load_data(self):
|
| 19 |
+
if self.load_button_clicked:
|
| 20 |
+
loader = StockDataLoader(self.ticker, self.start_date, self.end_date)
|
| 21 |
+
st.session_state['stock_data'] = loader.get_stock_data()
|
| 22 |
+
st.write("--------------------------------------------")
|
| 23 |
+
st.write(f"Data for {self.ticker} from {self.start_date} to {self.end_date} loaded successfully!")
|
| 24 |
+
|
| 25 |
+
def handle_model_training(self):
|
| 26 |
+
if 'stock_data' in st.session_state:
|
| 27 |
+
stock_data = st.session_state['stock_data']
|
| 28 |
+
if st.button('Train Model'):
|
| 29 |
+
st.write("Training Model...")
|
| 30 |
+
model = Model(stock_data)
|
| 31 |
+
model.train_lstm()
|
| 32 |
+
predictions = model.make_predictions()
|
| 33 |
+
future_predictions = model.forecast_future(days=5)
|
| 34 |
+
self.plot_predictions(stock_data, predictions, future_predictions)
|
| 35 |
+
else:
|
| 36 |
+
st.write("Click the button above to train the model.")
|
| 37 |
+
else:
|
| 38 |
+
st.write("--------------------------------------------")
|
| 39 |
+
st.write("Please load data before training the model.")
|
| 40 |
+
|
| 41 |
+
def plot_predictions(self, stock_data, predictions, future_predictions):
|
| 42 |
+
plot_instance = Plots(stock_data)
|
| 43 |
+
plot_instance.plot_predictions(predictions, future_predictions)
|
| 44 |
+
|
| 45 |
+
def run(self):
|
| 46 |
+
st.write("--------------------------------------------")
|
| 47 |
+
st.write(f'<div style="font-size:50px">π€ Real-Time Stock Prediction', unsafe_allow_html=True)
|
| 48 |
+
self.load_data()
|
| 49 |
+
self.handle_model_training()
|
STOCK-MARKET-APP/model.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 3 |
+
from keras.models import Sequential
|
| 4 |
+
from keras.layers import LSTM, Dense
|
| 5 |
+
import warnings
|
| 6 |
+
warnings.filterwarnings("ignore")
|
| 7 |
+
|
| 8 |
+
class Model:
|
| 9 |
+
def __init__(self, data):
|
| 10 |
+
self.data = data
|
| 11 |
+
self.scaler = MinMaxScaler(feature_range=(0, 1))
|
| 12 |
+
self.model = None
|
| 13 |
+
|
| 14 |
+
def prepare_data(self, look_back=1):
|
| 15 |
+
scaled_data = self.scaler.fit_transform(self.data['Close'].values.reshape(-1, 1))
|
| 16 |
+
def create_dataset(dataset):
|
| 17 |
+
X, Y = [], []
|
| 18 |
+
for i in range(len(dataset) - look_back):
|
| 19 |
+
a = dataset[i:(i + look_back), 0]
|
| 20 |
+
X.append(a)
|
| 21 |
+
Y.append(dataset[i + look_back, 0])
|
| 22 |
+
return np.array(X), np.array(Y)
|
| 23 |
+
|
| 24 |
+
X, Y = create_dataset(scaled_data)
|
| 25 |
+
X = np.reshape(X, (X.shape[0], 1, X.shape[1]))
|
| 26 |
+
return X, Y
|
| 27 |
+
|
| 28 |
+
def train_lstm(self, epochs=5, batch_size=1):
|
| 29 |
+
X, Y = self.prepare_data()
|
| 30 |
+
self.model = Sequential()
|
| 31 |
+
self.model.add(LSTM(50, input_shape=(1, 1)))
|
| 32 |
+
self.model.add(Dense(1))
|
| 33 |
+
self.model.compile(loss='mean_squared_error', optimizer='adam')
|
| 34 |
+
self.model.fit(X, Y, epochs=epochs, batch_size=batch_size, verbose=0)
|
| 35 |
+
|
| 36 |
+
def make_predictions(self):
|
| 37 |
+
X, _ = self.prepare_data()
|
| 38 |
+
predictions = self.model.predict(X)
|
| 39 |
+
predictions = self.scaler.inverse_transform(predictions)
|
| 40 |
+
return predictions
|
| 41 |
+
|
| 42 |
+
def forecast_future(self, days=5):
|
| 43 |
+
last_value = self.data['Close'].values[-1:].reshape(-1, 1)
|
| 44 |
+
last_scaled = self.scaler.transform(last_value)
|
| 45 |
+
future_predictions = []
|
| 46 |
+
for _ in range(days):
|
| 47 |
+
prediction = self.model.predict(last_scaled.reshape(1, 1, 1))[0]
|
| 48 |
+
future_predictions.append(prediction)
|
| 49 |
+
last_scaled = prediction
|
| 50 |
+
future_predictions = self.scaler.inverse_transform(future_predictions)
|
| 51 |
+
return future_predictions
|
STOCK-MARKET-APP/plots.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
from plotly.subplots import make_subplots
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class StockChart:
|
| 8 |
+
def __init__(self, data):
|
| 9 |
+
self.data = data
|
| 10 |
+
self.fig = make_subplots(rows=2, cols=1, vertical_spacing=0.01, shared_xaxes=True)
|
| 11 |
+
|
| 12 |
+
def add_price_chart(self):
|
| 13 |
+
self.fig.add_trace(go.Scatter(x=self.data.index, y=self.data['Open'], name='Open Price', marker_color='#1F77B4'), row=1, col=1)
|
| 14 |
+
self.fig.add_trace(go.Scatter(x=self.data.index, y=self.data['High'], name='High Price', marker_color='#9467BD'), row=1, col=1)
|
| 15 |
+
self.fig.add_trace(go.Scatter(x=self.data.index, y=self.data['Low'], name='Low Price', marker_color='#D62728'), row=1, col=1)
|
| 16 |
+
self.fig.add_trace(go.Scatter(x=self.data.index, y=self.data['Close'], name='Close Price', marker_color='#76B900'), row=1, col=1)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def add_oversold_overbought_lines(self):
|
| 20 |
+
self.fig.add_hline(y=30, line_dash='dash', line_color='limegreen', line_width=1, row=1, col=1)
|
| 21 |
+
self.fig.add_hline(y=70, line_dash='dash', line_color='red', line_width=1, row=1, col=1)
|
| 22 |
+
self.fig.update_yaxes(title_text='RSI Score', row=1, col=1)
|
| 23 |
+
|
| 24 |
+
def add_volume_chart(self):
|
| 25 |
+
colors = ['#9C1F0B' if row['Open'] - row['Close'] >= 0 else '#2B8308' for index, row in self.data.iterrows()]
|
| 26 |
+
self.fig.add_trace(go.Bar(x=self.data.index, y=self.data['Volume'], showlegend=False, marker_color=colors), row=2, col=1)
|
| 27 |
+
|
| 28 |
+
def render_chart(self):
|
| 29 |
+
self.fig.update_layout(title='Historical Price and Volume', height=500, margin=dict(l=0, r=10, b=10, t=25))
|
| 30 |
+
st.plotly_chart(self.fig, use_container_width=True)
|
| 31 |
+
|
| 32 |
+
class Plots:
|
| 33 |
+
def __init__(self, data):
|
| 34 |
+
self.data = data
|
| 35 |
+
|
| 36 |
+
def plot_predictions(self, predictions, future_predictions):
|
| 37 |
+
|
| 38 |
+
predicted_dates = self.data.index[-len(predictions):]
|
| 39 |
+
future_dates = pd.date_range(start=self.data.index[-1] + pd.Timedelta(days=1), periods=len(future_predictions), freq='B')
|
| 40 |
+
predictions = [float(val) for val in predictions if pd.notna(val)]
|
| 41 |
+
future_predictions = [float(val) for val in future_predictions if pd.notna(val)]
|
| 42 |
+
|
| 43 |
+
fig = make_subplots(rows=1, cols=1)
|
| 44 |
+
fig.add_trace(go.Scatter(x=self.data.index, y=self.data['Close'], mode='lines', name='Actual Stock Prices', marker_color='blue'))
|
| 45 |
+
fig.add_trace(go.Scatter(x=predicted_dates, y=predictions, mode='lines', name='LSTM Predicted Prices', marker_color='red', line=dict(dash='dash')))
|
| 46 |
+
fig.add_trace(go.Scatter(x=future_dates, y=future_predictions, mode='lines', name='Future Predictions', marker_color='green', line=dict(dash='dot')))
|
| 47 |
+
|
| 48 |
+
fig.update_layout(title='Comparison of Actual, Predicted, and Future Stock Prices', xaxis_title='Date', yaxis_title='Price', legend_title='Legend', height=500)
|
| 49 |
+
st.plotly_chart(fig, use_container_width=True)
|
STOCK-MARKET-APP/requirement.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
pandas
|
| 3 |
+
seaborn
|
| 4 |
+
matplotlib
|
| 5 |
+
keras
|
| 6 |
+
tensorflow
|
| 7 |
+
scikit-learn
|
| 8 |
+
yfinance
|
| 9 |
+
plotly
|
STOCK-MARKET-APP/stock_data_loader.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
|
| 4 |
+
import warnings
|
| 5 |
+
warnings.filterwarnings("ignore")
|
| 6 |
+
|
| 7 |
+
class StockDataLoader:
|
| 8 |
+
def __init__(self, ticker, start_date, end_date):
|
| 9 |
+
self.ticker = ticker
|
| 10 |
+
self.start_date = start_date
|
| 11 |
+
self.end_date = end_date
|
| 12 |
+
|
| 13 |
+
def get_stock_data(self):
|
| 14 |
+
stock = yf.Ticker(self.ticker)
|
| 15 |
+
stock_data = stock.history(start=self.start_date, end=self.end_date)
|
| 16 |
+
stock_data.reset_index(inplace=True)
|
| 17 |
+
stock_data['Date'] = pd.to_datetime(stock_data['Date'])
|
| 18 |
+
stock_data.set_index('Date', inplace=True)
|
| 19 |
+
return stock_data
|
STOCK-MARKET-APP/utils.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
from functools import wraps
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
import random
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
try:
|
| 10 |
+
import pandas_datareader.data as web
|
| 11 |
+
PANDAS_DATAREADER_AVAILABLE = True
|
| 12 |
+
except ImportError:
|
| 13 |
+
PANDAS_DATAREADER_AVAILABLE = False
|
| 14 |
+
st.warning("pandas_datareader not available. Install it with: pip install pandas-datareader")
|
| 15 |
+
|
| 16 |
+
class RateLimitManager:
|
| 17 |
+
"""Manages rate limiting for API calls"""
|
| 18 |
+
|
| 19 |
+
def __init__(self, min_delay=3.0):
|
| 20 |
+
self.min_delay = min_delay
|
| 21 |
+
self.last_call_time = 0
|
| 22 |
+
|
| 23 |
+
def wait_if_needed(self):
|
| 24 |
+
"""Ensure minimum delay between API calls"""
|
| 25 |
+
current_time = time.time()
|
| 26 |
+
time_since_last_call = current_time - self.last_call_time
|
| 27 |
+
|
| 28 |
+
if time_since_last_call < self.min_delay:
|
| 29 |
+
sleep_time = self.min_delay - time_since_last_call + random.uniform(0.5, 1.5)
|
| 30 |
+
time.sleep(sleep_time)
|
| 31 |
+
|
| 32 |
+
self.last_call_time = time.time()
|
| 33 |
+
|
| 34 |
+
# Global rate limit manager
|
| 35 |
+
rate_limiter = RateLimitManager()
|
| 36 |
+
|
| 37 |
+
def create_sample_data(ticker, period='1mo'):
|
| 38 |
+
"""Create sample data when API is unavailable"""
|
| 39 |
+
|
| 40 |
+
# Define sample data for common tickers
|
| 41 |
+
sample_data = {
|
| 42 |
+
'NVDA': {'base_price': 450, 'volatility': 0.03, 'trend': 0.001},
|
| 43 |
+
'AAPL': {'base_price': 190, 'volatility': 0.02, 'trend': 0.0005},
|
| 44 |
+
'GOOGL': {'base_price': 140, 'volatility': 0.025, 'trend': 0.0008},
|
| 45 |
+
'MSFT': {'base_price': 420, 'volatility': 0.02, 'trend': 0.0007},
|
| 46 |
+
'AMZN': {'base_price': 150, 'volatility': 0.025, 'trend': 0.0006}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Get parameters for ticker or use defaults
|
| 50 |
+
params = sample_data.get(ticker, {'base_price': 100, 'volatility': 0.02, 'trend': 0.0005})
|
| 51 |
+
|
| 52 |
+
# Generate date range based on period
|
| 53 |
+
if period == 'max' or period == '1y':
|
| 54 |
+
days = 252
|
| 55 |
+
elif period == '6mo':
|
| 56 |
+
days = 126
|
| 57 |
+
elif period == '1mo':
|
| 58 |
+
days = 30
|
| 59 |
+
else:
|
| 60 |
+
days = 30
|
| 61 |
+
|
| 62 |
+
# Create date range
|
| 63 |
+
end_date = datetime.now()
|
| 64 |
+
start_date = end_date - timedelta(days=days)
|
| 65 |
+
dates = pd.date_range(start=start_date, end=end_date, freq='D')
|
| 66 |
+
|
| 67 |
+
# Remove weekends
|
| 68 |
+
dates = dates[dates.weekday < 5]
|
| 69 |
+
|
| 70 |
+
# Generate price data
|
| 71 |
+
np.random.seed(42) # For consistent sample data
|
| 72 |
+
returns = np.random.normal(params['trend'], params['volatility'], len(dates))
|
| 73 |
+
|
| 74 |
+
prices = [params['base_price']]
|
| 75 |
+
for ret in returns[1:]:
|
| 76 |
+
prices.append(prices[-1] * (1 + ret))
|
| 77 |
+
|
| 78 |
+
# Create DataFrame
|
| 79 |
+
df = pd.DataFrame(index=dates[:len(prices)])
|
| 80 |
+
df['Close'] = prices
|
| 81 |
+
df['Open'] = df['Close'].shift(1).fillna(df['Close'])
|
| 82 |
+
df['High'] = df['Close'] * (1 + np.random.uniform(0, 0.02, len(df)))
|
| 83 |
+
df['Low'] = df['Close'] * (1 - np.random.uniform(0, 0.02, len(df)))
|
| 84 |
+
df['Volume'] = np.random.randint(1000000, 10000000, len(df))
|
| 85 |
+
|
| 86 |
+
return df
|
| 87 |
+
|
| 88 |
+
def retry_with_backoff(max_retries=5, base_delay=10):
|
| 89 |
+
"""Decorator for retrying functions with exponential backoff"""
|
| 90 |
+
def decorator(func):
|
| 91 |
+
@wraps(func)
|
| 92 |
+
def wrapper(*args, **kwargs):
|
| 93 |
+
for attempt in range(max_retries):
|
| 94 |
+
try:
|
| 95 |
+
rate_limiter.wait_if_needed()
|
| 96 |
+
return func(*args, **kwargs)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
error_msg = str(e).lower()
|
| 99 |
+
|
| 100 |
+
if any(keyword in error_msg for keyword in ['rate', 'limit', '429', 'too many requests']):
|
| 101 |
+
if attempt < max_retries - 1:
|
| 102 |
+
wait_time = base_delay * (2 ** attempt) + random.uniform(2, 5)
|
| 103 |
+
st.warning(f"π« Rate limit hit. Waiting {wait_time:.1f} seconds before retry {attempt + 2}/{max_retries}...")
|
| 104 |
+
time.sleep(wait_time)
|
| 105 |
+
continue
|
| 106 |
+
else:
|
| 107 |
+
st.error("β±οΈ Rate limit exceeded after all retries. Using sample data.")
|
| 108 |
+
return None
|
| 109 |
+
elif any(keyword in error_msg for keyword in ['expecting value', 'no timezone', 'delisted', 'json']):
|
| 110 |
+
if attempt < max_retries - 1:
|
| 111 |
+
wait_time = base_delay + random.uniform(2, 4)
|
| 112 |
+
st.warning(f"π Data parsing error. Retrying in {wait_time:.1f} seconds... (attempt {attempt + 2}/{max_retries})")
|
| 113 |
+
time.sleep(wait_time)
|
| 114 |
+
continue
|
| 115 |
+
else:
|
| 116 |
+
st.warning("β οΈ Unable to fetch real data. Using sample data for demonstration.")
|
| 117 |
+
return None
|
| 118 |
+
else:
|
| 119 |
+
if attempt < max_retries - 1:
|
| 120 |
+
wait_time = base_delay + random.uniform(1, 3)
|
| 121 |
+
st.warning(f"β Error: {str(e)[:100]}... Retrying in {wait_time:.1f} seconds...")
|
| 122 |
+
time.sleep(wait_time)
|
| 123 |
+
continue
|
| 124 |
+
else:
|
| 125 |
+
st.error(f"β Failed after {max_retries} attempts: {str(e)[:100]}...")
|
| 126 |
+
return None
|
| 127 |
+
return None
|
| 128 |
+
return wrapper
|
| 129 |
+
return decorator
|
| 130 |
+
|
| 131 |
+
def fetch_data_with_stooq(ticker_symbol, start_date=None, end_date=None, period='1mo'):
|
| 132 |
+
"""Fetch stock data using pandas_datareader with stooq as source"""
|
| 133 |
+
if not PANDAS_DATAREADER_AVAILABLE:
|
| 134 |
+
return None
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# Convert period to date range if start/end not provided
|
| 138 |
+
if start_date is None or end_date is None:
|
| 139 |
+
end_date = datetime.now()
|
| 140 |
+
if period == 'max' or period == '1y':
|
| 141 |
+
start_date = end_date - timedelta(days=365)
|
| 142 |
+
elif period == '6mo':
|
| 143 |
+
start_date = end_date - timedelta(days=180)
|
| 144 |
+
elif period == '1mo':
|
| 145 |
+
start_date = end_date - timedelta(days=30)
|
| 146 |
+
elif period == '5d':
|
| 147 |
+
start_date = end_date - timedelta(days=5)
|
| 148 |
+
else:
|
| 149 |
+
start_date = end_date - timedelta(days=30)
|
| 150 |
+
|
| 151 |
+
# Fetch data from stooq
|
| 152 |
+
df = web.DataReader(ticker_symbol, 'stooq', start_date, end_date)
|
| 153 |
+
|
| 154 |
+
if df.empty:
|
| 155 |
+
return None
|
| 156 |
+
|
| 157 |
+
# Stooq returns data in reverse chronological order, so sort it
|
| 158 |
+
df = df.sort_index()
|
| 159 |
+
|
| 160 |
+
# Ensure we have the required columns
|
| 161 |
+
required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
|
| 162 |
+
if all(col in df.columns for col in required_columns):
|
| 163 |
+
return df
|
| 164 |
+
else:
|
| 165 |
+
st.warning(f"Missing columns in stooq data: {[col for col in required_columns if col not in df.columns]}")
|
| 166 |
+
return None
|
| 167 |
+
|
| 168 |
+
except Exception as e:
|
| 169 |
+
st.error(f"Error fetching data from stooq: {str(e)}")
|
| 170 |
+
return None
|
| 171 |
+
|
| 172 |
+
def safe_yfinance_call(ticker_symbol, operation='history', **kwargs):
|
| 173 |
+
"""Safely call multiple data sources with fallback to sample data"""
|
| 174 |
+
|
| 175 |
+
# First try stooq (pandas_datareader) for historical data
|
| 176 |
+
if operation == 'history' and PANDAS_DATAREADER_AVAILABLE:
|
| 177 |
+
try:
|
| 178 |
+
st.sidebar.info(f"π Trying stooq API for {ticker_symbol}...")
|
| 179 |
+
stooq_data = fetch_data_with_stooq(
|
| 180 |
+
ticker_symbol,
|
| 181 |
+
start_date=kwargs.get('start'),
|
| 182 |
+
end_date=kwargs.get('end'),
|
| 183 |
+
period=kwargs.get('period', '1mo')
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
if stooq_data is not None and not stooq_data.empty:
|
| 187 |
+
st.sidebar.success(f"β
Real data from stooq for {ticker_symbol}")
|
| 188 |
+
return stooq_data
|
| 189 |
+
else:
|
| 190 |
+
st.sidebar.warning(f"β οΈ Stooq failed for {ticker_symbol}")
|
| 191 |
+
except Exception as e:
|
| 192 |
+
st.sidebar.warning(f"β οΈ Stooq error: {str(e)[:50]}...")
|
| 193 |
+
|
| 194 |
+
# If stooq fails or for info operation, try yfinance as backup
|
| 195 |
+
try:
|
| 196 |
+
st.sidebar.info(f"π Trying yfinance API for {ticker_symbol}...")
|
| 197 |
+
ticker = yf.Ticker(ticker_symbol)
|
| 198 |
+
|
| 199 |
+
if operation == 'history':
|
| 200 |
+
result = ticker.history(
|
| 201 |
+
timeout=10,
|
| 202 |
+
prepost=False,
|
| 203 |
+
auto_adjust=True,
|
| 204 |
+
back_adjust=False,
|
| 205 |
+
repair=True,
|
| 206 |
+
keepna=False,
|
| 207 |
+
actions=False,
|
| 208 |
+
**kwargs
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if result is not None and not result.empty and len(result) > 0:
|
| 212 |
+
st.sidebar.success(f"β
Real data from yfinance for {ticker_symbol}")
|
| 213 |
+
return result
|
| 214 |
+
else:
|
| 215 |
+
st.sidebar.warning(f"β οΈ yfinance returned empty data for {ticker_symbol}")
|
| 216 |
+
|
| 217 |
+
elif operation == 'info':
|
| 218 |
+
result = ticker.info
|
| 219 |
+
if result and isinstance(result, dict) and len(result) > 1:
|
| 220 |
+
st.sidebar.success(f"β
Info from yfinance for {ticker_symbol}")
|
| 221 |
+
return result
|
| 222 |
+
else:
|
| 223 |
+
st.sidebar.warning(f"β οΈ yfinance info empty for {ticker_symbol}")
|
| 224 |
+
|
| 225 |
+
else:
|
| 226 |
+
raise ValueError(f"Unsupported operation: {operation}")
|
| 227 |
+
|
| 228 |
+
except Exception as e:
|
| 229 |
+
st.sidebar.warning(f"β οΈ yfinance also failed: {str(e)[:50]}...")
|
| 230 |
+
|
| 231 |
+
# Finally fallback to sample data
|
| 232 |
+
if operation == 'history':
|
| 233 |
+
st.sidebar.warning(f"π Using sample data for {ticker_symbol}")
|
| 234 |
+
return create_sample_data(ticker_symbol, kwargs.get('period', '1mo'))
|
| 235 |
+
elif operation == 'info':
|
| 236 |
+
sample_prices = {
|
| 237 |
+
'NVDA': 450, 'AAPL': 190, 'GOOGL': 140, 'MSFT': 420, 'AMZN': 150
|
| 238 |
+
}
|
| 239 |
+
base_price = sample_prices.get(ticker_symbol, 100)
|
| 240 |
+
return {
|
| 241 |
+
'symbol': ticker_symbol,
|
| 242 |
+
'shortName': f'{ticker_symbol} Inc.',
|
| 243 |
+
'currentPrice': base_price + random.uniform(-2, 2),
|
| 244 |
+
'previousClose': base_price
|
| 245 |
+
}
|
| 246 |
+
else:
|
| 247 |
+
raise Exception(f"All data sources failed for {ticker_symbol}")
|
| 248 |
+
|
| 249 |
+
def get_cached_data(cache_key, ttl_seconds=300):
|
| 250 |
+
"""Get cached data from session state if still valid"""
|
| 251 |
+
if cache_key in st.session_state:
|
| 252 |
+
cache_time_key = f"cache_time_{cache_key}"
|
| 253 |
+
if cache_time_key in st.session_state:
|
| 254 |
+
cache_time = st.session_state[cache_time_key]
|
| 255 |
+
if time.time() - cache_time < ttl_seconds:
|
| 256 |
+
return st.session_state[cache_key]
|
| 257 |
+
return None
|
| 258 |
+
|
| 259 |
+
def set_cached_data(cache_key, data):
|
| 260 |
+
"""Cache data in session state with timestamp"""
|
| 261 |
+
st.session_state[cache_key] = data
|
| 262 |
+
st.session_state[f"cache_time_{cache_key}"] = time.time()
|
| 263 |
+
|
| 264 |
+
def clear_cache(pattern=None):
|
| 265 |
+
"""Clear cached data matching pattern"""
|
| 266 |
+
if pattern is None:
|
| 267 |
+
# Clear all cache
|
| 268 |
+
keys_to_remove = [key for key in st.session_state.keys()
|
| 269 |
+
if key.startswith('cache_time_') or key.startswith('data_')]
|
| 270 |
+
else:
|
| 271 |
+
keys_to_remove = [key for key in st.session_state.keys() if pattern in key]
|
| 272 |
+
|
| 273 |
+
for key in keys_to_remove:
|
| 274 |
+
del st.session_state[key]
|
| 275 |
+
|
| 276 |
+
return len(keys_to_remove)
|
| 277 |
+
|
| 278 |
+
def format_error_message(error):
|
| 279 |
+
"""Format error messages for better user experience"""
|
| 280 |
+
error_str = str(error).lower()
|
| 281 |
+
|
| 282 |
+
if "rate" in error_str or "limit" in error_str:
|
| 283 |
+
return ("π« **Rate Limit Exceeded**\n\n"
|
| 284 |
+
"Yahoo Finance has temporarily limited your requests. This happens when too many requests are made in a short time.\n\n"
|
| 285 |
+
"**What you can do:**\n"
|
| 286 |
+
"- Wait 5-10 minutes before trying again\n"
|
| 287 |
+
"- Use the cached data if available\n"
|
| 288 |
+
"- Try a different stock ticker\n\n"
|
| 289 |
+
"The app will automatically retry with delays between requests.")
|
| 290 |
+
elif "network" in error_str or "connection" in error_str:
|
| 291 |
+
return ("π **Network Error**\n\n"
|
| 292 |
+
"There seems to be a connectivity issue.\n\n"
|
| 293 |
+
"**What you can do:**\n"
|
| 294 |
+
"- Check your internet connection\n"
|
| 295 |
+
"- Try refreshing the page\n"
|
| 296 |
+
"- Wait a moment and try again")
|
| 297 |
+
else:
|
| 298 |
+
return f"β **Error**: {str(error)}"
|
| 299 |
+
|
| 300 |
+
def display_cache_info():
|
| 301 |
+
"""Display cache information in sidebar"""
|
| 302 |
+
with st.sidebar:
|
| 303 |
+
with st.expander("Cache Information"):
|
| 304 |
+
cache_items = [key for key in st.session_state.keys()
|
| 305 |
+
if key.startswith('data_') or key.startswith('model_data_')]
|
| 306 |
+
|
| 307 |
+
if cache_items:
|
| 308 |
+
st.write(f"**Cached items:** {len(cache_items)}")
|
| 309 |
+
for item in cache_items[:5]: # Show first 5 items
|
| 310 |
+
cache_time_key = f"cache_time_{item}"
|
| 311 |
+
if cache_time_key in st.session_state:
|
| 312 |
+
cache_time = st.session_state[cache_time_key]
|
| 313 |
+
age_minutes = (time.time() - cache_time) / 60
|
| 314 |
+
st.write(f"β’ {item.replace('data_', '')}: {age_minutes:.1f}m ago")
|
| 315 |
+
|
| 316 |
+
if len(cache_items) > 5:
|
| 317 |
+
st.write(f"... and {len(cache_items) - 5} more")
|
| 318 |
+
|
| 319 |
+
if st.button("Clear All Cache"):
|
| 320 |
+
cleared = clear_cache()
|
| 321 |
+
st.success(f"Cleared {cleared} cached items")
|
| 322 |
+
st.experimental_rerun()
|
| 323 |
+
else:
|
| 324 |
+
st.write("No cached data")
|
STOCK-MARKET-APP/view_page.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from stock_data_loader import StockDataLoader
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import yfinance as yf
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from plots import Plots, StockChart
|
| 7 |
+
|
| 8 |
+
class StockDashboard:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.tickers = ['NVDA', 'AAPL', 'GOOGL', 'MSFT', 'AMZN']
|
| 11 |
+
self.period_map = {'all': 'max','1m': '1mo', '6m': '6mo', '1y': '1y'}
|
| 12 |
+
|
| 13 |
+
def render_sidebar(self):
|
| 14 |
+
st.sidebar.header("Choose your filter:")
|
| 15 |
+
self.ticker = st.sidebar.selectbox('Choose Ticker', options=self.tickers, help='Select a ticker')
|
| 16 |
+
self.selected_range = st.sidebar.selectbox('Select Period', options=list(self.period_map.keys()))
|
| 17 |
+
|
| 18 |
+
def load_data(self):
|
| 19 |
+
self.yf_data = yf.Ticker(self.ticker)
|
| 20 |
+
self.df_history = self.yf_data.history(period=self.period_map[self.selected_range])
|
| 21 |
+
self.current_price = self.yf_data.info.get('currentPrice', 'N/A')
|
| 22 |
+
self.previous_close = self.yf_data.info.get('previousClose', 'N/A')
|
| 23 |
+
|
| 24 |
+
def display_header(self):
|
| 25 |
+
company_name = self.yf_data.info['shortName']
|
| 26 |
+
symbol = self.yf_data.info['symbol']
|
| 27 |
+
st.subheader(f'{company_name} ({symbol}) π°')
|
| 28 |
+
st.divider()
|
| 29 |
+
if self.current_price != 'N/A' and self.previous_close != 'N/A':
|
| 30 |
+
price_change = self.current_price - self.previous_close
|
| 31 |
+
price_change_ratio = (abs(price_change) / self.previous_close * 100)
|
| 32 |
+
price_change_direction = "+" if price_change > 0 else "-"
|
| 33 |
+
st.metric(label='Current Price', value=f"{self.current_price:.2f}",
|
| 34 |
+
delta=f"{price_change:.2f} ({price_change_direction}{price_change_ratio:.2f}%)")
|
| 35 |
+
|
| 36 |
+
def plot_data(self):
|
| 37 |
+
chart = StockChart(self.df_history)
|
| 38 |
+
chart.add_price_chart()
|
| 39 |
+
chart.add_oversold_overbought_lines()
|
| 40 |
+
chart.add_volume_chart()
|
| 41 |
+
chart.render_chart()
|
| 42 |
+
|
| 43 |
+
def run(self):
|
| 44 |
+
st.write("--------------------------------------------")
|
| 45 |
+
self.render_sidebar()
|
| 46 |
+
self.load_data()
|
| 47 |
+
self.display_header()
|
| 48 |
+
self.plot_data()
|