- 勾选阶段五第3项下线Python服务 - 描述将 backend/ 与 services/config-service/ 归档至 archive/python/ - 明确无Python组件下系统运行的验收标准
34 KiB
设计文档: 面向Rust的事件驱动数据微服务架构
1. 引言
1.1. 文档目的
本文档旨在为“基本面选股系统”设计一个完全基于Rust的、事件驱动的、去中心化的微服务架构。此设计将作为彻底替换现有Python组件、并构建下一代数据处理生态系统的核心技术蓝图。
新的架构目标是:
- 服务独立化:将每个外部数据源(Tushare, Finnhub等)封装成独立的、可独立部署和运行的微服务。
- 事件驱动:引入消息总线(Message Bus)作为服务间通信的主干,实现服务的高度解耦和异步协作。
- 数据中心化:所有微服务将标准化的数据写入一个由
data-persistence-service独占管理的中央数据库,实现“数据写入即共享”。 - 纯Rust生态:从前端网关到最末端的数据提供商,整个后端生态系统将100%使用Rust构建,确保端到端的类型安全、高性能和健壮性。
1.2. 核心架构理念
- 独立单元 (Independent Units): 每个服务都是一个完整的、自包含的应用程序,拥有自己的配置、逻辑和生命周期。
- 异步协作 (Asynchronous Collaboration): 服务之间通过发布/订阅消息进行通信,而非紧耦合的直接API调用。
- 单一事实源 (Single Source of Truth): 数据库是所有结构化数据的唯一事实源。服务通过向数据库写入数据来“广播”其工作成果。
2. 目标架构 (Target Architecture)
2.1. 架构图
+-------------+ +------------------+ +---------------------------+
| | HTTP | | | |
| Frontend |----->| API Gateway |----->| Message Bus |
| (Next.js) | | (Rust) | | (e.g., RabbitMQ, NATS) |
| | | | | |
+-------------+ +-------+----------+ +-------------+-------------+
| |
(Read operations) | | (Pub/Sub Commands & Events)
| |
+-----------------v------------------+ +------v------+ +----------------+ +----------------+
| | | Tushare | | Finnhub | | iFind |
| Data Persistence Service (Rust) |<---->| Provider | | Provider | | Provider |
| | | Service | | Service | | Service |
+-----------------+------------------+ | (Rust) | | (Rust) | | (Rust) |
| +-------------+ +----------------+ +----------------+
v
+-----------------------------------------------------+
| |
| PostgreSQL Database |
| |
+-----------------------------------------------------+
2.2. 服务职责划分
-
API Gateway (Rust):
- 面向前端的唯一入口 (BFF - Backend for Frontend)。
- 负责处理用户请求、认证鉴权。
- 将前端的查询请求转化为对
Data Persistence Service的数据读取调用。 - 将前端的操作请求(如“生成新报告”)转化为命令(Command)并发布到Message Bus。
-
*_provider-service(Rust):- 一组独立的微服务,每个服务对应一个外部数据API(如
tushare-provider-service)。 - 订阅Message Bus上的相关命令(如
FetchFinancialsRequest)。 - 独立调用外部API,对返回数据进行清洗、标准化。
- 调用
Data Persistence Service的接口,将标准化后的数据写入数据库。 - 操作完成后,可以向Message Bus发布事件(Event),如
FinancialsDataReady。
- 一组独立的微服务,每个服务对应一个外部数据API(如
-
Data Persistence Service (Rust):
- (职责不变) 数据库的唯一守门人。
- 为所有其他内部微服务提供稳定、统一的数据库读写gRPC/HTTP接口。
-
Message Bus (e.g., RabbitMQ, NATS):
- 整个系统的神经中枢,负责所有服务间的异步通信。
- 传递命令(“做什么”)和事件(“发生了什么”)。
3. 核心抽象与数据契约
3.1. DataProvider Trait (内部实现蓝图)
此Trait依然是构建每个独立Provider微服务内部逻辑的核心蓝图。它定义了一个Provider应该具备的核心能力。
// This trait defines the internal logic blueprint for each provider microservice.
#[async_trait]
pub trait DataProvider: Send + Sync {
// ... (trait definition remains the same as previous version) ...
fn get_id(&self) -> &'static str;
async fn get_company_profile(&self, symbol: &str) -> Result<CompanyProfile, DataProviderError>;
async fn get_historical_financials(&self, symbol: &str, years: &[u16]) -> Result<Vec<FinancialStatement>, DataProviderError>;
// ... etc ...
}
3.2. 标准化数据模型 (共享的数据契约)
这些模型是服务间共享的“通用语言”,也是存入数据库的最终形态,其重要性在新架构下更高。
// These structs are the shared "Data Contracts" across all services.
// Their definitions remain the same as the previous version.
#[derive(Debug, Clone, Serialize, Deserialize)] // Add Serialize/Deserialize for messaging
pub struct CompanyProfile { ... }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinancialStatement { ... }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketDataPoint { ... }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealtimeQuote { ... }
3.3. 消息/事件定义 (Message/Event Contracts)
这是新架构的核心,定义了在Message Bus上传递的消息格式。
use uuid::Uuid;
use serde::{Serialize, Deserialize};
// --- Commands (Instructions to do something) ---
#[derive(Serialize, Deserialize)]
pub struct FetchCompanyDataCommand {
pub request_id: Uuid,
pub symbol: String,
pub market: String, // To help providers route to the correct API endpoint
}
// --- Events (Notifications that something has happened) ---
#[derive(Serialize, Deserialize)]
pub struct CompanyProfilePersistedEvent {
pub request_id: Uuid,
pub symbol: String,
// We don't need to carry the full data, as it's now in the database.
// Interested services can query it.
}
#[derive(Serialize, Deserialize)]
pub struct FinancialsPersistedEvent {
pub request_id: Uuid,
pub symbol: String,
pub years_updated: Vec<u16>,
}
4. 数据工作流示例 (Example Data Workflow)
- 请求发起: 用户在前端请求
AAPL的分析报告。请求到达API Gateway。 - 命令发布:
API Gateway生成一个唯一的request_id,然后向Message Bus发布一个FetchCompanyDataCommand命令。 - 命令消费:
tushare-provider、finnhub-provider等所有订阅了此命令的服务都会收到消息。 - 独立执行:
finnhub-provider根据market和symbol,调用Finnhub API获取公司简介、财务、行情数据。- 数据获取成功后,它将数据转换为标准化的
CompanyProfile,Vec<FinancialStatement>等模型。 - 它调用
Data Persistence Service的接口,将这些标准化的数据写入数据库。 - 写入成功后,它向Message Bus发布
CompanyProfilePersistedEvent和FinancialsPersistedEvent等事件。 tushare-provider收到命令后,可能因为市场不匹配而直接忽略该消息。
- 下游响应: 一个潜在的
report-generator-service(图中未画出,属于业务层)可以订阅...PersistedEvent。当它收到了生成一份完整报告所需的所有数据事件后,便开始从数据库中拉取这些数据,进行AI分析,并将最终报告存回数据库。 - 前端轮询/通知:
API Gateway可以通过WebSocket或长轮询等方式,将最终报告的完成状态通知给前端。
5. 实施路线图 (Roadmap) - 更新于 2025-11-15
基于对项目现状的调研,本路线图已更新,明确标识了已完成的工作和接下来的行动计划。
✔ 阶段-1:容器化与初步服务拆分 (已完成)
- 核心服务已容器化:
data-persistence-service已完全开发、容器化,并通过Docker Compose与数据库和现有Python后端集成。 - 数据库已初始化: Rust服务的
migrations目录证实了数据库表结构已通过sqlx-cli创建和管理。 - Python后端部分重构: Python
backend服务已经作为客户端,通过HTTP API调用data-persistence-service来读写数据。 - 配置服务已拆分:
config-service作为一个独立的Python微服务也已存在并运行。 - 开发环境已建立: 整个系统可以通过
Docker Compose和Tilt一键启动。
阶段〇:奠定新架构的基石 (Laying the New Foundation)
- 1. 部署消息总线: 在
docker-compose.yml中添加一个消息总线服务 (NATS)。这是实现事件驱动架构的先决条件。 - 2. 创建共享契约库 (
common-contracts): 在services/下创建一个新的Rustcommon-contractscrate。- 将
data-persistence-service/src/dtos.rs和models.rs中的核心数据结构(如CompanyProfile,FinancialStatement等)迁移至此。 - 添加
architecture_module_specification.md中定义的消息契约 (FetchCompanyDataCommand等) 和可观测性结构 (HealthStatus,TaskProgress)。
- 将
- 3. 升级
data-persistence-service:- 使其依赖新的
common-contractscrate,替换掉本地的数据模型定义。 - 为其实现
SystemModule规范,即添加/health和/tasks端点。
- 使其依赖新的
阶段一:开发 alphavantage-provider-service (精确实现蓝图)
目标: 创建并实现 alphavantage-provider-service,使其成为我们新架构下的第一个功能完备、可独立运行的数据提供商微服务。
-
1. 项目初始化与依赖配置
- 任务: 基于我们的微服务模板,创建新的Rust项目
services/alphavantage-provider-service。 - 任务: 在其
Cargo.toml中添加核心依赖。# Cargo.toml [dependencies] # ... other dependencies like axum, tokio, etc. common-contracts = { path = "../common-contracts" } # Generic MCP Client rmcp = "0.8.5" # Message Queue (NATS) async-nats = "0.33" - 验收标准: 项目可以成功编译 (
cargo check)。
- 任务: 基于我们的微服务模板,创建新的Rust项目
-
2. 实现
SystemModule规范- 任务: 在
main.rs中启动一个Axum HTTP服务器。 - 任务: 实现强制的
/health端点,返回当前服务的健康状态。 - 任务: 实现强制的
/tasks端点。此端点需要从一个线程安全的内存存储(例如Arc<DashMap<Uuid, TaskProgress>>)中读取并返回所有正在进行的任务。 - 验收标准: 启动服务后,可以通过
curl或浏览器访问http://localhost:port/health和http://localhost:port/tasks并得到正确的JSON响应。
- 任务: 在
-
3. 实现核心业务逻辑:事件驱动的数据处理
- 任务: 实现连接到Message Bus并订阅
FetchCompanyDataCommand命令的逻辑。 - 任务: 当收到
FetchCompanyDataCommand命令时,执行以下异步工作流:- 在任务存储中创建并插入一个新的
TaskProgress记录。 - 从配置中读取
ALPHAVANTAGE_API_KEY,并构建MCP端点URL。 - 初始化通用的
rmcp客户端:let client = rmcp::mcp::Client::new(mcp_endpoint_url); - 使用
tokio::try_join!并行执行多个数据获取任务。注意:函数名是字符串,返回的是serde_json::Value。// 伪代码示例 let symbol = &command.symbol; let overview_task = client.query("OVERVIEW", &[("symbol", symbol)]); let income_task = client.query("INCOME_STATEMENT", &[("symbol", symbol)]); // ... 其他任务 match tokio::try_join!(overview_task, income_task, /*...*/) { Ok((overview_json, income_json, /*...*/)) => { // overview_json and income_json are of type serde_json::Value // ... 进入步骤 4 }, Err(e) => { /* ... */ } } - 在
try_join!前后,精确地更新内存中TaskProgress的状态。
- 在任务存储中创建并插入一个新的
- 验收标准: 在Message Bus中发布命令后,服务的日志能正确打印出从Alpha Vantage获取到的原始JSON数据。
- 任务: 实现连接到Message Bus并订阅
-
4. 实现数据转换与持久化 (强类型映射)
- 任务: (关键变更) 实现
TryFrom<serde_json::Value>Trait,完成从动态JSON到我们common-contracts模型的带错误处理的转换。// alphavantage-provider-service/src/mapping.rs use serde_json::Value; use common_contracts::models as our; impl TryFrom<Value> for our::CompanyProfile { type Error = anyhow::Error; // Or a more specific parsing error fn try_from(v: Value) -> Result<Self, Self::Error> { Ok(our::CompanyProfile { symbol: v["Symbol"].as_str().ok_or_else(|| anyhow!("Missing Symbol"))?.to_string(), name: v["Name"].as_str().ok_or_else(|| anyhow!("Missing Name"))?.to_string(), // ... 其他字段的安全解析和转换 }) } } - 任务: 创建一个类型化的HTTP客户端 (Data Persistence Client),用于与
data-persistence-service通信。 - 任务: 在所有数据转换成功后,调用上述客户端进行持久化。
- 验收标准: 数据库中查询到的数据,结构完全符合
common-contracts定义。
- 任务: (关键变更) 实现
-
5. 实现事件发布与任务完成
- 任务: 在数据成功持久化到数据库后,向Message Bus发布相应的数据就绪事件,如
CompanyProfilePersistedEvent和FinancialsPersistedEvent。 - 任务: 在所有流程执行完毕(无论成功或失败)后,从内存存储中移除对应的
TaskProgress对象(或将其标记为“已完成”并设置TTL)。 - 验收标准: 能够在Message Bus中监听到本服务发布的事件。
/tasks接口不再显示已完成的任务。
- 任务: 在数据成功持久化到数据库后,向Message Bus发布相应的数据就绪事件,如
阶段二:重构API网关与请求流程 (精确实现蓝图)
目标: 创建一个纯Rust的api-gateway服务,它将作为前端的唯一入口(BFF),负责发起数据获取任务、查询持久化数据以及追踪分布式任务进度。
-
1. 项目初始化与
SystemModule规范实现- 任务: 基于我们的微服务模板,创建新的Rust项目
services/api-gateway。 - 任务: 在其
Cargo.toml中添加核心依赖:axum,tokio,common-contracts,async-nats,reqwest,tracing,config。 - 任务: 实现强制的
/health端点。 - 任务: 实现强制的
/tasks端点。由于网关本身是无状态的、不执行长任务,此端点当前可以简单地返回一个空数组[]。 - 验收标准:
api-gateway服务可以独立编译和运行,并且/health接口按预期工作。
- 任务: 基于我们的微服务模板,创建新的Rust项目
-
2. 实现数据触发流程 (发布命令)
- 任务: 在
api-gateway中创建一个新的HTTP端点POST /v1/data-requests,它应接收一个JSON体,例如:{"symbol": "AAPL", "market": "US"}。 - 任务: 为此端点实现处理逻辑:
- 生成一个全局唯一的
request_id(UUID)。 - 创建一个
common_contracts::messages::FetchCompanyDataCommand消息,填入请求参数和request_id。 - 连接到Message Bus,并将此命令发布到
data_fetch_commands队列。 - 向前端立即返回
202 Accepted状态码,响应体中包含{ "request_id": "..." },以便前端后续追踪。
- 生成一个全局唯一的
- 验收标准: 通过工具(如Postman)调用此端点后,能够在NATS的管理界面看到相应的消息被发布,同时
alphavantage-provider-service的日志显示它已接收并开始处理该命令。
- 任务: 在
-
3. 实现数据查询流程 (读取持久化数据)
- 任务: 在
api-gateway中创建一个类型化的HTTP客户端 (Persistence Client),用于与data-persistence-service通信。 - 任务: 实现
GET /v1/companies/{symbol}/profile端点。该端点接收股票代码,通过Persistence Client调用data-persistence-service的相应接口,并将查询到的CompanyProfile数据返回给前端。 - 任务: (可选) 根据需要,实现查询财务报表、行情数据等其他数据类型的端点。
- 验收标准: 在
alphavantage-provider-service成功写入数据后,通过浏览器或curl调用这些新端点,可以查询到预期的JSON数据。
- 任务: 在
-
4. 实现分布式任务进度追踪
- 任务: 在
api-gateway的配置中,增加一个provider_services字段,用于列出所有数据提供商服务的地址,例如:["http://alphavantage-provider-service:8000"]。 - 任务: 实现
GET /v1/tasks/{request_id}端点。 - 任务: 该端点的处理逻辑需要:
- 读取配置中的
provider_services列表。 - 使用
tokio::join!或futures::future::join_all,并行地向所有provider服务的/tasks端点发起HTTP GET请求。 - 聚合所有服务的返回结果(一个
Vec<Vec<TaskProgress>>),并从中线性搜索与URL路径中request_id匹配的TaskProgress对象。 - 如果找到匹配的任务,将其作为JSON返回。如果遍历完所有结果都未找到,则返回
404 Not Found。
- 读取配置中的
- 验收标准: 当
alphavantage-provider-service正在处理一个任务时,通过api-gateway的这个新端点(并传入正确的request_id),能够实时查询到该任务的进度详情。
- 任务: 在
阶段三:逐步迁移与替换 (精确实现蓝图)
目标: 将前端应用无缝对接到新的Rust api-gateway,并随着数据提供商的逐步完善,最终彻底移除旧的Python backend服务,完成整个系统的架构升级。
-
1. 将
api-gateway集成到开发环境- 任务: 在根目录的
docker-compose.yml文件中,为我们新创建的api-gateway服务添加入口定义。# docker-compose.yml services: # ... other services api-gateway: build: context: ./services/api-gateway dockerfile: Dockerfile container_name: api-gateway environment: # 注入所有必要的配置 SERVER_PORT: 4000 NATS_ADDR: nats://nats:4222 DATA_PERSISTENCE_SERVICE_URL: http://data-persistence-service:3000/api/v1 # 注意: provider_services需要包含所有provider的内部地址 PROVIDER_SERVICES: '["http://alphavantage-provider-service:8000"]' ports: - "14000:4000" depends_on: - nats - data-persistence-service - alphavantage-provider-service - 任务: (如果尚未完成) 在
docker-compose.yml中添加nats服务。 - 验收标准: 运行
docker-compose up(或tilt up) 后,api-gateway服务能够成功启动并连接到消息总线。
- 任务: 在根目录的
-
2. 迁移前端应用的API调用逻辑
- 任务: 修改前端项目的环境变量,将API请求的目标从旧
backend指向新api-gateway。# frontend/.env.local (or in docker-compose.yml) NEXT_PUBLIC_BACKEND_URL=http://api-gateway:4000/v1 - 任务: 重构前端的数据获取Hooks(例如
useApi.ts)。- 旧逻辑: 发起一个长轮询GET请求,等待完整数据返回。
- 新逻辑:
- 触发: 发起一个
POST请求到/data-requests,并从响应中获取request_id。 - 轮询: 使用
useSWR或react-query的轮询功能,每隔2-3秒调用一次GET /tasks/{request_id}端点来获取任务进度。 - 展示: 根据任务进度更新UI(例如,显示加载条和状态信息)。
- 完成: 当任务状态变为 "completed" (或类似状态),或
GET /tasks/{request_id}返回404时,停止轮询,并调用GET /companies/{symbol}/profile等数据查询端点来获取最终数据并渲染。
- 触发: 发起一个
- 验收标准: 在前端页面输入股票代码并点击“生成报告”后,能够触发新的异步工作流,并在UI上看到实时进度的更新,最终成功展示由
alphavantage-provider-service获取的数据。
- 任务: 修改前端项目的环境变量,将API请求的目标从旧
阶段四:数据提供商生态系统扩展 (Data Provider Ecosystem Expansion)
目标: 将现有Python backend中的核心data_providers,逐一重写为独立的Rust微服务,丰富我们的数据维度。
-
0. (前置任务) 完成所有Provider的适配性分析
- 任务: 在开始大规模编码前,完成 附录A 中所有待迁移数据提供商的适配性分析,确保
common-contracts模型的完备性,并明确每个Provider的实现关键点。
- 任务: 在开始大规模编码前,完成 附录A 中所有待迁移数据提供商的适配性分析,确保
-
1. 实现
tushare-provider-service(中国市场核心)- 任务: 基于
alphavantage-provider-service模板,创建并实现服务的基本框架。 - 任务: 完成 Tushare 8个核心API的并行数据获取、聚合与报告期筛选逻辑。
- 任务: 在
mapping.rs中,精确复刻Python版本_calculate_derived_metrics方法中近20个派生财务指标的计算逻辑。 - 任务: 在
docker-compose.yml中添加此服务,并将其地址加入到api-gateway的PROVIDER_SERVICES环境变量中。 - 验收标准: 收到
market: "CN"的FetchCompanyDataCommand命令时,该服务能被触发,并成功将与Python版本逻辑一致的A股数据(包含所有派生指标)写入数据库。
- 任务: 基于
-
2. (可选) 迁移其他数据提供商
- 任务: 基于各自的适配性分析,创建并实现
finnhub-provider-service。 - 任务: 基于各自的适配性分析,创建并实现
yfinance-provider-service。 - 任务: 基于各自的适配性分析,创建并实现
ifind-provider-service。
- 任务: 基于各自的适配性分析,创建并实现
阶段五:业务逻辑迁移与最终替换 (Business Logic Migration & Final Replacement)
目标: 将Python backend中剩余的AI分析和配置管理逻辑迁移到Rust生态,并最终彻底下线Python服务。
-
1. 创建
report-generator-service- 任务: 创建一个新的Rust微服务
report-generator-service。 - 任务: 实现对Message Bus事件(如
FinancialsPersistedEvent)的订阅。 - 任务: 将原Python
backend中的analysis_client.py和company_profile_client.py的逻辑迁移至此服务。 - 验收标准: 当所有数据提供商完成数据写入后,此服务能被自动触发,并成功生成AI分析报告。
- 任务: 创建一个新的Rust微服务
-
2. (可选) 创建
config-service-rs- 任务: 用Rust重写现有的Python
config-service。 - 验收标准: 所有Rust微服务都能从新的配置服务中获取配置并正常启动。
- 任务: 用Rust重写现有的Python
-
3. 光荣退役:下线所有Python服务
- 前提条件: 所有数据获取和AI分析功能均已由新的Rust微服务完全承载。
- 任务: 在
docker-compose.yml中,删除backend和config-service的服务定义。 - 任务: 将
backend/和services/config-service/目录移动至archive/python/进行归档保留。 - 验收标准: 整个系统在没有任何Python组件的情况下,依然能够完整、正常地运行所有核心功能。架构升级正式完成。
附录A: 数据提供商适配性分析
本附录用于详细记录每个待迁移的数据提供商API与我们common-contracts标准模型之间的适配性。
A.1 Tushare 适配性分析
核心结论: 适配完全可行,但计算逻辑复杂。common-contracts无需调整。迁移工作的核心是精确复刻Python版本中近400行的财务数据聚合与派生指标计算逻辑。
1. 数据模型适配概览
common-contracts 模型 |
适配可行性 | 关键实现要点 |
|---|---|---|
CompanyProfile |
✅ 高 | 使用 stock_basic 和 stock_company 接口。 |
DailyMarketData |
✅ 高 | 关联 daily 和 daily_basic 接口。 |
RealtimeQuote |
⚠️ 中 | Tushare无直接对应接口,可使用最新日线数据作为“准实时”替代。 |
FinancialStatement |
✅ 高,但复杂 | (核心难点) 需聚合 balancesheet, income, cashflow, fina_indicator 等8个API,并复刻近20个派生指标的计算。 |
2. 关键迁移逻辑
- 多表聚合: Rust版本需实现并行调用多个Tushare API,并以
end_date为主键将结果聚合。 - 报告期筛选: 需复刻“今年的最新报告 + 往年所有年报”的筛选逻辑。
- 派生指标计算: 必须用Rust精确实现
_calculate_derived_metrics方法中的所有计算公式。
A.2 Finnhub 适配性分析
核心结论: 适配可行。Finnhub作为美股和全球市场的主要数据源,数据较为规范,但同样涉及多API聚合和少量派生计算。common-contracts无需调整。
1. 数据模型适配概览
common-contracts 模型 |
适配可行性 | 关键实现要点 |
|---|---|---|
CompanyProfile |
✅ 高 | 使用 /stock/profile2 接口。 |
DailyMarketData |
✅ 高 | 使用 /stock/candle 接口获取OHLCV,使用 /stock/metric 获取PE/PB等指标。 |
RealtimeQuote |
✅ 高 | 使用 /quote 接口。 |
FinancialStatement |
✅ 高,但需聚合 | 需聚合 /stock/financials-reported (按ic, bs, cf查询)返回的三张报表,并进行少量派生计算。 |
2. 关键迁移逻辑
- 多API聚合:
FinancialStatement的构建需要组合/stock/financials-reported接口的三次调用结果。DailyMarketData的构建也需要组合/stock/candle和/stock/metric。 - 派生指标计算: Python代码 (
finnhub.py) 中包含了自由现金流 (__free_cash_flow) 和其他一些比率的计算,这些需要在Rust中复刻。 - 字段名映射: Finnhub返回的字段名(如
netIncome)需要被映射到我们标准模型的字段名(如net_income)。
A.3 YFinance 适配性分析
核心结论: 适配可行,主要作为行情数据的补充或备用源。yfinance库的封装使得数据获取相对简单。
1. 数据模型适配概览
common-contracts 模型 |
适配可行性 | 关键实现要点 |
|---|---|---|
CompanyProfile |
✅ 中 | ticker.info 字典提供了大部分信息,但字段不如Finnhub或Tushare规范。 |
DailyMarketData |
✅ 高 | ticker.history() 方法是主要数据来源,可直接提供OHLCV。 |
RealtimeQuote |
⚠️ 低 | yfinance本身不是为实时流式数据设计的,获取的数据有延迟。 |
FinancialStatement |
✅ 中 | ticker.financials, ticker.balance_sheet, ticker.cashflow 提供了数据,但需要手动将多年度的数据列转换为按年份的记录行。 |
2. 关键迁移逻辑
- 数据结构转换:
yfinance返回的DataFrame需要被转换为我们期望的Vec<Record>结构。特别是财务报表,需要将列式(多年份)数据转换为行式(单年份)记录。 - 库的替代: Rust中没有
yfinance库。我们需要找到一个替代的Rust库 (如yahoo_finance_api),或者直接模拟其HTTP请求来获取数据。这将是迁移此模块的主要工作。
- 2. (可选) 迁移其他数据提供商
- 任务: 基于各自的适配性分析,创建并实现
finnhub-provider-service。 - 任务: 基于各自的适配性分析,创建并实现
yfinance-provider-service。 - 任务: (已暂停/待独立规划) 实现
ifind-provider-service。
- 任务: 基于各自的适配性分析,创建并实现
A.4 iFind 适配性分析 - 更新于 2025-11-16
核心结论: 当前阶段纯Rust迁移复杂度极高,任务已暂停。iFind的Python接口 (iFinDPy.py) 是一个基于 ctypes 的薄封装,它直接调用了底层的C/C++动态链接库 (.so 文件)。这意味着没有任何可见的网络协议或HTTP请求可供我们在Rust中直接模拟。
1. 迁移路径评估
基于对 ref/ifind 库文件的调研,我们确认了迁移此模块面临两个选择:
-
HTTP API方案 (首选,待调研):
- 描述: 您提到iFind存在一个HTTP API版本。这是最符合我们纯Rust、去中心化架构的理想路径。
- 工作量评估: 中等。如果该HTTP API文档齐全且功能满足需求,那么开发此服务的工作量将与
finnhub-provider-service类似。 - 规划: 此路径应作为一个独立的、后续的调研与开发任务。当前置于暂停状态。
-
FFI方案 (备选,不推荐):
- 描述: 在Rust服务中通过FFI(如
pyo3或rust-cpythoncrate)嵌入Python解释器,直接调用iFinDPy库。 - 工作量评估: 高。虽然可以复用逻辑,但这会引入技术栈污染,破坏我们纯Rust的目标,并显著增加部署和维护的复杂度(需要在容器中管理Python环境和iFind的二进制依赖)。这与我们“rustic”的确定性原则相悖。
- 描述: 在Rust服务中通过FFI(如
2. 最终决定
- 暂停实现:
ifind-provider-service的开发工作已正式暂停。 - 更新路线图: 在主路线图中,此任务已被标记为“已暂停/待独立规划”。
- 未来方向: 当项目进入下一阶段时,我们将启动一个独立的任务来专门调研其HTTP API,并基于调研结果决定最终的实现策略。
附录B: 业务逻辑模块迁移分析
本附录用于分析backend/app/services/中包含的核心业务逻辑,并为将其迁移至Rust服务制定策略。
B.1 analysis_client.py & company_profile_client.py
-
核心功能: 这两个模块是AI分析的核心,负责与大语言模型(如Gemini)API进行交互。
analysis_client.py: 提供一个通用的分析框架,可以根据不同的prompt_template执行任意类型的分析。它还包含一个SafeFormatter来安全地填充模板。company_profile_client.py: 是一个特化的版本,包含了用于生成公司简介的、具体的、硬编码的长篇Prompt。
-
迁移策略:
- 统一并重写为
report-generator-service: 这两个模块的功能应被合并并迁移到一个全新的Rust微服务——report-generator-service中。 - 订阅事件: 该服务将订阅Message Bus上的数据就绪事件(如
FinancialsPersistedEvent),而不是被HTTP直接调用。 - Prompt管理: 硬编码在
company_profile_client.py中的Prompt,以及analysis_client.py所依赖的、从analysis-config.json加载的模板,都应该由report-generator-service统一管理。在初期,可以从配置文件加载;未来,可以由Rust版的config-service-rs提供。 - 复刻
SafeFormatter: Python版本中用于安全填充模板的SafeFormatter逻辑需要在Rust中被等价复刻,以确保在上下文不完整时系统的健壮性。 - AI客户端: 使用
reqwest或其他HTTP客户端库在Rust中重新实现与大模型API的交互逻辑。
- 统一并重写为
-
结论: 迁移完全可行。核心工作是将Python中的Prompt管理和API调用逻辑,用Rust的异步方式重写。这将使AI分析任务成为一个独立的、可扩展的、事件驱动的后台服务。
B.2 config_manager.py
-
核心功能: 作为Python
backend内部的一个组件,它负责从config-service拉取配置,并与本地config.json文件进行合并。它还包含了测试各种配置有效性的逻辑(如测试数据库连接、Tushare Token等)。 -
迁移策略:
- 功能分散化:
ConfigManager本身不会作为一个独立的Rust服务存在,它的功能将被分散到每个需要它的微服务中。 - 配置拉取: 每个Rust微服务(
api-gateway,tushare-provider等)在启动时,都将负责独立地从环境变量或未来的config-service-rs中获取自己的配置。我们为每个服务编写的config.rs模块已经实现了这一点。 - 配置测试逻辑: 测试配置的逻辑(如
_test_database,_test_tushare)非常有用,但不属于运行时功能。这些逻辑可以被迁移到:- 独立的CLI工具: 创建一个Rust CLI工具,专门用于测试和验证整个系统的配置。
- 服务的
/health端点: 在每个服务的/health检查中,可以包含对其依赖服务(数据库、外部API)连通性的检查,从而在运行时提供健康状况反馈。
- 功能分散化:
-
结论:
ConfigManager的功能将被**“肢解”并吸收**到新的Rust微服务生态中,而不是直接迁移。
B.3 data_persistence_client.py
-
核心功能: 这是一个HTTP客户端,用于让Python
backend与其他微服务(data-persistence-service)通信。 -
迁移策略:
- 模式复用: 这个模块本身就是我们新架构模式的一个成功范例。
- Rust等价实现: 我们在
alphavantage-provider-service中创建的persistence.rs客户端,以及即将在api-gateway和report-generator-service中创建的类似客户端,正是data_persistence_client.py的Rust等价物。 - 最终废弃: 当Python
backend最终被下线时,这个客户端模块也将随之被废弃。
-
结论: 该模块无需迁移,其设计思想已被我们的Rust服务所采纳和实现。