首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 AI 搭建个人投研系统:从数据管线到语义检索的四层架构

用 AI 搭建个人投研系统:从数据管线到语义检索的四层架构

原创
作者头像
IT大佬 jzit-top
发布于 2026-10-14 17:16:37
发布于 2026-10-14 17:16:37
320
举报

核心结论: 个人投研系统的瓶颈从来不是"拿不到数据",而是缺一套可复用的数据口径。行情一个口径、财报一个口径、公告靠手动翻,每次复盘都要重建上下文。用 AI 改造的核心,是把采集、存储、分析、检索四层固化成管线——数据进得来、口径不漂移、结论可回溯,AI 才有用武之地。

一、个人投研的三个真实痛点

口径漂移。 这次用前复权,下次忘了改;这次按报告期取数,下次按公告日取数。同一只票两次分析结论不同,却找不到哪里变了。

前视偏差。 用当前数据库里的"最新财报"回看去年,等于用了当时还没公布的数据。回测看着漂亮,实盘必然打脸。

检索靠记忆。 想找"去年三季度提过产能扩张"的公告,只能凭印象翻。信息在,但取不出来。

这三点都不是"缺数据"造成的,是缺工程约束。

二、四层架构

层

职责

关键约束

数据层

多源采集、口径统一

复权方式、字段命名固定

存储层

本地落库、PIT 快照

公告日索引,禁止覆盖历史

分析层

因子计算、语义检索

只用当日及之前数据

应用层

AI 摘要与备忘录

只归纳不预测

分层的意义是误差不跨层传播。数据口径错了,只改数据层;检索召回差,只改分析层。

三、数据层:采集与口径统一

代码语言:javascript
复制
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 + PIT 快照

DuckDB 比 SQLite 更适合分析型查询,直接读 DataFrame,支持列式扫描:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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()

公告检索用向量化方案,比关键词匹配召回率高得多:

代码语言:javascript
复制
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])]

六、应用层:AI 生成投研备忘录

把结构化因子和检索结果拼成上下文,交给模型归纳:

代码语言:javascript
复制
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 删除。

目录
  • 一、个人投研的三个真实痛点
  • 二、四层架构
  • 三、数据层:采集与口径统一
  • 四、存储层:DuckDB + PIT 快照
  • 五、分析层:因子与语义检索
  • 六、应用层:AI 生成投研备忘录
  • 七、工程化要点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档