liurenchaxin/jixia_academy/ui/streamlit/tabs/openbb_tab.py

184 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import streamlit as st
import pandas as pd
import plotly.express as px
from datetime import datetime, timedelta
def _check_openbb_installed() -> bool:
try:
# OpenBB v4 推荐用法: from openbb import obb
from openbb import obb # noqa: F401
return True
except Exception:
return False
def _load_price_data(symbol: str, days: int = 365) -> pd.DataFrame:
"""Fetch OHLCV using OpenBB v4 when available; otherwise return demo/synthetic data."""
end = datetime.utcnow().date()
start = end - timedelta(days=days)
# 优先使用 OpenBB v4
try:
from openbb import obb
# 先尝试股票路由
try:
out = obb.equity.price.historical(
symbol,
start_date=str(start),
end_date=str(end),
)
except Exception:
out = None
# 若股票无数据,再尝试 ETF 路由
if out is None or (hasattr(out, "is_empty") and out.is_empty):
try:
out = obb.etf.price.historical(
symbol,
start_date=str(start),
end_date=str(end),
)
except Exception:
out = None
if out is not None:
if hasattr(out, "to_df"):
df = out.to_df()
elif hasattr(out, "to_dataframe"):
df = out.to_dataframe()
else:
# 兜底: 有些 provider 返回可序列化对象
df = pd.DataFrame(out) # type: ignore[arg-type]
# 规格化列名
if not isinstance(df, pd.DataFrame) or df.empty:
raise ValueError("OpenBB 返回空数据")
# 有的表以 index 为日期
if 'date' in df.columns:
df['Date'] = pd.to_datetime(df['date'])
elif df.index.name in ('date', 'Date') or isinstance(df.index, pd.DatetimeIndex):
df = df.copy()
df['Date'] = pd.to_datetime(df.index)
else:
# 尝试查找常见日期列
for cand in ['timestamp', 'time', 'datetime']:
if cand in df.columns:
df['Date'] = pd.to_datetime(df[cand])
break
# 归一化收盘价列
close_col = None
for cand in ['adj_close', 'close', 'Close', 'price', 'close_price', 'c']:
if cand in df.columns:
close_col = cand
break
if close_col is None:
raise ValueError("未找到收盘价列")
df['Close'] = pd.to_numeric(df[close_col], errors='coerce')
# 仅保留需要列并清洗
if 'Date' not in df.columns:
raise ValueError("未找到日期列")
df = df[['Date', 'Close']].dropna()
df = df.sort_values('Date').reset_index(drop=True)
# 限定时间窗口(有些 provider 可能返回更长区间)
df = df[df['Date'].dt.date.between(start, end)]
if df.empty:
raise ValueError("清洗后为空")
return df
except Exception:
# 如果 OpenBB 不可用或调用失败,进入本地演示/合成数据兜底
pass
# Fallback to demo from examples/data
try:
from pathlib import Path
root = Path(__file__).resolve().parents[2]
demo_map = {
'AAPL': root / 'examples' / 'data' / 'demo_results_aapl.json',
'MSFT': root / 'examples' / 'data' / 'demo_results_msft.json',
'TSLA': root / 'examples' / 'data' / 'demo_results_tsla.json',
}
path = demo_map.get(symbol.upper())
if path and path.exists():
df = pd.read_json(path)
if 'date' in df.columns:
df['Date'] = pd.to_datetime(df['date'])
if 'close' in df.columns:
df['Close'] = df['close']
df = df[['Date', 'Close']].dropna().sort_values('Date').reset_index(drop=True)
# 裁剪到时间窗口
df = df[df['Date'].dt.date.between(start, end)]
return df
except Exception:
pass
# Last resort: minimal synthetic data避免 FutureWarning
dates = pd.date_range(end=end, periods=min(days, 180))
return pd.DataFrame({
'Date': dates,
'Close': pd.Series(range(len(dates))).rolling(5).mean().bfill()
})
def _kpis_from_df(df: pd.DataFrame) -> dict:
if df.empty or 'Close' not in df.columns:
return {"最新价": "-", "近30日涨幅": "-", "最大回撤(近90日)": "-"}
latest = float(df['Close'].iloc[-1])
last_30 = df.tail(30)
if len(last_30) > 1:
pct_30 = (last_30['Close'].iloc[-1] / last_30['Close'].iloc[0] - 1) * 100
else:
pct_30 = 0.0
# max drawdown over last 90 days
lookback = df.tail(90)['Close']
roll_max = lookback.cummax()
drawdown = (lookback / roll_max - 1).min() * 100
return {
"最新价": f"{latest:,.2f}",
"近30日涨幅": f"{pct_30:.2f}%",
"最大回撤(近90日)": f"{drawdown:.2f}%",
}
def render_openbb_tab():
st.write("使用 OpenBB如可用或演示数据展示市场概览。")
col_a, col_b = st.columns([2, 1])
with col_b:
symbol = st.text_input("股票/ETF 代码", value="AAPL")
days = st.slider("时间窗口(天)", 90, 720, 365, step=30)
obb_ready = _check_openbb_installed()
if obb_ready:
st.success("OpenBB 已安装 ✅")
else:
st.info("未检测到 OpenBB将使用演示数据。可在 requirements.txt 中加入 openbb 后安装启用。")
with col_a:
df = _load_price_data(symbol, days)
if df is None or df.empty:
st.warning("未获取到数据")
return
# 绘制收盘价
if 'Date' in df.columns and 'Close' in df.columns:
fig = px.line(df, x='Date', y='Close', title=f"{symbol.upper()} 收盘价")
st.plotly_chart(fig, use_container_width=True)
else:
st.dataframe(df.head())
# KPI 卡片
st.markdown("#### 关键指标")
kpis = _kpis_from_df(df)
k1, k2, k3 = st.columns(3)
k1.metric("最新价", kpis["最新价"])
k2.metric("近30日涨幅", kpis["近30日涨幅"])
k3.metric("最大回撤(近90日)", kpis["最大回撤(近90日)"])
# 未来:基本面、新闻、情绪等组件占位
with st.expander("🚧 更多组件(即将推出)"):
st.write("基本面卡片、新闻与情绪、宏观指标、策略筛选等将逐步接入。")