Committing the prior uncommitted working-tree state that predates this session's data-source work (was already modified/untracked at session start) so the tree is clean before push. Includes: event-study + research report integrity/forward observation work, prices tests, research hash migration script, and the 2026-08-23/24 engineering-log + test-evidence notes. Verified green as part of the full 362-test suite.
862 lines
41 KiB
Python
862 lines
41 KiB
Python
import copy
|
|
import hashlib
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.prices import (
|
|
DEFAULT_SYMBOL_MAP,
|
|
PIT_ARCHIVE_CONTRACT,
|
|
PriceSnapshotStore,
|
|
PriceSourceError,
|
|
_normalized_snapshot_hash,
|
|
_observation_id,
|
|
collect_price_snapshot,
|
|
normalize_yahoo_chart,
|
|
trading_dates,
|
|
)
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "yahoo_chart_aot.json"
|
|
PAYLOAD = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
|
|
|
|
def valid_non_pit_snapshot(snapshot_id="prices-valid", retrieved_at="2026-02-01T00:00:00+00:00"):
|
|
return {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"source_url": "https://example.test/prices",
|
|
"snapshot_id": snapshot_id,
|
|
"retrieved_at": retrieved_at,
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-02",
|
|
"quality": "revised_vendor_history",
|
|
"point_in_time": False,
|
|
"parser_version": "test-v1",
|
|
"adjusted_prices": False,
|
|
},
|
|
"series": {
|
|
"AOT": {
|
|
"canonical_symbol": "AOT",
|
|
"provider_symbol": "AOT.BK",
|
|
"raw_payload_hash": hashlib.sha256(b"aot").hexdigest(),
|
|
"timezone": "Asia/Bangkok",
|
|
"bars": [{"date": "2026-01-01", "close": 100.0}],
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
class FakePriceProvider:
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
self.calls = []
|
|
|
|
def fetch_series(self, canonical_symbol, provider_symbol, start, end):
|
|
self.calls.append((canonical_symbol, provider_symbol, start, end))
|
|
body = json.dumps(self.payload, sort_keys=True).encode("utf-8")
|
|
return normalize_yahoo_chart(
|
|
self.payload,
|
|
canonical_symbol=canonical_symbol,
|
|
provider_symbol=provider_symbol,
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
raw_payload_hash=hashlib.sha256(body).hexdigest(),
|
|
), body
|
|
|
|
|
|
class PriceSnapshotTests(unittest.TestCase):
|
|
def test_default_symbol_map_uses_set_provider_symbols(self):
|
|
self.assertEqual(DEFAULT_SYMBOL_MAP["AOT"], "AOT.BK")
|
|
self.assertEqual(DEFAULT_SYMBOL_MAP["SET50"], "^SET.BK")
|
|
self.assertEqual(DEFAULT_SYMBOL_MAP["PTT"], "PTT.BK")
|
|
|
|
def test_normalize_yahoo_chart_preserves_adjusted_close_and_trading_dates(self):
|
|
series = normalize_yahoo_chart(
|
|
PAYLOAD,
|
|
canonical_symbol="AOT",
|
|
provider_symbol="AOT.BK",
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
raw_payload_hash="a" * 64,
|
|
)
|
|
self.assertEqual(series["exchange"], "SET")
|
|
self.assertEqual(series["currency"], "THB")
|
|
self.assertEqual([bar["date"] for bar in series["bars"]], ["2026-01-02", "2026-01-05"])
|
|
self.assertEqual(series["bars"][0]["adjusted_close"], 60.0)
|
|
self.assertEqual(trading_dates(series), ["2026-01-02", "2026-01-05"])
|
|
|
|
def test_normalize_yahoo_chart_skips_incomplete_rows(self):
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = None
|
|
payload["chart"]["result"][0]["indicators"]["adjclose"][0]["adjclose"][0] = None
|
|
series = normalize_yahoo_chart(
|
|
payload,
|
|
canonical_symbol="AOT",
|
|
provider_symbol="AOT.BK",
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
raw_payload_hash="a" * 64,
|
|
)
|
|
self.assertEqual(len(series["bars"]), 1)
|
|
|
|
def test_normalize_yahoo_chart_rejects_missing_result(self):
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
payload["chart"]["result"] = []
|
|
with self.assertRaisesRegex(PriceSourceError, "result"):
|
|
normalize_yahoo_chart(payload, canonical_symbol="AOT", provider_symbol="AOT.BK", retrieved_at="2026-08-23T06:00:00+00:00", raw_payload_hash="a" * 64)
|
|
|
|
def test_normalize_yahoo_chart_rejects_malformed_provider_shapes_and_numbers(self):
|
|
cases = []
|
|
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
payload["chart"]["result"][0]["meta"] = []
|
|
cases.append(("meta", payload))
|
|
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
payload["chart"]["result"][0]["indicators"]["quote"][0] = []
|
|
cases.append(("quote", payload))
|
|
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
payload["chart"]["result"][0]["timestamp"][0] = 10**100
|
|
cases.append(("timestamp overflow", payload))
|
|
|
|
for label, field, value in (
|
|
("boolean timestamp", "timestamp", True),
|
|
("boolean close", "close", True),
|
|
("boolean volume", "volume", True),
|
|
):
|
|
payload = copy.deepcopy(PAYLOAD)
|
|
if field == "timestamp":
|
|
payload["chart"]["result"][0][field][0] = value
|
|
else:
|
|
payload["chart"]["result"][0]["indicators"]["quote"][0][field][0] = value
|
|
cases.append((label, payload))
|
|
|
|
for label, payload in cases:
|
|
with self.subTest(label=label), self.assertRaises(PriceSourceError):
|
|
normalize_yahoo_chart(
|
|
payload,
|
|
canonical_symbol="AOT",
|
|
provider_symbol="AOT.BK",
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
raw_payload_hash="a" * 64,
|
|
)
|
|
|
|
def test_collector_persists_revised_vendor_history_snapshot(self):
|
|
provider = FakePriceProvider(PAYLOAD)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
snapshot = collect_price_snapshot(
|
|
Path(temp_dir),
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=provider,
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
self.assertEqual(snapshot["source"]["quality"], "revised_vendor_history")
|
|
self.assertFalse(snapshot["source"]["point_in_time"])
|
|
self.assertEqual(snapshot["source"]["bar_counts"]["AOT"], 2)
|
|
manifest = PriceSnapshotStore(Path(temp_dir)).load_manifest()
|
|
self.assertEqual(len(manifest["snapshots"]), 1)
|
|
self.assertEqual(len(provider.calls), 2)
|
|
|
|
def test_collector_records_observation_history_for_unchanged_payload(self):
|
|
provider = FakePriceProvider(PAYLOAD)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=provider,
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=provider,
|
|
retrieved_at="2026-08-24T06:00:00+00:00",
|
|
)
|
|
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
observations = manifest["observations"]
|
|
self.assertEqual(len(observations), 2)
|
|
self.assertEqual(observations[0]["revision_status"], "initial")
|
|
self.assertEqual(observations[1]["revision_status"], "unchanged")
|
|
self.assertEqual(observations[1]["previous_raw_payload_hash"], observations[0]["raw_payload_hash"])
|
|
self.assertFalse(observations[1]["diff"]["normalized_content_changed"])
|
|
entry = store.latest_snapshot_entry()
|
|
self.assertEqual(entry["first_seen_at"], "2026-08-23T06:00:00+00:00")
|
|
self.assertEqual(entry["last_seen_at"], "2026-08-24T06:00:00+00:00")
|
|
self.assertEqual(entry["observation_count"], 2)
|
|
|
|
def test_collector_records_revision_diff_without_overwriting_previous_snapshot(self):
|
|
changed_payload = copy.deepcopy(PAYLOAD)
|
|
changed_payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = 61.0
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
revised = collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(changed_payload),
|
|
retrieved_at="2026-08-24T06:00:00+00:00",
|
|
)
|
|
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
observations = manifest["observations"]
|
|
self.assertEqual(len(manifest["snapshots"]), 2)
|
|
self.assertEqual(len(observations), 2)
|
|
self.assertEqual(observations[1]["revision_status"], "revised")
|
|
self.assertTrue(observations[1]["diff"]["normalized_content_changed"])
|
|
self.assertIn("series.AOT.bars[0].close", observations[1]["diff"]["changed_paths"])
|
|
self.assertEqual(observations[1]["previous_snapshot_id"], observations[0]["snapshot_id"])
|
|
self.assertNotEqual(revised["source"]["snapshot_id"], observations[0]["snapshot_id"])
|
|
self.assertEqual(store.load_snapshot(observations[0]["snapshot_id"])["series"]["AOT"]["bars"][0]["close"], 61.5)
|
|
|
|
def test_price_store_rejects_tampered_observation_audit_entry(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
manifest["observations"][0]["diff"]["changed_paths"] = ["series.AOT.bars[0].close"]
|
|
store.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaises(PriceSourceError):
|
|
store.list_observations()
|
|
|
|
def test_price_store_rejects_recomputed_observation_id_with_mismatched_predecessor(self):
|
|
changed_payload = copy.deepcopy(PAYLOAD)
|
|
changed_payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = 61.0
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(changed_payload),
|
|
retrieved_at="2026-08-24T06:00:00+00:00",
|
|
)
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
tampered = manifest["observations"][1]
|
|
tampered["previous_raw_payload_hash"] = "0" * 64
|
|
tampered["observation_id"] = _observation_id({key: value for key, value in tampered.items() if key != "observation_id"})
|
|
store.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "previous.*hash|observation"):
|
|
store.list_observations()
|
|
|
|
def test_price_store_rejects_recomputed_observation_id_with_tampered_diff(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
tampered = manifest["observations"][0]
|
|
tampered["diff"]["changed_paths"] = ["series.AOT.bars[0].close"]
|
|
tampered["observation_id"] = _observation_id({key: value for key, value in tampered.items() if key != "observation_id"})
|
|
store.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "diff|observation"):
|
|
store.list_observations()
|
|
|
|
def test_price_store_rejects_malformed_observation_id_types_without_type_error(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
tampered = manifest["observations"][0]
|
|
tampered["snapshot_id"] = []
|
|
tampered["observation_id"] = _observation_id({key: value for key, value in tampered.items() if key != "observation_id"})
|
|
store.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "observation"):
|
|
store.list_observations()
|
|
|
|
def test_price_store_rejects_observation_history_truncation(self):
|
|
changed_payload = copy.deepcopy(PAYLOAD)
|
|
changed_payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = 61.0
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(changed_payload),
|
|
retrieved_at="2026-08-24T06:00:00+00:00",
|
|
)
|
|
store = PriceSnapshotStore(root)
|
|
manifest = store.load_manifest()
|
|
manifest["observations"] = manifest["observations"][:-1]
|
|
store.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "observation"):
|
|
store.list_observations()
|
|
|
|
def test_collector_rejects_out_of_order_observation_after_future_capture(self):
|
|
revised_payload = copy.deepcopy(PAYLOAD)
|
|
revised_payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = 61.0
|
|
late_payload = copy.deepcopy(PAYLOAD)
|
|
late_payload["chart"]["result"][0]["indicators"]["quote"][0]["close"][0] = 62.0
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(PAYLOAD),
|
|
retrieved_at="2026-08-23T06:00:00+00:00",
|
|
)
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(revised_payload),
|
|
retrieved_at="2026-08-24T06:00:00+00:00",
|
|
)
|
|
before = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
with self.assertRaisesRegex(PriceSourceError, "immutable predecessor"):
|
|
collect_price_snapshot(
|
|
root,
|
|
start="2026-01-01",
|
|
end="2026-01-06",
|
|
symbol_map={"AOT": "AOT.BK", "SET50": "^SET.BK"},
|
|
provider=FakePriceProvider(late_payload),
|
|
retrieved_at="2026-08-22T06:00:00+00:00",
|
|
)
|
|
after = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(after, before)
|
|
|
|
def test_price_store_rejects_snapshot_with_mismatched_raw_hash(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {"source": {"snapshot_id": "prices-test", "raw_payload_hash": "a" * 64}}
|
|
with self.assertRaisesRegex(PriceSourceError, "raw payload hash"):
|
|
store.persist(snapshot, {"AOT.BK": b"different"})
|
|
|
|
def test_price_store_rejects_same_raw_hash_with_changed_stable_content(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"snapshot_id": "prices-same-raw",
|
|
"retrieved_at": "2026-02-01T00:00:00+00:00",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-01",
|
|
"quality": "revised_vendor_history",
|
|
"point_in_time": False,
|
|
},
|
|
"series": {
|
|
"AOT": {
|
|
"canonical_symbol": "AOT",
|
|
"provider_symbol": "AOT.BK",
|
|
"raw_payload_hash": hashlib.sha256(b"aot").hexdigest(),
|
|
"timezone": "Asia/Bangkok",
|
|
"bars": [{"date": "2026-01-01", "close": 100.0}],
|
|
}
|
|
},
|
|
}
|
|
raw = {"AOT.BK": b"aot"}
|
|
store.persist(snapshot, raw)
|
|
changed = copy.deepcopy(snapshot)
|
|
changed["series"]["AOT"]["bars"][0]["close"] = 101.0
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "immutable"):
|
|
store.persist(changed, raw)
|
|
|
|
def test_price_store_rejects_same_raw_payload_with_changed_normalized_content(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
initial = valid_non_pit_snapshot("prices-same-raw-initial", "2026-02-01T00:00:00+00:00")
|
|
store.persist(initial, {"AOT.BK": b"aot"})
|
|
|
|
revised = valid_non_pit_snapshot("prices-same-raw-revised", "2026-02-02T00:00:00+00:00")
|
|
revised["series"]["AOT"]["bars"][0]["close"] = 101.0
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "normalized content"):
|
|
store.persist(revised, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_rejects_point_in_time_flag_without_archive_contract(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"snapshot_id": "prices-pit-missing-contract",
|
|
"retrieved_at": "2026-02-01T00:00:00+00:00",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-02",
|
|
"quality": "forward_market_archive",
|
|
"point_in_time": True,
|
|
},
|
|
"series": {"AOT": {"bars": [{"date": "2026-01-01", "close": 100.0}]}},
|
|
}
|
|
with self.assertRaisesRegex(PriceSourceError, "point-in-time archive contract"):
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_requires_known_at_for_each_pit_bar(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"source_url": "https://example.test/prices",
|
|
"snapshot_id": "prices-pit-missing-known-at",
|
|
"retrieved_at": "2026-02-01T00:00:00+00:00",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-01",
|
|
"quality": "point_in_time_archive",
|
|
"point_in_time": True,
|
|
"archive_contract": PIT_ARCHIVE_CONTRACT,
|
|
"provider_release_id": "release-2026-01-01",
|
|
"parser_version": "pit-test-v1",
|
|
"point_in_time_evidence": {
|
|
"known_at_field": "known_at",
|
|
"known_at_semantics": "provider_release_time",
|
|
"provider_release_id": "release-2026-01-01",
|
|
"release_published_at": "2026-01-01T17:00:00+07:00",
|
|
},
|
|
},
|
|
"series": {"AOT": {"canonical_symbol": "AOT", "timezone": "Asia/Bangkok", "bars": [{"session_date": "2026-01-01", "close": 100.0}]}},
|
|
}
|
|
with self.assertRaisesRegex(PriceSourceError, "known_at"):
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_persists_and_loads_explicit_pit_archive_contract(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"source_url": "https://example.test/prices",
|
|
"snapshot_id": "prices-pit-valid",
|
|
"retrieved_at": "2026-02-01T00:00:00+00:00",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-01",
|
|
"quality": "point_in_time_archive",
|
|
"point_in_time": True,
|
|
"archive_contract": PIT_ARCHIVE_CONTRACT,
|
|
"provider_release_id": "release-2026-01-01",
|
|
"parser_version": "pit-test-v1",
|
|
"point_in_time_evidence": {
|
|
"known_at_field": "known_at",
|
|
"known_at_semantics": "provider_release_time",
|
|
"provider_release_id": "release-2026-01-01",
|
|
"release_published_at": "2026-01-01T17:00:00+07:00",
|
|
},
|
|
},
|
|
"series": {
|
|
"AOT": {
|
|
"canonical_symbol": "AOT",
|
|
"timezone": "Asia/Bangkok",
|
|
"bars": [{
|
|
"session_date": "2026-01-01",
|
|
"open": 99.0,
|
|
"high": 101.0,
|
|
"low": 98.0,
|
|
"close": 100.0,
|
|
"adjusted_close": 100.0,
|
|
"volume": 1000,
|
|
"known_at": "2026-01-01T17:00:00+07:00",
|
|
}],
|
|
}
|
|
},
|
|
}
|
|
loaded = store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
self.assertEqual(loaded["source"]["archive_contract"], PIT_ARCHIVE_CONTRACT)
|
|
self.assertEqual(store.load_snapshot("prices-pit-valid")["source"]["provider_release_id"], "release-2026-01-01")
|
|
manifest_entry = store.load_manifest()["snapshots"]["prices-pit-valid"]
|
|
self.assertEqual(manifest_entry["archive_contract"], PIT_ARCHIVE_CONTRACT)
|
|
self.assertTrue(manifest_entry["point_in_time"])
|
|
|
|
snapshot["series"]["AOT"]["bars"][0]["close"] = 101.0
|
|
with self.assertRaisesRegex(PriceSourceError, "immutable"):
|
|
store.persist(snapshot, {"AOT.BK": b"aot-revision"})
|
|
|
|
manifest = store.load_manifest()
|
|
manifest["snapshots"]["prices-pit-valid"]["period_end"] = "2026-01-02"
|
|
(Path(temp_dir) / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
with self.assertRaisesRegex(PriceSourceError, "point-in-time metadata"):
|
|
store.load_snapshot("prices-pit-valid")
|
|
|
|
def test_price_store_totalizes_extreme_pit_numeric_values(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {
|
|
"source_id": "test.prices",
|
|
"source_url": "https://example.test/prices",
|
|
"snapshot_id": "prices-pit-extreme-number",
|
|
"retrieved_at": "2026-02-01T00:00:00+00:00",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-01",
|
|
"quality": "point_in_time_archive",
|
|
"point_in_time": True,
|
|
"archive_contract": PIT_ARCHIVE_CONTRACT,
|
|
"provider_release_id": "release-2026-01-01",
|
|
"parser_version": "pit-test-v1",
|
|
"point_in_time_evidence": {
|
|
"known_at_field": "known_at",
|
|
"known_at_semantics": "provider_release_time",
|
|
"provider_release_id": "release-2026-01-01",
|
|
"release_published_at": "2026-01-01T17:00:00+07:00",
|
|
},
|
|
},
|
|
"series": {
|
|
"AOT": {
|
|
"canonical_symbol": "AOT",
|
|
"timezone": "Asia/Bangkok",
|
|
"bars": [{
|
|
"session_date": "2026-01-01",
|
|
"open": 99.0,
|
|
"high": 101.0,
|
|
"low": 98.0,
|
|
"close": 100.0,
|
|
"adjusted_close": 100.0,
|
|
"known_at": "2026-01-01T17:00:00+07:00",
|
|
"volume": 10**1000,
|
|
}],
|
|
}
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(PriceSourceError, "point-in-time archive volume"):
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_rejects_non_object_manifest_before_persist(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
(root / "manifest.json").write_text("[]", encoding="utf-8")
|
|
store = PriceSnapshotStore(root)
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {"source_id": "test.prices", "snapshot_id": "prices-malformed-manifest", "point_in_time": False},
|
|
"series": {},
|
|
}
|
|
with self.assertRaisesRegex(PriceSourceError, "manifest"):
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_rejects_invalid_raw_payload_symbol(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
store = PriceSnapshotStore(Path(temp_dir))
|
|
snapshot = {
|
|
"schema_version": 1,
|
|
"source": {"source_id": "test.prices", "snapshot_id": "prices-invalid-raw-symbol", "point_in_time": False},
|
|
"series": {},
|
|
}
|
|
raw_payloads: Any = {1: b"aot"}
|
|
with self.assertRaisesRegex(PriceSourceError, "raw payload"):
|
|
store.persist(snapshot, raw_payloads)
|
|
def test_price_store_rejects_series_provenance_mismatch_after_normalized_hash_refresh(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
snapshot = valid_non_pit_snapshot()
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
snapshot_path = root / "snapshots" / "prices-valid.json"
|
|
original = json.loads(snapshot_path.read_text(encoding="utf-8"))
|
|
|
|
for field, value in (("provider_symbol", "PTT.BK"), ("raw_payload_hash", "0" * 64)):
|
|
tampered = copy.deepcopy(original)
|
|
tampered["series"]["AOT"][field] = value
|
|
tampered["source"]["normalized_snapshot_hash"] = _normalized_snapshot_hash(tampered)
|
|
snapshot_path.write_text(json.dumps(tampered), encoding="utf-8")
|
|
with self.subTest(field=field), self.assertRaisesRegex(PriceSourceError, "series"):
|
|
store.load_snapshot("prices-valid")
|
|
|
|
def test_price_store_rejects_missing_required_metadata_and_malformed_non_pit_series(self):
|
|
cases = []
|
|
snapshot = valid_non_pit_snapshot("prices-missing-schema")
|
|
snapshot.pop("schema_version")
|
|
cases.append(("schema_version", snapshot))
|
|
|
|
snapshot = valid_non_pit_snapshot("prices-missing-pit-flag")
|
|
snapshot["source"].pop("point_in_time")
|
|
cases.append(("point_in_time", snapshot))
|
|
|
|
snapshot = valid_non_pit_snapshot("prices-missing-source-id")
|
|
snapshot["source"].pop("source_id")
|
|
cases.append(("source identity", snapshot))
|
|
|
|
snapshot = valid_non_pit_snapshot("prices-malformed-series")
|
|
snapshot["series"]["AOT"]["bars"] = "not-a-list"
|
|
cases.append(("normalized series", snapshot))
|
|
|
|
snapshot = valid_non_pit_snapshot("prices-duplicate-bars")
|
|
snapshot["series"]["AOT"]["bars"].append(copy.deepcopy(snapshot["series"]["AOT"]["bars"][0]))
|
|
cases.append(("duplicate normalized dates", snapshot))
|
|
|
|
for label, snapshot in cases:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
with self.subTest(label=label), self.assertRaises(PriceSourceError):
|
|
PriceSnapshotStore(Path(temp_dir)).persist(snapshot, {"AOT.BK": b"aot"})
|
|
|
|
def test_price_store_rejects_non_pit_manifest_metadata_tampering(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot(), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
original = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
|
|
for field, value in (("parser_version", "tampered-parser"), ("symbols", ["PTT"])):
|
|
manifest = copy.deepcopy(original)
|
|
manifest["snapshots"]["prices-valid"][field] = value
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
with self.subTest(field=field), self.assertRaisesRegex(PriceSourceError, "metadata"):
|
|
store.load_snapshot("prices-valid")
|
|
|
|
manifest = copy.deepcopy(original)
|
|
manifest["snapshots"]["prices-valid"].pop("source_url")
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
with self.assertRaisesRegex(PriceSourceError, "metadata"):
|
|
store.load_snapshot("prices-valid")
|
|
|
|
def test_price_store_rejects_out_of_order_insertion_without_changing_manifest(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
late = valid_non_pit_snapshot("prices-late", "2026-02-01T18:30:00-07:00")
|
|
early = valid_non_pit_snapshot("prices-early", "2026-02-02T00:00:00+00:00")
|
|
store.persist(late, {"AOT.BK": b"aot"})
|
|
before = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "immutable predecessor"):
|
|
store.persist(early, {"AOT.BK": b"aot"})
|
|
|
|
after = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(after, before)
|
|
self.assertFalse((root / "snapshots" / "prices-early.json").exists())
|
|
|
|
def test_price_store_uses_deterministic_predecessor_for_equal_timestamps(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
timestamp = "2026-02-01T00:00:00+00:00"
|
|
store.persist(valid_non_pit_snapshot("prices-a", timestamp), {"AOT.BK": b"aot"})
|
|
store.persist(valid_non_pit_snapshot("prices-b", timestamp), {"AOT.BK": b"aot"})
|
|
|
|
observation = next(item for item in store.load_manifest()["observations"] if item["snapshot_id"] == "prices-b")
|
|
self.assertEqual(observation["previous_snapshot_id"], "prices-a")
|
|
|
|
def test_price_store_rejects_reverse_arrival_for_equal_timestamp(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
timestamp = "2026-02-01T00:00:00+00:00"
|
|
store.persist(valid_non_pit_snapshot("prices-b", timestamp), {"AOT.BK": b"aot"})
|
|
before = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "immutable predecessor"):
|
|
store.persist(valid_non_pit_snapshot("prices-a", timestamp), {"AOT.BK": b"aot"})
|
|
|
|
after = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(after, before)
|
|
|
|
def test_price_store_treats_same_snapshot_same_timestamp_as_idempotent(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
snapshot = valid_non_pit_snapshot("prices-idempotent", "2026-02-01T00:00:00+00:00")
|
|
store.persist(snapshot, {"AOT.BK": b"aot"})
|
|
store.persist(copy.deepcopy(snapshot), {"AOT.BK": b"aot"})
|
|
|
|
self.assertEqual(len(store.list_observations()), 1)
|
|
|
|
def test_price_store_rejects_equal_timestamp_initial_node_with_canonical_predecessor(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
timestamp = "2026-02-01T00:00:00+00:00"
|
|
store.persist(valid_non_pit_snapshot("prices-a", timestamp), {"AOT.BK": b"aot"})
|
|
store.persist(valid_non_pit_snapshot("prices-b", timestamp), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
observation = next(item for item in manifest["observations"] if item["snapshot_id"] == "prices-b")
|
|
observation.pop("observation_id")
|
|
observation["previous_snapshot_id"] = None
|
|
observation["previous_raw_payload_hash"] = None
|
|
observation["revision_status"] = "initial"
|
|
observation["observation_id"] = _observation_id(observation)
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "predecessor chain"):
|
|
store.load_manifest()
|
|
|
|
def test_price_store_rejects_duplicate_equal_time_observations_for_one_snapshot(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
snapshot_id = "prices-duplicate-time"
|
|
store.persist(valid_non_pit_snapshot(snapshot_id, "2026-02-01T00:00:00+00:00"), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
original = manifest["observations"][0]
|
|
duplicate_diff = copy.deepcopy(original["diff"])
|
|
duplicate_diff["raw_payload_changed"] = False
|
|
observations = []
|
|
for retrieved_at in ("2026-02-01T00:00:00+00:00", "2026-02-01T00:00:00Z"):
|
|
observation = copy.deepcopy(original)
|
|
observation["retrieved_at"] = retrieved_at
|
|
observation["previous_snapshot_id"] = snapshot_id
|
|
observation["previous_raw_payload_hash"] = original["raw_payload_hash"]
|
|
observation["revision_status"] = "unchanged"
|
|
observation["diff"] = copy.deepcopy(duplicate_diff)
|
|
observation["observation_id"] = _observation_id(
|
|
{key: value for key, value in observation.items() if key != "observation_id"}
|
|
)
|
|
observations.append(observation)
|
|
manifest["observations"] = observations
|
|
entry = manifest["snapshots"][snapshot_id]
|
|
entry["observation_count"] = 2
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "ambiguous observation"):
|
|
store.load_manifest()
|
|
|
|
def test_price_store_orders_latest_snapshot_by_utc_time(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot("prices-early", "2026-02-02T00:00:00+00:00"), {"AOT.BK": b"aot"})
|
|
store.persist(valid_non_pit_snapshot("prices-late", "2026-02-01T18:30:00-07:00"), {"AOT.BK": b"aot"})
|
|
|
|
self.assertEqual(store.latest_snapshot_entry()["snapshot_id"], "prices-late")
|
|
|
|
def test_price_store_validates_timing_metadata_when_observation_count_is_omitted(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot(), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
entry = manifest["snapshots"]["prices-valid"]
|
|
entry.pop("observation_count")
|
|
entry["first_seen_at"] = "not-a-timestamp"
|
|
entry["last_seen_at"] = "not-a-timestamp"
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "timing"):
|
|
store.load_manifest()
|
|
|
|
def test_price_store_rejects_observation_before_snapshot_first_capture(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot("prices-temporal", "2026-02-01T00:00:00+00:00"), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
observation = manifest["observations"][0]
|
|
observation["retrieved_at"] = "2026-01-31T00:00:00+00:00"
|
|
observation["observation_id"] = _observation_id({key: value for key, value in observation.items() if key != "observation_id"})
|
|
entry = manifest["snapshots"]["prices-temporal"]
|
|
entry["first_seen_at"] = "2026-01-31T00:00:00+00:00"
|
|
entry["last_seen_at"] = "2026-01-31T00:00:00+00:00"
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "before snapshot"):
|
|
store.load_manifest()
|
|
|
|
def test_price_store_rejects_unreferenced_contract_snapshot(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot(), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
unreferenced = copy.deepcopy(manifest["snapshots"]["prices-valid"])
|
|
unreferenced["snapshot_id"] = "prices-unreferenced"
|
|
unreferenced["snapshot_file"] = "prices-unreferenced.json"
|
|
unreferenced.pop("observation_count")
|
|
unreferenced.pop("first_seen_at")
|
|
unreferenced.pop("last_seen_at")
|
|
manifest["snapshots"]["prices-unreferenced"] = unreferenced
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "observation count"):
|
|
store.load_manifest()
|
|
|
|
def test_price_store_rejects_malformed_snapshot_manifest_entry(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
store = PriceSnapshotStore(root)
|
|
store.persist(valid_non_pit_snapshot(), {"AOT.BK": b"aot"})
|
|
manifest_path = root / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
manifest["snapshots"]["prices-malformed-entry"] = []
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with self.assertRaisesRegex(PriceSourceError, "snapshot manifest entry"):
|
|
store.load_manifest()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|