Morningstar 证券主数据(Security Master)
Morningstar 是 RightCapital 证券主数据(Security Master) 的主要来源——为所有证券(个股、基金、ETF)提供参考数据的数据库,包括价格、分类、各类标识符。
代码仓库:gitlab.rightcapital.io/integrations/morningstar 技术栈:Laravel 12 (PHP 8.4)、PostgreSQL(双数据库)、AWS S3、FTP 负责人:Yan Hu、Tingsong Xu
它提供哪些数据?
Section titled “它提供哪些数据?”| 类别 | 示例 |
|---|---|
| 标识符 | Morningstar ID、Performance ID、CUSIP(部分)、ISIN、Ticker |
| 基础信息 | 名称、类型(个股/基金)、发行公司、交易所 |
| 价格 | 每日收盘价 |
| 分类 | 行业(sector)、细分行业(industry)、类别(category)、持仓风格 |
| 基金详情 | 费用率、各维度配置占比(资产类型、债券信用等级、股票风格、市场、行业) |
| 组合构成 | 股票风格箱构成占比、债券信用等级构成占比 |
双数据库设计
Section titled “双数据库设计”该服务运行在两个 PostgreSQL 数据库上(通过两个独立的 Laravel 连接 morningstar 和 api):
| 数据库 | 用途 | 关键表 |
|---|---|---|
| morningstar(默认连接) | 从 Morningstar 文件提取的原始数据 | securities、companies、funds、prices、files、imports |
api(DB_API_* 环境变量) | RightCapital 核心模型(core-model)schema | securities、equities、funds、allocations_by_*、fund_*_portfolios、prices |
api 连接不是一份拷贝——DB_API_HOST/DB_API_DATABASE 直接指向 retail-api 自己的生产 PostgreSQL 实例(RDS 主机 prd-retail-pg*,数据库 rightcapital)。morningstar 服务用自己的数据库账号(morningstar)直接写入这些表,并使用与 retail-api 相同版本族的 rightcapital/core-models composer 包,保证表名/字段名保持一致。中间没有任何 ETL 环节或副本——sync:push 就是对 retail 主数据库的实时写入。
flowchart TD
FTP["Morningstar FTP 服务器<br/><code>ftp.morningstar.com</code>"]
S3["AWS S3 存储桶"]
MDB["Morningstar 数据库<br/>(原始数据)"]
ADB["API 数据库<br/>(core-models)"]
RAPI["Retail API"]
FTP -->|"<b>retrieve</b><br/>每小时,仅生产环境<br/>ZIP/GZ 文件"| S3
S3 -->|"<b>data:import</b><br/>下载并解压"| MDB
MDB -->|"<b>data:transform</b><br/>解析 allocations,<br/>portfolios, styles"| MDB
MDB -->|"<b>sync:push</b><br/>写入 core-models"| ADB
ADB -->|"<b>webhook</b><br/>POST /v2/webhooks/"| RAPI
完整的每日流水线通过 php artisan process 运行(美东时间凌晨 4:15,周二至周六):
1. 文件下载(retrieve)
Section titled “1. 文件下载(retrieve)”执行频率:每小时一次(仅生产环境),由 Cronitor 监控。
| 步骤 | 命令 | 具体做什么 |
|---|---|---|
| 下载 | file:retrieve | 连接 ftp.morningstar.com(用户名:ondemandrightcapital)。按 config/morningstar.php 里配置的规则列出匹配文件。校验触发文件(.ctrl / _trigger.txt)是否存在。把 ZIP/GZ 文件上传到 S3。 |
| 状态更新 | file:update-status | 把 imports 表里的记录状态从 NEW 改为 READY。 |
2. 数据导入(data:import)
Section titled “2. 数据导入(data:import)”把文件从 S3 下载到 /var/opt/morningstar,解压,并运行各个 extractor。
数据分组:
| 分组 | 内容 | Extractor |
|---|---|---|
equity | 个股参考数据、公司信息、价格 | SecurityReference、CompanyReference、AssetClassification、Price |
fund_FO/FE/FC/FM | 基金数据(DataWarehouse XML)、价格 | DataWarehouse37、AssetClassification30、Price |
每个分组都有日频 + 月频两种文件变体,从指定的 FTP 目录下载:
- 个股:
/Daily/NRA/Reference_v3/、/Daily/NRA/Price/等 - 基金:
/Daily/DataWarehouse_v3/、/Modules/daily/Prices/等
3. 数据转换(data:transform)
Section titled “3. 数据转换(data:transform)”按 5,000 条一批处理证券:
- 对每个证券运行
Transformer - 解析 6 个维度的配置占比(资产类型、债券信用等级、类别、股票风格、市场、行业)
- 把 Morningstar 的分类体系映射到 RightCapital 的 14 个资产类别
- 把转换结果以 JSON 形式存入
securities.derived字段 - 把价格存入
morningstar.prices表
关键映射文件:Transformers/Mapping.php —— 定义了:
- 14 个类别(Large Growth、Bonds 等)
- 9 种持仓风格(Large Value、Mid Growth 等)
- 11 个行业(Technology、Healthcare 等)
- 地理市场划分和市值规模划分
- 债券信用等级(AAA 到 Unrated)
4. 同步推送(sync:push)
Section titled “4. 同步推送(sync:push)”把 morningstar 数据库里处理好的数据,直接写入 retail-api 生产环境的 api 数据库(PostgreSQL,通过 App\Databases\ApiDb):
| 目标表 | 写入方式 | 内容 |
|---|---|---|
securities | ApiDb::securities() | name、symbol、normalized_symbol、type、cusip、isin、reference(= Morningstar ID)、exchange_id、source = 'morningstar'。先按 reference 匹配已有记录,没有再按 cusip、isin 匹配。 |
equities | ApiDb::equities() | capitalization、category_id、country、sector_id、valuation(股票风格箱),按 security_id 关联 |
funds | ApiDb::funds() | expense_ratio、subtype,按 security_id 关联 |
allocations_by_asset_type、allocations_by_category、allocations_by_market、allocations_by_sector | ApiDb::table(...) | 基金各维度配置占比,按 fund_id 关联 |
fund_equity_portfolios → allocations_by_equity_style | ApiDb::table(...) | 该基金持仓的股票风格箱(市值 × 估值)占比构成 |
fund_bond_portfolios → allocations_by_bond_quality | ApiDb::table(...) | 该基金持仓的债券信用等级(AAA…below/unrated)占比构成 |
prices | ApiDb::getConnection()->table(Price::TABLE)->upsert() | security_id、date、close,每 2,000 条一批 upsert |
相关子记录来自 morningstar.securities.derived 里的 JSON 数据(由 data:transform 生成),解码后按外键安全的顺序逐表写入(先 Equity/Fund,再写它们的 allocation/portfolio 子表)。
生命周期管理:没有任何价格记录的证券会在 api 数据库里被软删除(securities.deleted_at = NOW());硬编码的黑名单(例如 symbol PDRG)也用同样方式强制禁用。下面的 webhook 只负责处理删除事件——以上其它所有写入都是对 retail 生产数据库的直接 SQL 写入,不是 API 调用。
与 Retail API 的交互
Section titled “与 Retail API 的交互”Webhook 端点
Section titled “Webhook 端点”该服务通过 HTTP webhook 与 retail-api 通信:
| 端点 | 用途 | 请求体 |
|---|---|---|
POST /v2/webhooks/morningstar/security | 通知证券删除 | {action: 'delete', security_ids: [...]} |
POST /v2/webhooks/cache | 刷新 Redis 缓存 | {redis: {client: 'Morningstar', models: {security: []}, globs: ['Price:LatestArray:*', 'Security:Lookup:*'], keys: ['Security:TrashedIds']}} |
鉴权:Basic Auth,用户名 Morningstar,密码来自环境变量 RETAIL_API_KEY。
辅助函数:app/Support/helpers.php 里的 send_request_to_retail_api(path, body)。
Retail API 侧
Section titled “Retail API 侧”Retail-api 在 SecurityController(app/Http/Controllers/Webhooks/Morningstar/SecurityController.php)接收这些 webhook,由 auth.internal:Morningstar 中间件保护。
证券被删除时,retail-api 触发一个 SecurityDeleted 事件,该事件会:
- 解绑相关持仓(把证券元数据复制到
custom_securityJSON 字段) - 删除使用该证券的目标类别配置(target category mix allocations)
- 清空股票计划账户(stock plan account)里对该证券的引用
Morningstar 数据库:securities 表
Section titled “Morningstar 数据库:securities 表”id INTEGER, PK, IDENTITYmorningstar_id VARCHAR(10), NOT NULL, unique -- 例如 "F00000MLJO"morningstar_performance_id VARCHAR(10), NOT NULL, unique -- 例如 "0P00015GFM"morningstar_company_id VARCHAR(255), FK → companies.morningstar_idsymbol VARCHAR(255), NOT NULL, indexedcusip VARCHAR(9), nullableisin VARCHAR(12), nullabletype ENUM('equity', 'fund')api_exchange_id SMALLINT, NOT NULLname VARCHAR(255), nullableapi_id INTEGER, nullable -- FK 指向 retail-api securities.idapi_deleted BOOLEANhas_price BOOLEANderived JSONB -- 转换后供所有子模型使用的数据证券匹配(Security Matching)
Section titled “证券匹配(Security Matching)”当各家 vendor 集成把持仓同步进 retail-api 时,每笔持仓都需要匹配到一条证券主数据记录。匹配优先级:
- CUSIP —— 最可靠(但 2018 年后覆盖率有限)
- ISIN —— 国际标准
- Symbol + 名称校验 —— 先按 symbol 查找,再做名称相似度校验
- AI 名称匹配(DEV-19528,进行中) —— 用基于 LLM 的名称比对替代
similar_text,应用于按 symbol 匹配到候选项的场景
通过 Kubernetes CronJob 部署(deploy/cron-jobs.yaml.gotmpl):
| 任务 | 执行计划 | 环境 | 资源 |
|---|---|---|---|
retrieve | 0 * * * *(每小时) | 仅生产环境 | 1 CPU |
process | 15 4 * * 2,3,4,5,6(美东时间凌晨 4:15,周二至周六) | 所有环境 | 1 CPU、1Gi 内存 |
所有任务通过 Cronitor 监控,在启动/完成/失败时发送健康心跳。
关键环境变量
Section titled “关键环境变量”| 变量 | 用途 |
|---|---|
MORNINGSTAR_FTP_PASSWORD | 连接 ftp.morningstar.com 的 FTP 凭证 |
DB_* / DB_API_* | Morningstar 数据库 / API 数据库连接配置 |
AWS_BUCKET | 文件暂存用的 S3 存储桶 |
RETAIL_API_URL | webhook 调用的 Retail API 基础地址 |
RETAIL_API_KEY | webhook 调用的 Basic Auth 密码 |
CRONITOR_API_KEY | 监控凭证 |
文件模式配置
Section titled “文件模式配置”config/morningstar.php 定义了 7 组下载规则:FTP 目录、文件名匹配模式、触发文件后缀,覆盖个股参考数据、资产分类、价格、基金 DataWarehouse、基金价格等场景。
Morningstar Office 下线(DEV-19366)
Section titled “Morningstar Office 下线(DEV-19366)”Morningstar Office 是一个基于 OAuth 的集成,顾问可以借此把家庭/账户/持仓数据同步进 retail-api。该集成已于 2026 年 2 月 28 日下线。
数据迁移分两个阶段执行:
- 阶段 A(已完成):解绑 3,778 笔来自 MO 证券的持仓,把 11,571 个活跃账户转为手动管理(
source = NULL),软删除 228 个集成 - 阶段 B(延后执行):清理 MO 证券、被软删除的账户,以及硬删除相关集成
- Morningstar Office —— 数据同步集成(已于 2026/02/28 下线)
- Morningstar Advisor Workstation —— 报告生成(仍在使用)
- 投资分析数据模型 —— retail-api 如何用这些数据做基金穿透和投资分析
- Security Matching Enhancement (DEV-19528) —— AI 名称匹配提案
- Notion: Morningstar Home —— 团队 wiki
- Notion: Integration 的组成 —— 架构概览