"""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/
"""
import csv
from pathlib import Path

def read_curve(path):
    with open(path, newline='', encoding='utf-8') as f:
        rows = [(float(r['tenor_years']), float(r['yield_pct']) / 100) for r in csv.DictReader(f)]
    rows.sort()
    if len(rows) < 2 or any(t <= 0 for t, _ in rows) or len({t for t, _ in rows}) != len(rows):
        raise ValueError('Need at least two unique positive tenors')
    return rows

def interpolate(curve, tenor):
    """Piecewise-linear yield interpolation; NO extrapolation outside observed tenors."""
    if not curve[0][0] <= tenor <= curve[-1][0]: raise ValueError('Tenor outside curve')
    for t, y in curve:
        if t == tenor: return y
    for (t0,y0),(t1,y1) in zip(curve,curve[1:]):
        if t0 < tenor < t1: return y0 + (y1-y0)*(tenor-t0)/(t1-t0)
    raise ValueError('Tenor not found')

def scenario(curve, parallel_bp=0, steepener_bp=0, pivot_years=5):
    """Steepener shifts each tenor by (tenor-pivot)*steepener_bp/pivot basis points.
    Positive steepener means short end down and long end up around the pivot.
    """
    if pivot_years <= 0: raise ValueError('pivot_years must be positive')
    return [(t, y + (parallel_bp + steepener_bp*(t-pivot_years)/pivot_years)/10000) for t,y in curve]

if __name__ == '__main__':
    path = Path(__file__).with_name('sample_yield_curve.csv')
    base = read_curve(path)
    stressed = scenario(base, parallel_bp=50, steepener_bp=20)
    print('tenor_years,base_yield_pct,scenario_yield_pct,change_bp')
    for (t,y),(_,ys) in zip(base,stressed):
        print(f'{t:g},{y*100:.3f},{ys*100:.3f},{(ys-y)*10000:.1f}')
    print('Interpolated 7Y base yield:', f'{interpolate(base,7)*100:.3f}%')
