ohlcvault 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (28) hide show
  1. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/PKG-INFO +18 -6
  2. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/README.md +17 -5
  3. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/client.py +13 -1
  4. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/config.py +9 -6
  5. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/pyproject.toml +1 -1
  6. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/conftest.py +34 -3
  7. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/.gitignore +0 -0
  8. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/LICENSE +0 -0
  9. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/SPEC.md +0 -0
  10. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/__init__.py +0 -0
  11. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/adjust.py +0 -0
  12. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/codes.py +0 -0
  13. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/errors.py +0 -0
  14. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/net.py +0 -0
  15. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/sql.py +0 -0
  16. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/ohlcvault/store.py +0 -0
  17. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-calendar.json +0 -0
  18. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-idx-monthly.json +0 -0
  19. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-latest.json +0 -0
  20. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-manifest.json +0 -0
  21. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-monthly.json +0 -0
  22. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-snapshot.json +0 -0
  23. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/schema/example-symbols.json +0 -0
  24. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/test_adjust.py +0 -0
  25. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/test_client.py +0 -0
  26. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/test_perf.py +0 -0
  27. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/test_store.py +0 -0
  28. {ohlcvault-0.2.0 → ohlcvault-0.2.2}/tests/test_symbols.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ohlcvault
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Reproducible, checksummed daily OHLCV data for A-share, HK and US markets — static files, content-addressed snapshots, zero API keys.
5
5
  Project-URL: Homepage, https://helpquant.com
6
6
  Project-URL: Documentation, https://github.com/helpquant/ohlcvault#readme
@@ -132,6 +132,17 @@ A mirror can be an HTTP(S) URL **or a local directory**:
132
132
  ov.connect(mirrors=["/path/to/data"])
133
133
  ```
134
134
 
135
+ The built-in chain has two entries whose **failure domains are disjoint** — a
136
+ commercial CDN and a self-hosted origin:
137
+
138
+ | Mirror | Role | Independent failure domain |
139
+ |---|---|---|
140
+ | `https://data.ohlcvault.com` | primary | commercial CDN: account suspension, billing, regional outage |
141
+ | `https://data.helpquant.com` | fallback | own server + own DNS |
142
+
143
+ Override with `OHLCVAULT_MIRRORS=https://a,https://b`, or pass `mirrors=` / set
144
+ `OHLCVAULT_CACHE` to keep everything local.
145
+
135
146
  Mirrors are tried in order; whichever one succeeds is promoted to first place. Every
136
147
  file is checked against the `sha256` in the snapshot manifest, and anything that
137
148
  fails is **discarded and the next mirror is tried** — bad bytes are never handed to
@@ -144,7 +155,7 @@ the caller.
144
155
  | No silently-corrupted data | Every file verified against the snapshot's `sha256` |
145
156
  | No silently-changed history | Sealed months are never rewritten |
146
157
  | No unverifiable numbers | Missing adjustment factors raise, instead of returning raw prices |
147
- | No hidden survivorship bias | Delisted stocks are kept in the universe and in the data |
158
+ | No hidden survivorship bias | Delisted stocks are kept in the universe and in the data (`cn`; see coverage table for `hk`/`us` limits) |
148
159
  | Byte-for-byte reproduction | Fixed-point integers, `gzip` with `MTIME=0`, no wall-clock timestamps |
149
160
 
150
161
  **Delisted stocks matter.** If your backtest universe only contains companies that
@@ -159,12 +170,13 @@ Coverage is declared explicitly in `meta/symbols/{market}.json` under `coverage`
159
170
 
160
171
  | Market | Status | Gaps |
161
172
  |---|---|---|
162
- | `cn` | Daily bars + indices, 2000→present | **No Beijing Stock Exchange** (upstream source doesn't provide it) |
163
- | `hk` | Symbol list only | No daily bars yet; **list is current listings only** |
164
- | `us` | Symbol list only | No daily bars yet; **list is current listings only** |
173
+ | `cn` | Daily bars 1990-12→present (A-shares incl. 1,187 delisted) + 461 indices | **No Beijing Stock Exchange** (upstream source doesn't provide it); trade calendar starts 2000-01 (upstream boundary — earlier trading days must be inferred from bar dates); ETF/bond lists included, bars not collected |
174
+ | `hk` | Daily bars 2,806 stocks + HSI index (index history from 2013-08) | **List is current listings only** (survivorship bias, no IPO dates); calendar & index history start 2013-08 (free index source boundary) |
175
+ | `us` | Daily bars 7,513 stocks + SPX index (index history from 2004-01) | **List is current listings only** (survivorship bias, no IPO dates); 5,733 ETFs listed but bars not collected; calendar & index history start 2004-01 (free index source boundary) |
165
176
 
166
177
  `hk` / `us` symbol lists carry **survivorship bias** (the upstream source only returns
167
- currently-listed securities) and have no IPO dates. Their daily bars are pending.
178
+ currently-listed securities) and have no IPO dates. `cn` is unaffected: 1,187 delisted
179
+ stocks are in the list and their full history (e.g. `600001.SH` from 1998) is in the data.
168
180
 
169
181
  An honest data project states its gaps. A dataset that quietly omits them is worse
170
182
  than one that is merely incomplete.
@@ -100,6 +100,17 @@ A mirror can be an HTTP(S) URL **or a local directory**:
100
100
  ov.connect(mirrors=["/path/to/data"])
101
101
  ```
102
102
 
103
+ The built-in chain has two entries whose **failure domains are disjoint** — a
104
+ commercial CDN and a self-hosted origin:
105
+
106
+ | Mirror | Role | Independent failure domain |
107
+ |---|---|---|
108
+ | `https://data.ohlcvault.com` | primary | commercial CDN: account suspension, billing, regional outage |
109
+ | `https://data.helpquant.com` | fallback | own server + own DNS |
110
+
111
+ Override with `OHLCVAULT_MIRRORS=https://a,https://b`, or pass `mirrors=` / set
112
+ `OHLCVAULT_CACHE` to keep everything local.
113
+
103
114
  Mirrors are tried in order; whichever one succeeds is promoted to first place. Every
104
115
  file is checked against the `sha256` in the snapshot manifest, and anything that
105
116
  fails is **discarded and the next mirror is tried** — bad bytes are never handed to
@@ -112,7 +123,7 @@ the caller.
112
123
  | No silently-corrupted data | Every file verified against the snapshot's `sha256` |
113
124
  | No silently-changed history | Sealed months are never rewritten |
114
125
  | No unverifiable numbers | Missing adjustment factors raise, instead of returning raw prices |
115
- | No hidden survivorship bias | Delisted stocks are kept in the universe and in the data |
126
+ | No hidden survivorship bias | Delisted stocks are kept in the universe and in the data (`cn`; see coverage table for `hk`/`us` limits) |
116
127
  | Byte-for-byte reproduction | Fixed-point integers, `gzip` with `MTIME=0`, no wall-clock timestamps |
117
128
 
118
129
  **Delisted stocks matter.** If your backtest universe only contains companies that
@@ -127,12 +138,13 @@ Coverage is declared explicitly in `meta/symbols/{market}.json` under `coverage`
127
138
 
128
139
  | Market | Status | Gaps |
129
140
  |---|---|---|
130
- | `cn` | Daily bars + indices, 2000→present | **No Beijing Stock Exchange** (upstream source doesn't provide it) |
131
- | `hk` | Symbol list only | No daily bars yet; **list is current listings only** |
132
- | `us` | Symbol list only | No daily bars yet; **list is current listings only** |
141
+ | `cn` | Daily bars 1990-12→present (A-shares incl. 1,187 delisted) + 461 indices | **No Beijing Stock Exchange** (upstream source doesn't provide it); trade calendar starts 2000-01 (upstream boundary — earlier trading days must be inferred from bar dates); ETF/bond lists included, bars not collected |
142
+ | `hk` | Daily bars 2,806 stocks + HSI index (index history from 2013-08) | **List is current listings only** (survivorship bias, no IPO dates); calendar & index history start 2013-08 (free index source boundary) |
143
+ | `us` | Daily bars 7,513 stocks + SPX index (index history from 2004-01) | **List is current listings only** (survivorship bias, no IPO dates); 5,733 ETFs listed but bars not collected; calendar & index history start 2004-01 (free index source boundary) |
133
144
 
134
145
  `hk` / `us` symbol lists carry **survivorship bias** (the upstream source only returns
135
- currently-listed securities) and have no IPO dates. Their daily bars are pending.
146
+ currently-listed securities) and have no IPO dates. `cn` is unaffected: 1,187 delisted
147
+ stocks are in the list and their full history (e.g. `600001.SH` from 1998) is in the data.
136
148
 
137
149
  An honest data project states its gaps. A dataset that quietly omits them is worse
138
150
  than one that is merely incomplete.
@@ -71,7 +71,19 @@ class MarketClient:
71
71
 
72
72
  @staticmethod
73
73
  def _label(m: str) -> str:
74
- return m.rsplit("/", 2)[-2] if m.startswith("http") else Path(m).name
74
+ """镜像的短标签,用于 trace 与 `stats["served_by"]`。
75
+
76
+ 必须对**裸主机 URL** 也给出可区分的名字:默认镜像链全是
77
+ `https://<host>` 这种无路径形态,而按「最后一个路径段」取名会得到
78
+ 空串 —— 于是两个不同的镜像在统计里混成同一个 key,
79
+ 「到底命中了主源还是备用源」这个问题就再也答不出来了(实测踩到)。
80
+ """
81
+ s = str(m).rstrip("/")
82
+ if "://" in s:
83
+ rest = s.split("://", 1)[1]
84
+ host, _, path = rest.partition("/")
85
+ return f"{host}/{path}" if path else host
86
+ return Path(s).name
75
87
 
76
88
  def _fetch_raw(self, m: str, rel: str) -> bytes:
77
89
  if m.startswith("http"):
@@ -5,9 +5,13 @@
5
5
 
6
6
  | 镜像 | 角色 | 故障域 |
7
7
  |---|---|---|
8
- | Cloudflare R2 + CDN | 主源 | 商业 CDN:账号封禁、账单异常、区域故障 |
9
- | GitHub raw / jsDelivr | 备用 1 | 微软系托管:与 Cloudflare 完全独立 |
10
- | 自有服务器 HTTPS | 备用 2 | 自有机器:唯一完全可控的兜底 |
8
+ | `data.ohlcvault.com`(Cloudflare R2 + CDN) | 主源 | 商业 CDN:账号封禁、账单异常、区域故障 |
9
+ | `data.helpquant.com`(自有服务器 HTTPS) | 备用 1 | 自有机器 + 自有 DNS(DNSPod):唯一完全可控的兜底 |
10
+
11
+ 历史上这里还有第三个源(`cdn.jsdelivr.net/gh/…` 与 `raw.githubusercontent.com/…`),
12
+ 指向数据镜像仓库 `helpquant/ohlcvault-data`。该仓库经 GitHub API 查证 **404,从未
13
+ 创建**,两个 URL 都是死链,只会让客户端在冷启动时白付一次超时 —— 已移除。
14
+ 日后若真建了 GitHub 镜像仓库,再把它们加回来即可(届时故障域才是真正独立的)。
11
15
 
12
16
  客户端会**记住哪个镜像成功过**并把它提到首位,所以镜像顺序不是性能关键,
13
17
  减少的是「每次请求都先付一次失败超时」。
@@ -33,14 +37,13 @@ NAMESPACES = ("stock", "index")
33
37
 
34
38
  # HTTP User-Agent。必须带自定义 UA:Cloudflare 等边缘节点默认拦截
35
39
  # Python-urllib/* 的请求(403),裸 urllib 会被当成 bot。
36
- VERSION = "0.2.0"
40
+ VERSION = "0.2.2"
37
41
  USER_AGENT = f"ohlcvault/{VERSION} (+https://github.com/helpquant/ohlcvault)"
38
42
 
39
43
  # 默认镜像链。可用 OHLCVAULT_MIRRORS 覆盖。
40
44
  DEFAULT_MIRRORS: list[str] = [
41
45
  "https://data.ohlcvault.com",
42
- "https://cdn.jsdelivr.net/gh/helpquant/ohlcvault-data@main",
43
- "https://raw.githubusercontent.com/helpquant/ohlcvault-data/main",
46
+ "https://data.helpquant.com",
44
47
  ]
45
48
 
46
49
  DEFAULT_CACHE = Path(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ohlcvault"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Reproducible, checksummed daily OHLCV data for A-share, HK and US markets — static files, content-addressed snapshots, zero API keys."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -52,9 +52,39 @@ def store(data_dir, tmp_path):
52
52
  return ov.connect(mirrors=[str(data_dir)], cache_dir=tmp_path / "cache", quiet=True)
53
53
 
54
54
 
55
+ def _copy_tree_cow(src: Path, dst: Path) -> None:
56
+ """复制目录树,尽可能用「写时复制」(APFS clonefile / reflink)。
57
+
58
+ mirrors 夹具要为**每个**测试准备多份数据副本;真实数据集在 GB 量级,
59
+ 逐字节复制三遍就是数个 GB —— 会直接吃光开发机磁盘(实测测试进程被
60
+ SIGKILL 打断,就是因为这里)。COW 副本几乎不额外占空间,而且**写入时
61
+ 自动分离**,所以「篡改某个副本」的测试语义完全成立(硬链接做不到:
62
+ 原地改写会串到其他副本)。平台不支持时回退到普通复制。
63
+ """
64
+ import subprocess
65
+
66
+ dst.parent.mkdir(parents=True, exist_ok=True)
67
+ if sys.platform == "darwin":
68
+ cmd = ["cp", "-Rc"]
69
+ elif sys.platform.startswith("linux"):
70
+ cmd = ["cp", "-a", "--reflink=auto"]
71
+ else:
72
+ cmd = []
73
+ if cmd:
74
+ r = subprocess.run(cmd + [str(src), str(dst)],
75
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
76
+ if r.returncode == 0:
77
+ return
78
+ shutil.copytree(src, dst, dirs_exist_ok=True)
79
+
80
+
55
81
  @pytest.fixture()
56
82
  def mirrors(data_dir, tmp_path):
57
- """三份独立副本,模拟「R2 / jsDelivr / GitHub raw」的镜像链。
83
+ """三份独立副本,模拟「主源 + 两个备用源」的镜像链。
84
+
85
+ 这里的三份是**占位符**(名字沿用历史,不代表当前真实镜像链 —— 真实链见
86
+ `ohlcvault.config.DEFAULT_MIRRORS`,2026-10-09 起只有两个源)。保留三份是为了
87
+ 让「N 路降级」这条逻辑本身被测到,而不是绑死在当前有几个源上。
58
88
 
59
89
  用真实字节而不是合成数据 —— 校验、降级、篡改拒绝这些路径只有在
60
90
  真实的 gzip 分片上跑才有意义。
@@ -62,8 +92,9 @@ def mirrors(data_dir, tmp_path):
62
92
  base = tmp_path / "remote"
63
93
  names = ["cloudflare_r2", "jsdelivr", "github_raw"]
64
94
  for n in names:
65
- shutil.copytree(data_dir, base / n,
66
- ignore=shutil.ignore_patterns(".cache", ".months"))
95
+ _copy_tree_cow(data_dir, base / n)
96
+ for skip in (".cache", ".months"):
97
+ shutil.rmtree(base / n / skip, ignore_errors=True)
67
98
  return [str(base / n) for n in names], base
68
99
 
69
100
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes