aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/overview.py37
1 files changed, 27 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);}}"