106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
import copy
|
|
import hashlib
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from app.prices import (
|
|
DEFAULT_SYMBOL_MAP,
|
|
PriceSnapshotStore,
|
|
PriceSourceError,
|
|
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"))
|
|
|
|
|
|
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_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_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"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|