Spaces:
Sleeping
Sleeping
Update app/daily.py
Browse files- app/daily.py +37 -140
app/daily.py
CHANGED
|
@@ -4,93 +4,27 @@ import pandas as pd
|
|
| 4 |
from datetime import datetime as dt
|
| 5 |
import traceback
|
| 6 |
from . import persist
|
| 7 |
-
from .common import wrap_html
|
| 8 |
-
|
| 9 |
-
# Plotly
|
| 10 |
-
import plotly.graph_objs as go
|
| 11 |
-
from plotly.subplots import make_subplots
|
| 12 |
|
| 13 |
# ===========================================================
|
| 14 |
# RAW DAILY FETCHER
|
| 15 |
# ===========================================================
|
| 16 |
def daily(symbol, date_end, date_start):
|
| 17 |
-
|
| 18 |
-
|
| 19 |
start = dt.strptime(date_start, "%d-%m-%Y").strftime("%Y-%m-%d")
|
| 20 |
end = dt.strptime(date_end, "%d-%m-%Y").strftime("%Y-%m-%d")
|
| 21 |
-
|
| 22 |
df = yf.download(symbol + ".NS", start=start, end=end)
|
| 23 |
-
|
| 24 |
if isinstance(df.columns, pd.MultiIndex):
|
| 25 |
df.columns = df.columns.get_level_values(0)
|
| 26 |
-
|
| 27 |
df.columns.name = None
|
| 28 |
df.index.name = None
|
| 29 |
-
|
| 30 |
return df
|
| 31 |
|
| 32 |
# ===========================================================
|
| 33 |
-
#
|
| 34 |
-
# ===========================================================
|
| 35 |
-
def add_indicators(df):
|
| 36 |
-
df["SMA10"] = df["Close"].rolling(10).mean()
|
| 37 |
-
df["SMA50"] = df["Close"].rolling(50).mean()
|
| 38 |
-
df["SMA200"] = df["Close"].rolling(200).mean()
|
| 39 |
-
df["EMA20"] = df["Close"].ewm(span=20, adjust=False).mean()
|
| 40 |
-
delta = df["Close"].diff()
|
| 41 |
-
gain = (delta.where(delta>0,0)).rolling(14).mean()
|
| 42 |
-
loss = (-delta.where(delta<0,0)).rolling(14).mean()
|
| 43 |
-
rs = gain/loss
|
| 44 |
-
df["RSI14"] = 100 - (100/(1+rs))
|
| 45 |
-
df["Change %"] = ((df["Close"] - df["Open"])/df["Open"]*100).round(2)
|
| 46 |
-
return df
|
| 47 |
-
|
| 48 |
-
# ===========================================================
|
| 49 |
-
# SPARKLINE & MINI CANDLE
|
| 50 |
-
# ===========================================================
|
| 51 |
-
def sparkline(values, height=20, color="#1a4f8a"):
|
| 52 |
-
if len(values) == 0: return ""
|
| 53 |
-
max_val = max(values) if max(values)!=0 else 1
|
| 54 |
-
bars = "".join([f'<div style="display:inline-block;width:3px;height:{int(v/max_val*height)}px;margin-right:1px;background:{color};"></div>' for v in values])
|
| 55 |
-
return f'<div style="display:flex; align-items:flex-end;">{bars}</div>'
|
| 56 |
-
|
| 57 |
-
def volume_bar(values, height=20, color="#5584d6"):
|
| 58 |
-
if len(values) == 0: return ""
|
| 59 |
-
max_val = max(values) if max(values)!=0 else 1
|
| 60 |
-
bars = "".join([f'<div style="display:inline-block;width:3px;height:{int(v/max_val*height)}px;margin-right:1px;background:{color};"></div>' for v in values])
|
| 61 |
-
return f'<div style="display:flex; align-items:flex-end;">{bars}</div>'
|
| 62 |
-
|
| 63 |
# ===========================================================
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def plotly_dashboard(df, symbol):
|
| 67 |
-
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
| 68 |
-
vertical_spacing=0.05, row_heights=[0.7,0.3],
|
| 69 |
-
subplot_titles=[f"{symbol} OHLC + SMA/EMA", "Volume"])
|
| 70 |
-
|
| 71 |
-
# Candlestick
|
| 72 |
-
fig.add_trace(go.Candlestick(
|
| 73 |
-
x=df.index, open=df["Open"], high=df["High"], low=df["Low"], close=df["Close"],
|
| 74 |
-
name="OHLC"), row=1, col=1)
|
| 75 |
-
|
| 76 |
-
# Moving averages
|
| 77 |
-
for col, name in [("SMA10","SMA10"),("SMA50","SMA50"),("SMA200","SMA200"),("EMA20","EMA20")]:
|
| 78 |
-
fig.add_trace(go.Scatter(
|
| 79 |
-
x=df.index, y=df[col], mode="lines", line=dict(width=1.5), name=name), row=1, col=1)
|
| 80 |
-
|
| 81 |
-
# Volume bars
|
| 82 |
-
fig.add_trace(go.Bar(
|
| 83 |
-
x=df.index, y=df["Volume"], marker_color="#5584d6", name="Volume"), row=2, col=1)
|
| 84 |
-
|
| 85 |
-
fig.update_layout(height=700, showlegend=True, margin=dict(t=50,b=50), template="plotly_white")
|
| 86 |
-
|
| 87 |
-
# Return div as HTML
|
| 88 |
-
return fig.to_html(full_html=False, include_plotlyjs='cdn')
|
| 89 |
-
|
| 90 |
-
# ===========================================================
|
| 91 |
-
# DASHBOARD GENERATOR
|
| 92 |
-
# ===========================================================
|
| 93 |
-
def fetch_daily(symbol, date_end, date_start, spark_days=10):
|
| 94 |
key = f"daily_{symbol}"
|
| 95 |
if persist.exists(key, "html"):
|
| 96 |
cached = persist.load(key, "html")
|
|
@@ -102,87 +36,50 @@ def fetch_daily(symbol, date_end, date_start, spark_days=10):
|
|
| 102 |
if df.empty:
|
| 103 |
return wrap_html(f"<h1>No daily data for {symbol}</h1>")
|
| 104 |
|
| 105 |
-
|
| 106 |
-
df.reset_index(inplace=True)
|
| 107 |
-
|
| 108 |
-
# Numeric conversion
|
| 109 |
for col in ["Open","High","Low","Close","Adj Close","Volume"]:
|
| 110 |
if col in df.columns:
|
| 111 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 112 |
df = df.dropna(subset=["Open","High","Low","Close","Volume"]).reset_index(drop=True)
|
| 113 |
|
| 114 |
-
#
|
| 115 |
df["Date"] = pd.to_datetime(df.index if "Date" not in df.columns else df["Date"], errors='coerce')
|
| 116 |
df = df.dropna(subset=["Date"]).reset_index(drop=True)
|
| 117 |
df["Date"] = df["Date"].dt.strftime("%d-%b-%Y")
|
| 118 |
-
|
| 119 |
-
# Add indicators
|
| 120 |
-
df = add_indicators(df)
|
| 121 |
|
| 122 |
-
#
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
<h3>{symbol} Summary</h3>
|
| 126 |
-
<table border="1" style="border-collapse:collapse; width:400px;">
|
| 127 |
-
<tr><th>Metric</th><th>Value</th></tr>
|
| 128 |
-
<tr><td>Start Date</td><td>{df['Date'].iloc[0]}</td></tr>
|
| 129 |
-
<tr><td>End Date</td><td>{df['Date'].iloc[-1]}</td></tr>
|
| 130 |
-
<tr><td>Min Close</td><td>{format_large_number(df['Close'].min())}</td></tr>
|
| 131 |
-
<tr><td>Max Close</td><td>{format_large_number(df['Close'].max())}</td></tr>
|
| 132 |
-
<tr><td>Mean Close</td><td>{format_large_number(df['Close'].mean())}</td></tr>
|
| 133 |
-
<tr><td>Total Volume</td><td>{format_large_number(df['Volume'].sum())}</td></tr>
|
| 134 |
-
<tr><td>Avg Daily Change %</td><td>{df['Change %'].mean():.2f}%</td></tr>
|
| 135 |
-
<tr><td>Latest Close</td><td>{df['Close'].iloc[-1]}</td></tr>
|
| 136 |
-
<tr><td>Prev Close</td><td>{df['Close'].iloc[-2] if len(df)>1 else df['Close'].iloc[-1]}</td></tr>
|
| 137 |
-
</table>
|
| 138 |
-
</div>
|
| 139 |
-
"""
|
| 140 |
|
| 141 |
-
#
|
| 142 |
-
|
| 143 |
-
<div style="max-height:400px; overflow:auto; font-family:Arial,sans-serif;">
|
| 144 |
-
<table border="1" style="border-collapse:collapse; width:100%;">
|
| 145 |
-
<thead style="position:sticky; top:0; background:#1a4f8a; color:white;">
|
| 146 |
-
<tr>
|
| 147 |
-
<th>Date</th><th>Open</th><th>High</th><th>Low</th><th>Close</th><th>Adj Close</th>
|
| 148 |
-
<th>Volume</th><th>Change %</th><th>Close Trend</th><th>Vol Trend</th><th>Mini Candle</th>
|
| 149 |
-
</tr>
|
| 150 |
-
</thead>
|
| 151 |
-
<tbody>
|
| 152 |
-
"""
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
mini_c = sparkline([r["Open"], r["High"], r["Low"], r["Close"]], height=20, color="#1a4f8a")
|
| 161 |
-
|
| 162 |
-
html_table += f"""
|
| 163 |
-
<tr style='background:{row_color};'>
|
| 164 |
-
<td>{r['Date']}</td>
|
| 165 |
-
<td>{r['Open']}</td>
|
| 166 |
-
<td>{r['High']}</td>
|
| 167 |
-
<td>{r['Low']}</td>
|
| 168 |
-
<td>{r['Close']}</td>
|
| 169 |
-
<td>{r.get('Adj Close','')}</td>
|
| 170 |
-
<td>{r['Volume']}</td>
|
| 171 |
-
<td style='color:{change_color}; font-weight:600;'>{r['Change %']}%</td>
|
| 172 |
-
<td>{close_trend}</td>
|
| 173 |
-
<td>{vol_trend}</td>
|
| 174 |
-
<td>{mini_c}</td>
|
| 175 |
-
</tr>
|
| 176 |
-
"""
|
| 177 |
-
html_table += "</tbody></table></div>"
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
except Exception as e:
|
| 188 |
return wrap_html(f"<h1>Error fetch_daily: {e}</h1><pre>{traceback.format_exc()}</pre>")
|
|
|
|
| 4 |
from datetime import datetime as dt
|
| 5 |
import traceback
|
| 6 |
from . import persist
|
| 7 |
+
from .common import wrap_html
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# ===========================================================
|
| 10 |
# RAW DAILY FETCHER
|
| 11 |
# ===========================================================
|
| 12 |
def daily(symbol, date_end, date_start):
|
| 13 |
+
"""Fetch daily OHLCV from Yahoo Finance."""
|
|
|
|
| 14 |
start = dt.strptime(date_start, "%d-%m-%Y").strftime("%Y-%m-%d")
|
| 15 |
end = dt.strptime(date_end, "%d-%m-%Y").strftime("%Y-%m-%d")
|
|
|
|
| 16 |
df = yf.download(symbol + ".NS", start=start, end=end)
|
|
|
|
| 17 |
if isinstance(df.columns, pd.MultiIndex):
|
| 18 |
df.columns = df.columns.get_level_values(0)
|
|
|
|
| 19 |
df.columns.name = None
|
| 20 |
df.index.name = None
|
|
|
|
| 21 |
return df
|
| 22 |
|
| 23 |
# ===========================================================
|
| 24 |
+
# DAILY TABLE GENERATOR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# ===========================================================
|
| 26 |
+
def fetch_daily(symbol, date_end, date_start):
|
| 27 |
+
"""Return HTML table of daily stock data."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
key = f"daily_{symbol}"
|
| 29 |
if persist.exists(key, "html"):
|
| 30 |
cached = persist.load(key, "html")
|
|
|
|
| 36 |
if df.empty:
|
| 37 |
return wrap_html(f"<h1>No daily data for {symbol}</h1>")
|
| 38 |
|
| 39 |
+
# Ensure numeric columns
|
|
|
|
|
|
|
|
|
|
| 40 |
for col in ["Open","High","Low","Close","Adj Close","Volume"]:
|
| 41 |
if col in df.columns:
|
| 42 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 43 |
df = df.dropna(subset=["Open","High","Low","Close","Volume"]).reset_index(drop=True)
|
| 44 |
|
| 45 |
+
# Ensure Date column
|
| 46 |
df["Date"] = pd.to_datetime(df.index if "Date" not in df.columns else df["Date"], errors='coerce')
|
| 47 |
df = df.dropna(subset=["Date"]).reset_index(drop=True)
|
| 48 |
df["Date"] = df["Date"].dt.strftime("%d-%b-%Y")
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
# Safe Adj Close
|
| 51 |
+
if "Adj Close" not in df.columns:
|
| 52 |
+
df["Adj Close"] = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
# Daily change %
|
| 55 |
+
df["Change %"] = ((df["Close"] - df["Open"]) / df["Open"] * 100).round(2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
# Build HTML table
|
| 58 |
+
html_table = f'<div id="daily_dashboard" style="max-height:600px; overflow:auto; font-family:Arial,sans-serif;">'
|
| 59 |
+
html_table += '<table border="1" style="border-collapse:collapse; width:100%;">'
|
| 60 |
+
html_table += '<thead style="position:sticky; top:0; background:#1a4f8a; color:white;">'
|
| 61 |
+
html_table += '<tr><th>Date</th><th>Open</th><th>High</th><th>Low</th><th>Close</th><th>Adj Close</th><th>Volume</th><th>Change %</th></tr>'
|
| 62 |
+
html_table += '</thead><tbody>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
for idx, r in df.iterrows():
|
| 65 |
+
row_color = "#e8f5e9" if idx % 2 == 0 else "#f5f5f5"
|
| 66 |
+
change_color = "green" if r["Change %"] > 0 else "red" if r["Change %"] < 0 else "black"
|
| 67 |
+
|
| 68 |
+
html_table += f'<tr style="background:{row_color};">'
|
| 69 |
+
html_table += f'<td>{r["Date"]}</td>'
|
| 70 |
+
html_table += f'<td>{r["Open"]}</td>'
|
| 71 |
+
html_table += f'<td>{r["High"]}</td>'
|
| 72 |
+
html_table += f'<td>{r["Low"]}</td>'
|
| 73 |
+
html_table += f'<td>{r["Close"]}</td>'
|
| 74 |
+
html_table += f'<td>{r["Adj Close"]}</td>'
|
| 75 |
+
html_table += f'<td>{r["Volume"]}</td>'
|
| 76 |
+
html_table += f'<td style="color:{change_color}; font-weight:600;">{r["Change %"]}%</td>'
|
| 77 |
+
html_table += '</tr>'
|
| 78 |
+
|
| 79 |
+
html_table += '</tbody></table></div>'
|
| 80 |
+
|
| 81 |
+
persist.save(key, html_table, "html")
|
| 82 |
+
return html_table
|
| 83 |
|
| 84 |
except Exception as e:
|
| 85 |
return wrap_html(f"<h1>Error fetch_daily: {e}</h1><pre>{traceback.format_exc()}</pre>")
|