.gitignore
@@ -7,6 +7,7 @@ scripts/**/__pycache__/ scripts/**/*.pyc data/*.local.md data/raw/ 管理端上下文.md MB-X错误日志.md MB-X错误日志.md data/result/stat/ds_minute_backfill_20260619/background.stderr.log
New file @@ -0,0 +1,7 @@ Select-String : ·¾¶ÖоßÓзǷ¨×Ö·û¡£ ËùÔÚλÖà D:\manage_system\scripts\ds_minute_backfill.ps1:15 ×Ö·û: 32 + ... PASSWORD = (Select-String -LiteralPath $localDoc -Pattern '密码:`([^`] ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Select-String], ArgumentException + FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.SelectStringCommand data/result/stat/ds_minute_backfill_20260619/background.stdout.log
data/result/stat/ds_minute_backfill_20260619_r2/background.stderr.log
data/result/stat/ds_minute_backfill_20260619_r2/background.stdout.log
data/result/stat/ds_minute_backfill_20260619_r2/dates.txt
New file @@ -0,0 +1,37 @@ 2026-04-23 2026-04-24 2026-04-27 2026-04-28 2026-04-29 2026-04-30 2026-05-06 2026-05-07 2026-05-08 2026-05-11 2026-05-12 2026-05-13 2026-05-14 2026-05-15 2026-05-18 2026-05-19 2026-05-20 2026-05-21 2026-05-22 2026-05-25 2026-05-26 2026-05-27 2026-05-28 2026-05-29 2026-06-01 2026-06-02 2026-06-03 2026-06-04 2026-06-05 2026-06-08 2026-06-09 2026-06-10 2026-06-11 2026-06-12 2026-06-15 2026-06-16 2026-06-17 data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260423_162049.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260423_162049.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260424_162709.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260424_162709.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260427_163323.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260427_163323.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260428_163932.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260428_163932.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260429_164538.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260429_164538.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260430_165136.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260430_165136.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260506_165739.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260506_165739.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260507_170341.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260507_170341.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260508_170943.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260508_170943.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260511_171548.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260511_171548.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260512_172203.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260512_172203.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260513_172805.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260513_172805.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260514_173409.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260514_173409.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260515_174009.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260515_174009.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260518_174617.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260518_174617.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260519_175222.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260519_175222.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260520_175827.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260520_175827.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260521_180436.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260521_180436.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260522_181041.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260522_181041.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260525_181704.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260525_181704.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260526_182307.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260526_182307.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260527_182915.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260527_182915.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260528_183527.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260528_183527.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260529_184149.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260529_184149.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260601_184802.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260601_184802.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260602_185414.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260602_185414.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260603_190024.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260603_190024.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260604_190634.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260604_190634.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260605_191246.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260605_191246.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260608_191917.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260608_191917.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260609_192531.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260609_192531.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260610_193146.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260610_193146.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260611_193757.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260611_193757.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260612_194411.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260612_194411.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260615_195031.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260615_195031.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260616_195644.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260616_195644.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260617_200258.err.txt
data/result/stat/ds_minute_backfill_20260619_r2/ds_minute_sync_20260617_200258.out.jsonBinary files differ
data/result/stat/ds_minute_backfill_20260619_r2/summary.tsv
New file @@ -0,0 +1,38 @@ date status run_id started_at finished_at 2026-04-23 SUCCESS ds_minute_sync_20260423_162049 2026-06-19T16:20:49 2026-06-19T16:27:09 2026-04-24 SUCCESS ds_minute_sync_20260424_162709 2026-06-19T16:27:09 2026-06-19T16:33:23 2026-04-27 SUCCESS ds_minute_sync_20260427_163323 2026-06-19T16:33:23 2026-06-19T16:39:32 2026-04-28 SUCCESS ds_minute_sync_20260428_163932 2026-06-19T16:39:32 2026-06-19T16:45:38 2026-04-29 SUCCESS ds_minute_sync_20260429_164538 2026-06-19T16:45:38 2026-06-19T16:51:36 2026-04-30 SUCCESS ds_minute_sync_20260430_165136 2026-06-19T16:51:36 2026-06-19T16:57:39 2026-05-06 SUCCESS ds_minute_sync_20260506_165739 2026-06-19T16:57:39 2026-06-19T17:03:41 2026-05-07 SUCCESS ds_minute_sync_20260507_170341 2026-06-19T17:03:41 2026-06-19T17:09:43 2026-05-08 SUCCESS ds_minute_sync_20260508_170943 2026-06-19T17:09:43 2026-06-19T17:15:48 2026-05-11 SUCCESS ds_minute_sync_20260511_171548 2026-06-19T17:15:48 2026-06-19T17:22:03 2026-05-12 SUCCESS ds_minute_sync_20260512_172203 2026-06-19T17:22:03 2026-06-19T17:28:05 2026-05-13 SUCCESS ds_minute_sync_20260513_172805 2026-06-19T17:28:05 2026-06-19T17:34:09 2026-05-14 SUCCESS ds_minute_sync_20260514_173409 2026-06-19T17:34:09 2026-06-19T17:40:09 2026-05-15 SUCCESS ds_minute_sync_20260515_174009 2026-06-19T17:40:09 2026-06-19T17:46:17 2026-05-18 SUCCESS ds_minute_sync_20260518_174617 2026-06-19T17:46:17 2026-06-19T17:52:22 2026-05-19 SUCCESS ds_minute_sync_20260519_175222 2026-06-19T17:52:22 2026-06-19T17:58:27 2026-05-20 SUCCESS ds_minute_sync_20260520_175827 2026-06-19T17:58:27 2026-06-19T18:04:36 2026-05-21 SUCCESS ds_minute_sync_20260521_180436 2026-06-19T18:04:36 2026-06-19T18:10:41 2026-05-22 SUCCESS ds_minute_sync_20260522_181041 2026-06-19T18:10:41 2026-06-19T18:17:04 2026-05-25 SUCCESS ds_minute_sync_20260525_181704 2026-06-19T18:17:04 2026-06-19T18:23:07 2026-05-26 SUCCESS ds_minute_sync_20260526_182307 2026-06-19T18:23:07 2026-06-19T18:29:15 2026-05-27 SUCCESS ds_minute_sync_20260527_182915 2026-06-19T18:29:15 2026-06-19T18:35:27 2026-05-28 SUCCESS ds_minute_sync_20260528_183527 2026-06-19T18:35:27 2026-06-19T18:41:49 2026-05-29 SUCCESS ds_minute_sync_20260529_184149 2026-06-19T18:41:49 2026-06-19T18:48:02 2026-06-01 SUCCESS ds_minute_sync_20260601_184802 2026-06-19T18:48:02 2026-06-19T18:54:14 2026-06-02 SUCCESS ds_minute_sync_20260602_185414 2026-06-19T18:54:14 2026-06-19T19:00:24 2026-06-03 SUCCESS ds_minute_sync_20260603_190024 2026-06-19T19:00:24 2026-06-19T19:06:34 2026-06-04 SUCCESS ds_minute_sync_20260604_190634 2026-06-19T19:06:34 2026-06-19T19:12:46 2026-06-05 SUCCESS ds_minute_sync_20260605_191246 2026-06-19T19:12:46 2026-06-19T19:19:17 2026-06-08 SUCCESS ds_minute_sync_20260608_191917 2026-06-19T19:19:17 2026-06-19T19:25:31 2026-06-09 SUCCESS ds_minute_sync_20260609_192531 2026-06-19T19:25:31 2026-06-19T19:31:46 2026-06-10 SUCCESS ds_minute_sync_20260610_193146 2026-06-19T19:31:46 2026-06-19T19:37:57 2026-06-11 SUCCESS ds_minute_sync_20260611_193757 2026-06-19T19:37:57 2026-06-19T19:44:11 2026-06-12 SUCCESS ds_minute_sync_20260612_194411 2026-06-19T19:44:11 2026-06-19T19:50:31 2026-06-15 SUCCESS ds_minute_sync_20260615_195031 2026-06-19T19:50:31 2026-06-19T19:56:44 2026-06-16 SUCCESS ds_minute_sync_20260616_195644 2026-06-19T19:56:44 2026-06-19T20:02:58 2026-06-17 SUCCESS ds_minute_sync_20260617_200258 2026-06-19T20:02:58 2026-06-19T20:09:14 data/全复权分钟线导入说明.md
New file @@ -0,0 +1,151 @@ # 全复权分钟线导入说明 创建时间:2026-06-14 ## 1. 数据定位 本次新增的是 A 股全复权 1 分钟 K 线数据,作为全局 MySQL 行情底座中的独立公共表使用。 目标表: - `a_share_minute_price_full_adj` - `a_share_minute_full_adj_import_run` - `a_share_minute_full_adj_import_file_log` 使用边界: - 这是全复权分钟线,不替代当前默认分钟线 `a_share_minute_price`。 - 这是独立口径,不与前复权同步影子表 `a_share_minute_price_front_sync` 混用。 - 查询必须带 `trade_date`、`symbol`,最好再带 `trade_time` 范围;禁止无边界全表扫描。 ## 2. 来源 源压缩包: ```text D:\A股日级行情数据近两年2023csv.rar ``` 解压目录: ```text D:\manage_system\data\raw\a_share_minute_full_adj_2023_2026_csv ``` 压缩包目录结构: ```text 2023_front_m/<市场>/price_<股票代码>.csv ``` CSV 字段: ```text timetag,open,high,low,close,volumn,amount ``` 字段说明: - `timetag`:分钟 bar 时间,格式如 `20230523 09:30:00` - `open/high/low/close`:全复权 OHLC - `volumn`:源文件字段名如此拼写,入库后映射为 `volume` - `amount`:成交额 ## 3. 导入结果 导入 run_id: ```text full_adj_minute_20260614_rar ``` 导入结果: | 项目 | 数值 | |---|---:| | 源 CSV 文件数 | 3161 | | 已处理文件数 | 3161 | | 有效入库行数 | 422,373,172 | | 非空数据起始日期 | 2023-03-24 | | 非空数据结束日期 | 2026-06-12 | | 源 0 字节文件数 | 732 | 按市场统计: | 市场 | 文件数 | 入库行数 | 0 字节文件数 | |---|---:|---:|---:| | BJ | 320 | 44,413,040 | 0 | | SH | 2314 | 331,542,089 | 508 | | SZ | 527 | 46,418,043 | 224 | 0 字节源文件已记录到 `a_share_minute_full_adj_import_file_log`,不写入行情表。 ## 4. 表结构摘要 `a_share_minute_price_full_adj` 主键: ```text symbol + trade_date + trade_time ``` 核心字段: | 字段 | 含义 | |---|---| | `trade_date` | 交易日 | | `trade_time` | 分钟时间 | | `bar_time` | 完整 datetime | | `symbol` | 股票代码,格式如 `600000.SH` | | `market` | `SH` / `SZ` / `BJ` | | `open_price` | 全复权开盘价 | | `high_price` | 全复权最高价 | | `low_price` | 全复权最低价 | | `close_price` | 全复权收盘价 | | `volume` | 源成交量 | | `amount` | 源成交额 | | `source_file` | 解压后相对文件路径 | | `source_archive` | 来源压缩包路径 | | `import_run_id` | 导入批次 | ## 5. 导入脚本与证据 导入脚本: ```text scripts/import_full_adj_minute_csv.py ``` 运行 wrapper: ```text tmp/run_full_adj_minute_import_20260614.ps1 ``` 证据日志: ```text tmp/full_adj_minute_import_20260614.stdout.log tmp/full_adj_minute_import_20260614.stderr.log tmp/full_adj_minute_import_20260614.wrapper.log tmp/rar_list_A股日级行情数据近两年2023csv_20260614.txt tmp/unrar_A股日级行情数据近两年2023csv_20260614.log ``` ## 6. 查询示例 ```sql SELECT * FROM a_share_minute_price_full_adj WHERE symbol = '600000.SH' AND trade_date = '2024-01-02' AND trade_time BETWEEN '09:30:00' AND '10:00:00' ORDER BY trade_time; ``` 禁止示例: ```sql SELECT COUNT(*) FROM a_share_minute_price_full_adj; ``` 全表统计必须优先使用导入运行表和文件日志表。 data/目录导读.md
@@ -14,10 +14,19 @@ ## 2. 当前状态 当前目录暂不放置具体数据文件,仅保留本导读文件用于占位和 Git 跟踪。 当前目录保存全局数据源说明、数据库索引说明、导入脚本说明和可跨项目复用的数据入口说明。 后续新增全局数据材料时,应先确认其是否满足跨项目复用、统一口径和长期维护要求。 当前已登记材料: | 文件 | 用途 | |---|---| | `数据库索引数据.local.md` | 本机 MySQL 连接和历史索引说明;包含本机敏感信息,不提交远程 | | `HTTP与MySQL使用说明.md` | 外部前复权 K 线服务 HTTP/MySQL 使用说明 | | `K线同步到本地数据库全景说明.md` | 前复权 K 线同步到本地数据库的全景说明 | | `全复权分钟线导入说明.md` | 2026-06-14 新增全复权 1 分钟 K 线入库说明 | ## 3. 使用原则 1. 不存放临时文件、缓存文件或单项目私有数据。 scripts/ds_minute_backfill.ps1
New file @@ -0,0 +1,61 @@ param( [string]$StartDate = "2026-04-23", [string]$EndDate = "2026-06-17", [string]$LogDir = "D:\manage_system\data\result\stat\ds_minute_backfill_20260619" ) $ErrorActionPreference = "Stop" $root = Split-Path -Parent $PSScriptRoot $script = Join-Path $root "scripts\kline_front_incremental_sync.py" $localDoc = Get-ChildItem -LiteralPath (Join-Path $root "data") -Filter "*.local.md" | Select-String -Pattern "mysql://root:" -List | Select-Object -First 1 -ExpandProperty Path $sourceDoc = Get-ChildItem -LiteralPath (Join-Path $root "data") -Filter "*.local.md" | Select-String -Pattern "trading_xuntou" -List | Select-Object -First 1 -ExpandProperty Path New-Item -ItemType Directory -Force -Path $LogDir | Out-Null $env:TIANXIA_MYSQL_PASSWORD = (Select-String -LiteralPath $localDoc -Pattern 'mysql://root:([^@]+)@' | Select-Object -First 1).Matches[0].Groups[1].Value $env:KLINE_SOURCE_MYSQL_PASSWORD = (Select-String -LiteralPath $sourceDoc -Pattern 'Password.*`([^`]+)`' | Select-Object -First 1).Matches[0].Groups[1].Value $datesPath = Join-Path $LogDir "dates.txt" @" import os, pymysql source = pymysql.connect(host='317w7246e5.vicp.fun', port=50176, user='root', password=os.environ['KLINE_SOURCE_MYSQL_PASSWORD'], database='trading_xuntou', charset='utf8mb4', connect_timeout=10, read_timeout=120) local = pymysql.connect(host='127.0.0.1', port=3306, user='root', password=os.environ['TIANXIA_MYSQL_PASSWORD'], database='tianxia', charset='utf8mb4', connect_timeout=10, read_timeout=120) with source.cursor() as cur: cur.execute("SELECT trade_date FROM cn_stock_kline_front_coverage_daily WHERE period='1m' AND trade_date BETWEEN %s AND %s AND row_count > 0 ORDER BY trade_date", ('$StartDate', '$EndDate')) source_dates = [str(row[0]) for row in cur.fetchall()] with local.cursor() as cur: cur.execute("SELECT DISTINCT trade_date FROM a_share_minute_price WHERE trade_date BETWEEN %s AND %s", ('$StartDate', '$EndDate')) done_dates = {str(row[0]) for row in cur.fetchall()} for d in source_dates: if d not in done_dates: print(d) source.close() local.close() "@ | python - | Set-Content -LiteralPath $datesPath -Encoding ASCII $dates = Get-Content -LiteralPath $datesPath | Where-Object { $_ -match '^\d{4}-\d{2}-\d{2}$' } $summaryPath = Join-Path $LogDir "summary.tsv" "date`tstatus`trun_id`tstarted_at`tfinished_at" | Set-Content -LiteralPath $summaryPath -Encoding UTF8 foreach ($date in $dates) { $runId = "ds_minute_sync_" + ($date -replace "-", "") + "_" + (Get-Date -Format "HHmmss") $started = Get-Date -Format "s" $out = Join-Path $LogDir "$runId.out.json" $err = Join-Path $LogDir "$runId.err.txt" try { python $script --run-id $runId --periods 1m --start-date $date --end-date $date --minute-max-minutes-per-batch 120 --minute-symbols-per-batch 200 --append-to-main > $out 2> $err $status = if ($LASTEXITCODE -eq 0) { "SUCCESS" } else { "FAILED_$LASTEXITCODE" } } catch { $_ | Out-File -LiteralPath $err -Append -Encoding UTF8 $status = "FAILED_EXCEPTION" } $finished = Get-Date -Format "s" "$date`t$status`t$runId`t$started`t$finished" | Add-Content -LiteralPath $summaryPath -Encoding UTF8 if ($status -notlike "SUCCESS") { break } } scripts/import_full_adj_minute_csv.py
New file @@ -0,0 +1,330 @@ from __future__ import annotations import argparse import os import re import sys import time from datetime import datetime from pathlib import Path import pymysql MARKET_SUFFIX = { "SH": ".SH", "SZ": ".SZ", "BJ": ".BJ", } def connect(args: argparse.Namespace): password = args.password or os.environ.get("TIANXIA_MYSQL_PASSWORD") if not password: raise RuntimeError("missing MySQL password: pass --password or set TIANXIA_MYSQL_PASSWORD") return pymysql.connect( host=args.host, port=args.port, user=args.user, password=password, database=args.database, charset="utf8mb4", autocommit=False, local_infile=True, ) def ensure_schema(conn) -> None: ddl = [ """ CREATE TABLE IF NOT EXISTS a_share_minute_price_full_adj ( trade_date date NOT NULL, trade_time time NOT NULL, bar_time datetime NOT NULL, symbol char(9) NOT NULL, market varchar(8) NOT NULL, open_price decimal(14,4) NULL, high_price decimal(14,4) NULL, low_price decimal(14,4) NULL, close_price decimal(14,4) NULL, volume decimal(24,4) NULL, amount decimal(24,4) NULL, source_file varchar(255) NOT NULL, source_archive varchar(255) NOT NULL, import_run_id varchar(96) NOT NULL, imported_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (symbol, trade_date, trade_time), KEY idx_full_adj_minute_date_symbol_time (trade_date, symbol, trade_time), KEY idx_full_adj_minute_run (import_run_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """, """ CREATE TABLE IF NOT EXISTS a_share_minute_full_adj_import_run ( run_id varchar(96) NOT NULL, started_at datetime NOT NULL, finished_at datetime NULL, status varchar(32) NOT NULL, source_archive varchar(512) NOT NULL, source_root varchar(512) NOT NULL, files_total int NOT NULL DEFAULT 0, files_loaded int NOT NULL DEFAULT 0, rows_loaded bigint NOT NULL DEFAULT 0, min_trade_date date NULL, max_trade_date date NULL, error_message text NULL, PRIMARY KEY (run_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """, """ CREATE TABLE IF NOT EXISTS a_share_minute_full_adj_import_file_log ( id bigint NOT NULL AUTO_INCREMENT, run_id varchar(96) NOT NULL, symbol char(9) NOT NULL, market varchar(8) NOT NULL, source_file varchar(512) NOT NULL, file_size bigint NOT NULL, rows_loaded bigint NOT NULL, started_at datetime NOT NULL, finished_at datetime NOT NULL, duration_ms bigint NOT NULL, status varchar(32) NOT NULL, error_message text NULL, PRIMARY KEY (id), UNIQUE KEY uq_full_adj_file_run (run_id, source_file), KEY idx_full_adj_file_symbol (symbol), KEY idx_full_adj_file_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """, ] with conn.cursor() as cur: for sql in ddl: cur.execute(sql) conn.commit() def discover_files(root: Path) -> list[Path]: files = sorted(root.rglob("price_*.csv")) if not files: raise RuntimeError(f"no price_*.csv files found under {root}") return files def symbol_for(path: Path) -> tuple[str, str]: market = path.parent.name.upper() suffix = MARKET_SUFFIX.get(market) if not suffix: raise RuntimeError(f"unsupported market directory {market}: {path}") m = re.match(r"price_(\d{6})\.csv$", path.name, re.IGNORECASE) if not m: raise RuntimeError(f"unsupported file name: {path}") return m.group(1) + suffix, market def init_run(conn, run_id: str, source_archive: str, source_root: str, files_total: int) -> None: with conn.cursor() as cur: cur.execute( """ INSERT INTO a_share_minute_full_adj_import_run (run_id, started_at, status, source_archive, source_root, files_total) VALUES (%s, NOW(), 'running', %s, %s, %s) ON DUPLICATE KEY UPDATE status='running', source_archive=VALUES(source_archive), source_root=VALUES(source_root), files_total=VALUES(files_total), error_message=NULL """, (run_id, source_archive, source_root, files_total), ) conn.commit() def finish_run(conn, run_id: str, status: str, error: str | None = None) -> None: with conn.cursor() as cur: cur.execute( """ UPDATE a_share_minute_full_adj_import_run r LEFT JOIN ( SELECT import_run_id, COUNT(*) AS rows_loaded, MIN(trade_date) AS min_trade_date, MAX(trade_date) AS max_trade_date FROM a_share_minute_price_full_adj WHERE import_run_id=%s GROUP BY import_run_id ) x ON x.import_run_id = r.run_id SET r.finished_at=NOW(), r.status=%s, r.rows_loaded=COALESCE(x.rows_loaded, r.rows_loaded), r.min_trade_date=COALESCE(x.min_trade_date, r.min_trade_date), r.max_trade_date=COALESCE(x.max_trade_date, r.max_trade_date), r.error_message=%s WHERE r.run_id=%s """, (run_id, status, error, run_id), ) conn.commit() def load_file(conn, path: Path, root: Path, archive: str, run_id: str) -> int: symbol, market = symbol_for(path) rel = str(path.relative_to(root)).replace("\\", "/") started = datetime.now() t0 = time.perf_counter() with conn.cursor() as cur: cur.execute("DROP TEMPORARY TABLE IF EXISTS tmp_full_adj_minute_csv") cur.execute( """ CREATE TEMPORARY TABLE tmp_full_adj_minute_csv ( timetag varchar(19) NOT NULL, open_price decimal(14,4) NULL, high_price decimal(14,4) NULL, low_price decimal(14,4) NULL, close_price decimal(14,4) NULL, volume decimal(24,4) NULL, amount decimal(24,4) NULL ) ENGINE=InnoDB """ ) cur.execute( """ LOAD DATA LOCAL INFILE %s INTO TABLE tmp_full_adj_minute_csv CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (timetag, open_price, high_price, low_price, close_price, volume, amount) """, (str(path),), ) loaded_tmp = cur.rowcount cur.execute( """ INSERT INTO a_share_minute_price_full_adj (trade_date, trade_time, bar_time, symbol, market, open_price, high_price, low_price, close_price, volume, amount, source_file, source_archive, import_run_id) SELECT DATE(STR_TO_DATE(timetag, '%%Y%%m%%d %%H:%%i:%%s')), TIME(STR_TO_DATE(timetag, '%%Y%%m%%d %%H:%%i:%%s')), STR_TO_DATE(timetag, '%%Y%%m%%d %%H:%%i:%%s'), %s, %s, open_price, high_price, low_price, close_price, volume, amount, %s, %s, %s FROM tmp_full_adj_minute_csv ON DUPLICATE KEY UPDATE open_price=VALUES(open_price), high_price=VALUES(high_price), low_price=VALUES(low_price), close_price=VALUES(close_price), volume=VALUES(volume), amount=VALUES(amount), source_file=VALUES(source_file), source_archive=VALUES(source_archive), import_run_id=VALUES(import_run_id), imported_at=CURRENT_TIMESTAMP """, (symbol, market, rel, archive, run_id), ) inserted = cur.rowcount duration_ms = int((time.perf_counter() - t0) * 1000) cur.execute( """ INSERT INTO a_share_minute_full_adj_import_file_log (run_id, symbol, market, source_file, file_size, rows_loaded, started_at, finished_at, duration_ms, status) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), %s, 'loaded') ON DUPLICATE KEY UPDATE rows_loaded=VALUES(rows_loaded), file_size=VALUES(file_size), finished_at=VALUES(finished_at), duration_ms=VALUES(duration_ms), status='loaded', error_message=NULL """, (run_id, symbol, market, rel, path.stat().st_size, loaded_tmp, started, duration_ms), ) cur.execute( """ UPDATE a_share_minute_full_adj_import_run SET files_loaded = ( SELECT COUNT(*) FROM a_share_minute_full_adj_import_file_log WHERE run_id=%s AND status='loaded' ) WHERE run_id=%s """, (run_id, run_id), ) conn.commit() return loaded_tmp if loaded_tmp >= 0 else inserted def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", required=True) parser.add_argument("--source-archive", required=True) parser.add_argument("--run-id") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=3306) parser.add_argument("--user", default="root") parser.add_argument("--password") parser.add_argument("--database", default="tianxia") parser.add_argument("--limit", type=int) parser.add_argument("--skip-loaded", action="store_true") args = parser.parse_args() root = Path(args.source_root).resolve() files = discover_files(root) if args.limit: files = files[: args.limit] run_id = args.run_id or "full_adj_minute_" + datetime.now().strftime("%Y%m%d_%H%M%S") conn = connect(args) try: ensure_schema(conn) init_run(conn, run_id, args.source_archive, str(root), len(files)) loaded_total = 0 with conn.cursor() as cur: loaded_files = set() if args.skip_loaded: cur.execute( "SELECT source_file FROM a_share_minute_full_adj_import_file_log WHERE run_id=%s AND status='loaded'", (run_id,), ) loaded_files = {row[0] for row in cur.fetchall()} for idx, path in enumerate(files, 1): rel = str(path.relative_to(root)).replace("\\", "/") if rel in loaded_files: continue rows = load_file(conn, path, root, args.source_archive, run_id) loaded_total += rows if idx % 10 == 0 or idx == 1 or idx == len(files): print(f"{datetime.now():%Y-%m-%d %H:%M:%S} loaded {idx}/{len(files)} files; last={rel}; rows={rows}; total_rows_seen={loaded_total}", flush=True) finish_run(conn, run_id, "completed") print(f"completed run_id={run_id} files={len(files)} rows_seen={loaded_total}") return 0 except Exception as exc: conn.rollback() try: finish_run(conn, run_id, "failed", str(exc)) except Exception: pass print(f"failed run_id={run_id}: {exc}", file=sys.stderr) return 1 finally: conn.close() if __name__ == "__main__": raise SystemExit(main()) scripts/kline_front_incremental_sync.py
@@ -99,9 +99,10 @@ extra.extend(["--batch", "--raw"]) if skip_column_names: extra.append("--skip-column-names") cmd = mysql_cmd(conn, extra + ["-e", sql]) cmd = mysql_cmd(conn, extra) proc = subprocess.run( cmd, input=sql, text=True, capture_output=True, env=mysql_env(conn), 全局存储体系.md
@@ -47,11 +47,14 @@ |---|---|---| | `a_share_daily_price` | 默认 A 股前复权日线 K 线 | 当前默认日线主表 | | `a_share_minute_price` | A 股分钟 K 线 | 当前默认分钟线主表,体量最大 | | `a_share_minute_price_full_adj` | A 股全复权 1 分钟 K 线 | 新增全复权分钟线公共表;不替代默认分钟线,查询必须带 `trade_date + symbol + trade_time` 边界 | | `a_share_daily_front_adj_missing_ohlc_20230101_20260508` | 前复权源包中停牌 / 无 OHLC 行审计 | 不能补价进默认 K 线表 | | `a_share_daily_price_backup_before_front_adj_20260523_1808` | 前复权切换前旧日线备份 | 只用于追溯旧口径 | | `a_share_daily_price_front_sync` | 前复权日线同步增量 / 影子表 | 用于同步和补数审计,不替代默认主表 | | `a_share_minute_price_front_sync` | 前复权分钟线同步增量 / 影子表 | 用于同步和补数审计,不替代默认主表 | | `a_share_kline_front_sync_run` | K 线同步运行记录 | 用于追踪同步批次、行数和状态 | | `a_share_minute_full_adj_import_run` | 全复权分钟线导入批次 | 记录 `full_adj_minute_20260614_rar` 等导入批次、文件数、行数和日期范围 | | `a_share_minute_full_adj_import_file_log` | 全复权分钟线文件级导入日志 | 记录每个源 CSV 的市场、代码、文件大小、入库行数;0 字节源文件在此表留痕 | ### 4.2 A 股基础维表和元数据表 @@ -172,3 +175,4 @@ ## 9. 变更记录 - 2026-06-03:首次建立全局存储体系文档;根据本机 MySQL `tianxia` 的 `information_schema`、数据库索引文档和 stock-data 文件镜像整理当前公共表边界。 - 2026-06-14:新增全复权 1 分钟 K 线公共表 `a_share_minute_price_full_adj`,来源为 `D:\A股日级行情数据近两年2023csv.rar`;导入批次 `full_adj_minute_20260614_rar`,3161 个 CSV 已处理,有效入库 422,373,172 行,非空日期范围 `2023-03-24` 到 `2026-06-12`;详见 `data/全复权分钟线导入说明.md`。 全局数据格式.md
@@ -117,6 +117,66 @@ | `sync_run_id` | `varchar(96)` | indexed | 同步运行 ID | | `synced_at` | `timestamp` | auto update | 同步写入时间 | ### `a_share_minute_price_full_adj` A 股全复权 1 分钟 K 线公共表。该表是全复权口径,不替代默认分钟线主表 `a_share_minute_price`,也不与前复权同步影子表混用。 | 字段 | 类型 | 约束 | 含义 | |---|---|---|---| | `trade_date` | `date` | PK | 交易日 | | `trade_time` | `time` | PK | 分钟时间 | | `bar_time` | `datetime` | indexed | 完整 bar 时间 | | `symbol` | `char(9)` | PK | 股票代码,形如 `600000.SH`、`000001.SZ`、`920000.BJ` | | `market` | `varchar(8)` | not null | 市场,`SH` / `SZ` / `BJ` | | `open_price` | `decimal(14,4)` | nullable | 全复权开盘价 | | `high_price` | `decimal(14,4)` | nullable | 全复权最高价 | | `low_price` | `decimal(14,4)` | nullable | 全复权最低价 | | `close_price` | `decimal(14,4)` | nullable | 全复权收盘价 | | `volume` | `decimal(24,4)` | nullable | 源成交量 | | `amount` | `decimal(24,4)` | nullable | 源成交额 | | `source_file` | `varchar(255)` | not null | 解压后相对源文件路径 | | `source_archive` | `varchar(255)` | not null | 来源压缩包路径 | | `import_run_id` | `varchar(96)` | indexed | 导入批次 | | `imported_at` | `timestamp` | auto | 入库时间 | 主键:`symbol + trade_date + trade_time`。 查询约束:必须带 `trade_date`、`symbol`,最好同时限制 `trade_time` 范围;全表统计优先读 `a_share_minute_full_adj_import_run` 和 `a_share_minute_full_adj_import_file_log`。 ### `a_share_minute_full_adj_import_run` 全复权分钟线导入批次记录表。 | 字段 | 类型 | 约束 | 含义 | |---|---|---|---| | `run_id` | `varchar(96)` | PK | 导入批次 ID | | `started_at` / `finished_at` | `datetime` | indexed / nullable | 开始和结束时间 | | `status` | `varchar(32)` | indexed | 运行状态 | | `source_archive` | `varchar(512)` | not null | 来源压缩包 | | `source_root` | `varchar(512)` | not null | 解压根目录 | | `files_total` / `files_loaded` | `int` | not null | 文件总数和已处理文件数 | | `rows_loaded` | `bigint` | not null | 有效入库行数 | | `min_trade_date` / `max_trade_date` | `date` | nullable | 非空数据日期范围 | | `error_message` | `text` | nullable | 失败信息 | ### `a_share_minute_full_adj_import_file_log` 全复权分钟线文件级导入日志。 | 字段 | 类型 | 约束 | 含义 | |---|---|---|---| | `id` | `bigint` | PK auto_increment | 日志 ID | | `run_id` | `varchar(96)` | indexed | 导入批次 | | `symbol` | `char(9)` | indexed | 股票代码 | | `market` | `varchar(8)` | indexed | 市场 | | `source_file` | `varchar(512)` | unique with run | 源文件相对路径 | | `file_size` | `bigint` | not null | 源文件大小 | | `rows_loaded` | `bigint` | not null | 该文件入库行数;0 表示源文件为空或无有效行 | | `started_at` / `finished_at` | `datetime` | not null | 文件处理时间 | | `duration_ms` | `bigint` | not null | 文件处理耗时 | | `status` | `varchar(32)` | indexed | 文件处理状态 | | `error_message` | `text` | nullable | 文件级错误 | ### `a_share_kline_front_sync_run` K 线同步运行记录。 @@ -415,3 +475,4 @@ ## 11. 变更记录 - 2026-06-03:首次建立全局数据格式文档;根据本机 MySQL `information_schema`、数据库索引文档和 stock-data 脚本整理公共表字段。 - 2026-06-14:新增 `a_share_minute_price_full_adj`、`a_share_minute_full_adj_import_run`、`a_share_minute_full_adj_import_file_log` 的字段 schema,登记全复权 1 分钟 K 线公共表和导入审计表。 管理执行日志.md
New file @@ -0,0 +1,140 @@ # 管理执行日志 ## 2026-06-16 16:26:30 MGMT-DS-20260616-KLINE-SYNC 关联事项:全局行情数据同步 执行人员:management 动作类型:数据同步 执行目标:使用 ds 同步 2026-06-16 可用的 A 股前复权 K 线数据到本地全局数据库。 执行过程: 1. 读取 ds 技能说明,确认同步脚本 `scripts/kline_front_incremental_sync.py` 可用并通过 `py_compile`。 2. 查询源端与本地覆盖范围,确认源端日线已有 `2026-06-16`,源端分钟线覆盖表最新为 `2026-06-12`,`2026-06-16` 分钟线覆盖行数为 `0`。 3. 执行日线同步 run_id=`ds_daily_sync_20260616_162459`,从 `2026-06-11` 同步到 `2026-06-16`。 4. 同步后校验本地主表 `a_share_daily_price` 在 `2026-06-16` 有 `5513` 行;镜像表本次 run 写入 `22044` 行。 使用命令或 skill:`ds`;`python scripts/kline_front_incremental_sync.py --run-id ds_daily_sync_20260616_162459 --periods 1d --append-to-main` 关键输出: - 日线源行数:`22044` - 日线镜像写入行数:`22044` - 日线主表追加行数:`22044` - 本地 `2026-06-16` 日线主表行数:`5513` - 源端 `2026-06-16` 分钟线覆盖行数:`0` - 源端分钟线覆盖表最新日期:`2026-06-12` 自检结果:日线同步完成并通过本地行数校验;分钟线因源端尚无 `2026-06-16` 覆盖数据,本次未同步。 异常 / 偏离:源端分钟线未提供 `2026-06-16` 数据,不触发外部 HTTP 补数任务,不写外部数据库。 当前状态:部分完成 ## 2026-06-19 16:25:00 MGMT-DS-20260619-KLINE-SYNC 关联事项:全局行情数据同步 执行人员:management 动作类型:数据同步 / 脚本修复 执行目标:使用 ds 同步截至 2026-06-19 源端可用的 A 股前复权日线与分钟线数据到本地全局数据库。 执行过程: 1. 读取 ds 技能说明,确认 `scripts/kline_front_incremental_sync.py` 可用并通过 `py_compile`。 2. 查询覆盖范围:源端日线最新为 `2026-06-18`,源端 `2026-06-19` 日线行数为 `0`;源端分钟线覆盖表最新为 `2026-06-17`,`2026-06-19` 分钟线覆盖行数为 `0`。 3. 执行日线同步 run_id=`ds_daily_sync_20260619_155322`,从 `2026-06-17` 同步到 `2026-06-18`。 4. 分钟线先执行 `2026-04-21`,数据已落镜像表和主表各 `1324777` 行;收尾写 run log 时触发 Windows 命令行长度限制。 5. 修复 `scripts/kline_front_incremental_sync.py` 的 MySQL 调用方式:长 SQL 改为 stdin 输入,避免 `-e <large sql>` 触发命令行长度限制。 6. 补齐 `2026-04-21` run_id=`ds_minute_sync_20260421_155345` 的状态为 `SUCCESS`。 7. 使用优化参数完成 `2026-04-22` 分钟线同步,run_id=`ds_minute_sync_20260422_161255`,写入和追加 `1324777` 行。 8. 新增并启动后台分日补齐脚本 `scripts/ds_minute_backfill.ps1`,从 `2026-04-23` 到 `2026-06-17` 按交易日继续同步,日志目录为 `data/result/stat/ds_minute_backfill_20260619_r2`。 使用命令或 skill:`ds`;`python scripts/kline_front_incremental_sync.py --periods 1d --append-to-main`;`python scripts/kline_front_incremental_sync.py --periods 1m --minute-max-minutes-per-batch 120 --minute-symbols-per-batch 200 --append-to-main`;`scripts/ds_minute_backfill.ps1` 关键输出: - 日线 run_id:`ds_daily_sync_20260619_155322` - 日线同步范围:`2026-06-17` 至 `2026-06-18` - 日线源行数 / 镜像写入 / 主表追加:`11016 / 11016 / 11016` - 分钟线已完成日期:`2026-04-21`、`2026-04-22` - 分钟线后台待补日期数:`37` - 分钟线后台 PID:`51640` - 分钟线后台日志目录:`data/result/stat/ds_minute_backfill_20260619_r2` 自检结果:日线已同步到源端最新;分钟线因缺口约 `51690549` 行,已转入后台分日任务。源端尚无 `2026-06-19` 日线和分钟线数据。 异常 / 偏离:`2026-04-21` 分钟线 run log 首次收尾因 Windows 命令行长度限制失败;已修复脚本并手工恢复该 run 的成功状态。未写外部数据库,未触发外部 HTTP 补数。 当前状态:部分完成,分钟线后台同步中 ## 2026-06-23 04:10:00 MGMT-DS-20260623-KLINE-SYNC 关联事项:全局行情数据同步 执行人员:management 动作类型:数据同步 执行目标:使用 ds 同步截至 2026-06-23 源端可用的 A 股前复权 K 线数据到本地全局数据库。 执行过程: 1. 读取 ds 技能说明,确认 `scripts/kline_front_incremental_sync.py` 可用并通过 `py_compile`。 2. 查询覆盖范围:源端日线最新为 `2026-06-22`,源端 `2026-06-23` 日线行数为 `0`;源端分钟线覆盖表最新为 `2026-06-17`,`2026-06-17` 之后至 `2026-06-23` 覆盖行数为 `0`。 3. 执行日线同步 run_id=`ds_daily_sync_20260623_040911`,从 `2026-06-19` 同步到 `2026-06-22`。 4. 同步后校验本地主表 `a_share_daily_price` 在 `2026-06-22` 有 `5510` 行,本地主表最大日线日期为 `2026-06-22`。 使用命令或 skill:`ds`;`python scripts/kline_front_incremental_sync.py --run-id ds_daily_sync_20260623_040911 --periods 1d --append-to-main` 关键输出: - 日线同步范围:`2026-06-19` 至 `2026-06-22` - 日线源行数 / 镜像写入 / 主表追加:`5510 / 5510 / 5510` - 本地 `2026-06-22` 日线主表行数:`5510` - 源端分钟线覆盖表最新日期:`2026-06-17` - 源端 `2026-06-17` 之后至 `2026-06-23` 分钟线覆盖行数:`0` 自检结果:日线已同步到源端最新;分钟线源端无新增数据,本次未执行分钟线写入。 异常 / 偏离:源端尚无 `2026-06-23` 日线数据,也无 `2026-06-17` 之后的分钟线覆盖数据。未写外部数据库,未触发外部 HTTP 补数。 当前状态:完成源端可用范围同步 ## 2026-06-23 20:46:00 MGMT-DS-20260623-KLINE-SYNC-2 关联事项:全局行情数据同步 执行人员:management 动作类型:数据同步 执行目标:再次使用 ds 检查并同步 2026-06-23 当日源端新增的 A 股前复权 K 线数据。 执行过程: 1. 读取 ds 技能说明,确认 `scripts/kline_front_incremental_sync.py` 可用并通过 `py_compile`。 2. 查询覆盖范围:源端日线最新已更新为 `2026-06-23`,源端 `2026-06-23` 日线行数为 `5513`;源端分钟线覆盖表最新仍为 `2026-06-17`,`2026-06-17` 之后至 `2026-06-23` 覆盖行数为 `0`。 3. 执行日线同步 run_id=`ds_daily_sync_20260623_204520`,同步 `2026-06-23`。 4. 同步后校验本地主表 `a_share_daily_price` 在 `2026-06-23` 有 `5513` 行,本地主表最大日线日期为 `2026-06-23`。 使用命令或 skill:`ds`;`python scripts/kline_front_incremental_sync.py --run-id ds_daily_sync_20260623_204520 --periods 1d --append-to-main` 关键输出: - 日线同步范围:`2026-06-23` 至 `2026-06-23` - 日线源行数 / 镜像写入 / 主表追加:`5513 / 5513 / 5513` - 本地 `2026-06-23` 日线主表行数:`5513` - 源端分钟线覆盖表最新日期:`2026-06-17` - 源端 `2026-06-17` 之后至 `2026-06-23` 分钟线覆盖行数:`0` 自检结果:日线已同步到源端最新;分钟线源端无新增数据,本次未执行分钟线写入。 异常 / 偏离:未写外部数据库,未触发外部 HTTP 补数。 当前状态:完成源端可用范围同步