From aba107d082b991d99692c7afb8b8090a21baecca Mon Sep 17 00:00:00 2001 From: Gniewkob Date: Fri, 13 Nov 2026 00:39:51 +0100 Subject: [PATCH 1/3] research: one-shot multi-source public NBP + Eurostat macro evidence bundle and exchange rights queue (#154) --- ...ic_first_multisource_macro_evidence_v05.py | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 scripts/public_first_multisource_macro_evidence_v05.py diff --git a/scripts/public_first_multisource_macro_evidence_v05.py b/scripts/public_first_multisource_macro_evidence_v05.py new file mode 100644 index 00000000..781464c4 --- /dev/null +++ b/scripts/public_first_multisource_macro_evidence_v05.py @@ -0,0 +1,251 @@ +"""Integrated public-first source bundle: NBP + Eurostat + exchange rights. + +Public official macro reference only: no historic PIT, market-data license +grant, equity/bond total-return admission, investor advice or execution. +One explicitly authorized, bounded, one-shot read of four fixed HTTPS URLs. +""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +from decimal import Decimal, InvalidOperation, localcontext +from hashlib import sha256 +import json +from typing import Callable + +from scripts.nbp_reference_capture import capture +from scripts.public_first_nbp_reference_research_v01 import official_fetch +from scripts.eurostat_hicp_macro_context_v01 import ( + _fetch as official_hicp_fetch, _query as hicp_query, inspect_hicp, +) +from scripts.eurostat_pl_bond_yield_source_probe_v04 import ( + fetch_official as official_yield_fetch, URL as YIELD_URL, +) +from scripts.eurostat_pl_government_bond_yield_context_v04 import ( + parse_original_yields, +) +from scripts.public_first_prospective_source_observation_v03 import ( + assemble_receipt, +) + +SCHEMA = "ISL_PUBLIC_MULTISOURCE_MACRO_EVIDENCE_V05" +NBP_START, NBP_END = "2026-09-01", "2026-09-30" +HICP_SINCE, HICP_UNTIL = "2025-09", "2026-09" +YIELD_SINCE, YIELD_UNTIL = "2021-11", "2026-09" + +# Source metadata and rights statuses, not live access tokens or model grants. +SOURCE_QUEUE = ( + { + "id": "BONDSPOT_TBSP_INDEX", + "official_url": "https://www.bondspot.pl/tbsp_index_archiwum", + "publisher": "BondSpot / GPW Benchmark", + "data_kind": "POLISH_GOV_BOND_BENCHMARK_INDEX_LEVEL", + "state": "PUBLIC_EXCHANGE_ARCHIVE_RIGHTS_AND_PIT_BLOCKED", + "rights_url": "https://www.bondspot.pl/1303", + "automated_ingestion_authorized": False, + "historic_first_published_proven": False, + "asset_total_return_qualified": False, + }, + { + "id": "GPW_STOCK_INDEX_HISTORY", + "official_url": "https://www.gpw.pl/archiwum-notowan", + "publisher": "GPW / GPW Benchmark", + "data_kind": "EQUITY_INDEX_HISTORY_CHANNEL_UNQUALIFIED", + "state": "PUBLIC_EXCHANGE_EXPORT_RIGHTS_AND_INDEX_SEMANTICS_BLOCKED", + "rights_url": "https://gpwbenchmark.pl/zastrzezenia-prawne", + "automated_ingestion_authorized": False, + "historic_first_published_proven": False, + "asset_total_return_qualified": False, + }, + { + "id": "VANGUARD_ALL_WORLD_ACC_NAV_USD", + "official_url": "https://www.pl.vanguard/inwestorzy-indywidualni/produkt/etf/akcje/9679/ftse-all-world-ucits-etf-usd-accumulating", + "publisher": "Vanguard", + "data_kind": "FUND_SHARE_CLASS_NET_ASSET_VALUE_NOT_EXECUTABLE_PRICE", + "state": "PRIVATE_ORIGINAL_FILE_VERIFIED_RIGHTS_PIT_BLOCKED", + "rights_url": "https://www.vanguard.co.uk/professional/terms-and-conditions", + "automated_ingestion_authorized": False, + "historic_first_published_proven": False, + "asset_total_return_qualified": False, + }, + { + "id": "SWINKELS_GOV_BOND_2020_2022", + "official_url": "https://doi.org/10.34894/YWT2IX", + "publisher": "Laurens Swinkels / DataverseNL", + "data_kind": "HISTORIC_MODELED_NON_POLISH_BOND_HOLDING_PERIOD_RETURN", + "state": "CC_BY_HISTORIC_RESEARCH_NOT_POLISH_TR_OR_CURRENT_PIT", + "rights_url": "https://creativecommons.org/licenses/by/4.0/", + "automated_ingestion_authorized": False, + "historic_first_published_proven": False, + "asset_total_return_qualified": False, + }, +) + + +class MultiSourceBlocked(ValueError): + """One source failed validation; do not produce a partial success.""" + + +def _decimal(value: object, *, positive: bool = False) -> Decimal: + if isinstance(value, bool) or type(value) not in (str, int, float): + raise MultiSourceBlocked("NONFINITE_OR_INVALID_SOURCE_NUMBER") + try: + number = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise MultiSourceBlocked("NONFINITE_OR_INVALID_SOURCE_NUMBER") from exc + if not number.is_finite() or (positive and number <= 0): + raise MultiSourceBlocked("NONFINITE_OR_INVALID_SOURCE_NUMBER") + return number + + +def _utc(value: object) -> datetime: + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise MultiSourceBlocked("UTC_AWARE_CLOCK_REQUIRED") + return value.astimezone(timezone.utc) + + +def _canonical(data: object) -> bytes: + return json.dumps(data, separators=(",", ":"), sort_keys=True).encode() + + +def _sep2026_bond_yield(yield_raw: bytes) -> Decimal: + """After the v0.4 typed source validator: 1×1×1×59, all dimensions fixed.""" + payload = json.loads(yield_raw) + ids = payload["id"] + cats = payload["dimension"]["time"]["category"]["index"] + position = cats[YIELD_UNTIL] + coordinates = [position if k == "time" else 0 for k in ids] + offset = 0 + for coordinate, size in zip(coordinates, payload["size"]): + offset = offset * size + coordinate + values = payload["value"] + value = (values[offset] if isinstance(values, list) + else values.get(str(offset))) + number = _decimal(value) + if not Decimal("-20") <= number <= Decimal("50"): + raise MultiSourceBlocked("YIELD_UNIT_NOT_PERCENT_PER_ANNUM") + return number + + +def evaluate_combined_sources(nbp: object, hicp: object, yield_raw: bytes, *, + completed_at: datetime) -> dict: + """In-memory source-receipt reconciliation; no external calls.""" + clock = _utc(completed_at) + if clock.date().isoformat() < NBP_END or clock.strftime("%Y-%m") < YIELD_UNTIL: + raise MultiSourceBlocked("FUTURE_SOURCE_WINDOW") + yield_report = parse_original_yields(yield_raw, observed_at=clock) + joint = assemble_receipt( + nbp, hicp, nbp_start=NBP_START, nbp_end=NBP_END, + hicp_since=HICP_SINCE, hicp_until=HICP_UNTIL, completed_at=clock) + if (yield_report.get("observed_months") != 59 + or yield_report.get("last_month") != YIELD_UNTIL + or yield_report.get("country") != "PL" + or yield_report.get("source_unit") != "PERCENT_PER_ANNUM" + or yield_report.get("historical_pit_certified") is not False + or joint.get("historical_pit_certified") is not False + or any(yield_report.get(key) != 0 for key in + ("financial_operations", "database_writes", "orders_sent"))): + raise MultiSourceBlocked("WRONG_OR_UNQUALIFIED_SOURCE") + observations = hicp["observations"] + if (len(observations) != 13 + or observations[0].get("month") != HICP_SINCE + or observations[-1].get("month") != HICP_UNTIL + or joint.get("eurostat_observation_months") != 13): + raise MultiSourceBlocked("HICP_13_MONTH_BASELINE_REQUIRED") + with localcontext() as ctx: + ctx.prec = 40 + before = _decimal(observations[0]["index_2025_100"], positive=True) + now = _decimal(observations[-1]["index_2025_100"], positive=True) + trailing_hicp_pct = (now / before - Decimal("1")) * Decimal("100") + nominal_yield_pct = _sep2026_bond_yield(yield_raw) + gap_pp = nominal_yield_pct - trailing_hicp_pct + if len(nbp["series"]["gold"]) != len(nbp["series"]["usdpln"]) or ( + len(nbp["series"]["gold"]) != len(nbp["series"]["eurpln"])): + raise MultiSourceBlocked("NBP_SOURCE_SERIES_MISALIGNED") + # NBP gold and FX are reference quotations, not custody/market fills. + if any( + [row["publication_date"] for row in nbp["series"][kind]] != + [row["publication_date"] for row in nbp["series"]["gold"]] + for kind in ("usdpln", "eurpln") + ): + raise MultiSourceBlocked("NBP_PUBLICATION_DATES_NOT_IDENTICAL") + report = { + "schema": SCHEMA, + "state": "PUBLIC_FIRST_JOINT_MACRO_RESEARCH_ONLY", + "observed_at_utc": clock.isoformat(), + "source_families": ["NBP", "Eurostat HICP", "Eurostat PL government yield"], + "series_types": { + "nbp_usdpln": "TABLE_A_MID_FX_REFERENCE", + "nbp_eurpln": "TABLE_A_MID_FX_REFERENCE", + "nbp_gold": "GOLD_PRICE_PLN_PER_GRAM", + "hicp_pl": "CONSUMER_PRICE_INDEX_I25", + "pl_government_10y": "NOMINAL_YIELD_PERCENT_PER_ANNUM_NOT_BOND_RETURN", + }, + "nbp_observed_business_days": len(nbp["series"]["gold"]), + "nbp_reference_window": [NBP_START, NBP_END], + "hicp_months": 13, + "hicp_window": [HICP_SINCE, HICP_UNTIL], + "government_yield_months": 59, + "government_yield_window": [YIELD_SINCE, YIELD_UNTIL], + "joint_reference_month": YIELD_UNTIL, + "pl_nominal_10y_government_yield_pct_per_annum": str(nominal_yield_pct), + "pl_trailing_12m_hicp_inflation_pct": format(trailing_hicp_pct.quantize(Decimal("0.0001")), "f"), + "arithmetic_yield_minus_trailing_inflation_pp": format(gap_pp.quantize(Decimal("0.0001")), "f"), + "gap_semantics": "DESCRIPTIVE_LEVEL_MINUS_BACKWARD_LOOKING_PRICE_INDEX_CHANGE_NOT_REAL_YIELD_RETURN_OR_FORECAST", + "nbp_eurostat_joint_source_receipt_sha256": joint["receipt_sha256"], + "yield_original_source_sha256": yield_report["original_response_sha256"], + "source_fingerprints_sha256": { + **joint["source_fingerprints_sha256"], + "eurostat_pl_government_yield": yield_report["original_response_sha256"], + }, + "external_exchange_source_queue": [dict(x) for x in SOURCE_QUEUE], + "exchange_licensed_bulk_source_approved": False, + "historical_point_in_time_certified": False, + "source_rights_fully_approved": False, + "asset_total_return_series_qualified": [], + "m1_m2_m3": "INSUFFICIENT_DATA_HOLD_CASH", + "financial_operations": 0, + "account_connections": 0, + "database_writes": 0, + "paper_events_created": 0, + "orders_sent": 0, + } + report["receipt_sha256"] = sha256(_canonical(report)).hexdigest() + return report + + +def observe_once(*, nbp_fetch: Callable | None, hicp_fetch: Callable | None, + yield_fetch: Callable | None, + clock: Callable = lambda: datetime.now(timezone.utc)) -> dict: + if nbp_fetch is None or hicp_fetch is None or yield_fetch is None: + raise MultiSourceBlocked("EXPLICIT_PUBLIC_SOURCE_FETCH_REQUIRED") + # Validate clock before opening any network connection. + started = _utc(clock()) + if started.date().isoformat() < NBP_END: + raise MultiSourceBlocked("FUTURE_SOURCE_WINDOW") + nbp = capture(NBP_START, NBP_END, fetch=nbp_fetch, clock=clock) + hicp = inspect_hicp( + hicp_fetch(hicp_query(HICP_SINCE, HICP_UNTIL)), + since=HICP_SINCE, until=HICP_UNTIL, + retrieval_scope="EUROSTAT_PUBLIC_READ_ONLY") + # Yield fetch is explicitly injected; official implementation has fixed URL. + raw = yield_fetch() + return evaluate_combined_sources(nbp, hicp, raw, completed_at=clock()) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--allow-public-official-fetch", action="store_true") + args = parser.parse_args() + if not args.allow_public_official_fetch: + parser.error("Explicit --allow-public-official-fetch is required") + report = observe_once( + nbp_fetch=official_fetch, hicp_fetch=official_hicp_fetch, + yield_fetch=official_yield_fetch) + # No per-day NBP observations or 59-row Eurostat source vectors. + print("ISL_PUBLIC_MULTISOURCE_CONTEXT=" + + json.dumps(report, sort_keys=True, separators=(",", ":"))) + + +if __name__ == "__main__": + main() From ff7d2007cc4a1529168a7b8a5a042a2ed87cbab2 Mon Sep 17 00:00:00 2001 From: Gniewkob Date: Fri, 13 Nov 2026 00:40:43 +0100 Subject: [PATCH 2/3] test: 12 end-to-end offline public-macro source and rights safety contracts (#154) --- ...ic_first_multisource_macro_evidence_v05.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/test_public_first_multisource_macro_evidence_v05.py diff --git a/tests/test_public_first_multisource_macro_evidence_v05.py b/tests/test_public_first_multisource_macro_evidence_v05.py new file mode 100644 index 00000000..75d97f7f --- /dev/null +++ b/tests/test_public_first_multisource_macro_evidence_v05.py @@ -0,0 +1,180 @@ +"""End-to-end public-first 5-series source bundle, network forbidden offline.""" +from __future__ import annotations + +from copy import deepcopy +from datetime import datetime, timezone +from decimal import Decimal +import json +import unittest +from unittest.mock import patch + +from scripts.nbp_reference_capture import capture +from scripts.eurostat_hicp_macro_context_v01 import inspect_hicp +from scripts.public_first_multisource_macro_evidence_v05 import ( + evaluate_combined_sources, observe_once, SOURCE_QUEUE, + MultiSourceBlocked, SCHEMA, HICP_SINCE, HICP_UNTIL, + NBP_START, NBP_END, +) +from tests.test_public_first_prospective_source_observation_v03 import source_fetch +from tests.test_eurostat_hicp_macro_context_v01 import fixture +from tests.test_eurostat_pl_government_bond_yield_context_v04 import original_shape + +AT = datetime(2026, 11, 13, 0, 0, tzinfo=timezone.utc) + + +def months(): + y, m = 2025, 9 + out = [] + for _ in range(13): + out.append(f"{y}-{m:02d}") + y, m = ((y + 1, 1) if m == 12 else (y, m + 1)) + assert out[0] == HICP_SINCE and out[-1] == HICP_UNTIL + return out + + +def data_fixture(): + return fixture( + months=months(), + values=[100 + n * 0.4 for n in range(13)]) + + +def inputs(): + nbp = capture(NBP_START, NBP_END, fetch=source_fetch(), + clock=lambda: AT) + hicp = inspect_hicp( + data_fixture(), since=HICP_SINCE, until=HICP_UNTIL, + retrieval_scope="EUROSTAT_PUBLIC_READ_ONLY") + yield_raw = json.dumps(original_shape()).encode() + return nbp, hicp, yield_raw + + +class MultiSourceMacroEvidenceTests(unittest.TestCase): + def test_five_source_series_and_derived_backward_gap(self): + n, h, y = inputs() + out = evaluate_combined_sources(n, h, y, completed_at=AT) + self.assertEqual(out["schema"], SCHEMA) + self.assertEqual(len(out["series_types"]), 5) + self.assertEqual(out["hicp_months"], 13) + self.assertEqual(out["government_yield_months"], 59) + self.assertEqual(out["joint_reference_month"], "2026-09") + self.assertEqual(out["pl_nominal_10y_government_yield_pct_per_annum"], "4.58") + self.assertEqual(out["pl_trailing_12m_hicp_inflation_pct"], "4.8000") + self.assertEqual(out["arithmetic_yield_minus_trailing_inflation_pp"], "-0.2200") + self.assertFalse(out["historical_point_in_time_certified"]) + self.assertFalse(out["source_rights_fully_approved"]) + self.assertEqual(out["asset_total_return_series_qualified"], []) + self.assertEqual(out["m1_m2_m3"], "INSUFFICIENT_DATA_HOLD_CASH") + self.assertEqual(out["orders_sent"], 0) + self.assertEqual(out["financial_operations"], 0) + self.assertEqual(out["database_writes"], 0) + self.assertNotIn('"observations"', json.dumps(out)) + + def test_source_hashes_linked_to_original_payload(self): + n, h, y = inputs() + out = evaluate_combined_sources(n, h, y, completed_at=AT) + self.assertEqual(len(out["source_fingerprints_sha256"]), 4) + self.assertTrue(all(len(x) == 64 for x in out["source_fingerprints_sha256"].values())) + self.assertRegex(out["receipt_sha256"], r"^[a-f0-9]{64}$") + + def test_exchange_archives_all_licence_blocked(self): + n, h, y = inputs() + out = evaluate_combined_sources(n, h, y, completed_at=AT) + self.assertEqual(len(out["external_exchange_source_queue"]), 4) + self.assertEqual(tuple(x["id"] for x in SOURCE_QUEUE), + tuple(x["id"] for x in out["external_exchange_source_queue"])) + self.assertTrue(all(x["automated_ingestion_authorized"] is False + and x["historic_first_published_proven"] is False + and x["asset_total_return_qualified"] is False + for x in out["external_exchange_source_queue"])) + self.assertTrue(all(x["official_url"].startswith("https://") + for x in out["external_exchange_source_queue"])) + + def test_one_shot_explicit_injected_source_reads_only(self): + def hicp_fetch(url): + self.assertIn("prc_hicp_minr", url) + return data_fixture() + def yield_fetch(): + return json.dumps(original_shape()).encode() + out = observe_once( + nbp_fetch=source_fetch(), hicp_fetch=hicp_fetch, + yield_fetch=yield_fetch, clock=lambda: AT) + self.assertEqual(out["joint_reference_month"], "2026-09") + self.assertEqual(out["financial_operations"], 0) + + def test_no_implicit_fetch_and_naive_clock_fail_before_network(self): + with self.assertRaisesRegex(MultiSourceBlocked, "EXPLICIT"): + observe_once(nbp_fetch=None, hicp_fetch=None, yield_fetch=None) + calls = [] + def deny(*_a, **_k): + calls.append(True) + raise AssertionError("NETWORK_SHOULD_NOT_RUN") + with self.assertRaisesRegex(MultiSourceBlocked, "UTC_AWARE_CLOCK"): + observe_once(nbp_fetch=deny, hicp_fetch=deny, yield_fetch=deny, + clock=lambda: datetime(2026, 11, 13)) + self.assertEqual(calls, []) + + def test_no_future_window(self): + n, h, y = inputs() + with self.assertRaisesRegex(MultiSourceBlocked, "FUTURE"): + evaluate_combined_sources( + n, h, y, + completed_at=datetime(2026, 8, 1, tzinfo=timezone.utc)) + + def test_missing_hicp_month_fails_closed(self): + n, h, y = inputs() + h["observations"][2]["index_2025_100"] = None + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, y, completed_at=AT) + + def test_hicp_date_or_retrieval_scope_fails_closed(self): + n, h, y = inputs() + h["observations"][0]["month"] = "2025-08" + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, y, completed_at=AT) + n, h, y = inputs() + h["retrieval_scope"] = "SECRET_UNTRUSTED_SOURCE" + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, y, completed_at=AT) + + def test_wrong_nbp_source_or_units_rejected(self): + n, h, y = inputs() + n["receipts"]["fx"]["url"] = "https://untrusted.invalid/" + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, y, completed_at=AT) + n, h, y = inputs() + n["series"]["gold"][0]["basis"] = "TOTAL_RETURN" + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, y, completed_at=AT) + + def test_bad_bond_source_country_or_incomplete_months_rejected(self): + n, h, y = inputs() + data = original_shape() + data["dimension"]["geo"]["category"]["index"] = {"DE": 0} + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources( + n, h, json.dumps(data).encode(), completed_at=AT) + data = original_shape() + data["value"].pop("10") + with self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources( + n, h, json.dumps(data).encode(), completed_at=AT) + + def test_invalid_bond_rate_unit_and_nonfinite_rejected(self): + n, h, _ = inputs() + for bad in (float("nan"), 60, True): + data = original_shape() + data["value"]["58"] = bad + with self.subTest(bad=str(bad)), self.assertRaises((ValueError, MultiSourceBlocked)): + evaluate_combined_sources(n, h, json.dumps(data).encode(), + completed_at=AT) + + def test_all_offline_test_execution_denies_sockets(self): + with patch("socket.socket.connect", side_effect=AssertionError( + "UNEXPECTED_NETWORK")): + n, h, y = inputs() + self.assertEqual(evaluate_combined_sources( + n, h, y, completed_at=AT)["account_connections"], 0) + + +if __name__ == "__main__": + unittest.main() From 731e3da03bae3ec4cd4ae47556cef87da59069fd Mon Sep 17 00:00:00 2001 From: Gniewkob Date: Fri, 13 Nov 2026 00:42:23 +0100 Subject: [PATCH 3/3] ci/docs: one integrated original NBP + Eurostat run, exchange licensing matrix and full-regression gates (#154) --- .../full-agent-staging-canary-ci.yml | 4 + ...isl-public-first-prospective-codeql-ci.yml | 2 + .../isl-public-multisource-v05-ci.yml | 90 +++++++++++++++++++ ...IC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md | 45 ++++++++++ 4 files changed, 141 insertions(+) create mode 100644 .github/workflows/isl-public-multisource-v05-ci.yml create mode 100644 docs/ISL_PUBLIC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md diff --git a/.github/workflows/full-agent-staging-canary-ci.yml b/.github/workflows/full-agent-staging-canary-ci.yml index 814ef2dd..44b1ffe1 100644 --- a/.github/workflows/full-agent-staging-canary-ci.yml +++ b/.github/workflows/full-agent-staging-canary-ci.yml @@ -26,6 +26,8 @@ on: - tests/test_public_first_hicp_nbp_purchasing_power_v01.py - scripts/eurostat_pl_*.py - tests/test_eurostat_pl_*.py + - scripts/public_first_multisource_macro_evidence_v05.py + - tests/test_public_first_multisource_macro_evidence_v05.py - scripts/public_first_prospective_source_observation_v03.py - tests/test_public_first_prospective_source_observation_v03.py - scripts/public_first_nbp_reference_research_v01.py @@ -57,6 +59,8 @@ on: - tests/test_public_first_hicp_nbp_purchasing_power_v01.py - scripts/eurostat_pl_*.py - tests/test_eurostat_pl_*.py + - scripts/public_first_multisource_macro_evidence_v05.py + - tests/test_public_first_multisource_macro_evidence_v05.py - scripts/public_first_prospective_source_observation_v03.py - tests/test_public_first_prospective_source_observation_v03.py - scripts/public_first_nbp_reference_research_v01.py diff --git a/.github/workflows/isl-public-first-prospective-codeql-ci.yml b/.github/workflows/isl-public-first-prospective-codeql-ci.yml index e04a8cbc..741da143 100644 --- a/.github/workflows/isl-public-first-prospective-codeql-ci.yml +++ b/.github/workflows/isl-public-first-prospective-codeql-ci.yml @@ -7,6 +7,8 @@ on: paths: - 'scripts/eurostat_pl_*.py' - 'tests/test_eurostat_pl_*.py' + - 'scripts/public_first_multisource_macro_evidence_v05.py' + - 'tests/test_public_first_multisource_macro_evidence_v05.py' - 'scripts/public_first_prospective_source_observation_v03.py' - 'tests/test_public_first_prospective_source_observation_v03.py' - '.github/workflows/isl-public-first-prospective-source-ci.yml' diff --git a/.github/workflows/isl-public-multisource-v05-ci.yml b/.github/workflows/isl-public-multisource-v05-ci.yml new file mode 100644 index 00000000..2e6f6977 --- /dev/null +++ b/.github/workflows/isl-public-multisource-v05-ci.yml @@ -0,0 +1,90 @@ +name: ISL v0.5 public multi-source macro research bundle + +# Source scope: only fixed official NBP+Eurostat HTTPS, four GET total. +# No automation schedule, no paid licence, no user accounts or transactions. +on: + pull_request: + paths: + - 'scripts/public_first_multisource_macro_evidence_v05.py' + - 'tests/test_public_first_multisource_macro_evidence_v05.py' + - 'docs/ISL_PUBLIC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md' + - '.github/workflows/isl-public-multisource-v05-ci.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + offline-multi-source-contract: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15] + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Offline five-series source, PIT and rights regression + run: | + set -euo pipefail + python - <<'PY' + import socket, unittest + def offline(*a, **kw): + raise AssertionError("OFFLINE_TEST_NETWORK_ATTEMPT") + socket.socket.connect = offline + socket.socket.connect_ex = offline + socket.create_connection = offline + tests = unittest.defaultTestLoader.loadTestsFromName( + "tests.test_public_first_multisource_macro_evidence_v05") + result = unittest.TextTestRunner(verbosity=2).run(tests) + if not result.wasSuccessful() or result.skipped or result.testsRun != 12: + raise SystemExit("MULTISOURCE_OFFLINE_ACCEPTANCE_FAILED") + PY + + original-four-endpoint-source-evidence: + runs-on: ubuntu-24.04 + timeout-minutes: 12 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Real public NBP Table A + NBP gold + Eurostat HICP + PL 10Y yield + run: | + set -euo pipefail + python - <<'PY' + import json + from scripts.public_first_multisource_macro_evidence_v05 import ( + observe_once, + ) + from scripts.public_first_nbp_reference_research_v01 import official_fetch + from scripts.eurostat_hicp_macro_context_v01 import _fetch as hicp_fetch + from scripts.eurostat_pl_bond_yield_source_probe_v04 import ( + fetch_official as bond_yield_fetch, + ) + result = observe_once( + nbp_fetch=official_fetch, hicp_fetch=hicp_fetch, + yield_fetch=bond_yield_fetch) + if (not 18 <= result["nbp_observed_business_days"] <= 23 + or result["hicp_months"] != 13 + or result["government_yield_months"] != 59 + or len(result["source_fingerprints_sha256"]) != 4 + or len(result["series_types"]) != 5 + or result["historical_point_in_time_certified"] is not False + or result["source_rights_fully_approved"] is not False + or result["asset_total_return_series_qualified"] != [] + or result["m1_m2_m3"] != "INSUFFICIENT_DATA_HOLD_CASH" + or result["financial_operations"] != 0 + or result["orders_sent"] != 0 + or result["database_writes"] != 0): + raise SystemExit("OFFICIAL_MULTISOURCE_EVIDENCE_BLOCKED") + print("ISL_PUBLIC_MULTISOURCE_CONTEXT=" + + json.dumps(result, sort_keys=True, separators=(",", ":"))) + PY diff --git a/docs/ISL_PUBLIC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md b/docs/ISL_PUBLIC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md new file mode 100644 index 00000000..fc79dac0 --- /dev/null +++ b/docs/ISL_PUBLIC_FIRST_MULTISOURCE_BUNDLE_20261113_PL.md @@ -0,0 +1,45 @@ +# Investment Strategy Lab — public-first v0.5: zintegrowany pakiet 5 serii i giełdowa ścieżka licencji + +**Zakres:** issue [#154](https://github.com/gniewkob/Investment-Strategy-Lab/issues/154). **Cel:** jedna iteracja obejmująca rzeczywiste źródła, analizę, audyt jakości, ryzyka prawne, CI i merge — bez dzielenia na osobne PR-y dla każdego dokumentu. + +## 1. Rzeczywiste źródła publiczne — cztery zapytania w jednym teście + +| Seria | Źródło pierwotne | Dane / jednostka | Status | +|---|---|---|---| +| USD/PLN i EUR/PLN | [NBP Web API, tabela A](https://api.nbp.pl/) | kurs średni PLN/1 waluta, **referencyjny**, nie transakcyjny | Pierwotne API, 1 GET, 2026-09 | +| Złoto NBP | [NBP Web API, ceny złota](https://api.nbp.pl/) | PLN/g, **price-only**, nie całkowita stopa zwrotu | Pierwotne API, 1 GET, 2026-09 | +| Inflacja HICP Polski | [Eurostat prc_hicp_minr](https://ec.europa.eu/eurostat/databrowser/product/view/prc_hicp_minr) | miesięczny indeks I25, 2025=100, 13 miesięcy 2025-09…2026-09 | Pierwotne API, 1 GET, historycznie aktualny stan bazy | +| Rentowność długu PL ~10Y | [Eurostat irt_lt_mcby_m](https://ec.europa.eu/eurostat/databrowser/product/view/irt_lt_mcby_m) | procent rocznie, 59 miesięcy 2021-11…2026-09 | Pierwotne API, 1 GET, **nie zwrot posiadacza obligacji** | + +Oficjalna [dokumentacja API NBP](https://api.nbp.pl/) wyjaśnia 93-dniowy limit i ceny referencyjne. [Eurostat ESMS — interest rates](https://ec.europa.eu/eurostat/cache/metadata/en/irt_lt_mcby_esms.htm), §§ 3.4, 4, 14.1, 17 i 18: rentowność obligacji o terminie ok. 10 lat to statystyka procentowa roczna, z możliwymi rewizjami, nie indeks dochodowy. [Eurostat API](https://ec.europa.eu/eurostat/en/web/user-guides/data-browser/api-data-access/api-introduction): domyślny odczyt najnowszej wersji, nie historycznych kolejnych wersji danych. [Ogólna polityka reuse Eurostat](https://ec.europa.eu/eurostat/help/copyright-notice) wymaga atrybucji i uwzględnienia wyjątków; nie jest blankietowym pozwoleniem na wszystkie zastosowania. + +## 2. Zintegrowana analiza bez transakcji + +Z dwóch pierwotnie zweryfikowanych 2026-09 zestawów danych wyliczamy: + +- **HICP 12m wstecz** = (indeks HICP 2026-09 / indeks HICP 2025-09 − 1) × 100%. +- **Poziom rentowności nominalnej PL ~10Y w 2026-09** = bezpośrednia wartość pierwotnej statystyki Eurostat, jednostka % rocznie. +- **Luka poziomu nominalnej rentowności i przeszłej inflacji, w punktach procentowych** = rentowność roczna – trailing HICP 12m. To wyłącznie *statystyka opisowa*. Nie jest rzeczywistą stopą zwrotu, zyskiem, stopą realną ex ante, prognozą inflacji, ani serią benchmarku TR. +- **NBP FX + złoto** to niezależne źródłowe konteksty z września 2026; nie są automatycznie składnikami portfela, a obecność danych nie jest dowodem prawa do inwestowania. + +Wynik zawiera oryginalne skróty SHA-256 dla czterech odpowiedzi publicznych (2 NBP + 2 Eurostat), UTC odczytu, zakresy, liczbę obserwacji, liczbę serii, zagregowane statystyki oraz jawne statusy NO-PIT/NO-TR. Brak pełnych 59-miesięcznych szeregów w publicznym logu CI. Sam SHA-256 nie poświadcza samodzielnie historycznej godziny pierwszego wydania. + +## 3. Odrębna ścieżka danych giełdowych — źródła istnieją, prawa wymagają przeglądu + +| Kandydat | Co istnieje | Czy zautomatyzowano pobieranie? | Powód | +|---|---|---|---| +| **TBSP.Index** | [Archiwum BondSpot](https://www.bondspot.pl/tbsp_index_archiwum), np. [2026-11-02](https://www.bondspot.pl/tbsp_index_archiwum?date=20261102&month=11&year=2026) pokazuje rzeczywistą wartość otwarcia i zamknięcia; indeks jest administrowany przez GPW Benchmark od 2019-12-01 [BondSpot](https://www.bondspot.pl/tbsp_index_opis) | **NIE** | [BondSpot polityka danych rynkowych](https://www.bondspot.pl/1303) odsyła do deklaracji użytkownika, umowy licencyjnej i cennika. Publiczny HTML to nie potwierdzenie praw do hurtowego pobierania/AI/replikacji | +| **GPW indeksy akcji** | [GPW archiwum z eksportem XLS](https://www.gpw.pl/archiwum-notowan), [GPW Benchmark notowania](https://gpwbenchmark.pl/notowania) | **NIE** | Do sprawdzenia dokładny indeks, waluta, wariant cenowy/doch. TR, prawa automatyzacji i oryginalna historia publikacji | +| **Vanguard All-World NAV** | Pierwotny prywatny XLSX i preflight **już sprawdzone** w [#97](https://github.com/gniewkob/Investment-Strategy-Lab/issues/97) | **NIE (w ramach pakietu)** | Nie kopiujemy prywatnego surowego XLSX do publicznego repo. NAV ≠ giełdowy fill ≠ MSCI ACWI TR | +| **Swinkels obligacje międzynarodowe** | Historyczny oryginalny plik naukowy, licencja **CC BY 4.0**, szczegóły [#123](https://github.com/gniewkob/Investment-Strategy-Lab/issues/123) | **NIE (ponowny import niepotrzebny)** | Niebędący długiem polskim zbiór 2020–2022; nie zastąpi TBSP i nie zapewnia aktualnego PIT | + +**Decyzja wdrożeniowa:** publiczny podgląd wartości indeksu jest dowodem istnienia kanału, nie praw do automatyzacji. Nie korzystamy z nieautoryzowanych mirrorów ani prób omijania rejestracji, limitów i opłat. W rejestrze status takich źródeł to BLOCKED dla kwalifikacji portfelowej, choć publiczne istnienie danych jest zweryfikowane. + +## 4. Odbiór całego pakietu + +- Jedna komenda CLI `python -m scripts.public_first_multisource_macro_evidence_v05 --allow-public-official-fetch` uruchamia **4 oficjalne GET**, bez sekretów, bez zapisów. +- Testy syntetyczne, network-disabled na Ubuntu + macOS, walidują źródła, zakresy, daty, SHA, prawa, 0 operacji. +- Jeden test oryginalnych, realnych źródeł w GitHub Actions + pełna regresja, GitGuardian i CodeQL. +- Nie wolno zamknąć [#116](https://github.com/gniewkob/Investment-Strategy-Lab/issues/116) na podstawie tego pakietu: `PL_BONDS_TR_PLN`, `GLOBAL_EQUITY_TR_PLN` i `GOLD_TR_PLN` nadal `BLOCKED`, a M1–M3 `INSUFFICIENT_DATA_HOLD_CASH`. + +**Zasada:** następne iteracje idą w szerokich, przekrojowych pakietach (źródła + kod + dane + wyniki + odbiór), a nie w poszukiwaniu pojedynczej strony.