Skip to content

Instantly share code, notes, and snippets.

@YuriyGuts
Last active June 26, 2025 19:57
Show Gist options
  • Save YuriyGuts/0d997af3966f72951194ee5576ac9166 to your computer and use it in GitHub Desktop.
Save YuriyGuts/0d997af3966f72951194ee5576ac9166 to your computer and use it in GitHub Desktop.
Extracts information about Ukrainian treasury bonds from the NBU API / Minfin website and presents it as CSV/TSV.
"""
Extracts information about Ukrainian treasury bonds from the NBU API / Minfin
and presents it as CSV/TSV.
usage: bondinfo.py [-h] [--format {tsv,csv}] isin [isin ...]
positional arguments:
isin The ISIN to process. Multiple space-separated values are also accepted.
options:
--format {tsv,csv} Specify the output format. The default is 'tsv'.
Example: uv run bondinfo.py UA4000214506 UA4000215495
"""
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "bs4",
# "requests",
# ]
# ///
import argparse
from datetime import datetime
import requests
from bs4 import BeautifulSoup
NBU_API_URL = "https://bank.gov.ua/depo_securities?json"
MINFIN_ISIN_URL_TEMPLATE = "https://index.minfin.com.ua/ua/finance/bonds/{isin}/"
USER_AGENT = "Mozilla/5.0"
MINFIN_COUPON_AMOUNT_LABEL = "Купон"
MINFIN_PAYMENT_SCHEDULE_LABEL = "Сплата відсотків"
MINFIN_MATURITY_DATE_LABEL = "Погашення"
MINFIN_SOURCE_DATE_FORMAT = "%d.%m.%Y"
MINFIN_OUTPUT_DATE_FORMAT = "%Y-%m-%d"
def download_nbu_bond_catalog(url: str) -> dict:
headers = {"User-Agent": USER_AGENT}
response = requests.get(url, headers=headers)
return response.json()
def download_minfin_isin_webpage(isin: str) -> str:
url = MINFIN_ISIN_URL_TEMPLATE.format(isin=isin.lower())
headers = {"User-Agent": USER_AGENT}
response = requests.get(url, headers=headers)
response.encoding = "utf-8"
return response.text
def parse_minfin_payout_amounts(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
# Find all <dt>/<dd> pairs.
dl = soup.find("dl")
dt_dd_pairs = list(zip(dl.find_all("dt"), dl.find_all("dd")))
coupon_amount = None
coupon_dates = []
maturity_date = None
# Parse the relevant fields.
for dt, dd in dt_dd_pairs:
label = dt.get_text(strip=True)
value = dd.get_text(separator=" ", strip=True)
if label.startswith(MINFIN_COUPON_AMOUNT_LABEL):
coupon_amount = value.replace(",", ".")
elif label.startswith(MINFIN_PAYMENT_SCHEDULE_LABEL):
coupon_dates = [
datetime.strptime(date.strip(), MINFIN_SOURCE_DATE_FORMAT).strftime(
MINFIN_OUTPUT_DATE_FORMAT
)
for date in dd.stripped_strings
]
elif label.startswith(MINFIN_MATURITY_DATE_LABEL):
parsed_date = datetime.strptime(dd.get_text(strip=True), MINFIN_SOURCE_DATE_FORMAT)
maturity_date = parsed_date.strftime(MINFIN_OUTPUT_DATE_FORMAT)
# Assemble the results.
payouts = []
for date in coupon_dates:
amount = float(coupon_amount)
payouts.append({"pay_date": date, "pay_val": amount})
# Maturity date: include the principal as a separate payout.
if date == maturity_date:
payouts.append({"pay_date": date, "pay_val": 1000.0})
return payouts
def main() -> None:
parser = argparse.ArgumentParser(
description="Extract information about a UA treasury bond."
)
parser.add_argument(
"isin",
nargs="+",
help="The ISIN to process. Multiple space-separated values are also accepted.",
)
parser.add_argument(
"--format",
choices=["tsv", "csv"],
default="tsv",
help="Specify the output format. The default is 'tsv'.",
)
args = parser.parse_args()
nbu_catalog_raw = download_nbu_bond_catalog(NBU_API_URL)
catalog = {sec["cpcode"]: sec for sec in nbu_catalog_raw}
for isin in args.isin:
if isin not in catalog:
print(f"Warning: bond {isin} not found in NBU API, falling back to Minfin")
minfin_webpage = download_minfin_isin_webpage(isin)
minfin_payments = parse_minfin_payout_amounts(minfin_webpage)
if not minfin_payments:
print(f"Warning: bond {isin} not found on Minfin either")
catalog[isin] = {"payments": minfin_payments}
for isin in args.isin:
if isin not in catalog:
continue
for payment in catalog[isin]["payments"]:
if args.format == "tsv":
print(f"{payment['pay_date']}\t{isin}\t{payment['pay_val']:.2f}")
elif args.format == "csv":
print(f"{payment['pay_date']},{isin},{payment['pay_val']:.2f}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment