apps/tradecat 当前运行口径是:Google Sheets CSV -> 本地 JSON 快照缓存 -> CLI/TUI 展示。服务不再携带后端数据库,不再创建 SQLite schema,不再提供本地 SQL 查询层,也不再执行数据库压缩、vacuum、repair/backfill 等维护流程。
旧 SQLite 章节只作为历史事故和退役背景保留,不能作为当前实现依据。后续修改必须以 cache.py、registry.py、sync.py、lifecycle.py、tui.py 和 README.md/AGENTS.md 的缓存文件契约为准。
- 新增
cache.py:用~/.tradecat/cache下的 JSON 文件保存 dataset registry、manifest、snapshot matrix 与事件流增量文件。 - 删除
db.py、query.py、storage.py、runtime/lock.py和sql/*.sql,从代码层移除数据库后端。 sync/lifecycle/cli改为缓存文件入口;默认 TUI 只读缓存并按需探针当前 tap。- snapshot tap 保留多批快照;event_stream 独立维护
stream_events.json,用时间(北京)+内容去重并更新 seen 状态。 - TUI 按真实内容宽度生成整表 psql 视图,不冻结主键列,也不提供右侧列横向滚动;终端只裁剪可见区域,不做按 tap 自动缩放、撑满空隙或固定宽度省略。
覆盖项:
pytest -q tests:缓存初始化、同步、事件流增量、TUI plain/curses 辅助逻辑、鼠标/交易对链接。ruff check src tests:缓存文件实现无 lint 错误。compileall src tests:模块删除后仍可完整编译。
market_snapshot 是 529 x 1694 级别的超宽快照。即使已经移除完整 dataset_cells 历史,长期永久保存 CSV 原文与 snapshot 行体仍会持续占用大量 SQLite 空间。
旧永久事实层把大文本 payload 作为明文 TEXT 存储:raw_bodies.body_text 和 snapshot_row_store.row_json 会重复保存大量字段名、逗号、JSON 结构和短数值文本。SQLite 页与索引优化不能替代 payload 压缩。
- 新增
codec.py,统一提供zlib文本压缩与解压。 raw_bodies新增body_blob/body_codec/compressed_byte_count;新写入不再保存完整body_text明文。snapshot_row_store新增row_blob/row_codec/byte_count/compressed_byte_count;新写入不再保存完整row_json明文。- 历史读取路径兼容旧 TEXT 与新 BLOB;
read_snapshot_batch_rows()统一解码后返回原有 dict 结构。 - 新增
tradecat-terminal storage stats/compress/vacuum,把旧库明文 payload 显式迁移为压缩 BLOB,并按需 VACUUM 回收文件空间。
覆盖项:
- 新同步写入
raw_bodies/snapshot_row_store时,TEXT 兼容字段为空,BLOB 字段可用。 - 旧库只有
body_text/row_json时,storage compress可迁移为 BLOB。 read_snapshot_batch_rows()可读取压缩行体并完整回放历史快照。storage vacuum返回文件空间回收报告。
用户侧终端需要默认永久保存在线表格历史,并且要同时支持快照型 tap 的历史回放、事件流 tap 的增量追加、TUI 快速打开和 SQL 可控查询。单纯保留当前镜像会丢历史;把全部宽表展开成历史 cell 又会让 SQLite 膨胀。
旧模型没有把“原始拉取真相、快照历史、事件流历史、当前视图、查询索引”拆成独立层。历史保存语义只能在 sheet_row_snapshots/dataset_cells 和 sheet_mirror_rows 之间摇摆,无法同时满足永久回放和轻量运行。
registry.py为每个 active dataset 增加data_mode/history_policy/index_columns/event_key_columns/table_region_policy。- 新增
storage.py,统一处理 CSV 原始记录、表格区域解析、snapshot/stream 写入分流。 - 新增
raw_bodies/raw_fetches保存远端 CSV 原始真相。 - 新增
dataset_batches/snapshot_row_store/snapshot_row_refs保存 snapshot 永久版本;相同 matrix 不新增 batch,相同行体不重复保存。 - 新增
stream_events保存事件流永久事件;相同 event_key 不重复插入,只更新seen_count/last_seen_at。 - 新增
dataset_current_rows/dataset_index_cells分别承载当前视图和关键字段索引,TUI/SQL 不需要默认扫全量历史。 TRADECAT_TERMINAL_MAX_BATCHES_PER_DATASET默认改为0,旧兼容快照也不再隐式裁剪。
覆盖项:
- 初始化数据库会创建永久逻辑存储表。
- registry 输出
snapshot/streamdata_mode 和 event_key/index_columns。 - 相同 CSV body 只保存一个 raw body,但每次 fetch 都有 raw_fetch 记录。
- snapshot 内容不变不新增 batch;只变一行时只新增变化行体,任意 batch 可完整回放。
- event_stream 重复事件只更新
seen_count,新增事件追加。 market_snapshot仍不写完整dataset_cells历史。
tradecat-terminal 已能从在线表格 CSV 读取行数据,但落库主路径只写入 sheet_rows.row_json 与 sheet_row_snapshots.row_json。这会把 Google Sheets 的表头、列号、字段名和值关系压扁成 JSON 文本,TUI 只能展示前几个键值,SQL 也难以按字段稳定查询。
旧实现把在线表格当作“行 JSON 镜像”,没有把在线表格天然存在的结构化契约落为本地数据库索引层。
- 保留
sheet_rows/sheet_row_snapshots作为完整原始审计层。 - 新增
dataset_current_cells/dataset_cells作为结构化当前层和历史批次层。 - 写入时同步维护字段清单、当前单元格、历史单元格。
repair从已有原始 JSON 行非破坏性重建结构化层,避免内容 hash 未变化时结构化索引缺失。- TUI 优先读取结构化层,只有旧库未重建时才回退到原始 JSON。
覆盖项:
- 写入后可从
dataset_current_cells按字段名查询真实值。 - 写入后可从
dataset_cells按批次查询历史结构化值。 repair可补齐结构化表。- TUI 静态输出读取结构化层。
交互式 tradecat 在渲染宽表时出现 _curses.error: addwstr() returned ERR。截图中 psql 表格线条、中文字段和长元信息在终端内出现错位、断裂和越界。
TUI 渲染层把 Python 字符数当成终端显示宽度使用:len(line) 计算横向滚动范围,普通字符串切片裁剪屏幕行。中文、全角字符和部分 Unicode 字符的显示宽度大于 1,导致实际写入宽度超过 curses 窗口边界,触发 addwstr() 错误;同时横向 viewport 的切片位置也会落在错误显示列上。
- 引入显示宽度计算:按 Unicode combining / control / East Asian Width 计算终端显示格。
_add_line()、_add_table_line()、_safe_addstr()全部改为显示宽度感知裁剪。- 横向滚动最大范围改用显示宽度,而不是
len()。 - 保留数据库与表格数据完整性,只在 curses viewport 层做显示裁剪。
覆盖项:
- 模拟
addwstr() returned ERR时,安全写屏幕会缩短显示内容而不是崩溃。 - 中文宽字符切片后的显示宽度不会超过目标终端宽度。
ruff check src tests通过。bash scripts/verify.sh通过。TERM=xterm-256color script -q -c 'timeout 2s tradecat' ...无 Traceback / curses error。
anomaly_panel、event_stream、market_stats、market_snapshot 都复用同一个表格渲染入口。截图中 market_stats 出现中文字段、psql 边框和终端横向显示错位;其它 tab 也存在同类风险。
旧实现依赖第三方 tabulate 生成 psql 文本,但宽字符显示宽度、超宽表 viewport 和 curses 安全写入都在项目内控制。第三方默认输出不属于本项目的显示宽度契约,导致渲染链出现两套宽度认知:表格生成按一套规则,终端裁剪按另一套规则。
- 移除
tabulate运行依赖。 render_rows_table()改为项目内 psql 风格渲染器。- 表格列宽、padding、分隔线全部使用
_display_width()计算。 - plain 与 curses 继续复用同一套
render_rows_table(),所有 tab 一次性生效。 - 文档与 AGENTS 同步禁止回退到第三方表格库默认宽度逻辑。
覆盖项:
- psql 输出每一行显示宽度一致。
- CJK 字段列可正确对齐。
anomaly_panel/event_stream/market_snapshot/market_statsplain smoke 均走同一渲染器。- 伪终端启动
tradecat无 Traceback / curses error。
用户期望 tradecat 打开后默认就是最新实时滚动版本,而不是停留在本地旧缓存;同时仍要能用上下键切换历史快照。
旧 TUI 只读取 SQLite 当前已有数据,不负责启动探针,也没有 live/history 状态。它能浏览本地数据,但不能保证进入时先更新到最新,也不能区分“实时最新视图”和“用户正在查看历史快照”。
run_tui()默认 live 模式:启动前执行一次 repair/probe,先把最新在线表格写入本地 SQLite。- curses 主循环按
TRADECAT_TERMINAL_TUI_PROBE_INTERVAL持续探测当前 tab。 - live 模式下强制显示最新批次
batch_index=0。 ↑ / ↓切历史快照后进入 history 模式,避免实时刷新打断历史查看。r重新探测当前 tab,并回到最新 live 模式。- CLI 增加
--no-live和--probe-interval,用于离线调试或调整探针间隔。
覆盖项:
- CLI 无参数默认进入 live TUI。
- CLI 可用
--no-live禁用实时探针。 - live 模式到达探针间隔后写入并回到最新批次。
- history 模式不会触发探针打断用户。
- 伪终端启动
tradecat无 Traceback / curses error。 tradecat-terminal tui event_stream --plain --limit 3 --probe-interval 1可实时 probe 到最新事件流。
执行 tradecat 后长时间停在:
tradecat: 正在探测最新在线表格数据 (all active datasets) ...
进程在进入 curses 面板前占用 CPU;用户看到的现象是“没反应、一直卡”。
run_tui() 在无参数 live 启动时把 dataset_key=None 传给 _probe_latest();旧实现把 None 解释为 probe_all_datasets()。这会在打开 TUI 前同步所有 active dataset,其中包含超宽的 market_snapshot,启动路径被全量远端拉取、CSV 解析、SQLite 写入和历史结构处理阻塞。
第二层问题是启动前使用 repair_local_store(),它会执行结构化回填;本地库已有大量宽表历史行时,这一步也会拖慢启动。
- 新增
ensure_local_store():只初始化 SQLite 和 dataset registry,不做全库结构化回填。 run_tui()默认 live 启动只探测一个启动 dataset,默认event_stream。- 新增
TRADECAT_TERMINAL_TUI_DEFAULT_DATASET,允许覆盖默认启动 dataset。 _probe_latest(dataset_key=None)不再触发全量探测,返回错误 payload,防止后续误把 TUI 启动改回全量同步。
覆盖项:
- 单元测试确认无参数 live 启动只 probe
event_stream。 - 单元测试确认 plain 模式不会启动前 probe。
- 伪终端烟测确认启动日志为
tradecat: 正在探测最新在线表格数据 (event_stream) ...。 - 伪终端烟测确认
tradecat可进入 live TUI,无 Traceback / curses error。
交互式 tradecat 进入 TUI 后,界面停在某个 tap,输入响应变慢甚至像卡死。现场 ps 显示 tradecat 仍在运行并占用 CPU;lsof ~/.tradecat-terminal/tradecat.db 显示同一个进程打开了大量 SQLite 文件句柄,FD 从 3、6、8 一直增长到上百个。
db.connect() 返回裸 sqlite3.Connection。Python 的 sqlite3.Connection 虽然支持 with conn:,但这个上下文只负责提交或回滚事务,并不会在退出时关闭连接。TUI 每轮绘制都会读取 datasets、batches、top info、rows;这些 with connect(...) 实际都没有 close,最终导致 SQLite FD 泄漏。
第二层问题是 curses 主循环每 1 秒都会完整重画 psql 宽表。宽表列数多时,即使没有输入、没有探针变化,也会持续做整表渲染,放大 CPU 占用和交互迟滞。
db.connect()改为真正的@contextmanager,退出时显式 commit/rollback/close。- 增加回归测试,确认
with connect(...)退出后连接已关闭。 - TUI 主循环改为 dirty redraw:只有首次绘制、输入改变状态、鼠标滚动、手动刷新或 live 探针触发后才重画。
- 无输入且未到探针周期时不再每秒整表重绘。
覆盖项:
- 单元测试确认 SQLite 连接上下文退出后不可再执行 SQL。
ruff check apps/tradecat/src apps/tradecat/tests通过。pytest -q apps/tradecat/tests通过,31 passed。bash scripts/verify.sh通过。- 伪终端 live 烟测期间
db_fd_count=0,CPU 约 1%,无 Traceback / curses error。
终端 TUI 已能展示在线表格镜像,但交易对只是普通文本。用户在 TUI 中看到 交易对 / 合约代码 后,无法像在线表格超链接一样直接跳到交易页面。
TUI 展示层之前只负责表格渲染,没有“当前选中行”和“交易对列推断”两个状态。在线表格镜像按 A/B/C 物理列保存,交易对不一定总在固定列,必须从最近的表头行推断。
- 增加
n / p可见行选择状态。 - 增加
Enter / o打开当前选中行交易对链接。 - 增加交易对单元格 hover 下划线链接态。
- 增加鼠标左键点击交易对单元格后选择并打开链接,点击非交易对单元格不跳转。
- 从当前行向上扫描最近的
交易对/合约代码/symbol表头列,按 USDT 永续口径标准化符号。 - WSL 使用
cmd.exe /c start打开浏览器,Linux 使用xdg-open,macOS 使用open。
覆盖项:
BTC/BTCUSDT/BTC/USDT/BTC-USDT标准化为BTCUSDT。- A/B/C 镜像表通过最近表头行推断交易对列。
- 键盘选中行打开 Binance futures URL。
- 鼠标 hover 只有落在交易对单元格内才进入链接态。
- 鼠标点击交易对单元格打开 Binance futures URL,点击同一行其它单元格不跳转。
交易对单元格下划线不是鼠标移动过去就出现,而是需要先点击或多次点击后才出现;点击交易对单元格也不是每次立即打开浏览器。
curses.mousemask() 只告诉 curses “愿意接收鼠标事件”,但很多终端不会默认把鼠标移动事件上报给程序。Windows Terminal / xterm 需要显式开启 xterm mouse tracking,否则程序只能在点击时收到事件,下划线自然只能在点击后才刷新。
第二层问题是左键点击只识别 BUTTON1_CLICKED。不同终端实际可能发送 BUTTON1_PRESSED 或 BUTTON1_RELEASED,导致点击动作被漏判。
- TUI 进入 curses 后显式开启
1006SGR mouse mode 和1003all-motion tracking。 - TUI 退出时关闭上述 mouse tracking,避免污染终端状态。
- 鼠标移动到交易对单元格时立即更新
hover_row_offset并触发重绘。 - 左键点击兼容
BUTTON1_CLICKED、BUTTON1_PRESSED、BUTTON1_RELEASED。
覆盖项:
- 鼠标移动到交易对单元格,即使没有点击,也会返回 dirty 并更新 hover 行。
- 左键 pressed 事件可直接打开交易对链接。
- 滚轮仍只改变纵向滚动。
执行 tradecat 后,TUI 打印启动探针信息后立即退回 shell,shell 中残留 35;52;58M 这类字符。
这是 SGR mouse tracking 的原始鼠标序列后半段。终端发送的完整形态类似 ESC [ < 35 ; 52 ; 58 M。旧代码把 ESC 仍然当成退出键处理,程序看到 27 后直接退出,后续 35;52;58M 没被 curses 消费,泄漏到了 shell。
第二层问题是部分 curses/terminfo 组合不会把 SGR mouse 序列自动转换成 KEY_MOUSE,因此不能只依赖 curses.getmouse()。
- TUI 不再把
ESC作为退出键;退出只保留q / Q。 - 当读到
ESC时,尝试解析后续ESC [ < button ; x ; y M/m原始 SGR 鼠标序列。 - SGR move / click / wheel 事件复用同一套 hover、打开链接和滚动逻辑。
覆盖项:
"[<35;52;58M"可被解析为(35, 51, 57, "M"),不会泄漏半截序列。- SGR hover 事件不需要点击即可更新链接 hover。
- SGR 左键 press 事件可直接打开交易对链接。
- 伪终端烟测可保持 TUI 运行到 timeout,并在退出时关闭 mouse tracking。
tradecat 进入 TUI 后报错或长时间无响应,堆栈停在 urllib.request.urlopen() 读取 Google Sheets CSV。用户手动 Ctrl-C 后看到 KeyboardInterrupt,说明卡死点在 TUI 主循环里的同步网络请求。
TUI 主循环 _maybe_probe_live() 直接执行 probe_dataset(),最终调用 fetch_csv_body()。该函数默认 timeout=30.0,在 Google Sheets 网络慢、代理不稳或 SSL 读阻塞时,整个 curses 交互主线程被 urlopen() 占住,界面无法响应。
- 增加 TUI 专用
TRADECAT_TERMINAL_TUI_FETCH_TIMEOUT,默认 2 秒。 probe_dataset()/collect_validate_hash_with_mirror()支持传入fetch_timeout。- TUI startup probe、live probe、手动
r刷新都使用短超时。 KeyboardInterrupt在 TUI 探针层转成错误 payload,避免 traceback 打穿 curses。
覆盖项:
- TUI live probe 会把 fetch timeout 传到
probe_dataset()。 - TUI 默认 fetch timeout 是 2 秒,最小钳制为 0.5 秒。
- 原有 sync/probe/watch 测试仍保持默认 30 秒服务级行为。