create-dwy 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sync.js +41 -2
- package/templates/claude-global/rules/dwy-payload-limits.md +8 -0
- package/templates/claude-global/settings.json +1 -21
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +91 -0
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +44 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +342 -0
- package/templates/claude-global/skills/dwy-eui/SKILL.md +0 -123
package/package.json
CHANGED
package/src/sync.js
CHANGED
|
@@ -93,6 +93,29 @@ async function scanCommands(sourceDir) {
|
|
|
93
93
|
return commands.sort((a, b) => a.name.localeCompare(b.name))
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 扫描 hooks 目录
|
|
98
|
+
*/
|
|
99
|
+
async function scanHooks(sourceDir) {
|
|
100
|
+
const hooksDir = path.join(sourceDir, 'hooks')
|
|
101
|
+
if (!await fs.pathExists(hooksDir)) return []
|
|
102
|
+
|
|
103
|
+
const entries = await fs.readdir(hooksDir, { withFileTypes: true })
|
|
104
|
+
const hooks = []
|
|
105
|
+
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.name === '.gitkeep') continue
|
|
108
|
+
hooks.push({
|
|
109
|
+
name: entry.name,
|
|
110
|
+
description: entry.isDirectory() ? '钩子目录' : '钩子脚本',
|
|
111
|
+
sourcePath: path.join(hooksDir, entry.name),
|
|
112
|
+
type: 'hook',
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return hooks.sort((a, b) => a.name.localeCompare(b.name))
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
/**
|
|
97
120
|
* 用 inquirer 做交互式多选
|
|
98
121
|
*/
|
|
@@ -225,15 +248,17 @@ export async function syncClaude() {
|
|
|
225
248
|
const skills = await scanSkills(sourceDir)
|
|
226
249
|
const rules = await scanRules(sourceDir)
|
|
227
250
|
const commands = await scanCommands(sourceDir)
|
|
251
|
+
const hooks = await scanHooks(sourceDir)
|
|
228
252
|
|
|
229
|
-
console.log(chalk.yellow(`Found ${skills.length} skills, ${rules.length} rules, ${commands.length} commands\n`))
|
|
253
|
+
console.log(chalk.yellow(`Found ${skills.length} skills, ${rules.length} rules, ${commands.length} commands, ${hooks.length} hooks\n`))
|
|
230
254
|
|
|
231
255
|
// 交互式选择
|
|
232
256
|
const selectedSkills = await promptSelection(skills, 'Skills')
|
|
233
257
|
const selectedRules = await promptSelection(rules, 'Rules')
|
|
234
258
|
const selectedCommands = await promptSelection(commands, 'Commands')
|
|
259
|
+
const selectedHooks = await promptSelection(hooks, 'Hooks')
|
|
235
260
|
|
|
236
|
-
const totalSelected = selectedSkills.length + selectedRules.length + selectedCommands.length
|
|
261
|
+
const totalSelected = selectedSkills.length + selectedRules.length + selectedCommands.length + selectedHooks.length
|
|
237
262
|
if (totalSelected === 0) {
|
|
238
263
|
console.log(chalk.yellow('\nNo skills/rules/commands selected.'))
|
|
239
264
|
}
|
|
@@ -250,12 +275,26 @@ export async function syncClaude() {
|
|
|
250
275
|
}
|
|
251
276
|
syncedCount += await syncSettings(sourceDir, projectTargetDir)
|
|
252
277
|
|
|
278
|
+
// Hooks 同步到项目级 .claude/hooks/(settings.json 用 $CLAUDE_PROJECT_DIR 引用)
|
|
279
|
+
if (selectedHooks.length > 0) {
|
|
280
|
+
const hooksTargetDir = path.join(projectTargetDir, 'hooks')
|
|
281
|
+
await fs.ensureDir(hooksTargetDir)
|
|
282
|
+
for (const hook of selectedHooks) {
|
|
283
|
+
const dest = path.join(hooksTargetDir, hook.name)
|
|
284
|
+
await fs.copy(hook.sourcePath, dest, { overwrite: true })
|
|
285
|
+
await fs.chmod(dest, 0o755)
|
|
286
|
+
syncedCount++
|
|
287
|
+
console.log(chalk.green(` ✓ hooks/${hook.name}`))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
253
291
|
// CLAUDE.md 同步到全局
|
|
254
292
|
await fs.ensureDir(globalTargetDir)
|
|
255
293
|
syncedCount += await syncClaudeMd(sourceDir, globalTargetDir)
|
|
256
294
|
|
|
257
295
|
console.log(chalk.blue(`\nDone! Synced ${syncedCount} items.`))
|
|
258
296
|
console.log(chalk.gray(` Project config → ${projectTargetDir}`))
|
|
297
|
+
console.log(chalk.gray(` Hooks → ${path.join(projectTargetDir, 'hooks')}`))
|
|
259
298
|
console.log(chalk.gray(` CLAUDE.md → ${globalTargetDir}`))
|
|
260
299
|
}
|
|
261
300
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 用户输入 / HTTP 请求体 / 文件上传的长度与大小限制。涉及表单字段 maxlength、Nginx client_max_body_size、Pydantic max_length、分页 page_size 上限、批量操作 items 限制、文件上传大小与像素校验时触发。
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.vue"
|
|
5
|
+
- "**/*.py"
|
|
6
|
+
- "**/nginx*.conf"
|
|
7
|
+
---
|
|
8
|
+
|
|
1
9
|
# 数据载荷三层长度限制规则
|
|
2
10
|
|
|
3
11
|
防止大文本 DoS、内存耗尽、带宽滥用。所有涉及用户输入 / HTTP 请求体 / 文件上传的场景必须遵循三层限制,单层失守即高风险。
|
|
@@ -6,27 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "bash
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"PostToolUse": [
|
|
15
|
-
{
|
|
16
|
-
"matcher": "Edit",
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "bash ~/.claude/hooks/post-sync-claude-global.sh"
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"matcher": "Write",
|
|
26
|
-
"hooks": [
|
|
27
|
-
{
|
|
28
|
-
"type": "command",
|
|
29
|
-
"command": "bash ~/.claude/hooks/post-sync-claude-global.sh"
|
|
9
|
+
"command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/pre-git-commit-sensitive-check.sh"
|
|
30
10
|
}
|
|
31
11
|
]
|
|
32
12
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-dolphindb
|
|
3
|
+
description: >
|
|
4
|
+
DolphinDB 开发规范与最佳实践审查。当用户要求生成、修改、审查或优化涉及
|
|
5
|
+
DolphinDB 的 Python/SQL 代码、脚本拼接、批量写入、查询优化、连接管理、分区
|
|
6
|
+
裁剪、数据类型转换、Docker 配置或测试 Mock 时,**必须**使用此 skill。即使
|
|
7
|
+
用户没有明确提到 "DolphinDB",但只要代码中出现 dolphindb、run_ddb、
|
|
8
|
+
loadTable、append!、dropPartition、DBConnectionPool、DDBSession、tickDB、
|
|
9
|
+
factorDB 等关键字,或涉及 TSDB 分区、时间序列数据库存储、金融 tick 数据/因子
|
|
10
|
+
数据写入,也要触发此 skill。用于确保代码符合社区版 8GB 内存限制、2 节点集群
|
|
11
|
+
限制、分区裁剪、输入校验、批量写入 ≤500K 行等硬约束。
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# DolphinDB 开发规范 Skill
|
|
15
|
+
|
|
16
|
+
## 何时使用
|
|
17
|
+
|
|
18
|
+
只要任务涉及以下任一内容,立即读取 `references/dolphindb-rules.md` 并按本 skill 执行审查:
|
|
19
|
+
|
|
20
|
+
- 编写或修改调用 DolphinDB 的 Python 代码(`run_ddb`、`run_ddb_with_data`、`DDBSession`、`DBConnectionPool` 等)
|
|
21
|
+
- 编写或修改 DolphinDB 脚本(`loadTable`、`append!`、`dropPartition`、`select` 等)
|
|
22
|
+
- 批量写入 tick 数据或因子数据到 DolphinDB
|
|
23
|
+
- 查询 DolphinDB 中的 tick/因子数据,尤其是 WHERE 条件设计
|
|
24
|
+
- 数据类型转换(Feather/Pandas → DolphinDB)
|
|
25
|
+
- Docker 中部署或配置 DolphinDB
|
|
26
|
+
- 为 DolphinDB 相关代码编写测试 Mock
|
|
27
|
+
- 代码审查(CR)时发现文件包含 DolphinDB 调用
|
|
28
|
+
|
|
29
|
+
## 使用步骤
|
|
30
|
+
|
|
31
|
+
### 1. 加载完整规则
|
|
32
|
+
|
|
33
|
+
读取 `references/dolphindb-rules.md`。该文件包含全部 12 章规则,是审查的唯一权威来源。
|
|
34
|
+
|
|
35
|
+
### 2. 识别代码中涉及的规则章节
|
|
36
|
+
|
|
37
|
+
根据当前任务内容,判断主要涉及哪些章节:
|
|
38
|
+
|
|
39
|
+
| 任务类型 | 重点章节 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 查询脚本编写 | 三(分区裁剪)、八(查询优化) |
|
|
42
|
+
| 批量数据写入 | 二(连接管理)、四(批量写入)、十(数据类型映射) |
|
|
43
|
+
| 脚本拼接/动态生成 | 五(脚本语法)、六(输入校验) |
|
|
44
|
+
| 服务层 Python 代码 | 二(连接管理)、六(输入校验)、十(数据类型映射) |
|
|
45
|
+
| Docker/部署配置 | 九(Docker 环境) |
|
|
46
|
+
| 测试代码 | 十一(测试 Mock) |
|
|
47
|
+
| 代码审查/CR | 全部,尤其是十二(检查清单) |
|
|
48
|
+
|
|
49
|
+
### 3. 执行规则审查
|
|
50
|
+
|
|
51
|
+
对生成的或已有的代码,逐条检查相关章节的**强制规则**和**违规检测**项。
|
|
52
|
+
|
|
53
|
+
**审查输出格式:**
|
|
54
|
+
|
|
55
|
+
对每处问题,使用以下格式报告:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
⚠️ [规则章节] 具体问题
|
|
59
|
+
违规代码:<摘录>
|
|
60
|
+
修复建议:<具体修改后的代码>
|
|
61
|
+
原因:<引用规则中的说明>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
对符合规则的代码,可以简要确认:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
✅ [规则章节] 符合要求 — <简要说明>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. 特别关注以下致命违规
|
|
71
|
+
|
|
72
|
+
以下问题会直接导致生产故障或 OOM,必须零容忍:
|
|
73
|
+
|
|
74
|
+
1. **分区列被函数包裹**(如 `date(trade_date)`)→ 全表扫描 → 8GB OOM
|
|
75
|
+
2. **单次 append 超过 500K 行** → TSDB 报错 `exceeds max limit`
|
|
76
|
+
3. **未校验的外部输入拼接到脚本** → 脚本注入风险
|
|
77
|
+
4. **连接池做 upload + run** → 变量跨连接不可见
|
|
78
|
+
5. **select *** → 传输全部 30+ 列,浪费带宽和内存
|
|
79
|
+
6. **分页使用 `LIMIT x OFFSET y`** → DolphinDB 不支持 OFFSET 关键字
|
|
80
|
+
7. **`asyncio.to_thread(pool.run, script)`** → SDK v3 的 `pool.run()` 已是原生 async,无需包装
|
|
81
|
+
|
|
82
|
+
### 5. 检查清单应用
|
|
83
|
+
|
|
84
|
+
如果任务是**代码审查**或**最终确认**,逐条过一遍 `references/dolphindb-rules.md` 第十二章的检查清单(12 项),并给出通过/不通过的明确结论。
|
|
85
|
+
|
|
86
|
+
## 输出要求
|
|
87
|
+
|
|
88
|
+
- 审查结果必须结构化,按规则章节分组
|
|
89
|
+
- 对每一处违规,必须给出**可落地的修复代码**,不能只说"不符合规则"
|
|
90
|
+
- 如果代码完全符合规则,明确声明"所有 DolphinDB 相关规则检查通过"
|
|
91
|
+
- 如果任务只是生成代码,在生成过程中就内联应用规则,生成完毕后给出简要的合规确认
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "dwy-dolphindb",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "帮我写一个 Python 函数,把一个大 pandas DataFrame(约 200 万行 tick 数据)写入 DolphinDB 的 tickDB snapshots 表。表的分区列是 trade_date 和 order_book_id。函数接收 filepath(feather 格式)和 trade_date 参数。",
|
|
7
|
+
"expected_output": "生成的代码必须使用 DDBSession 复用连接、分批次 ≤500K 行写入、每批后 del + gc.collect()、用 pyarrow 分片读取、外部输入经过校验、使用 run_ddb_with_data 或 session.upload_and_run。不能出现连接池 upload、单次 append 全部数据、pd.read_feather 全量加载。",
|
|
8
|
+
"files": [],
|
|
9
|
+
"assertions": [
|
|
10
|
+
"代码中包含 BATCH_SIZE 且值 ≤ 500000",
|
|
11
|
+
"代码中使用了 gc.collect()",
|
|
12
|
+
"代码中没有使用 pd.read_feather 一次性读取大文件",
|
|
13
|
+
"代码中使用了 DDBSession 或 run_ddb_with_data 而非连接池做 upload",
|
|
14
|
+
"代码中包含对外部输入(如 trade_date、filepath)的校验逻辑"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": 2,
|
|
19
|
+
"prompt": "写一个 FastAPI 端点 /api/tick/query,接收 stock_code、start_date、end_date 参数,从 DolphinDB tickDB snapshots 表查询数据并返回。请直接写实现代码。",
|
|
20
|
+
"expected_output": "生成的代码必须对输入参数校验、WHERE 条件直接命中分区列(trade_date 和 order_book_id)而不使用函数包裹、只 select 需要的列、使用 DolphinDB 分页语法 limit offset, count(而非 LIMIT OFFSET)、使用 PIVOT BY 或合并聚合查询减少往返。",
|
|
21
|
+
"files": [],
|
|
22
|
+
"assertions": [
|
|
23
|
+
"WHERE 子句中没有 date()、month()、year() 等函数包裹分区列",
|
|
24
|
+
"查询中没有 select *",
|
|
25
|
+
"分页语法是 limit {offset}, {page_size} 而不是 limit {page_size} offset {offset}",
|
|
26
|
+
"代码中包含输入校验(validate_stock_code 或类似逻辑)",
|
|
27
|
+
"没有使用 asyncio.to_thread(pool.run)"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": 3,
|
|
32
|
+
"prompt": "审查以下代码是否符合 DolphinDB 开发规范,指出所有问题并给出修复后的代码:\n```python\nasync def import_tick_data(filepath: str, trade_date: str):\n df = pd.read_feather(filepath)\n script = f'append!{loadTable(\"dfs://tickDB\", \"snapshots\"), data}'\n await run_ddb(f'upload data = {df}')\n await run_ddb(script)\n return {\"status\": \"ok\"}\n\nasync def query_ticks(stock_code: str, start: str, end: str):\n script = (\n f'select * from loadTable(\"dfs://tickDB\", \"snapshots\") '\n f'where date(trade_date) >= {start} and date(trade_date) <= {end} '\n f'and order_book_id = \"{stock_code}\" '\n f'limit 100 offset 0'\n )\n return await run_ddb(script)\n```",
|
|
33
|
+
"expected_output": "审查报告必须指出:1) pd.read_feather 全量加载问题;2) 连接池 upload + run 导致变量跨连接不可见;3) 未校验 filepath、trade_date、stock_code;4) date(trade_date) 函数包裹分区列导致全表扫描;5) select *;6) limit 100 offset 0 语法错误;7) 缺少 BATCH_SIZE 和 gc.collect()。每项必须给出修复代码。",
|
|
34
|
+
"files": [],
|
|
35
|
+
"assertions": [
|
|
36
|
+
"输出中明确指出了 date(trade_date) 或分区列被函数包裹的问题",
|
|
37
|
+
"输出中明确指出了 select * 的问题",
|
|
38
|
+
"输出中明确指出了 limit 100 offset 0 语法错误",
|
|
39
|
+
"输出中明确指出了连接池 upload + run 的问题",
|
|
40
|
+
"输出中为每个问题都提供了修复后的代码片段"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# DolphinDB 开发规则
|
|
2
|
+
|
|
3
|
+
所有涉及 DolphinDB 的代码必须遵循以下规则。AI 生成或修改代码时,必须逐条检查。
|
|
4
|
+
|
|
5
|
+
## 一、社区版硬约束
|
|
6
|
+
|
|
7
|
+
| 约束 | 值 | 违反后果 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| 最大内存 | 8GB(建议 `maxMemSize=5`,留 3GB 给 OS) | 进程 OOM 崩溃 |
|
|
10
|
+
| 最大集群节点数 | 2(允许 1 controller + 1 datanode 的最小集群) | 超出被许可证拒绝;无法扩展到 3 节点以上 |
|
|
11
|
+
| 最大并发客户端连接 | 512(`maxConnections=512`) | 新连接直接被拒 |
|
|
12
|
+
| TSDB 单次 append | ≤ 204MB(约 800K 行 × 30 列) | 报错 `exceeds max limit` |
|
|
13
|
+
| 高可用 / Raft / 异步复制 | 不支持 | 多副本能力需企业版 |
|
|
14
|
+
|
|
15
|
+
### 本项目部署侧配置(非社区版硬限,是 VM/机器约束)
|
|
16
|
+
|
|
17
|
+
- VM 规格:**2 核 8 GB**(与社区版 8 GB 内存硬限一致)
|
|
18
|
+
- 因 CPU 只有 2 核,`workerNum` 和客户端 `dolphindb_pool_size` 按经验值 `2`,避免大量并行争抢 CPU;**这不是社区版硬限,而是 2 核机器上的合理取值**
|
|
19
|
+
- 512 并发连接是协议层上限,实际可持有的连接远少于这个数,不要把它当作"应该开 512 条"的指令
|
|
20
|
+
|
|
21
|
+
### 违规检测
|
|
22
|
+
如果代码中出现以下情况,必须提示:
|
|
23
|
+
- 单次 `append!` 数据超过 500K 行(安全上限)
|
|
24
|
+
- `maxMemSize` > 8
|
|
25
|
+
- 集群配置超过 2 个节点
|
|
26
|
+
- 客户端 `dolphindb_pool_size` 显著高于 CPU 核数(在 2 核机器上 > 4 需说明理由)
|
|
27
|
+
|
|
28
|
+
## 二、连接与会话管理
|
|
29
|
+
|
|
30
|
+
### 强制规则
|
|
31
|
+
|
|
32
|
+
| 场景 | 使用方式 | 原因 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| 只读查询 | `run_ddb(script)` — 连接池 | 池自动管理连接复用 |
|
|
35
|
+
| 单次 upload + run | `run_ddb_with_data(script, data)` — 独立 Session | 确保 upload 和 run 在同一连接 |
|
|
36
|
+
| 批量 upload(多次循环) | `DDBSession` — 复用 Session | 避免每批新建/关闭连接的开销 |
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# ❌ 用连接池做 upload + run(变量跨连接不可见)
|
|
40
|
+
await run_ddb("upload_var = ...") # 连接 A
|
|
41
|
+
await run_ddb("select * from upload_var") # 可能路由到连接 B → 变量不存在
|
|
42
|
+
|
|
43
|
+
# ✅ 独立 Session 保证同一连接
|
|
44
|
+
await run_ddb_with_data(script, {"batch_data": df})
|
|
45
|
+
|
|
46
|
+
# ✅ 批量操作复用 Session
|
|
47
|
+
session = DDBSession()
|
|
48
|
+
try:
|
|
49
|
+
for batch in batches:
|
|
50
|
+
await session.upload_and_run(script, {"batch_data": batch})
|
|
51
|
+
finally:
|
|
52
|
+
session.close()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### SDK v3 异步 API
|
|
56
|
+
- `DBConnectionPool.run()` 在 SDK v3 中是 **原生 async**,直接 `await pool.run(script)`
|
|
57
|
+
- **禁止** `asyncio.to_thread(pool.run, script)`(会产生 "coroutine never awaited" 警告)
|
|
58
|
+
- `Session.run()` 仍然是 **同步**,需要 `asyncio.to_thread()` 包装
|
|
59
|
+
|
|
60
|
+
## 三、分区裁剪(性能关键)
|
|
61
|
+
|
|
62
|
+
### 强制规则
|
|
63
|
+
**WHERE 条件必须直接命中分区列,禁止对分区列使用函数。**
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# ❌ 函数包裹分区列 → 全表扫描 → 8GB OOM
|
|
67
|
+
WHERE date(datetime) = 2020.01.01
|
|
68
|
+
WHERE month(trade_date) = 2020.01M
|
|
69
|
+
WHERE string(order_book_id) = "000001.XSHE"
|
|
70
|
+
|
|
71
|
+
# ✅ 直接范围过滤 → 分区裁剪生效
|
|
72
|
+
WHERE trade_date = 2020.01.01
|
|
73
|
+
WHERE trade_date >= 2020.01.01, trade_date < 2020.02.01
|
|
74
|
+
WHERE order_book_id in [`000001.XSHE`000002.XSHE]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### tickDB 分区列
|
|
78
|
+
- 第一级:`trade_date`(VALUE 分区,按日)
|
|
79
|
+
- 第二级:`order_book_id`(HASH 分区,20 桶)
|
|
80
|
+
|
|
81
|
+
### factorDB 分区列
|
|
82
|
+
- 第一级:`trade_date`(VALUE 分区,按月)
|
|
83
|
+
- 第二级:`factor_name`(VALUE 分区)
|
|
84
|
+
|
|
85
|
+
### 违规检测
|
|
86
|
+
如果 DolphinDB 查询脚本中出现以下模式,必须提示:
|
|
87
|
+
- `date(...)` / `month(...)` / `year(...)` 包裹分区列
|
|
88
|
+
- `WHERE 1=1` 无分区条件(全表扫描)
|
|
89
|
+
- `select * from loadTable(...)` 无 WHERE 子句
|
|
90
|
+
|
|
91
|
+
## 四、批量写入
|
|
92
|
+
|
|
93
|
+
### 强制规则
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
BATCH_SIZE = 500_000 # TSDB 上限 204MB,500K 行 ≈ 120MB(安全余量)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 规则 | 说明 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| 每批 ≤ 500K 行 | TSDB 单次 append 内存上限 204MB |
|
|
102
|
+
| 每批后 `gc.collect()` | 释放 Python 侧内存 |
|
|
103
|
+
| 每批后 `del batch_df` | 显式释放 DataFrame 引用 |
|
|
104
|
+
| 大文件用 pyarrow 分片读取 | `pf.read_table()` + `table.slice()` 避免全量加载 |
|
|
105
|
+
| 复用 Session | 批量循环中不要每批新建连接 |
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# ✅ 标准批量上传模式
|
|
109
|
+
table = pf.read_table(filepath) # memory-mapped,不全量加载
|
|
110
|
+
session = DDBSession()
|
|
111
|
+
try:
|
|
112
|
+
for i in range(num_batches):
|
|
113
|
+
arrow_slice = table.slice(i * BATCH_SIZE, BATCH_SIZE)
|
|
114
|
+
batch_df = prepare_batch(arrow_slice.to_pandas())
|
|
115
|
+
del arrow_slice
|
|
116
|
+
await session.upload_and_run(script, {"batch_data": batch_df})
|
|
117
|
+
del batch_df
|
|
118
|
+
gc.collect()
|
|
119
|
+
finally:
|
|
120
|
+
session.close()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 禁止的写法
|
|
124
|
+
```python
|
|
125
|
+
# ❌ 一次性读取大文件到 pandas(OOM)
|
|
126
|
+
df = pd.read_feather("10M_rows.ftr") # 2.6GB 内存
|
|
127
|
+
|
|
128
|
+
# ❌ 单次 append 超过 500K 行
|
|
129
|
+
await run_ddb_with_data(script, {"data": df_1million_rows})
|
|
130
|
+
|
|
131
|
+
# ❌ 不释放内存
|
|
132
|
+
for batch in batches:
|
|
133
|
+
await session.upload_and_run(script, {"data": batch})
|
|
134
|
+
# 忘记 del + gc.collect()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 五、DolphinDB 脚本语法
|
|
138
|
+
|
|
139
|
+
### 列名定义必须单行
|
|
140
|
+
```python
|
|
141
|
+
# ❌ 多行 backtick 列名 → 语法错误
|
|
142
|
+
schema = table(1:0,
|
|
143
|
+
`col1`col2
|
|
144
|
+
`col3`col4,
|
|
145
|
+
[INT, INT, INT, INT])
|
|
146
|
+
|
|
147
|
+
# ✅ 单行定义
|
|
148
|
+
colNames = `col1`col2`col3`col4
|
|
149
|
+
colTypes = [INT, INT, INT, INT]
|
|
150
|
+
schema = table(1:0, colNames, colTypes)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 多值过滤的 backtick 拼接
|
|
154
|
+
```python
|
|
155
|
+
# ✅ 正确的 Symbol 列表拼接
|
|
156
|
+
symbols = ["000001.XSHE", "000002.XSHE"]
|
|
157
|
+
symbols_str = "`" + "`".join(symbols)
|
|
158
|
+
script = f"... order_book_id in [{symbols_str}]"
|
|
159
|
+
# 生成: order_book_id in [`000001.XSHE`000002.XSHE]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 脚本拼接用分号 + 换行
|
|
163
|
+
```python
|
|
164
|
+
# ✅ 多语句用 ;\n 分隔
|
|
165
|
+
script = (
|
|
166
|
+
f'tickData = select * from loadTable("dfs://tickDB","snapshots") '
|
|
167
|
+
f'where {ts_filter};\n'
|
|
168
|
+
f'{user_script};\n'
|
|
169
|
+
f'exec count(*) from result'
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 六、输入校验(安全关键)
|
|
174
|
+
|
|
175
|
+
### 强制规则
|
|
176
|
+
**所有外部输入在拼接到 DolphinDB 脚本前,必须经过 `validators.py` 校验。**
|
|
177
|
+
|
|
178
|
+
| 类型 | 正则 | 示例 |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| 因子名 | `^[a-zA-Z0-9_]{1,50}$` | `alpha_vwap_001` |
|
|
181
|
+
| 股票代码 (ukey) | `^\d{6}\.[A-Z]{2}$` | `000001.SZ` |
|
|
182
|
+
| 日期 | `^\d{4}\.\d{2}\.\d{2}$` | `2024.01.02` |
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# ✅ 先校验再拼接
|
|
186
|
+
validate_factor_name(factor_name) # 不通过则抛 BusinessError
|
|
187
|
+
script = f'... factor_name = "{factor_name}"'
|
|
188
|
+
|
|
189
|
+
# ❌ 未校验直接拼接 → 脚本注入风险
|
|
190
|
+
script = f'... factor_name = "{user_input}"'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 违规检测
|
|
194
|
+
如果代码中出现以下情况,必须提示:
|
|
195
|
+
- f-string 拼接 DolphinDB 脚本时使用了未经 `validate_*` 校验的变量
|
|
196
|
+
- 用户输入直接出现在 `run_ddb()` 的 script 参数中
|
|
197
|
+
|
|
198
|
+
## 七、分区操作
|
|
199
|
+
|
|
200
|
+
### 删除分区用 `dropPartition`,不用 `DELETE`
|
|
201
|
+
```python
|
|
202
|
+
# ✅ dropPartition — 原子操作,无 tombstone
|
|
203
|
+
dropPartition(db, partitionPath, tableName)
|
|
204
|
+
|
|
205
|
+
# ❌ DELETE — 标记删除,产生 tombstone,需后续 GC
|
|
206
|
+
delete from loadTable("dfs://tickDB", "snapshots") where trade_date = 2020.01.01
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 新增分区值
|
|
210
|
+
配置 `newValuePartitionPolicy=add`,VALUE 分区自动扩展,无需预创建。
|
|
211
|
+
|
|
212
|
+
## 八、查询优化
|
|
213
|
+
|
|
214
|
+
### 聚合查询合并
|
|
215
|
+
```python
|
|
216
|
+
# ❌ 3 次查询
|
|
217
|
+
min_date = await run_ddb("select min(trade_date) ...")
|
|
218
|
+
max_date = await run_ddb("select max(trade_date) ...")
|
|
219
|
+
count = await run_ddb("select count(*) ...")
|
|
220
|
+
|
|
221
|
+
# ✅ 1 次查询
|
|
222
|
+
result = await run_ddb(
|
|
223
|
+
'select min(trade_date) as earliest, max(trade_date) as latest, count(*) as total '
|
|
224
|
+
'from loadTable("dfs://tickDB","snapshots")'
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 多因子查询用 PIVOT BY
|
|
229
|
+
```python
|
|
230
|
+
# ✅ DolphinDB 服务端完成列转换,无需 Python 端 merge
|
|
231
|
+
script = (
|
|
232
|
+
'select value from loadTable("dfs://factorDB","factors") '
|
|
233
|
+
f'where {where_clause} '
|
|
234
|
+
'pivot by trade_date, ukey, factor_name'
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 分页查询用 `limit offset, count`
|
|
239
|
+
```python
|
|
240
|
+
# ❌ SQL 标准语法 — DolphinDB 不支持 OFFSET 关键字
|
|
241
|
+
f"select * from loadTable(...) limit {page_size} offset {offset}"
|
|
242
|
+
|
|
243
|
+
# ✅ DolphinDB 分页语法:limit 跳过行数, 返回行数
|
|
244
|
+
offset = (page - 1) * page_size
|
|
245
|
+
f"select * from loadTable(...) limit {offset}, {page_size}"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 只查需要的列
|
|
249
|
+
```python
|
|
250
|
+
# ❌ select *(传输全部 30 列)
|
|
251
|
+
"select * from loadTable(...)"
|
|
252
|
+
|
|
253
|
+
# ✅ 只取需要的列
|
|
254
|
+
"select trade_date, time, order_book_id, last, volume from loadTable(...)"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 九、Docker 环境注意事项
|
|
258
|
+
|
|
259
|
+
### DolphinDB v3 Alpine 镜像
|
|
260
|
+
| 问题 | 解决方案 |
|
|
261
|
+
|---|---|
|
|
262
|
+
| 无 `curl` | 用 `wget` 替代 |
|
|
263
|
+
| `localhost` 解析到 IPv6 | healthcheck 用 `127.0.0.1` |
|
|
264
|
+
| macOS 无 `/etc/hostname` `/etc/machine-id` | 不要挂载这两个文件 |
|
|
265
|
+
| 自定义 cfg 中 `logFile`/`volumes` 路径与 Docker volume 冲突 | 只覆盖必要参数,其余用默认值 |
|
|
266
|
+
| `dataSync + CacheEngine` 必须同时启用 | 开发环境建议都不配置,用默认值 |
|
|
267
|
+
|
|
268
|
+
### healthcheck 配置
|
|
269
|
+
```yaml
|
|
270
|
+
healthcheck:
|
|
271
|
+
test: ["CMD-SHELL", "wget -qO- --timeout=3 http://127.0.0.1:8848/ || exit 1"]
|
|
272
|
+
start_period: 30s # DolphinDB 启动较慢,给足初始化时间
|
|
273
|
+
interval: 5s
|
|
274
|
+
timeout: 5s
|
|
275
|
+
retries: 10
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 后端连接配置
|
|
279
|
+
```
|
|
280
|
+
DOLPHINDB_HOST=dolphindb # Docker 服务名(不是 localhost)
|
|
281
|
+
DOLPHINDB_PORT=8848
|
|
282
|
+
DOLPHINDB_POOL_SIZE=2 # 2 核机器的经验值;社区版协议上限是 512 并发,不是 2
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## 十、数据类型映射
|
|
286
|
+
|
|
287
|
+
### Feather/Pandas → DolphinDB
|
|
288
|
+
|
|
289
|
+
| Python/Pandas 类型 | DolphinDB 类型 | 转换方式 |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `uint32` (日期如 20191205) | `DATE` | `pd.to_datetime(str, format='%Y%m%d')` |
|
|
292
|
+
| `uint32` (时间如 91509000) | `INT` | `.astype('int64')` |
|
|
293
|
+
| `float64` | `DOUBLE` | 无需转换 |
|
|
294
|
+
| `uint64` | `LONG` | `.astype('int64')` |
|
|
295
|
+
| `uint32` | `INT` | `.astype('int64')` |
|
|
296
|
+
| `float64` 含 NaN → 整数 | `INT` | `.fillna(0).astype('int64')` |
|
|
297
|
+
| `object` (字符串) | `SYMBOL` | `.astype(str)` |
|
|
298
|
+
|
|
299
|
+
### 注意事项
|
|
300
|
+
- DolphinDB 不接受 `uint` 类型,必须先转 `int64`
|
|
301
|
+
- NaN 不能直接转整数,必须先 `fillna(0)`
|
|
302
|
+
- `SYMBOL` 类型是 DolphinDB 的字典编码字符串,适用于低基数列(如股票代码)
|
|
303
|
+
|
|
304
|
+
## 十一、测试 Mock 规范
|
|
305
|
+
|
|
306
|
+
### DolphinDB 测试不连真实服务
|
|
307
|
+
```python
|
|
308
|
+
# conftest.py 中使用 autouse fixture mock 所有 DolphinDB 调用
|
|
309
|
+
@pytest.fixture(autouse=True)
|
|
310
|
+
def mock_dolphindb():
|
|
311
|
+
async def mock_run_ddb(script):
|
|
312
|
+
if "snapshots" in script and "count" in script.lower():
|
|
313
|
+
return pd.DataFrame({"count": [1000]})
|
|
314
|
+
# ... 基于脚本内容的模式匹配
|
|
315
|
+
return pd.DataFrame()
|
|
316
|
+
|
|
317
|
+
with patch("app.services.tick_service.run_ddb", side_effect=mock_run_ddb):
|
|
318
|
+
yield
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Mock 必须覆盖所有 import 路径
|
|
322
|
+
```python
|
|
323
|
+
# 每个 service 文件都有独立的 import,必须分别 patch
|
|
324
|
+
patch("app.services.tick_service.run_ddb", ...)
|
|
325
|
+
patch("app.services.tick_archive.run_ddb", ...)
|
|
326
|
+
patch("app.services.tick_compute.run_ddb", ...)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## 十二、检查清单
|
|
330
|
+
|
|
331
|
+
每次涉及 DolphinDB 的代码变更,必须逐条检查:
|
|
332
|
+
|
|
333
|
+
- [ ] WHERE 条件是否命中分区列?是否有函数包裹分区列?
|
|
334
|
+
- [ ] 批量写入是否 ≤ 500K 行?是否有 `gc.collect()`?
|
|
335
|
+
- [ ] 外部输入是否经过 `validators.py` 校验?
|
|
336
|
+
- [ ] 使用 `run_ddb` 还是 `run_ddb_with_data`?场景是否匹配?
|
|
337
|
+
- [ ] 连接池大小是否与 CPU 核数匹配(2 核机器建议 `pool_size=2`)?
|
|
338
|
+
- [ ] 查询是否只 select 了需要的列?
|
|
339
|
+
- [ ] 多次聚合查询能否合并为一次?
|
|
340
|
+
- [ ] 脚本中 backtick 列名是否在单行内?
|
|
341
|
+
- [ ] 新增 service 文件的 `run_ddb` 导入是否在 conftest.py 中 mock?
|
|
342
|
+
- [ ] 分页查询是否使用 `limit offset, count` 语法?(禁止 `LIMIT x OFFSET y`)
|
|
@@ -18,129 +18,6 @@ description: "@dwydev/eui Vue 3 组件库速查。当用户在使用 Vue 3 构
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## 安装与引入
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
pnpm add @dwydev/eui
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
// 按需导入(推荐)
|
|
29
|
-
import { EButton, EInput, ETable } from '@dwydev/eui'
|
|
30
|
-
|
|
31
|
-
// 全局注册(Vue plugin)
|
|
32
|
-
import EUI from '@dwydev/eui'
|
|
33
|
-
app.use(EUI)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
// 主题 CSS(必须引入)
|
|
38
|
-
import '@dwydev/eui/theme' // 基础样式 + tokens
|
|
39
|
-
import '@dwydev/eui/theme/dark' // 暗色模式(可选)
|
|
40
|
-
import '@dwydev/eui/theme/tokens' // 仅 design tokens(可选)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 全局配置 EConfigProvider(必须)
|
|
44
|
-
|
|
45
|
-
**EConfigProvider 是使用 EUI 的前置条件。** 必须在 App 根组件用 `<EConfigProvider>` 包裹整个应用,否则以下功能不生效:
|
|
46
|
-
|
|
47
|
-
- 国际化(日期选择器、月份/年份选择器等显示为英文而非中文)
|
|
48
|
-
- 全局组件尺寸统一控制
|
|
49
|
-
- 弹层 z-index 统一管理
|
|
50
|
-
- 组件内部 UI 文案(确定/取消/暂无数据等)
|
|
51
|
-
|
|
52
|
-
#### 基础用法
|
|
53
|
-
|
|
54
|
-
```vue
|
|
55
|
-
<!-- App.vue -->
|
|
56
|
-
<script setup lang="ts">
|
|
57
|
-
import { EConfigProvider } from '@dwydev/eui'
|
|
58
|
-
</script>
|
|
59
|
-
|
|
60
|
-
<template>
|
|
61
|
-
<EConfigProvider>
|
|
62
|
-
<RouterView />
|
|
63
|
-
</EConfigProvider>
|
|
64
|
-
</template>
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
使用默认配置即可满足中文项目需求,无需传任何 props。
|
|
68
|
-
|
|
69
|
-
#### Props
|
|
70
|
-
|
|
71
|
-
| Prop | 类型 | 默认值 | 说明 |
|
|
72
|
-
|------|------|--------|------|
|
|
73
|
-
| size | `'sm' \| 'default' \| 'lg'` | `'default'` | 所有组件的全局尺寸 |
|
|
74
|
-
| zIndex | `number` | `2000` | 弹层组件(Dialog/Drawer/Popover 等)的基准 z-index |
|
|
75
|
-
| locale | `Record<string, string>` | 见下方 | 国际化配置 |
|
|
76
|
-
|
|
77
|
-
#### locale 配置
|
|
78
|
-
|
|
79
|
-
locale 是一个扁平的键值对象,包含两部分内容:
|
|
80
|
-
|
|
81
|
-
| 字段 | 默认值 | 说明 |
|
|
82
|
-
|------|--------|------|
|
|
83
|
-
| name | `'zh-CN'` | **BCP 47 语言标签**,控制日期选择器(EDatePicker)、日历(ECalendar)等组件的本地化显示(月份名、星期名、年份格式等) |
|
|
84
|
-
| confirm | `'确定'` | 确认按钮文案 |
|
|
85
|
-
| cancel | `'取消'` | 取消按钮文案 |
|
|
86
|
-
| close | `'关闭'` | 关闭按钮文案 |
|
|
87
|
-
| loading | `'加载中...'` | 加载状态文案 |
|
|
88
|
-
| empty | `'暂无数据'` | 空状态文案 |
|
|
89
|
-
| search | `'搜索'` | 搜索占位文案 |
|
|
90
|
-
| selectPlaceholder | `'请选择'` | 选择器默认占位文案 |
|
|
91
|
-
| inputPlaceholder | `'请输入'` | 输入框默认占位文案 |
|
|
92
|
-
|
|
93
|
-
#### 自定义配置示例
|
|
94
|
-
|
|
95
|
-
```vue
|
|
96
|
-
<!-- 英文项目 -->
|
|
97
|
-
<EConfigProvider
|
|
98
|
-
size="sm"
|
|
99
|
-
:z-index="3000"
|
|
100
|
-
:locale="{
|
|
101
|
-
name: 'en-US',
|
|
102
|
-
confirm: 'OK',
|
|
103
|
-
cancel: 'Cancel',
|
|
104
|
-
close: 'Close',
|
|
105
|
-
loading: 'Loading...',
|
|
106
|
-
empty: 'No data',
|
|
107
|
-
search: 'Search',
|
|
108
|
-
selectPlaceholder: 'Please select',
|
|
109
|
-
inputPlaceholder: 'Please input',
|
|
110
|
-
}"
|
|
111
|
-
>
|
|
112
|
-
<RouterView />
|
|
113
|
-
</EConfigProvider>
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
#### 嵌套覆盖
|
|
117
|
-
|
|
118
|
-
EConfigProvider 支持嵌套,内层配置覆盖外层。适用于局部区域需要不同配置的场景:
|
|
119
|
-
|
|
120
|
-
```vue
|
|
121
|
-
<EConfigProvider> <!-- 全局中文 -->
|
|
122
|
-
<EConfigProvider :locale="{ name: 'en-US', ... }"> <!-- 局部英文 -->
|
|
123
|
-
<EDatePicker type="month" /> <!-- 显示英文月份 -->
|
|
124
|
-
</EConfigProvider>
|
|
125
|
-
</EConfigProvider>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### 实现原理
|
|
129
|
-
|
|
130
|
-
EConfigProvider 内部做了两件事:
|
|
131
|
-
1. 通过 Vue `provide/inject` 向子树注入 size、zIndex、locale 配置,EUI 组件通过 `useConfigProvider()` 读取
|
|
132
|
-
2. 包裹 reka-ui 的 `ConfigProvider`,将 `locale.name` 透传为 reka-ui 的全局 locale,所有基于 reka-ui 的日期类组件自动继承
|
|
133
|
-
|
|
134
|
-
#### 不使用 EConfigProvider 的后果
|
|
135
|
-
|
|
136
|
-
| 功能 | 不包裹时的表现 |
|
|
137
|
-
|------|--------------|
|
|
138
|
-
| EDatePicker 月份/星期 | 显示英文(Jan、Feb、Mon、Tue) |
|
|
139
|
-
| ECalendar 月份/星期 | 显示英文 |
|
|
140
|
-
| 组件尺寸 | 各组件使用各自默认值,无法统一控制 |
|
|
141
|
-
| 弹层 z-index | 各组件使用各自默认值,可能层级混乱 |
|
|
142
|
-
| UI 文案 | useConfigProvider() 使用内部默认值(中文),但不保证一致性 |
|
|
143
|
-
|
|
144
21
|
## 命名约定
|
|
145
22
|
|
|
146
23
|
- 组件前缀 `E`:`EButton`, `EInput`, `EDialog`
|