from pathlib import Path
import json, textwrap
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
from matplotlib.patches import Rectangle
from PIL import Image
R=Path(__file__).resolve().parent;A=R/'assets';A.mkdir(exist_ok=True)
s=json.loads((R/'calculations.json').read_text());bg='#0A0A0A';cream='#F2E9D6';sage='#A8B89C';muted='#A69E92';gold='#D7BC82';blue='#8CA6BC';grid='#33302B'
plt.rcParams.update({'figure.facecolor':bg,'axes.facecolor':bg,'text.color':cream,'axes.labelcolor':cream,'xtick.color':muted,'ytick.color':muted,'axes.edgecolor':grid,'font.family':'DejaVu Sans','font.size':12,'axes.spines.top':False,'axes.spines.right':False,'axes.spines.left':False,'axes.titleweight':'bold','svg.fonttype':'none','savefig.facecolor':bg})
meta=[]
def save(fig,name,title,alt,source,caption,mobile=False):
 suffix='-mobile' if mobile else '';stem=name+suffix
 fig.savefig(A/(stem+'.svg'),metadata={'Creator':'Loop XXI Research','Title':title,'Description':alt,'Date':'2026-09-30'})
 fig.savefig(A/(stem+'.png'),dpi=160,metadata={'Software':'Loop XXI Research','Title':title,'Description':alt})
 with Image.open(A/(stem+'.png')) as im:im.convert('RGB').save(A/(stem+'.webp'),quality=88,method=6)
 meta.append(dict(filename=stem+'.webp',svg=stem+'.svg',png=stem+'.png',alt=alt,title=title,caption=caption,source=source,width=Image.open(A/(stem+'.png')).width,height=Image.open(A/(stem+'.png')).height,bytes=(A/(stem+'.webp')).stat().st_size,mobile=mobile))
 plt.close(fig)
def frame(title,subtitle,source,mobile=False,rows=1):
 fig,axes=plt.subplots(rows,1,figsize=(7,9.2) if mobile else (11,7.7 if rows==1 else 9.6),squeeze=False)
 fig.subplots_adjust(left=.13,right=.94,top=.77 if mobile else .79,bottom=.18 if mobile else .16,hspace=.64 if mobile else .55)
 fig.text(.065,.95,'LOOP XXI  /  RESEARCH',color=sage,fontsize=10,weight='bold')
 fig.text(.065,.898,'\n'.join(textwrap.wrap(title,35 if mobile else 59)),fontsize=20 if mobile else 24,weight='bold',va='top')
 fig.text(.065,.815 if mobile else .835,'\n'.join(textwrap.wrap(subtitle,68 if mobile else 105)),fontsize=10.2,color=muted,va='top')
 fig.text(.065,.045,'\n'.join(textwrap.wrap(source,77 if mobile else 125)),fontsize=9,color=muted,va='bottom')
 return fig,list(axes[:,0])
def clean(ax):ax.grid(axis='y',color=grid,alpha=.8);ax.set_axisbelow(True);ax.tick_params(length=0,pad=8)
for mobile in [False,True]:
 # Chart 1 independent-panel scales visibly labelled.
 source='Sources: Fed/FRED M2SL; NYU Damodaran; S&P TR via Yahoo; WGC; NLS quote via Zorinaq; Coinbase. Calculations: Loop XXI, 30 Sep 2026. M2 is not CPI.'
 fig,axs=frame('Nominal returns, a different denominator','Annualized return (%). Separate panels use different scales and starting periods.',source,mobile,2)
 for ax,rr,labels,lim in [(axs[0],s['historical'][:2],['Gold*','S&P 500 TR'],12),(axs[1],s['historical'][2:3],['Bitcoin†'],220)]:
  yy=np.arange(len(rr));ax.barh(yy+.16,[v['nominal_cagr']*100 for v in rr],height=.28,color=cream,label='Nominal');ax.barh(yy-.16,[v['adjusted_cagr']*100 for v in rr],height=.28,color=sage,label='M2-adjusted')
  ax.set_yticks(yy,labels,fontsize=10);ax.set_xlim(0,lim);ax.set_xlabel('CAGR (% per year)',fontsize=10);ax.grid(axis='x',color=grid);ax.set_axisbelow(True)
  for j,v in enumerate(rr):
   for off,k,c in [(.16,'nominal_cagr',cream),(-.16,'adjusted_cagr',sage)]:ax.text(v[k]*100+lim*.025,j+off,f'{v[k]*100:.2f}%',va='center',fontsize=11,color=c)
  ax.set_xlim(0,lim*1.14)
 axs[0].set_title('31 Dec 1959 → 31 Aug 2026*',fontsize=12,loc='left',pad=15)
 axs[1].set_title('5 Oct 2009 → 30 Sep 2026†',fontsize=12,loc='left',pad=15)
 axs[0].legend(frameon=False,loc='lower right',fontsize=10)
 fig.text(.065,.12,'*1959 gold uses an annual-average proxy.\n†Tiny initial quote; August M2 carried forward to September.',fontsize=9.5,color=muted)
 fig.subplots_adjust(left=.34 if mobile else .29, top=.715 if mobile else .735, bottom=.23)
 save(fig,'01-nominal-vs-m2-cagr','Nominal CAGR versus M2-adjusted CAGR','Two panels: gold 7.57% nominal and 0.76% M2-adjusted; S&P total return 10.57% and 3.57%, end-1959 to August 2026. Bitcoin 197.38% and 180.18%, October 2009 quote to September 2026 price; August M2 carried forward. Panels have different scales.',source,'The periods differ. Bitcoin’s quote-based inception return is not a comparable long-run investment record.',mobile)
 # Chart 2 common start
 p=pd.read_csv(R/'data/annual-asset-levels.csv',parse_dates=['date']);p=p[p.date>='2010-12-31'];first=p.iloc[0]
 source='Sources: NYU Damodaran (annual observations); WGC (Aug 2026 gold); S&P TR via Yahoo (2026); Coin Metrics PriceUSD; Fed/FRED M2SL. Lines join observations. Calculations: Loop XXI.'
 fig,(ax,)=frame('$1 measured against M2 growth','Common period: 31 Dec 2010–31 Aug 2026. Log scale; not consumer purchasing power.',source,mobile)
 for key,label,c in [('gold','Gold',gold),('sp_total','S&P 500 TR',blue),('btc','Bitcoin',sage)]:
  v=(p[key]/first[key])/(p.m2_bn/first.m2_bn);ax.plot(p.date,v,color=c,lw=2.4,marker='o',ms=3,label=f'{label}  ·  {v.iloc[-1]:,.2f}×')
 ax.set_yscale('log');ax.set_ylim(.1,250000);ax.set_yticks([.1,1,10,100,1000,10000,100000]);ax.yaxis.set_major_formatter(FuncFormatter(lambda x,pos:f'{x:,.0f}×' if x>=1 else '0.1×'));ax.axhline(1,color=muted,lw=.8,ls=':');ax.set_ylabel('M2-relative wealth multiple (start = 1)');ax.legend(frameon=False,loc='upper left',fontsize=10);clean(ax)
 save(fig,'02-one-dollar-relative-to-m2','$1 measured against M2 growth','Common-start logarithmic wealth chart from December 2010 to August 2026. After dividing by M2 growth, $1 becomes 1.23 relative units in gold, 3.03 in the S&P 500 total-return series, and 98,935 in Bitcoin. Annual observations omit intrayear drawdowns.',source,'All three lines share the same dates and denominator. Values are M2-relative units, not CPI-adjusted dollars.',mobile)
 # Chart 3
 m=pd.read_csv(R/'data/m2.csv',parse_dates=['observation_date']);fig,(ax,)=frame('The denominator grew 81-fold','U.S. M2 · Jan 1959–Aug 2026 · Seasonally adjusted · Log scale','Source: Board of Governors, H.6 via FRED M2SL; release 22 Sep 2026. Monthly USD billions converted to trillions. Full-span CAGR: 6.73%.',mobile)
 ax.plot(m.observation_date,m.M2SL/1000,color=sage,lw=2.5);ax.set_yscale('log');ax.set_yticks([.25,.5,1,2,5,10,25]);ax.yaxis.set_major_formatter(FuncFormatter(lambda x,p:f'${x:g}T'));ax.set_ylim(.2,40);ax.set_ylabel('U.S. dollars (trillions)');clean(ax)
 ax.annotate('$0.287T',xy=(m.observation_date.iloc[0],m.M2SL.iloc[0]/1000),xytext=(7,13),textcoords='offset points',color=cream,fontsize=11)
 ax.annotate('$23.343T',xy=(m.observation_date.iloc[-1],m.M2SL.iloc[-1]/1000),xytext=(-90,14),textcoords='offset points',color=sage,fontsize=11)
 save(fig,'03-us-m2-1959-2026','U.S. M2 growth from 1959','Logarithmic chart of monthly seasonally adjusted U.S. M2, rising from $286.6 billion in January 1959 to $23,342.8 billion in August 2026: 81.45 times the starting level and 6.73% annualized.', 'Federal Reserve H.6; FRED M2SL','Equal vertical distances indicate equal proportional changes. M2 is broad money, distinct from the monetary base.',mobile)
 # Chart 4
 b=pd.read_csv(R/'data/btc-price.csv',parse_dates=['time']);model=s['model'];origin=pd.Timestamp(model['origin']);now=pd.Timestamp(s['asof']);dates=pd.date_range(b.time.min(),'2050-12-31',freq='MS');t=(dates-origin).days;f=10**model['a']*t**model['b'];hist=dates<=now
 fig,(ax,)=frame('Bitcoin: price and fitted power law','USD per BTC · Daily prices Jul 2010–Sep 2026 · Model extension to Dec 2050','Source: Coin Metrics PriceUSD; Coinbase spot at 21:26 UTC, 30 Sep 2026. Loop XXI equal-weight daily log–log OLS. Shading and dashes = extrapolation, not a forecast. Log price axis.',mobile)
 ax.axvspan(now,pd.Timestamp('2050-12-31'),color=sage,alpha=.07);ax.plot(b.time,b.PriceUSD,color=muted,lw=.8,label='Historical daily price');ax.plot(dates[hist],f[hist],color=sage,lw=2,label='OLS fitted trend');ax.plot(dates[~hist],f[~hist],color=sage,lw=2,ls='--',label='Projected trend')
 ax.scatter([now],[model['actual']],color=cream,s=28,zorder=5);ax.set_yscale('log');ax.set_ylim(.01,1e8);ax.set_yticks([.01,1,100,10000,1e6,1e8]);ax.yaxis.set_major_formatter(FuncFormatter(lambda x,p:'$'+(f'{x/1e6:g}m' if x>=1e6 else f'{x:,.0f}' if x>=1 else f'{x:.2f}')));ax.set_ylabel('Price (USD per BTC, log scale)');clean(ax);ax.legend(frameon=False,loc='lower right',fontsize=9)
 ax.text(.04,.93,'Current trend ≈ $141,972\nCurrent quote ≈ $83,677',transform=ax.transAxes,fontsize=11,color=cream,va='top')
 save(fig,'04-bitcoin-power-law','Bitcoin price and OLS power-law trajectory','Daily Bitcoin prices and an OLS power-law trend fitted to July 2010 through September 2026. Dashed projections reach about $477,300 in 2030 and $18.37 million in 2050. These are unvalidated model extrapolations. Current price is about $83,677 versus a fitted trend of $141,972.', 'Coin Metrics; Coinbase; Loop XXI OLS','The fitted line is a conditional median only under a zero-median log-error assumption. A smooth historical fit does not establish forecast reliability.',mobile)
 # Chart 5
 f=pd.DataFrame(s['forward']);xx=pd.to_datetime(f.target).dt.year
 fig,(ax,)=frame('The model implies declining returns','Bitcoin forward M2-adjusted CAGR · 30 Sep 2026 to each year-end','Source: Loop XXI OLS calculations from Coin Metrics daily price history. Future M2 assumption: 6.727% annually, matching Jan 1959–Aug 2026. Model scenarios only; no guaranteed convergence.',mobile)
 for key,label,c in [('market_adjusted','Current market → future trend',cream),('trend_adjusted','Current trend → future trend',sage)]:
  ax.plot(xx,f[key]*100,color=c,lw=2.6,marker='o',label=label)
  for x,y in zip(xx,f[key]*100):ax.annotate(f'{y:.1f}%',(x,y),xytext=(0,12 if key=='market_adjusted' else -24),textcoords='offset points',ha='center',color=c,fontsize=11)
 ax.set_ylim(0,50);ax.set_xlim(2028,2052);ax.set_xticks([2030,2035,2040,2050]);ax.set_ylabel('M2-adjusted CAGR (% per year)');ax.set_xlabel('Projection endpoint (31 December)');ax.legend(frameon=False,loc='upper right',fontsize=9);clean(ax)
 save(fig,'05-forward-m2-adjusted-cagr','Forward M2-adjusted Bitcoin CAGR','Projected Bitcoin M2-adjusted CAGR declines across the 2030, 2035, 2040 and 2050 endpoints. Trend-to-trend returns are 24.6%, 21.0%, 18.3% and 14.5%; current-market-to-future-trend returns are 41.1%, 28.2%, 22.8% and 17.0%.', 'Coin Metrics; FRED M2SL; Loop XXI calculations','The trend-to-trend line isolates the model’s growth. The market-to-trend line also assumes the current price gap closes.',mobile)
# Original editorial hero; conceptual vector illustration, not a data chart.
fig=plt.figure(figsize=(12,6.3));ax=fig.add_axes([0,0,1,1]);ax.set_xlim(0,1200);ax.set_ylim(0,630);ax.axis('off')
ax.text(62,565,'LOOP XXI  /  RESEARCH',color=sage,fontsize=13,weight='bold')
ax.text(62,475,'The denominator\ncompounds.',fontsize=43,fontfamily='DejaVu Serif',linespacing=1.18,va='top',color=cream)
ax.text(65,254,'ASSET GROWTH  /  MONEY-SUPPLY GROWTH',fontsize=11,color=sage,weight='bold')
ax.text(65,203,'A different measure of\nlong-term performance.',fontsize=19,color=muted,linespacing=1.35,va='top')
for i in range(9):
 size=90+i*30;ax.add_patch(Rectangle((925-size/2,325-size/2),size,size,fill=False,edgecolor=muted,linewidth=.8,alpha=.25+i*.055))
ax.add_patch(Rectangle((880,280),90,90,facecolor=sage,edgecolor=sage))
ax.plot([65,1135],[88,88],color=grid,lw=1);ax.text(65,48,'GOLD  ·  U.S. EQUITIES  ·  LONG-RUN RETURN MATHEMATICS',fontsize=10,color=cream)
ax.text(1135,48,'CONCEPTUAL ILLUSTRATION',fontsize=8,color=muted,ha='right')
save(fig,'hero-the-denominator-compounds','The denominator compounds','A fixed sage square sits inside progressively larger neutral outlines, illustrating an asset measured against an expanding monetary denominator. Dark background with cream typography. Conceptual illustration, not measured data.','Original vector illustration by Loop XXI. No third-party imagery.','An asset can increase in dollar value while gaining little relative to the denominator.')
# Supporting explanatory card, exact hypothetical math.
fig=plt.figure(figsize=(10,7));ax=fig.add_axes([0,0,1,1]);ax.axis('off');ax.text(.06,.91,'LOOP XXI  /  COMPOUNDING',color=sage,fontsize=12,weight='bold');ax.text(.06,.80,'Two growing numbers.\nOne ratio.',fontsize=31,fontfamily='DejaVu Serif',va='top')
for xpos,big,small,c in [(.08,'7%','ASSET CAGR',cream),(.41,'6.7%','M2 CAGR',muted),(.75,'0.28%','ADJUSTED CAGR',sage)]:
 ax.text(xpos,.48,big,fontsize=31,color=c);ax.text(xpos,.41,small,fontsize=10,color=c)
ax.text(.06,.29,'(1.07 ÷ 1.067) − 1 = 0.002812',fontsize=22,color=sage);ax.text(.06,.17,'After 30 years: 7.61× ÷ 7.00× = 1.09×',fontsize=17,color=cream);ax.text(.06,.065,'Illustrative constant-growth example. M2-relative units are not CPI-adjusted dollars.',fontsize=10,color=muted)
save(fig,'support-compounding-ratio','Two growing numbers, one ratio','Hypothetical example: an asset growing 7% annually against M2 growth of 6.7% gains 0.28% annually in M2-relative terms. Over 30 years, nominal wealth grows 7.61-fold, M2 grows 7.00-fold, and the ratio grows 1.09-fold.','Loop XXI calculation; hypothetical example.','Compounding applies to the numerator and denominator.')
(R/'asset-manifest.json').write_text(json.dumps(meta,indent=2))
print('Exported',len(meta),'asset variants in SVG, PNG and WebP')
