Spaces:
Sleeping
Sleeping
Update app/daily.py
Browse files- app/daily.py +139 -29
app/daily.py
CHANGED
|
@@ -3,15 +3,17 @@ import yfinance as yf
|
|
| 3 |
import pandas as pd
|
| 4 |
from datetime import datetime as dt
|
| 5 |
import traceback
|
| 6 |
-
|
| 7 |
from . import persist
|
| 8 |
-
from .common import wrap_html
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# ===========================================================
|
| 11 |
# RAW DAILY FETCHER
|
| 12 |
# ===========================================================
|
| 13 |
def daily(symbol, date_end, date_start):
|
| 14 |
-
"""Fetch daily OHLCV from Yahoo Finance."""
|
| 15 |
print(f"[{dt.now().strftime('%Y-%m-%d %H:%M:%S')}] yf called for {symbol}")
|
| 16 |
|
| 17 |
start = dt.strptime(date_start, "%d-%m-%Y").strftime("%Y-%m-%d")
|
|
@@ -19,60 +21,168 @@ def daily(symbol, date_end, date_start):
|
|
| 19 |
|
| 20 |
df = yf.download(symbol + ".NS", start=start, end=end)
|
| 21 |
|
| 22 |
-
# Flatten MultiIndex columns if present
|
| 23 |
if isinstance(df.columns, pd.MultiIndex):
|
| 24 |
df.columns = df.columns.get_level_values(0)
|
| 25 |
|
| 26 |
-
# Remove column names / DataFrame name to avoid "Price" display
|
| 27 |
df.columns.name = None
|
| 28 |
df.index.name = None
|
| 29 |
|
| 30 |
return df
|
| 31 |
|
| 32 |
# ===========================================================
|
| 33 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# ===========================================================
|
| 35 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
key = f"daily_{symbol}"
|
| 37 |
if persist.exists(key, "html"):
|
| 38 |
cached = persist.load(key, "html")
|
| 39 |
if cached:
|
| 40 |
-
print(f"[{date_end}] Using cached daily for {symbol}")
|
| 41 |
return cached
|
| 42 |
|
| 43 |
try:
|
| 44 |
df = daily(symbol, date_end, date_start)
|
| 45 |
-
if df
|
| 46 |
-
return wrap_html(f
|
| 47 |
|
| 48 |
-
# Reset index if not simple RangeIndex
|
| 49 |
if not isinstance(df.index, pd.RangeIndex):
|
| 50 |
df.reset_index(inplace=True)
|
| 51 |
-
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
for col in numeric_cols:
|
| 55 |
if col in df.columns:
|
| 56 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 57 |
-
|
| 58 |
-
# Drop rows with missing essential data
|
| 59 |
df = df.dropna(subset=["Open","High","Low","Close","Volume"]).reset_index(drop=True)
|
| 60 |
|
| 61 |
# Format date
|
| 62 |
-
if "Date" in df.columns
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
|
| 72 |
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
except Exception as e:
|
| 78 |
-
return wrap_html(f
|
|
|
|
| 3 |
import pandas as pd
|
| 4 |
from datetime import datetime as dt
|
| 5 |
import traceback
|
|
|
|
| 6 |
from . import persist
|
| 7 |
+
from .common import wrap_html, format_large_number
|
| 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 |
print(f"[{dt.now().strftime('%Y-%m-%d %H:%M:%S')}] yf called for {symbol}")
|
| 18 |
|
| 19 |
start = dt.strptime(date_start, "%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 |
+
# TECHNICAL INDICATORS
|
| 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 |
+
# PLOTLY CHART GENERATOR
|
| 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")
|
| 97 |
if cached:
|
|
|
|
| 98 |
return cached
|
| 99 |
|
| 100 |
try:
|
| 101 |
df = daily(symbol, date_end, date_start)
|
| 102 |
+
if df.empty:
|
| 103 |
+
return wrap_html(f"<h1>No daily data for {symbol}</h1>")
|
| 104 |
|
|
|
|
| 105 |
if not isinstance(df.index, pd.RangeIndex):
|
| 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 |
# Format date
|
| 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 |
+
# Summary
|
| 123 |
+
summary_html = f"""
|
| 124 |
+
<div style="margin-bottom:10px; font-family:Arial,sans-serif;">
|
| 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 |
+
# Table with sparkline, volume, mini candle
|
| 142 |
+
html_table = f"""
|
| 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 |
+
for idx, r in df.iterrows():
|
| 155 |
+
row_color = "#e8f5e9" if idx%2==0 else "#f5f5f5"
|
| 156 |
+
change_color = "green" if r["Change %"]>0 else "red" if r["Change %"]<0 else "black"
|
| 157 |
+
start_idx = max(0, idx-spark_days+1)
|
| 158 |
+
close_trend = sparkline(df["Close"].iloc[start_idx:idx+1].tolist())
|
| 159 |
+
vol_trend = volume_bar(df["Volume"].iloc[start_idx:idx+1].tolist())
|
| 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 |
+
# Plotly chart
|
| 180 |
+
chart_html = plotly_dashboard(df, symbol)
|
| 181 |
|
| 182 |
+
# Combine full HTML
|
| 183 |
+
full_html = summary_html + html_table + chart_html
|
| 184 |
+
persist.save(key, full_html, "html")
|
| 185 |
+
return full_html
|
| 186 |
|
| 187 |
except Exception as e:
|
| 188 |
+
return wrap_html(f"<h1>Error fetch_daily: {e}</h1><pre>{traceback.format_exc()}</pre>")
|