Repair first-slot OHLC sentinels in `NativeDB`
Filter known zero-initialized epoch slots before persisted history hydrates SHM or merges with a fresh provider frame. Deats, - reindex the repaired read view for same-session chart publication - treat sentinel-only files as empty history for fresh backfill - reject new first-slot sentinels but preserve modern zero-price bars - classify raw sentinel evidence separately in `store audit` Prompt-IO: ai/prompt-io/opencode/20260729T041723Z_ce33deb6_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))backfiller_deep_fixes
parent
ce33deb6b9
commit
d359b1cbfb
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
provider: openai
|
||||||
|
service: opencode
|
||||||
|
session: 24fb9765-a550-4570-8350-f0fc9b7e17db
|
||||||
|
timestamp: 2026-07-29T04:17:23Z
|
||||||
|
git_ref: ce33deb6
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260729T041723Z_ce33deb6_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Diagnose why the chart still showed a zero first OHLCV row after the
|
||||||
|
epoch-zero timestamp repair, causing auto-y-ranging to include zero.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Audited live 60s and 1s NativeDB files and found one exact 60s first-slot
|
||||||
|
sentinel at epoch 60 with a completely zero OHLCV payload. Added narrowly
|
||||||
|
scoped read and merge repair, matching incoming validation, sentinel-only
|
||||||
|
startup handling, canonical reindexing, and explicit audit classification.
|
||||||
|
Preserved modern all-zero bars as unclassified evidence rather than assuming
|
||||||
|
all zero-priced instruments are corrupt.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `piker/storage/nativedb.py` - first-slot sentinel repair and validation
|
||||||
|
- `piker/storage/_audit.py` - sentinel classification and zero-row evidence
|
||||||
|
- `tests/test_storage_nativedb.py` - hydration, merge, and boundary regressions
|
||||||
|
- `tests/test_storage_audit.py` - sentinel and modern-zero audit regressions
|
||||||
|
- `ai/prompt-io/opencode/20260729T041723Z_ce33deb6_prompt_io.raw.md`
|
||||||
|
- unedited response record
|
||||||
|
- `ai/prompt-io/opencode/20260729T041723Z_ce33deb6_prompt_io.md`
|
||||||
|
- provenance metadata and response summary
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
None - generated changes have not been edited by the human.
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
provider: openai
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-07-29T04:17:23Z
|
||||||
|
git_ref: ce33deb6
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/storage/nativedb.py piker/storage/_audit.py tests/test_storage_nativedb.py tests/test_storage_audit.py`
|
||||||
|
|
||||||
|
Fixed the remaining legacy first-slot sentinel observed in live MNQ 60s
|
||||||
|
history: `time=60` with zero open, high, low, close, and volume. NativeDB now
|
||||||
|
repairs the exact sentinel before read hydration and merge, reindexes the
|
||||||
|
filtered view, and persists the repaired frame on the next valid update.
|
||||||
|
|
||||||
|
The repair remains narrowly scoped. Modern all-zero OHLC bars stay valid for
|
||||||
|
spreads or synthetic instruments, null-containing malformed rows still fail
|
||||||
|
validation, new writes can not recreate the first-slot sentinel, and a file
|
||||||
|
containing only sentinels loads as no history so fresh backfill can proceed.
|
||||||
|
|
||||||
|
The raw audit reports total all-zero price rows, classifies first-slot
|
||||||
|
sentinels as structural violations, and leaves later all-zero rows visible as
|
||||||
|
unclassified warnings.
|
||||||
|
|
||||||
|
Verification generated with the patch:
|
||||||
|
|
||||||
|
- NativeDB, audit, history, xonsh, and IB regressions: 61 passed
|
||||||
|
- focused Ruff: passed
|
||||||
|
- `git diff --check`: passed
|
||||||
|
- final adversarial review: no findings
|
||||||
|
|
@ -30,6 +30,12 @@ _value_fields: tuple[str, ...] = (
|
||||||
'close',
|
'close',
|
||||||
'volume',
|
'volume',
|
||||||
)
|
)
|
||||||
|
_price_fields: tuple[str, ...] = (
|
||||||
|
'open',
|
||||||
|
'high',
|
||||||
|
'low',
|
||||||
|
'close',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _utc_str(timestamp: float|int|None) -> str|None:
|
def _utc_str(timestamp: float|int|None) -> str|None:
|
||||||
|
|
@ -494,6 +500,36 @@ def audit_ohlcv_frame(
|
||||||
if counts['nonfinite'] != 0:
|
if counts['nonfinite'] != 0:
|
||||||
all_values_finite = False
|
all_values_finite = False
|
||||||
|
|
||||||
|
all_zero_price_rows: int|None = None
|
||||||
|
epoch_sentinel_rows: int|None = None
|
||||||
|
if all(
|
||||||
|
field in df.columns
|
||||||
|
and
|
||||||
|
df[field].dtype.is_numeric()
|
||||||
|
for field in _price_fields
|
||||||
|
):
|
||||||
|
zero_prices: pl.Expr = pl.all_horizontal([
|
||||||
|
pl.col(field) == 0
|
||||||
|
for field in _price_fields
|
||||||
|
]).fill_null(False)
|
||||||
|
all_zero_price_rows = int(
|
||||||
|
df.select(zero_prices).to_series().sum()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
'time' in df.columns
|
||||||
|
and
|
||||||
|
df['time'].dtype.is_numeric()
|
||||||
|
):
|
||||||
|
epoch_sentinel_rows = int(
|
||||||
|
df.select(
|
||||||
|
(pl.col('time') <= period_s)
|
||||||
|
&
|
||||||
|
zero_prices
|
||||||
|
).to_series().sum()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
schema_ok: bool = bool(
|
schema_ok: bool = bool(
|
||||||
not missing
|
not missing
|
||||||
and
|
and
|
||||||
|
|
@ -516,6 +552,8 @@ def audit_ohlcv_frame(
|
||||||
violations.append('canonical_dtypes')
|
violations.append('canonical_dtypes')
|
||||||
if not all_values_finite:
|
if not all_values_finite:
|
||||||
violations.append('nonfinite_ohlcv')
|
violations.append('nonfinite_ohlcv')
|
||||||
|
if epoch_sentinel_rows:
|
||||||
|
violations.append('epoch_ohlc_sentinel')
|
||||||
if timestamps.get('nonfinite') != 0:
|
if timestamps.get('nonfinite') != 0:
|
||||||
violations.append('nonfinite_timestamps')
|
violations.append('nonfinite_timestamps')
|
||||||
if (
|
if (
|
||||||
|
|
@ -540,6 +578,15 @@ def audit_ohlcv_frame(
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
if gaps['count']:
|
if gaps['count']:
|
||||||
warnings.append('positive_time_gaps_unclassified')
|
warnings.append('positive_time_gaps_unclassified')
|
||||||
|
unclassified_zero_rows: int|None = None
|
||||||
|
if all_zero_price_rows is not None:
|
||||||
|
unclassified_zero_rows = (
|
||||||
|
all_zero_price_rows
|
||||||
|
-
|
||||||
|
(epoch_sentinel_rows or 0)
|
||||||
|
)
|
||||||
|
if unclassified_zero_rows:
|
||||||
|
warnings.append('all_zero_ohlc_prices_unclassified')
|
||||||
|
|
||||||
structural_ok: bool = not violations
|
structural_ok: bool = not violations
|
||||||
gap_free: bool|None = (
|
gap_free: bool|None = (
|
||||||
|
|
@ -573,6 +620,9 @@ def audit_ohlcv_frame(
|
||||||
'nan_by_column': nans,
|
'nan_by_column': nans,
|
||||||
'infinity_by_column': infinities,
|
'infinity_by_column': infinities,
|
||||||
'all_finite': all_values_finite,
|
'all_finite': all_values_finite,
|
||||||
|
'all_zero_price_rows': all_zero_price_rows,
|
||||||
|
'epoch_sentinel_rows': epoch_sentinel_rows,
|
||||||
|
'unclassified_zero_price_rows': unclassified_zero_rows,
|
||||||
},
|
},
|
||||||
'gaps': gaps,
|
'gaps': gaps,
|
||||||
'result': {
|
'result': {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,12 @@ from . import TimeseriesNotFound
|
||||||
|
|
||||||
|
|
||||||
log = get_logger('storage.nativedb')
|
log = get_logger('storage.nativedb')
|
||||||
|
_price_fields: tuple[str, ...] = (
|
||||||
|
'open',
|
||||||
|
'high',
|
||||||
|
'low',
|
||||||
|
'close',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def detect_period(shm: ShmArray) -> float:
|
def detect_period(shm: ShmArray) -> float:
|
||||||
|
|
@ -233,6 +239,8 @@ class NativeStorageClient:
|
||||||
) from fnfe
|
) from fnfe
|
||||||
|
|
||||||
times = array['time']
|
times = array['time']
|
||||||
|
if array.size == 0:
|
||||||
|
return None
|
||||||
return (
|
return (
|
||||||
array,
|
array,
|
||||||
from_timestamp(times[0]),
|
from_timestamp(times[0]),
|
||||||
|
|
@ -319,6 +327,41 @@ class NativeStorageClient:
|
||||||
.cast(schema)
|
.cast(schema)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _repair_stored_ohlcv(
|
||||||
|
self,
|
||||||
|
df: pl.DataFrame,
|
||||||
|
timeframe: int,
|
||||||
|
|
||||||
|
) -> pl.DataFrame:
|
||||||
|
'''
|
||||||
|
Drop known legacy sentinels before hydration or merge.
|
||||||
|
|
||||||
|
'''
|
||||||
|
stored: pl.DataFrame = self._canonicalize_ohlcv(df)
|
||||||
|
zero_prices: pl.Expr = pl.all_horizontal([
|
||||||
|
pl.col(field) == 0
|
||||||
|
for field in _price_fields
|
||||||
|
]).fill_null(False)
|
||||||
|
invalid: pl.Expr = (
|
||||||
|
(pl.col('time') <= 0)
|
||||||
|
|
|
||||||
|
(
|
||||||
|
(pl.col('time') <= timeframe)
|
||||||
|
&
|
||||||
|
zero_prices
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stored_len: int = stored.height
|
||||||
|
stored = stored.filter(~invalid)
|
||||||
|
dropped: int = stored_len - stored.height
|
||||||
|
if dropped:
|
||||||
|
log.warning(
|
||||||
|
f'Dropping {dropped} invalid persisted OHLCV '
|
||||||
|
f'row(s) during read repair'
|
||||||
|
)
|
||||||
|
stored = self._canonicalize_ohlcv(stored)
|
||||||
|
return stored
|
||||||
|
|
||||||
async def read_ohlcv(
|
async def read_ohlcv(
|
||||||
self,
|
self,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
@ -331,7 +374,10 @@ class NativeStorageClient:
|
||||||
fqme,
|
fqme,
|
||||||
period=int(timeframe),
|
period=int(timeframe),
|
||||||
)
|
)
|
||||||
df: pl.DataFrame = pl.read_parquet(path)
|
df: pl.DataFrame = self._repair_stored_ohlcv(
|
||||||
|
pl.read_parquet(path),
|
||||||
|
timeframe=int(timeframe),
|
||||||
|
)
|
||||||
|
|
||||||
self._cache_df(
|
self._cache_df(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
|
|
@ -384,7 +430,7 @@ class NativeStorageClient:
|
||||||
else:
|
else:
|
||||||
df = ohlcv
|
df = ohlcv
|
||||||
df = self._canonicalize_ohlcv(df)
|
df = self._canonicalize_ohlcv(df)
|
||||||
self._validate_ohlcv(df)
|
self._validate_ohlcv(df, timeframe)
|
||||||
|
|
||||||
# TODO: in terms of managing the ultra long term data
|
# TODO: in terms of managing the ultra long term data
|
||||||
# -[ ] use a proper profiler to measure all this IO and
|
# -[ ] use a proper profiler to measure all this IO and
|
||||||
|
|
@ -405,7 +451,7 @@ class NativeStorageClient:
|
||||||
try:
|
try:
|
||||||
df.write_parquet(tmp_path)
|
df.write_parquet(tmp_path)
|
||||||
committed: pl.DataFrame = pl.read_parquet(tmp_path)
|
committed: pl.DataFrame = pl.read_parquet(tmp_path)
|
||||||
self._validate_ohlcv(committed)
|
self._validate_ohlcv(committed, timeframe)
|
||||||
if not committed.equals(df):
|
if not committed.equals(df):
|
||||||
raise IOError(
|
raise IOError(
|
||||||
'Temporary parquet differs from input frame'
|
'Temporary parquet differs from input frame'
|
||||||
|
|
@ -442,6 +488,7 @@ class NativeStorageClient:
|
||||||
def _validate_ohlcv(
|
def _validate_ohlcv(
|
||||||
self,
|
self,
|
||||||
df: pl.DataFrame,
|
df: pl.DataFrame,
|
||||||
|
timeframe: int,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -483,6 +530,17 @@ class NativeStorageClient:
|
||||||
'OHLCV timestamps must be finite and positive'
|
'OHLCV timestamps must be finite and positive'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
prices: np.ndarray = df.select(_price_fields).to_numpy()
|
||||||
|
epoch_sentinel: np.ndarray = (
|
||||||
|
(times <= timeframe)
|
||||||
|
&
|
||||||
|
np.all(prices == 0, axis=1)
|
||||||
|
)
|
||||||
|
if np.any(epoch_sentinel):
|
||||||
|
raise ValueError(
|
||||||
|
'OHLCV frame contains a zero-initialized epoch sentinel'
|
||||||
|
)
|
||||||
|
|
||||||
if np.any(np.diff(times) <= 0):
|
if np.any(np.diff(times) <= 0):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'OHLCV timestamps must be strictly increasing'
|
'OHLCV timestamps must be strictly increasing'
|
||||||
|
|
@ -507,21 +565,14 @@ class NativeStorageClient:
|
||||||
else:
|
else:
|
||||||
incoming = ohlcv
|
incoming = ohlcv
|
||||||
incoming = self._canonicalize_ohlcv(incoming)
|
incoming = self._canonicalize_ohlcv(incoming)
|
||||||
self._validate_ohlcv(incoming)
|
self._validate_ohlcv(incoming, timeframe)
|
||||||
|
|
||||||
path: Path = self.mk_path(fqme, timeframe)
|
path: Path = self.mk_path(fqme, timeframe)
|
||||||
if path.exists():
|
if path.exists():
|
||||||
stored: pl.DataFrame = self._canonicalize_ohlcv(
|
stored: pl.DataFrame = self._repair_stored_ohlcv(
|
||||||
pl.read_parquet(path)
|
pl.read_parquet(path),
|
||||||
|
timeframe=timeframe,
|
||||||
)
|
)
|
||||||
stored_len: int = stored.height
|
|
||||||
stored = stored.filter(pl.col('time') > 0)
|
|
||||||
dropped: int = stored_len - stored.height
|
|
||||||
if dropped:
|
|
||||||
log.warning(
|
|
||||||
f'Dropping {dropped} persisted OHLCV row(s) with '
|
|
||||||
f'non-positive timestamps during merge repair'
|
|
||||||
)
|
|
||||||
merged: pl.DataFrame = pl.concat(
|
merged: pl.DataFrame = pl.concat(
|
||||||
[stored, incoming],
|
[stored, incoming],
|
||||||
how='diagonal_relaxed',
|
how='diagonal_relaxed',
|
||||||
|
|
@ -537,7 +588,7 @@ class NativeStorageClient:
|
||||||
else:
|
else:
|
||||||
merged = incoming
|
merged = incoming
|
||||||
|
|
||||||
self._validate_ohlcv(merged)
|
self._validate_ohlcv(merged, timeframe)
|
||||||
return self._write_ohlcv(
|
return self._write_ohlcv(
|
||||||
fqme,
|
fqme,
|
||||||
merged,
|
merged,
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,69 @@ def test_audit_preserves_raw_defect_evidence() -> None:
|
||||||
assert report['values']['infinity_by_column']['volume'] == 1
|
assert report['values']['infinity_by_column']['volume'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_rejects_epoch_zero_price_sentinel() -> None:
|
||||||
|
'''
|
||||||
|
The legacy first-slot sentinel is structural corruption.
|
||||||
|
|
||||||
|
The repaired MNQ Parquet retained a row at epoch 60 whose OHLCV
|
||||||
|
payload was zero. Numeric and finite checks marked it valid even
|
||||||
|
though chart auto-ranging then included zero. Audit a canonical
|
||||||
|
frame with that sentinel and prove the report counts it and fails
|
||||||
|
structural qualification.
|
||||||
|
|
||||||
|
'''
|
||||||
|
frame = mk_frame((60, 120)).with_columns(
|
||||||
|
pl.Series('open', [0, 1], dtype=pl.Float64),
|
||||||
|
pl.Series('high', [0, 2], dtype=pl.Float64),
|
||||||
|
pl.Series('low', [0, 1], dtype=pl.Float64),
|
||||||
|
pl.Series('close', [0, 2], dtype=pl.Float64),
|
||||||
|
)
|
||||||
|
|
||||||
|
report: dict = audit_ohlcv_frame(
|
||||||
|
frame,
|
||||||
|
fqme='x.test',
|
||||||
|
period_s=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report['values']['all_zero_price_rows'] == 1
|
||||||
|
assert report['values']['epoch_sentinel_rows'] == 1
|
||||||
|
assert 'epoch_ohlc_sentinel' in report['result']['violations']
|
||||||
|
assert report['result']['structural_ok'] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_leaves_modern_zero_prices_unclassified() -> None:
|
||||||
|
'''
|
||||||
|
Audit must not call every all-zero price bar corrupt.
|
||||||
|
|
||||||
|
A spread or synthetic instrument may legitimately trade at zero.
|
||||||
|
The known corruption is tied to the first slot. Place an all-zero
|
||||||
|
bar later and prove it remains visible as a warning without
|
||||||
|
failing structural qualification.
|
||||||
|
|
||||||
|
'''
|
||||||
|
frame = mk_frame((120,)).with_columns(
|
||||||
|
pl.Series('open', [0], dtype=pl.Float64),
|
||||||
|
pl.Series('high', [0], dtype=pl.Float64),
|
||||||
|
pl.Series('low', [0], dtype=pl.Float64),
|
||||||
|
pl.Series('close', [0], dtype=pl.Float64),
|
||||||
|
)
|
||||||
|
|
||||||
|
report: dict = audit_ohlcv_frame(
|
||||||
|
frame,
|
||||||
|
fqme='x.test',
|
||||||
|
period_s=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report['values']['all_zero_price_rows'] == 1
|
||||||
|
assert report['values']['epoch_sentinel_rows'] == 0
|
||||||
|
assert report['values']['unclassified_zero_price_rows'] == 1
|
||||||
|
assert report['result']['structural_ok'] is True
|
||||||
|
assert (
|
||||||
|
'all_zero_ohlc_prices_unclassified'
|
||||||
|
in report['result']['warnings']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_gap_aggregates_are_not_truncated_with_details() -> None:
|
def test_gap_aggregates_are_not_truncated_with_details() -> None:
|
||||||
'''
|
'''
|
||||||
Limiting JSON detail must not undercount total missing coverage.
|
Limiting JSON detail must not undercount total missing coverage.
|
||||||
|
|
|
||||||
|
|
@ -131,47 +131,162 @@ def test_update_preserves_history_and_resolves_conflicts(
|
||||||
assert stored['index'].to_list() == [0, 1, 2, 3]
|
assert stored['index'].to_list() == [0, 1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
def test_update_repairs_nonpositive_persisted_timestamps(
|
def test_update_repairs_invalid_persisted_sentinel_rows(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Valid incoming history must repair a legacy epoch-zero row.
|
Valid incoming history must repair legacy sentinel rows.
|
||||||
|
|
||||||
The known MNQ baseline contains one zero timestamp plus extra derived
|
The known MNQ baseline contained epoch-zero and all-zero rows,
|
||||||
columns and noncanonical absolute indexes. ``update_ohlcv()`` used to
|
plus extra columns and absolute indexes. ``update_ohlcv()`` first
|
||||||
canonicalize those bytes but retain the zero row, then reject the
|
retained both, then removed only epoch zero and left chart range
|
||||||
whole merged frame before fresh IB bars could publish. Write that
|
anchored at zero. Write both sentinels, append a valid provider
|
||||||
legacy shape directly, append a valid provider frame, and prove only
|
frame, and prove only valid bars survive with canonical
|
||||||
the invalid persisted row disappears while old and new valid bars
|
indexes.
|
||||||
survive with canonical indexes.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
client = NativeStorageClient(tmp_path)
|
client = NativeStorageClient(tmp_path)
|
||||||
legacy = (
|
legacy = (
|
||||||
tsp.np2pl(mk_ohlcv(
|
tsp.np2pl(mk_ohlcv(
|
||||||
(0, 60, 120),
|
(0, 60, 120, 180),
|
||||||
(0, 1, 2),
|
(0, 0, 2, 3),
|
||||||
))
|
))
|
||||||
.with_columns(
|
.with_columns(
|
||||||
pl.Series('index', [3137800, 3137801, 3137802]),
|
pl.Series(
|
||||||
pl.Series('time_prev', [None, 0, 60]),
|
'index',
|
||||||
|
[3137800, 3137801, 3137802, 3137803],
|
||||||
|
),
|
||||||
|
pl.Series('time_prev', [None, 0, 60, 120]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
path: Path = client.mk_path('x.test', 60)
|
path: Path = client.mk_path('x.test', 60)
|
||||||
legacy.write_parquet(path)
|
legacy.write_parquet(path)
|
||||||
|
|
||||||
|
loaded = trio.run(client.read_ohlcv, 'x.test', 60)
|
||||||
|
assert loaded['time'].tolist() == [120, 180]
|
||||||
|
assert loaded['index'].tolist() == [0, 1]
|
||||||
|
assert pl.read_parquet(path).height == 4
|
||||||
|
|
||||||
run(client.update_ohlcv(
|
run(client.update_ohlcv(
|
||||||
'x.test',
|
'x.test',
|
||||||
mk_ohlcv((180,), (3,)),
|
mk_ohlcv((240,), (4,)),
|
||||||
60,
|
60,
|
||||||
))
|
))
|
||||||
stored: pl.DataFrame = pl.read_parquet(path)
|
stored: pl.DataFrame = pl.read_parquet(path)
|
||||||
|
|
||||||
assert stored['time'].to_list() == [60, 120, 180]
|
assert stored['time'].to_list() == [120, 180, 240]
|
||||||
assert stored['close'].to_list() == [1, 2, 3]
|
assert stored['close'].to_list() == [2, 3, 4]
|
||||||
assert stored['index'].to_list() == [0, 1, 2]
|
assert stored['index'].to_list() == [0, 1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_preserves_modern_all_zero_prices(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Zero prices outside the legacy sentinel range remain valid.
|
||||||
|
|
||||||
|
Some spreads or synthetic instruments may legitimately trade at
|
||||||
|
zero. Repair targets the observed first-slot sentinel, not every
|
||||||
|
all-zero OHLC bar. Write a modern all-zero row and
|
||||||
|
prove it survives normal replacement unchanged.
|
||||||
|
|
||||||
|
'''
|
||||||
|
client = NativeStorageClient(tmp_path)
|
||||||
|
zero_bar = mk_ohlcv((1_800_000_000,), (0,))
|
||||||
|
|
||||||
|
run(client.write_ohlcv('x.test', zero_bar, 60))
|
||||||
|
stored = pl.read_parquet(client.mk_path('x.test', 60))
|
||||||
|
|
||||||
|
assert stored['time'].to_list() == [1_800_000_000]
|
||||||
|
assert stored['close'].to_list() == [0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_rejects_first_slot_zero_sentinel(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
New writes must not recreate the repaired legacy sentinel.
|
||||||
|
|
||||||
|
Read repair removes all-zero OHLC first-slot rows. Accepting one from
|
||||||
|
new input would recreate a file whose audit fails and whose read view
|
||||||
|
hides a row. Submit the exact 60-second
|
||||||
|
sentinel and prove no file publishes.
|
||||||
|
|
||||||
|
'''
|
||||||
|
client = NativeStorageClient(tmp_path)
|
||||||
|
sentinel = mk_ohlcv((60,), (0,))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='epoch sentinel'):
|
||||||
|
run(client.write_ohlcv('x.test', sentinel, 60))
|
||||||
|
|
||||||
|
assert not client.mk_path('x.test', 60).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sentinel_only_storage_loads_as_no_history(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
A fully repairable legacy file must permit fresh startup.
|
||||||
|
|
||||||
|
Filtering a file containing only sentinels yields an empty array.
|
||||||
|
``load()`` previously indexed its endpoint timestamps and crashed
|
||||||
|
before valid data could replace it. Persist only sentinel rows,
|
||||||
|
prove load reports no history, then append a valid bar and
|
||||||
|
verify durable storage becomes canonical.
|
||||||
|
|
||||||
|
'''
|
||||||
|
client = NativeStorageClient(tmp_path)
|
||||||
|
path: Path = client.mk_path('x.test', 60)
|
||||||
|
tsp.np2pl(mk_ohlcv(
|
||||||
|
(0, 60),
|
||||||
|
(0, 0),
|
||||||
|
)).write_parquet(path)
|
||||||
|
|
||||||
|
assert trio.run(client.load, 'x.test', 60) is None
|
||||||
|
run(client.update_ohlcv(
|
||||||
|
'x.test',
|
||||||
|
mk_ohlcv((120,), (1,)),
|
||||||
|
60,
|
||||||
|
))
|
||||||
|
stored: pl.DataFrame = pl.read_parquet(path)
|
||||||
|
|
||||||
|
assert stored['time'].to_list() == [120]
|
||||||
|
assert stored['index'].to_list() == [0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_does_not_hide_null_price_rows(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Sentinel repair must not silently discard other malformed rows.
|
||||||
|
|
||||||
|
Polars boolean filters drop null predicates. Without an explicit
|
||||||
|
false fill, one null plus three zeros looked neither valid nor
|
||||||
|
all-zero but disappeared before validation. Persist that shape,
|
||||||
|
append valid data, and prove merge still rejects the null
|
||||||
|
while preserving the original evidence file.
|
||||||
|
|
||||||
|
'''
|
||||||
|
client = NativeStorageClient(tmp_path)
|
||||||
|
path: Path = client.mk_path('x.test', 60)
|
||||||
|
malformed = tsp.np2pl(
|
||||||
|
mk_ohlcv((1_800_000_000,), (0,))
|
||||||
|
).with_columns(
|
||||||
|
pl.Series('open', [None], dtype=pl.Float64)
|
||||||
|
)
|
||||||
|
malformed.write_parquet(path)
|
||||||
|
before: bytes = path.read_bytes()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='finite numeric'):
|
||||||
|
run(client.update_ohlcv(
|
||||||
|
'x.test',
|
||||||
|
mk_ohlcv((1_800_000_060,), (1,)),
|
||||||
|
60,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert path.read_bytes() == before
|
||||||
|
|
||||||
|
|
||||||
def test_write_ohlcv_remains_an_explicit_replacement(
|
def test_write_ohlcv_remains_an_explicit_replacement(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue