创建数据库表

NTQ 的所有数据表都用 Python 字典声明——没有 SQL 文件,不用手写 CREATE TABLE。你在 schema.py 里定义表结构,系统启动时自动发现并建表。

内置表放在 core/tables/ 下,用户自定义表放在 userspace/extensions/tables/ 下。

一个完整的案例:创建一张龙虎榜数据表

你接入了龙虎榜的 CSV 数据,现在想把它存进 NTQ 的数据库里,方便策略和扫描时快速查询。你需要创建一张表来存这些数据。

第一步:建目录结构

在 userspace/extensions/tables/ 下创建你的表目录:

userspace/extensions/tables/
└── dragon_tiger/            # 目录名随便起(建议和表名一致)
    ├── schema.py             # 必需:表结构定义
    └── model.py              # 可选:数据操作类

每个表一个子目录,目录里至少有一个 schema.py。系统启动时自动扫描所有含 schema.py 的目录。

 

第二步:写 schema.py

schema.py 里定义一个 schema 字典变量,描述表的结构:

schema = {
    "storage_domain": "strategy",
    "update_key": "user_dragon_tiger",
    "name": "dragon_tiger",
    "primaryKey": ["entity_id", "date"],
    "fields": [
        {
            "name": "entity_id",
            "type": "varchar",
            "length": 16,
            "isRequired": True,
            "nullable": False,
            "description": "股票代码",
        },
        {
            "name": "date",
            "type": "varchar",
            "length": 8,
            "isRequired": True,
            "nullable": False,
            "description": "日期 YYYYMMDD",
        },
        {
            "name": "net_buy_amount",
            "type": "float",
            "isRequired": True,
            "nullable": True,
            "description": "净买入额(元)",
        },
        {
            "name": "rank",
            "type": "int",
            "isRequired": False,
            "nullable": True,
            "description": "当日排名",
        },
        {
            "name": "reason",
            "type": "varchar",
            "length": 255,
            "isRequired": False,
            "nullable": True,
            "description": "上榜原因",
        },
        {
            "name": "last_update",
            "type": "datetime",
            "isRequired": True,
            "nullable": True,
            "description": "本行最后同步时间",
        },
    ],
    "indexes": [
        {"name": "idx_date", "fields": ["date"]},
        {"name": "idx_entity_date", "fields": ["entity_id", "date"], "unique": True},
    ],
}

 

关键字段的意思:

字段

填什么

为什么

storage_domain

"data" / "tag" / "strategy"

决定数据存到哪个库。DuckDB 下每个域是独立的 .duckdb 文件;用户表默认用 strategy

update_key

全局唯一的字符串

系统升级时用来做表迁移的锚点,不能和其他表重复

name

物理表名

数据库里的实际表名。内置表必须 sys_ 前缀,用户表无限制

primaryKey

字段名或字段名列表

主键。单字段写字符串,多字段写列表

fields

字段定义列表

表的列,每个字段至少有 name 和 type

indexes

索引定义列表

可选。每个索引有 name、fields,可加 unique: True

每个字段定义里:

属性

必需

说明

name

是

列名

type

是

数据类型,见下方类型表

length

varchar 时建议

字符串长度

isRequired

否

业务层必填约束(不影响 DDL)

nullable

否

False 时生成 NOT NULL,默认 True

description

否

列注释,方便维护

支持的字段类型:

类型

对应 SQL

典型用途

varchar

VARCHAR(n)

字符串:股票代码、日期、文本

int

INT

敇数:排名、计数

bigint

BIGINT

大整敇:成交量(可能超 int 范围)

float

FLOAT

浮点:价格、金额、比率

datetime

DATETIME

时间戳:同步时间

json

JSON

结构化数据:嵌套对象

第三步:写 model.py(可选但推荐)

schema.py 只是声明了表结构,要往表里读写数据,还需要一个 Model 类。在 model.py 里继承 DbBaseModel:

from typing import Any, Dict, List
from core.infra.db.contracts import DbBaseModel
from .schema import schema as _schema

class DragonTigerModel(DbBaseModel):
    def __init__(self, db=None):
        super().__init__(_schema["name"], db)

    def save_records(self, records: List[Dict[str, Any]]) -> int:
        """批量写入龙虎榜数据(有则更新,无则插入)。"""
        return self.upsert_many(records, unique_keys=["entity_id", "date"])

    def get_by_date(self, date: str) -> List[Dict[str, Any]]:
        """查询某天的龙虎榜数据。"""
        return self.load(where="date = ?", params=[date])

 

DbBaseModel 提供了常用的数据操作方法,你只需要写业务相关的封装:

方法

用途

upsert_many(rows, unique_keys)

批量插入或更新(按唯一键去重)

load(where, params)

条件查询

insert(row)

单行插入

upsert(row, unique_keys)

单行插入或更新

delete(where, params)

条件删除

count(where, params)

计数

第四步:使用

重启 NTQ,系统自动发现 userspace/extensions/tables/dragon_tiger/schema.py,创建 dragon_tiger 表。之后就可以用了:

from userspace.extensions.tables.dragon_tiger.model import DragonTigerModel

model = DragonTigerModel()

# 写入数据
model.save_records([
    {"entity_id": "600000.SH", "date": "20240115", "net_buy_amount": 50000000, "rank": 1, "reason": "日涨幅偏离值达7%", "last_update": "2024-01-15 18:00:00"},
    {"entity_id": "000001.SZ", "date": "20240115", "net_buy_amount": -30000000, "rank": 3, "reason": "日跌幅偏离值达-7%", "last_update": "2024-01-15 18:00:00"},
])

# 查询数据
rows = model.get_by_date("20240115")
# [{"entity_id": "600000.SH", "date": "20240115", "net_buy_amount": 50000000, ...}, ...]

如果你同时写了 data contract 的 loader,可以让 loader 从这张表读数据,而不是读 CSV——查询快得多,还支持时间范围过滤。

其他可用配置

storage_domain 怎么选

域

DuckDB 文件

放什么

用户表能不能用

data

data.duckdb

行情数据:K线、财务、指标、日历等

可以,但一般不建议和内置表混放

tag

tag.duckdb

标签和扫描结果

可以,如果表和 tag 模块相关

strategy

strategy.duckdb

策略和回测数据

推荐,用户自定义表默认放这里

用户表如果没写 storage_domain,默认归入 strategy 域。

 

update_key 的作用

update_key 是表迁移的唯一锚点。NTQ 升级时,迁移工具会比较旧 schema 快照和当前 schema.py 的差异,自动生成 ALTER TABLE 语句(加列、加索引等)。update_key 就是用来追踪"这张表是哪个声明"的。

规则:

  • 全局唯一,不能和其他表重复
  • 一旦确定就不要改(改了等于声明了一张新表)
  • 命名建议:user_{表名}(用户表)或 {域}_{表名}(内置表)

 

多字段主键 vs 唯一索引

NTQ 的主键和唯一索引是两个不同的约束:

  • primaryKey:表级主键约束。DuckDB 下会创建唯一索引。
  • indexes 里加 "unique": True:唯一索引。

两者效果类似,但主键不能为 NULL,唯一索引可以。建议:用 primaryKey 声明业务主键,用唯一索引做额外约束。

 

不写 model.py 行不行

行。schema.py 单独就能建表。只是没有 Model 类的话,你需要直接用数据库操作类来读写数据:

from core.infra.db import Db

db = Db.manager.get_default()
db.insert("dragon_tiger", {"entity_id": "600000.SH", "date": "20240115", "net_buy_amount": 50000000})

但这种方式不推荐——没有类型提示,也没有业务方法封装。写个 Model 类花不了几分钟,用起来方便得多。

 

内置表的结构

内置表放在 core/tables/ 下,按域分子目录:

core/tables/
├── stock/          # 股票相关表
│   ├── stock_list/       # sys_stock_list(股票列表)
│   ├── stock_klines/     # sys_stock_klines(K线行情)
│   ├── stock_indicators/ # sys_stock_indicators(技术指标)
│   ├── corporate_finance/# sys_corporate_finance(财务数据)
│   └── ...
├── index/          # 指数相关表
├── calendar/        # 日历表
├── macro/           # 宏观数据表
├── tag/             # 标签表
└── system/          # 系统表

每个内置表的 schema.py 格式和用户表完全一样,只是表名带 sys_ 前缀。你可以参考内置表的写法来写自己的表。

常见问题

Q: 建好的表在哪看? A: DuckDB 模式下,表在 userspace/system/db/ 对应的 .duckdb 文件里。用 DuckDB CLI 或 DBeaver 打开文件即可查看。

Q: 改了 schema.py 之后表结构会自动更新吗? A: 不会。系统启动时只执行 CREATE TABLE IF NOT EXISTS——表已存在就跳过。改了字段需要手动执行迁移:python -m core.infra.db.core.migrate_manager plan 然后 apply。

Q: 用户表名能和内置表重名吗? A: 不能。表名是全局唯一的。内置表用 sys_ 前缀就是为了避免和用户表冲突。

Q: 我用 MySQL/PostgreSQL,建表流程一样吗? A: 一样。schema.py 是数据库无关的,系统会根据你配置的数据库类型自动生成对应的 DDL。唯一区别是 DuckDB 按域分文件,MySQL/PostgreSQL 所有表在同一个库里。