%23%20%2F%2F%2F%20script%0A%23%20requires-python%20%3D%20%22%3E%3D3.12%22%0A%23%20dependencies%20%3D%20%5B%0A%23%20%20%20%20%20%22marimo%3D%3D0.14.13%22%2C%0A%23%20%20%20%20%20%22numpy%3D%3D2.3.0%22%2C%0A%23%20%20%20%20%20%22plotly%3D%3D6.7.0%22%2C%0A%23%20%20%20%20%20%22polars%3D%3D1.44.1%22%2C%0A%23%20%20%20%20%20%22jquantstats%3D%3D0.11.0%22%2C%0A%23%20%20%20%20%20%22cvx-linalg%3E%3D0.9.3%22%2C%0A%23%20%20%20%20%20%22cvxcla%22%0A%23%20%5D%0A%23%0A%23%20%5Btool.uv.sources%5D%0A%23%20cvxcla%20%3D%20%7B%20path%20%3D%20%22..%2F..%2F..%22%2C%20editable%3Dtrue%20%7D%0A%23%0A%23%20%2F%2F%2F%0A%22%22%22Little%20demo%20for%20the%20Critical%20Line%20Algorithm.%22%22%22%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.25.1%22%0Aapp%20%3D%20marimo.App()%0A%0Awith%20app.setup%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20polars%20as%20pl%0A%20%20%20%20from%20jquantstats%20import%20Data%0A%0A%20%20%20%20from%20cvxcla%20import%20CLA%0A%0A%20%20%20%20%23%20Trading%20days%20in%20a%20year%2C%20and%20the%20length%20of%20the%20simulated%20history.%0A%20%20%20%20PERIODS%20%3D%20260%0A%20%20%20%20HISTORY%20%3D%204%20*%20PERIODS%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20The%20Critical%20Line%20Algorithm%0A%20%20%20%20We%20compute%20an%20efficient%20frontier%20using%20the%20critical%20line%20algorithm%20(cla).%0A%20%20%20%20The%20method%20was%20introduced%20by%20Harry%20M%20Markowitz%20in%201956.%0A%0A%20%20%20%20Rather%20than%20invent%20a%20mean%20vector%20and%20a%20covariance%20matrix%20out%20of%20thin%20air%2C%20we%0A%20%20%20%20simulate%20a%20return%20history%2C%20estimate%20both%20from%20it%2C%20and%20trace%20the%20frontier%20of%0A%20%20%20%20the%20estimated%20problem.%20That%20way%20every%20portfolio%20on%20the%20frontier%20has%20a%0A%20%20%20%20*realised*%20return%20series%20too%2C%20which%20we%20hand%20to%0A%20%20%20%20%5BjQuantStats%5D(https%3A%2F%2Fgithub.com%2Fjebel-quant%2Fjquantstats)%20at%20the%20bottom%20of%0A%20%20%20%20this%20notebook.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20slider%20%3D%20mo.ui.slider(4%2C%20100%2C%20step%3D1%2C%20value%3D10%2C%20label%3D%22Size%20of%20the%20problem%22)%0A%20%20%20%20%23%20display%20the%20slider%0A%20%20%20%20slider%0A%20%20%20%20return%20(slider%2C)%0A%0A%0A%40app.function(hide_code%3DTrue)%0Adef%20business_dates(periods)%3A%0A%20%20%20%20%22%22%22Build%20a%20Monday-to-Friday%20date%20index%20of%20the%20given%20length.%22%22%22%0A%20%20%20%20calendar%20%3D%20pl.date_range(pl.date(2015%2C%201%2C%201)%2C%20pl.date(2035%2C%201%2C%201)%2C%20interval%3D%221d%22%2C%20eager%3DTrue)%0A%20%20%20%20return%20calendar.filter(calendar.dt.weekday()%20%3C%3D%205).head(periods)%0A%0A%0A%40app.function(hide_code%3DTrue)%0Adef%20simulate(n%2C%20seed%3D42)%3A%0A%20%20%20%20%22%22%22Simulate%20a%20daily%20return%20history%20for%20n%20assets.%0A%0A%20%20%20%20The%20assets%20differ%20in%20their%20true%20drift%20and%20load%20on%20a%20handful%20of%20common%0A%20%20%20%20factors%2C%20so%20the%20estimated%20problem%20below%20has%20a%20genuinely%20tilted%20frontier%0A%20%20%20%20instead%20of%20one%20driven%20purely%20by%20estimation%20noise.%0A%0A%20%20%20%20Args%3A%0A%20%20%20%20%20%20%20%20n%20(int)%3A%20Number%20of%20assets.%0A%20%20%20%20%20%20%20%20seed%20(int)%3A%20Seed%20for%20the%20random%20generator%2C%20so%20the%20notebook%20is%20reproducible.%0A%0A%20%20%20%20Returns%3A%0A%20%20%20%20%20%20%20%20polars.DataFrame%3A%20A%20frame%20with%20a%20%60%60date%60%60%20column%20and%20one%20return%20column%0A%20%20%20%20%20%20%20%20%20%20%20%20per%20asset%2C%20with%20%60%60HISTORY%60%60%20rows.%0A%20%20%20%20%22%22%22%0A%20%20%20%20rng%20%3D%20np.random.default_rng(seed)%0A%0A%20%20%20%20%23%20True%20annual%20drifts%20spread%20across%20the%20assets%2C%20expressed%20per%20day.%0A%20%20%20%20drift%20%3D%20np.linspace(0.02%2C%200.20%2C%20n)%20%2F%20PERIODS%0A%20%20%20%20%23%20A%20low-rank%20common%20factor%20structure%20plus%20idiosyncratic%20noise.%0A%20%20%20%20k%20%3D%20max(2%2C%20n%20%2F%2F%2010)%0A%20%20%20%20exposures%20%3D%20rng.standard_normal((n%2C%20k))%20*%200.4%0A%20%20%20%20factors%20%3D%20rng.standard_normal((HISTORY%2C%20k))%20*%200.01%0A%20%20%20%20idiosyncratic%20%3D%20rng.standard_normal((HISTORY%2C%20n))%20*%200.01%0A%0A%20%20%20%20returns%20%3D%20drift%20%2B%20factors%20%40%20exposures.T%20%2B%20idiosyncratic%0A%20%20%20%20columns%20%3D%20%5Bf%22asset_%7Bi%3A03d%7D%22%20for%20i%20in%20range(n)%5D%0A%20%20%20%20return%20pl.DataFrame(%7B%22date%22%3A%20business_dates(HISTORY)%2C%20**dict(zip(columns%2C%20returns.T%2C%20strict%3DTrue))%7D)%0A%0A%0A%40app.function(hide_code%3DTrue)%0Adef%20cla(returns)%3A%0A%20%20%20%20%22%22%22Compute%20using%20the%20Critical%20Line%20Algorithm%20(CLA)%20an%20efficient%20frontier.%0A%0A%20%20%20%20The%20mean%20vector%20and%20the%20covariance%20matrix%20are%20the%20sample%20estimates%20taken%0A%20%20%20%20from%20the%20simulated%20return%20history.%20The%20portfolios%20are%20long-only%2C%20capped%20at%0A%20%20%20%20100%25%20per%20name%2C%20and%20fully%20invested.%0A%0A%20%20%20%20Args%3A%0A%20%20%20%20%20%20%20%20returns%20(polars.DataFrame)%3A%20Return%20history%20with%20a%20leading%20%60%60date%60%60%20column.%0A%0A%20%20%20%20Returns%3A%0A%20%20%20%20%20%20%20%20cvxcla.types.Frontier%3A%20The%20efficient%20frontier%20of%20the%20estimated%20problem.%0A%20%20%20%20%22%22%22%0A%20%20%20%20matrix%20%3D%20returns.drop(%22date%22).to_numpy()%0A%20%20%20%20n%20%3D%20matrix.shape%5B1%5D%0A%0A%20%20%20%20return%20CLA(%0A%20%20%20%20%20%20%20%20mean%3Dmatrix.mean(axis%3D0)%2C%0A%20%20%20%20%20%20%20%20covariance%3Dnp.cov(matrix%2C%20rowvar%3DFalse)%2C%0A%20%20%20%20%20%20%20%20lower_bounds%3Dnp.zeros(n)%2C%0A%20%20%20%20%20%20%20%20upper_bounds%3Dnp.ones(n)%2C%0A%20%20%20%20%20%20%20%20a%3Dnp.ones((1%2C%20n))%2C%0A%20%20%20%20%20%20%20%20b%3Dnp.ones(1)%2C%0A%20%20%20%20).frontier%0A%0A%0A%40app.cell%0Adef%20_(slider)%3A%0A%20%20%20%20returns%20%3D%20simulate(slider.value)%0A%20%20%20%20frontier%20%3D%20cla(returns)%0A%20%20%20%20mo.md(f%22The%20frontier%20of%20the%20estimated%20problem%20has%20**%7Blen(frontier)%7D**%20turning%20points.%22)%0A%20%20%20%20return%20frontier%2C%20returns%0A%0A%0A%40app.cell%0Adef%20_(frontier)%3A%0A%20%20%20%20frontier.plot(volatility%3DTrue%2C%20markers%3DTrue)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20From%20weights%20to%20a%20track%20record%0A%0A%20%20%20%20The%20frontier%20is%20a%20set%20of%20weight%20vectors.%20Applied%20to%20the%20return%20history%20they%0A%20%20%20%20generated%2C%20each%20one%20becomes%20a%20return%20series%2C%20and%20a%20return%20series%20is%20what%0A%20%20%20%20jQuantStats%20analyses.%20We%20look%20at%20three%20portfolios%3A%20the%20maximum-Sharpe%20point%0A%20%20%20%20on%20the%20frontier%2C%20the%20minimum-variance%20point%2C%20and%20equal%20weight%20as%20a%0A%20%20%20%20reference.%0A%0A%20%20%20%20These%20are%20*in-sample*%20numbers%20--%20the%20same%20history%20produced%20the%20estimates%20the%0A%20%20%20%20optimiser%20used%20--%20so%20read%20the%20table%20as%20a%20description%20of%20the%20frontier%2C%20not%20as%0A%20%20%20%20a%20backtest.%20%60experiments%2Ffrontier_stats.py%60%20runs%20the%20out-of-sample%20version%0A%20%20%20%20on%20real%20S%26P%20500%20data.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.function(hide_code%3DTrue)%0Adef%20track_records(frontier%2C%20returns)%3A%0A%20%20%20%20%22%22%22Turn%20frontier%20portfolios%20into%20a%20jQuantStats%20%60Data%60%20object.%0A%0A%20%20%20%20Args%3A%0A%20%20%20%20%20%20%20%20frontier%20(cvxcla.types.Frontier)%3A%20The%20traced%20efficient%20frontier.%0A%20%20%20%20%20%20%20%20returns%20(polars.DataFrame)%3A%20The%20return%20history%20the%20frontier%20was%20estimated%20on.%0A%0A%20%20%20%20Returns%3A%0A%20%20%20%20%20%20%20%20jquantstats.Data%3A%20The%20realised%20return%20series%20of%20the%20maximum-Sharpe%2C%20the%0A%20%20%20%20%20%20%20%20%20%20%20%20minimum-variance%20and%20the%20equal-weight%20portfolio.%0A%20%20%20%20%22%22%22%0A%20%20%20%20matrix%20%3D%20returns.drop(%22date%22).to_numpy()%0A%20%20%20%20n%20%3D%20matrix.shape%5B1%5D%0A%0A%20%20%20%20_%2C%20max_sharpe%20%3D%20frontier.max_sharpe%0A%20%20%20%20%23%20Frontier%20order%20runs%20from%20maximum%20return%20towards%20minimum%20variance%2C%20but%20read%0A%20%20%20%20%23%20the%20minimiser%20off%20the%20variance%20vector%20rather%20than%20relying%20on%20that%20order.%0A%20%20%20%20min_variance%20%3D%20frontier.weights%5Bint(np.argmin(frontier.variance))%5D%0A%0A%20%20%20%20portfolios%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22max_sharpe%22%3A%20max_sharpe%2C%0A%20%20%20%20%20%20%20%20%22min_variance%22%3A%20min_variance%2C%0A%20%20%20%20%20%20%20%20%22equal_weight%22%3A%20np.full(n%2C%201.0%20%2F%20n)%2C%0A%20%20%20%20%7D%0A%20%20%20%20series%20%3D%20%7Bname%3A%20matrix%20%40%20weights%20for%20name%2C%20weights%20in%20portfolios.items()%7D%0A%20%20%20%20return%20Data.from_returns(pl.DataFrame(%7B%22date%22%3A%20returns%5B%22date%22%5D%2C%20**series%7D))%0A%0A%0A%40app.cell%0Adef%20_(frontier%2C%20returns)%3A%0A%20%20%20%20data%20%3D%20track_records(frontier%2C%20returns)%0A%20%20%20%20return%20(data%2C)%0A%0A%0A%40app.cell%0Adef%20_(data)%3A%0A%20%20%20%20mo.ui.table(data.stats.summary()%2C%20selection%3DNone)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(data)%3A%0A%20%20%20%20data.plots.returns(title%3D%22Cumulative%20return%20of%20three%20frontier%20portfolios%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
d481d594b8696fa8fe2f36905cf85d44