Created
April 11, 2025 12:33
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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