Spaces:
Sleeping
Sleeping
Falcao Zane Vijay commited on
Commit ·
8e0b458
1
Parent(s): 0475bbf
deploy #1
Browse files- requirements.txt +3 -1
- src/.gitignore +3 -0
- src/README.md +1 -0
- src/config.py +6 -0
- src/indicators/bb.py +0 -0
- src/indicators/ema.py +9 -0
- src/indicators/enhanced_features.py +67 -0
- src/indicators/macd.py +17 -0
- src/indicators/rsi.py +19 -0
- src/indicators/sma.py +18 -0
- src/logs/log_2025-08-03_22-03-43.log +7 -0
- src/main.py +64 -0
- src/main.txt +65 -0
- src/main_app.py +663 -0
- src/models/H2H_model.ipynb +0 -0
- src/models/app.py +440 -0
- src/models/h2h_model.py +491 -0
- src/models/logistic_regression_model.pkl +3 -0
- src/models/ml_model.py +1 -0
- src/models/scaler.pkl +3 -0
- src/requirements.txt +12 -0
- src/script.ps1 +51 -0
- src/strategy/rule_based_strategy.py +51 -0
- src/streamlit_app.py +806 -38
- src/utils/backtester.py +186 -0
- src/utils/data_loader.py +41 -0
- src/utils/google_sheets.py +426 -0
- src/utils/logger.py +24 -0
requirements.txt
CHANGED
|
@@ -1,3 +1,5 @@
|
|
| 1 |
-
altair
|
| 2 |
pandas
|
|
|
|
|
|
|
|
|
|
| 3 |
streamlit
|
|
|
|
|
|
|
| 1 |
pandas
|
| 2 |
+
numpy
|
| 3 |
+
matplotlib
|
| 4 |
+
yfinance
|
| 5 |
streamlit
|
src/.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
.streamlit/
|
| 3 |
+
config.toml
|
src/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Placeholder for README.md
|
src/config.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config.py
|
| 2 |
+
STOCKS = ['RELIANCE.NS', 'TCS.NS', 'INFY.NS']
|
| 3 |
+
START_DATE = '2014-02-01'
|
| 4 |
+
RSI_PERIOD = 14
|
| 5 |
+
SMA_SHORT = 20
|
| 6 |
+
SMA_LONG = 50
|
src/indicators/bb.py
ADDED
|
File without changes
|
src/indicators/ema.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# indicators/sma.py
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
def ema(df, period=20, column="Close"):
|
| 6 |
+
"""
|
| 7 |
+
Calculates Exponential Moving Average (EMA)
|
| 8 |
+
"""
|
| 9 |
+
return df[column].ewm(span=period, adjust=False).mean()
|
src/indicators/enhanced_features.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def create_volatility_features(df):
|
| 2 |
+
if 'return_1d' not in df.columns:
|
| 3 |
+
df['return_1d'] = df['Close'].pct_change()
|
| 4 |
+
|
| 5 |
+
for period in [5, 10, 20, 30]:
|
| 6 |
+
df[f'volatility_{period}d'] = df['return_1d'].rolling(period).std()
|
| 7 |
+
|
| 8 |
+
df['vol_ratio_5_20'] = df['volatility_5d'] / df['volatility_20d']
|
| 9 |
+
df['vol_ratio_10_20'] = df['volatility_10d'] / df['volatility_20d']
|
| 10 |
+
df['vol_rank_20'] = df['volatility_5d'].rolling(20).rank(pct=True)
|
| 11 |
+
df['vol_rank_50'] = df['volatility_5d'].rolling(50).rank(pct=True)
|
| 12 |
+
|
| 13 |
+
return df
|
| 14 |
+
|
| 15 |
+
def create_enhanced_lag_features(df):
|
| 16 |
+
for lag in [1, 2, 3, 5, 10]:
|
| 17 |
+
df[f'return_lag_{lag}'] = df['return_1d'].shift(lag)
|
| 18 |
+
|
| 19 |
+
for lag in [1, 2, 3]:
|
| 20 |
+
if 'RSI14' in df.columns:
|
| 21 |
+
df[f'rsi_lag_{lag}'] = df['RSI14'].shift(lag)
|
| 22 |
+
if 'MACD' in df.columns:
|
| 23 |
+
df[f'macd_lag_{lag}'] = df['MACD'].shift(lag)
|
| 24 |
+
|
| 25 |
+
if 'volume_ratio_20' in df.columns:
|
| 26 |
+
for lag in [1, 2]:
|
| 27 |
+
df[f'volume_ratio_lag_{lag}'] = df['volume_ratio_20'].shift(lag)
|
| 28 |
+
|
| 29 |
+
return df
|
| 30 |
+
|
| 31 |
+
def create_volume_features(df):
|
| 32 |
+
df['volume_sma_10'] = df['Volume'].rolling(10).mean()
|
| 33 |
+
df['volume_sma_20'] = df['Volume'].rolling(20).mean()
|
| 34 |
+
df['volume_sma_50'] = df['Volume'].rolling(50).mean()
|
| 35 |
+
|
| 36 |
+
df['volume_ratio_10'] = df['Volume'] / df['volume_sma_10']
|
| 37 |
+
df['volume_ratio_20'] = df['Volume'] / df['volume_sma_20']
|
| 38 |
+
df['volume_ratio_50'] = df['Volume'] / df['volume_sma_50']
|
| 39 |
+
|
| 40 |
+
df['price_volume'] = df['Close'] * df['Volume']
|
| 41 |
+
df['pv_sma_5'] = df['price_volume'].rolling(5).mean()
|
| 42 |
+
df['volume_momentum_5'] = df['Volume'] / df['Volume'].shift(5)
|
| 43 |
+
|
| 44 |
+
return df
|
| 45 |
+
|
| 46 |
+
def create_momentum_features(df):
|
| 47 |
+
for period in [3, 5, 10, 20]:
|
| 48 |
+
df[f'momentum_{period}d'] = df['Close'] / df['Close'].shift(period) - 1
|
| 49 |
+
|
| 50 |
+
for period in [5, 10]:
|
| 51 |
+
df[f'roc_{period}d'] = (df['Close'] - df['Close'].shift(period)) / df['Close'].shift(period)
|
| 52 |
+
|
| 53 |
+
return df
|
| 54 |
+
|
| 55 |
+
def create_position_features(df):
|
| 56 |
+
for period in [10, 20, 50]:
|
| 57 |
+
df[f'high_{period}d'] = df['High'].rolling(period).max()
|
| 58 |
+
df[f'low_{period}d'] = df['Low'].rolling(period).min()
|
| 59 |
+
df[f'price_position_{period}'] = (df['Close'] - df[f'low_{period}d']) / (df[f'high_{period}d'] - df[f'low_{period}d'])
|
| 60 |
+
|
| 61 |
+
if 'SMA20' in df.columns:
|
| 62 |
+
bb_std = df['Close'].rolling(20).std()
|
| 63 |
+
df['bb_upper'] = df['SMA20'] + (bb_std * 2)
|
| 64 |
+
df['bb_lower'] = df['SMA20'] - (bb_std * 2)
|
| 65 |
+
df['bb_position'] = (df['Close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
| 66 |
+
|
| 67 |
+
return df
|
src/indicators/macd.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# indicators/macd.py
|
| 3 |
+
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
def macd(df, fast_period=12, slow_period=26, signal_period=9, column="Close"):
|
| 7 |
+
"""
|
| 8 |
+
Calculates MACD Line, Signal Line, and Histogram
|
| 9 |
+
"""
|
| 10 |
+
ema_fast = df[column].ewm(span=fast_period, adjust=False).mean()
|
| 11 |
+
ema_slow = df[column].ewm(span=slow_period, adjust=False).mean()
|
| 12 |
+
|
| 13 |
+
macd_line = ema_fast - ema_slow
|
| 14 |
+
signal_line = macd_line.ewm(span=signal_period, adjust=False).mean()
|
| 15 |
+
histogram = macd_line - signal_line
|
| 16 |
+
|
| 17 |
+
return macd_line, signal_line, histogram
|
src/indicators/rsi.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# indicators/rsi.py
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
def rsi(df, period=14, column="Close"):
|
| 6 |
+
"""
|
| 7 |
+
Calculates Relative Strength Index (RSI)
|
| 8 |
+
"""
|
| 9 |
+
delta = df[column].diff()
|
| 10 |
+
gain = delta.clip(lower=0)
|
| 11 |
+
loss = -1 * delta.clip(upper=0)
|
| 12 |
+
|
| 13 |
+
avg_gain = gain.ewm(com=period-1, min_periods=period).mean()
|
| 14 |
+
avg_loss = loss.ewm(com=period-1, min_periods=period).mean()
|
| 15 |
+
|
| 16 |
+
rs = avg_gain / avg_loss
|
| 17 |
+
rsi = 100 - (100 / (1 + rs))
|
| 18 |
+
|
| 19 |
+
return rsi
|
src/indicators/sma.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# indicators/sma.py
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
def sma(df, period=20, column="Close"):
|
| 6 |
+
"""
|
| 7 |
+
Calculates Simple Moving Average (SMA)
|
| 8 |
+
"""
|
| 9 |
+
return df[column].rolling(window=period).mean()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def ema(df, period=20, column="Close"):
|
| 14 |
+
"""
|
| 15 |
+
Calculates Exponential Moving Average (EMA)
|
| 16 |
+
"""
|
| 17 |
+
return df[column].ewm(span=period, adjust=False).mean()
|
| 18 |
+
|
src/logs/log_2025-08-03_22-03-43.log
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2025-08-03 22:03:43,463 [INFO] - Logger initialized.
|
| 2 |
+
2025-08-03 22:03:43,463 [INFO] - Fetching data for RELIANCE.NS from 2024-02-01 to 2025-08-03 22:03:39.386764
|
| 3 |
+
2025-08-03 22:03:49,344 [INFO] - Downloaded 372 rows for RELIANCE.NS
|
| 4 |
+
2025-08-03 22:04:04,219 [INFO] - Fetching data for TCS.NS from 2024-02-01 to 2025-08-03 22:03:39.386764
|
| 5 |
+
2025-08-03 22:04:04,496 [INFO] - Downloaded 372 rows for TCS.NS
|
| 6 |
+
2025-08-03 22:04:08,637 [INFO] - Fetching data for INFY.NS from 2024-02-01 to 2025-08-03 22:03:39.386764
|
| 7 |
+
2025-08-03 22:04:08,888 [INFO] - Downloaded 372 rows for INFY.NS
|
src/main.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main.py
|
| 2 |
+
|
| 3 |
+
from utils.logger import setup_logger
|
| 4 |
+
from utils.data_loader import fetch_stock_data
|
| 5 |
+
from indicators.rsi import rsi
|
| 6 |
+
from indicators.sma import sma
|
| 7 |
+
from indicators.ema import ema
|
| 8 |
+
from indicators.macd import macd
|
| 9 |
+
from strategy.rule_based_strategy import generate_signals
|
| 10 |
+
from utils.backtester import backtest_signals
|
| 11 |
+
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import matplotlib.pyplot as plt
|
| 14 |
+
|
| 15 |
+
# 1. Setup logging
|
| 16 |
+
setup_logger()
|
| 17 |
+
|
| 18 |
+
# 2. Define configuration
|
| 19 |
+
stocks = ['RELIANCE.NS', 'TCS.NS', 'INFY.NS']
|
| 20 |
+
start_date = '2024-02-01'
|
| 21 |
+
rsi_period = 14
|
| 22 |
+
sma_short = 20
|
| 23 |
+
sma_long = 50
|
| 24 |
+
ema_short = 20
|
| 25 |
+
ema_long = 50
|
| 26 |
+
|
| 27 |
+
for symbol in stocks:
|
| 28 |
+
print(f"\n--- Running for: {symbol} ---")
|
| 29 |
+
|
| 30 |
+
# 3. Fetch stock data
|
| 31 |
+
df = fetch_stock_data(symbol, start_date=start_date)
|
| 32 |
+
|
| 33 |
+
if df.empty:
|
| 34 |
+
print(f"No data for {symbol}, skipping...")
|
| 35 |
+
continue
|
| 36 |
+
|
| 37 |
+
# 4. Add indicators
|
| 38 |
+
df['RSI'] = rsi(df, period=rsi_period)
|
| 39 |
+
df['SMA20'] = sma(df, period=sma_short)
|
| 40 |
+
df['SMA50'] = sma(df, period=sma_long)
|
| 41 |
+
df['EMA20'] = ema(df, period=ema_short)
|
| 42 |
+
df['EMA50'] = ema(df, period=ema_long)
|
| 43 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = macd(df)
|
| 44 |
+
|
| 45 |
+
# 5. Generate buy/sell signals
|
| 46 |
+
df = generate_signals(df, rsi_col='RSI', sma_short_col='SMA20', sma_long_col='SMA50')
|
| 47 |
+
|
| 48 |
+
# 6. Backtest strategy
|
| 49 |
+
results = backtest_signals(df, signal_col='Signal')
|
| 50 |
+
|
| 51 |
+
# 7. Plot equity curve
|
| 52 |
+
plt.figure(figsize=(10, 5))
|
| 53 |
+
plt.plot(results['Total'], label='Equity Curve')
|
| 54 |
+
plt.title(f"{symbol} - Backtest Equity Curve")
|
| 55 |
+
plt.xlabel("Date")
|
| 56 |
+
plt.ylabel("Portfolio Value (₹)")
|
| 57 |
+
plt.legend()
|
| 58 |
+
plt.grid(True)
|
| 59 |
+
plt.tight_layout()
|
| 60 |
+
plt.show()
|
| 61 |
+
|
| 62 |
+
# 8. Print final portfolio value
|
| 63 |
+
final_value = results['Total'].iloc[-1]
|
| 64 |
+
print(f"Final portfolio value for {symbol}: ₹{final_value:,.2f}")
|
src/main.txt
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main.py
|
| 2 |
+
|
| 3 |
+
from utils.logger import setup_logger
|
| 4 |
+
from utils.data_loader import fetch_stock_data
|
| 5 |
+
from indicators.rsi import rsi
|
| 6 |
+
from indicators.sma import sma
|
| 7 |
+
from indicators.macd import macd
|
| 8 |
+
from strategy.rule_based_strategy import generate_signals
|
| 9 |
+
from utils.backtester import backtest_signals
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
|
| 14 |
+
# ---------------------- Configuration ----------------------
|
| 15 |
+
STOCK = 'RELIANCE.NS'
|
| 16 |
+
START_DATE = '2024-02-01'
|
| 17 |
+
|
| 18 |
+
# Indicator Parameters
|
| 19 |
+
RSI_PERIOD = 14
|
| 20 |
+
SMA_SHORT = 20
|
| 21 |
+
SMA_LONG = 50
|
| 22 |
+
|
| 23 |
+
# Backtest Settings
|
| 24 |
+
INITIAL_CASH = 100000
|
| 25 |
+
# -----------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
# Initialize Logger
|
| 30 |
+
setup_logger()
|
| 31 |
+
|
| 32 |
+
print(f"\nFetching data for {STOCK}...")
|
| 33 |
+
df = fetch_stock_data(STOCK, start_date=START_DATE)
|
| 34 |
+
|
| 35 |
+
if df.empty:
|
| 36 |
+
print("No data available. Exiting.")
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
print("Calculating indicators...")
|
| 40 |
+
df['RSI'] = rsi(df, period=RSI_PERIOD)
|
| 41 |
+
df['SMA20'] = sma(df, period=SMA_SHORT)
|
| 42 |
+
df['SMA50'] = sma(df, period=SMA_LONG)
|
| 43 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = macd(df)
|
| 44 |
+
|
| 45 |
+
print("Generating signals...")
|
| 46 |
+
df = generate_signals(df, rsi_col='RSI', sma_short_col='SMA20', sma_long_col='SMA50')
|
| 47 |
+
|
| 48 |
+
print("Backtesting strategy...")
|
| 49 |
+
results = backtest_signals(df, signal_col='Signal', price_col='Close', initial_cash=INITIAL_CASH)
|
| 50 |
+
|
| 51 |
+
final_equity = results['Total'].iloc[-1]
|
| 52 |
+
print(f"\n✅ Final Portfolio Value: ₹{final_equity:,.2f}")
|
| 53 |
+
|
| 54 |
+
# Plotting equity curve
|
| 55 |
+
plt.figure(figsize=(12, 6))
|
| 56 |
+
results['Total'].plot(label='Equity Curve', color='green')
|
| 57 |
+
results['Close'].plot(label='Close Price', secondary_y=True, alpha=0.3)
|
| 58 |
+
plt.title(f"Backtest Results for {STOCK}")
|
| 59 |
+
plt.legend()
|
| 60 |
+
plt.tight_layout()
|
| 61 |
+
plt.show()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
main()
|
src/main_app.py
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main_app.py
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import numpy as np
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
from plotly.subplots import make_subplots
|
| 9 |
+
|
| 10 |
+
from utils.data_loader import fetch_stock_data
|
| 11 |
+
from indicators.rsi import rsi
|
| 12 |
+
from indicators.sma import sma
|
| 13 |
+
from indicators.ema import ema
|
| 14 |
+
from indicators.macd import macd
|
| 15 |
+
from strategy.rule_based_strategy import generate_signals_sma, generate_signals_ema
|
| 16 |
+
from utils.backtester import backtest_signals
|
| 17 |
+
|
| 18 |
+
# Function to display strategy results with Plotly
|
| 19 |
+
def display_strategy_results(df, results, metrics, strategy_name, period_short, period_long, initial_cash, selected_stock):
|
| 20 |
+
"""
|
| 21 |
+
Display comprehensive strategy results in Streamlit interface using Plotly
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
# Performance metrics in columns
|
| 25 |
+
st.subheader("📊 Performance Overview")
|
| 26 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 27 |
+
|
| 28 |
+
with col1:
|
| 29 |
+
st.metric("💰 Final Value", metrics['Final Portfolio Value'])
|
| 30 |
+
st.metric("📈 Total Return", metrics['Total Return'])
|
| 31 |
+
|
| 32 |
+
with col2:
|
| 33 |
+
st.metric("🎯 Buy & Hold Return", metrics['Buy & Hold Return'])
|
| 34 |
+
st.metric("📊 Total Trades", metrics['Total Trades'])
|
| 35 |
+
|
| 36 |
+
with col3:
|
| 37 |
+
st.metric("🏆 Win Rate", metrics['Win Rate'])
|
| 38 |
+
st.metric("⚡ Sharpe Ratio", metrics['Sharpe Ratio'])
|
| 39 |
+
|
| 40 |
+
with col4:
|
| 41 |
+
st.metric("📉 Max Drawdown", metrics['Maximum Drawdown'])
|
| 42 |
+
st.metric("🔥 Volatility", metrics['Volatility (Annual)'])
|
| 43 |
+
|
| 44 |
+
# Get signals for plotting
|
| 45 |
+
signal_col = f'{strategy_name}_Signal'
|
| 46 |
+
buy_signals = df[df[signal_col] == 1]
|
| 47 |
+
sell_signals = df[df[signal_col] == -1]
|
| 48 |
+
|
| 49 |
+
# 1. Main price chart with signals and moving averages
|
| 50 |
+
st.subheader(f"📉 {selected_stock} Price Chart with {strategy_name} Strategy")
|
| 51 |
+
|
| 52 |
+
fig_price = go.Figure()
|
| 53 |
+
|
| 54 |
+
# Add price line
|
| 55 |
+
fig_price.add_trace(go.Scatter(
|
| 56 |
+
x=df.index,
|
| 57 |
+
y=df['Close'],
|
| 58 |
+
mode='lines',
|
| 59 |
+
name='Close Price',
|
| 60 |
+
line=dict(color='purple', width=2),
|
| 61 |
+
hovertemplate='<b>Price</b>: ₹%{y:.2f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 62 |
+
))
|
| 63 |
+
|
| 64 |
+
# Add moving averages
|
| 65 |
+
fig_price.add_trace(go.Scatter(
|
| 66 |
+
x=df.index,
|
| 67 |
+
y=df[f'{strategy_name}{period_short}'],
|
| 68 |
+
mode='lines',
|
| 69 |
+
name=f'{strategy_name}{period_short}',
|
| 70 |
+
line=dict(color='blue', width=2),
|
| 71 |
+
hovertemplate=f'<b>{strategy_name}{period_short}</b>: ₹%{{y:.2f}}<br><b>Date</b>: %{{x}}<extra></extra>'
|
| 72 |
+
))
|
| 73 |
+
|
| 74 |
+
fig_price.add_trace(go.Scatter(
|
| 75 |
+
x=df.index,
|
| 76 |
+
y=df[f'{strategy_name}{period_long}'],
|
| 77 |
+
mode='lines',
|
| 78 |
+
name=f'{strategy_name}{period_long}',
|
| 79 |
+
line=dict(color='red', width=2),
|
| 80 |
+
hovertemplate=f'<b>{strategy_name}{period_long}</b>: ₹%{{y:.2f}}<br><b>Date</b>: %{{x}}<extra></extra>'
|
| 81 |
+
))
|
| 82 |
+
|
| 83 |
+
# Add buy signals
|
| 84 |
+
if not buy_signals.empty:
|
| 85 |
+
fig_price.add_trace(go.Scatter(
|
| 86 |
+
x=buy_signals.index,
|
| 87 |
+
y=buy_signals['Close'],
|
| 88 |
+
mode='markers',
|
| 89 |
+
name='Buy Signal',
|
| 90 |
+
marker=dict(
|
| 91 |
+
symbol='triangle-up',
|
| 92 |
+
size=12,
|
| 93 |
+
color='green',
|
| 94 |
+
line=dict(color='darkgreen', width=1)
|
| 95 |
+
),
|
| 96 |
+
hovertemplate='<b>BUY</b><br><b>Price</b>: ₹%{y:.2f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 97 |
+
))
|
| 98 |
+
|
| 99 |
+
# Add sell signals
|
| 100 |
+
if not sell_signals.empty:
|
| 101 |
+
fig_price.add_trace(go.Scatter(
|
| 102 |
+
x=sell_signals.index,
|
| 103 |
+
y=sell_signals['Close'],
|
| 104 |
+
mode='markers',
|
| 105 |
+
name='Sell Signal',
|
| 106 |
+
marker=dict(
|
| 107 |
+
symbol='triangle-down',
|
| 108 |
+
size=12,
|
| 109 |
+
color='red',
|
| 110 |
+
line=dict(color='darkred', width=1)
|
| 111 |
+
),
|
| 112 |
+
hovertemplate='<b>SELL</b><br><b>Price</b>: ₹%{y:.2f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 113 |
+
))
|
| 114 |
+
|
| 115 |
+
# Add trend zones
|
| 116 |
+
fig_price.add_trace(go.Scatter(
|
| 117 |
+
x=df.index,
|
| 118 |
+
y=df[f'{strategy_name}{period_short}'],
|
| 119 |
+
fill=None,
|
| 120 |
+
mode='lines',
|
| 121 |
+
line_color='rgba(0,0,0,0)',
|
| 122 |
+
showlegend=False
|
| 123 |
+
))
|
| 124 |
+
|
| 125 |
+
fig_price.add_trace(go.Scatter(
|
| 126 |
+
x=df.index,
|
| 127 |
+
y=df[f'{strategy_name}{period_long}'],
|
| 128 |
+
fill='tonexty',
|
| 129 |
+
mode='lines',
|
| 130 |
+
line_color='rgba(0,0,0,0)',
|
| 131 |
+
fillcolor='rgba(0,255,0,0.1)',
|
| 132 |
+
name='Bullish Zone',
|
| 133 |
+
showlegend=True
|
| 134 |
+
))
|
| 135 |
+
|
| 136 |
+
fig_price.update_layout(
|
| 137 |
+
title=f"{selected_stock} - {strategy_name} Strategy Signals",
|
| 138 |
+
xaxis_title="Date",
|
| 139 |
+
yaxis_title="Price (₹)",
|
| 140 |
+
height=600,
|
| 141 |
+
hovermode='x unified',
|
| 142 |
+
template='plotly_white'
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
st.plotly_chart(fig_price, use_container_width=True)
|
| 146 |
+
|
| 147 |
+
# 2. Portfolio performance comparison
|
| 148 |
+
st.subheader("📈 Portfolio Performance vs Buy & Hold")
|
| 149 |
+
|
| 150 |
+
# Calculate buy & hold
|
| 151 |
+
buy_hold_value = initial_cash * (df['Close'] / df['Close'].iloc[0])
|
| 152 |
+
|
| 153 |
+
fig_perf = go.Figure()
|
| 154 |
+
|
| 155 |
+
fig_perf.add_trace(go.Scatter(
|
| 156 |
+
x=results.index,
|
| 157 |
+
y=results['Total'],
|
| 158 |
+
mode='lines',
|
| 159 |
+
name='Strategy Portfolio',
|
| 160 |
+
line=dict(color='green', width=3),
|
| 161 |
+
hovertemplate='<b>Strategy</b>: ₹%{y:,.0f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 162 |
+
))
|
| 163 |
+
|
| 164 |
+
fig_perf.add_trace(go.Scatter(
|
| 165 |
+
x=df.index,
|
| 166 |
+
y=buy_hold_value,
|
| 167 |
+
mode='lines',
|
| 168 |
+
name='Buy & Hold',
|
| 169 |
+
line=dict(color='blue', width=2, dash='dash'),
|
| 170 |
+
hovertemplate='<b>Buy & Hold</b>: ₹%{y:,.0f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 171 |
+
))
|
| 172 |
+
|
| 173 |
+
fig_perf.update_layout(
|
| 174 |
+
title="Strategy vs Buy & Hold Performance",
|
| 175 |
+
xaxis_title="Date",
|
| 176 |
+
yaxis_title="Portfolio Value (₹)",
|
| 177 |
+
height=500,
|
| 178 |
+
hovermode='x unified',
|
| 179 |
+
template='plotly_white'
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
st.plotly_chart(fig_perf, use_container_width=True)
|
| 183 |
+
|
| 184 |
+
# 3. Technical indicators in columns
|
| 185 |
+
col1, col2 = st.columns(2)
|
| 186 |
+
|
| 187 |
+
with col1:
|
| 188 |
+
st.subheader("💹 RSI Indicator")
|
| 189 |
+
|
| 190 |
+
fig_rsi = go.Figure()
|
| 191 |
+
|
| 192 |
+
# RSI line
|
| 193 |
+
fig_rsi.add_trace(go.Scatter(
|
| 194 |
+
x=df.index,
|
| 195 |
+
y=df['RSI'],
|
| 196 |
+
mode='lines',
|
| 197 |
+
name='RSI',
|
| 198 |
+
line=dict(color='purple', width=2),
|
| 199 |
+
hovertemplate='<b>RSI</b>: %{y:.1f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 200 |
+
))
|
| 201 |
+
|
| 202 |
+
# Overbought/Oversold lines
|
| 203 |
+
fig_rsi.add_hline(y=70, line_dash="dash", line_color="red",
|
| 204 |
+
annotation_text="Overbought (70)")
|
| 205 |
+
fig_rsi.add_hline(y=30, line_dash="dash", line_color="green",
|
| 206 |
+
annotation_text="Oversold (30)")
|
| 207 |
+
fig_rsi.add_hline(y=50, line_dash="solid", line_color="gray",
|
| 208 |
+
annotation_text="Midline (50)", opacity=0.5)
|
| 209 |
+
|
| 210 |
+
# Fill zones
|
| 211 |
+
fig_rsi.add_hrect(y0=0, y1=30, fillcolor="red", opacity=0.1,
|
| 212 |
+
line_width=0, annotation_text="Oversold Zone")
|
| 213 |
+
fig_rsi.add_hrect(y0=70, y1=100, fillcolor="green", opacity=0.1,
|
| 214 |
+
line_width=0, annotation_text="Overbought Zone")
|
| 215 |
+
|
| 216 |
+
# Add buy/sell signals on RSI
|
| 217 |
+
if not buy_signals.empty:
|
| 218 |
+
fig_rsi.add_trace(go.Scatter(
|
| 219 |
+
x=buy_signals.index,
|
| 220 |
+
y=buy_signals['RSI'],
|
| 221 |
+
mode='markers',
|
| 222 |
+
name='Buy Signal',
|
| 223 |
+
marker=dict(symbol='triangle-up', size=10, color='green'),
|
| 224 |
+
showlegend=False
|
| 225 |
+
))
|
| 226 |
+
|
| 227 |
+
if not sell_signals.empty:
|
| 228 |
+
fig_rsi.add_trace(go.Scatter(
|
| 229 |
+
x=sell_signals.index,
|
| 230 |
+
y=sell_signals['RSI'],
|
| 231 |
+
mode='markers',
|
| 232 |
+
name='Sell Signal',
|
| 233 |
+
marker=dict(symbol='triangle-down', size=10, color='red'),
|
| 234 |
+
showlegend=False
|
| 235 |
+
))
|
| 236 |
+
|
| 237 |
+
fig_rsi.update_layout(
|
| 238 |
+
title="RSI with Trading Signals",
|
| 239 |
+
xaxis_title="Date",
|
| 240 |
+
yaxis_title="RSI Value",
|
| 241 |
+
height=400,
|
| 242 |
+
yaxis=dict(range=[0, 100]),
|
| 243 |
+
template='plotly_white'
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
st.plotly_chart(fig_rsi, use_container_width=True)
|
| 247 |
+
|
| 248 |
+
with col2:
|
| 249 |
+
st.subheader("📊 MACD Indicator")
|
| 250 |
+
|
| 251 |
+
fig_macd = make_subplots(rows=2, cols=1,
|
| 252 |
+
shared_xaxes=True,
|
| 253 |
+
vertical_spacing=0.05,
|
| 254 |
+
row_width=[0.7, 0.3])
|
| 255 |
+
|
| 256 |
+
# MACD line
|
| 257 |
+
fig_macd.add_trace(go.Scatter(
|
| 258 |
+
x=df.index,
|
| 259 |
+
y=df['MACD'],
|
| 260 |
+
mode='lines',
|
| 261 |
+
name='MACD',
|
| 262 |
+
line=dict(color='blue', width=2),
|
| 263 |
+
hovertemplate='<b>MACD</b>: %{y:.3f}<extra></extra>'
|
| 264 |
+
), row=1, col=1)
|
| 265 |
+
|
| 266 |
+
# Signal line
|
| 267 |
+
fig_macd.add_trace(go.Scatter(
|
| 268 |
+
x=df.index,
|
| 269 |
+
y=df['MACD_signal'],
|
| 270 |
+
mode='lines',
|
| 271 |
+
name='Signal Line',
|
| 272 |
+
line=dict(color='orange', width=2),
|
| 273 |
+
hovertemplate='<b>Signal</b>: %{y:.3f}<extra></extra>'
|
| 274 |
+
), row=1, col=1)
|
| 275 |
+
|
| 276 |
+
# Zero line
|
| 277 |
+
fig_macd.add_hline(y=0, line_dash="solid", line_color="pink",
|
| 278 |
+
opacity=0.5, row=1, col=1)
|
| 279 |
+
|
| 280 |
+
# MACD histogram
|
| 281 |
+
colors = ['green' if val >= 0 else 'red' for val in df['MACD_hist']]
|
| 282 |
+
fig_macd.add_trace(go.Bar(
|
| 283 |
+
x=df.index,
|
| 284 |
+
y=df['MACD_hist'],
|
| 285 |
+
name='MACD Histogram',
|
| 286 |
+
marker_color=colors,
|
| 287 |
+
opacity=0.6,
|
| 288 |
+
hovertemplate='<b>Histogram</b>: %{y:.3f}<extra></extra>'
|
| 289 |
+
), row=2, col=1)
|
| 290 |
+
|
| 291 |
+
fig_macd.update_layout(
|
| 292 |
+
title="MACD Indicator",
|
| 293 |
+
height=500,
|
| 294 |
+
template='plotly_white',
|
| 295 |
+
showlegend=True
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
fig_macd.update_xaxes(title_text="Date", row=2, col=1)
|
| 299 |
+
fig_macd.update_yaxes(title_text="MACD Value", row=1, col=1)
|
| 300 |
+
fig_macd.update_yaxes(title_text="Histogram", row=2, col=1)
|
| 301 |
+
|
| 302 |
+
st.plotly_chart(fig_macd, use_container_width=True)
|
| 303 |
+
|
| 304 |
+
# 4. Bollinger Bands
|
| 305 |
+
st.subheader("📈 Bollinger Bands")
|
| 306 |
+
|
| 307 |
+
fig_bb = go.Figure()
|
| 308 |
+
|
| 309 |
+
# Price line
|
| 310 |
+
fig_bb.add_trace(go.Scatter(
|
| 311 |
+
x=df.index,
|
| 312 |
+
y=df['Close'],
|
| 313 |
+
mode='lines',
|
| 314 |
+
name='Close Price',
|
| 315 |
+
line=dict(color='purple', width=2),
|
| 316 |
+
hovertemplate='<b>Price</b>: ₹%{y:.2f}<extra></extra>'
|
| 317 |
+
))
|
| 318 |
+
|
| 319 |
+
# 20-day SMA
|
| 320 |
+
fig_bb.add_trace(go.Scatter(
|
| 321 |
+
x=df.index,
|
| 322 |
+
y=df['SMA20'],
|
| 323 |
+
mode='lines',
|
| 324 |
+
name='20-day SMA',
|
| 325 |
+
line=dict(color='blue', width=1.5),
|
| 326 |
+
hovertemplate='<b>SMA20</b>: ₹%{y:.2f}<extra></extra>'
|
| 327 |
+
))
|
| 328 |
+
|
| 329 |
+
# Upper Band
|
| 330 |
+
fig_bb.add_trace(go.Scatter(
|
| 331 |
+
x=df.index,
|
| 332 |
+
y=df['Upper_Band'],
|
| 333 |
+
mode='lines',
|
| 334 |
+
name='Upper Band',
|
| 335 |
+
line=dict(color='red', dash='dash', width=1.5),
|
| 336 |
+
hovertemplate='<b>Upper Band</b>: ₹%{y:.2f}<extra></extra>'
|
| 337 |
+
))
|
| 338 |
+
|
| 339 |
+
# Lower Band with fill
|
| 340 |
+
fig_bb.add_trace(go.Scatter(
|
| 341 |
+
x=df.index,
|
| 342 |
+
y=df['Lower_Band'],
|
| 343 |
+
mode='lines',
|
| 344 |
+
name='Lower Band',
|
| 345 |
+
line=dict(color='green', dash='dash', width=1.5),
|
| 346 |
+
fill='tonexty',
|
| 347 |
+
fillcolor='rgba(128,128,128,0.2)',
|
| 348 |
+
hovertemplate='<b>Lower Band</b>: ₹%{y:.2f}<extra></extra>'
|
| 349 |
+
))
|
| 350 |
+
|
| 351 |
+
fig_bb.update_layout(
|
| 352 |
+
title="Bollinger Bands",
|
| 353 |
+
xaxis_title="Date",
|
| 354 |
+
yaxis_title="Price (₹)",
|
| 355 |
+
height=500,
|
| 356 |
+
template='plotly_white'
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
st.plotly_chart(fig_bb, use_container_width=True)
|
| 360 |
+
|
| 361 |
+
# 5. Drawdown Analysis
|
| 362 |
+
st.subheader("📉 Drawdown Analysis")
|
| 363 |
+
|
| 364 |
+
# Calculate drawdown
|
| 365 |
+
returns = results['Total'].pct_change().fillna(0)
|
| 366 |
+
cumulative = (1 + returns).cumprod()
|
| 367 |
+
running_max = cumulative.expanding().max()
|
| 368 |
+
drawdown = (cumulative - running_max) / running_max
|
| 369 |
+
|
| 370 |
+
fig_dd = go.Figure()
|
| 371 |
+
|
| 372 |
+
fig_dd.add_trace(go.Scatter(
|
| 373 |
+
x=df.index,
|
| 374 |
+
y=drawdown * 100,
|
| 375 |
+
mode='lines',
|
| 376 |
+
name='Drawdown',
|
| 377 |
+
fill='tozeroy',
|
| 378 |
+
fillcolor='rgba(255,0,0,0.3)',
|
| 379 |
+
line=dict(color='red', width=1),
|
| 380 |
+
hovertemplate='<b>Drawdown</b>: %{y:.1f}%<extra></extra>'
|
| 381 |
+
))
|
| 382 |
+
|
| 383 |
+
fig_dd.update_layout(
|
| 384 |
+
title="Portfolio Drawdown Over Time",
|
| 385 |
+
xaxis_title="Date",
|
| 386 |
+
yaxis_title="Drawdown (%)",
|
| 387 |
+
height=400,
|
| 388 |
+
template='plotly_white'
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
st.plotly_chart(fig_dd, use_container_width=True)
|
| 392 |
+
|
| 393 |
+
# 6. Trade analysis
|
| 394 |
+
if not metrics['Trades DataFrame'].empty:
|
| 395 |
+
st.subheader("📋 Trade Analysis")
|
| 396 |
+
|
| 397 |
+
trades_df = metrics['Trades DataFrame']
|
| 398 |
+
|
| 399 |
+
# Trade statistics
|
| 400 |
+
col1, col2, col3 = st.columns(3)
|
| 401 |
+
with col1:
|
| 402 |
+
avg_trade_duration = (pd.to_datetime(trades_df['exit_date']) -
|
| 403 |
+
pd.to_datetime(trades_df['entry_date'])).dt.days.mean()
|
| 404 |
+
st.metric("📅 Avg Trade Duration", f"{avg_trade_duration:.1f} days")
|
| 405 |
+
|
| 406 |
+
with col2:
|
| 407 |
+
best_trade = trades_df['return_pct'].max()
|
| 408 |
+
st.metric("🚀 Best Trade", f"{best_trade:.2%}")
|
| 409 |
+
|
| 410 |
+
with col3:
|
| 411 |
+
worst_trade = trades_df['return_pct'].min()
|
| 412 |
+
st.metric("💥 Worst Trade", f"{worst_trade:.2%}")
|
| 413 |
+
|
| 414 |
+
# Trade returns distribution
|
| 415 |
+
st.subheader("📊 Trade Returns Distribution")
|
| 416 |
+
|
| 417 |
+
returns_pct = trades_df['return_pct'] * 100
|
| 418 |
+
|
| 419 |
+
fig_hist = px.histogram(
|
| 420 |
+
x=returns_pct,
|
| 421 |
+
nbins=20,
|
| 422 |
+
title="Distribution of Trade Returns",
|
| 423 |
+
labels={'x': 'Return (%)', 'y': 'Number of Trades'},
|
| 424 |
+
color_discrete_sequence=['steelblue']
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
# Add vertical lines for mean and zero
|
| 428 |
+
fig_hist.add_vline(x=0, line_dash="dash", line_color="red",
|
| 429 |
+
annotation_text="Break Even")
|
| 430 |
+
fig_hist.add_vline(x=returns_pct.mean(), line_dash="solid", line_color="green",
|
| 431 |
+
annotation_text=f"Mean: {returns_pct.mean():.1f}%")
|
| 432 |
+
|
| 433 |
+
fig_hist.update_layout(
|
| 434 |
+
height=400,
|
| 435 |
+
template='plotly_white',
|
| 436 |
+
showlegend=False
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
st.plotly_chart(fig_hist, use_container_width=True)
|
| 440 |
+
|
| 441 |
+
# Trade timeline
|
| 442 |
+
st.subheader("📅 Trade Timeline")
|
| 443 |
+
|
| 444 |
+
fig_timeline = go.Figure()
|
| 445 |
+
|
| 446 |
+
for i, trade in trades_df.iterrows():
|
| 447 |
+
color = 'green' if trade['return_pct'] > 0 else 'red'
|
| 448 |
+
fig_timeline.add_trace(go.Scatter(
|
| 449 |
+
x=[trade['entry_date'], trade['exit_date']],
|
| 450 |
+
y=[trade['entry_price'], trade['exit_price']],
|
| 451 |
+
mode='lines+markers',
|
| 452 |
+
name=f"Trade {i+1}",
|
| 453 |
+
line=dict(color=color, width=3),
|
| 454 |
+
marker=dict(size=8),
|
| 455 |
+
hovertemplate=f'<b>Trade {i+1}</b><br>' +
|
| 456 |
+
f'Entry: ₹{trade["entry_price"]:.2f}<br>' +
|
| 457 |
+
f'Exit: ₹{trade["exit_price"]:.2f}<br>' +
|
| 458 |
+
f'Return: {trade["return_pct"]:.2%}<br>' +
|
| 459 |
+
f'Duration: {(pd.to_datetime(trade["exit_date"]) - pd.to_datetime(trade["entry_date"])).days} days<extra></extra>',
|
| 460 |
+
showlegend=False
|
| 461 |
+
))
|
| 462 |
+
|
| 463 |
+
fig_timeline.update_layout(
|
| 464 |
+
title="Individual Trade Performance Timeline",
|
| 465 |
+
xaxis_title="Date",
|
| 466 |
+
yaxis_title="Price (₹)",
|
| 467 |
+
height=500,
|
| 468 |
+
template='plotly_white'
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
st.plotly_chart(fig_timeline, use_container_width=True)
|
| 472 |
+
|
| 473 |
+
# Trade history table
|
| 474 |
+
st.subheader("📊 Detailed Trade History")
|
| 475 |
+
display_trades = trades_df.copy()
|
| 476 |
+
display_trades['Entry Date'] = pd.to_datetime(display_trades['entry_date']).dt.strftime('%Y-%m-%d')
|
| 477 |
+
display_trades['Exit Date'] = pd.to_datetime(display_trades['exit_date']).dt.strftime('%Y-%m-%d')
|
| 478 |
+
display_trades['Entry Price'] = display_trades['entry_price'].apply(lambda x: f"₹{x:.2f}")
|
| 479 |
+
display_trades['Exit Price'] = display_trades['exit_price'].apply(lambda x: f"₹{x:.2f}")
|
| 480 |
+
display_trades['P&L (₹)'] = display_trades['profit_loss'].apply(lambda x: f"₹{x:,.2f}")
|
| 481 |
+
display_trades['Return %'] = display_trades['return_pct'].apply(lambda x: f"{x:.2%}")
|
| 482 |
+
display_trades['Duration'] = (pd.to_datetime(trades_df['exit_date']) -
|
| 483 |
+
pd.to_datetime(trades_df['entry_date'])).dt.days
|
| 484 |
+
|
| 485 |
+
trade_display = display_trades[['Entry Date', 'Exit Date', 'Entry Price', 'Exit Price',
|
| 486 |
+
'P&L (₹)', 'Return %', 'Duration', 'exit_reason']].copy()
|
| 487 |
+
trade_display.columns = ['Entry Date', 'Exit Date', 'Entry Price', 'Exit Price',
|
| 488 |
+
'Profit/Loss', 'Return %', 'Days', 'Exit Reason']
|
| 489 |
+
|
| 490 |
+
st.dataframe(trade_display, use_container_width=True)
|
| 491 |
+
|
| 492 |
+
else:
|
| 493 |
+
st.info("📝 No trades were executed during this period with the current parameters.")
|
| 494 |
+
|
| 495 |
+
# 7. Signal summary table
|
| 496 |
+
st.subheader("📋 Trading Signals Summary")
|
| 497 |
+
signal_summary = df[df[signal_col] != 0].copy()
|
| 498 |
+
|
| 499 |
+
if not signal_summary.empty:
|
| 500 |
+
signal_summary['Signal Type'] = signal_summary[signal_col].map({1: '🟢 BUY', -1: '🔴 SELL'})
|
| 501 |
+
signal_summary['Price'] = signal_summary['Close'].apply(lambda x: f"₹{x:.2f}")
|
| 502 |
+
signal_summary['RSI'] = signal_summary['RSI'].apply(lambda x: f"{x:.1f}")
|
| 503 |
+
signal_summary[f'{strategy_name}{period_short}'] = signal_summary[f'{strategy_name}{period_short}'].apply(lambda x: f"₹{x:.2f}")
|
| 504 |
+
signal_summary[f'{strategy_name}{period_long}'] = signal_summary[f'{strategy_name}{period_long}'].apply(lambda x: f"₹{x:.2f}")
|
| 505 |
+
|
| 506 |
+
display_signals = signal_summary[['Signal Type', 'Price', 'RSI',
|
| 507 |
+
f'{strategy_name}{period_short}',
|
| 508 |
+
f'{strategy_name}{period_long}']].copy()
|
| 509 |
+
display_signals.index = display_signals.index.strftime('%Y-%m-%d')
|
| 510 |
+
|
| 511 |
+
st.dataframe(display_signals, use_container_width=True)
|
| 512 |
+
else:
|
| 513 |
+
st.info("📝 No trading signals were generated during this period with the current parameters.")
|
| 514 |
+
|
| 515 |
+
# ---------------------------------------
|
| 516 |
+
st.set_page_config(layout="wide", page_title="Algo Trading Dashboard", page_icon="📈")
|
| 517 |
+
st.title("📈 Algo-Trading Dashboard: Technical Analysis & Backtesting")
|
| 518 |
+
|
| 519 |
+
# Sidebar config
|
| 520 |
+
st.sidebar.header("📊 Configuration")
|
| 521 |
+
|
| 522 |
+
# Stock selection
|
| 523 |
+
stocks = ['ADANIENT.NS', 'ADANIPORTS.NS', 'APOLLOHOSP.NS', 'ASIANPAINT.NS', 'AXISBANK.NS',
|
| 524 |
+
'BAJAJ-AUTO.NS', 'BAJFINANCE.NS', 'BAJAJFINSV.NS', 'BEL.NS', 'BHARTIARTL.NS',
|
| 525 |
+
'CIPLA.NS', 'COALINDIA.NS', 'DRREDDY.NS', 'EICHERMOT.NS', 'GRASIM.NS',
|
| 526 |
+
'HCLTECH.NS', 'HDFCBANK.NS', 'HDFCLIFE.NS', 'HEROMOTOCO.NS', 'HINDALCO.NS',
|
| 527 |
+
'HINDUNILVR.NS', 'ICICIBANK.NS', 'INDUSINDBK.NS', 'INFY.NS', 'ITC.NS',
|
| 528 |
+
'JIOFIN.NS', 'JSWSTEEL.NS', 'KOTAKBANK.NS', 'LT.NS', 'M&M.NS', 'MARUTI.NS',
|
| 529 |
+
'NESTLEIND.NS', 'NTPC.NS', 'ONGC.NS', 'POWERGRID.NS', 'RELIANCE.NS',
|
| 530 |
+
'SBILIFE.NS', 'SHRIRAMFIN.NS', 'SBIN.NS', 'SUNPHARMA.NS', 'TATACONSUM.NS',
|
| 531 |
+
'TCS.NS', 'TATAMOTORS.NS', 'TATASTEEL.NS', 'TECHM.NS', 'TITAN.NS',
|
| 532 |
+
'TRENT.NS', 'ULTRACEMCO.NS', 'WIPRO.NS', 'ETERNAL.NS']
|
| 533 |
+
|
| 534 |
+
selected_stock = st.sidebar.selectbox("Select Stock", stocks)
|
| 535 |
+
start_date = st.sidebar.date_input("Start Date", pd.to_datetime("2024-01-01"))
|
| 536 |
+
|
| 537 |
+
# Strategy selection
|
| 538 |
+
strategy_type = st.sidebar.selectbox("Strategy Type", ["SMA-based", "EMA-based", "Both"])
|
| 539 |
+
|
| 540 |
+
st.sidebar.subheader("📈 Technical Indicators")
|
| 541 |
+
rsi_period = st.sidebar.slider("RSI Period", 5, 30, 14)
|
| 542 |
+
sma_short = st.sidebar.slider("Short-term SMA", 5, 30, 20)
|
| 543 |
+
sma_long = st.sidebar.slider("Long-term SMA", 30, 100, 50)
|
| 544 |
+
ema_short = st.sidebar.slider("Short-term EMA", 5, 30, 20)
|
| 545 |
+
ema_long = st.sidebar.slider("Long-term EMA", 30, 100, 50)
|
| 546 |
+
|
| 547 |
+
st.sidebar.subheader("💰 Backtesting Parameters")
|
| 548 |
+
initial_cash = st.sidebar.number_input("Initial Capital (₹)", min_value=10000, value=100000, step=10000)
|
| 549 |
+
transaction_cost = st.sidebar.slider("Transaction Cost (%)", 0.0, 1.0, 0.1, step=0.05) / 100
|
| 550 |
+
stop_loss = st.sidebar.slider("Stop Loss (%)", 0.0, 20.0, 5.0, step=1.0) / 100
|
| 551 |
+
take_profit = st.sidebar.slider("Take Profit (%)", 0.0, 50.0, 15.0, step=5.0) / 100
|
| 552 |
+
|
| 553 |
+
# Enable/disable risk management
|
| 554 |
+
use_risk_mgmt = st.sidebar.checkbox("Enable Risk Management", value=True)
|
| 555 |
+
|
| 556 |
+
# Load data with progress bar
|
| 557 |
+
with st.spinner(f'Loading data for {selected_stock}...'):
|
| 558 |
+
df = fetch_stock_data(selected_stock, start_date=start_date.strftime("%Y-%m-%d"))
|
| 559 |
+
|
| 560 |
+
st.subheader(f"📊 Stock Data for {selected_stock}")
|
| 561 |
+
st.write(f"**Date Range:** {start_date.strftime('%Y-%m-%d')} to Present")
|
| 562 |
+
st.write(f"**Total Records:** {len(df)} days")
|
| 563 |
+
|
| 564 |
+
if df.empty:
|
| 565 |
+
st.error("❌ No data found for the selected stock and date range.")
|
| 566 |
+
st.stop()
|
| 567 |
+
|
| 568 |
+
# Apply indicators
|
| 569 |
+
with st.spinner('Calculating technical indicators...'):
|
| 570 |
+
df['RSI'] = rsi(df, period=rsi_period)
|
| 571 |
+
df['SMA20'] = sma(df, period=sma_short)
|
| 572 |
+
df['SMA50'] = sma(df, period=sma_long)
|
| 573 |
+
df['EMA20'] = ema(df, period=ema_short)
|
| 574 |
+
df['EMA50'] = ema(df, period=ema_long)
|
| 575 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = macd(df)
|
| 576 |
+
df['Upper_Band'] = df['SMA20'] + 2 * df['Close'].rolling(window=20).std()
|
| 577 |
+
df['Lower_Band'] = df['SMA20'] - 2 * df['Close'].rolling(window=20).std()
|
| 578 |
+
|
| 579 |
+
# Apply strategies based on selection
|
| 580 |
+
if strategy_type in ["SMA-based", "Both"]:
|
| 581 |
+
df = generate_signals_sma(df, rsi_col='RSI', sma_short_col='SMA20', sma_long_col='SMA50')
|
| 582 |
+
|
| 583 |
+
if strategy_type in ["EMA-based", "Both"]:
|
| 584 |
+
df = generate_signals_ema(df, rsi_col='RSI', ema_short_col='EMA20', ema_long_col='EMA50')
|
| 585 |
+
|
| 586 |
+
# Backtesting section
|
| 587 |
+
st.header("🔍 Backtesting Results")
|
| 588 |
+
|
| 589 |
+
# Create tabs for different strategies
|
| 590 |
+
if strategy_type == "Both":
|
| 591 |
+
tab1, tab2 = st.tabs(["SMA Strategy", "EMA Strategy"])
|
| 592 |
+
|
| 593 |
+
with tab1:
|
| 594 |
+
st.subheader("📊 SMA Strategy Results")
|
| 595 |
+
sma_results, sma_metrics = backtest_signals(
|
| 596 |
+
df,
|
| 597 |
+
signal_col='SMA_Signal',
|
| 598 |
+
price_col='Close',
|
| 599 |
+
initial_cash=initial_cash,
|
| 600 |
+
transaction_cost=transaction_cost if use_risk_mgmt else 0,
|
| 601 |
+
stop_loss=stop_loss if use_risk_mgmt else None,
|
| 602 |
+
take_profit=take_profit if use_risk_mgmt else None
|
| 603 |
+
)
|
| 604 |
+
display_strategy_results(df, sma_results, sma_metrics, "SMA", sma_short, sma_long, initial_cash, selected_stock)
|
| 605 |
+
|
| 606 |
+
with tab2:
|
| 607 |
+
st.subheader("📊 EMA Strategy Results")
|
| 608 |
+
ema_results, ema_metrics = backtest_signals(
|
| 609 |
+
df,
|
| 610 |
+
signal_col='EMA_Signal',
|
| 611 |
+
price_col='Close',
|
| 612 |
+
initial_cash=initial_cash,
|
| 613 |
+
transaction_cost=transaction_cost if use_risk_mgmt else 0,
|
| 614 |
+
stop_loss=stop_loss if use_risk_mgmt else None,
|
| 615 |
+
take_profit=take_profit if use_risk_mgmt else None
|
| 616 |
+
)
|
| 617 |
+
display_strategy_results(df, ema_results, ema_metrics, "EMA", ema_short, ema_long, initial_cash, selected_stock)
|
| 618 |
+
|
| 619 |
+
else:
|
| 620 |
+
# Single strategy
|
| 621 |
+
signal_col = 'SMA_Signal' if strategy_type == "SMA-based" else 'EMA_Signal'
|
| 622 |
+
strategy_name = strategy_type.split('-')[0]
|
| 623 |
+
|
| 624 |
+
results, metrics = backtest_signals(
|
| 625 |
+
df,
|
| 626 |
+
signal_col=signal_col,
|
| 627 |
+
price_col='Close',
|
| 628 |
+
initial_cash=initial_cash,
|
| 629 |
+
transaction_cost=transaction_cost if use_risk_mgmt else 0,
|
| 630 |
+
stop_loss=stop_loss if use_risk_mgmt else None,
|
| 631 |
+
take_profit=take_profit if use_risk_mgmt else None
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
period_short = sma_short if strategy_type == "SMA-based" else ema_short
|
| 635 |
+
period_long = sma_long if strategy_type == "SMA-based" else ema_long
|
| 636 |
+
display_strategy_results(df, results, metrics, strategy_name, period_short, period_long, initial_cash, selected_stock)
|
| 637 |
+
|
| 638 |
+
# Data download section
|
| 639 |
+
st.subheader("💾 Download Data")
|
| 640 |
+
col1, col2 = st.columns(2)
|
| 641 |
+
|
| 642 |
+
with col1:
|
| 643 |
+
csv_data = df.to_csv(index=True)
|
| 644 |
+
st.download_button(
|
| 645 |
+
label="📁 Download Full Dataset (CSV)",
|
| 646 |
+
data=csv_data,
|
| 647 |
+
file_name=f"{selected_stock}_analysis_{start_date.strftime('%Y%m%d')}.csv",
|
| 648 |
+
mime="text/csv"
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
with col2:
|
| 652 |
+
if 'results' in locals():
|
| 653 |
+
results_csv = results.to_csv(index=True)
|
| 654 |
+
st.download_button(
|
| 655 |
+
label="📊 Download Backtest Results (CSV)",
|
| 656 |
+
data=results_csv,
|
| 657 |
+
file_name=f"{selected_stock}_backtest_{start_date.strftime('%Y%m%d')}.csv",
|
| 658 |
+
mime="text/csv"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
# Footer
|
| 662 |
+
st.markdown("---")
|
| 663 |
+
st.markdown("Developed by Zane Vijay Falcao")
|
src/models/H2H_model.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/models/app.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import yfinance as yf
|
| 5 |
+
import pickle
|
| 6 |
+
import plotly.graph_objects as go
|
| 7 |
+
import plotly.express as px
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import warnings
|
| 10 |
+
from curl_cffi import requests
|
| 11 |
+
session = requests.Session(impersonate="chrome")
|
| 12 |
+
warnings.filterwarnings('ignore')
|
| 13 |
+
|
| 14 |
+
# Page config
|
| 15 |
+
st.set_page_config(
|
| 16 |
+
page_title="Stock Price Prediction App",
|
| 17 |
+
page_icon="📈",
|
| 18 |
+
layout="wide"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Title and description
|
| 22 |
+
st.title("📈 Stock Price Prediction App")
|
| 23 |
+
st.markdown("This app uses a trained Logistic Regression model to predict whether a stock will go **UP** ⬆️ or **DOWN** ⬇️ the next day.")
|
| 24 |
+
|
| 25 |
+
# Sidebar for user inputs
|
| 26 |
+
st.sidebar.header("🔧 Configuration")
|
| 27 |
+
|
| 28 |
+
# Stock symbols from your model
|
| 29 |
+
STOCK_SYMBOLS = [
|
| 30 |
+
'ADANIENT.NS', 'ADANIPORTS.NS', 'APOLLOHOSP.NS', 'ASIANPAINT.NS',
|
| 31 |
+
'AXISBANK.NS', 'BAJAJ-AUTO.NS', 'BAJFINANCE.NS', 'BAJAJFINSV.NS',
|
| 32 |
+
'BEL.NS', 'BHARTIARTL.NS', 'CIPLA.NS', 'COALINDIA.NS', 'DRREDDY.NS',
|
| 33 |
+
'EICHERMOT.NS', 'GRASIM.NS', 'HCLTECH.NS', 'HDFCBANK.NS', 'HDFCLIFE.NS',
|
| 34 |
+
'HEROMOTOCO.NS', 'HINDALCO.NS', 'HINDUNILVR.NS', 'ICICIBANK.NS',
|
| 35 |
+
'INDUSINDBK.NS', 'INFY.NS', 'ITC.NS', 'JIOFIN.NS', 'JSWSTEEL.NS',
|
| 36 |
+
'KOTAKBANK.NS', 'LT.NS', 'M&M.NS', 'MARUTI.NS', 'NESTLEIND.NS',
|
| 37 |
+
'NTPC.NS', 'ONGC.NS', 'POWERGRID.NS', 'RELIANCE.NS', 'SBILIFE.NS',
|
| 38 |
+
'SHRIRAMFIN.NS', 'SBIN.NS', 'SUNPHARMA.NS', 'TATACONSUM.NS', 'TCS.NS',
|
| 39 |
+
'TATAMOTORS.NS', 'TATASTEEL.NS', 'TECHM.NS', 'TITAN.NS', 'TRENT.NS',
|
| 40 |
+
'ULTRACEMCO.NS', 'WIPRO.NS', 'ETERNAL.NS'
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
# User inputs
|
| 44 |
+
selected_stock = st.sidebar.selectbox("Select Stock Symbol", STOCK_SYMBOLS, index=35) # Default to RELIANCE.NS
|
| 45 |
+
start_date = st.sidebar.date_input("Start Date", value=datetime(2020, 1, 1))
|
| 46 |
+
end_date = st.sidebar.date_input("End Date", value=datetime.now())
|
| 47 |
+
prediction_mode = st.sidebar.button("Start Analysis")
|
| 48 |
+
rsi_period = st.sidebar.slider("RSI Period", min_value=5, max_value=30, value=14, step=1)
|
| 49 |
+
short_period = st.sidebar.slider("Short-term", min_value=5, max_value=50, value=20, step=1)
|
| 50 |
+
long_period = st.sidebar.slider("Long-term", min_value=50, max_value=200, value=50, step=1)
|
| 51 |
+
|
| 52 |
+
# Helper functions (same as in your original code)
|
| 53 |
+
def SMA(series, period):
|
| 54 |
+
return series.rolling(window=period).mean()
|
| 55 |
+
|
| 56 |
+
def EMA(series, period):
|
| 57 |
+
return series.ewm(span=period, adjust=False).mean()
|
| 58 |
+
|
| 59 |
+
def MACD(series, fast=12, slow=26, signal=9):
|
| 60 |
+
ema_fast = EMA(series, fast)
|
| 61 |
+
ema_slow = EMA(series, slow)
|
| 62 |
+
macd = ema_fast - ema_slow
|
| 63 |
+
macd_signal = EMA(macd, signal)
|
| 64 |
+
macd_hist = macd - macd_signal
|
| 65 |
+
return macd, macd_signal, macd_hist
|
| 66 |
+
|
| 67 |
+
def RSI(series, period=14):
|
| 68 |
+
delta = series.diff()
|
| 69 |
+
gain = (delta.where(delta > 0, 0)).ewm(alpha=1/period, min_periods=period).mean()
|
| 70 |
+
loss = (-delta.where(delta < 0, 0)).ewm(alpha=1/period, min_periods=period).mean()
|
| 71 |
+
RS = gain / loss
|
| 72 |
+
return 100 - (100 / (1 + RS))
|
| 73 |
+
|
| 74 |
+
def create_volatility_features(df):
|
| 75 |
+
if 'return_1d' not in df.columns:
|
| 76 |
+
df['return_1d'] = df['Close'].pct_change()
|
| 77 |
+
|
| 78 |
+
for period in [5, 10, 20, 30]:
|
| 79 |
+
df[f'volatility_{period}d'] = df['return_1d'].rolling(period).std()
|
| 80 |
+
|
| 81 |
+
df['vol_ratio_5_20'] = df['volatility_5d'] / df['volatility_20d']
|
| 82 |
+
df['vol_ratio_10_20'] = df['volatility_10d'] / df['volatility_20d']
|
| 83 |
+
df['vol_rank_20'] = df['volatility_5d'].rolling(20).rank(pct=True)
|
| 84 |
+
df['vol_rank_50'] = df['volatility_5d'].rolling(50).rank(pct=True)
|
| 85 |
+
|
| 86 |
+
return df
|
| 87 |
+
|
| 88 |
+
def create_enhanced_lag_features(df):
|
| 89 |
+
for lag in [1, 2, 3, 5, 10]:
|
| 90 |
+
df[f'return_lag_{lag}'] = df['return_1d'].shift(lag)
|
| 91 |
+
|
| 92 |
+
for lag in [1, 2, 3]:
|
| 93 |
+
if 'RSI14' in df.columns:
|
| 94 |
+
df[f'rsi_lag_{lag}'] = df['RSI14'].shift(lag)
|
| 95 |
+
if 'MACD' in df.columns:
|
| 96 |
+
df[f'macd_lag_{lag}'] = df['MACD'].shift(lag)
|
| 97 |
+
|
| 98 |
+
if 'volume_ratio_20' in df.columns:
|
| 99 |
+
for lag in [1, 2]:
|
| 100 |
+
df[f'volume_ratio_lag_{lag}'] = df['volume_ratio_20'].shift(lag)
|
| 101 |
+
|
| 102 |
+
return df
|
| 103 |
+
|
| 104 |
+
def create_volume_features(df):
|
| 105 |
+
df['volume_sma_10'] = df['Volume'].rolling(10).mean()
|
| 106 |
+
df['volume_sma_20'] = df['Volume'].rolling(20).mean()
|
| 107 |
+
df['volume_sma_50'] = df['Volume'].rolling(50).mean()
|
| 108 |
+
|
| 109 |
+
df['volume_ratio_10'] = df['Volume'] / df['volume_sma_10']
|
| 110 |
+
df['volume_ratio_20'] = df['Volume'] / df['volume_sma_20']
|
| 111 |
+
df['volume_ratio_50'] = df['Volume'] / df['volume_sma_50']
|
| 112 |
+
|
| 113 |
+
df['price_volume'] = df['Close'] * df['Volume']
|
| 114 |
+
df['pv_sma_5'] = df['price_volume'].rolling(5).mean()
|
| 115 |
+
df['volume_momentum_5'] = df['Volume'] / df['Volume'].shift(5)
|
| 116 |
+
|
| 117 |
+
return df
|
| 118 |
+
|
| 119 |
+
def create_momentum_features(df):
|
| 120 |
+
for period in [3, 5, 10, 20]:
|
| 121 |
+
df[f'momentum_{period}d'] = df['Close'] / df['Close'].shift(period) - 1
|
| 122 |
+
|
| 123 |
+
for period in [5, 10]:
|
| 124 |
+
df[f'roc_{period}d'] = (df['Close'] - df['Close'].shift(period)) / df['Close'].shift(period)
|
| 125 |
+
|
| 126 |
+
return df
|
| 127 |
+
|
| 128 |
+
def create_position_features(df):
|
| 129 |
+
for period in [10, 20, 50]:
|
| 130 |
+
df[f'high_{period}d'] = df['High'].rolling(period).max()
|
| 131 |
+
df[f'low_{period}d'] = df['Low'].rolling(period).min()
|
| 132 |
+
df[f'price_position_{period}'] = (df['Close'] - df[f'low_{period}d']) / (df[f'high_{period}d'] - df[f'low_{period}d'])
|
| 133 |
+
|
| 134 |
+
if 'SMA20' in df.columns:
|
| 135 |
+
bb_std = df['Close'].rolling(20).std()
|
| 136 |
+
df['bb_upper'] = df['SMA20'] + (bb_std * 2)
|
| 137 |
+
df['bb_lower'] = df['SMA20'] - (bb_std * 2)
|
| 138 |
+
df['bb_position'] = (df['Close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
| 139 |
+
|
| 140 |
+
return df
|
| 141 |
+
|
| 142 |
+
def process_stock_data(df):
|
| 143 |
+
"""Process stock data to create all features"""
|
| 144 |
+
df = df.copy()
|
| 145 |
+
|
| 146 |
+
# Basic technical indicators
|
| 147 |
+
df['SMA20'] = SMA(df['Close'], short_period)
|
| 148 |
+
df['SMA50'] = SMA(df['Close'], long_period)
|
| 149 |
+
df['EMA20'] = EMA(df['Close'], short_period)
|
| 150 |
+
df['EMA50'] = EMA(df['Close'], long_period)
|
| 151 |
+
df['RSI14'] = RSI(df['Close'], rsi_period)
|
| 152 |
+
df['RSI20'] = RSI(df['Close'], rsi_period + 6) # Example for another RSI period
|
| 153 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = MACD(df['Close'])
|
| 154 |
+
|
| 155 |
+
# Create feature sets
|
| 156 |
+
df = create_volatility_features(df)
|
| 157 |
+
df = create_enhanced_lag_features(df)
|
| 158 |
+
df = create_volume_features(df)
|
| 159 |
+
df = create_momentum_features(df)
|
| 160 |
+
df = create_position_features(df)
|
| 161 |
+
|
| 162 |
+
# Additional features
|
| 163 |
+
df['SMA_crossover'] = (df['SMA20'] > df['SMA50']).astype(int)
|
| 164 |
+
df['RSI_oversold'] = (df['RSI14'] < 30).astype(int)
|
| 165 |
+
# Target: next-day up/down
|
| 166 |
+
df['next_close'] = df['Close'].shift(-1)
|
| 167 |
+
df['target'] = (df['next_close'] > df['Close']).astype(int)
|
| 168 |
+
|
| 169 |
+
return df
|
| 170 |
+
|
| 171 |
+
@st.cache_data
|
| 172 |
+
def load_stock_data(symbol, start_date, end_date):
|
| 173 |
+
"""Load stock data from Yahoo Finance"""
|
| 174 |
+
try:
|
| 175 |
+
data = yf.download(symbol, start=start_date, end=end_date,session=session)
|
| 176 |
+
# Flatten the MultiIndex columns
|
| 177 |
+
data.columns = [col[0] for col in data.columns]
|
| 178 |
+
return data
|
| 179 |
+
except Exception as e:
|
| 180 |
+
st.error(f"Error loading data: {e}")
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
# Feature list (same as in your model)
|
| 184 |
+
FEATURES = [
|
| 185 |
+
'Close', 'Volume', 'SMA20', 'SMA50', 'EMA20', 'EMA50',
|
| 186 |
+
'RSI14', 'MACD', 'MACD_signal', 'MACD_hist',
|
| 187 |
+
'SMA_crossover', 'RSI_oversold',
|
| 188 |
+
'return_1d', 'volatility_5d', 'volatility_10d', 'volatility_20d',
|
| 189 |
+
'volatility_30d', 'vol_ratio_5_20', 'vol_ratio_10_20', 'vol_rank_20',
|
| 190 |
+
'vol_rank_50', 'return_lag_1', 'return_lag_2', 'return_lag_3',
|
| 191 |
+
'return_lag_5', 'return_lag_10', 'rsi_lag_1', 'macd_lag_1', 'rsi_lag_2',
|
| 192 |
+
'macd_lag_2', 'rsi_lag_3', 'macd_lag_3', 'volume_sma_10',
|
| 193 |
+
'volume_sma_20', 'volume_sma_50', 'volume_ratio_10', 'volume_ratio_20',
|
| 194 |
+
'volume_ratio_50', 'price_volume', 'pv_sma_5', 'volume_momentum_5',
|
| 195 |
+
'momentum_3d', 'momentum_5d', 'momentum_10d', 'momentum_20d', 'roc_5d',
|
| 196 |
+
'roc_10d', 'high_10d', 'low_10d', 'price_position_10', 'high_20d',
|
| 197 |
+
'low_20d', 'price_position_20', 'high_50d', 'low_50d',
|
| 198 |
+
'price_position_50', 'bb_upper', 'bb_lower', 'bb_position','target'
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
# Main app logic
|
| 202 |
+
st.header(f"📊 Latest Data Prediction for {selected_stock}")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
with st.spinner("Loading stock data..."):
|
| 206 |
+
stock_data = load_stock_data(selected_stock, start_date, end_date)
|
| 207 |
+
|
| 208 |
+
if stock_data is not None and not stock_data.empty:
|
| 209 |
+
# Process the data
|
| 210 |
+
processed_data = process_stock_data(stock_data)
|
| 211 |
+
processed_data = processed_data.dropna()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
if len(processed_data) > 0:
|
| 215 |
+
# Get the latest row for prediction
|
| 216 |
+
latest_data = processed_data.iloc[-1]
|
| 217 |
+
|
| 218 |
+
# Display current stock info
|
| 219 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 220 |
+
with col1:
|
| 221 |
+
st.metric("Current Price", f"₹{latest_data['Close']:.2f}")
|
| 222 |
+
with col2:
|
| 223 |
+
daily_change = ((latest_data['Close'] - processed_data.iloc[-2]['Close']) / processed_data.iloc[-2]['Close']) * 100
|
| 224 |
+
st.metric("Daily Change", f"{daily_change:.2f}%")
|
| 225 |
+
with col3:
|
| 226 |
+
st.metric("Volume", f"{latest_data['Volume']:,.0f}")
|
| 227 |
+
with col4:
|
| 228 |
+
st.metric("RSI14", f"{latest_data['RSI14']:.2f}")
|
| 229 |
+
|
| 230 |
+
# Create feature vector
|
| 231 |
+
feature_vector = latest_data[FEATURES].values.reshape(1, -1)
|
| 232 |
+
|
| 233 |
+
# For demo purposes, create a mock prediction (since we don't have the actual model file)
|
| 234 |
+
# In real implementation, you would load your saved model:
|
| 235 |
+
model = pickle.load(open('logistic_regression_model.pkl', 'rb'))
|
| 236 |
+
scaler = pickle.load(open('scaler.pkl', 'rb')) # You'd need to save this too
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# Scale the features
|
| 240 |
+
feature_vector_scaled = scaler.transform(feature_vector)
|
| 241 |
+
|
| 242 |
+
# Make prediction
|
| 243 |
+
prediction = model.predict(feature_vector_scaled)[0]
|
| 244 |
+
probability = model.predict_proba(feature_vector_scaled)[0].max()
|
| 245 |
+
|
| 246 |
+
# Display prediction
|
| 247 |
+
st.header("🔮 Prediction")
|
| 248 |
+
col1, col2 = st.columns(2)
|
| 249 |
+
|
| 250 |
+
with col1:
|
| 251 |
+
if prediction == 1:
|
| 252 |
+
st.success("📈 **PREDICTION: UP**")
|
| 253 |
+
st.write(f"The model predicts the stock will go **UP** tomorrow with {probability:.1%} confidence.")
|
| 254 |
+
else:
|
| 255 |
+
st.error("📉 **PREDICTION: DOWN**")
|
| 256 |
+
st.write(f"The model predicts the stock will go **DOWN** tomorrow with {probability:.1%} confidence.")
|
| 257 |
+
|
| 258 |
+
with col2:
|
| 259 |
+
# Confidence gauge
|
| 260 |
+
fig_gauge = go.Figure(go.Indicator(
|
| 261 |
+
mode = "gauge+number",
|
| 262 |
+
value = probability * 100,
|
| 263 |
+
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 264 |
+
title = {'text': "Confidence %"},
|
| 265 |
+
gauge = {
|
| 266 |
+
'axis': {'range': [None, 100]},
|
| 267 |
+
'bar': {'color': "darkgreen" if prediction == 1 else "darkred"},
|
| 268 |
+
'steps': [
|
| 269 |
+
{'range': [0, 50], 'color': "lightgray"},
|
| 270 |
+
{'range': [50, 80], 'color': "yellow"},
|
| 271 |
+
{'range': [80, 100], 'color': "lightgreen"}
|
| 272 |
+
],
|
| 273 |
+
'threshold': {
|
| 274 |
+
'line': {'color': "red", 'width': 4},
|
| 275 |
+
'thickness': 0.75,
|
| 276 |
+
'value': 90
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
))
|
| 280 |
+
fig_gauge.update_layout(height=300)
|
| 281 |
+
st.plotly_chart(fig_gauge, use_container_width=True)
|
| 282 |
+
|
| 283 |
+
# Technical indicators chart
|
| 284 |
+
st.header("📈 Technical Analysis")
|
| 285 |
+
|
| 286 |
+
# Price and Simple moving averages
|
| 287 |
+
fig_price = go.Figure()
|
| 288 |
+
fig_price.add_trace(go.Scatter(
|
| 289 |
+
x=processed_data.index[-60:],
|
| 290 |
+
y=processed_data['Close'][-60:],
|
| 291 |
+
mode='lines',
|
| 292 |
+
name='Close Price',
|
| 293 |
+
line=dict(color='blue', width=2)
|
| 294 |
+
))
|
| 295 |
+
fig_price.add_trace(go.Scatter(
|
| 296 |
+
x=processed_data.index[-60:],
|
| 297 |
+
y=processed_data['SMA20'][-60:],
|
| 298 |
+
mode='lines',
|
| 299 |
+
name='SMA20',
|
| 300 |
+
line=dict(color='orange', width=1)
|
| 301 |
+
))
|
| 302 |
+
fig_price.add_trace(go.Scatter(
|
| 303 |
+
x=processed_data.index[-60:],
|
| 304 |
+
y=processed_data['SMA50'][-60:],
|
| 305 |
+
mode='lines',
|
| 306 |
+
name='SMA50',
|
| 307 |
+
line=dict(color='red', width=1)
|
| 308 |
+
))
|
| 309 |
+
|
| 310 |
+
fig_price.update_layout(
|
| 311 |
+
title=f"{selected_stock} - Price and Simple Moving Averages (Last 60 Days)",
|
| 312 |
+
xaxis_title="Date",
|
| 313 |
+
yaxis_title="Price (₹)",
|
| 314 |
+
height=400
|
| 315 |
+
)
|
| 316 |
+
st.plotly_chart(fig_price, use_container_width=True)
|
| 317 |
+
|
| 318 |
+
# Price and Exponential moving averages
|
| 319 |
+
fig_price = go.Figure()
|
| 320 |
+
fig_price.add_trace(go.Scatter(
|
| 321 |
+
x=processed_data.index[-30:],
|
| 322 |
+
y=processed_data['Close'][-30:],
|
| 323 |
+
mode='lines',
|
| 324 |
+
name='Close Price',
|
| 325 |
+
line=dict(color='blue', width=2)
|
| 326 |
+
))
|
| 327 |
+
fig_price.add_trace(go.Scatter(
|
| 328 |
+
x=processed_data.index[-30:],
|
| 329 |
+
y=processed_data['EMA20'][-30:],
|
| 330 |
+
mode='lines',
|
| 331 |
+
name='EMA20',
|
| 332 |
+
line=dict(color='orange', width=1)
|
| 333 |
+
))
|
| 334 |
+
fig_price.add_trace(go.Scatter(
|
| 335 |
+
x=processed_data.index[-30:],
|
| 336 |
+
y=processed_data['EMA50'][-30:],
|
| 337 |
+
mode='lines',
|
| 338 |
+
name='EMA50',
|
| 339 |
+
line=dict(color='red', width=1)
|
| 340 |
+
))
|
| 341 |
+
|
| 342 |
+
fig_price.update_layout(
|
| 343 |
+
title=f"{selected_stock} - Price and Exponential Moving Averages (Last 60 Days)",
|
| 344 |
+
xaxis_title="Date",
|
| 345 |
+
yaxis_title="Price (₹)",
|
| 346 |
+
height=400
|
| 347 |
+
)
|
| 348 |
+
st.plotly_chart(fig_price, use_container_width=True)
|
| 349 |
+
|
| 350 |
+
# RSI chart
|
| 351 |
+
col1, col2 = st.columns(2)
|
| 352 |
+
with col1:
|
| 353 |
+
fig_rsi = go.Figure()
|
| 354 |
+
fig_rsi.add_trace(go.Scatter(
|
| 355 |
+
x=processed_data.index[-30:],
|
| 356 |
+
y=processed_data['RSI14'][-30:],
|
| 357 |
+
mode='lines',
|
| 358 |
+
name='RSI14',
|
| 359 |
+
line=dict(color='purple')
|
| 360 |
+
))
|
| 361 |
+
fig_rsi.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="Overbought")
|
| 362 |
+
fig_rsi.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="Oversold")
|
| 363 |
+
fig_rsi.update_layout(
|
| 364 |
+
title="RSI (14-day)",
|
| 365 |
+
xaxis_title="Date",
|
| 366 |
+
yaxis_title="RSI",
|
| 367 |
+
height=300
|
| 368 |
+
)
|
| 369 |
+
st.plotly_chart(fig_rsi, use_container_width=True)
|
| 370 |
+
|
| 371 |
+
with col2:
|
| 372 |
+
# MACD chart
|
| 373 |
+
fig_macd = go.Figure()
|
| 374 |
+
fig_macd.add_trace(go.Scatter(
|
| 375 |
+
x=processed_data.index[-30:],
|
| 376 |
+
y=processed_data['MACD'][-30:],
|
| 377 |
+
mode='lines',
|
| 378 |
+
name='MACD',
|
| 379 |
+
line=dict(color='blue')
|
| 380 |
+
))
|
| 381 |
+
fig_macd.add_trace(go.Scatter(
|
| 382 |
+
x=processed_data.index[-30:],
|
| 383 |
+
y=processed_data['MACD_signal'][-30:],
|
| 384 |
+
mode='lines',
|
| 385 |
+
name='Signal',
|
| 386 |
+
line=dict(color='red')
|
| 387 |
+
))
|
| 388 |
+
fig_macd.update_layout(
|
| 389 |
+
title="MACD",
|
| 390 |
+
xaxis_title="Date",
|
| 391 |
+
yaxis_title="MACD",
|
| 392 |
+
height=300
|
| 393 |
+
)
|
| 394 |
+
st.plotly_chart(fig_macd, use_container_width=True)
|
| 395 |
+
|
| 396 |
+
# Feature importance (mock data for demo)
|
| 397 |
+
st.header("🎯 Key Factors")
|
| 398 |
+
st.write("Most important features affecting the prediction:")
|
| 399 |
+
|
| 400 |
+
mock_features = ['RSI14', 'return_lag_1', 'volatility_5d', 'MACD', 'volume_ratio_20']
|
| 401 |
+
mock_importance = [0.15, 0.12, 0.10, 0.08, 0.07]
|
| 402 |
+
|
| 403 |
+
fig_importance = px.bar(
|
| 404 |
+
x=mock_importance,
|
| 405 |
+
y=mock_features,
|
| 406 |
+
orientation='h',
|
| 407 |
+
title="Feature Importance"
|
| 408 |
+
)
|
| 409 |
+
fig_importance.update_layout(height=300)
|
| 410 |
+
st.plotly_chart(fig_importance, use_container_width=True)
|
| 411 |
+
|
| 412 |
+
else:
|
| 413 |
+
st.error("Not enough data to make a prediction. Please try a different stock or date range.")
|
| 414 |
+
else:
|
| 415 |
+
st.error("Unable to load stock data. Please check the symbol and try again.")
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# Sidebar information
|
| 420 |
+
st.sidebar.markdown("---")
|
| 421 |
+
st.sidebar.header("ℹ️ About")
|
| 422 |
+
st.sidebar.write("""
|
| 423 |
+
This app uses a Logistic Regression model trained on:
|
| 424 |
+
- **50 Indian stocks** from NSE
|
| 425 |
+
- **59 technical features** including RSI, MACD, moving averages, volatility measures, and lag features
|
| 426 |
+
- **Historical data** for pattern recognition
|
| 427 |
+
|
| 428 |
+
**Disclaimer**: This is for educational purposes only. Always do your own research before making investment decisions.
|
| 429 |
+
""")
|
| 430 |
+
|
| 431 |
+
st.sidebar.markdown("---")
|
| 432 |
+
st.sidebar.write("**Model Performance:**")
|
| 433 |
+
st.sidebar.write("• Accuracy: 55%")
|
| 434 |
+
st.sidebar.write("• F1 Score: 0.4839")
|
| 435 |
+
st.sidebar.write("• AUC: 0.5370")
|
| 436 |
+
st.sidebar.write("Average Precision (AP): 0.5300")
|
| 437 |
+
|
| 438 |
+
# Footer
|
| 439 |
+
st.markdown("---")
|
| 440 |
+
st.markdown("**⚠️ Disclaimer**: This prediction model is for research purposes only. Stock market investments are subject to market risks. Please consult with a financial advisor before making investment decisions.")
|
src/models/h2h_model.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""H2H model.ipynb
|
| 3 |
+
|
| 4 |
+
Automatically generated by Colab.
|
| 5 |
+
|
| 6 |
+
Original file is located at
|
| 7 |
+
https://colab.research.google.com/drive/1uxbLGJ4l9i0bdWy43Oz4rgsyTdA5FTSd
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
!pip install yfinance
|
| 11 |
+
|
| 12 |
+
# Data and computation
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
# Plotting
|
| 17 |
+
import matplotlib.pyplot as plt
|
| 18 |
+
import seaborn as sns
|
| 19 |
+
sns.set_style('whitegrid')
|
| 20 |
+
plt.style.use("fivethirtyeight")
|
| 21 |
+
|
| 22 |
+
# Yahoo Finance data import
|
| 23 |
+
import yfinance as yf
|
| 24 |
+
|
| 25 |
+
# Machine Learning
|
| 26 |
+
from sklearn.model_selection import train_test_split
|
| 27 |
+
from sklearn.tree import DecisionTreeClassifier
|
| 28 |
+
from sklearn.linear_model import LogisticRegression
|
| 29 |
+
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, roc_curve, auc
|
| 30 |
+
from sklearn.preprocessing import StandardScaler, RobustScaler
|
| 31 |
+
from sklearn.pipeline import Pipeline
|
| 32 |
+
import scipy.stats as stats
|
| 33 |
+
from sklearn.model_selection import GridSearchCV
|
| 34 |
+
|
| 35 |
+
# Misc
|
| 36 |
+
import warnings
|
| 37 |
+
warnings.filterwarnings('ignore')
|
| 38 |
+
|
| 39 |
+
# Select stocks and date range
|
| 40 |
+
symbols = ['ADANIENT.NS',
|
| 41 |
+
'ADANIPORTS.NS', 'APOLLOHOSP.NS', 'ASIANPAINT.NS',
|
| 42 |
+
'AXISBANK.NS', 'BAJAJ-AUTO.NS', 'BAJFINANCE.NS', 'BAJAJFINSV.NS', 'BEL.NS', 'BHARTIARTL.NS', 'CIPLA.NS',
|
| 43 |
+
'COALINDIA.NS', 'DRREDDY.NS', 'EICHERMOT.NS', 'GRASIM.NS', 'HCLTECH.NS', 'HDFCBANK.NS',
|
| 44 |
+
'HDFCLIFE.NS', 'HEROMOTOCO.NS', 'HINDALCO.NS', 'HINDUNILVR.NS', 'ICICIBANK.NS', 'INDUSINDBK.NS',
|
| 45 |
+
'INFY.NS', 'ITC.NS', 'JIOFIN.NS', 'JSWSTEEL.NS', 'KOTAKBANK.NS', 'LT.NS', 'M&M.NS', 'MARUTI.NS',
|
| 46 |
+
'NESTLEIND.NS', 'NTPC.NS', 'ONGC.NS', 'POWERGRID.NS', 'RELIANCE.NS', 'SBILIFE.NS', 'SHRIRAMFIN.NS',
|
| 47 |
+
'SBIN.NS', 'SUNPHARMA.NS', 'TATACONSUM.NS', 'TCS.NS',
|
| 48 |
+
'TATAMOTORS.NS', 'TATASTEEL.NS', 'TECHM.NS',
|
| 49 |
+
'TITAN.NS', 'TRENT.NS', 'ULTRACEMCO.NS',
|
| 50 |
+
'WIPRO.NS',
|
| 51 |
+
'ETERNAL.NS']
|
| 52 |
+
|
| 53 |
+
start_date = '2024-07-31'
|
| 54 |
+
end_date = '2025-07-31'
|
| 55 |
+
|
| 56 |
+
# Download daily close data for both stocks
|
| 57 |
+
raw_data = yf.download(symbols, start=start_date, end=end_date)
|
| 58 |
+
|
| 59 |
+
# Flatten MultiIndex columns
|
| 60 |
+
raw_data.columns = ['_'.join(col).strip() for col in raw_data.columns.values]
|
| 61 |
+
|
| 62 |
+
# For simplicity, stack to long format and process each stock similarly
|
| 63 |
+
data = raw_data.copy()
|
| 64 |
+
|
| 65 |
+
data
|
| 66 |
+
|
| 67 |
+
# Helper functions
|
| 68 |
+
|
| 69 |
+
def SMA(series, period):
|
| 70 |
+
return series.rolling(window=period).mean()
|
| 71 |
+
|
| 72 |
+
def EMA(series, period):
|
| 73 |
+
return series.ewm(span=period, adjust=False).mean()
|
| 74 |
+
|
| 75 |
+
def MACD(series, fast=12, slow=26, signal=9):
|
| 76 |
+
ema_fast = EMA(series, fast)
|
| 77 |
+
ema_slow = EMA(series, slow)
|
| 78 |
+
macd = ema_fast - ema_slow
|
| 79 |
+
macd_signal = EMA(macd, signal)
|
| 80 |
+
macd_hist = macd - macd_signal
|
| 81 |
+
return macd, macd_signal, macd_hist
|
| 82 |
+
|
| 83 |
+
def RSI(series, period=14):
|
| 84 |
+
delta = series.diff()
|
| 85 |
+
gain = (delta.where(delta > 0, 0)).ewm(alpha=1/period, min_periods=period).mean()
|
| 86 |
+
loss = (-delta.where(delta < 0, 0)).ewm(alpha=1/period, min_periods=period).mean()
|
| 87 |
+
RS = gain / loss
|
| 88 |
+
return 100 - (100 / (1 + RS))
|
| 89 |
+
|
| 90 |
+
def create_volatility_features(df):
|
| 91 |
+
|
| 92 |
+
# Calculate returns if not exists
|
| 93 |
+
if 'return_1d' not in df.columns:
|
| 94 |
+
df['return_1d'] = df['Close'].pct_change()
|
| 95 |
+
|
| 96 |
+
# Volatility features (crucial for logistic regression)
|
| 97 |
+
for period in [5, 10, 20, 30]:
|
| 98 |
+
df[f'volatility_{period}d'] = df['return_1d'].rolling(period).std()
|
| 99 |
+
|
| 100 |
+
# Volatility ratios
|
| 101 |
+
df['vol_ratio_5_20'] = df['volatility_5d'] / df['volatility_20d']
|
| 102 |
+
df['vol_ratio_10_20'] = df['volatility_10d'] / df['volatility_20d']
|
| 103 |
+
|
| 104 |
+
# Volatility rank (where current vol sits in historical range)
|
| 105 |
+
df['vol_rank_20'] = df['volatility_5d'].rolling(20).rank(pct=True)
|
| 106 |
+
df['vol_rank_50'] = df['volatility_5d'].rolling(50).rank(pct=True)
|
| 107 |
+
|
| 108 |
+
return df
|
| 109 |
+
|
| 110 |
+
def create_enhanced_lag_features(df):
|
| 111 |
+
"""Add comprehensive lag features - Critical for time series"""
|
| 112 |
+
#print("Adding enhanced lag features...")
|
| 113 |
+
|
| 114 |
+
# Price momentum lags
|
| 115 |
+
for lag in [1, 2, 3, 5, 10]:
|
| 116 |
+
df[f'return_lag_{lag}'] = df['return_1d'].shift(lag)
|
| 117 |
+
|
| 118 |
+
# Technical indicator lags
|
| 119 |
+
for lag in [1, 2, 3]:
|
| 120 |
+
if 'RSI14' in df.columns:
|
| 121 |
+
df[f'rsi_lag_{lag}'] = df['RSI14'].shift(lag)
|
| 122 |
+
if 'MACD' in df.columns:
|
| 123 |
+
df[f'macd_lag_{lag}'] = df['MACD'].shift(lag)
|
| 124 |
+
|
| 125 |
+
# Volume lags
|
| 126 |
+
if 'volume_ratio_20' in df.columns:
|
| 127 |
+
for lag in [1, 2]:
|
| 128 |
+
df[f'volume_ratio_lag_{lag}'] = df['volume_ratio_20'].shift(lag)
|
| 129 |
+
|
| 130 |
+
return df
|
| 131 |
+
|
| 132 |
+
def create_volume_features(df):
|
| 133 |
+
"""Enhanced volume features"""
|
| 134 |
+
#print("Adding volume features...")
|
| 135 |
+
|
| 136 |
+
# Volume moving averages
|
| 137 |
+
df['volume_sma_10'] = df['Volume'].rolling(10).mean()
|
| 138 |
+
df['volume_sma_20'] = df['Volume'].rolling(20).mean()
|
| 139 |
+
df['volume_sma_50'] = df['Volume'].rolling(50).mean()
|
| 140 |
+
|
| 141 |
+
# Volume ratios
|
| 142 |
+
df['volume_ratio_10'] = df['Volume'] / df['volume_sma_10']
|
| 143 |
+
df['volume_ratio_20'] = df['Volume'] / df['volume_sma_20']
|
| 144 |
+
df['volume_ratio_50'] = df['Volume'] / df['volume_sma_50']
|
| 145 |
+
|
| 146 |
+
# Price-volume features
|
| 147 |
+
df['price_volume'] = df['Close'] * df['Volume']
|
| 148 |
+
df['pv_sma_5'] = df['price_volume'].rolling(5).mean()
|
| 149 |
+
|
| 150 |
+
# Volume momentum
|
| 151 |
+
df['volume_momentum_5'] = df['Volume'] / df['Volume'].shift(5)
|
| 152 |
+
|
| 153 |
+
return df
|
| 154 |
+
|
| 155 |
+
def create_momentum_features(df):
|
| 156 |
+
"""Add momentum features"""
|
| 157 |
+
#print("Adding momentum features...")
|
| 158 |
+
|
| 159 |
+
# Price momentum
|
| 160 |
+
for period in [3, 5, 10, 20]:
|
| 161 |
+
df[f'momentum_{period}d'] = df['Close'] / df['Close'].shift(period) - 1
|
| 162 |
+
|
| 163 |
+
# Rate of change
|
| 164 |
+
for period in [5, 10]:
|
| 165 |
+
df[f'roc_{period}d'] = (df['Close'] - df['Close'].shift(period)) / df['Close'].shift(period)
|
| 166 |
+
|
| 167 |
+
return df
|
| 168 |
+
|
| 169 |
+
def create_position_features(df):
|
| 170 |
+
"""Add price position features"""
|
| 171 |
+
#print("Adding position features...")
|
| 172 |
+
|
| 173 |
+
# Price position in recent range
|
| 174 |
+
for period in [10, 20, 50]:
|
| 175 |
+
df[f'high_{period}d'] = df['High'].rolling(period).max()
|
| 176 |
+
df[f'low_{period}d'] = df['Low'].rolling(period).min()
|
| 177 |
+
df[f'price_position_{period}'] = (df['Close'] - df[f'low_{period}d']) / (df[f'high_{period}d'] - df[f'low_{period}d'])
|
| 178 |
+
|
| 179 |
+
# Bollinger Band position (if BB exists)
|
| 180 |
+
if 'SMA20' in df.columns:
|
| 181 |
+
bb_std = df['Close'].rolling(20).std()
|
| 182 |
+
df['bb_upper'] = df['SMA20'] + (bb_std * 2)
|
| 183 |
+
df['bb_lower'] = df['SMA20'] - (bb_std * 2)
|
| 184 |
+
df['bb_position'] = (df['Close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
| 185 |
+
|
| 186 |
+
return df
|
| 187 |
+
|
| 188 |
+
def create_rolling_stats(df):
|
| 189 |
+
"""Add rolling statistical features"""
|
| 190 |
+
#print("Adding rolling statistics...")
|
| 191 |
+
|
| 192 |
+
# Rolling statistics of returns
|
| 193 |
+
for period in [5, 10]:
|
| 194 |
+
df[f'return_mean_{period}d'] = df['return_1d'].rolling(period).mean()
|
| 195 |
+
df[f'return_std_{period}d'] = df['return_1d'].rolling(period).std()
|
| 196 |
+
df[f'return_skew_{period}d'] = df['return_1d'].rolling(period).skew()
|
| 197 |
+
df[f'return_kurt_{period}d'] = df['return_1d'].rolling(period).kurt()
|
| 198 |
+
|
| 199 |
+
# Rolling statistics of RSI
|
| 200 |
+
if 'RSI14' in df.columns:
|
| 201 |
+
df['rsi_mean_5d'] = df['RSI14'].rolling(5).mean()
|
| 202 |
+
df['rsi_std_5d'] = df['RSI14'].rolling(5).std()
|
| 203 |
+
|
| 204 |
+
return df
|
| 205 |
+
|
| 206 |
+
# Process each stock separately and then concatenate for ML
|
| 207 |
+
all_ml_data = []
|
| 208 |
+
|
| 209 |
+
for ticker in symbols:
|
| 210 |
+
df = pd.DataFrame({
|
| 211 |
+
'Open': data[f'Open_{ticker}'],
|
| 212 |
+
'High': data[f'High_{ticker}'],
|
| 213 |
+
'Low': data[f'Low_{ticker}'],
|
| 214 |
+
'Close': data[f'Close_{ticker}'],
|
| 215 |
+
'Volume': data[f'Volume_{ticker}']
|
| 216 |
+
})
|
| 217 |
+
|
| 218 |
+
df['SMA20'] = SMA(df['Close'], 20)
|
| 219 |
+
df['SMA50'] = SMA(df['Close'], 50)
|
| 220 |
+
df['EMA20'] = EMA(df['Close'], 20)
|
| 221 |
+
df['EMA50'] = EMA(df['Close'], 50)
|
| 222 |
+
df['RSI14'] = RSI(df['Close'], 14)
|
| 223 |
+
df['RSI20'] = RSI(df['Close'], 20)
|
| 224 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = MACD(df['Close'])
|
| 225 |
+
df = create_volatility_features(df)
|
| 226 |
+
df = create_enhanced_lag_features(df)
|
| 227 |
+
df = create_volume_features(df)
|
| 228 |
+
df = create_momentum_features(df)
|
| 229 |
+
df = create_position_features(df)
|
| 230 |
+
|
| 231 |
+
# Feature: SMA 20 above SMA 50 (bullish crossover)
|
| 232 |
+
df['SMA_crossover'] = (df['SMA20'] > df['SMA50']).astype(int)
|
| 233 |
+
# Feature: RSI oversold signal
|
| 234 |
+
df['RSI_oversold'] = (df['RSI14'] < 30).astype(int)
|
| 235 |
+
# Target: next-day up/down
|
| 236 |
+
df['next_close'] = df['Close'].shift(-1)
|
| 237 |
+
df['target'] = (df['next_close'] > df['Close']).astype(int)
|
| 238 |
+
|
| 239 |
+
df['ticker'] = ticker
|
| 240 |
+
|
| 241 |
+
# Drop rows with NaN (from indicator calculations)
|
| 242 |
+
df = df.dropna().copy()
|
| 243 |
+
all_ml_data.append(df)
|
| 244 |
+
|
| 245 |
+
# Concatenate all stocks
|
| 246 |
+
ml_data = pd.concat(all_ml_data)
|
| 247 |
+
ml_data.reset_index(inplace=True)
|
| 248 |
+
|
| 249 |
+
ml_data
|
| 250 |
+
|
| 251 |
+
ml_data.columns
|
| 252 |
+
|
| 253 |
+
for ticker in ml_data['ticker'].unique():
|
| 254 |
+
plt.figure(figsize=(20,12))
|
| 255 |
+
plt.plot(
|
| 256 |
+
ml_data[ml_data['ticker'] == ticker]['Date'],
|
| 257 |
+
ml_data[ml_data['ticker'] == ticker]['Close'],
|
| 258 |
+
label=f"{ticker}"
|
| 259 |
+
)
|
| 260 |
+
plt.title("Closing Price Over Time")
|
| 261 |
+
plt.xlabel("Date")
|
| 262 |
+
plt.ylabel("Close Price (USD)")
|
| 263 |
+
plt.legend(loc='upper left')
|
| 264 |
+
|
| 265 |
+
plt.show()
|
| 266 |
+
|
| 267 |
+
sample = ml_data[ml_data['ticker'] == 'RELIANCE.NS'].copy()
|
| 268 |
+
plt.figure(figsize=(14,7))
|
| 269 |
+
plt.plot(sample['Date'], sample['Close'], label='Close')
|
| 270 |
+
plt.plot(sample['Date'], sample['SMA20'], label='SMA20')
|
| 271 |
+
plt.plot(sample['Date'], sample['SMA50'], label='SMA50')
|
| 272 |
+
plt.title('RELIANCE: Close with SMA20 & SMA50')
|
| 273 |
+
plt.legend()
|
| 274 |
+
plt.show()
|
| 275 |
+
|
| 276 |
+
plt.figure(figsize=(14,4))
|
| 277 |
+
plt.plot(sample['Date'], sample['RSI14'], label='RSI14', color='green')
|
| 278 |
+
plt.axhline(70, linestyle='--', color='r')
|
| 279 |
+
plt.axhline(30, linestyle='--', color='b')
|
| 280 |
+
plt.title('RELIANCE: RSI14 Time Series')
|
| 281 |
+
plt.legend()
|
| 282 |
+
plt.show()
|
| 283 |
+
|
| 284 |
+
plt.figure(figsize=(14,4))
|
| 285 |
+
plt.plot(sample['Date'], sample['MACD'], label='MACD')
|
| 286 |
+
plt.plot(sample['Date'], sample['MACD_signal'], label='MACD Signal')
|
| 287 |
+
plt.title('RELIANCE: MACD & Signal')
|
| 288 |
+
plt.legend()
|
| 289 |
+
plt.show()
|
| 290 |
+
|
| 291 |
+
# Select features
|
| 292 |
+
features = [
|
| 293 |
+
'Close', 'Volume', 'SMA20', 'SMA50', 'EMA20', 'EMA50',
|
| 294 |
+
'RSI14', 'MACD', 'MACD_signal', 'MACD_hist',
|
| 295 |
+
'SMA_crossover', 'RSI_oversold',
|
| 296 |
+
'return_1d', 'volatility_5d', 'volatility_10d', 'volatility_20d',
|
| 297 |
+
'volatility_30d', 'vol_ratio_5_20', 'vol_ratio_10_20', 'vol_rank_20',
|
| 298 |
+
'vol_rank_50', 'return_lag_1', 'return_lag_2', 'return_lag_3',
|
| 299 |
+
'return_lag_5', 'return_lag_10', 'rsi_lag_1', 'macd_lag_1', 'rsi_lag_2',
|
| 300 |
+
'macd_lag_2', 'rsi_lag_3', 'macd_lag_3', 'volume_sma_10',
|
| 301 |
+
'volume_sma_20', 'volume_sma_50', 'volume_ratio_10', 'volume_ratio_20',
|
| 302 |
+
'volume_ratio_50', 'price_volume', 'pv_sma_5', 'volume_momentum_5',
|
| 303 |
+
'momentum_3d', 'momentum_5d', 'momentum_10d', 'momentum_20d', 'roc_5d',
|
| 304 |
+
'roc_10d', 'high_10d', 'low_10d', 'price_position_10', 'high_20d',
|
| 305 |
+
'low_20d', 'price_position_20', 'high_50d', 'low_50d',
|
| 306 |
+
'price_position_50', 'bb_upper', 'bb_lower', 'bb_position',
|
| 307 |
+
'SMA_crossover', 'RSI_oversold', 'next_close'
|
| 308 |
+
]
|
| 309 |
+
target = 'target'
|
| 310 |
+
|
| 311 |
+
# Standardize features (recommended for LR)
|
| 312 |
+
X = ml_data[features]
|
| 313 |
+
y = ml_data[target]
|
| 314 |
+
|
| 315 |
+
scaler = RobustScaler()
|
| 316 |
+
X_scaled = scaler.fit_transform(X)
|
| 317 |
+
|
| 318 |
+
sns.countplot(x='target', data=ml_data)
|
| 319 |
+
plt.title("Class Balance: Next-Day Up/Down Distribution")
|
| 320 |
+
plt.xlabel("0 = Down, 1 = Up")
|
| 321 |
+
plt.ylabel("Count")
|
| 322 |
+
plt.show()
|
| 323 |
+
|
| 324 |
+
# # Sort by date (if multi-stock, by ticker as well)
|
| 325 |
+
# ml_data = ml_data.sort_values(['ticker', 'Date'])
|
| 326 |
+
|
| 327 |
+
# # Chronological split: 80% train, 20% test
|
| 328 |
+
# split_idx = int(0.8 * len(ml_data))
|
| 329 |
+
# X_train, X_test = X_scaled[:split_idx], X_scaled[split_idx:]
|
| 330 |
+
# y_train, y_test = y[:split_idx], y[split_idx:]
|
| 331 |
+
|
| 332 |
+
from sklearn.model_selection import TimeSeriesSplit
|
| 333 |
+
|
| 334 |
+
tscv = TimeSeriesSplit(n_splits=5)
|
| 335 |
+
|
| 336 |
+
# Use only the last fold for final testing
|
| 337 |
+
for train_index, test_index in tscv.split(X_scaled):
|
| 338 |
+
pass # this will give you the last split
|
| 339 |
+
|
| 340 |
+
X_train, X_test = X_scaled[train_index], X_scaled[test_index]
|
| 341 |
+
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
|
| 342 |
+
|
| 343 |
+
# Hyperparameter tuning
|
| 344 |
+
from sklearn.model_selection import GridSearchCV
|
| 345 |
+
|
| 346 |
+
param_grid = {
|
| 347 |
+
'max_depth': [5, 7, 10, 15],
|
| 348 |
+
'min_samples_split': [2, 5, 10],
|
| 349 |
+
'min_samples_leaf': [1, 2, 4],
|
| 350 |
+
'criterion': ['gini', 'entropy']
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
# Decision Tree Classifier
|
| 354 |
+
dt_model = GridSearchCV(DecisionTreeClassifier(random_state=42), param_grid, cv=tscv, n_jobs=-1)
|
| 355 |
+
dt_model.fit(X_train, y_train)
|
| 356 |
+
dt_preds = dt_model.predict(X_test)
|
| 357 |
+
|
| 358 |
+
# Logistic Regression
|
| 359 |
+
lr_model = LogisticRegression(random_state=42, max_iter=1000)
|
| 360 |
+
lr_model.fit(X_train, y_train)
|
| 361 |
+
lr_preds = lr_model.predict(X_test)
|
| 362 |
+
|
| 363 |
+
# Performance
|
| 364 |
+
def print_metrics(model_name, y_true, y_pred):
|
| 365 |
+
print(f"\n=== {model_name} ===")
|
| 366 |
+
print("Accuracy:", accuracy_score(y_true, y_pred))
|
| 367 |
+
print(classification_report(y_true, y_pred, target_names=['Down','Up']))
|
| 368 |
+
|
| 369 |
+
print_metrics("Decision Tree", y_test, dt_preds)
|
| 370 |
+
print_metrics("Logistic Regression", y_test, lr_preds)
|
| 371 |
+
|
| 372 |
+
# Decision Tree feature importances
|
| 373 |
+
importances = pd.Series(dt_model.best_index_, index=features)
|
| 374 |
+
importances = importances.sort_values(ascending=False)
|
| 375 |
+
print("\nTop Feature Importances (Decision Tree):")
|
| 376 |
+
print(importances)
|
| 377 |
+
|
| 378 |
+
plt.figure(figsize=(35,20))
|
| 379 |
+
corr = ml_data[[
|
| 380 |
+
'Close', 'Volume', 'SMA20', 'SMA50', 'EMA20', 'EMA50',
|
| 381 |
+
'RSI14', 'MACD', 'MACD_signal', 'MACD_hist',
|
| 382 |
+
'SMA_crossover', 'RSI_oversold',
|
| 383 |
+
'return_1d', 'volatility_5d', 'volatility_10d', 'volatility_20d',
|
| 384 |
+
'volatility_30d', 'vol_ratio_5_20', 'vol_ratio_10_20', 'vol_rank_20',
|
| 385 |
+
'vol_rank_50', 'return_lag_1', 'return_lag_2', 'return_lag_3',
|
| 386 |
+
'return_lag_5', 'return_lag_10', 'rsi_lag_1', 'macd_lag_1', 'rsi_lag_2',
|
| 387 |
+
]].corr()
|
| 388 |
+
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0)
|
| 389 |
+
plt.title("Correlation Heatmap of Features")
|
| 390 |
+
plt.show()
|
| 391 |
+
|
| 392 |
+
from sklearn.metrics import confusion_matrix
|
| 393 |
+
import seaborn as sns
|
| 394 |
+
|
| 395 |
+
# Calculate the confusion matrix
|
| 396 |
+
cm = confusion_matrix(y_test, lr_preds)
|
| 397 |
+
|
| 398 |
+
# Display the confusion matrix using a heatmap
|
| 399 |
+
plt.figure(figsize=(8, 6))
|
| 400 |
+
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Down', 'Up'], yticklabels=['Down', 'Up'])
|
| 401 |
+
plt.xlabel('Predicted')
|
| 402 |
+
plt.ylabel('Actual')
|
| 403 |
+
plt.title('Confusion Matrix for LR model')
|
| 404 |
+
plt.show()
|
| 405 |
+
|
| 406 |
+
print("\n=======Confusion Matrix========\n")
|
| 407 |
+
print(cm)
|
| 408 |
+
|
| 409 |
+
from sklearn.metrics import precision_recall_curve, average_precision_score
|
| 410 |
+
|
| 411 |
+
# Get predicted probabilities for the positive class
|
| 412 |
+
y_scores = lr_model.predict_proba(X_test)[:, 1]
|
| 413 |
+
|
| 414 |
+
# Calculate precision and recall for different thresholds
|
| 415 |
+
precision, recall, _ = precision_recall_curve(y_test, y_scores)
|
| 416 |
+
|
| 417 |
+
# Calculate the Average Precision (AP) score
|
| 418 |
+
average_precision = average_precision_score(y_test, y_scores)
|
| 419 |
+
|
| 420 |
+
# Plot the Precision-Recall curve
|
| 421 |
+
plt.figure(figsize=(8, 6))
|
| 422 |
+
plt.plot(recall, precision, color='red', lw=2, label='Precision-Recall curve (AP = %0.2f)' % average_precision)
|
| 423 |
+
plt.xlabel('Recall')
|
| 424 |
+
plt.ylabel('Precision')
|
| 425 |
+
plt.title('Precision-Recall Curve')
|
| 426 |
+
plt.ylim([0.0, 1.05])
|
| 427 |
+
plt.xlim([0.0, 1.0])
|
| 428 |
+
plt.legend(loc="lower left")
|
| 429 |
+
plt.tight_layout()
|
| 430 |
+
plt.show()
|
| 431 |
+
|
| 432 |
+
print(f"\nAverage Precision (AP) for Logistic Regression: {average_precision:.4f}")
|
| 433 |
+
|
| 434 |
+
from sklearn.metrics import roc_curve, auc
|
| 435 |
+
|
| 436 |
+
# Calculate ROC curve
|
| 437 |
+
fpr, tpr, thresholds = roc_curve(y_test, lr_model.predict_proba(X_test)[:,1])
|
| 438 |
+
roc_auc = auc(fpr, tpr)
|
| 439 |
+
|
| 440 |
+
# Plot ROC curve
|
| 441 |
+
plt.figure(figsize=(8, 6))
|
| 442 |
+
plt.plot(fpr, tpr, color='red', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
|
| 443 |
+
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
|
| 444 |
+
plt.xlim([0.0, 1.0])
|
| 445 |
+
plt.ylim([0.0, 1.05])
|
| 446 |
+
plt.xlabel('False Positive Rate')
|
| 447 |
+
plt.ylabel('True Positive Rate')
|
| 448 |
+
plt.title('Receiver Operating Characteristic (ROC) Curve')
|
| 449 |
+
plt.legend(loc="lower right")
|
| 450 |
+
plt.show()
|
| 451 |
+
|
| 452 |
+
print(f"\nAUC for Logistic Regression: {roc_auc:.4f}")
|
| 453 |
+
|
| 454 |
+
from sklearn.metrics import f1_score, classification_report
|
| 455 |
+
|
| 456 |
+
# Calculate F1 score
|
| 457 |
+
f1 = f1_score(y_test, lr_preds)
|
| 458 |
+
print(f"\nF1 Score for Logistic Regression: {f1:.4f}")
|
| 459 |
+
|
| 460 |
+
# Print classification report
|
| 461 |
+
print("\nClassification Report for Logistic Regression:")
|
| 462 |
+
print(classification_report(y_test, lr_preds, target_names=['Down', 'Up']))
|
| 463 |
+
|
| 464 |
+
# Calculate training accuracy for Logistic Regression
|
| 465 |
+
lr_train_accuracy = lr_model.score(X_train, y_train)
|
| 466 |
+
print(f"\nLogistic Regression Training Accuracy: {lr_train_accuracy:.4f}")
|
| 467 |
+
print(f"Logistic Regression Test Accuracy: {accuracy_score(y_test, lr_preds):.4f}")
|
| 468 |
+
|
| 469 |
+
import pickle
|
| 470 |
+
|
| 471 |
+
# Save the Logistic Regression model
|
| 472 |
+
filename = 'logistic_regression_model.pkl'
|
| 473 |
+
pickle.dump(lr_model, open(filename, 'wb'))
|
| 474 |
+
|
| 475 |
+
print(f"Logistic Regression model saved to {filename}")
|
| 476 |
+
|
| 477 |
+
import pickle
|
| 478 |
+
|
| 479 |
+
# Load the saved Logistic Regression model
|
| 480 |
+
filename = 'logistic_regression_model.pkl'
|
| 481 |
+
loaded_model = pickle.load(open(filename, 'rb'))
|
| 482 |
+
|
| 483 |
+
print(f"Logistic Regression model loaded from {filename}")
|
| 484 |
+
|
| 485 |
+
# Test the loaded model
|
| 486 |
+
loaded_preds = loaded_model.predict(X_test)
|
| 487 |
+
|
| 488 |
+
# Evaluate the loaded model's performance
|
| 489 |
+
print("\n=== Loaded Logistic Regression Model Performance ===")
|
| 490 |
+
print("\n Accuracy:", accuracy_score(y_test, loaded_preds)," \n")
|
| 491 |
+
print(classification_report(y_test, loaded_preds, target_names=['Down','Up']))
|
src/models/logistic_regression_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:919c661816bdbe075897c7f6497a39463fc75a28d24fd9d61698578c0489f1a5
|
| 3 |
+
size 1200
|
src/models/ml_model.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Placeholder for ml_model.py
|
src/models/scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:04a408395df58f2d61dabfe3131509fe00170012806d34248a095b1446910404
|
| 3 |
+
size 2254
|
src/requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Placeholder for requirements.txt
|
| 2 |
+
|
| 3 |
+
pandas
|
| 4 |
+
numpy
|
| 5 |
+
matplotlib
|
| 6 |
+
yfinance
|
| 7 |
+
streamlit
|
| 8 |
+
|
| 9 |
+
gspread
|
| 10 |
+
google-auth
|
| 11 |
+
google-auth-oauthlib
|
| 12 |
+
google-auth-httplib2
|
src/script.ps1
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# create_project_structure.ps1
|
| 2 |
+
|
| 3 |
+
# Root directory
|
| 4 |
+
$root = "src"
|
| 5 |
+
|
| 6 |
+
# Define folders
|
| 7 |
+
$folders = @(
|
| 8 |
+
"$root\data\historical",
|
| 9 |
+
"$root\indicators",
|
| 10 |
+
"$root\models",
|
| 11 |
+
"$root\strategy",
|
| 12 |
+
"$root\utils"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Define files
|
| 16 |
+
$files = @(
|
| 17 |
+
"$root\main.py",
|
| 18 |
+
"$root\config.py",
|
| 19 |
+
"$root\requirements.txt",
|
| 20 |
+
"$root\README.md",
|
| 21 |
+
"$root\indicators\sma.py",
|
| 22 |
+
"$root\indicators\ema.py",
|
| 23 |
+
"$root\indicators\rsi.py",
|
| 24 |
+
"$root\indicators\enhanced_features.py",
|
| 25 |
+
"$root\indicators\macd.py",
|
| 26 |
+
"$root\models\ml_model.py",
|
| 27 |
+
"$root\strategy\rule_based_strategy.py",
|
| 28 |
+
"$root\utils\data_loader.py",
|
| 29 |
+
"$root\utils\logger.py",
|
| 30 |
+
"$root\utils\backtester.py"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Create folders
|
| 34 |
+
foreach ($folder in $folders) {
|
| 35 |
+
if (-Not (Test-Path -Path $folder)) {
|
| 36 |
+
New-Item -ItemType Directory -Path $folder | Out-Null
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Create files with placeholder content
|
| 41 |
+
foreach ($file in $files) {
|
| 42 |
+
if (-Not (Test-Path -Path $file)) {
|
| 43 |
+
New-Item -ItemType File -Path $file | Out-Null
|
| 44 |
+
Add-Content -Path $file -Value "# Placeholder for $([System.IO.Path]::GetFileName($file))"
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
Write-Host "Project structure created successfully under $root"
|
| 49 |
+
# End of script
|
| 50 |
+
# Save this script as create_project_structure.ps1 and run it in PowerShell to create the project structure.
|
| 51 |
+
# Ensure you have the necessary permissions to create directories and files in the specified location.
|
src/strategy/rule_based_strategy.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# strategy/rule_based_strategy.py
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Generates buy/sell signals based on:
|
| 5 |
+
- Buy when RSI < 30 and SMA20 crosses above SMA50
|
| 6 |
+
- Sell when RSI > 70 and SMA20 crosses below SMA50
|
| 7 |
+
|
| 8 |
+
Returns:
|
| 9 |
+
pd.DataFrame: DataFrame with new 'Signal' column: 1 = Buy, -1 = Sell, 0 = Hold
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import pandas as pd
|
| 13 |
+
|
| 14 |
+
def generate_signals_sma(df, rsi_col='RSI', sma_short_col='SMA20', sma_long_col='SMA50'):
|
| 15 |
+
df = df.copy()
|
| 16 |
+
df['SMA_Signal'] = 0
|
| 17 |
+
|
| 18 |
+
# Method 1: Use OR condition (either RSI or SMA crossover)
|
| 19 |
+
# Buy condition: RSI < 30 OR SMA20 crosses above SMA50
|
| 20 |
+
rsi_oversold = df[rsi_col] < 30
|
| 21 |
+
sma_bullish_cross = (df[sma_short_col].shift(1) < df[sma_long_col].shift(1)) & (df[sma_short_col] > df[sma_long_col])
|
| 22 |
+
buy_signal = rsi_oversold | sma_bullish_cross
|
| 23 |
+
df.loc[buy_signal, 'SMA_Signal'] = 1
|
| 24 |
+
|
| 25 |
+
# Sell condition: RSI > 70 OR SMA20 crosses below SMA50
|
| 26 |
+
rsi_overbought = df[rsi_col] > 70
|
| 27 |
+
sma_bearish_cross = (df[sma_short_col].shift(1) > df[sma_long_col].shift(1)) & (df[sma_short_col] < df[sma_long_col])
|
| 28 |
+
sell_signal = rsi_overbought | sma_bearish_cross
|
| 29 |
+
df.loc[sell_signal, 'SMA_Signal'] = -1
|
| 30 |
+
|
| 31 |
+
return df
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def generate_signals_ema(df, rsi_col='RSI', ema_short_col='EMA20', ema_long_col='EMA50'):
|
| 35 |
+
df = df.copy()
|
| 36 |
+
df['EMA_Signal'] = 0
|
| 37 |
+
|
| 38 |
+
# Method 1: Use OR condition (either RSI or EMA crossover)
|
| 39 |
+
# Buy condition: RSI < 30 OR EMA20 crosses above EMA50
|
| 40 |
+
rsi_oversold = df[rsi_col] < 30
|
| 41 |
+
ema_bullish_cross = (df[ema_short_col].shift(1) < df[ema_long_col].shift(1)) & (df[ema_short_col] > df[ema_long_col])
|
| 42 |
+
buy_signal = rsi_oversold | ema_bullish_cross
|
| 43 |
+
df.loc[buy_signal, 'EMA_Signal'] = 1
|
| 44 |
+
|
| 45 |
+
# Sell condition: RSI > 70 OR EMA20 crosses below EMA50
|
| 46 |
+
rsi_overbought = df[rsi_col] > 70
|
| 47 |
+
ema_bearish_cross = (df[ema_short_col].shift(1) > df[ema_long_col].shift(1)) & (df[ema_short_col] < df[ema_long_col])
|
| 48 |
+
sell_signal = rsi_overbought | ema_bearish_cross
|
| 49 |
+
df.loc[sell_signal, 'EMA_Signal'] = -1
|
| 50 |
+
|
| 51 |
+
return df
|
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,808 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
| 11 |
-
forums](https://discuss.streamlit.io).
|
| 12 |
-
|
| 13 |
-
In the meantime, below is an example of what you can do with just a few lines of code:
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
| 17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
| 18 |
-
|
| 19 |
-
indices = np.linspace(0, 1, num_points)
|
| 20 |
-
theta = 2 * np.pi * num_turns * indices
|
| 21 |
-
radius = indices
|
| 22 |
-
|
| 23 |
-
x = radius * np.cos(theta)
|
| 24 |
-
y = radius * np.sin(theta)
|
| 25 |
-
|
| 26 |
-
df = pd.DataFrame({
|
| 27 |
-
"x": x,
|
| 28 |
-
"y": y,
|
| 29 |
-
"idx": indices,
|
| 30 |
-
"rand": np.random.randn(num_points),
|
| 31 |
-
})
|
| 32 |
-
|
| 33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
| 34 |
-
.mark_point(filled=True)
|
| 35 |
-
.encode(
|
| 36 |
-
x=alt.X("x", axis=None),
|
| 37 |
-
y=alt.Y("y", axis=None),
|
| 38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
| 39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
| 40 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.graph_objects as go
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
import yfinance as yf
|
| 8 |
+
import pickle
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import warnings
|
| 11 |
+
from curl_cffi import requests
|
| 12 |
+
session = requests.Session(impersonate="chrome")
|
| 13 |
+
|
| 14 |
+
from indicators.rsi import rsi
|
| 15 |
+
from indicators.sma import sma
|
| 16 |
+
from indicators.ema import ema
|
| 17 |
+
from indicators.macd import macd
|
| 18 |
+
|
| 19 |
+
from strategy.rule_based_strategy import generate_signals_sma, generate_signals_ema
|
| 20 |
+
from utils.backtester import backtest_signals
|
| 21 |
+
|
| 22 |
+
from indicators.enhanced_features import (
|
| 23 |
+
create_volatility_features, create_enhanced_lag_features,
|
| 24 |
+
create_volume_features, create_momentum_features, create_position_features
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Suppress warnings
|
| 28 |
+
warnings.filterwarnings('ignore')
|
| 29 |
+
|
| 30 |
+
# Page config
|
| 31 |
+
st.set_page_config(
|
| 32 |
+
page_title="Complete Stock Trading & Prediction Platform",
|
| 33 |
+
page_icon="📈",
|
| 34 |
+
layout="wide"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Stock symbols
|
| 38 |
+
STOCK_SYMBOLS = [
|
| 39 |
+
'ADANIENT.NS', 'ADANIPORTS.NS', 'APOLLOHOSP.NS', 'ASIANPAINT.NS',
|
| 40 |
+
'AXISBANK.NS', 'BAJAJ-AUTO.NS', 'BAJFINANCE.NS', 'BAJAJFINSV.NS',
|
| 41 |
+
'BEL.NS', 'BHARTIARTL.NS', 'CIPLA.NS', 'COALINDIA.NS', 'DRREDDY.NS',
|
| 42 |
+
'EICHERMOT.NS', 'GRASIM.NS', 'HCLTECH.NS', 'HDFCBANK.NS', 'HDFCLIFE.NS',
|
| 43 |
+
'HEROMOTOCO.NS', 'HINDALCO.NS', 'HINDUNILVR.NS', 'ICICIBANK.NS',
|
| 44 |
+
'INDUSINDBK.NS', 'INFY.NS', 'ITC.NS', 'JIOFIN.NS', 'JSWSTEEL.NS',
|
| 45 |
+
'KOTAKBANK.NS', 'LT.NS', 'M&M.NS', 'MARUTI.NS', 'NESTLEIND.NS',
|
| 46 |
+
'NTPC.NS', 'ONGC.NS', 'POWERGRID.NS', 'RELIANCE.NS', 'SBILIFE.NS',
|
| 47 |
+
'SHRIRAMFIN.NS', 'SBIN.NS', 'SUNPHARMA.NS', 'TATACONSUM.NS', 'TCS.NS',
|
| 48 |
+
'TATAMOTORS.NS', 'TATASTEEL.NS', 'TECHM.NS', 'TITAN.NS', 'TRENT.NS',
|
| 49 |
+
'ULTRACEMCO.NS', 'WIPRO.NS', 'ETERNAL.NS'
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
# Feature list for ML model
|
| 53 |
+
FEATURES = [
|
| 54 |
+
'Close', 'Volume', 'SMA20', 'SMA50', 'EMA20', 'EMA50',
|
| 55 |
+
'RSI14', 'MACD', 'MACD_signal', 'MACD_hist',
|
| 56 |
+
'SMA_crossover', 'RSI_oversold',
|
| 57 |
+
'return_1d', 'volatility_5d', 'volatility_10d', 'volatility_20d',
|
| 58 |
+
'volatility_30d', 'vol_ratio_5_20', 'vol_ratio_10_20', 'vol_rank_20',
|
| 59 |
+
'vol_rank_50', 'return_lag_1', 'return_lag_2', 'return_lag_3',
|
| 60 |
+
'return_lag_5', 'return_lag_10', 'rsi_lag_1', 'macd_lag_1', 'rsi_lag_2',
|
| 61 |
+
'macd_lag_2', 'rsi_lag_3', 'macd_lag_3', 'volume_sma_10',
|
| 62 |
+
'volume_sma_20', 'volume_sma_50', 'volume_ratio_10', 'volume_ratio_20',
|
| 63 |
+
'volume_ratio_50', 'price_volume', 'pv_sma_5', 'volume_momentum_5',
|
| 64 |
+
'momentum_3d', 'momentum_5d', 'momentum_10d', 'momentum_20d', 'roc_5d',
|
| 65 |
+
'roc_10d', 'high_10d', 'low_10d', 'price_position_10', 'high_20d',
|
| 66 |
+
'low_20d', 'price_position_20', 'high_50d', 'low_50d',
|
| 67 |
+
'price_position_50', 'bb_upper', 'bb_lower', 'bb_position', 'target'
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
# ========================= SHARED FUNCTIONS =========================
|
| 71 |
+
|
| 72 |
+
@st.cache_data
|
| 73 |
+
def load_stock_data(symbol, start_date, end_date):
|
| 74 |
+
"""Load stock data from Yahoo Finance"""
|
| 75 |
+
try:
|
| 76 |
+
data = yf.download(symbol, start=start_date, end=end_date, session=session)
|
| 77 |
+
# Flatten the MultiIndex columns
|
| 78 |
+
if data.columns.nlevels > 1:
|
| 79 |
+
data.columns = [col[0] for col in data.columns]
|
| 80 |
+
return data
|
| 81 |
+
except Exception as e:
|
| 82 |
+
st.error(f"Error loading data: {e}")
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def process_stock_data(df, short_period, long_period, rsi_period):
|
| 86 |
+
"""Process stock data to create all features"""
|
| 87 |
+
df = df.copy()
|
| 88 |
+
|
| 89 |
+
# Basic technical indicators
|
| 90 |
+
df['SMA20'] = sma(df, short_period)
|
| 91 |
+
df['SMA50'] = sma(df, long_period)
|
| 92 |
+
df['EMA20'] = ema(df, short_period)
|
| 93 |
+
df['EMA50'] = ema(df, long_period)
|
| 94 |
+
df['RSI14'] = rsi(df, rsi_period)
|
| 95 |
+
df['RSI20'] = rsi(df, rsi_period + 6)
|
| 96 |
+
df['MACD'], df['MACD_signal'], df['MACD_hist'] = macd(df)
|
| 97 |
+
|
| 98 |
+
# Bollinger Bands
|
| 99 |
+
df['Upper_Band'] = df['SMA20'] + 2 * df['Close'].rolling(window=20).std()
|
| 100 |
+
df['Lower_Band'] = df['SMA20'] - 2 * df['Close'].rolling(window=20).std()
|
| 101 |
+
|
| 102 |
+
# Create feature sets
|
| 103 |
+
df = create_volatility_features(df)
|
| 104 |
+
df = create_enhanced_lag_features(df)
|
| 105 |
+
df = create_volume_features(df)
|
| 106 |
+
df = create_momentum_features(df)
|
| 107 |
+
df = create_position_features(df)
|
| 108 |
+
|
| 109 |
+
# Additional features
|
| 110 |
+
df['SMA_crossover'] = (df['SMA20'] > df['SMA50']).astype(int)
|
| 111 |
+
df['RSI_oversold'] = (df['RSI14'] < 30).astype(int)
|
| 112 |
+
|
| 113 |
+
# Target: next-day up/down
|
| 114 |
+
df['next_close'] = df['Close'].shift(-1)
|
| 115 |
+
df['target'] = (df['next_close'] > df['Close']).astype(int)
|
| 116 |
+
|
| 117 |
+
return df
|
| 118 |
+
|
| 119 |
+
# ========================= MAIN APPLICATION =========================
|
| 120 |
+
|
| 121 |
+
# Main navigation
|
| 122 |
+
st.title("📈 Stock Trading & Prediction Platform")
|
| 123 |
+
|
| 124 |
+
# Navigation tabs
|
| 125 |
+
tab1, tab2 = st.tabs(["🔮 Price Prediction", "📊 Trading Dashboard"])
|
| 126 |
+
|
| 127 |
+
# ========================= SIDEBAR CONFIGURATION =========================
|
| 128 |
+
|
| 129 |
+
st.sidebar.header("📊 Configuration")
|
| 130 |
+
|
| 131 |
+
# Common inputs
|
| 132 |
+
selected_stock = st.sidebar.selectbox("Select Stock Symbol", STOCK_SYMBOLS, index=35)
|
| 133 |
+
start_date = st.sidebar.date_input("Start Date", value=datetime(2020, 1, 1))
|
| 134 |
+
end_date = st.sidebar.date_input("End Date", value=datetime.now())
|
| 135 |
+
|
| 136 |
+
st.sidebar.subheader("📈 Technical Indicators")
|
| 137 |
+
rsi_period = st.sidebar.slider("RSI Period", min_value=5, max_value=30, value=14, step=1)
|
| 138 |
+
short_period = st.sidebar.slider("Short-term Period", min_value=5, max_value=50, value=20, step=1)
|
| 139 |
+
long_period = st.sidebar.slider("Long-term Period", min_value=50, max_value=200, value=50, step=1)
|
| 140 |
+
|
| 141 |
+
# Strategy selection (for trading dashboard)
|
| 142 |
+
strategy_type = st.sidebar.selectbox("Strategy Type", ["SMA-based", "EMA-based", "Both"])
|
| 143 |
+
|
| 144 |
+
st.sidebar.subheader("💰 Backtesting Parameters")
|
| 145 |
+
initial_cash = st.sidebar.number_input("Initial Capital (₹)", min_value=10000, value=100000, step=10000)
|
| 146 |
+
transaction_cost = st.sidebar.slider("Transaction Cost (%)", 0.0, 1.0, 0.1, step=0.05) / 100
|
| 147 |
+
stop_loss = st.sidebar.slider("Stop Loss (%)", 0.0, 20.0, 5.0, step=1.0) / 100
|
| 148 |
+
take_profit = st.sidebar.slider("Take Profit (%)", 0.0, 50.0, 15.0, step=5.0) / 100
|
| 149 |
+
use_risk_mgmt = st.sidebar.checkbox("Enable Risk Management", value=True)
|
| 150 |
+
|
| 151 |
+
# ========================= PRICE PREDICTION TAB =========================
|
| 152 |
+
|
| 153 |
+
with tab1:
|
| 154 |
+
st.header(f"🔮 Price Prediction for {selected_stock}")
|
| 155 |
+
|
| 156 |
+
with st.spinner("Loading stock data..."):
|
| 157 |
+
stock_data = load_stock_data(selected_stock, start_date, end_date)
|
| 158 |
+
|
| 159 |
+
if stock_data is not None and not stock_data.empty:
|
| 160 |
+
# Display sample data
|
| 161 |
+
st.subheader("📊 Latest Stock Data")
|
| 162 |
+
st.dataframe(stock_data.tail(10), use_container_width=True)
|
| 163 |
+
|
| 164 |
+
# Process the data
|
| 165 |
+
processed_data = process_stock_data(stock_data, short_period, long_period, rsi_period)
|
| 166 |
+
processed_data = processed_data.dropna()
|
| 167 |
+
|
| 168 |
+
if len(processed_data) > 0:
|
| 169 |
+
# Get the latest row for prediction
|
| 170 |
+
latest_data = processed_data.iloc[-1]
|
| 171 |
+
|
| 172 |
+
# Display current stock info
|
| 173 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 174 |
+
with col1:
|
| 175 |
+
st.metric("Current Price", f"₹{latest_data['Close']:.2f}")
|
| 176 |
+
with col2:
|
| 177 |
+
daily_change = ((latest_data['Close'] - processed_data.iloc[-2]['Close']) / processed_data.iloc[-2]['Close']) * 100
|
| 178 |
+
st.metric("Daily Change", f"{daily_change:.2f}%")
|
| 179 |
+
with col3:
|
| 180 |
+
st.metric("Volume", f"{latest_data['Volume']:,.0f}")
|
| 181 |
+
with col4:
|
| 182 |
+
st.metric("RSI14", f"{latest_data['RSI14']:.2f}")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
model = pickle.load(open('models/logistic_regression_model.pkl', 'rb'))
|
| 186 |
+
scaler = pickle.load(open('models/scaler.pkl', 'rb'))
|
| 187 |
+
|
| 188 |
+
# Create feature vector
|
| 189 |
+
feature_vector = latest_data[FEATURES].values.reshape(1, -1)
|
| 190 |
+
feature_vector_scaled = scaler.transform(feature_vector)
|
| 191 |
+
|
| 192 |
+
# Make prediction
|
| 193 |
+
prediction = model.predict(feature_vector_scaled)[0]
|
| 194 |
+
probability = model.predict_proba(feature_vector_scaled)[0].max()
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# Display prediction
|
| 198 |
+
st.header("🔮 Prediction Results")
|
| 199 |
+
col1, col2 = st.columns(2)
|
| 200 |
+
|
| 201 |
+
with col1:
|
| 202 |
+
if prediction == 1:
|
| 203 |
+
st.success("📈 **PREDICTION: UP**")
|
| 204 |
+
st.write(f"The model predicts the stock will go **UP** tomorrow with {probability:.1%} confidence.")
|
| 205 |
+
else:
|
| 206 |
+
st.error("📉 **PREDICTION: DOWN**")
|
| 207 |
+
st.write(f"The model predicts the stock will go **DOWN** tomorrow with {probability:.1%} confidence.")
|
| 208 |
+
|
| 209 |
+
with col2:
|
| 210 |
+
# Confidence gauge
|
| 211 |
+
fig_gauge = go.Figure(go.Indicator(
|
| 212 |
+
mode = "gauge+number",
|
| 213 |
+
value = probability * 100,
|
| 214 |
+
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 215 |
+
title = {'text': "Confidence %"},
|
| 216 |
+
gauge = {
|
| 217 |
+
'axis': {'range': [None, 100]},
|
| 218 |
+
'bar': {'color': "darkgreen" if prediction == 1 else "darkred"},
|
| 219 |
+
'steps': [
|
| 220 |
+
{'range': [0, 50], 'color': "lightgray"},
|
| 221 |
+
{'range': [50, 80], 'color': "yellow"},
|
| 222 |
+
{'range': [80, 100], 'color': "lightgreen"}
|
| 223 |
+
],
|
| 224 |
+
'threshold': {
|
| 225 |
+
'line': {'color': "red", 'width': 4},
|
| 226 |
+
'thickness': 0.75,
|
| 227 |
+
'value': 90
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
))
|
| 231 |
+
fig_gauge.update_layout(height=300)
|
| 232 |
+
st.plotly_chart(fig_gauge, use_container_width=True)
|
| 233 |
+
|
| 234 |
+
# Technical Analysis Charts
|
| 235 |
+
st.header("📈 Technical Analysis")
|
| 236 |
+
|
| 237 |
+
# Price charts
|
| 238 |
+
col1, col2 = st.columns(2)
|
| 239 |
+
|
| 240 |
+
with col1:
|
| 241 |
+
# SMA Chart
|
| 242 |
+
fig_sma = go.Figure()
|
| 243 |
+
fig_sma.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['Close'][-60:],
|
| 244 |
+
mode='lines', name='Close Price', line=dict(color='blue', width=2)))
|
| 245 |
+
fig_sma.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['SMA20'][-60:],
|
| 246 |
+
mode='lines', name='SMA20', line=dict(color='orange', width=1)))
|
| 247 |
+
fig_sma.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['SMA50'][-60:],
|
| 248 |
+
mode='lines', name='SMA50', line=dict(color='red', width=1)))
|
| 249 |
+
fig_sma.update_layout(title=f"{selected_stock} - Simple Moving Averages", height=400)
|
| 250 |
+
st.plotly_chart(fig_sma, use_container_width=True)
|
| 251 |
+
|
| 252 |
+
with col2:
|
| 253 |
+
# EMA Chart
|
| 254 |
+
fig_ema = go.Figure()
|
| 255 |
+
fig_ema.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['Close'][-60:],
|
| 256 |
+
mode='lines', name='Close Price', line=dict(color='blue', width=2)))
|
| 257 |
+
fig_ema.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['EMA20'][-60:],
|
| 258 |
+
mode='lines', name='EMA20', line=dict(color='orange', width=1)))
|
| 259 |
+
fig_ema.add_trace(go.Scatter(x=processed_data.index[-60:], y=processed_data['EMA50'][-60:],
|
| 260 |
+
mode='lines', name='EMA50', line=dict(color='red', width=1)))
|
| 261 |
+
fig_ema.update_layout(title=f"{selected_stock} - Exponential Moving Averages", height=400)
|
| 262 |
+
st.plotly_chart(fig_ema, use_container_width=True)
|
| 263 |
+
|
| 264 |
+
# RSI and MACD
|
| 265 |
+
col1, col2 = st.columns(2)
|
| 266 |
+
|
| 267 |
+
with col1:
|
| 268 |
+
fig_rsi = go.Figure()
|
| 269 |
+
fig_rsi.add_trace(go.Scatter(x=processed_data.index[-30:], y=processed_data['RSI14'][-30:],
|
| 270 |
+
mode='lines', name='RSI14', line=dict(color='purple')))
|
| 271 |
+
fig_rsi.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="Overbought")
|
| 272 |
+
fig_rsi.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="Oversold")
|
| 273 |
+
fig_rsi.update_layout(title=f"RSI ({rsi_period}-day)", height=300)
|
| 274 |
+
st.plotly_chart(fig_rsi, use_container_width=True)
|
| 275 |
+
|
| 276 |
+
with col2:
|
| 277 |
+
fig_macd = go.Figure()
|
| 278 |
+
fig_macd.add_trace(go.Scatter(x=processed_data.index[-30:], y=processed_data['MACD'][-30:],
|
| 279 |
+
mode='lines', name='MACD', line=dict(color='blue')))
|
| 280 |
+
fig_macd.add_trace(go.Scatter(x=processed_data.index[-30:], y=processed_data['MACD_signal'][-30:],
|
| 281 |
+
mode='lines', name='Signal', line=dict(color='red')))
|
| 282 |
+
fig_macd.update_layout(title="MACD", height=300)
|
| 283 |
+
st.plotly_chart(fig_macd, use_container_width=True)
|
| 284 |
+
|
| 285 |
+
else:
|
| 286 |
+
st.error("Not enough data to make a prediction.")
|
| 287 |
+
else:
|
| 288 |
+
st.error("Unable to load stock data.")
|
| 289 |
+
|
| 290 |
+
# ========================= TRADING DASHBOARD TAB =========================
|
| 291 |
+
|
| 292 |
+
with tab2:
|
| 293 |
+
st.header("📊 Trading Dashboard")
|
| 294 |
+
|
| 295 |
+
with st.spinner(f'Loading data for {selected_stock}...'):
|
| 296 |
+
df = load_stock_data(selected_stock, start_date, end_date)
|
| 297 |
+
|
| 298 |
+
if df is not None and not df.empty:
|
| 299 |
+
st.subheader(f"📊 Stock Data for {selected_stock}")
|
| 300 |
+
st.write(f"**Date Range:** {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
| 301 |
+
st.write(f"**Total Records:** {len(df)} days")
|
| 302 |
+
|
| 303 |
+
# Process data for trading
|
| 304 |
+
df = process_stock_data(df, short_period, long_period, rsi_period)
|
| 305 |
+
df = df.dropna()
|
| 306 |
+
|
| 307 |
+
# Generate trading signals
|
| 308 |
+
if strategy_type in ["SMA-based", "Both"]:
|
| 309 |
+
df = generate_signals_sma(df, rsi_col='RSI14', sma_short_col='SMA20', sma_long_col='SMA50')
|
| 310 |
+
|
| 311 |
+
if strategy_type in ["EMA-based", "Both"]:
|
| 312 |
+
df = generate_signals_ema(df, rsi_col='RSI14', ema_short_col='EMA20', ema_long_col='EMA50')
|
| 313 |
+
|
| 314 |
+
# Initialize variables to avoid NameError
|
| 315 |
+
results = None
|
| 316 |
+
metrics = None
|
| 317 |
+
signal_col = None
|
| 318 |
+
strategy_name = None
|
| 319 |
+
|
| 320 |
+
# Backtesting section
|
| 321 |
+
st.header("🔍 Backtesting Results")
|
| 322 |
+
|
| 323 |
+
if strategy_type == "Both":
|
| 324 |
+
tab_sma, tab_ema = st.tabs(["SMA Strategy", "EMA Strategy"])
|
| 325 |
+
|
| 326 |
+
with tab_sma:
|
| 327 |
+
st.subheader("📊 SMA Strategy Results")
|
| 328 |
+
sma_results, sma_metrics = backtest_signals(
|
| 329 |
+
df, signal_col='SMA_Signal', price_col='Close',
|
| 330 |
+
initial_cash=initial_cash, transaction_cost=transaction_cost if use_risk_mgmt else 0
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
# Set variables for common sections
|
| 334 |
+
results = sma_results
|
| 335 |
+
metrics = sma_metrics
|
| 336 |
+
signal_col = 'SMA_Signal'
|
| 337 |
+
strategy_name = 'SMA'
|
| 338 |
+
|
| 339 |
+
# Display metrics
|
| 340 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 341 |
+
with col1:
|
| 342 |
+
st.metric("💰 Final Value", sma_metrics['Final Portfolio Value'])
|
| 343 |
+
st.metric("📈 Total Return", sma_metrics['Total Return'])
|
| 344 |
+
with col2:
|
| 345 |
+
st.metric("🎯 Buy & Hold Return", sma_metrics['Buy & Hold Return'])
|
| 346 |
+
st.metric("📊 Total Trades", sma_metrics['Total Trades'])
|
| 347 |
+
with col3:
|
| 348 |
+
st.metric("🏆 Win Rate", sma_metrics['Win Rate'])
|
| 349 |
+
st.metric("⚡ Sharpe Ratio", sma_metrics['Sharpe Ratio'])
|
| 350 |
+
with col4:
|
| 351 |
+
st.metric("📉 Max Drawdown", sma_metrics['Maximum Drawdown'])
|
| 352 |
+
st.metric("🔥 Volatility", sma_metrics['Volatility (Annual)'])
|
| 353 |
+
|
| 354 |
+
# SMA Price Chart with Signals
|
| 355 |
+
fig_sma_signals = go.Figure()
|
| 356 |
+
fig_sma_signals.add_trace(go.Scatter(x=df.index, y=df['Close'], mode='lines',
|
| 357 |
+
name='Close Price', line=dict(color='purple', width=2)))
|
| 358 |
+
fig_sma_signals.add_trace(go.Scatter(x=df.index, y=df['SMA20'], mode='lines',
|
| 359 |
+
name='SMA20', line=dict(color='blue', width=2)))
|
| 360 |
+
fig_sma_signals.add_trace(go.Scatter(x=df.index, y=df['SMA50'], mode='lines',
|
| 361 |
+
name='SMA50', line=dict(color='red', width=2)))
|
| 362 |
+
|
| 363 |
+
# Add buy/sell signals
|
| 364 |
+
buy_signals = df[df['SMA_Signal'] == 1]
|
| 365 |
+
sell_signals = df[df['SMA_Signal'] == -1]
|
| 366 |
+
|
| 367 |
+
if not buy_signals.empty:
|
| 368 |
+
fig_sma_signals.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'],
|
| 369 |
+
mode='markers', name='Buy Signal',
|
| 370 |
+
marker=dict(symbol='triangle-up', size=12, color='green')))
|
| 371 |
+
|
| 372 |
+
if not sell_signals.empty:
|
| 373 |
+
fig_sma_signals.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'],
|
| 374 |
+
mode='markers', name='Sell Signal',
|
| 375 |
+
marker=dict(symbol='triangle-down', size=12, color='red')))
|
| 376 |
+
|
| 377 |
+
fig_sma_signals.update_layout(title=f"{selected_stock} - SMA Strategy Signals", height=500)
|
| 378 |
+
st.plotly_chart(fig_sma_signals, use_container_width=True)
|
| 379 |
+
|
| 380 |
+
# Portfolio Performance
|
| 381 |
+
buy_hold_value = initial_cash * (df['Close'] / df['Close'].iloc[0])
|
| 382 |
+
fig_perf_sma = go.Figure()
|
| 383 |
+
fig_perf_sma.add_trace(go.Scatter(x=sma_results.index, y=sma_results['Total'],
|
| 384 |
+
mode='lines', name='SMA Strategy', line=dict(color='green', width=3)))
|
| 385 |
+
fig_perf_sma.add_trace(go.Scatter(x=df.index, y=buy_hold_value,
|
| 386 |
+
mode='lines', name='Buy & Hold', line=dict(color='blue', width=2, dash='dash')))
|
| 387 |
+
fig_perf_sma.update_layout(title="SMA Strategy vs Buy & Hold Performance", height=400)
|
| 388 |
+
st.plotly_chart(fig_perf_sma, use_container_width=True)
|
| 389 |
+
|
| 390 |
+
with tab_ema:
|
| 391 |
+
st.subheader("📊 EMA Strategy Results")
|
| 392 |
+
ema_results, ema_metrics = backtest_signals(
|
| 393 |
+
df, signal_col='EMA_Signal', price_col='Close',
|
| 394 |
+
initial_cash=initial_cash, transaction_cost=transaction_cost if use_risk_mgmt else 0
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
# Set variables for common sections
|
| 398 |
+
results = ema_results
|
| 399 |
+
metrics = ema_metrics
|
| 400 |
+
signal_col = 'EMA_Signal'
|
| 401 |
+
strategy_name = 'EMA'
|
| 402 |
+
|
| 403 |
+
# Display metrics
|
| 404 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 405 |
+
with col1:
|
| 406 |
+
st.metric("💰 Final Value", ema_metrics['Final Portfolio Value'])
|
| 407 |
+
st.metric("📈 Total Return", ema_metrics['Total Return'])
|
| 408 |
+
with col2:
|
| 409 |
+
st.metric("🎯 Buy & Hold Return", ema_metrics['Buy & Hold Return'])
|
| 410 |
+
st.metric("📊 Total Trades", ema_metrics['Total Trades'])
|
| 411 |
+
with col3:
|
| 412 |
+
st.metric("🏆 Win Rate", ema_metrics['Win Rate'])
|
| 413 |
+
st.metric("⚡ Sharpe Ratio", ema_metrics['Sharpe Ratio'])
|
| 414 |
+
with col4:
|
| 415 |
+
st.metric("📉 Max Drawdown", ema_metrics['Maximum Drawdown'])
|
| 416 |
+
st.metric("🔥 Volatility", ema_metrics['Volatility (Annual)'])
|
| 417 |
+
|
| 418 |
+
# EMA Price Chart with Signals
|
| 419 |
+
fig_ema_signals = go.Figure()
|
| 420 |
+
fig_ema_signals.add_trace(go.Scatter(x=df.index, y=df['Close'], mode='lines',
|
| 421 |
+
name='Close Price', line=dict(color='purple', width=2)))
|
| 422 |
+
fig_ema_signals.add_trace(go.Scatter(x=df.index, y=df['EMA20'], mode='lines',
|
| 423 |
+
name='EMA20', line=dict(color='blue', width=2)))
|
| 424 |
+
fig_ema_signals.add_trace(go.Scatter(x=df.index, y=df['EMA50'], mode='lines',
|
| 425 |
+
name='EMA50', line=dict(color='red', width=2)))
|
| 426 |
+
|
| 427 |
+
# Add buy/sell signals
|
| 428 |
+
buy_signals = df[df['EMA_Signal'] == 1]
|
| 429 |
+
sell_signals = df[df['EMA_Signal'] == -1]
|
| 430 |
+
|
| 431 |
+
if not buy_signals.empty:
|
| 432 |
+
fig_ema_signals.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'],
|
| 433 |
+
mode='markers', name='Buy Signal',
|
| 434 |
+
marker=dict(symbol='triangle-up', size=12, color='green')))
|
| 435 |
+
|
| 436 |
+
if not sell_signals.empty:
|
| 437 |
+
fig_ema_signals.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'],
|
| 438 |
+
mode='markers', name='Sell Signal',
|
| 439 |
+
marker=dict(symbol='triangle-down', size=12, color='red')))
|
| 440 |
+
|
| 441 |
+
fig_ema_signals.update_layout(title=f"{selected_stock} - EMA Strategy Signals", height=500)
|
| 442 |
+
st.plotly_chart(fig_ema_signals, use_container_width=True)
|
| 443 |
+
|
| 444 |
+
# Portfolio Performance
|
| 445 |
+
buy_hold_value = initial_cash * (df['Close'] / df['Close'].iloc[0])
|
| 446 |
+
fig_perf_ema = go.Figure()
|
| 447 |
+
fig_perf_ema.add_trace(go.Scatter(x=ema_results.index, y=ema_results['Total'],
|
| 448 |
+
mode='lines', name='EMA Strategy', line=dict(color='green', width=3)))
|
| 449 |
+
fig_perf_ema.add_trace(go.Scatter(x=df.index, y=buy_hold_value,
|
| 450 |
+
mode='lines', name='Buy & Hold', line=dict(color='blue', width=2, dash='dash')))
|
| 451 |
+
fig_perf_ema.update_layout(title="EMA Strategy vs Buy & Hold Performance", height=400)
|
| 452 |
+
st.plotly_chart(fig_perf_ema, use_container_width=True)
|
| 453 |
+
|
| 454 |
+
else:
|
| 455 |
+
# Single strategy
|
| 456 |
+
signal_col = 'SMA_Signal' if strategy_type == "SMA-based" else 'EMA_Signal'
|
| 457 |
+
strategy_name = strategy_type.split('-')[0]
|
| 458 |
+
|
| 459 |
+
results, metrics = backtest_signals(
|
| 460 |
+
df, signal_col=signal_col, price_col='Close',
|
| 461 |
+
initial_cash=initial_cash, transaction_cost=transaction_cost if use_risk_mgmt else 0
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
# Display metrics
|
| 465 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 466 |
+
with col1:
|
| 467 |
+
st.metric("💰 Final Value", metrics['Final Portfolio Value'])
|
| 468 |
+
st.metric("📈 Total Return", metrics['Total Return'])
|
| 469 |
+
with col2:
|
| 470 |
+
st.metric("🎯 Buy & Hold Return", metrics['Buy & Hold Return'])
|
| 471 |
+
st.metric("📊 Total Trades", metrics['Total Trades'])
|
| 472 |
+
with col3:
|
| 473 |
+
st.metric("🏆 Win Rate", metrics['Win Rate'])
|
| 474 |
+
st.metric("⚡ Sharpe Ratio", metrics['Sharpe Ratio'])
|
| 475 |
+
with col4:
|
| 476 |
+
st.metric("📉 Max Drawdown", metrics['Maximum Drawdown'])
|
| 477 |
+
st.metric("🔥 Volatility", metrics['Volatility (Annual)'])
|
| 478 |
+
|
| 479 |
+
# Price Chart with Signals
|
| 480 |
+
fig_signals = go.Figure()
|
| 481 |
+
fig_signals.add_trace(go.Scatter(x=df.index, y=df['Close'], mode='lines',
|
| 482 |
+
name='Close Price', line=dict(color='purple', width=2)))
|
| 483 |
+
|
| 484 |
+
if strategy_name == 'SMA':
|
| 485 |
+
fig_signals.add_trace(go.Scatter(x=df.index, y=df['SMA20'], mode='lines',
|
| 486 |
+
name='SMA20', line=dict(color='blue', width=2)))
|
| 487 |
+
fig_signals.add_trace(go.Scatter(x=df.index, y=df['SMA50'], mode='lines',
|
| 488 |
+
name='SMA50', line=dict(color='red', width=2)))
|
| 489 |
+
else:
|
| 490 |
+
fig_signals.add_trace(go.Scatter(x=df.index, y=df['EMA20'], mode='lines',
|
| 491 |
+
name='EMA20', line=dict(color='blue', width=2)))
|
| 492 |
+
fig_signals.add_trace(go.Scatter(x=df.index, y=df['EMA50'], mode='lines',
|
| 493 |
+
name='EMA50', line=dict(color='red', width=2)))
|
| 494 |
+
|
| 495 |
+
# Add buy/sell signals
|
| 496 |
+
buy_signals = df[df[signal_col] == 1]
|
| 497 |
+
sell_signals = df[df[signal_col] == -1]
|
| 498 |
+
|
| 499 |
+
if not buy_signals.empty:
|
| 500 |
+
fig_signals.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'],
|
| 501 |
+
mode='markers', name='Buy Signal',
|
| 502 |
+
marker=dict(symbol='triangle-up', size=12, color='green')))
|
| 503 |
+
|
| 504 |
+
if not sell_signals.empty:
|
| 505 |
+
fig_signals.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'],
|
| 506 |
+
mode='markers', name='Sell Signal',
|
| 507 |
+
marker=dict(symbol='triangle-down', size=12, color='red')))
|
| 508 |
+
|
| 509 |
+
fig_signals.update_layout(title=f"{selected_stock} - {strategy_name} Strategy Signals", height=500)
|
| 510 |
+
st.plotly_chart(fig_signals, use_container_width=True)
|
| 511 |
+
|
| 512 |
+
# Portfolio Performance
|
| 513 |
+
buy_hold_value = initial_cash * (df['Close'] / df['Close'].iloc[0])
|
| 514 |
+
fig_perf = go.Figure()
|
| 515 |
+
fig_perf.add_trace(go.Scatter(x=results.index, y=results['Total'],
|
| 516 |
+
mode='lines', name=f'{strategy_name} Strategy', line=dict(color='green', width=3)))
|
| 517 |
+
fig_perf.add_trace(go.Scatter(x=df.index, y=buy_hold_value,
|
| 518 |
+
mode='lines', name='Buy & Hold', line=dict(color='blue', width=2, dash='dash')))
|
| 519 |
+
fig_perf.update_layout(title=f"{strategy_name} Strategy vs Buy & Hold Performance", height=400)
|
| 520 |
+
st.plotly_chart(fig_perf, use_container_width=True)
|
| 521 |
+
|
| 522 |
+
# Additional Technical Analysis Charts (only show if we have results)
|
| 523 |
+
if results is not None:
|
| 524 |
+
st.header("📈 Additional Technical Analysis")
|
| 525 |
+
|
| 526 |
+
col1, col2 = st.columns(2)
|
| 527 |
+
|
| 528 |
+
with col1:
|
| 529 |
+
# RSI Chart
|
| 530 |
+
fig_rsi = go.Figure()
|
| 531 |
+
fig_rsi.add_trace(go.Scatter(x=df.index, y=df['RSI14'], mode='lines',
|
| 532 |
+
name='RSI14', line=dict(color='purple', width=2)))
|
| 533 |
+
|
| 534 |
+
# Add buy/sell signals on RSI if available
|
| 535 |
+
if not buy_signals.empty:
|
| 536 |
+
fig_rsi.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['RSI14'],
|
| 537 |
+
mode='markers', name='Buy Signal',
|
| 538 |
+
marker=dict(symbol='triangle-up', size=10, color='green'),
|
| 539 |
+
showlegend=False))
|
| 540 |
+
|
| 541 |
+
if not sell_signals.empty:
|
| 542 |
+
fig_rsi.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['RSI14'],
|
| 543 |
+
mode='markers', name='Sell Signal',
|
| 544 |
+
marker=dict(symbol='triangle-down', size=10, color='red'),
|
| 545 |
+
showlegend=False))
|
| 546 |
+
|
| 547 |
+
fig_rsi.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="Overbought (70)")
|
| 548 |
+
fig_rsi.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="Oversold (30)")
|
| 549 |
+
fig_rsi.add_hline(y=50, line_dash="solid", line_color="gray", annotation_text="Midline (50)", opacity=0.5)
|
| 550 |
+
|
| 551 |
+
fig_rsi.update_layout(title="RSI with Trading Signals", yaxis=dict(range=[0, 100]), height=400)
|
| 552 |
+
st.plotly_chart(fig_rsi, use_container_width=True)
|
| 553 |
+
|
| 554 |
+
with col2:
|
| 555 |
+
# MACD Chart
|
| 556 |
+
fig_macd = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.7, 0.3])
|
| 557 |
+
|
| 558 |
+
# MACD line
|
| 559 |
+
fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD'], mode='lines', name='MACD',
|
| 560 |
+
line=dict(color='blue', width=2)), row=1, col=1)
|
| 561 |
+
|
| 562 |
+
# Signal line
|
| 563 |
+
fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD_signal'], mode='lines', name='Signal Line',
|
| 564 |
+
line=dict(color='orange', width=2)), row=1, col=1)
|
| 565 |
+
|
| 566 |
+
# Zero line
|
| 567 |
+
fig_macd.add_hline(y=0, line_dash="solid", line_color="pink", opacity=0.5, row=1, col=1)
|
| 568 |
+
|
| 569 |
+
# MACD histogram
|
| 570 |
+
colors = ['green' if val >= 0 else 'red' for val in df['MACD_hist']]
|
| 571 |
+
fig_macd.add_trace(go.Bar(x=df.index, y=df['MACD_hist'], name='MACD Histogram',
|
| 572 |
+
marker_color=colors, opacity=0.6), row=2, col=1)
|
| 573 |
+
|
| 574 |
+
fig_macd.update_layout(title="MACD Indicator", height=400, showlegend=True)
|
| 575 |
+
fig_macd.update_xaxes(title_text="Date", row=2, col=1)
|
| 576 |
+
fig_macd.update_yaxes(title_text="MACD Value", row=1, col=1)
|
| 577 |
+
fig_macd.update_yaxes(title_text="Histogram", row=2, col=1)
|
| 578 |
+
|
| 579 |
+
st.plotly_chart(fig_macd, use_container_width=True)
|
| 580 |
+
|
| 581 |
+
# Bollinger Bands
|
| 582 |
+
st.subheader("📈 Bollinger Bands")
|
| 583 |
+
fig_bb = go.Figure()
|
| 584 |
+
|
| 585 |
+
fig_bb.add_trace(go.Scatter(x=df.index, y=df['Close'], mode='lines', name='Close Price',
|
| 586 |
+
line=dict(color='purple', width=2)))
|
| 587 |
+
fig_bb.add_trace(go.Scatter(x=df.index, y=df['SMA20'], mode='lines', name='20-day SMA',
|
| 588 |
+
line=dict(color='blue', width=1.5)))
|
| 589 |
+
fig_bb.add_trace(go.Scatter(x=df.index, y=df['Upper_Band'], mode='lines', name='Upper Band',
|
| 590 |
+
line=dict(color='red', dash='dash', width=1.5)))
|
| 591 |
+
fig_bb.add_trace(go.Scatter(x=df.index, y=df['Lower_Band'], mode='lines', name='Lower Band',
|
| 592 |
+
line=dict(color='green', dash='dash', width=1.5),
|
| 593 |
+
fill='tonexty', fillcolor='rgba(128,128,128,0.2)'))
|
| 594 |
+
|
| 595 |
+
fig_bb.update_layout(title="Bollinger Bands", height=500)
|
| 596 |
+
st.plotly_chart(fig_bb, use_container_width=True)
|
| 597 |
+
|
| 598 |
+
# Drawdown Analysis
|
| 599 |
+
st.subheader("📉 Drawdown Analysis")
|
| 600 |
+
|
| 601 |
+
# Calculate drawdown
|
| 602 |
+
returns = results['Total'].pct_change().fillna(0)
|
| 603 |
+
cumulative = (1 + returns).cumprod()
|
| 604 |
+
running_max = cumulative.expanding().max()
|
| 605 |
+
drawdown = (cumulative - running_max) / running_max
|
| 606 |
+
|
| 607 |
+
fig_dd = go.Figure()
|
| 608 |
+
|
| 609 |
+
fig_dd.add_trace(go.Scatter(
|
| 610 |
+
x=df.index,
|
| 611 |
+
y=drawdown * 100,
|
| 612 |
+
mode='lines',
|
| 613 |
+
name='Drawdown',
|
| 614 |
+
fill='tozeroy',
|
| 615 |
+
fillcolor='rgba(255,0,0,0.3)',
|
| 616 |
+
line=dict(color='red', width=1),
|
| 617 |
+
hovertemplate='<b>Drawdown</b>: %{y:.1f}%<extra></extra>'
|
| 618 |
+
))
|
| 619 |
+
|
| 620 |
+
fig_dd.update_layout(
|
| 621 |
+
title="Portfolio Drawdown Over Time",
|
| 622 |
+
xaxis_title="Date",
|
| 623 |
+
yaxis_title="Drawdown (%)",
|
| 624 |
+
height=400,
|
| 625 |
+
template='plotly_white'
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
st.plotly_chart(fig_dd, use_container_width=True)
|
| 629 |
+
|
| 630 |
+
# Trade analysis
|
| 631 |
+
if metrics is not None and not metrics['Trades DataFrame'].empty:
|
| 632 |
+
st.subheader("📋 Trade Analysis")
|
| 633 |
+
|
| 634 |
+
trades_df = metrics['Trades DataFrame']
|
| 635 |
+
|
| 636 |
+
# Trade statistics
|
| 637 |
+
col1, col2, col3 = st.columns(3)
|
| 638 |
+
with col1:
|
| 639 |
+
avg_trade_duration = (pd.to_datetime(trades_df['exit_date']) -
|
| 640 |
+
pd.to_datetime(trades_df['entry_date'])).dt.days.mean()
|
| 641 |
+
st.metric("📅 Avg Trade Duration", f"{avg_trade_duration:.1f} days")
|
| 642 |
+
|
| 643 |
+
with col2:
|
| 644 |
+
best_trade = trades_df['return_pct'].max()
|
| 645 |
+
st.metric("🚀 Best Trade", f"{best_trade:.2%}")
|
| 646 |
+
|
| 647 |
+
with col3:
|
| 648 |
+
worst_trade = trades_df['return_pct'].min()
|
| 649 |
+
st.metric("💥 Worst Trade", f"{worst_trade:.2%}")
|
| 650 |
+
|
| 651 |
+
# Trade returns distribution
|
| 652 |
+
st.subheader("📊 Trade Returns Distribution")
|
| 653 |
+
|
| 654 |
+
returns_pct = trades_df['return_pct'] * 100
|
| 655 |
+
|
| 656 |
+
fig_hist = px.histogram(
|
| 657 |
+
x=returns_pct,
|
| 658 |
+
nbins=20,
|
| 659 |
+
title="Distribution of Trade Returns",
|
| 660 |
+
labels={'x': 'Return (%)', 'y': 'Number of Trades'},
|
| 661 |
+
color_discrete_sequence=['steelblue']
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
# Add vertical lines for mean and zero
|
| 665 |
+
fig_hist.add_vline(x=0, line_dash="dash", line_color="red",
|
| 666 |
+
annotation_text="Break Even")
|
| 667 |
+
fig_hist.add_vline(x=returns_pct.mean(), line_dash="solid", line_color="green",
|
| 668 |
+
annotation_text=f"Mean: {returns_pct.mean():.1f}%")
|
| 669 |
+
|
| 670 |
+
fig_hist.update_layout(
|
| 671 |
+
height=400,
|
| 672 |
+
template='plotly_white',
|
| 673 |
+
showlegend=False
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
st.plotly_chart(fig_hist, use_container_width=True)
|
| 677 |
+
|
| 678 |
+
# Trade timeline
|
| 679 |
+
st.subheader("📅 Trade Timeline")
|
| 680 |
+
|
| 681 |
+
fig_timeline = go.Figure()
|
| 682 |
+
|
| 683 |
+
for i, trade in trades_df.iterrows():
|
| 684 |
+
color = 'green' if trade['return_pct'] > 0 else 'red'
|
| 685 |
+
fig_timeline.add_trace(go.Scatter(
|
| 686 |
+
x=[trade['entry_date'], trade['exit_date']],
|
| 687 |
+
y=[trade['entry_price'], trade['exit_price']],
|
| 688 |
+
mode='lines+markers',
|
| 689 |
+
name=f"Trade {i+1}",
|
| 690 |
+
line=dict(color=color, width=3),
|
| 691 |
+
marker=dict(size=8),
|
| 692 |
+
hovertemplate=f'<b>Trade {i+1}</b><br>' +
|
| 693 |
+
f'Entry: ₹{trade["entry_price"]:.2f}<br>' +
|
| 694 |
+
f'Exit: ₹{trade["exit_price"]:.2f}<br>' +
|
| 695 |
+
f'Return: {trade["return_pct"]:.2%}<br>' +
|
| 696 |
+
f'Duration: {(pd.to_datetime(trade["exit_date"]) - pd.to_datetime(trade["entry_date"])).days} days<extra></extra>',
|
| 697 |
+
showlegend=False
|
| 698 |
+
))
|
| 699 |
+
|
| 700 |
+
fig_timeline.update_layout(
|
| 701 |
+
title="Individual Trade Performance Timeline",
|
| 702 |
+
xaxis_title="Date",
|
| 703 |
+
yaxis_title="Price (₹)",
|
| 704 |
+
height=500,
|
| 705 |
+
template='plotly_white'
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
st.plotly_chart(fig_timeline, use_container_width=True)
|
| 709 |
+
|
| 710 |
+
# Trade history table
|
| 711 |
+
st.subheader("📊 Detailed Trade History")
|
| 712 |
+
display_trades = trades_df.copy()
|
| 713 |
+
display_trades['Entry Date'] = pd.to_datetime(display_trades['entry_date']).dt.strftime('%Y-%m-%d')
|
| 714 |
+
display_trades['Exit Date'] = pd.to_datetime(display_trades['exit_date']).dt.strftime('%Y-%m-%d')
|
| 715 |
+
display_trades['Entry Price'] = display_trades['entry_price'].apply(lambda x: f"₹{x:.2f}")
|
| 716 |
+
display_trades['Exit Price'] = display_trades['exit_price'].apply(lambda x: f"₹{x:.2f}")
|
| 717 |
+
display_trades['P&L (₹)'] = display_trades['profit_loss'].apply(lambda x: f"₹{x:,.2f}")
|
| 718 |
+
display_trades['Return %'] = display_trades['return_pct'].apply(lambda x: f"{x:.2%}")
|
| 719 |
+
display_trades['Duration'] = (pd.to_datetime(trades_df['exit_date']) -
|
| 720 |
+
pd.to_datetime(trades_df['entry_date'])).dt.days
|
| 721 |
+
|
| 722 |
+
trade_display = display_trades[['Entry Date', 'Exit Date', 'Entry Price', 'Exit Price',
|
| 723 |
+
'P&L (₹)', 'Return %', 'Duration', 'exit_reason']].copy()
|
| 724 |
+
trade_display.columns = ['Entry Date', 'Exit Date', 'Entry Price', 'Exit Price',
|
| 725 |
+
'Profit/Loss', 'Return %', 'Days', 'Exit Reason']
|
| 726 |
+
|
| 727 |
+
st.dataframe(trade_display, use_container_width=True)
|
| 728 |
+
|
| 729 |
+
else:
|
| 730 |
+
st.info("📝 No trades were executed during this period with the current parameters.")
|
| 731 |
+
|
| 732 |
+
# Signal summary table
|
| 733 |
+
if signal_col is not None:
|
| 734 |
+
st.subheader("📋 Trading Signals Summary")
|
| 735 |
+
signal_summary = df[df[signal_col] != 0].copy()
|
| 736 |
+
|
| 737 |
+
if not signal_summary.empty:
|
| 738 |
+
signal_summary['Signal Type'] = signal_summary[signal_col].map({1: '🟢 BUY', -1: '🔴 SELL'})
|
| 739 |
+
signal_summary['Price'] = signal_summary['Close'].apply(lambda x: f"₹{x:.2f}")
|
| 740 |
+
signal_summary['RSI'] = signal_summary['RSI14'].apply(lambda x: f"{x:.1f}")
|
| 741 |
+
signal_summary[f'{strategy_name}{short_period}'] = signal_summary[f'{strategy_name}{short_period}'].apply(lambda x: f"₹{x:.2f}")
|
| 742 |
+
signal_summary[f'{strategy_name}{long_period}'] = signal_summary[f'{strategy_name}{long_period}'].apply(lambda x: f"₹{x:.2f}")
|
| 743 |
+
|
| 744 |
+
display_signals = signal_summary[['Signal Type', 'Price', 'RSI',
|
| 745 |
+
f'{strategy_name}{short_period}',
|
| 746 |
+
f'{strategy_name}{long_period}']].copy()
|
| 747 |
+
display_signals.index = display_signals.index.strftime('%Y-%m-%d')
|
| 748 |
+
|
| 749 |
+
st.dataframe(display_signals, use_container_width=True)
|
| 750 |
+
else:
|
| 751 |
+
st.info("📝 No trading signals were generated during this period with the current parameters.")
|
| 752 |
+
|
| 753 |
+
# Data Download Section
|
| 754 |
+
st.subheader("💾 Download Data")
|
| 755 |
+
col1, col2 = st.columns(2)
|
| 756 |
+
|
| 757 |
+
with col1:
|
| 758 |
+
csv_data = df.to_csv(index=True)
|
| 759 |
+
st.download_button(
|
| 760 |
+
label="📁 Download Full Dataset (CSV)",
|
| 761 |
+
data=csv_data,
|
| 762 |
+
file_name=f"{selected_stock}_analysis_{start_date.strftime('%Y%m%d')}.csv",
|
| 763 |
+
mime="text/csv"
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
with col2:
|
| 767 |
+
if results is not None:
|
| 768 |
+
results_csv = results.to_csv(index=True)
|
| 769 |
+
st.download_button(
|
| 770 |
+
label="📊 Download Backtest Results (CSV)",
|
| 771 |
+
data=results_csv,
|
| 772 |
+
file_name=f"{selected_stock}_backtest_{start_date.strftime('%Y%m%d')}.csv",
|
| 773 |
+
mime="text/csv"
|
| 774 |
+
)
|
| 775 |
+
|
| 776 |
+
else:
|
| 777 |
+
st.error("❌ No data found for the selected stock and date range.")
|
| 778 |
+
|
| 779 |
+
# ========================= SIDEBAR INFORMATION =========================
|
| 780 |
+
|
| 781 |
+
st.sidebar.markdown("---")
|
| 782 |
+
st.sidebar.header("ℹ️ About")
|
| 783 |
+
st.sidebar.write("""
|
| 784 |
+
**Price Prediction Features:**
|
| 785 |
+
- Logistic Regression model for next-day prediction
|
| 786 |
+
- 59+ technical features including volatility, momentum, and lag features
|
| 787 |
+
- Confidence gauge and feature importance analysis
|
| 788 |
+
|
| 789 |
+
**Trading Dashboard Features:**
|
| 790 |
+
- SMA and EMA-based strategies
|
| 791 |
+
- Comprehensive backtesting with risk management
|
| 792 |
+
- Detailed performance metrics and trade analysis
|
| 793 |
+
- Interactive visualizations with Plotly
|
| 794 |
+
|
| 795 |
+
**Disclaimer**: This is for educational purposes only. Always do your own research before making investment decisions.
|
| 796 |
+
""")
|
| 797 |
+
|
| 798 |
+
st.sidebar.markdown("---")
|
| 799 |
+
st.sidebar.write("**Model Performance:**")
|
| 800 |
+
st.sidebar.write("• Accuracy: 55%")
|
| 801 |
+
st.sidebar.write("• F1 Score: 0.4839")
|
| 802 |
+
st.sidebar.write("• AUC: 0.5370")
|
| 803 |
+
st.sidebar.write("• Average Precision: 0.5300")
|
| 804 |
|
| 805 |
+
# Footer
|
| 806 |
+
st.markdown("---")
|
| 807 |
+
st.markdown("**⚠️ Disclaimer**: This platform is for research and educational purposes only. Stock market investments are subject to market risks. Please consult with a financial advisor before making investment decisions.")
|
| 808 |
+
st.markdown("**Developed by**: Zane Vijay Falcao")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/backtester.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/backtester.py
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
def backtest_signals(df, signal_col='Signal', price_col='Close', initial_cash=100000,
|
| 7 |
+
transaction_cost=0.001, stop_loss=None, take_profit=None):
|
| 8 |
+
"""
|
| 9 |
+
Enhanced backtest strategy using buy/sell signals.
|
| 10 |
+
|
| 11 |
+
Parameters:
|
| 12 |
+
df (pd.DataFrame): DataFrame with signal and price columns
|
| 13 |
+
signal_col (str): Name of the signal column (1 = Buy, -1 = Sell)
|
| 14 |
+
price_col (str): Name of the price column to use for trading
|
| 15 |
+
initial_cash (float): Starting cash for the backtest
|
| 16 |
+
transaction_cost (float): Transaction cost as percentage (0.001 = 0.1%)
|
| 17 |
+
stop_loss (float): Stop loss percentage (0.05 = 5%)
|
| 18 |
+
take_profit (float): Take profit percentage (0.10 = 10%)
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
tuple: (results_df, performance_metrics)
|
| 22 |
+
"""
|
| 23 |
+
df = df.copy()
|
| 24 |
+
df['Position'] = 0 # 1 if holding, 0 otherwise
|
| 25 |
+
df['Cash'] = initial_cash
|
| 26 |
+
df['Holdings_Value'] = 0
|
| 27 |
+
df['Total'] = initial_cash
|
| 28 |
+
df['Returns'] = 0
|
| 29 |
+
df['Trade_Action'] = ''
|
| 30 |
+
|
| 31 |
+
position = 0 # Whether we hold a stock
|
| 32 |
+
cash = initial_cash
|
| 33 |
+
shares = 0
|
| 34 |
+
entry_price = 0
|
| 35 |
+
trades = []
|
| 36 |
+
|
| 37 |
+
for i in range(len(df)):
|
| 38 |
+
current_price = df[price_col].iloc[i]
|
| 39 |
+
signal = df[signal_col].iloc[i]
|
| 40 |
+
|
| 41 |
+
# Check stop loss and take profit if holding position
|
| 42 |
+
if position == 1 and shares > 0:
|
| 43 |
+
price_change = (current_price - entry_price) / entry_price
|
| 44 |
+
|
| 45 |
+
# Stop loss check
|
| 46 |
+
if stop_loss and price_change <= -stop_loss:
|
| 47 |
+
# Force sell due to stop loss
|
| 48 |
+
cash = shares * current_price * (1 - transaction_cost)
|
| 49 |
+
trades.append({
|
| 50 |
+
'entry_date': entry_date,
|
| 51 |
+
'exit_date': df.index[i],
|
| 52 |
+
'entry_price': entry_price,
|
| 53 |
+
'exit_price': current_price,
|
| 54 |
+
'shares': shares,
|
| 55 |
+
'profit_loss': cash - (shares * entry_price),
|
| 56 |
+
'return_pct': price_change,
|
| 57 |
+
'exit_reason': 'Stop Loss'
|
| 58 |
+
})
|
| 59 |
+
shares = 0
|
| 60 |
+
position = 0
|
| 61 |
+
df.at[df.index[i], 'Trade_Action'] = 'STOP_LOSS'
|
| 62 |
+
|
| 63 |
+
# Take profit check
|
| 64 |
+
elif take_profit and price_change >= take_profit:
|
| 65 |
+
# Force sell due to take profit
|
| 66 |
+
cash = shares * current_price * (1 - transaction_cost)
|
| 67 |
+
trades.append({
|
| 68 |
+
'entry_date': entry_date,
|
| 69 |
+
'exit_date': df.index[i],
|
| 70 |
+
'entry_price': entry_price,
|
| 71 |
+
'exit_price': current_price,
|
| 72 |
+
'shares': shares,
|
| 73 |
+
'profit_loss': cash - (shares * entry_price),
|
| 74 |
+
'return_pct': price_change,
|
| 75 |
+
'exit_reason': 'Take Profit'
|
| 76 |
+
})
|
| 77 |
+
shares = 0
|
| 78 |
+
position = 0
|
| 79 |
+
df.at[df.index[i], 'Trade_Action'] = 'TAKE_PROFIT'
|
| 80 |
+
|
| 81 |
+
# Process regular buy/sell signals
|
| 82 |
+
if signal == 1 and position == 0 and cash > 0:
|
| 83 |
+
# Buy signal
|
| 84 |
+
cost_with_fees = cash * (1 + transaction_cost)
|
| 85 |
+
if cost_with_fees <= cash:
|
| 86 |
+
shares = cash / (current_price * (1 + transaction_cost))
|
| 87 |
+
cash = 0
|
| 88 |
+
position = 1
|
| 89 |
+
entry_price = current_price
|
| 90 |
+
entry_date = df.index[i]
|
| 91 |
+
df.at[df.index[i], 'Trade_Action'] = 'BUY'
|
| 92 |
+
|
| 93 |
+
elif signal == -1 and position == 1 and shares > 0:
|
| 94 |
+
# Sell signal
|
| 95 |
+
cash = shares * current_price * (1 - transaction_cost)
|
| 96 |
+
|
| 97 |
+
# Record trade
|
| 98 |
+
price_change = (current_price - entry_price) / entry_price
|
| 99 |
+
trades.append({
|
| 100 |
+
'entry_date': entry_date,
|
| 101 |
+
'exit_date': df.index[i],
|
| 102 |
+
'entry_price': entry_price,
|
| 103 |
+
'exit_price': current_price,
|
| 104 |
+
'shares': shares,
|
| 105 |
+
'profit_loss': cash - (shares * entry_price),
|
| 106 |
+
'return_pct': price_change,
|
| 107 |
+
'exit_reason': 'Signal'
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
shares = 0
|
| 111 |
+
position = 0
|
| 112 |
+
df.at[df.index[i], 'Trade_Action'] = 'SELL'
|
| 113 |
+
|
| 114 |
+
# Update portfolio values
|
| 115 |
+
holdings_value = shares * current_price if shares > 0 else 0
|
| 116 |
+
total_value = cash + holdings_value
|
| 117 |
+
|
| 118 |
+
df.at[df.index[i], 'Position'] = position
|
| 119 |
+
df.at[df.index[i], 'Cash'] = cash
|
| 120 |
+
df.at[df.index[i], 'Holdings_Value'] = holdings_value
|
| 121 |
+
df.at[df.index[i], 'Total'] = total_value
|
| 122 |
+
|
| 123 |
+
# Calculate daily returns
|
| 124 |
+
if i > 0:
|
| 125 |
+
prev_total = df['Total'].iloc[i-1]
|
| 126 |
+
df.at[df.index[i], 'Returns'] = (total_value - prev_total) / prev_total
|
| 127 |
+
|
| 128 |
+
# Calculate performance metrics
|
| 129 |
+
performance_metrics = calculate_performance_metrics(df, trades, initial_cash)
|
| 130 |
+
|
| 131 |
+
return df[['Close', signal_col, 'Position', 'Cash', 'Holdings_Value', 'Total',
|
| 132 |
+
'Returns', 'Trade_Action']], performance_metrics
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def calculate_performance_metrics(df, trades, initial_cash):
|
| 136 |
+
"""Calculate comprehensive performance metrics"""
|
| 137 |
+
final_value = df['Total'].iloc[-1]
|
| 138 |
+
total_return = (final_value - initial_cash) / initial_cash
|
| 139 |
+
|
| 140 |
+
# Calculate buy and hold return for comparison
|
| 141 |
+
buy_hold_return = (df['Close'].iloc[-1] - df['Close'].iloc[0]) / df['Close'].iloc[0]
|
| 142 |
+
|
| 143 |
+
# Risk metrics
|
| 144 |
+
returns = df['Returns'].dropna()
|
| 145 |
+
if len(returns) > 0:
|
| 146 |
+
volatility = returns.std() * np.sqrt(252) # Annualized volatility
|
| 147 |
+
sharpe_ratio = (returns.mean() * 252) / volatility if volatility > 0 else 0
|
| 148 |
+
|
| 149 |
+
# Maximum drawdown
|
| 150 |
+
cumulative = (1 + returns).cumprod()
|
| 151 |
+
running_max = cumulative.expanding().max()
|
| 152 |
+
drawdown = (cumulative - running_max) / running_max
|
| 153 |
+
max_drawdown = drawdown.min()
|
| 154 |
+
else:
|
| 155 |
+
volatility = 0
|
| 156 |
+
sharpe_ratio = 0
|
| 157 |
+
max_drawdown = 0
|
| 158 |
+
|
| 159 |
+
# Trade statistics
|
| 160 |
+
if trades:
|
| 161 |
+
trades_df = pd.DataFrame(trades)
|
| 162 |
+
win_rate = len(trades_df[trades_df['return_pct'] > 0]) / len(trades_df)
|
| 163 |
+
avg_win = trades_df[trades_df['return_pct'] > 0]['return_pct'].mean() if len(trades_df[trades_df['return_pct'] > 0]) > 0 else 0
|
| 164 |
+
avg_loss = trades_df[trades_df['return_pct'] < 0]['return_pct'].mean() if len(trades_df[trades_df['return_pct'] < 0]) > 0 else 0
|
| 165 |
+
profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
|
| 166 |
+
else:
|
| 167 |
+
win_rate = 0
|
| 168 |
+
avg_win = 0
|
| 169 |
+
avg_loss = 0
|
| 170 |
+
profit_factor = 0
|
| 171 |
+
|
| 172 |
+
return {
|
| 173 |
+
'Total Return': f"{total_return:.2%}",
|
| 174 |
+
'Buy & Hold Return': f"{buy_hold_return:.2%}",
|
| 175 |
+
'Final Portfolio Value': f"₹{final_value:,.2f}",
|
| 176 |
+
'Total Trades': len(trades),
|
| 177 |
+
'Win Rate': f"{win_rate:.2%}",
|
| 178 |
+
'Average Win': f"{avg_win:.2%}",
|
| 179 |
+
'Average Loss': f"{avg_loss:.2%}",
|
| 180 |
+
'Profit Factor': f"{profit_factor:.2f}",
|
| 181 |
+
'Volatility (Annual)': f"{volatility:.2%}",
|
| 182 |
+
'Sharpe Ratio': f"{sharpe_ratio:.2f}",
|
| 183 |
+
'Maximum Drawdown': f"{max_drawdown:.2%}",
|
| 184 |
+
'Trades DataFrame': pd.DataFrame(trades) if trades else pd.DataFrame()
|
| 185 |
+
}
|
| 186 |
+
|
src/utils/data_loader.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/data_loader.py
|
| 2 |
+
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
from curl_cffi import requests
|
| 9 |
+
session = requests.Session(impersonate="chrome")
|
| 10 |
+
|
| 11 |
+
today = datetime.today()
|
| 12 |
+
|
| 13 |
+
def fetch_stock_data(symbol, start_date="2023-01-01", end_date=today, interval="1d"):
|
| 14 |
+
"""
|
| 15 |
+
Fetch historical stock data from Yahoo Finance.
|
| 16 |
+
|
| 17 |
+
Parameters:
|
| 18 |
+
symbol (str): Ticker symbol (e.g., "RELIANCE.NS")
|
| 19 |
+
start_date (str): Start date in "YYYY-MM-DD"
|
| 20 |
+
end_date (str): End date (default is today)
|
| 21 |
+
interval (str): Data interval ("1d", "1h", etc.)
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
pd.DataFrame: Historical OHLCV stock data
|
| 25 |
+
"""
|
| 26 |
+
try:
|
| 27 |
+
logging.info(f"Fetching data for {symbol} from {start_date} to {end_date or 'today'}")
|
| 28 |
+
df = yf.download(symbol, start=start_date, end=end_date, interval=interval, progress=False, session=session, auto_adjust=True, threads=True)
|
| 29 |
+
# Flatten the MultiIndex columns
|
| 30 |
+
df.columns = [col[0] for col in df.columns]
|
| 31 |
+
|
| 32 |
+
if df.empty:
|
| 33 |
+
logging.warning(f"No data found for {symbol}")
|
| 34 |
+
else:
|
| 35 |
+
logging.info(f"Downloaded {len(df)} rows for {symbol}")
|
| 36 |
+
return df
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logging.error(f"Failed to fetch data for {symbol}: {e}")
|
| 40 |
+
return pd.DataFrame()
|
| 41 |
+
|
src/utils/google_sheets.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/google_sheets.py
|
| 2 |
+
|
| 3 |
+
import gspread
|
| 4 |
+
from google.oauth2.service_account import Credentials
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
class TradingGoogleSheets:
|
| 11 |
+
def __init__(self, credentials_json_path=None, credentials_dict=None, sheet_name="Trading_Log"):
|
| 12 |
+
"""
|
| 13 |
+
Initialize Google Sheets connection
|
| 14 |
+
|
| 15 |
+
Parameters:
|
| 16 |
+
credentials_json_path (str): Path to service account JSON file
|
| 17 |
+
credentials_dict (dict): Service account credentials as dictionary (for Streamlit secrets)
|
| 18 |
+
sheet_name (str): Name of the Google Sheet to create/use
|
| 19 |
+
"""
|
| 20 |
+
self.sheet_name = sheet_name
|
| 21 |
+
self.scope = [
|
| 22 |
+
"https://spreadsheets.google.com/feeds",
|
| 23 |
+
"https://www.googleapis.com/auth/drive"
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
# Initialize credentials
|
| 27 |
+
if credentials_dict:
|
| 28 |
+
# For Streamlit deployment with secrets
|
| 29 |
+
self.creds = Credentials.from_service_account_info(credentials_dict, scopes=self.scope)
|
| 30 |
+
elif credentials_json_path:
|
| 31 |
+
# For local development with JSON file
|
| 32 |
+
self.creds = Credentials.from_service_account_file(credentials_json_path, scopes=self.scope)
|
| 33 |
+
else:
|
| 34 |
+
raise ValueError("Either credentials_json_path or credentials_dict must be provided")
|
| 35 |
+
|
| 36 |
+
self.client = gspread.authorize(self.creds)
|
| 37 |
+
self.spreadsheet = None
|
| 38 |
+
|
| 39 |
+
def create_or_get_spreadsheet(self):
|
| 40 |
+
"""Create a new spreadsheet or get existing one"""
|
| 41 |
+
try:
|
| 42 |
+
# Try to open existing spreadsheet
|
| 43 |
+
self.spreadsheet = self.client.open(self.sheet_name)
|
| 44 |
+
print(f"Opened existing spreadsheet: {self.sheet_name}")
|
| 45 |
+
except gspread.SpreadsheetNotFound:
|
| 46 |
+
# Create new spreadsheet
|
| 47 |
+
self.spreadsheet = self.client.create(self.sheet_name)
|
| 48 |
+
print(f"Created new spreadsheet: {self.sheet_name}")
|
| 49 |
+
|
| 50 |
+
# Share with your email (replace with your email)
|
| 51 |
+
# self.spreadsheet.share('your-email@gmail.com', perm_type='user', role='writer')
|
| 52 |
+
|
| 53 |
+
# Create the three required worksheets
|
| 54 |
+
self.setup_worksheets()
|
| 55 |
+
return self.spreadsheet
|
| 56 |
+
|
| 57 |
+
def setup_worksheets(self):
|
| 58 |
+
"""Setup the required worksheets with headers"""
|
| 59 |
+
worksheets_config = {
|
| 60 |
+
"Trade_Log": [
|
| 61 |
+
"Timestamp", "Stock", "Strategy", "Signal_Type", "Price", "RSI",
|
| 62 |
+
"MA_Short", "MA_Long", "Entry_Date", "Exit_Date", "Entry_Price",
|
| 63 |
+
"Exit_Price", "Shares", "Profit_Loss", "Return_Pct", "Exit_Reason", "Duration_Days"
|
| 64 |
+
],
|
| 65 |
+
"Summary_PL": [
|
| 66 |
+
"Date", "Stock", "Strategy", "Total_Trades", "Winning_Trades",
|
| 67 |
+
"Losing_Trades", "Win_Rate", "Total_PL", "Best_Trade", "Worst_Trade",
|
| 68 |
+
"Avg_Win", "Avg_Loss", "Profit_Factor", "Max_Drawdown", "Sharpe_Ratio",
|
| 69 |
+
"Final_Portfolio_Value", "Total_Return"
|
| 70 |
+
],
|
| 71 |
+
"Performance_Metrics": [
|
| 72 |
+
"Date", "Stock", "Strategy", "Initial_Capital", "Final_Value",
|
| 73 |
+
"Total_Return", "Buy_Hold_Return", "Alpha", "Volatility",
|
| 74 |
+
"Sharpe_Ratio", "Max_Drawdown", "Total_Trades", "Win_Rate",
|
| 75 |
+
"Avg_Trade_Duration", "Transaction_Cost", "Notes"
|
| 76 |
+
]
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
existing_sheets = [ws.title for ws in self.spreadsheet.worksheets()]
|
| 80 |
+
|
| 81 |
+
for sheet_name, headers in worksheets_config.items():
|
| 82 |
+
if sheet_name not in existing_sheets:
|
| 83 |
+
# Create new worksheet
|
| 84 |
+
worksheet = self.spreadsheet.add_worksheet(title=sheet_name, rows=1000, cols=len(headers))
|
| 85 |
+
worksheet.append_row(headers)
|
| 86 |
+
print(f"Created worksheet: {sheet_name}")
|
| 87 |
+
else:
|
| 88 |
+
print(f"Worksheet already exists: {sheet_name}")
|
| 89 |
+
|
| 90 |
+
def log_trade_signals(self, df, strategy_name, stock_symbol):
|
| 91 |
+
"""Log all trade signals to Trade_Log worksheet"""
|
| 92 |
+
try:
|
| 93 |
+
worksheet = self.spreadsheet.worksheet("Trade_Log")
|
| 94 |
+
|
| 95 |
+
# Get signals from dataframe
|
| 96 |
+
signal_col = f'{strategy_name}_Signal'
|
| 97 |
+
signals_df = df[df[signal_col] != 0].copy()
|
| 98 |
+
|
| 99 |
+
if signals_df.empty:
|
| 100 |
+
print("No signals to log")
|
| 101 |
+
return
|
| 102 |
+
|
| 103 |
+
# Prepare data for logging
|
| 104 |
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 105 |
+
|
| 106 |
+
rows_to_add = []
|
| 107 |
+
for idx, row in signals_df.iterrows():
|
| 108 |
+
signal_type = "BUY" if row[signal_col] == 1 else "SELL"
|
| 109 |
+
|
| 110 |
+
row_data = [
|
| 111 |
+
current_time, # Timestamp
|
| 112 |
+
stock_symbol, # Stock
|
| 113 |
+
strategy_name, # Strategy
|
| 114 |
+
signal_type, # Signal_Type
|
| 115 |
+
round(row['Close'], 2), # Price
|
| 116 |
+
round(row['RSI'], 2), # RSI
|
| 117 |
+
round(row[f'{strategy_name}20'], 2), # MA_Short
|
| 118 |
+
round(row[f'{strategy_name}50'], 2), # MA_Long
|
| 119 |
+
"", # Entry_Date (filled when trade completes)
|
| 120 |
+
"", # Exit_Date
|
| 121 |
+
"", # Entry_Price
|
| 122 |
+
"", # Exit_Price
|
| 123 |
+
"", # Shares
|
| 124 |
+
"", # Profit_Loss
|
| 125 |
+
"", # Return_Pct
|
| 126 |
+
"", # Exit_Reason
|
| 127 |
+
"" # Duration_Days
|
| 128 |
+
]
|
| 129 |
+
rows_to_add.append(row_data)
|
| 130 |
+
|
| 131 |
+
# Add all rows at once
|
| 132 |
+
if rows_to_add:
|
| 133 |
+
worksheet.append_rows(rows_to_add)
|
| 134 |
+
print(f"Logged {len(rows_to_add)} signals to Trade_Log")
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"Error logging trade signals: {e}")
|
| 138 |
+
|
| 139 |
+
def log_completed_trades(self, trades_df, strategy_name, stock_symbol):
|
| 140 |
+
"""Log completed trades to Trade_Log worksheet"""
|
| 141 |
+
try:
|
| 142 |
+
worksheet = self.spreadsheet.worksheet("Trade_Log")
|
| 143 |
+
|
| 144 |
+
if trades_df.empty:
|
| 145 |
+
print("No completed trades to log")
|
| 146 |
+
return
|
| 147 |
+
|
| 148 |
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 149 |
+
|
| 150 |
+
rows_to_add = []
|
| 151 |
+
for _, trade in trades_df.iterrows():
|
| 152 |
+
row_data = [
|
| 153 |
+
current_time, # Timestamp
|
| 154 |
+
stock_symbol, # Stock
|
| 155 |
+
strategy_name, # Strategy
|
| 156 |
+
"COMPLETED_TRADE", # Signal_Type
|
| 157 |
+
round(trade['exit_price'], 2), # Price (exit price)
|
| 158 |
+
"", # RSI
|
| 159 |
+
"", # MA_Short
|
| 160 |
+
"", # MA_Long
|
| 161 |
+
trade['entry_date'], # Entry_Date
|
| 162 |
+
trade['exit_date'], # Exit_Date
|
| 163 |
+
round(trade['entry_price'], 2), # Entry_Price
|
| 164 |
+
round(trade['exit_price'], 2), # Exit_Price
|
| 165 |
+
round(trade['shares'], 4), # Shares
|
| 166 |
+
round(trade['profit_loss'], 2), # Profit_Loss
|
| 167 |
+
round(trade['return_pct'] * 100, 2), # Return_Pct
|
| 168 |
+
trade['exit_reason'], # Exit_Reason
|
| 169 |
+
(pd.to_datetime(trade['exit_date']) - pd.to_datetime(trade['entry_date'])).days # Duration_Days
|
| 170 |
+
]
|
| 171 |
+
rows_to_add.append(row_data)
|
| 172 |
+
|
| 173 |
+
if rows_to_add:
|
| 174 |
+
worksheet.append_rows(rows_to_add)
|
| 175 |
+
print(f"Logged {len(rows_to_add)} completed trades to Trade_Log")
|
| 176 |
+
|
| 177 |
+
except Exception as e:
|
| 178 |
+
print(f"Error logging completed trades: {e}")
|
| 179 |
+
|
| 180 |
+
def log_summary_pl(self, metrics, strategy_name, stock_symbol, trades_df):
|
| 181 |
+
"""Log summary P&L to Summary_PL worksheet"""
|
| 182 |
+
try:
|
| 183 |
+
worksheet = self.spreadsheet.worksheet("Summary_PL")
|
| 184 |
+
|
| 185 |
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
| 186 |
+
|
| 187 |
+
# Calculate additional metrics
|
| 188 |
+
total_trades = len(trades_df) if not trades_df.empty else 0
|
| 189 |
+
winning_trades = len(trades_df[trades_df['return_pct'] > 0]) if not trades_df.empty else 0
|
| 190 |
+
losing_trades = len(trades_df[trades_df['return_pct'] < 0]) if not trades_df.empty else 0
|
| 191 |
+
|
| 192 |
+
# Extract numeric values from metrics
|
| 193 |
+
win_rate = float(metrics['Win Rate'].strip('%')) if metrics['Win Rate'] != '0.00%' else 0
|
| 194 |
+
total_pl = float(metrics['Final Portfolio Value'].replace('₹', '').replace(',', '')) - 100000 # Assuming 100k initial
|
| 195 |
+
best_trade = trades_df['return_pct'].max() * 100 if not trades_df.empty else 0
|
| 196 |
+
worst_trade = trades_df['return_pct'].min() * 100 if not trades_df.empty else 0
|
| 197 |
+
avg_win = float(metrics['Average Win'].strip('%')) if metrics['Average Win'] != '0.00%' else 0
|
| 198 |
+
avg_loss = float(metrics['Average Loss'].strip('%')) if metrics['Average Loss'] != '0.00%' else 0
|
| 199 |
+
profit_factor = float(metrics['Profit Factor']) if metrics['Profit Factor'] != '0.00' else 0
|
| 200 |
+
max_drawdown = float(metrics['Maximum Drawdown'].strip('%'))
|
| 201 |
+
sharpe_ratio = float(metrics['Sharpe Ratio'])
|
| 202 |
+
final_value = float(metrics['Final Portfolio Value'].replace('₹', '').replace(',', ''))
|
| 203 |
+
total_return = float(metrics['Total Return'].strip('%'))
|
| 204 |
+
|
| 205 |
+
row_data = [
|
| 206 |
+
current_date, # Date
|
| 207 |
+
stock_symbol, # Stock
|
| 208 |
+
strategy_name, # Strategy
|
| 209 |
+
total_trades, # Total_Trades
|
| 210 |
+
winning_trades, # Winning_Trades
|
| 211 |
+
losing_trades, # Losing_Trades
|
| 212 |
+
round(win_rate, 2), # Win_Rate
|
| 213 |
+
round(total_pl, 2), # Total_PL
|
| 214 |
+
round(best_trade, 2), # Best_Trade
|
| 215 |
+
round(worst_trade, 2), # Worst_Trade
|
| 216 |
+
round(avg_win, 2), # Avg_Win
|
| 217 |
+
round(avg_loss, 2), # Avg_Loss
|
| 218 |
+
round(profit_factor, 2), # Profit_Factor
|
| 219 |
+
round(max_drawdown, 2), # Max_Drawdown
|
| 220 |
+
round(sharpe_ratio, 2), # Sharpe_Ratio
|
| 221 |
+
round(final_value, 2), # Final_Portfolio_Value
|
| 222 |
+
round(total_return, 2) # Total_Return
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
worksheet.append_row(row_data)
|
| 226 |
+
print("Logged summary P&L to Summary_PL")
|
| 227 |
+
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f"Error logging summary P&L: {e}")
|
| 230 |
+
|
| 231 |
+
def log_performance_metrics(self, metrics, strategy_name, stock_symbol, initial_capital,
|
| 232 |
+
transaction_cost, notes=""):
|
| 233 |
+
"""Log performance metrics to Performance_Metrics worksheet"""
|
| 234 |
+
try:
|
| 235 |
+
worksheet = self.spreadsheet.worksheet("Performance_Metrics")
|
| 236 |
+
|
| 237 |
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
| 238 |
+
|
| 239 |
+
# Extract and clean numeric values
|
| 240 |
+
final_value = float(metrics['Final Portfolio Value'].replace('₹', '').replace(',', ''))
|
| 241 |
+
total_return = float(metrics['Total Return'].strip('%'))
|
| 242 |
+
buy_hold_return = float(metrics['Buy & Hold Return'].strip('%'))
|
| 243 |
+
alpha = total_return - buy_hold_return
|
| 244 |
+
volatility = float(metrics['Volatility (Annual)'].strip('%'))
|
| 245 |
+
sharpe_ratio = float(metrics['Sharpe Ratio'])
|
| 246 |
+
max_drawdown = float(metrics['Maximum Drawdown'].strip('%'))
|
| 247 |
+
total_trades = metrics['Total Trades']
|
| 248 |
+
win_rate = float(metrics['Win Rate'].strip('%'))
|
| 249 |
+
|
| 250 |
+
# Calculate average trade duration (you might need to pass this from trades_df)
|
| 251 |
+
avg_trade_duration = 0 # You can calculate this from trades_df if needed
|
| 252 |
+
|
| 253 |
+
row_data = [
|
| 254 |
+
current_date, # Date
|
| 255 |
+
stock_symbol, # Stock
|
| 256 |
+
strategy_name, # Strategy
|
| 257 |
+
initial_capital, # Initial_Capital
|
| 258 |
+
round(final_value, 2), # Final_Value
|
| 259 |
+
round(total_return, 2), # Total_Return
|
| 260 |
+
round(buy_hold_return, 2), # Buy_Hold_Return
|
| 261 |
+
round(alpha, 2), # Alpha
|
| 262 |
+
round(volatility, 2), # Volatility
|
| 263 |
+
round(sharpe_ratio, 2), # Sharpe_Ratio
|
| 264 |
+
round(max_drawdown, 2), # Max_Drawdown
|
| 265 |
+
total_trades, # Total_Trades
|
| 266 |
+
round(win_rate, 2), # Win_Rate
|
| 267 |
+
avg_trade_duration, # Avg_Trade_Duration
|
| 268 |
+
transaction_cost * 100, # Transaction_Cost (as percentage)
|
| 269 |
+
notes # Notes
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
worksheet.append_row(row_data)
|
| 273 |
+
print("Logged performance metrics to Performance_Metrics")
|
| 274 |
+
|
| 275 |
+
except Exception as e:
|
| 276 |
+
print(f"Error logging performance metrics: {e}")
|
| 277 |
+
|
| 278 |
+
def get_sheet_url(self):
|
| 279 |
+
"""Get the URL of the Google Sheet"""
|
| 280 |
+
if self.spreadsheet:
|
| 281 |
+
return self.spreadsheet.url
|
| 282 |
+
return None
|
| 283 |
+
|
| 284 |
+
def clear_worksheet(self, worksheet_name):
|
| 285 |
+
"""Clear all data from a worksheet (except headers)"""
|
| 286 |
+
try:
|
| 287 |
+
worksheet = self.spreadsheet.worksheet(worksheet_name)
|
| 288 |
+
worksheet.clear()
|
| 289 |
+
# Re-add headers based on the worksheet
|
| 290 |
+
if worksheet_name == "Trade_Log":
|
| 291 |
+
headers = ["Timestamp", "Stock", "Strategy", "Signal_Type", "Price", "RSI",
|
| 292 |
+
"MA_Short", "MA_Long", "Entry_Date", "Exit_Date", "Entry_Price",
|
| 293 |
+
"Exit_Price", "Shares", "Profit_Loss", "Return_Pct", "Exit_Reason", "Duration_Days"]
|
| 294 |
+
elif worksheet_name == "Summary_PL":
|
| 295 |
+
headers = ["Date", "Stock", "Strategy", "Total_Trades", "Winning_Trades",
|
| 296 |
+
"Losing_Trades", "Win_Rate", "Total_PL", "Best_Trade", "Worst_Trade",
|
| 297 |
+
"Avg_Win", "Avg_Loss", "Profit_Factor", "Max_Drawdown", "Sharpe_Ratio",
|
| 298 |
+
"Final_Portfolio_Value", "Total_Return"]
|
| 299 |
+
elif worksheet_name == "Performance_Metrics":
|
| 300 |
+
headers = ["Date", "Stock", "Strategy", "Initial_Capital", "Final_Value",
|
| 301 |
+
"Total_Return", "Buy_Hold_Return", "Alpha", "Volatility",
|
| 302 |
+
"Sharpe_Ratio", "Max_Drawdown", "Total_Trades", "Win_Rate",
|
| 303 |
+
"Avg_Trade_Duration", "Transaction_Cost", "Notes"]
|
| 304 |
+
|
| 305 |
+
worksheet.append_row(headers)
|
| 306 |
+
print(f"Cleared and reset worksheet: {worksheet_name}")
|
| 307 |
+
|
| 308 |
+
except Exception as e:
|
| 309 |
+
print(f"Error clearing worksheet {worksheet_name}: {e}")
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
# Integration function for your Streamlit app
|
| 313 |
+
def log_to_google_sheets(df, results, metrics, strategy_name, stock_symbol,
|
| 314 |
+
initial_cash, transaction_cost, credentials_dict=None,
|
| 315 |
+
credentials_json_path=None):
|
| 316 |
+
"""
|
| 317 |
+
Main function to log all data to Google Sheets
|
| 318 |
+
|
| 319 |
+
Parameters:
|
| 320 |
+
df: DataFrame with signals and indicators
|
| 321 |
+
results: Backtest results DataFrame
|
| 322 |
+
metrics: Performance metrics dictionary
|
| 323 |
+
strategy_name: Name of the strategy (SMA/EMA)
|
| 324 |
+
stock_symbol: Stock symbol
|
| 325 |
+
initial_cash: Initial capital
|
| 326 |
+
transaction_cost: Transaction cost percentage
|
| 327 |
+
credentials_dict: Google service account credentials (for Streamlit)
|
| 328 |
+
credentials_json_path: Path to JSON credentials file (for local)
|
| 329 |
+
"""
|
| 330 |
+
try:
|
| 331 |
+
# Initialize Google Sheets connection
|
| 332 |
+
sheets_logger = TradingGoogleSheets(
|
| 333 |
+
credentials_dict=credentials_dict,
|
| 334 |
+
credentials_json_path=credentials_json_path,
|
| 335 |
+
sheet_name=f"Trading_Log_{stock_symbol}"
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Create or get spreadsheet
|
| 339 |
+
spreadsheet = sheets_logger.create_or_get_spreadsheet()
|
| 340 |
+
|
| 341 |
+
# Log trade signals
|
| 342 |
+
sheets_logger.log_trade_signals(df, strategy_name, stock_symbol)
|
| 343 |
+
|
| 344 |
+
# Log completed trades if available
|
| 345 |
+
if not metrics['Trades DataFrame'].empty:
|
| 346 |
+
sheets_logger.log_completed_trades(metrics['Trades DataFrame'], strategy_name, stock_symbol)
|
| 347 |
+
|
| 348 |
+
# Log summary P&L
|
| 349 |
+
trades_df = metrics['Trades DataFrame'] if not metrics['Trades DataFrame'].empty else pd.DataFrame()
|
| 350 |
+
sheets_logger.log_summary_pl(metrics, strategy_name, stock_symbol, trades_df)
|
| 351 |
+
|
| 352 |
+
# Log performance metrics
|
| 353 |
+
sheets_logger.log_performance_metrics(
|
| 354 |
+
metrics, strategy_name, stock_symbol, initial_cash, transaction_cost
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
return sheets_logger.get_sheet_url()
|
| 358 |
+
|
| 359 |
+
except Exception as e:
|
| 360 |
+
print(f"Error in log_to_google_sheets: {e}")
|
| 361 |
+
return None
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
# # Streamlit integration function
|
| 365 |
+
# def add_google_sheets_to_streamlit(df, results, metrics, strategy_name, stock_symbol,
|
| 366 |
+
# initial_cash, transaction_cost):
|
| 367 |
+
# """Add Google Sheets logging functionality to your Streamlit app"""
|
| 368 |
+
|
| 369 |
+
# st.subheader("📊 Google Sheets Integration")
|
| 370 |
+
|
| 371 |
+
# # Check if credentials are configured
|
| 372 |
+
# if 'google_sheets_credentials' in st.secrets:
|
| 373 |
+
# col1, col2 = st.columns([2, 1])
|
| 374 |
+
|
| 375 |
+
# with col1:
|
| 376 |
+
# if st.button("📤 Log to Google Sheets", type="primary"):
|
| 377 |
+
# with st.spinner("Logging data to Google Sheets..."):
|
| 378 |
+
# sheet_url = log_to_google_sheets(
|
| 379 |
+
# df=df,
|
| 380 |
+
# results=results,
|
| 381 |
+
# metrics=metrics,
|
| 382 |
+
# strategy_name=strategy_name,
|
| 383 |
+
# stock_symbol=stock_symbol,
|
| 384 |
+
# initial_cash=initial_cash,
|
| 385 |
+
# transaction_cost=transaction_cost,
|
| 386 |
+
# credentials_dict=dict(st.secrets.google_sheets_credentials)
|
| 387 |
+
# )
|
| 388 |
+
|
| 389 |
+
# if sheet_url:
|
| 390 |
+
# st.success("✅ Data logged successfully!")
|
| 391 |
+
# st.markdown(f"🔗 [View Google Sheet]({sheet_url})")
|
| 392 |
+
# else:
|
| 393 |
+
# st.error("❌ Failed to log data to Google Sheets")
|
| 394 |
+
|
| 395 |
+
# with col2:
|
| 396 |
+
# st.info("💡 **Auto-logging enabled**\n\nData will be saved to:\n- Trade signals\n- P&L summary\n- Performance metrics")
|
| 397 |
+
|
| 398 |
+
# else:
|
| 399 |
+
# st.warning("⚠️ Google Sheets credentials not configured. Add your service account credentials to Streamlit secrets to enable logging.")
|
| 400 |
+
|
| 401 |
+
# with st.expander("📋 Setup Instructions"):
|
| 402 |
+
# st.markdown("""
|
| 403 |
+
# **To enable Google Sheets integration:**
|
| 404 |
+
|
| 405 |
+
# 1. **Create a Google Cloud Project**
|
| 406 |
+
# 2. **Enable Google Sheets & Drive APIs**
|
| 407 |
+
# 3. **Create a Service Account**
|
| 408 |
+
# 4. **Download the JSON credentials**
|
| 409 |
+
# 5. **Add credentials to Streamlit secrets**
|
| 410 |
+
|
| 411 |
+
# **In your `.streamlit/secrets.toml` file:**
|
| 412 |
+
# ```toml
|
| 413 |
+
# [google_sheets_credentials]
|
| 414 |
+
# type = "service_account"
|
| 415 |
+
# project_id = "your-project-id"
|
| 416 |
+
# private_key_id = "your-private-key-id"
|
| 417 |
+
# private_key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
| 418 |
+
# client_email = "your-service-account@your-project.iam.gserviceaccount.com"
|
| 419 |
+
# client_id = "your-client-id"
|
| 420 |
+
# auth_uri = "https://accounts.google.com/o/oauth2/auth"
|
| 421 |
+
# token_uri = "https://oauth2.googleapis.com/token"
|
| 422 |
+
# auth_provider_x509_cert_url = "https://www.googleapis.com/oauth2/v1/certs"
|
| 423 |
+
# client_x509_cert_url = "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project.iam.gserviceaccount.com"
|
| 424 |
+
# universe_domain = "googleapis.com"
|
| 425 |
+
# ```
|
| 426 |
+
# """)
|
src/utils/logger.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/logger.py
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
def setup_logger(log_dir="logs", log_level=logging.INFO):
|
| 8 |
+
"""
|
| 9 |
+
Sets up a logger that writes to both console and a timestamped log file.
|
| 10 |
+
"""
|
| 11 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 12 |
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
| 13 |
+
log_file = os.path.join(log_dir, f"log_{timestamp}.log")
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=log_level,
|
| 17 |
+
format="%(asctime)s [%(levelname)s] - %(message)s",
|
| 18 |
+
handlers=[
|
| 19 |
+
logging.FileHandler(log_file),
|
| 20 |
+
logging.StreamHandler()
|
| 21 |
+
]
|
| 22 |
+
)
|
| 23 |
+
logging.info("Logger initialized.")
|
| 24 |
+
|