Skip to content

Instantly share code, notes, and snippets.

@YuriyGuts
Created April 11, 2025 12:33
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 a Ukrainian treasury bond from Minfin and presents it as CSV/TSV.
"""
Extracts information about a Ukrainian treasury bond from Minfin and presents it as CSV/TSV.
Usage: uv run bondinfo.py [-h] [--format {tsv,csv}] isin
Example: uv run bondinfo.py UA4000234215
"""
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "requests",
# "bs4",
# ]
# ///
import argparse
from datetime import datetime
import requests
from bs4 import BeautifulSoup
ISIN_URL_TEMPLATE = "https://index.minfin.com.ua/ua/finance/bonds/{isin}/"
USER_AGENT = "Mozilla/5.0"
COUPON_AMOUNT_LABEL = "Купон"
PAYMENT_SCHEDULE_LABEL = "Сплата відсотків"
MATURITY_DATE_LABEL = "Погашення"
SOURCE_DATE_FORMAT = "%d.%m.%Y"
OUTPUT_DATE_FORMAT = "%Y-%m-%d"
def download_isin_webpage(isin: str) -> str:
url = 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_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(COUPON_AMOUNT_LABEL):
coupon_amount = value.replace(",", ".")
elif label.startswith(PAYMENT_SCHEDULE_LABEL):
coupon_dates = [
datetime.strptime(date.strip(), SOURCE_DATE_FORMAT).strftime(OUTPUT_DATE_FORMAT)
for date in dd.stripped_strings
]
elif label.startswith(MATURITY_DATE_LABEL):
parsed_date = datetime.strptime(dd.get_text(strip=True), SOURCE_DATE_FORMAT)
maturity_date = parsed_date.strftime(OUTPUT_DATE_FORMAT)
# Assemble the results.
payouts = []
for date in coupon_dates:
amount = float(coupon_amount)
payouts.append({"date": date, "amount": amount})
# Maturity date: include the principal as a separate payout.
if date == maturity_date:
payouts.append({"date": date, "amount": 1000.0})
return payouts
def main() -> None:
parser = argparse.ArgumentParser(
description="Extract information about a UA treasury bond."
)
parser.add_argument("isin", help="The ISIN to process.")
parser.add_argument(
"--format",
choices=["tsv", "csv"],
default="tsv",
help="Specify the output format. The default is 'tsv'.",
)
args = parser.parse_args()
html = download_isin_webpage(args.isin)
payout_amounts = parse_payout_amounts(html)
for payout in payout_amounts:
if args.format == "tsv":
print(f"{payout['date']}\t{args.isin}\t{payout['amount']:.2f}")
elif args.format == "csv":
print(f"{payout['date']},{args.isin},{payout['amount']:.2f}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment