From 74e8703d601695dbf714009f0bb3030cf4a336d8 Mon Sep 17 00:00:00 2001 From: Youssef Aitousarrah Date: Tue, 17 Feb 2026 12:55:12 -0800 Subject: [PATCH] Add daily price movement to charts and fix intraday data rendering - Add daily price movement display with color coding (green/red) - Add 1D (intraday) and 7D chart options with granular data: - 1D: 5-minute interval for detailed intraday view - 7D: hourly interval for smooth 7-day chart - Fix discontinuous chart rendering by plotting against sequential index for intraday data - Eliminate overnight/weekend gaps in hourly charts - Add timezone normalization for consistent date handling between daily and intraday data - Improve fallback logic when data is sparse - Better handling of yfinance column names (Datetime vs Date) Co-Authored-By: Claude Haiku 4.5 --- tradingagents/ui/pages/todays_picks.py | 177 +++++++++++++++++++------ 1 file changed, 138 insertions(+), 39 deletions(-) diff --git a/tradingagents/ui/pages/todays_picks.py b/tradingagents/ui/pages/todays_picks.py index b3ec629a..69b36b98 100644 --- a/tradingagents/ui/pages/todays_picks.py +++ b/tradingagents/ui/pages/todays_picks.py @@ -15,6 +15,7 @@ from tradingagents.ui.theme import COLORS, get_plotly_template, page_header, sig from tradingagents.ui.utils import load_recommendations TIMEFRAME_LOOKBACK_DAYS = { + "1D": 1, "7D": 7, "1M": 30, "3M": 90, @@ -23,8 +24,18 @@ TIMEFRAME_LOOKBACK_DAYS = { } +def _get_interval_for_timeframe(timeframe: str) -> str: + """Return appropriate data interval for a given timeframe.""" + if timeframe == "1D": + return "5m" # 5-minute for intraday detail + elif timeframe == "7D": + return "1h" # Hourly for smooth 7-day view + else: + return "1d" # Daily for longer timeframes + + @st.cache_data(ttl=3600) -def _load_price_history(ticker: str, period: str) -> pd.DataFrame: +def _load_price_history(ticker: str, period: str, interval: str = "1d") -> pd.DataFrame: try: from tradingagents.dataflows.y_finance import download_history except Exception: @@ -33,7 +44,7 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame: data = download_history( ticker, period=period, - interval="1d", + interval=interval, auto_adjust=True, progress=False, ) @@ -46,13 +57,17 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame: data = data.xs(target, level=1, axis=1).copy() data = data.reset_index() - date_col = "Date" if "Date" in data.columns else data.columns[0] + # yfinance uses "Datetime" for intraday and "Date" for daily data + date_col = next( + (c for c in ("Datetime", "Date") if c in data.columns), + data.columns[0], + ) close_col = "Close" if "Close" in data.columns else "Adj Close" if close_col not in data.columns: return pd.DataFrame() history = data[[date_col, close_col]].rename(columns={date_col: "date", close_col: "close"}) - history["date"] = pd.to_datetime(history["date"]) + history["date"] = pd.to_datetime(history["date"], utc=True).dt.tz_localize(None) history = history.dropna(subset=["close"]).sort_values("date") return history @@ -114,6 +129,11 @@ def _get_daily_movement(ticker: str) -> str: return "N/A" +def _is_intraday(timeframe: str) -> bool: + """Return True for timeframes that use intraday (sub-daily) data.""" + return timeframe in ("1D", "7D") + + def _build_dynamic_chart( history: pd.DataFrame, timeframe: str, ticker: str = "" ) -> tuple[go.Figure, str, str, str]: @@ -127,33 +147,74 @@ def _build_dynamic_chart( move_text = _format_move_pct(window) daily_move_text = _get_daily_movement(ticker) if ticker else "N/A" + intraday = _is_intraday(timeframe) + template = dict(get_plotly_template()) fig = go.Figure() - fig.add_trace( - go.Scatter( - x=history["date"], - y=history["close"], - mode="lines", - line=dict(color="rgba(148,163,184,0.22)", width=1.1), - hovertemplate="%{x|%b %d, %Y}
$%{y:.2f}", - name="History", + + if intraday: + # For intraday charts, plot against a sequential index to eliminate + # overnight and weekend gaps. Use customdata for hover timestamps. + window = window.reset_index(drop=True) + x_vals = list(range(len(window))) + + # Build tick labels: show date at the start of each trading day + tick_vals = [] + tick_labels = [] + prev_day = None + for i, row in window.iterrows(): + day = row["date"].date() + if day != prev_day: + tick_vals.append(i) + tick_labels.append(row["date"].strftime("%b %d")) + prev_day = day + + fig.add_trace( + go.Scatter( + x=x_vals, + y=window["close"], + mode="lines", + line=dict(color=line_color, width=2.4), + fill="tozeroy", + fillcolor=( + "rgba(34,197,94,0.18)" + if line_color == COLORS["green"] + else "rgba(239,68,68,0.18)" + ), + customdata=window["date"], + hovertemplate="%{customdata|%b %d %H:%M}
$%{y:.2f}", + name=f"{timeframe} Focus", + ) ) - ) - fig.add_trace( - go.Scatter( - x=window["date"], - y=window["close"], - mode="lines", - line=dict(color=line_color, width=2.8), - fill="tozeroy", - fillcolor=( - "rgba(34,197,94,0.18)" if line_color == COLORS["green"] else "rgba(239,68,68,0.18)" - ), - hovertemplate=f"{timeframe}
%{{x|%b %d, %Y}}
$%{{y:.2f}}", - name=f"{timeframe} Focus", + else: + # For daily charts, keep the original two-trace approach + fig.add_trace( + go.Scatter( + x=history["date"], + y=history["close"], + mode="lines", + line=dict(color="rgba(148,163,184,0.22)", width=1.1), + hovertemplate="%{x|%b %d, %Y}
$%{y:.2f}", + name="History", + ) + ) + fig.add_trace( + go.Scatter( + x=window["date"], + y=window["close"], + mode="lines", + line=dict(color=line_color, width=2.8), + fill="tozeroy", + fillcolor=( + "rgba(34,197,94,0.18)" + if line_color == COLORS["green"] + else "rgba(239,68,68,0.18)" + ), + hovertemplate=f"{timeframe}
%{{x|%b %d, %Y}}
$%{{y:.2f}}", + name=f"{timeframe} Focus", + ) ) - ) # Override template keys before expansion to avoid duplicate keyword args. template["height"] = 210 @@ -169,12 +230,23 @@ def _build_dynamic_chart( else: pad = max((y_max - y_min) * 0.08, y_max * 0.01) - fig.update_xaxes( - showticklabels=False, - showgrid=False, - range=[window["date"].min(), history["date"].max()], - rangeslider=dict(visible=False), - ) + if intraday: + fig.update_xaxes( + showticklabels=True, + showgrid=False, + tickvals=tick_vals, + ticktext=tick_labels, + tickfont=dict(size=9, color=COLORS["text_muted"]), + rangeslider=dict(visible=False), + ) + else: + fig.update_xaxes( + showticklabels=False, + showgrid=False, + range=[window["date"].min(), history["date"].max()], + rangeslider=dict(visible=False), + ) + fig.update_yaxes( showgrid=True, gridcolor="rgba(42,53,72,0.28)", @@ -186,18 +258,45 @@ def _build_dynamic_chart( def _render_single_dynamic_chart(ticker: str, timeframe: str) -> None: - base_history = _load_price_history(ticker, "1y") + # Load base history with daily data for context (full year) + base_history = _load_price_history(ticker, "1y", interval="1d") if base_history.empty: st.caption("No price history available for this ticker.") return - window = _slice_history_window(base_history, timeframe) - if window.empty: - st.caption(f"Not enough data to render {timeframe} window.") - return + # For 1D and 7D, load higher granularity (intraday) data + intraday = _is_intraday(timeframe) + if intraday: + # Map timeframe to yfinance period: 1D -> "1d", 7D -> "7d" + yf_period = {"1D": "1d", "7D": "7d"}[timeframe] + interval = _get_interval_for_timeframe(timeframe) + history_for_chart = _load_price_history(ticker, yf_period, interval=interval) + + if history_for_chart.empty or len(history_for_chart) < 2: + # Fallback to daily data + history_for_chart = base_history + intraday = False + + if intraday: + # Use all loaded intraday data directly — yfinance already + # returned exactly the period we asked for. + window = history_for_chart + else: + window = _slice_history_window(history_for_chart, timeframe) + else: + history_for_chart = base_history + window = _slice_history_window(history_for_chart, timeframe) + + if window.empty or len(window) < 2: + # Last-resort fallback: use all available data + if len(history_for_chart) >= 2: + window = history_for_chart.copy() + else: + st.caption(f"Not enough data to render {timeframe} window.") + return fig, move_text, move_color, daily_move_text = _build_dynamic_chart( - base_history, timeframe, ticker + history_for_chart, timeframe, ticker ) # Determine daily movement color @@ -330,7 +429,7 @@ def render(): chart_timeframe = st.radio( f"Timeframe for {ticker}", list(TIMEFRAME_LOOKBACK_DAYS.keys()), - index=2, + index=3, horizontal=True, label_visibility="collapsed", key=f"chart_tf_{ticker}_{idx}",