修掉 gzip 追加会毁掉整个文件的数据丢失,并分开买卖两侧的成交分布曲线
两件事,都是「静默出错」那一类。 一、BookLog/TapeLog 追加到同一个 .gz,进程被 SIGKILL 时当前成员停在 deflate 块中间,下一轮追加的新成员接在垃圾字节之后。顺序解压在损坏点抛 invalid block type,该点之后全部读不出来——包括后续每轮写进去的。而读侧的异常处理 把这个当成「正常的尾部截断」静默跳过,于是只读出 21 行还不报错。 原 docstring 里写的「只丢最后一个缓冲块,不会毁掉整个文件」是错的,已证伪。 写侧改成每轮运行一个文件;读侧按 gzip 成员边界扫描、坏成员单独跳过并出声 报告,同时把同前缀的多轮文件一并读入。旧损坏文件因此多恢复出 31/21 条 (tape)与 132/95 条(books)。 二、tape_shape 只统计主动买、只自区间顶部累积,这条曲线只适用于多头止盈。 exit_fill 两侧共用它,等于把空头的可成交量按多头分布高估。实测二者不对称: 主动买在顶部 20% 内已占 40%,主动卖在底部 20% 内只有 18%。分成 SHAPE_F 与 SHAPE_F_SHORT,avail_at 按方向查各自曲线。 两条曲线只有 45 根成交流样本,所以补了 --sensitivity:把空头可成交量砍一半, 10 万仓位下 BTC/ETH 预算完全不动、SOL 动 0.07bp。结论不依赖这 45 根样本。 顺带撤掉 step43 docstring 里已作废的「成交率 30%/16%/1.5%」。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,8 +28,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -41,27 +41,69 @@ def out_dir() -> Path:
|
||||
return p if p.is_dir() else Path(__file__).resolve().parents[1] / "out"
|
||||
|
||||
|
||||
def read_jsonl_gz(path: Path):
|
||||
"""逐行读 gzip JSONL,末尾截断则静默停止。
|
||||
GZ_MAGIC = b"\x1f\x8b\x08"
|
||||
|
||||
采集仍在进行时,最后一个 gzip 成员缺结尾标记;不接这个异常的话,
|
||||
整个分析会因为文件尾而失败,前面几万条完好记录一起丢掉。
|
||||
|
||||
def _members(blob: bytes):
|
||||
"""把可能损坏的多成员 gzip 拆成「逐个成员解压」,坏成员跳过。
|
||||
|
||||
采集进程被 SIGKILL 时,当前 gzip 成员停在 deflate 块中间、没有结尾标记。
|
||||
下一次运行以追加方式写入的新成员就接在这段垃圾字节后面。此时用
|
||||
`gzip.open` 顺序读会在损坏点抛 `invalid block type`,**该点之后的所有
|
||||
数据都读不出来**——包括后续每一轮运行写进去的。曾因此只读出 21 行而
|
||||
误以为样本就那么少,且不报错。
|
||||
|
||||
所以按成员边界扫描:某个成员解压失败,只丢它,然后前进到下一个 magic
|
||||
继续。返回 (解压出的字节, 跳过的成员数)。
|
||||
"""
|
||||
if not path.exists():
|
||||
out, skipped, i, n = [], 0, blob.find(GZ_MAGIC), len(blob)
|
||||
while 0 <= i < n:
|
||||
d = zlib.decompressobj(16 + zlib.MAX_WBITS)
|
||||
try:
|
||||
chunk = d.decompress(blob[i:])
|
||||
except zlib.error:
|
||||
chunk = b""
|
||||
if chunk:
|
||||
out.append(chunk)
|
||||
# 成员完整时 unused_data 指向下一成员;否则只能往前找 magic
|
||||
if d.eof and d.unused_data:
|
||||
nxt = n - len(d.unused_data)
|
||||
else:
|
||||
nxt = blob.find(GZ_MAGIC, i + 3)
|
||||
if chunk == b"":
|
||||
skipped += 1
|
||||
i = nxt if nxt > i else -1
|
||||
return b"".join(out), skipped
|
||||
|
||||
|
||||
def read_jsonl_gz(path: Path, quiet: bool = False):
|
||||
"""读 gzip JSONL,跨运行文件汇总,坏成员跳过而非静默截断。
|
||||
|
||||
`path` 既可以是单个文件,也当作前缀用:同目录下 `<stem>.*.jsonl.gz`
|
||||
(每轮运行一个)会一并读入,这样重启不再把历史数据连坐。
|
||||
"""
|
||||
base = path.name.replace(".jsonl.gz", "")
|
||||
files = sorted({*path.parent.glob(f"{base}.*.jsonl.gz"),
|
||||
*([path] if path.exists() else [])})
|
||||
if not files:
|
||||
return
|
||||
n_ok = 0
|
||||
try:
|
||||
with gzip.open(path, "rt", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
break # 半行,说明写到这里被打断
|
||||
n_ok += 1
|
||||
yield rec
|
||||
except (EOFError, OSError, gzip.BadGzipFile):
|
||||
# 采集进程正在写,尾部不完整属正常
|
||||
pass
|
||||
for f in files:
|
||||
blob = f.read_bytes()
|
||||
data, skipped = _members(blob)
|
||||
n_ok = n_bad = 0
|
||||
for line in data.split(b"\n"):
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
n_bad += 1 # 损坏边界上的半行
|
||||
continue
|
||||
n_ok += 1
|
||||
yield rec
|
||||
if not quiet and (skipped or n_bad):
|
||||
print(f" ⚠ {f.name}: 读出 {n_ok:,} 条,"
|
||||
f"跳过 {skipped} 个损坏成员 / {n_bad} 个半行")
|
||||
|
||||
|
||||
def impact_bp(levels: list, notional: float, mid: float) -> float | None:
|
||||
@@ -239,8 +281,17 @@ def maker_fill(tape_path: Path, mults=(3.0, 8.0),
|
||||
def tape_shape(tape_path: Path, kgrid: np.ndarray) -> dict[str, np.ndarray]:
|
||||
"""成交流给「形状」:一根的主动买成交额里,有多少比例落在区间顶部 k 之内。
|
||||
|
||||
形状与规模分开是为了绕开成交流样本小的限制——形状是微观结构性质,
|
||||
几十根就相当稳定;规模(每根成交多少钱)则由 210 天历史成交量提供。
|
||||
形状与规模分开是为了绕开成交流样本小的限制——规模(每根成交多少钱)由
|
||||
210 天历史成交量提供,成交流只需给出形状。
|
||||
|
||||
⚠ 只算主动**买**、只自顶部累积,所以这条曲线只适用于**多头**止盈(挂卖
|
||||
出,靠主动买打上来)。空头止盈要用主动卖自底部累积的曲线,二者实测并不
|
||||
对称:主动买在区间顶部 20% 内已占 40%,主动卖在底部 20% 内只有 18%,
|
||||
BTC/ETH 平均偏差 0.21。lib/exit_fill 里两条曲线是分开的(SHAPE_F 与
|
||||
SHAPE_F_SHORT);用同一条会把空头的可成交量按多头分布高估。
|
||||
|
||||
「形状几十根就稳定」这个说法要打折:45 根样本足以看出上面那个方向性差异,
|
||||
但不足以定数值。好在预算对形状不敏感(见 step43 --sensitivity)。
|
||||
"""
|
||||
acc: dict[str, list[np.ndarray]] = {}
|
||||
for r in read_jsonl_gz(tape_path):
|
||||
|
||||
Reference in New Issue
Block a user