Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/full-agent-staging-canary-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ on:
- tests/test_shadow_offsite_export_v13.py
- scripts/public_first_hicp_nbp_purchasing_power_v01.py
- tests/test_public_first_hicp_nbp_purchasing_power_v01.py
- scripts/eurostat_pl_*.py
- tests/test_eurostat_pl_*.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
Expand Down Expand Up @@ -53,6 +55,8 @@ on:
- tests/test_shadow_offsite_export_v13.py
- scripts/public_first_hicp_nbp_purchasing_power_v01.py
- tests/test_public_first_hicp_nbp_purchasing_power_v01.py
- scripts/eurostat_pl_*.py
- tests/test_eurostat_pl_*.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
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/isl-eurostat-pl-yield-probe-v04.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: ISL v0.4 original Eurostat PL sovereign yield reference
# Public and official Eurostat, source-typed, no paid APIs, no credentials,
# no financial operations and no automatic schedules.
on:
pull_request:
paths:
- 'scripts/eurostat_pl_bond_yield_source_probe_v04.py'
- 'scripts/eurostat_pl_government_bond_yield_context_v04.py'
- 'tests/test_eurostat_pl_bond_yield_source_probe_v04.py'
- 'tests/test_eurostat_pl_government_bond_yield_context_v04.py'
- 'docs/ISL_EUROSTAT_PL_SOVEREIGN_YIELD_REFERENCE_V04_20261112_PL.md'
- '.github/workflows/isl-eurostat-pl-yield-probe-v04.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
offline-contract:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15]
runs-on: ${{ matrix.os }}
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: No-network original-source source shape and typed yield cases
run: |
set -euo pipefail
python - <<'PY'
import socket, unittest
def deny(*args, **kwargs):
raise RuntimeError("UNEXPECTED_OFFLINE_NETWORK")
socket.socket.connect = deny
socket.socket.connect_ex = deny
socket.create_connection = deny
suite = unittest.TestSuite()
for name in (
"tests.test_eurostat_pl_bond_yield_source_probe_v04",
"tests.test_eurostat_pl_government_bond_yield_context_v04",
):
suite.addTests(unittest.defaultTestLoader.loadTestsFromName(name))
result = unittest.TextTestRunner(verbosity=2).run(suite)
if not result.wasSuccessful() or result.skipped or result.testsRun != 19:
raise SystemExit("EUROSTAT_PL_YIELD_SOURCE_CONTRACT_FAILURE")
PY
official-original-source-evidence:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: One official Eurostat HTTPS GET; print schema + yield aggregates only
run: |
set -euo pipefail
python - <<'PY'
from datetime import datetime, timezone
import json
from scripts.eurostat_pl_bond_yield_source_probe_v04 import (
fetch_official, inspect_source_shape,
)
from scripts.eurostat_pl_government_bond_yield_context_v04 import (
parse_original_yields,
)
raw = fetch_official()
observed_at = datetime.now(timezone.utc)
shape = inspect_source_shape(raw, observed_at=observed_at)
receipt = parse_original_yields(raw, observed_at=observed_at)
if (shape["source_bytes_sha256"] != receipt["original_response_sha256"]
or shape["time_count"] != receipt["observed_months"]
or receipt["observed_months"] != 59
or receipt["historical_pit_certified"] is not False
or receipt["qualified_model_series"] != []
or receipt["orders_sent"] != 0
or receipt["financial_operations"] != 0):
raise SystemExit("ORIGINAL_PL_EUROSTAT_YIELD_NOT_ACCEPTED")
print("ISL_ORIGINAL_PL_YIELD_SOURCE_SHAPE=" +
json.dumps(shape, sort_keys=True, separators=(",", ":")))
print("ISL_ORIGINAL_PL_YIELD_CONTEXT=" +
json.dumps(receipt, sort_keys=True, separators=(",", ":")))
PY
2 changes: 2 additions & 0 deletions .github/workflows/isl-public-first-prospective-codeql-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ name: ISL public-first source CodeQL static security scan
on:
pull_request:
paths:
- 'scripts/eurostat_pl_*.py'
- 'tests/test_eurostat_pl_*.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'
Expand Down
36 changes: 36 additions & 0 deletions docs/ISL_EUROSTAT_PL_SOVEREIGN_YIELD_REFERENCE_V04_20261112_PL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Public-first v0.4 — Polska: rentowności obligacji 10Y (Eurostat)

**Wątek:** [#152](https://github.com/gniewkob/Investment-Strategy-Lab/issues/152), PR #153. **Data:** 12.11.2026. **Koszt dostępu:** 0 PLN, bez kluczy i subskrypcji.

## Oficjalne pierwotne źródło i rozdzielenie twierdzeń

| Twierdzenie lub ograniczenie | Źródło / sekcja | Ocena |
|---|---|---|
| Eurostat publikuje miesięczne rentowności długoterminowych obligacji skarbowych, wyrażone jako **procent rocznie**, o zapadalności ok. 10 lat | [Eurostat: Maastricht criterion interest rates, ESMS](https://ec.europa.eu/eurostat/cache/metadata/en/irt_lt_mcby_esms.htm), sekcje **3.4**, **4**, **5** | Oficjalna metodologia dotycząca rentowności, **nie** holding-period total return |
| Dane MCBY pozyskiwane przez ECB z krajowych banków centralnych, w tym Polski | Ten sam [Eurostat ESMS](https://ec.europa.eu/eurostat/cache/metadata/en/irt_lt_mcby_esms.htm), sekcja **18.1–18.5** | Źródło pochodzenia statystyki, nie identyfikacja obligacji czy kuponów |
| Dane miesięczne publikowane zwykle po ok. 10 dniach | Ten sam ESMS, sekcja **14.1** | Statystyczna typowa aktualność, **nie** weryfikacja historycznego `first_available_at` dla obserwacji |
| Dane mogą być korygowane zgodnie z polityką ECB | Ten sam ESMS, sekcja **17** | Nie traktować dzisiejszych liczb jak niezmiennych oryginalnych publikacji |
| Ogólna możliwość ponownego wykorzystania statystyk Eurostat z podaniem źródła i wyjątkami | [Eurostat: Copyright notice and free re-use of data](https://ec.europa.eu/eurostat/help/copyright-notice), pkt **2–3** | Ogólna polityka reuse, nie aprobata wszystkich celów AI/automatycznego inwestowania ani praw podmiotów trzecich |

**Identyfikator:** `irt_lt_mcby_m`; `freq=M`, `int_rt=MCBY`, `geo=PL`, `time=2021-11...2026-09`. Kod źródła: `scripts/eurostat_pl_bond_yield_source_probe_v04.py` i `scripts/eurostat_pl_government_bond_yield_context_v04.py`. Jawne `--allow-public-eurostat-read`; odpowiedzi oficjalne HTTPS; brak przekierowań, limit 600 KB.

## Wynik pierwszego rzeczywistego odczytu

[Jednorazowy probe original publisher API w PR #153](https://github.com/gniewkob/Investment-Strategy-Lab/actions/runs/43324574226) w dniu 12.11.2026 o 22:57:18 UTC potwierdził rzeczywistą strukturę JSON-stat: `[freq,int_rt,geo,time]`, `[1,1,1,59]`, kody `[M,MCBY,PL]`, `2021-11` do `2026-09`. Original-source SHA-256 odpowiedzi: `acc302b641ca981b5c8e22259a613d092e5c12d0cb01e39e7e093c07c59c3abf`. Provider last update: `2026-11-12T11:00:00+0100`. Pierwszy probe potwierdził 59 etykiet czasu, **nie liczbę niepustych wartości**, które są weryfikowane w osobnym typed-parser CI. Nie uzyskano dowodu historycznych oryginalnych wersji.

## Techniczny zakres

- `parse_original_yields` weryfikuje 59 prawdziwych, kolejnych miesięcy, dokładnie 4 wymiary `freq/int_rt/geo/time`, porządek kategorii JSON-stat, wartości procentowe, ewentualne statusy danych i oryginalne raw SHA-256. Brak obserwacji = BLOKADA, nie zero. Fałszywy kraj, `int_rt`, data w przyszłości lub błędne jednostki = BLOKADA.
- Raport ograniczony do **statystyk poziomów rentowności**, nie publikujemy 59 surowych wartości w logach CI. Min, max i średnia procentowa rentowności oraz największa bezwzględna miesięczna różnica poziomów w punktach bazowych; **nie** traktować jej jako rentowności z inwestycji.
- `historical_pit_certified=false`, `bond_holding_period_return_observed=false`, `tbsp_income_index_observed=false`, `qualified_model_series=[]`, `INSUFFICIENT_DATA_HOLD_CASH`.
- **Bez symulacji kupna/sprzedaży, rekomendacji aktywów, importów baz, połączeń portfela ani auto-schedule.** Przetwarzane są tylko oficjalne publiczne dane w kontrolowanym jednorazowym odczycie, offline testy i wersja źródła.

## Rzeczy pozostające poza zakresem

Rentowność w procentach p.a. to nie zrealizowany zwrot z portfela obligacji; brak cen transakcyjnych, kupowanych ISIN, kuponów, reinwestycji, kosztów i różnych terminów zapadalności. Sam hash dzisiejszej odpowiedzi nie dowodzi historycznego stanu `first_available_at`. Nadal otwarty [P0 #116](https://github.com/gniewkob/Investment-Strategy-Lab/issues/116) dotyczący praw do oryginalnego indeksu TR, source vintages i uprawnień. Ta analiza **nie kwalifikuje** `PL_BONDS_TR_PLN` ani M1–M3.

## Faktycznie opublikowane wartości z oryginalnego odczytu — PASS

Zrealizowany [jednorazowy odczyt oficjalnego Eurostat w CI #43325200200](https://github.com/gniewkob/Investment-Strategy-Lab/actions/runs/43325200200) wykazał **59/59 niepustych miesięcy 2021-11…2026-09**, bez statusów specjalnych (`source_statistical_flags_count={}`). Rzeczywisty SHA-256 pobranych bajtów `acc302b641ca981b5c8e22259a613d092e5c12d0cb01e39e7e093c07c59c3abf`, receipt `4157225ea7cc1f2a5bf9816f9141ab036f098a29f0d5e5f3cfecdda42388b9f6`. Źródło zgłosiło ostatnią aktualizację `2026-11-12T11:00:00+0100`. Zapis **nie** poświadcza dokładnej pierwotnej daty żadnej historycznej obserwacji.

Na oficjalnych liczbach: minimum **3,12% p.a.**, maksimum **7,82% p.a.**, średnia z miesięcznych poziomów **5,6115% p.a.**, największa bezwzględna różnica między poziomami sąsiadujących miesięcy **154 pb** (tj. 1,54 punktu procentowego). Ta różnica **nie jest** stopą zwrotu z obligacji ani maksymalną stratą/zyskiem inwestora. Ani poziom rentowności, ani jego zmiana nie reprezentują realizowalnego indeksu dochodowego.
132 changes: 132 additions & 0 deletions scripts/eurostat_pl_bond_yield_source_probe_v04.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Official Eurostat monthly Polish sovereign convergence-yield source shape probe.

No model data, account access, trades, databases or local files. One-shot
explicit opt-in. Logs *schema only*, never values, yields or series.
"""
from __future__ import annotations

import argparse
from datetime import datetime, timezone
from hashlib import sha256
import json
import re
from scripts.public_first_prospective_source_observation_v03 import _provider_update
from urllib.parse import urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, HTTPSHandler, Request, build_opener

ROOT = "https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data/irt_lt_mcby_m"
ARGS = (
("lang", "EN"), ("freq", "M"), ("int_rt", "MCBY"), ("geo", "PL"),
("sinceTimePeriod", "2021-11"), ("untilTimePeriod", "2026-09"),
)
URL = ROOT + "?" + urlencode(ARGS)
MAX_BYTES = 600_000


class PublicEurostatProbeBlocked(ValueError):
pass


class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, fp, code, msg, headers, newurl):
raise PublicEurostatProbeBlocked("REDIRECT_REJECTED")


def fetch_official() -> bytes:
"""One official allowlisted HTTPS request, never redirect to third party."""
req = Request(URL, headers={"Accept": "application/json",
"User-Agent": "ISL-edu-public-source/1"})
opener = build_opener(NoRedirect(), HTTPSHandler())
with opener.open(req, timeout=25) as response:
if response.status != 200 or response.geturl() != URL:
raise PublicEurostatProbeBlocked("OFFICIAL_RESPONSE_STATUS_OR_URL_CHANGED")
content = response.read(MAX_BYTES + 1)
if not content or len(content) > MAX_BYTES:
raise PublicEurostatProbeBlocked("SOURCE_RESPONSE_OUT_OF_BOUNDS")
return content


def inspect_source_shape(raw: object, *, observed_at: datetime) -> dict:
if type(raw) is not bytes or not raw or len(raw) > MAX_BYTES:
raise PublicEurostatProbeBlocked("SOURCE_RESPONSE_OUT_OF_BOUNDS")
if (not isinstance(observed_at, datetime) or observed_at.tzinfo is None
or observed_at.utcoffset() is None):
raise PublicEurostatProbeBlocked("UTC_AWARE_OBSERVATION_CLOCK_REQUIRED")
try:
data = json.loads(raw)
except (UnicodeError, json.JSONDecodeError) as exc:
raise PublicEurostatProbeBlocked("NOT_OFFICIAL_JSON") from exc
if (not isinstance(data, dict) or data.get("version") != "2.0"
or data.get("class") != "dataset"
or data.get("source") not in ("ESTAT", "Eurostat")
or not isinstance(data.get("id"), list)
or not isinstance(data.get("size"), list)
or not isinstance(data.get("dimension"), dict)):
raise PublicEurostatProbeBlocked("JSONSTAT_STRUCTURE_NOT_MATCHED")
dims = data["dimension"]
names = data["id"]
if (len(names) != 4 or any(type(name) is not str for name in names)
or len(names) != len(set(names))
or set(names) != {"freq", "int_rt", "geo", "time"}
or set(names) != set(dims) or len(data["size"]) != len(names)):
raise PublicEurostatProbeBlocked("DIMENSIONS_INCOHERENT")
codes = {}
for name, count in zip(names, data["size"]):
if (not isinstance(name, str) or not name.isidentifier()
or type(count) is not int or not 1 <= count <= 120
or not isinstance(dims[name], dict)):
raise PublicEurostatProbeBlocked("DIMENSION_INVALID")
cat = dims[name].get("category")
ix = cat.get("index") if isinstance(cat, dict) else None
if (not isinstance(ix, dict) or len(ix) != count
or any(type(v) is not int for v in ix.values())
or set(ix.values()) != set(range(count))
or any(type(k) is not str or not re.fullmatch(
r"20\d{2}-(0[1-9]|1[0-2])" if name == "time"
else r"[A-Z0-9_]{1,16}", k) for k in ix)):
raise PublicEurostatProbeBlocked("CATEGORIES_INVALID")
codes[name] = sorted(ix)
try:
provider_update = _provider_update(
data.get("updated", "NOT_PROVIDED"),
observed_at.astimezone(timezone.utc))
except ValueError as exc:
raise PublicEurostatProbeBlocked("PROVIDER_UPDATED_INVALID") from exc
# No raw data or complete time series is ever printed/returned.
return {
"schema": "ISL_PUBLIC_EUROSTAT_PL_BONDS_YIELD_SHAPE_PROBE_V04",
"source": "Eurostat",
"dataset": "irt_lt_mcby_m",
"query": URL,
"source_bytes_sha256": sha256(raw).hexdigest(),
"observed_by_runner_at_utc": observed_at.astimezone(timezone.utc).isoformat(),
"dimensions": names,
"sizes": data["size"],
"fixed_category_codes": {k: v for k, v in codes.items() if k != "time"},
"time_first": codes["time"][0] if "time" in codes else None,
"time_last": codes["time"][-1] if "time" in codes else None,
"time_count": len(codes.get("time", [])),
"provider_updated_not_first_release": provider_update,
"actual_rate_values_in_output": False,
"historical_pit_certified": False,
"bond_holding_period_total_return": False,
"qualified_model_series": [],
"financial_operations": 0,
"database_writes": 0,
}


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--allow-public-eurostat-read", action="store_true")
args = parser.parse_args()
if not args.allow_public_eurostat_read:
parser.error("explicit --allow-public-eurostat-read required")
result = inspect_source_shape(fetch_official(),
observed_at=datetime.now(timezone.utc))
print("ISL_PUBLIC_EUROSTAT_PL_YIELD_SHAPE=" +
json.dumps(result, separators=(",", ":"), sort_keys=True))


if __name__ == "__main__":
main()
Loading
Loading