diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-17 00:06:36 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-17 00:06:36 -0700 |
| commit | 2b229cff5f99a00b6cc984f4c1c3c41de7f1e04c (patch) | |
| tree | 706f19c58589aeef8bbfab6aa94a75daf50da70c | |
| parent | 5424b83d8173435632dd59f4072d37ac68d33593 (diff) | |
Add 1D and 5D intraday periods to overview chart
Adds get_intraday_history() to data_service.py (5m/30m bars, ttl=60s)
and wires it into the overview chart with HH:MM x-axis formatting,
market-hours filtering, intraday-only primary fetch, hidden comparison
buttons, and 1D as the default period on load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | components/overview.py | 37 | ||||
| -rw-r--r-- | services/data_service.py | 14 |
2 files changed, 41 insertions, 10 deletions
diff --git a/components/overview.py b/components/overview.py index 53358f0..c52cae2 100644 --- a/components/overview.py +++ b/components/overview.py @@ -6,10 +6,10 @@ import pandas as pd import streamlit as st import streamlit.components.v1 as components -from services.data_service import get_company_info, get_latest_price, get_price_history +from services.data_service import get_company_info, get_intraday_history, get_latest_price, get_price_history from utils.security import json_for_script -PERIODS = {"1mo": "1M", "3mo": "3M", "6mo": "6M", "1y": "1Y", "5y": "5Y"} +PERIODS = {"1d": "1D", "5d": "5D", "1mo": "1M", "3mo": "3M", "1y": "1Y", "5y": "5Y"} SECTOR_ETF_MAP = { "Technology": "XLK", "Communication Services": "XLC", @@ -69,6 +69,21 @@ def _to_int(val): def _series_points(symbol: str, period: str) -> list[dict]: + if period in ("1d", "5d"): + interval = "5m" if period == "1d" else "30m" + df = get_intraday_history(symbol, period=period, interval=interval) + if df is None or df.empty or "Close" not in df.columns: + return [] + closes = pd.to_numeric(df["Close"], errors="coerce") + try: + closes = closes.between_time("09:30", "16:00") + except Exception: + pass + out = [] + for idx, v in closes.dropna().items(): + ts = idx.strftime("%Y-%m-%dT%H:%M") + out.append({"d": ts, "c": float(v)}) + return out hist = get_price_history(symbol, period=period) if hist is None or hist.empty or "Close" not in hist.columns: return [] @@ -216,7 +231,8 @@ def render_overview(ticker: str): series = {} for period in PERIODS.keys(): bucket = {} - for sym in symbols: + fetch_syms = [tkr] if period in ("1d", "5d") else symbols + for sym in fetch_syms: pts = _series_points(sym, period) if pts: bucket[sym] = pts @@ -265,7 +281,7 @@ def render_overview(ticker: str): meta = { "updated_label": datetime.now().strftime("%Y-%m-%d %H:%M"), - "default_period": "1y", + "default_period": "1d", "default_mode": "price", "default_comparisons": ["^GSPC"], } @@ -440,10 +456,11 @@ def render_overview(ticker: str): '<section class="ov-controls">' + '<div class="ctl-group">' + '<span class="ctl-lbl">Period</span>' + + '<button class="ctl-btn active" id="btn-p-1d" onclick="setPeriod(\'1d\',this)">1D</button>' + + '<button class="ctl-btn" id="btn-p-5d" onclick="setPeriod(\'5d\',this)">5D</button>' + '<button class="ctl-btn" id="btn-p-1mo" onclick="setPeriod(\'1mo\',this)">1M</button>' + '<button class="ctl-btn" id="btn-p-3mo" onclick="setPeriod(\'3mo\',this)">3M</button>' - + '<button class="ctl-btn" id="btn-p-6mo" onclick="setPeriod(\'6mo\',this)">6M</button>' - + '<button class="ctl-btn active" id="btn-p-1y" onclick="setPeriod(\'1y\',this)">1Y</button>' + + '<button class="ctl-btn" id="btn-p-1y" onclick="setPeriod(\'1y\',this)">1Y</button>' + '<button class="ctl-btn" id="btn-p-5y" onclick="setPeriod(\'5y\',this)">5Y</button>' + "</div>" + '<div class="ctl-group">' @@ -465,7 +482,7 @@ def render_overview(ticker: str): "<script>" + payload_js + meta_js - + "var activePeriod='1y';var activeMode='price';var activeComparisons=['^GSPC'];" + + "var activePeriod='1d';var activeMode='price';var activeComparisons=['^GSPC'];" + "function cssVar(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();}" + "function withAlpha(hex,a){if(!hex||hex.charAt(0)!=='#'||hex.length!==7)return 'rgba(0,0,0,'+a+')';var r=parseInt(hex.substring(1,3),16),g=parseInt(hex.substring(3,5),16),b=parseInt(hex.substring(5,7),16);return 'rgba('+r+','+g+','+b+','+a+')';}" + "var C_FG1=cssVar('--fg-1');var C_FG2=cssVar('--fg-2');var C_FG3=cssVar('--fg-3');var C_LINE=cssVar('--line-1');var C_BRASS=cssVar('--brass');var C_BRASS_B=cssVar('--brass-bright');var C_POS=cssVar('--positive');var C_NEG=cssVar('--negative');var C_WARN=cssVar('--warning');var C_INFO=cssVar('--info');" @@ -476,7 +493,7 @@ def render_overview(ticker: str): + "function fmtRatio(v,d){var n=asNum(v);if(n===null)return '—';return n.toFixed(d);};" + "function fmtCurrency(v,d){var n=asNum(v);if(n===null)return '—';return '$'+n.toLocaleString(undefined,{minimumFractionDigits:d,maximumFractionDigits:d});}" + "function fmtLarge(v){var n=asNum(v);if(n===null)return '—';var a=Math.abs(n);if(a>=1e12)return (n<0?'-':'')+(a/1e12).toFixed(2)+'T';if(a>=1e9)return (n<0?'-':'')+(a/1e9).toFixed(2)+'B';if(a>=1e6)return (n<0?'-':'')+(a/1e6).toFixed(1)+'M';if(a>=1e3)return (n<0?'-':'')+(a/1e3).toFixed(1)+'K';return String(Math.round(n));}" - + "function periodLabel(p){return ({'1mo':'1M','3mo':'3M','6mo':'6M','1y':'1Y','5y':'5Y'})[p]||p;}" + + "function periodLabel(p){return ({'1d':'1D','5d':'5D','1mo':'1M','3mo':'3M','1y':'1Y','5y':'5Y'})[p]||p;}" + "function getSeries(sym){var b=((OVERVIEW_DATA.series||{})[activePeriod]||{});return b[sym]||[];}" + "function toTraceData(list){var x=[],y=[];list.forEach(function(r){if(r&&r.d&&r.c!==null&&r.c!==undefined){x.push(r.d);y.push(Number(r.c));}});return {x:x,y:y};}" + "function rebased(list){var clean=[];list.forEach(function(r){if(r&&r.d&&r.c!==null&&r.c!==undefined){clean.push({d:r.d,c:Number(r.c)});}});if(!clean.length)return {x:[],y:[]};var base=clean[0].c;if(!isFinite(base)||base<=0)return {x:[],y:[]};var x=[],y=[];clean.forEach(function(r){x.push(r.d);y.push(((r.c/base)-1)*100);});return {x:x,y:y};}" @@ -484,11 +501,11 @@ def render_overview(ticker: str): + "function renderKpis(){var s=OVERVIEW_DATA.stats||{};var rows=[['Market Cap',fmtLarge(s.marketCap)],['P/E (TTM)',fmtRatio(s.trailingPE,2)],['EPS (TTM)',fmtCurrency(s.trailingEps,2)],['Volume',fmtLarge(s.volume)],['Avg Volume',fmtLarge(s.averageVolume)],['Beta',fmtRatio(s.beta,2)]];var html='';rows.forEach(function(r){html+='<div class=\"ov-kpi\"><div class=\"lbl\">'+r[0]+'</div><div class=\"v num\">'+r[1]+'</div></div>';});document.getElementById('ov-kpis').innerHTML=html;}" + "function renderRangeCard(){var rg=OVERVIEW_DATA.range_52w||{};var lo=asNum(rg.low),hi=asNum(rg.high),px=asNum(rg.price);if(lo===null||hi===null||px===null||!(hi>lo)){document.getElementById('ov-range').innerHTML='<div class=\"short-cell\"><div class=\"lbl\">52W Range</div><div class=\"v num\">Unavailable</div></div>';return;}var pct=Math.max(0,Math.min(100,((px-lo)/(hi-lo))*100));var fromLow=((px-lo)/lo)*100;var toHigh=((hi-px)/px)*100;var html='';html+='<div class=\"range-grid\"><div class=\"edge num\">'+fmtCurrency(lo,2)+'</div><div class=\"mid num\">'+fmtCurrency(px,2)+'</div><div class=\"edge num\" style=\"text-align:right\">'+fmtCurrency(hi,2)+'</div></div>';html+='<div class=\"range-rail\"><div class=\"range-fill\" style=\"width:'+pct+'%\"></div><div class=\"range-pin\" style=\"left:calc('+pct+'% - 1px)\"></div></div>';html+='<div class=\"range-meta\"><span class=\"num\">'+fromLow.toFixed(1)+'% from low</span><span class=\"num\">'+toHigh.toFixed(1)+'% to high</span></div>';document.getElementById('ov-range').innerHTML=html;}" + "function renderShortCard(){var s=OVERVIEW_DATA.short_interest||{};var d=s.sharesShortDeltaPct;var dCls='';var dTxt='—';if(asNum(d)!==null){dCls=d>=0?'delta-pos':'delta-neg';dTxt=(d>=0?'+':'')+(d*100).toFixed(1)+'%';}var rows=[['Short % Float',asNum(s.shortPercentOfFloat)===null?'—':(s.shortPercentOfFloat*100).toFixed(2)+'%'],['Days to Cover',asNum(s.shortRatio)===null?'—':Number(s.shortRatio).toFixed(2)],['Shares Short',fmtInt(s.sharesShort)],['Vs Prior Month',dTxt,dCls]];var html='';rows.forEach(function(r){html+='<div class=\"short-cell\"><div class=\"lbl\">'+r[0]+'</div><div class=\"v num '+(r[2]||'')+'\">'+r[1]+'</div></div>';});document.getElementById('ov-short').innerHTML=html;}" - + "function renderCompButtons(){var root=document.getElementById('ov-comps');var arr=OVERVIEW_DATA.comparisons||[];var html='<span class=\"ctl-lbl\">Compare</span>';arr.forEach(function(c){var sym=c.symbol||'';var act=activeComparisons.indexOf(sym)>=0;html+='<button class=\"comp-btn '+(act?'active':'')+'\" onclick=\"toggleComparison(\\\''+sym+'\\\',this)\">'+esc(c.label||sym)+'</button>';});root.innerHTML=html;root.classList.toggle('hidden',activeMode!=='relative'||arr.length===0);}" + + "function renderCompButtons(){var root=document.getElementById('ov-comps');var arr=OVERVIEW_DATA.comparisons||[];var html='<span class=\"ctl-lbl\">Compare</span>';arr.forEach(function(c){var sym=c.symbol||'';var act=activeComparisons.indexOf(sym)>=0;html+='<button class=\"comp-btn '+(act?'active':'')+'\" onclick=\"toggleComparison(\\\''+sym+'\\\',this)\">'+esc(c.label||sym)+'</button>';});root.innerHTML=html;root.classList.toggle('hidden',activeMode!=='relative'||arr.length===0||activePeriod==='1d'||activePeriod==='5d');}" + "function setPeriod(period,btn){activePeriod=period;document.querySelectorAll('[id^=btn-p-]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');refreshAll();}" + "function setMode(mode,btn){activeMode=mode;document.querySelectorAll('[id^=btn-m-]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');renderCompButtons();renderChart();renderReadout();}" + "function toggleComparison(sym,btn){var i=activeComparisons.indexOf(sym);if(i>=0){activeComparisons.splice(i,1);}else{activeComparisons.push(sym);}if(!activeComparisons.length){activeComparisons=['^GSPC'];}renderCompButtons();renderChart();renderReadout();}" - + "function renderChart(){if(typeof Plotly==='undefined'){return;}var tkr=OVERVIEW_DATA.ticker;var base=getSeries(tkr);var traces=[];if(activeMode==='price'){var p=toTraceData(base);traces.push({x:p.x,y:p.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.2},fill:'tozeroy',fillcolor:withAlpha(C_BRASS,0.10)});}else{var p2=rebased(base);traces.push({x:p2.x,y:p2.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.4}});var idx=1;activeComparisons.forEach(function(sym){if(sym===tkr)return;var rr=rebased(getSeries(sym));if(!rr.x.length)return;var label=sym;var comps=OVERVIEW_DATA.comparisons||[];for(var i=0;i<comps.length;i+=1){if(comps[i].symbol===sym){label=comps[i].label||sym;break;}}traces.push({x:rr.x,y:rr.y,type:'scatter',mode:'lines',name:label,line:{color:REL_COLORS[idx%REL_COLORS.length],width:1.8}});idx+=1;});}var layout={height:320,margin:{l:52,r:14,t:14,b:34},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',hovermode:'x unified',font:{family:'IBM Plex Mono',color:C_FG3},xaxis:{showgrid:false,zeroline:false,tickfont:{family:'IBM Plex Mono'}},yaxis:{showgrid:true,gridcolor:C_LINE,zeroline:(activeMode==='relative'),zerolinecolor:C_LINE,tickprefix:(activeMode==='price'?'$':''),ticksuffix:(activeMode==='relative'?'%':''),tickformat:(activeMode==='relative'?'.1f':',.2f')},legend:{orientation:'h',x:0,y:1.12,bgcolor:'rgba(0,0,0,0)',font:{family:'IBM Plex Mono',color:C_FG2,size:11}}};Plotly.react('ov-chart',traces,layout,{displayModeBar:false,responsive:true});}" + + "function renderChart(){if(typeof Plotly==='undefined'){return;}var tkr=OVERVIEW_DATA.ticker;var base=getSeries(tkr);var traces=[];if(activeMode==='price'){var p=toTraceData(base);traces.push({x:p.x,y:p.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.2},fill:'tozeroy',fillcolor:withAlpha(C_BRASS,0.10)});}else{var p2=rebased(base);traces.push({x:p2.x,y:p2.y,type:'scatter',mode:'lines',name:tkr,line:{color:C_BRASS,width:2.4}});var idx=1;activeComparisons.forEach(function(sym){if(sym===tkr)return;var rr=rebased(getSeries(sym));if(!rr.x.length)return;var label=sym;var comps=OVERVIEW_DATA.comparisons||[];for(var i=0;i<comps.length;i+=1){if(comps[i].symbol===sym){label=comps[i].label||sym;break;}}traces.push({x:rr.x,y:rr.y,type:'scatter',mode:'lines',name:label,line:{color:REL_COLORS[idx%REL_COLORS.length],width:1.8}});idx+=1;});}var isIntraday=(activePeriod==='1d'||activePeriod==='5d');var layout={height:320,margin:{l:52,r:14,t:14,b:34},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',hovermode:'x unified',font:{family:'IBM Plex Mono',color:C_FG3},xaxis:{showgrid:false,zeroline:false,tickfont:{family:'IBM Plex Mono'},tickformat:isIntraday?'%H:%M':'%b %d'},yaxis:{showgrid:true,gridcolor:C_LINE,zeroline:(activeMode==='relative'),zerolinecolor:C_LINE,tickprefix:(activeMode==='price'?'$':''),ticksuffix:(activeMode==='relative'?'%':''),tickformat:(activeMode==='relative'?'.1f':',.2f')},legend:{orientation:'h',x:0,y:1.12,bgcolor:'rgba(0,0,0,0)',font:{family:'IBM Plex Mono',color:C_FG2,size:11}}};Plotly.react('ov-chart',traces,layout,{displayModeBar:false,responsive:true});}" + "function renderReadout(){var rg=OVERVIEW_DATA.range_52w||{};var lo=asNum(rg.low),hi=asNum(rg.high),px=asNum(rg.price);var txt='';if(lo!==null&&hi!==null&&px!==null&&hi>lo&&px>0){var above=((px-lo)/lo)*100;var below=((hi-px)/px)*100;txt='Stock is '+above.toFixed(1)+'% above 52-week low and '+below.toFixed(1)+'% below 52-week high.';}if(activeMode==='relative'){var t=rebased(getSeries(OVERVIEW_DATA.ticker));var tLast=t.y.length?t.y[t.y.length-1]:null;var sp=rebased(getSeries('^GSPC'));var spLast=sp.y.length?sp.y[sp.y.length-1]:null;if(tLast!==null&&spLast!==null){txt='Relative over '+periodLabel(activePeriod)+': '+OVERVIEW_DATA.ticker+' '+(tLast>=0?'+':'')+tLast.toFixed(1)+'% vs S&P 500 '+(spLast>=0?'+':'')+spLast.toFixed(1)+'%.';}}if(!txt){txt='Data is limited for this ticker and selected period, but chart controls remain available.';}document.getElementById('ov-readout').textContent=txt;}" + "function refreshAll(){renderSignals();renderKpis();renderRangeCard();renderShortCard();renderCompButtons();renderChart();renderReadout();}" + "function bootOverview(){refreshAll();if(typeof Plotly==='undefined'){setTimeout(refreshAll,350);setTimeout(refreshAll,900);}}" diff --git a/services/data_service.py b/services/data_service.py index 9c82e14..c481014 100644 --- a/services/data_service.py +++ b/services/data_service.py @@ -102,6 +102,20 @@ def get_price_history(ticker: str, period: str = "1y") -> pd.DataFrame: return pd.DataFrame() +@st.cache_data(ttl=60) +def get_intraday_history(ticker: str, period: str, interval: str) -> pd.DataFrame | None: + """Return intraday OHLCV history. Use period='1d'/interval='5m' or period='5d'/interval='30m'.""" + try: + t = yf.Ticker(ticker.upper()) + df = t.history(period=period, interval=interval) + if df is None or df.empty: + return None + df.index = pd.to_datetime(df.index) + return df + except Exception: + return None + + @st.cache_data(ttl=3600) def get_income_statement(ticker: str, quarterly: bool = False) -> pd.DataFrame: try: |
