需要特别说明的一点:在本手册中,当我提到“copilot”时,我指的不是GitHub上的Copilot插件。这里所说的copilot是一个内置在产品中的辅助系统,它通过调用各种数据工具并根据这些工具提供的结果来生成市场分析报告。
先决条件与所需工具
-
已安装Python 3.10或更高版本
-
拥有一个EODHD API密钥
-
拥有一个OpenAI API密钥
-
拥有一个可正常使用的本地开发环境(venv或conda都可以)
-
EODHD:它用于获取当日收盘价、基本财务数据以及新闻信息。
-
OpenAI:你会使用其聊天模型来生成分析报告,但前提是这些模型必须先获得所需的数据支持。
-
LangChain + LangGraph:这些工具与ReAct风格的代理机制结合使用,可以帮助模型决定应该调用哪些数据函数,从而生成简洁的分析报告。
-
Streamlit:它只是用于快速演示这个辅助系统如何作为一个可交互的产品界面来使用的工具而已。
目录
MVP的功能
从宏观角度来看,这个MVP只有一个任务:将一个常规问题转化为一份简短且可重复使用的市场分析报告。
你需要提供以下信息:
-
一只股票代码(例如AAPL.US)
-
指定的交易天数范围(比如60天或120天)
-
你真正想要了解的信息
-
可选参数,用于指定必须包含的部分内容,如基本数据、风险分析或新闻标题等
在实际操作中,这些信息会决定报告的具体内容。而可选参数的存在则是为了确保团队能够获得标准格式的报告。
最终,这个系统会返回两样结果:
-
一份结构清晰、用Markdown格式编写的简短报告,便于快速阅读
-
一组由工具生成的数据资料,这些原始数据可以直接在用户界面中展示,无需再次调用API
第二种输出结果非常重要。它能使应用程序运行得更加高效,同时也能确保所有数据的可核查性。
不可妥协的原则
这个MVP的设计初衷是作为一个完整的产品功能,而不是一个简单的演示工具。
-
所有数据分析都基于工具提供的原始数据,系统不会进行任何主观猜测
-
如果缺少某些数据,系统会明确告知用户这一点
-
不会提供未经处理的原始价格数据,也不会列出冗长的新闻列表
-
系统只会计算用户请求的具体内容
-
生成的报告格式类似于内部备忘录或Slack消息
一旦这种工作模式建立起来,就会带来很多好处。
首先,所有团队成员都可以重复使用这些统一格式的报告;同时,生成每周市场分析报告的速度也会大大加快。演示过程也会变得非常简单:只需输入查询参数,就能立即得到报告,并在旁边查看相关数据。
系统架构
我们的架构设计得非常简洁:只需要两个文件,各自承担明确的职责即可。
copilot.py – 核心执行模块
这个文件包含了让整个系统正常运行的所有关键组件:
-
用于获取价格、基本数据、新闻及风险信息的工具
-
代理程序的配置规则以及用户交互提示机制
-
一个名为run_brief()的函数,它接收输入参数并生成以下结果:
-
Markdown格式的报告文本
-
供用户界面使用的结构化数据资料
如果你以后想在其他地方重复使用这个系统,这个文件就是你需要保留的关键文件。
app.py – MVP外壳程序
这个文件主要负责实现Streamlit的用户界面:
-
用户输入界面,包括股票代码、时间范围、查询参数等选项
-
双栏布局:左侧显示报告内容,右侧展示数据分析和新闻标题
这个文件本身不包含任何数据处理逻辑,它只是调用run_brief()函数并展示返回的结果而已。
为何这种分离设计如此重要
如果将所有代码混合放在一个Streamlit脚本中,那么以后一旦需要更换技术框架,你就不得不重新编写所有的代码。而采用这种分离结构,你日后可以用FastAPI替换Streamlit,而无需修改核心逻辑。此外,将“产品逻辑”集中在一个地方,也能大大简化测试和迭代流程,同时还能避免出现用户界面代码与数据处理代码难以维护的问题。
copilot.py:构建后端引擎
在本节中,我们将构建后端引擎。完成本节的编写后,你将会得到一个可执行的函数——该函数接收查询请求,使用EODHD工具获取所需数据,并返回两样结果:一份适合人类阅读的Markdown格式摘要,以及一份供用户界面使用的结构化数据字典。
1. 导入包
我们尽量简化代码架构。这样做的目的不是为了炫耀所使用的工具,而是为了打造一个功能完备且易于维护的系统。
import json
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
import requests
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
eodhd_api_key = '你的EODHD API密钥'
openai_api_key = '你的OPENAI API密钥'
除了导入各种包之外,我们还在文件开头定义了eodhd_api_key和openai_api_key,这样文件就可以直接运行了。在实际部署环境中,这些配置信息通常会被存储在环境变量中。
2. 辅助函数
在开始使用各种工具或构建智能体之前,我们先编写了三个辅助函数。虽然这些函数与“人工智能”技术没有直接关系,但它们对于确保系统能够稳定运行却至关重要。
def normalize_ticker(t: str) -> str:
t = (t or "").strip().upper()
if not t:
return t
if "." in t:
return t
return f"{t}.US"
def _safe_json_loads(x: Any) -> Optional[Any]:
if x is None:
return None
if isinstance(x, (dict, list)):
return x
if not isinstance(x, str):
return None
try:
return json.loads(x)
except Exception:
return None
def get_eod_prices_raw(ticker: str, start: str, end: str) -> pd.DataFrame:
url = f"https://eodhd.com/api/eod/{ticker}"
params = {"from": start, "to": end, "api_token": eodhd_api_key, "fmt": "json"}
r = requests.get(url, params=params)
data = r.json()
if not isinstance(data, list) or not data:
return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume", "ticker"])
df = pd.DataFrame(data)
keep = [c for c in ["date", "open", "high", "low", "close", "volume"] if c in df.columns]
df = df[keep].copy()
df["ticker"] = ticker
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["date", "close")).sort_values("date").reset_index(drop=True)
return df
以下是这三个辅助函数的简要说明:
-
normalize_ticker()用于规范用户输入。人们有时会输入aapl、AAPL,或者加上空格输入AAPL.US。而EODHD要求输入的符号格式必须统一。这个函数会在进行任何API调用之前确保输入格式的一致性。 -
_safe_json_loads()的作用是处理从智能体返回的数据。这些数据可能是Python字典或列表,也可能是JSON字符串。这个辅助函数能够确保我们能够正确解析这些数据而不会引发错误。 -
get_eod_prices_raw()是一个基础功能模块,所有需要获取OHLCV数据的工具都会使用它,而不必每次都重新编写请求逻辑或进行数据清洗操作。该函数会利用EODHD的日终历史数据API获取数据,然后对数据进行整理和处理,确保后续工具能够使用到完整且格式正确的数据。
就是这么简单。没有什么复杂的环节,这样做只是为了让其余的代码具有可预测性而已。
3. 数据工具
在构建这个系统之前,我们需要一个可靠的数据层。
如果将这个项目开发成一款产品,那么这些工具就相当于你的“内部API”;它们决定了辅助系统能做什么、不能做什么。辅助系统只是调用这些工具,并将它们的输出结果整理成简洁的信息而已。
在这个最小可行产品中,每个工具都负责特定的任务,并且会返回结构紧凑的输出结果。这是有意为之的——你希望用户界面中的信息具有可预测的结构;同时,你也应该尽量避免将原始数据直接输入到模型中,除非确实有这样的需求。
@tool
def last_n_days_prices(ticker: str, n: int = 60) -> Dict[str, Any]:
"""
返回过去N个交易日的价格信息。输出结果结构简洁,不包含原始数据行。
"""
ticker = normalize_ticker(ticker)
end = datetime.utcnow().date().isoformat()
start = (datetime.utcnow().date() - timedelta(days=240)).isoformat()
df = get_eod_prices_raw(ticker, start, end)
if df.empty:
return {"ticker": ticker, "error": "no_price_data"}
df = df.tail(int(n)).reset_index(drop=True)
if df.empty:
return {"ticker": ticker, "error": "no_price_data"}
first_close = float(df.loc[0, "close"])
last_close = float(df.loc[len(df) - 1, "close"])
total_return = float((last_close / first_close) - 1.0)
return {
"ticker": ticker,
"n": int(n),
"start_date": str(df.loc[0, "date"].date()),
"end_date": str(df.loc[len(df) - 1, "date"].date()),
"first_close": first_close,
"last_close": last_close,
"total_return": total_return,
}
@tool
def fundamentals_snapshot(ticker: str) -> Dict[str, Any]:
"""
提供该股票的基本面信息摘要。返回一个结构简单的字典。
```
ticker = normalize_ticker(ticker)
url = f"https://eodhd.com/api/fundamentals/{ticker}"
params = {"api_token": eodhd_api_key, "fmt": "json"}
r = requests.get(url, params=params)
data = r.json()
if not isinstance(data, dict) or not data:
return {"ticker": ticker, "error": "no_data"}
highlights = data.get("Highlights", {}) or {}
general = data.get("General", "") or {}
valuation = data.get("Valuation", "") or {}
technicals = data.get("Technicals",**) or {}
return {
"ticker": ticker,
"name": general.get("Name"),
"sector": general.get("Sector"),
"industry": general.get("Industry"),
"market_cap": highlights.get("MarketCapitalization"),
"pe": valuation.get("TrailingPE"),
"pb": valuation.get("PriceBookMRQ"),
"profit_margin": highlights.get("ProfitMargin"),
"dividend_yield": highlights.get("DividendYield"),
"beta": technicals.get("Beta"),
}
@tool
def latest_news(ticker: str, limit: int = 5) -> List[Dict[str, Any]]:
"""
提供该股票的最新新闻标题。返回一个包含新闻摘要的字典列表。
```
ticker = normalize_ticker(ticker)
url = f"https://eodhd.com/api/news"
params = {"s": ticker, "limit": int(limit), "offset": 0, "api_token": eodhd_api_key, "fmt": "json"}
r = requests.get(url, params=params)
data = r.json()
if not isinstance(data, list) or not data:
return []
df = pd.DataFrame(data)
keep = [c for c in ["date", "title", "link", "source"] if c in df.columns]
df = df[keep].copy()
if "date" in df.columns:
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.sort_values("date", ascending=False)
out = df.head(int(limit)).reset_index(drop=True).to_dict(orient="records")
for row in out:
dt = row.get("date")
if isinstance(dt, (pd.Timestamp, datetime)):
row["date"] = dt.isoformat()
return out
@tool
def risk_metrics(ticker: str, start: str, end: str) -> Dict[str, Any]:
"""
计算该股票在指定时间窗口内的风险指标。
volatility_ann:基于日收益率计算得出的年化波动率
max_drawdown:该时间窗口内的最大回撤幅度
```
ticker = normalize_ticker(ticker)
df = get_eod_prices_raw(ticker, start, end)
if df.empty:
return {"ticker": ticker, "error": "no_price_data"}
df = df.sort_values("date").reset_index(drop=True)
df["ret"] = df["close"].pct_change().fillna(0.0)
vol_ann = float(df["ret"].std(ddof=0) * np.sqrt(252))
cummax = df["close"].cummax()
dd = (df["close"] / cummax) - 1.0
max_dd = float(dd.min())
first_close = float(df.loc[0, "close"])
last_close = float(df.loc[len(df) - 1, "close"])
total_return = float((last_close / first_close) - 1.0)
return {
"ticker": ticker,
"start_date": str(df.loc[0, "date"].date()),
"end_date": str(df.loc[len(df) - 1, "date"].date()),
"n": int(len(df)),
"total_return": total_return,
"volatility_ann": vol_ann,
"max_drawdown": max_dd,
}
@tool
def eod_prices(ticker: str, start: str, end: str) -> List[Dict[str, Any]:
"""
提供该股票的原始OHLCV数据。仅适用于那些需要使用其他工具无法实现的自定义计算。
```
ticker = normalize_ticker(ticker)
df = get_eod_prices_raw(ticker, start, end)
return json.loads(df.to_json(orient="records"))
让我们来了解一下这段代码中的关键部分。
1. `last_n_days_prices` – 价格数据窗口
大多数实际查询的起点都是类似“最近发生了什么?”这样的问题。
因此,这个工具的作用就是:它会获取足够多的每日数据,以确保能够覆盖过去N个交易日的信息(通过使用一个缓冲区来确保数据的完整性),然后返回一份简洁的汇总结果:
-
数据窗口的起始日期和结束日期
-
该时间段内的最高价和最低价
-
总回报率
-
实际被用于计算的数据交易天数
这个工具不会返回原始的数据记录。这样既可以避免输出信息量过大,也能保证用户界面的响应速度。
2. `fundamentals_snapshot` – 基本财务数据概览
这个工具用于提供快速的基本信息参考。在编写简短的报告时,你通常需要一个大致的估值参考依据,但又不希望将这个MVP系统发展成一个完整的财务分析系统。
因此,我们保持其简洁性。它会一次性调用EODHD财务数据API,然后提取出在报告中常用的几项关键信息:
-
市盈率、市净率
-
市值
-
所属行业及贝塔系数
-
还有一些可选的额外信息,比如股息收益率和利润率
如果某个数据字段缺失,系统会直接返回`None`,而不会进行任何猜测或处理。
3. `latest_news` – 最新新闻标题
如果没有相关的背景信息,单纯的价格变动是没有任何参考价值的。
这个工具会通过EODHD金融新闻API获取某个股票的最新新闻标题,并根据发布时间对它们进行排序,然后返回一份仅包含我们应用程序所需信息的简洁列表:
-
发布日期
-
新闻标题
链接地址
信息来源
我们在这里并不是要分析市场情绪,而是为了让报告的内容能够置于具体的背景信息之中。
4. `risk_metrics` – 风险指标
有时候,人们关心的问题不是“发生了什么?”,而是“这种价格变动的幅度有多大?”
这时,波动率和中值回撤率就显得非常有用了。这个工具会根据用户指定的起始日期和结束日期,获取该时间段内的每日收盘价数据,然后计算出以下指标:
-
基于每日回报率的年化波动率
-
该时间段内的最大中值回撤幅度
同时,它还会再次为同一时间段计算总回报率,以确保所有数据的一致性。
在实际应用中,这个工具应该只在用户明确要求提供风险分析信息时才被调用。因为它会消耗额外的计算资源,并需要发起更多的API请求。
5. `eod_prices` – 基础数据支持
这个工具是为将来可能进行的扩展功能预留的。
在大多数情况下,MVP版本并不需要原始的OHLCV数据记录。但一旦你需要自定义一些分析指标(比如移动平均线、ATR值、自定义信号或模式识别功能),那么原始数据就变得必不可少了。
因此,`eod_prices`会以字典列表的形式返回完整的每日数据记录。
使用规则很简单:除非确实有需要,否则不要调用这个函数。因为它会消耗较多的系统资源,而且也很容易导致令牌使用量急剧增加或应用程序运行速度变慢。
4. 测试数据工具(在copilot.py之外进行)
在代理程序开始执行任何操作之前,你需要确认数据层能够正常工作。这种测试并非为了娱乐,而是一种快速的验证手段,旨在回答以下三个问题:
-
我们能否顺利获取普通股票代码对应的数据而不会出现错误?
-
我们所依赖的那些字段确实存在吗?
-
输出结果是否看起来合理,这样生成的报告就不会包含无用的信息吧?
以下是我实际运行的测试代码片段。每个工具只调用了一次,并且我只打印了关键部分,因此代码量保持得比较简短。
print("\n--- 过去n天价格数据 ---")
out_price = last_n_days_prices.invoke({"ticker": "AAPL.US", "n": 60})
print(out_price)
print("\n--- 基本面数据概览 ---")
out_fund = fundamentals_snapshot.invoke({"ticker": "AAPL.US"})
print(out_fund)
print("\n--- 最新新闻 ---")
out_news = latest_news.invoke({"ticker": "AAPL.US", "limit": 5})
print(f"新闻条目数量: {len(out_news)}")
print(out_news[:2])
print("\n--- 风险指标 ---")
end = datetime.utcnow().date()
start = (end - timedelta(days=180)).isoformat()
out_risk = risk_metrics.invoke({"ticker": "AAPL.US", "start": start, "end": end})
print(out_risk)
print("\n--- 收盘价格数据(原始记录,小时间窗口) ---")
raw_rows = eod_prices.invoke({"ticker": "AAPL.US", "start": "2025-12-01", "end": "2026-01-15"})
print(f"记录条目数量: {len(raw_rows)}")
print(raw_rows[:2])

这些输出结果基本上证明了数据层是正常工作的。
last_n_days_prices提供了过去60个交易日的价格数据,其中开盘价为269.0,收盘价为248.04,总回报率为-7.79%。fundamentals_snapshot也返回了编写报告所需的关键信息,包括市盈率33.2048、市净率49.4443、市值约3.665万亿,以及所属行业和板块。
latest_news返回了5条格式统一的新闻记录,包含日期、标题和链接等信息。risk_metrics也正常运行了,但它使用的是不同的时间窗口(过去180个日历天数对应的是123个交易天数),因此其计算得出的总回报率为+18.65%,这与基于60个交易日的结果不一致。后来我们强制要求risk_metrics使用与last_n_days_prices相同的起止日期。
eod_prices按预期返回了32条原始数据记录。这里的日期字段以纪元格式显示,这是合理的,因为这个工具主要是用于内部计算,并非直接用于展示结果。
5. 创建代理程序
在这个阶段,所有这些组件才会真正组合成一个完整的代理系统,而不再是一堆独立的函数。我们需要定义代理程序的行为规则,指定它只能使用哪些工具,同时还要建立一种机制,以便将工具的输出结果传递给用户界面进行展示。
system_prompt = (
"你是一个嵌入在产品中的市场分析辅助工具。\n"
"规则如下:\n"
"1) 使用现有数据进行分析,切勿自行编造数字。\n"
"2) 不要直接输出原始的价格数据或冗长的新闻列表。\n"
"3) 如果用户没有提出具体要求,就不要进行相关计算。\n"
"4) 以清晰的Markdown格式输出信息,并使用分段结构来组织内容。\n"
"5) 保持信息简洁实用,就像一份内部仪表盘笔记一样。\n"
"工具使用指南:\n"
"- 使用`last_n_days_prices`获取最近几天的价格数据。\n"
"- 使用`fundamentals_snapshot`获取市盈率、市净率、市值、行业类别及贝塔系数等信息。\n"
"- 使用`latest_news`获取新闻标题。\n"
"- 只有在用户询问波动率或回撤率时,才使用`risk_metrics`。\n"
"- 仅在需要进行自定义计算时,才使用`eod_prices`。\n"
)
def _build_agent() -> Any:
llm = ChatOpenAI(
model='gpt-5-nano',
temperature=0,
api_key=openai_api_key,
)
tools = [last_n_days_prices, fundamentals_snapshot, latest_news, risk_metrics, eod_prices]
return create_react_agent(model=llm, tools=tools)
AGENT = _build_agent()
def _extract_artifacts(messages: List[Any]) -> Dict[str, Any]:
"""
从LangGraph发送的消息中提取工具生成的输出结果。
这样可以避免在Streamlit中多次调用相同的接口函数。\n"
"""
out: Dict[str, Any] = {}
for m in messages:
name = getattr(m, "name", None)
content = getattr(m, "content", None)
if not name:
continue
payload = _safe_json_loads(content)
if payload is None:
continue
if name.endswith("last_n_days_prices"):
out["price"] = payload
elif name.endswith("fundamentals_snapshot"):
out["valuation"] = payload
elif name.endswith("risk_metrics"):
out["risk"] = payload
elif name.endswith("latest_news"):
out["headlines"] = payload
return out
系统提示实际上就相当于一份契约。如果你不明确指定这些要求,代理程序最终就会偏离预定的方向——它可能会开始随意猜测数字、生成冗长的输出,或者执行你根本没有要求它做的事情。而这个明确的提示机制能够确保代理程序始终按照既定路径运行,工具的使用指南也能有效避免误用。
_build_agent()主要负责连接各个组件:一个模型、一套固定的工具,以及一个能够决定在何时使用哪些工具的ReAct代理程序。另一个关键部分是_extract_artifacts()——我们开发这个功能并不是仅仅为了生成一段美观的文字,而是为了让系统能够输出结构化的数据,这样用户界面才能顺利渲染这些信息。因此,在Streamlit中我们并不会再次调用那些接口,而是直接使用在代理程序运行过程中已经产生的结果。
6. 将代理程序转变为可被应用程序调用的后端服务
到目前为止,我们已经构建了工具和代理程序,但接下来需要做的就是将它们转化为应用程序可以像调用普通后端函数一样来使用的形式。输入一个参数,就能得到一份简报以及渲染用户界面所需的结构化数据。
def run_brief(
ticker: str,
n_days: int = 60,
include_fundamentals: bool = True,
include_risk: bool = False,
include_news: bool = True,
news_limit: int = 5,
) -> Tuple[str, Dict[str, Any]]:
"""
返回结果:
- 以markdown格式生成的简报
- 当使用了相关工具时,包含价格、估值、风险因素及头条新闻等信息的字典
"""
t = normalize_ticker(ticker)
request_parts = [
f"股票代码:{t}.",
f"计算过去{int(n_days)}个交易日的总收益。",
]
if include_fundamentals:
request_parts.append("获取基本财务数据,并报告市盈率、市净率、市值、所属行业及贝塔系数。")
if include_risk:
request_parts.append("计算同一时间窗口内的年化波动率和最大回撤幅度。」
request_parts.append("使用与收益计算相同的起止日期。")
if include_news:
request_parts.append(f"获取最近{int(news_limit)}条头条新闻,并简要引用它们。")
request_parts.append(
"撰写一份简短的市场分析报告,内容包括:市场现状、关键指标、可能的影响以及需要注意的事项。"
)
request_parts.append("保持简洁,不要复制原始数据。")
user.prompt = " ".join(request_parts)
response = AGENT.invoke(
{"messages": [("system", systemprompt), ("user", user_prompt)]}
)
messages = response.get("messages", [])
final_msg = messages[-1]
brief_md = getattr(final_msg, "content", "") or ""
artifacts = _extract_artifacts(messages)
return brief_md, artifacts
run_brief函数承担了两项任务。首先,它将“用户的需求”转化为一系列具体的指令,从而确保代理程序按照预定的路径执行操作。因此,它会先构建request_parts列表,而不是直接给用户一个空白的提示框然后听天由命。
其次,这个函数会返回两个结果:brief_md用于在应用程序的左侧显示,而artifacts则用于在右侧渲染。这些数据实际上都是通过_extract_artifacts(messages)获取的——这种方式能够高效地复用在代理程序运行过程中已经产生的结果,而无需再次调用相关函数来填充用户界面。
演示运行(在copilot.py文件之外进行)
以下是三段演示,展示了产品经理、创始人或分析师在实际使用该工具时的操作方式。每段演示都包含简短的设置步骤、实际运行的代码、输出结果,以及针对输出内容的解读。
演示1:基础信息查询
这是最常见的“提供当前情况”的请求类型。在输出结果中,你会看到一个窗口界面、相应的回答内容、关键财务数据,以及一篇简短的报道。
def run_agent(query: str):
resp = AGENT.invoke({"messages": [("system", system_prompt), ("user", query)]})
msgs = resp.get("messages", [])
final = msgs[-1].content if msgs else ""
print("\n" + "=" * 80)
print("查询内容:")
print(query)
print("\n回答结果:")
print(final)
return resp
resp = run_agent(
"股票代码:AAPL.US。计算过去60个交易日的总回报;获取基本财务数据,报告市盈率、市净率、市值、所属行业及贝塔系数;获取5条最新新闻并简要介绍;撰写一份包含‘基本情况’‘财务指标’‘可能意味着什么’及‘注意事项’等部分的简短市场分析报告。请保持内容简洁,不要复制原始数据行。"
)
输出结果:
================================================================================
查询内容:
股票代码:AAPL.US。计算过去60个交易日的总回报;获取基本财务数据,报告市盈率、市净率、市值、所属行业及贝塔系数;获取5条最新新闻并简要介绍;撰写一份包含‘基本情况’‘财务指标’‘可能意味着什么’及‘注意事项’等部分的简短市场分析报告。请保持内容简洁,不要复制原始数据行。
回答结果:
### 基本情况
- 时间范围:过去60个交易日(2025-10-28至2026-01-23)
- 价格走势:269.00 → 248.04
- 总回报:-7.79%
### 财务指标
- 行业:科技行业
- 市值:3.665万亿美元(3,665,126,490,112)
-市盈率:33.20
-市净率:49.44
-贝塔系数:1.09
### 可能的含义
- 在科技行业这个大环境下,过去60个交易日的总回报为-7.79%。
- 较高的市净率可能意味着投资者对该公司资产负债表状况或增长前景抱有较高预期。
- 当前的宏观环境存在一些风险因素(如美联储的政策动向、地缘政治/经济事件),这些因素可能会影响该股票的短期走势。
### 最新新闻简介
- 小盘股投资与大盘股投资的对比——探讨多元化投资策略的效果
- 随着政府关门担忧加剧,股票期货价格在美联储会议前开始下跌——宏观风险因素的影响
> 一位创始人曾从事消防工作,现在他正在开发一个人工智能项目——人工智能/科技领域的最新动态
> 道琼斯期货价格下跌,人们关注特朗普的关税政策、政府关门事件以及企业财报——市场关注点集中在财报和宏观经济形势上
- SPDR的SPTM指数覆盖范围较广,而Vanguard的VTV指数则侧重于价值股投资。哪种指数更适合投资者?——关于市场广度与价值投资的讨论
### 注意事项
- 这些数据反映了最新的情况;后续的数据更新可能会改变回报率、倍数及财务指标的值。
- 这里的市净率偏高,这可能反映的是市场对该公司资产价值的看法,而并非完全基于其盈利能力的评估。
- 本内容不构成投资建议,请结合更全面的背景信息以及您自身的风险承受能力来做出决策。
核心数据在于在明确指定的时间区间内(2025年10月28日至2026年1月23日),该指标出现了-7.79%的跌幅。 “各项指标”部分正好符合我们编写内部报告时的需求:它提供了相关行业的信息以及简洁的估值数据(市盈率、市净率、贝塔系数、市值等),但并不会变成一份详尽的基本面分析报告。
这些标题被刻意写得简短,且更侧重于宏观层面的内容。如果你只需要了解市场当前的总体趋势,而不是深入了解苹果公司的具体新闻动态,那么这样的格式就非常有用。
演示2:以风险为重点的简要分析报告(在同一时间区间内分析波动性及最大跌幅)
这种分析方式的重点是展示某段时间内市场的最糟糕表现。输出结果会同时显示回报率、波动性以及最大跌幅,而这些数据都是根据相同的日期计算得出的。
resp = run_agent(
"Ticker: MSFT.US. 计算过去90个交易日的总回报率。
同一时间区间内计算年化波动率及最大跌幅。
使用与回报率计算相同的起始日期和结束日期。
编写一份简短的市场分析报告,内容包括:市场概况、各项指标、可能意味着什么以及需要注意的事项。"
)
输出结果:
================================================================================
QUERY:
Ticker: MSFT.US. 计算过去90个交易日的总回报率。同一时间区间内计算年化波动率及最大跌幅。使用与回报率计算相同的起始日期和结束日期。编写一份简短的市场分析报告,内容包括:市场概况、各项指标、可能意味着什么以及需要注意的事项。
ANSWER:
## 市场分析报告 - MSFT.US
市场概况
- 时间区间:2025年9月16日至2026年1月23日(共90个交易日)
- 开始价格:509.04美元
- 结束价格:465.95美元
- 总回报率(仅考虑价格变化):在这段时间内为-8.46%(未包含股息)
各项指标
- 年化波动率:19.30%
- 最大跌幅:-18.07%(该时间区间内的最大价格跌幅)
可能意味着什么
- 这段时期市场出现了明显的下跌趋势,且波动性较高,导致在90个交易日后,该股票的价格处于较低水平。
- 大约18%的最大跌幅以及约19%的年化波动率表明,在未来一段时间内,该股票的价格波动幅度可能会比较大。
- 如果你在评估投资风险,那么这段数据说明你面临一定的下行风险,这可能会影响你近期的投资决策。
需要注意的事项
- 这个总回报率仅反映了股价的变化情况,未包含股息因素。
- 结果会随着所选时间区间的不同而有所差异,未来的表现也可能会有很大差别。
- 数据截至2026年1月23日,市场和基本面情况可能会迅速发生变化。如果你需要,我可以补充一些基本面的简要分析或相关新闻信息。
回报率为-8.46%,这一数据说明了股价的下跌方向;最大跌幅为-18.07%,这一数据则反映了在这段时间内市场最严重的波动情况,而这一点通常才是投资者真正关心的。年化波动率为19.30%,这一数据让你能够了解市场波动的剧烈程度。另外,请注意,这个时间区间是明确指定的(2025年9月16日至2026年1月23日),正是这样的规定使得这些指标具有可比性,并且可以重复使用来进行分析。
演示3:仅包含新闻信息的“变化情况分析”报告(除非有必要,否则不提供各项指标数据)
<这就是“快速提供相关背景信息”的工作流程。输出内容应该保持叙述性,不得添加任何额外的指标数据,因为查询要求明确禁止这样做。
resp = run_agent(
"Ticker: AAPL.US. 获取最新的7条头条新闻。"
"用6到8行简洁地总结‘发生了哪些变化’,重点关注主题而非每条具体的头条新闻。」
"除非有必要,否则不要计算收益数据。"
)
输出结果:
================================================================================
查询内容:
Ticker: AAPL.US. 获取最新的7条头条新闻,并用6到8行简洁地总结‘发生了哪些变化’,重点关注主题而非每条具体的头条新闻。除非有必要,否则不要计算收益数据。
回复结果:
## AAPL.US – 7条最新头条新闻:按主题分类的简要汇总
变化点(主题方向):
- 宏观风险再度成为关注焦点:在美联储会议召开之前,期货价格走势下跌,政府关门带来的担忧也影响了市场情绪。
- 政策风险依然存在:关于关税和政府关门的报道使得政策不确定性持续成为人们关注的焦点。
- 关于资产配置的讨论仍在继续:小盘股与大盘股的投资策略之争引发了关于资产多元化配置的讨论。
- 对成长型股票与价值型股票的关注角度也在发生变化:SPTM与VTV、VOOG与IWO等投资组合的选择反映了不同的风险偏好。
- 业绩报告季为市场带来了额外的波动性,这种宏观层面的不确定性也与这些因素共同影响了市场走势。
- 人工智能相关的话题也越来越受到关注:一篇关于某位创始人的人工智能相关经历的报道表明,人们对利用人工智能进行投资的兴趣正在逐渐增加。
这种处理方式确实做到了恰当的压缩。它并没有简单地列出七条头条新闻就结束分析,而是将这些新闻按照主题进行了分类(宏观风险、政策因素、资产配置策略、市场风格变化、业绩报告、人工智能相关话题)。更重要的是,它严格遵守了最初设定的要求:没有因为某些原因而添加任何收益或风险相关的指标,而这正是如果要将这个工具作为产品中的一个快速信息展示界面时所应该做到的。
构建Streamlit的最小可行产品
在这个阶段,我们的目标并不是打造一个完美的用户界面,而是要开发出一个能够向团队成员展示的功能性产品。
如果你是唯一的使用者,使用笔记本来开发这个工具也是完全可行的。但一旦你需要从项目经理、创始人或其他非技术背景的人员那里获得反馈,你就需要一个他们可以方便操作的界面。Streamlit正是将你的辅助功能整合成这种可用界面的最快方式,而且你无需从头开始构建前端开发环境。
用户界面设计:以查询内容为核心,提供可选参数
用户界面最大的变化在于将查询内容作为主要的输入方式。人们的思考方式本来就是这样的——他们不会先列出“60个交易日的数据、基本面信息以及7条头条新闻”,而是会首先提出一个具体的问题。
因此,侧边栏应该设置一个查询框,让用户可以输入像这样的内容:“请为我提供关于AAPL的60天简要分析报告,其中要包括基本面数据和5条头条新闻。”
其他控制选项则可以作为可选参数来使用。这些并不是“主要输入内容”,而是用于调整功能设置的选项。如果你的团队要求所有的分析报告中都必须包含基本面数据,你可以强制设置这一要求;如果你希望在整个工作流程中始终关注风险因素,也可以将其设置为必选选项;而如果对于你的具体使用场景来说,头条新闻带来的信息量过大,你也可以选择关闭这个功能。
双窗格布局:左侧显示分析报告,右侧展示数据数值
<一旦你点击“生成”按钮,你就希望生成的界面看起来更像一个产品展示页面,而不是一个聊天窗口。>

左侧是市场简报。你可以将其复制到Slack中,或者放入每周的备忘录中。这种简报是以叙述性的形式呈现的,并且内容经过压缩。
右侧则是基于各种工具生成的数据和分析结果。这些数据才是建立信任的基础。你可以直接查看回报区间、关键基本数据、风险指标以及头条新闻列表,而无需阅读冗长的段落。这样也能清楚地了解模型实际上是从哪些工具中获取了这些信息,而不是它自己进行了怎样的解读。
i. 应用程序框架
我们在这里并没有编写具体的逻辑代码,只是定义了应用程序的外层结构,使得这个应用看起来更像一个独立的产品界面,而不仅仅是一个简单的笔记本单元格。
import streamlit as st
import pandas as pd
from copilot import run_query
st.set_page_config(page_title="市场简报生成器", layout="wide")
st.title("市场简报生成器")
st.caption("LangChain + EODHD。提供简洁的基本信息,并附带基于工具的分析数据。")
这里最重要的代码行是from copilot import run_query。这一设计使得应用程序的结构更加清晰:Streamlit只负责处理用户界面,而copilot的具体逻辑则保存在copilot.py文件中。这样的分离结构意味着,如果你以后决定将同样的后端功能封装到FastAPI或其他类型的用户界面系统中,这些代码仍然可以重复使用。
st.set_page_config(..., layout="wide")这一设置主要是出于用户体验的考虑。由于我们希望在左侧显示市场简报,在右侧展示基于工具的分析数据,因此采用宽布局可以让输出内容看起来更加整齐、不拥挤。
ii. 输入面板
这个部分是用户界面的核心,因为它决定了人们如何使用这个工具。
之所以采用“先输入查询条件再生成结果”的设计方式,是因为这符合人们实际获取市场信息的方式。人们并不会先考虑“应该选择哪些选项”,而是会直接提出自己的问题。股票代码和时间区间这些选项仍然存在,但它们只是默认设置;只有当用户没有明确指定这些参数时,它们才会被自动使用。
我们还添加了“可选参数”功能,这样一些团队就可以确保报告内容的一致性。例如,即使用户在查询中没有要求,你也可以确保每份简报中都包含基本数据、风险指标或头条新闻等内容。
with st.sidebar:
st.header("输入信息")
query = st.text_area("查询条件", value="针对AAPL.US股票,计算过去60个交易日的总回报率;获取市盈率和市净率数据;提取最近5条头条新闻;并提供简报解读。")
default_ticker = st.text_input("默认股票代码(仅在用户未指定时使用)", value="AAPL.US")
default_n_days = st.slider("默认交易天数区间(仅在用户未指定时使用)", min_value=20, max_value=180, value=60, step=5)
st.divider()
with st.sidebar.expander("可选参数(强制包含)"):
include_fund = stcheckbox("基本数据(市盈率、市净率等)", value=False)
include_risk = st.checkbox("风险指标(波动性、回撤幅度等)", value=False)
include_news = stCheckbox("头条新闻", value=False)
news_limit = st.slider("头条新闻数量", min_value=3, max_value=10, value=5, step=1, disabled=not include_news)
run_btn = st.button("生成简报", type="primary")
query文本区域是主要的输入界面。在演示中,你可以直接粘贴那些在测试代理时使用过的提示信息。这样设计是有意为之的——这样做能确保产品界面与这款工具实际适用的工作流程保持一致。
default_ticker和default_n_days属于次要设置。只有当查询内容模糊不清时,这些参数才会发挥作用。在实际产品环境中,它们的重要性远超表面上看起来的程度。人们经常会输入“给我一份60天的分析报告”,却忘记指定相关代码,因为他们认为上下文信息已经明确确定了。默认值的设定可以避免整个操作流程因此失败。
扩展功能的设计体现了“团队统一规范”的理念——默认情况下,该功能是处于折叠状态的,这样就不会干扰普通用户的界面使用体验。但当需要使用固定模板时(比如“每份报告都必须包含基础数据和头条新闻”),这些控制选项依然可以被启用。
iii. 数据指标的呈现方式
分析报告本身固然有用,但在产品环境中,人们还需要能够方便地查看和重复利用这些数据数值。
因此,我们将输出结果分为两个层次来处理:
-
文字描述(即Markdown格式的分析报告)。
-
结构化数据(如价格区间、基础数据、风险分析结果以及头条新闻等)。
关键在于:我们不希望Streamlit仅仅为了展示数据指标就再次调用相关的工具。代理程序已经一次性调用了这些工具,因此我们可以直接从代理程序返回的结果中提取所需的数据,并将其呈现到用户界面上。
在copilot.py中提取工具输出结果
这个辅助函数会遍历LangGraph发送的消息列表,从中筛选出所有来自我们自定义工具的输出数据。最终它会生成一个结构统一的artifacts字典,供用户界面使用。
def _extract_artifacts(messages: List[Any]) -> Dict[str, Any]:
out = Dict[str, Any] = {}
for m in messages:
name = getattr(m, "name", None)
content = getattr(m, "content", None)
if not name:
continue
payload = _safe_json_loads(content)
if payload is None:
continue
if name.endswith("last_n_days_prices"):
out["price"] = payload
elif name.endswith("fundamentals_snapshot"):
out["valuation"] = payload
elif name.endswith("risk_metrics"):
out["risk"] = payload
elif name.endswith("latest_news"):
out["headlines"] = payload
return out
这就是连接“代理程序运行环境”与“用户界面”的桥梁。run_query()函数在执行完操作后会同时返回brief_md和artifacts这两个结果。
在app.py中呈现数据结果
在Streamlit框架中,我们将所有数据渲染逻辑集中放在一个地方。_render_metrics()函数会接收artifacts字典,并将其转换成用户界面中可以显示的格式。
def _render_metrics(artifacts: dict):
cols = st.columns(3)
price = artifacts.get("price")
valuation = artifacts.get("valuation")
risk = artifacts.get("risk")
headlines = artifacts.get("headlines")
with cols[0]:
st.subheader("价格信息")
if isinstance(price, dict) and "error" not in price:
st.metric("总回报", f"{price.get('total_return', 0.0) * 100:.2f}%")
stcaption(f"{price.get('start_date')} 至 {price.get('end_date')} . 周数={price.get('n')}")
st.write(
pd.DataFrame([price]).rename(
columns={
"first_close": "首次收盘价",
"last_close": "最终收盘价",
"total_return": "总回报",
}
).T
)
elif isinstance(price, dict) and "error" in price:
st.warning(price["error"])
else:
st.info("没有价格相关数据输出(未请求该数据或相关工具未被使用)。")
with cols[1]:
st.subheader("基本信息")
if isinstance(valuation, dict) and "error" not in valuation:
df = pd.DataFrame([valuation])
keep = ["ticker", "name", "sector", "market_cap", "pe", "pb", "beta", "dividend_yield", "profit_margin"]
keep = [c for c in keep if c in df.columns]
st.write(df[keep].T)
elif isinstance(valuation, dict) and "error" in valuation:
st.warning(valuation["error"])
else:
st.info("没有基本信息相关数据输出(未请求该数据或相关工具未被使用)。")
with cols[2]:
st.subheader("风险信息")
if isinstance(risk, dict) and "error" not in risk:
st(metric("年波动率", f"{risk.get('volatility_ann', 0.0) * 100:.2f}%")
st.metric("最大回撤幅度", f"{risk.get('max_drawdown', 0.0) * 100:.2f}%")
stcaption(f"{risk.get('start_date')} 至 {risk.get('end_date')}. 周数={risk.get('n')}")
st.write(pd.DataFrame([risk]).T)
elif isinstance(risk, dict) and "error" in risk:
st.warning(risk["error"])
else:
st.info("没有风险相关数据输出(未请求该数据或相关工具未被使用)。")
st.subheader("头条新闻")
if isinstance(headlines, list) and len(headlines) > 0:
for h in headlines:
title = h.get("title", "无标题")
link = h.get("link")
src = h.get("source")
dt = h.get("date")
line = f"- {title}"
if src:
line += f" ({src})"
if dt:
line += f" . {dt}"
if link:
st.markdown(f"{line} \n {link}")
else:
st.markdown(line)
else:
st.info("没有头条新闻相关数据输出(未请求该数据或相关工具未被使用)。")
这就是为什么整个应用程序会给人一种“像真正产品一样”的感觉。该模型能够生成简短的报告,但用户界面仍然会以固定的布局显示具体的数字数据。此外,我们并不会重新获取任何信息,而是直接展示在代理程序运行过程中已经返回的结果。
iv. 将用户界面与引擎连接起来
在这个阶段,Streamlit应用程序不应该进行复杂的“思考”,它只需要接收用户的输入,调用相应的函数,然后显示处理后的结果即可。
最初,copilot.py中的函数是run_brief(ticker, n_days, …)。但当我们改用基于查询的用户界面后,这种设计就不再适用了。因此我们将后端函数修改为run_query(query, default_ticker, default_n_days, force_..., …)。这样一来,应用程序的结构依然简洁,但引擎的功能却变得足够灵活,能够处理真正符合产品风格的请求。
这是更新后的run_query函数代码:
def run_query(
query: str,
default_ticker: str = "AAPL.US",
default_n_days: int = 60,
force_fundamentals: bool = True,
force_risk: bool = False,
force_news: bool = True,
news_limit: int = 5,
) -> Tuple[str, Dict[str, Any]]:
q = (query or "").strip()
if not q:
q = f"For {default_ticker}, compute total return over the last {int(default_n_days)} trading days."
constraints = [
"约束条件:",
"1) 必须使用现有的工具来获取数据,切勿自行编造数字。",
"2> 不要直接输出原始的价格数据或冗长的新闻列表。",
"3> 输出内容必须使用清晰的Markdown格式,并分为几个部分:概况、各项指标、可能的意义以及需要注意的事项。",
"4> 保持内容简短且有用。",
f"5> 如果查询中没有指定时间范围,那么默认使用过去{int(default_n_days)}个交易日的数据。",
f"6> 如果查询中没有指定股票代码,那么默认使用{normalize_ticker(default_ticker)}作为对象。",
]
if force_fundamentals:
constraints.append("7> 必须包含基本财务指标(市盈率、市净率、市值、所属行业、贝塔系数)。请使用fundamentals_snapshot函数来获取这些数据。")
if force_risk:
constraints.append("8> 必须包含风险指标(年化波动率及最大回撤幅度)。请使用risk_metrics函数来获取这些数据。」
constraints.append("9> 使用与计算回报范围相同的起始日期和结束日期来获取风险指标数据。")
if force_news:
constraints.append(f"10> 必须包含新闻标题。只提取恰好{int(news_limit)}条新闻,并使用latest_news函数来获取最新消息。")
user_prompt = "用户查询:\n" + q + "\n\n" + "\n".join(constraints)
response = AGENT.invoke(
{"messages": [("system", system.prompt), ("user", userprompt)]}
)
messages = response.get("messages", [])
final_msg = messages[-1] if messages else None
brief_md = getattr(final_msg, "content", "") or ""
artifacts = _extract_artifacts(messages)
return brief_md, artifacts
以下是app.py中的核心代码片段。这段代码仅在用户点击按钮时才会被执行。
if run_btn:
with stspinner("正在运行工具并生成简报..."):
brief_md, artifacts = run_query(
query=query,
default_ticker=default_ticker,
default_n_days=default_n_days,
force_fundamentals=include_fund,
force_risk=include_risk,
force_news=include_news,
news_limit=news_limit,
)
left, right = st.columns([1.2, 1])
with left:
st.subheader("市场简报")
st.markdown(brief_md)
with right:
st.subheader("基于工具的指标分析")
_render_metrics(artifacts)
else:
st.info("请在左侧输入所需参数,然后点击**生成简报**。")
这个接口会返回两样信息,原理与之前相同。brief_md是显示在左侧的Markdown简报内容,而artifacts则是可以在右侧直接展示的工具输出结果,无需再进行额外的API调用。
重要的变化在于引擎现在的处理方式:原本是由UI负责构建“request_parts”这个请求参数,现在UI只需传递原始查询语句和相关的执行标志即可。具体的执行逻辑被放在了run_query()函数内部,而非Streamlit框架中。这样的设计使得代码结构更加清晰,未来UI可能会发生变化,但产品的整体功能依然能够保持一致。
应用演示
这一部分展示了Streamlit最小可行产品版本的演示内容。这些示例查询语句可以直接在应用程序中使用,用来验证UI界面、工具调用以及简报输出的结果是否符合预期。
演示1:基础信息简报(包含总回报、估值及头条新闻)
这是默认的“了解当前情况”类型的查询语句。它会要求系统结合价格走势、基本面的简要分析以及几条头条新闻来提供完整的背景信息。
查询语句:
针对AAPL.US股票,计算过去60个交易日的总回报;获取市盈率和市净率数据;提取最新的5条头条新闻,并对其进行简要解读。
%[https://gumlet.tv/watch/6986e2cb4db88a967f4169a0/\]
演示2:以风险分析为主的工作流程(包含波动率及最大回撤幅度,不包含新闻内容)
这种查询方式适用于那些需要了解投资组合面临的风险程度,或者想要理解为什么某个持仓会带来不适感的情况,即使其整体回报并不极端。
查询语句:
针对MSFT.US股票,计算过去90个交易日的年化波动率及最大回撤幅度。报告内容要简短,不要包含头条新闻。
%[https://gumlet.tv/watch/6986e4b54db88a967f4190e4/\]
演示3:仅提供新闻信息的界面(不含其他指标数据)
这种查询方式是最快捷的“了解发生了什么变化”类型的方法。其核心在于简化信息呈现,除非确实有必要,否则不会显示回报率或风险指标等相关数据。
查询语句:
针对NVDA.US股票,提取最新的7条头条新闻,并用6到8行文字总结其中发生的变化。只需提及相关主题即可,无需列出所有标题;只有在确实需要时才计算回报率。
%[https://gumlet.tv/watch/6986e794924a60df4b1298c9/\]
实用注意事项
在实际使用中可能会遇到的一些问题
用户可能会输入各种不规范的股票代码格式,有些人会输入aapl,有些人会输入AAPL,还有人会直接粘贴AAPL.US。如果不在前期对输入内容进行标准化处理,就会花费大量时间去调试那些莫名其妙的API错误。这就是为什么会有normalize_ticker()这个函数的存在。
此外,还会遇到数据缺失的情况。有些股票代码对应的新闻信息可能不存在,某些基本面的数据也可能为空值,有时价格相关的API也会返回空结果。这些工具本身就会返回错误信息,Streamlit界面应该将这些错误以警告的形式显示出来,而不是让页面直接崩溃或显示空白内容。
最大的“无声杀手”其实是工具使用成本。eod_prices这个工具确实很有用,但它是导致应用程序运行速度变慢、模型处理的数据量增加的最主要原因。因此,应该将其作为一种应急措施来使用;默认情况下,应选择那些占用资源较少的工具,比如60天数据总结、基本情况快照以及标题列表等。
最后需要强调的是,输出结果出现偏差是客观存在的。如果让系统自由运行,它就会开始执行额外的操作,从而导致输出格式逐渐变差。解决这个问题的方法虽然有些繁琐,但却非常有效:必须严格控制输入提示的内容,限制可使用的工具数量,并保持输出格式的一致性。
适合这种MVP架构的简单扩展功能
一个简单的下一步功能就是实现多只股票代码的对比分析。使用相同的查询模式,针对两到三只股票进行查询,然后生成一份简洁的对比总结报告。
你还可以安排定期更新报告的功能。例如,可以每天或每周对关注列表中的股票进行一次数据查询,然后将结果推送到Slack或发送邮件。核心的处理流程依然是不变的。
缓存也是另一个能够快速提升系统性能的方法。可以通过(股票代码, 时间窗口)这样的键值对来缓存工具查询的结果,这样在重复执行相同操作时,就不会频繁访问API,从而保证用户界面的响应速度始终很快。
如果你希望这个功能能被集成到真正的产品中,可以将run_query()函数封装在一个FastAPI接口后面。Streamlit可以继续作为演示工具使用,而你的应用程序就可以像调用其他内部服务一样来使用这个后端接口。
总结
目前,你已经开发出了一个功能完备的市场辅助系统MVP版本。它能够接收自然语言形式的查询请求,通过相应的工具获取相关数据,然后生成简洁的摘要报告以及相关的指标信息供用户界面展示。真正的价值并不在于模型的响应结果,而在于这种可重复的工作流程,以及将数据处理引擎与应用程序清晰地分开设计的架构。
如果你正在开发金融科技产品,这种设计模式能够很好地满足用户的常见需求。团队通常已经掌握了各种基础数据(比如股票收盘价、基本财务信息、新闻报道等),但这些数据分散在不同的接口和仪表板上。通过添加这个辅助系统,就可以将这些数据整合成一份格式统一的市场分析报告,供项目经理、分析师或销售团队重复使用。此外,这种内部演示工具也非常实用,因为所有的数据都直观可见且便于追踪,而不会被隐藏在聊天记录中。
接下来的最佳步骤是,用真实的内部业务需求来测试这个系统一周时间,看看大家最常需要哪些功能。这样你就能判断是否需要添加缓存机制、多股票代码对比功能、定期更新报告的功能,或者API封装层。目前这个MVP版本已经足够用来验证这些功能的需求了,无需再进行过度开发。