核心结论: 个人投研系统的瓶颈从来不是"拿不到数据",而是缺一套可复用的数据口径。行情一个口径、财报一个口径、公告靠手动翻,每次复盘都要重建上下文。用 AI 改造的核心,是把采集、存储、分析、检索四层固化成管线——数据进得来、口径不漂移、结论可回溯,AI 才有用武之地。
口径漂移。 这次用前复权,下次忘了改;这次按报告期取数,下次按公告日取数。同一只票两次分析结论不同,却找不到哪里变了。
前视偏差。 用当前数据库里的"最新财报"回看去年,等于用了当时还没公布的数据。回测看着漂亮,实盘必然打脸。
检索靠记忆。 想找"去年三季度提过产能扩张"的公告,只能凭印象翻。信息在,但取不出来。
这三点都不是"缺数据"造成的,是缺工程约束。
层 | 职责 | 关键约束 |
|---|---|---|
数据层 | 多源采集、口径统一 | 复权方式、字段命名固定 |
存储层 | 本地落库、PIT 快照 | 公告日索引,禁止覆盖历史 |
分析层 | 因子计算、语义检索 | 只用当日及之前数据 |
应用层 | AI 摘要与备忘录 | 只归纳不预测 |
分层的意义是误差不跨层传播。数据口径错了,只改数据层;检索召回差,只改分析层。
import akshare as ak
import pandas as pd
SCHEMA = ["date", "open", "high", "low", "close", "volume"]
def fetch_price(symbol: str, start: str, end: str) -> pd.DataFrame:
"""日线行情,前复权固定不变"""
df = ak.stock_zh_a_hist(
symbol=symbol, period="daily",
start_date=start, end_date=end, adjust="qfq")
df = df.rename(columns={
"日期": "date", "开盘": "open", "收盘": "close",
"最高": "high", "最低": "low", "成交量": "volume"})
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()
return df[SCHEMA[1:]].astype(float)
def fetch_financial(symbol: str) -> pd.DataFrame:
"""财务指标,保留公告日用于 PIT 对齐"""
df = ak.stock_financial_abstract(symbol=symbol)
df["ann_date"] = pd.to_datetime(df["公告日期"])
return df.dropna(subset=["ann_date"]).sort_values("ann_date")关键工程细节有两条。复权方式写死在函数里,不留给调用方决定,避免混用。财务数据必须保留公告日,这是后面 PIT 查询的唯一依据。
DuckDB 比 SQLite 更适合分析型查询,直接读 DataFrame,支持列式扫描:
import duckdb
con = duckdb.connect("research.db")
def init_schema():
con.execute("""
CREATE TABLE IF NOT EXISTS price (
symbol VARCHAR, date DATE,
open DOUBLE, high DOUBLE, low DOUBLE,
close DOUBLE, volume DOUBLE,
PRIMARY KEY (symbol, date)
)""")
con.execute("""
CREATE TABLE IF NOT EXISTS financial (
symbol VARCHAR, ann_date DATE, period VARCHAR,
revenue DOUBLE, net_profit DOUBLE,
PRIMARY KEY (symbol, ann_date, period)
)""")
def upsert_price(symbol: str, df: pd.DataFrame):
tmp = df.reset_index()
tmp.insert(0, "symbol", symbol)
con.execute("INSERT OR REPLACE INTO price SELECT * FROM tmp")
def query_pit(symbol: str, as_of: str) -> pd.DataFrame:
"""按公告日查询:只返回当时已知的财务数据"""
return con.execute("""
SELECT * FROM financial
WHERE symbol = ? AND ann_date <= ?
ORDER BY ann_date DESC LIMIT 1
""", [symbol, as_of]).df()query_pit 是整套系统里最重要的一个函数。它保证任何时点的分析,只使用当时已经公布的数据。没有这一步,所有回测都是自欺欺人。
因子计算保持纯函数,输入输出都是 DataFrame:
import numpy as np
def compute_factors(df: pd.DataFrame,
windows=(5, 20, 60)) -> pd.DataFrame:
f = pd.DataFrame(index=df.index)
ret = df["close"].pct_change()
for w in windows:
f[f"mom_{w}"] = df["close"].pct_change(w)
f[f"vol_{w}"] = ret.rolling(w).std()
f[f"vr_{w}"] = df["volume"] / df["volume"].rolling(w).mean()
f["ma20"] = df["close"].rolling(20).mean()
f["ma60"] = df["close"].rolling(60).mean()
f["trend"] = (f["ma20"] > f["ma60"]).astype(int)
return f.dropna()公告检索用向量化方案,比关键词匹配召回率高得多:
import chromadb
from openai import OpenAI
client = OpenAI(api_key="sk-xxx", base_url="https://api.deepseek.com/v1")
col = chromadb.PersistentClient(path="./ann_db") \
.get_or_create_collection("announcements")
def index_announcement(doc_id: str, text: str, meta: dict):
chunks = [text[i:i+500] for i in range(0, len(text), 450)]
col.add(
ids=[f"{doc_id}_{i}" for i in range(len(chunks))],
documents=chunks,
embeddings=[client.embeddings.create(
model="text-embedding-3-small",
input=c).data[0].embedding for c in chunks],
metadatas=[meta] * len(chunks))
def search_ann(q: str, k: int = 3) -> list[dict]:
emb = client.embeddings.create(
model="text-embedding-3-small", input=q).data[0].embedding
hits = col.query(query_embeddings=[emb], n_results=k)
return [{"text": d, "meta": m}
for d, m in zip(hits["documents"][0], hits["metadatas"][0])]把结构化因子和检索结果拼成上下文,交给模型归纳:
import json
def build_memo(symbol: str, as_of: str) -> str:
px = con.execute(
"SELECT * FROM price WHERE symbol=? AND date<=? "
"ORDER BY date DESC LIMIT 120", [symbol, as_of]).df()
factors = compute_factors(px.set_index("date"))
fin = query_pit(symbol, as_of)
anns = search_ann(f"{symbol} 产能 扩张", k=3)
ctx = {
"factors": factors.iloc[-1].round(4).to_dict(),
"financial": fin.to_dict("records"),
"announcements": [a["text"][:200] for a in anns],
}
prompt = f"""以下是某标的的公开数据。请用客观语言归纳当前状态。
严禁给出买卖建议,严禁预测价格,严禁评价估值高低。
数据:{json.dumps(ctx, ensure_ascii=False)}
输出:一段不超过 300 字的客观描述。"""
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}],
temperature=0.2)
return resp.choices[0].message.content提示词里三条"严禁"是硬约束。AI 在这个系统里的定位是信息组织者,不是决策者。约束写得越明确,输出越稳定,也越不容易越界。
第一,口径写死在代码里。 复权方式、字段名、时间对齐规则,都封装进函数,不给调用方留选择空间。
第二,所有财务查询走 PIT。 任何绕过 ann_date 过滤的取数,都会引入前视偏差。
第三,公告检索用向量而非关键词。 "产能扩张"和"新增产线"是同一件事,但关键词匹配抓不到。
第四,AI 只做归纳。 提示词里明确禁止建议和预测,既合规也让输出可复用。
第五,全链路留痕。 每次生成备忘录,把当时的数据快照和检索结果一起存档。结论可以推翻,依据必须可查。
个人投研系统的价值不是"帮你选股",而是把重复的数据整理工作自动化,让你把时间花在真正需要判断的地方。数据采集、口径对齐、信息检索交给管线,解读和决策仍然是你自己的事。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。