Cloudflare Durable Object SQLite(全局单例) · ARCHIVE_COLLECTOR → ArchiveCollector(apps/strategy/wrangler.jsonc) · contexts/strategy

ArchiveCollector DO

行情归档的「调度台 + 记账本」:全局单例的采集 DO 记着在采哪些币、每条序列采到哪、下一步跑什么任务、HL 权重这一分钟还剩多少,以及盘中检查要盯的价位和临时拉到的 1m。

不存行情本身:K 线 / 资金费 / 市场参数 / 覆盖范围索引都在 R2 桶 MARKET_ARCHIVE(只追加,更正走数据修订号);行情权威是 Hyperliquid。部署、模拟账户、成交都在各自的 StrategyOwner DO(strategy-owner-do),这里只存所有者报上来的副本。

11 张表 57 个字段 5 个聚合 4 个迁移 语义层已对齐 · sha256:e78deed551d346b9

板块颜色贯穿全文

币表与序列

「在采哪些币、每条序列采到哪了」

coins
币表
series
序列游标

任务与限流

「下一步跑什么、HL 权重还够不够」

jobs
任务队列
reconciliations
已对账日
weight_spends
HL 权重流水

所有者报告

「哪些币有部署在跑、盘中要盯哪些价位」

deployed_coins
有部署的币
intrabar_watches
盘中监视

1m 缓冲

「盘中检查拉到了哪些 1m、收录到哪」

minute_candles
1m 缓冲
minute_coverage
1m 收录范围

运维

「出过什么告警、库升到第几版」

events
告警 / 运营事件
schema_migrations
迁移版本

全库约定

业务流程跟着流程看表怎么变

⏱️ 一轮 15m 采集

  1. Cron(*/15 * * * *,只有 production)调 tick → jobsForTick 读 coins(在采)、deployed_coins(先抓)、reconciliations(已对的跳过),enqueue 进 jobs
    同键已在队里只会提前、提优先级
  2. alarm 到 → drainCollection 循环:nextDueJob 取任务,读 weight_spends 过去 60 秒的账,checkShare 不放行就把 alarm 拨到能放行的时刻
  3. 放行 → 按 series 游标打一次 HL candleSnapshot,recordSpend 记一笔 weight_spends(来源 collection)
    权重按实际返回条目算:20 + 每 60 根 +1
  4. 新 K 线追加进 R2 → saveSeries 推进游标;15m 之后顺带把 1h / 4h / 1d 能聚合的桶聚合并推进各自游标
  5. completeJob 删任务;本轮有游标变动就重写 R2 覆盖范围索引(可回测区间)

🆕 HL 新上一个币

  1. 整点的 universe 任务打 meta(20 权重)→ diffMarkets 对比 coins,发现新币
  2. 先往 R2 写市场参数(修订 1);参数变了的老币生成新修订,记一条 events(market_revised)
  3. 给新币 enqueue 一次性回补:15m / 1h / 4h / 1d 原生 K 线 + 资金费全量(优先级 4)
  4. 最后 syncMarkets 插入 coins(active = 1)
    顺序保证中途失败重跑时还能认出它是新币

🎯 模拟部署的盘中止损检查

  1. 所有者 DO 推进部署后调 replaceDeployments:整组替换 deployed_coins 与 intrabar_watches,有监视就把 alarm 拨到下一分钟收盘后 2 秒
  2. alarm → runIntrabarCheck:币少按 1m 拉(每币 20 权重),币多改 allMids 每 6 秒采样(2 权重);结果写 minute_candles + minute_coverage,权重记进 weight_spends(来源 intrabar)
  3. 按币比对 intrabar_watches:1m 低点 ≤ below 或高点 ≥ above 才按所有者分组唤醒,带上那根开盘以来的 1m
    开盘以来收录不完整的不比,留给收盘兜底
  4. 所有者用引擎算成交,回新的区间 → resolveWatch 改行(回 null 则两侧清空、留行防重复唤醒)
  5. 过了整点:production 把 archive = 1 的原生 1m 追加进 R2 并 markArchived;pruneMinutes 裁掉不再需要的 1m

🧾 每日对账发现不一致

  1. 整点排期回看最近 2 个 UTC 日,reconciliations 里没有的 币 × 周期 × 天 排 reconcile 任务(日终后 30 分钟起,优先级 5)
  2. 归档还没收录到那天 → 每 15 分钟再看一次(attempts +1),满 8 次记 reconcile_skipped 放弃
  3. 收录到了 → 拉那天的 HL 原生 K 线逐根比对,不一致记 events(reconcile_mismatch,最多列 20 条)
    不自动改归档;更正由人工确认后生成新数据修订号
  4. 不论一致与否都 markReconciled 写 reconciliations,之后不再排

设计要点

🗄️ 状态在 SQLite,数据在 R2本库只放调度与进度(游标、队列、权重账)和盘中检查的短期缓冲;行情本身只追加进 R2 MARKET_ARCHIVE。游标总在 R2 写成功之后才推进,重跑最多重抓一次,不会漏。
⚖️ 一本权重账,两份限额15m 采集与盘中检查记进同一张 weight_spends,source 区分。放行要同时过「本来源那一份」和「合计」两道(checkShare);有在盯的部署时 15m 采集让出盘中那 300。依据 [HL rate limits](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/rate-limits-and-user-limits)。
🔑 任务按序列去重jobs.key 由 jobKey 生成,一条序列同时只有一个任务;任务按游标决定从哪抓,所以 Cron 漏几轮也只是下一个任务多抓几根。
📨 所有者报告整组替换deployed_coins 与 intrabar_watches 都按 owner 先删后插,不做增量对账——报告是幂等的,丢一次下次补上。两张表各自一个事务,不在同一事务里。
🧮 价格存字符串1m OHLCV 与触发价位都是十进制字符串,比对走 safeBN(金融算术全程 BigNumber);SQL 里从不对价格做比较或排序。