"""BondStats Open Research Templates | Educational, reproducible analysis.
Independent research. No investment advice. Inputs are illustrative unless stated otherwise.
https://www.bondstats.org/tools/developer-tools/research-templates/
"""
from math import isfinite

def bond_metrics(face: float, annual_coupon_rate: float, ytm: float, years: int, frequency: int = 2):
    """Annualized yield and coupon as decimals; integer years and coupon frequency.
    Assumes regular periods, settlement on coupon date, no accrued interest or credit risk.
    Returns clean/full price (equal at coupon date), Macaulay and modified duration, convexity.
    """
    if face <= 0 or years <= 0 or frequency not in (1, 2, 4, 12):
        raise ValueError('face and years must be positive; frequency 1,2,4,12')
    if ytm <= -frequency or not all(map(isfinite, (face, annual_coupon_rate, ytm))):
        raise ValueError('invalid input')
    n = years * frequency
    coupon = face * annual_coupon_rate / frequency
    periodic_yield = ytm / frequency
    cashflows = [(coupon + (face if t == n else 0.0)) for t in range(1, n + 1)]
    pvs = [cf / (1 + periodic_yield)**t for t, cf in enumerate(cashflows, 1)]
    price = sum(pvs)
    macaulay = sum((t / frequency) * pv for t, pv in enumerate(pvs, 1)) / price
    modified = macaulay / (1 + periodic_yield)
    convexity = sum(t * (t + 1) * pv for t, pv in enumerate(pvs, 1)) / (price * frequency**2 * (1 + periodic_yield)**2)
    return {'price': price, 'macaulay_duration_years': macaulay,
            'modified_duration_years': modified, 'convexity_years_squared': convexity,
            'dv01_per_100bp_unit': price * modified * 0.0001}

if __name__ == '__main__':
    m = bond_metrics(100, 0.04, 0.045, 5)
    for k, v in m.items(): print(f'{k}: {v:.6f}')
