"""Independent Decimal/iterative audit of Article 10 outputs and raw observations.""" import csv,datetime as dt,gzip,hashlib,json,math,statistics from decimal import Decimal as D,localcontext,ROUND_HALF_EVEN from pathlib import Path ROOT=Path(__file__).resolve().parents[1];DATA=ROOT/'articles/data/dividends' def audit(result=None): r=result or json.loads((DATA/'results.json').read_text());manifest=json.loads((DATA/'sources.json').read_text());checks=0 def close(a,b,tol=1e-9): nonlocal checks if not math.isclose(float(a),float(b),rel_tol=tol,abs_tol=tol):raise ValueError(f'Numeric audit mismatch: {a} / {b}') checks+=1 with localcontext() as ctx: ctx.prec=45 for s in ('VYM','SPY'): name=f'raw/{s}.json.gz';raw=(DATA/name).read_bytes() if hashlib.sha256(raw).hexdigest()!=manifest['input_sha256'][name]:raise ValueError('Checksum mismatch') o=json.loads(gzip.decompress(raw))['chart']['result'][0];dates=[dt.datetime.fromtimestamp(t,dt.timezone.utc).date().isoformat() for t in o['timestamp']];q=o['indicators']['quote'][0]['close'];adj=o['indicators']['adjclose'][0]['adjclose'] points={d:(D(str(p)),D(str(a))) for d,p,a in zip(dates,q,adj) if p is not None and a is not None};div={dt.datetime.fromtimestamp(e['date'],dt.timezone.utc).date().isoformat():D(str(e['amount'])) for e in o['events']['dividends'].values()} factor=D(1);start=next(iter(points));prev=points[start][0];maxerror=0 for date,(price,adjusted) in list(points.items())[1:]: factor*=1-div.get(date,D(0))/prev;relative=adjusted/price/(points[start][1]/points[start][0]);maxerror=max(maxerror,abs(float(relative*factor-1)));prev=price;checks+=1 if maxerror>1e-5:raise ValueError('Dividend reconstruction mismatch') for p in [p for p in r['periods'] if p['asset']==s]: rows=[(d,v) for d,v in points.items() if p['start']<=d<=p['end']];a,b=rows[0][1],rows[-1][1] if rows[0][0]!=p['start'] or rows[-1][0]!=p['end'] or len(rows)!=p['observations']:raise ValueError('Period mismatch') growth=b[1]/a[1];close(p['total_return'],growth-1);close(p['price_return'],b[0]/a[0]-1);close(p['cagr'],growth**(D(1)/p['years'])-1) # Brute prefix maxima independently of the production running-peak algorithm. values=[v[1] for _,v in rows];dds=[v/max(values[:i+1])-1 for i,v in enumerate(values)];worst=min(dds);idx=dds.index(worst);peak=values[:idx+1].index(max(values[:idx+1]));close(p['mdd']['value'],worst) if (p['mdd']['peak'],p['mdd']['trough'])!=(rows[peak][0],rows[idx][0]):raise ValueError('Drawdown dates differ') months={d[:7]:v[1] for d,v in rows};vs=list(months.values());returns=[vs[i]/vs[i-1]-1 for i in range(1,len(vs))];mean=sum(returns)/len(returns);variance=sum((v-mean)**2 for v in returns)/(len(returns)-1);close(p['annualized_volatility'],variance.sqrt()*D(12).sqrt()) distributions=sum(v for d,v in div.items() if '2025-09-30'