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 <noreply@anthropic.com>
This commit is contained in:
parent
f4dba2940c
commit
74e8703d60
|
|
@ -15,6 +15,7 @@ from tradingagents.ui.theme import COLORS, get_plotly_template, page_header, sig
|
||||||
from tradingagents.ui.utils import load_recommendations
|
from tradingagents.ui.utils import load_recommendations
|
||||||
|
|
||||||
TIMEFRAME_LOOKBACK_DAYS = {
|
TIMEFRAME_LOOKBACK_DAYS = {
|
||||||
|
"1D": 1,
|
||||||
"7D": 7,
|
"7D": 7,
|
||||||
"1M": 30,
|
"1M": 30,
|
||||||
"3M": 90,
|
"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)
|
@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:
|
try:
|
||||||
from tradingagents.dataflows.y_finance import download_history
|
from tradingagents.dataflows.y_finance import download_history
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -33,7 +44,7 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
|
||||||
data = download_history(
|
data = download_history(
|
||||||
ticker,
|
ticker,
|
||||||
period=period,
|
period=period,
|
||||||
interval="1d",
|
interval=interval,
|
||||||
auto_adjust=True,
|
auto_adjust=True,
|
||||||
progress=False,
|
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.xs(target, level=1, axis=1).copy()
|
||||||
|
|
||||||
data = data.reset_index()
|
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"
|
close_col = "Close" if "Close" in data.columns else "Adj Close"
|
||||||
if close_col not in data.columns:
|
if close_col not in data.columns:
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
history = data[[date_col, close_col]].rename(columns={date_col: "date", close_col: "close"})
|
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")
|
history = history.dropna(subset=["close"]).sort_values("date")
|
||||||
return history
|
return history
|
||||||
|
|
||||||
|
|
@ -114,6 +129,11 @@ def _get_daily_movement(ticker: str) -> str:
|
||||||
return "N/A"
|
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(
|
def _build_dynamic_chart(
|
||||||
history: pd.DataFrame, timeframe: str, ticker: str = ""
|
history: pd.DataFrame, timeframe: str, ticker: str = ""
|
||||||
) -> tuple[go.Figure, str, str, str]:
|
) -> tuple[go.Figure, str, str, str]:
|
||||||
|
|
@ -127,9 +147,48 @@ def _build_dynamic_chart(
|
||||||
move_text = _format_move_pct(window)
|
move_text = _format_move_pct(window)
|
||||||
daily_move_text = _get_daily_movement(ticker) if ticker else "N/A"
|
daily_move_text = _get_daily_movement(ticker) if ticker else "N/A"
|
||||||
|
|
||||||
|
intraday = _is_intraday(timeframe)
|
||||||
|
|
||||||
template = dict(get_plotly_template())
|
template = dict(get_plotly_template())
|
||||||
|
|
||||||
fig = go.Figure()
|
fig = go.Figure()
|
||||||
|
|
||||||
|
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}<br>$%{y:.2f}<extra></extra>",
|
||||||
|
name=f"{timeframe} Focus",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For daily charts, keep the original two-trace approach
|
||||||
fig.add_trace(
|
fig.add_trace(
|
||||||
go.Scatter(
|
go.Scatter(
|
||||||
x=history["date"],
|
x=history["date"],
|
||||||
|
|
@ -148,7 +207,9 @@ def _build_dynamic_chart(
|
||||||
line=dict(color=line_color, width=2.8),
|
line=dict(color=line_color, width=2.8),
|
||||||
fill="tozeroy",
|
fill="tozeroy",
|
||||||
fillcolor=(
|
fillcolor=(
|
||||||
"rgba(34,197,94,0.18)" if line_color == COLORS["green"] else "rgba(239,68,68,0.18)"
|
"rgba(34,197,94,0.18)"
|
||||||
|
if line_color == COLORS["green"]
|
||||||
|
else "rgba(239,68,68,0.18)"
|
||||||
),
|
),
|
||||||
hovertemplate=f"{timeframe}<br>%{{x|%b %d, %Y}}<br>$%{{y:.2f}}<extra></extra>",
|
hovertemplate=f"{timeframe}<br>%{{x|%b %d, %Y}}<br>$%{{y:.2f}}<extra></extra>",
|
||||||
name=f"{timeframe} Focus",
|
name=f"{timeframe} Focus",
|
||||||
|
|
@ -169,12 +230,23 @@ def _build_dynamic_chart(
|
||||||
else:
|
else:
|
||||||
pad = max((y_max - y_min) * 0.08, y_max * 0.01)
|
pad = max((y_max - y_min) * 0.08, y_max * 0.01)
|
||||||
|
|
||||||
|
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(
|
fig.update_xaxes(
|
||||||
showticklabels=False,
|
showticklabels=False,
|
||||||
showgrid=False,
|
showgrid=False,
|
||||||
range=[window["date"].min(), history["date"].max()],
|
range=[window["date"].min(), history["date"].max()],
|
||||||
rangeslider=dict(visible=False),
|
rangeslider=dict(visible=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
fig.update_yaxes(
|
fig.update_yaxes(
|
||||||
showgrid=True,
|
showgrid=True,
|
||||||
gridcolor="rgba(42,53,72,0.28)",
|
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:
|
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:
|
if base_history.empty:
|
||||||
st.caption("No price history available for this ticker.")
|
st.caption("No price history available for this ticker.")
|
||||||
return
|
return
|
||||||
|
|
||||||
window = _slice_history_window(base_history, timeframe)
|
# For 1D and 7D, load higher granularity (intraday) data
|
||||||
if window.empty:
|
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.")
|
st.caption(f"Not enough data to render {timeframe} window.")
|
||||||
return
|
return
|
||||||
|
|
||||||
fig, move_text, move_color, daily_move_text = _build_dynamic_chart(
|
fig, move_text, move_color, daily_move_text = _build_dynamic_chart(
|
||||||
base_history, timeframe, ticker
|
history_for_chart, timeframe, ticker
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine daily movement color
|
# Determine daily movement color
|
||||||
|
|
@ -330,7 +429,7 @@ def render():
|
||||||
chart_timeframe = st.radio(
|
chart_timeframe = st.radio(
|
||||||
f"Timeframe for {ticker}",
|
f"Timeframe for {ticker}",
|
||||||
list(TIMEFRAME_LOOKBACK_DAYS.keys()),
|
list(TIMEFRAME_LOOKBACK_DAYS.keys()),
|
||||||
index=2,
|
index=3,
|
||||||
horizontal=True,
|
horizontal=True,
|
||||||
label_visibility="collapsed",
|
label_visibility="collapsed",
|
||||||
key=f"chart_tf_{ticker}_{idx}",
|
key=f"chart_tf_{ticker}_{idx}",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue