{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Homework 7\n",
"\n",
"Projection and portfolio Optimization"
],
"id": "2a1576f4-7e68-4f23-87b2-c615a8ed3133"
},
{
"cell_type": "raw",
"metadata": {
"raw_mimetype": "text/html"
},
"source": [
""
],
"id": "7a71b061-5df0-4eb0-b641-d0b3202605da"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"using LinearAlgebra, DataFrames, Statistics, StatsPlots, YFinance, Dates, CSV"
],
"id": "2"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1 Projection onto a halfspace\n",
"\n",
"A nonzero n-vector $a$ and a scalar $\\beta$ define the halfspace $$\n",
"H^{-}_{a,β}=\\{\\,x∈\\mathbb R^n\\mid ⟨a,x⟩≤β\\,\\}.\n",
"$$\n",
"\n",
"### 1.1 Derive the expression for the projection\n",
"\n",
"Derive an explicit expression for projecting an arbitrary n-vector b\n",
"onto the halfspace $H^-_{a,β}$.\n",
"\n",
"### 1.2 Implement the projection\n",
"\n",
"Implement your derived solution by completing the following function.\n",
"\n",
"``` julia\n",
"\"\"\"\n",
"Project the n-vector `b` onto the halfspace {x ∣ ⟨a,x⟩ ≤ β} defined by the nonzero vector `a` and the scalar `β`.\n",
"\"\"\"\n",
"function proj_halfspace(b, a, β)\n",
" # your code here\n",
"end;\n",
"```\n",
"\n",
"### 1.3 Implement a unit test\n",
"\n",
"Verify numerically with a simple example that your projection code is\n",
"correct. Formal verification is not required.\n",
"\n",
"``` julia\n",
"function test_proj_halfspace()\n",
" a = ...\n",
" β = ...\n",
" b = ...\n",
" expected = ...\n",
" result = proj_halfspace(b, a, β)\n",
" result == expected\n",
"end\n",
"test_proj_halfspace()\n",
"```\n",
"\n",
"## 2 Alternating projection method\n",
"\n",
"The function `projsplx`, defined here, computes the projection of any\n",
"$n$-vector onto the probability simplex\n",
"$\\{x\\in\\mathbb R^n\\mid \\sum_j^nx_j=1,\\ x≥0\\}$."
],
"id": "7b5290f9-6eb9-4e56-916d-e00136496f87"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Projection onto the unit simplex.\n",
"\n",
" projsplx(b) -> x\n",
"\n",
"Variant of `projsplx`.\n",
"\"\"\"\n",
"function proj_simplex(b::Vector; τ=1.0)\n",
" x = copy(b)\n",
" projsplx!(x, τ)\n",
" return x\n",
"end;\n",
"\n",
"\"\"\"\n",
"Projection onto the unit simplex {x | sum(x) = τ, x ≥ 0}.\n",
"\n",
" projsplx!(b, τ)\n",
"\n",
"In-place variant of `projsplx`.\n",
"\"\"\"\n",
"function projsplx!(b::Vector{T}, τ::T) where T\n",
"\n",
" n = length(b)\n",
" bget = false\n",
"\n",
" idx = sortperm(b, rev=true)\n",
" tsum = zero(T)\n",
"\n",
" @inbounds for i = 1:n-1\n",
" tsum += b[idx[i]]\n",
" tmax = (tsum - τ)/i\n",
" if tmax ≥ b[idx[i+1]]\n",
" bget = true\n",
" break\n",
" end\n",
" end\n",
"\n",
" if !bget\n",
" tmax = (tsum + b[idx[n]] - τ) / n\n",
" end\n",
"\n",
" @inbounds for i = 1:n\n",
" b[i] = max(b[i] - tmax, 0)\n",
" end\n",
"end;"
],
"id": "8"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The cell below demonstrates the projection of a vector `b` onto the\n",
"simplex."
],
"id": "f5f2da86-b8a4-48d0-a48e-6ddce5fc92ba"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"output_type": "display_data",
"metadata": {},
"data": {
"text/plain": [
"3-element Vector{Float64}:\n",
" 0.0\n",
" 0.0\n",
" 1.0"
]
}
}
],
"source": [
"let\n",
" b = [12.0, -23.0, 16.0]\n",
" x = proj_simplex(b)\n",
"end"
],
"id": "10"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, suppose we want to compute the projection onto the **intersection**\n",
"of the probability simplex and an arbitrary halfspace. Can we achieve\n",
"this using the projection functions `proj_halfspace` and `proj_simplex`?\n",
"\n",
"Yes, we can use the **[Projection Onto Convex\n",
"Sets](https://en.wikipedia.org/wiki/Projections_onto_convex_sets)**\n",
"algorithm, which projects onto the intersection of convex sets\n",
"$C_1\\bigcap C_2$ by itereratively projecting onto $C_1$ and $C_2$. The\n",
"algorithm is straightforward and involves the following iteration:\n",
"\n",
"$$x_{k+1}=P_1(P_2(x_k)),$$\n",
"\n",
"where $P_1$ and $P_2$ are projection operators onto the sets $C_1$ and\n",
"$C_2$, respectively. Implement this algorithm for generic projection\n",
"operators $P_1$ and $P_2$. Exit the loop when\n",
"$$\\|x_{k+1}-x_k\\|≤ϵ\\|x_0\\|$$ for some positive tolerance $\\epsilon$.\n",
"\n",
"Note that this method can in some situations require many iterations.\n",
"However, since we’re dealing with a small problem, this should not be an\n",
"issue.\n",
"\n",
"### 2.1 Implement the intersection projection\n",
"\n",
"Complete the following function.\n",
"\n",
"``` julia\n",
"\"\"\"\n",
"Project a vector `x` onto the intersection of two convex sets. Takes as input two functions `p1` and `p2` that each take a vector and return the projection of `b0` onto the corresponding sets.\n",
"\"\"\"\n",
"function proj_intersection(x::Vector, p1::Function, p2::Function; maxits=100, ϵ=1e-7)\n",
" err = []\n",
" x0 = copy(x)\n",
" ϵ *= norm(x0, Inf)\n",
" for i ∈ 1:maxits\n",
" # your code here\n",
" push!(err, norm(x-x0, Inf))\n",
" err[end] ≤ ϵ && break\n",
" x0 = copy(x)\n",
" end\n",
" return x\n",
"end;\n",
"```\n",
"\n",
"### 2.2 Implement a unit test\n",
"\n",
"Test your implementation of `proj_intersection` on a small problem that\n",
"aims to find the projection of a vector onto the intersection of the\n",
"unit probability simplex and a halfspace. Verify that the answer is\n",
"correct.\n",
"\n",
"> **Warning**\n",
">\n",
"> Make sure that your test problem is **feasible** – i.e., that the\n",
"> intersection isn’t empty.\n",
"\n",
"## 3 Financial portfolio optimization\n",
"\n",
"[Modern portfolio optimization\n",
"theory](http://en.wikipedia.org/wiki/Modern_portfolio_theory) is based\n",
"on the [Markovitz](http://en.wikipedia.org/wiki/Harry_Markowitz) model\n",
"for determining a portfolio of stocks with a desired expected rate of\n",
"return that has the smallest amount of risk, as measured by the variance\n",
"of the portfolio’s return. The main idea is that by *diversifying*\n",
"(i.e., investing in a mixture of different stocks), one can guard\n",
"against large amounts of variance in the rates of return of the\n",
"individual stocks in the portfolio.\n",
"\n",
"### Rate of return and risk\n",
"\n",
"Suppose that $p_1,\\ldots,p_m$ are the historical prices of a stock over\n",
"some period of time. We define the *rPPate of return* at time $t$,\n",
"relative to the intial price $p_1$ by $$\n",
"r_t := (p_t-p_1)/p_1, \\quad t=1,\\ldots,m.\n",
" \\qquad(1)$$\n",
"\n",
"The *expected rate of return* is the mean $\\mu$ of the rates of return.\n",
"The *risk* of the portfolio is measured as the standard deviation\n",
"$\\sigma$ of the rates of return: $$\n",
"\\mu := \\frac{1}{m}\\sum_{t=1}^m r_t,\n",
"\\quad\\text{and}\\quad\n",
"\\sigma:=\\sqrt{\\frac{1}{m}\\sum_{t=1}^m (r_t-\\mu)^2}.\n",
"$$\n",
"\n",
"Given a collection of $n$ stocks, let $r_t^i$ be the rate of return of\n",
"stock $i$ at time $t$. Let $r$ be the $n$-vector of the expected rates\n",
"of return of the $n$ stocks. In addition, let $\\Sigma$ be the\n",
"$n\\times n$ *covariance matrix* of the rates of return of the $n$\n",
"stocks. Thus, $r_i$ is the mean of the rates of return of stock $i$, the\n",
"$i$th diagonal element $\\Sigma_{ii}$ is its variance, and $\\Sigma_{ij}$\n",
"is the covariance of the rates of return of stocks $i$ and $j$, i.e., $$\n",
"r_i:=\\frac{1}{m}\\sum_{t=1}^m r_t^i\n",
"\\quad\\text{and}\\quad\n",
"\\Sigma_{ij}:=\\frac{1}{m}\\sum_{t=1}^m (r_t^i-r_i)(r_t^j-r_j).\n",
"$$\n",
"\n",
"### Portfolio expected return and risk\n",
"\n",
"We let $x_i$ be the fraction of our investment money we put into stock\n",
"$i$, for $i=1,\\ldots,n$. For the sake of this study, we assume there is\n",
"no *short selling* (i.e., holding a stock in negative quantity.) Thus,\n",
"$x=(x_1,\\ldots,x_n)$ is a vector of length $n$ that has nonnegative\n",
"entries that sum to one (i.e, $x\\ge0$ and $\\sum_{i=1}^n x_i=1$). The\n",
"vector $x$ represents our portfolio of investments. The expected rate of\n",
"return and standard deviation of a portfolio $x$ are then given by $$\n",
"\\mu:= r^T x \\quad\\text{and}\\quad \\sigma:=\\sqrt{x^T\\Sigma x}.\n",
"$$\n",
"\n",
"The code below downloads one year (2022-2023) of historical prices of a\n",
"collection of stocks and implements `meancov`, which computes the\n",
"expected rate of return and covariance matrix of the rates of return."
],
"id": "cded1209-638a-4079-88f7-733278a45d2f"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"stocks = [\n",
" \"AAPL\", \"META\", \"GOOG\", \"MSFT\", \"NVDA\", # technology\n",
" \"AMZN\", \"COST\", \"EBAY\", \"TGT\", \"WMT\", # services\n",
" \"BMO\", \"BNS\", \"HBCP\", \"RY\", \"TD\", # finance\n",
" \"BP\", \"CVX\", \"IMO\", \"TTE\", \"XOM\" # energy\n",
"]\n",
"\n",
"# Calculate dates for one year of data\n",
"end_date = today()\n",
"start_date = end_date - Year(1)\n",
"\n",
"# Create cache directory if it doesn't exist\n",
"cache_dir = joinpath(@__DIR__, \"cache\")\n",
"mkpath(cache_dir)\n",
"\n",
"\"\"\"\n",
" fetch_and_process_stock(symbol::String, start_date::Date, end_date::Date, cache_dir::String) -> DataFrame\n",
"\n",
"Fetch stock data for the given symbol and process it to calculate relative returns from the first day.\n",
"If cached data exists and is recent enough, use that instead of downloading.\n",
"\n",
"Returns a DataFrame with columns `Date` and `relative returns`.\n",
"\"\"\"\n",
"function fetch_and_process_stock(symbol::String, start_date::Date, end_date::Date, cache_dir::String)\n",
" cache_file = joinpath(cache_dir, \"$(symbol)_$(start_date)_$(end_date).csv\")\n",
" \n",
" # Use cache if it exists and is recent (less than 1 day old)\n",
" if isfile(cache_file) && (time() - mtime(cache_file)) < 24*60*60\n",
" df = DataFrame(CSV.File(cache_file))\n",
" return df\n",
" end\n",
" \n",
" # Get daily stock data\n",
" stock_data = get_prices(\n",
" symbol,\n",
" startdt=start_date,\n",
" enddt=end_date,\n",
" interval=\"1d\"\n",
" )\n",
" \n",
" # Convert to DataFrame and process\n",
" df = DataFrame(stock_data)\n",
" \n",
" # Calculate relative returns\n",
" initial_price = first(df.adjclose)\n",
" df[!, symbol] = @. (df.adjclose - initial_price) / initial_price\n",
" \n",
" # Keep only date and relative returns\n",
" select!(df, :timestamp => :Date, symbol)\n",
" \n",
" # Save to cache\n",
" CSV.write(cache_file, df)\n",
" \n",
" return df\n",
"end\n",
"\n",
"# Fetch and process all stocks\n",
"df = mapreduce(\n",
" s -> fetch_and_process_stock(s, start_date, end_date, cache_dir),\n",
" (x, y) -> innerjoin(x, y, on=:Date),\n",
" stocks\n",
")\n",
"\n",
"\"\"\"\n",
" meancov(df) -> Tuple{Vector{Float64}, Matrix{Float64}}\n",
"\n",
"Compute the expected rate of return and covariance matrix from a DataFrame of stock returns.\n",
"\n",
"Returns:\n",
"- r: Vector of expected returns for each stock\n",
"- Σ: Covariance matrix of returns\n",
"\"\"\"\n",
"function meancov(df)\n",
" r = mean.(eachcol(df[:,2:end]))\n",
" Σ = cov(Matrix(df[:,2:end]))\n",
" return r, Σ\n",
"end"
],
"id": "14"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here are the expected rates of return for each stock."
],
"id": "392b0df6-4be6-4aed-b46e-7ca1d2eaa3ff"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"output_type": "display_data",
"metadata": {},
"data": {
"text/html": [
"\n",
""
]
}
}
],
"source": [
"r, Σ = meancov(df)\n",
"df_plot = DataFrame(stock=stocks, r=r, risk=diag(Σ))\n",
"sort!(df_plot, :r)\n",
"\n",
"# Create primary y-axis for returns\n",
"p = @df df_plot bar(:stock, :r, legend=false, ylabel=\"Return\", title=\"Returns and Risk of Stocks\")\n",
"xlabel!(\"Stock\")\n",
"\n",
"# Add secondary y-axis for risk\n",
"p2 = twinx(p)\n",
"@df df_plot scatter!(p2, :stock, :risk, color=:red, markersize=4, \n",
" marker=:circle, label=\"Risk\", legend=:topleft)\n",
"ylabel!(p2, \"Risk (Variance)\")"
],
"id": "16"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **In-class: Stock Analysis - NVDA**\n",
">\n",
"> Let’s examine NVIDIA (NVDA) stock in more detail — it’s been one of\n",
"> the most talked-about technology stocks in recent years because of AI\n",
"> advancements.\n",
">\n",
"> ``` julia\n",
"> # Extract NVDA data\n",
"> nvda_idx = findfirst(s -> s == \"NVDA\", stocks)\n",
"> nvda_returns = df[:, nvda_idx+1] # +1 because first column is Date\n",
">\n",
"> # Calculate rate of return and risk\n",
"> nvda_return = mean(nvda_returns)\n",
"> nvda_risk = var(nvda_returns)\n",
">\n",
"> println(\"NVDA Rate of Return: ...\")\n",
"> println(\"NVDA Risk (Variance): ...\")\n",
"> ```\n",
">\n",
"> > **Clicker Question 1**\n",
"> >\n",
"> > What is the approximate rate of return for NVDA over the past year?\n",
"> >\n",
"> > 1. 33%\n",
"> > 2. 34%\n",
"> > 3. 35%\n",
"> > 4. 36%\n",
"\n",
"### 3.1 Markowitz model\n",
"\n",
"The Markowitz portfolio optimization model finds the allocation of\n",
"investments across $n$ stocks that minimizes risk while achieving a\n",
"target expected return $\\mu$. The model is formulated as: $$\n",
"\\min_{x}\\ \\set{\\tfrac{1}{2} x^T \\Sigma x \\mid r^T x \\ge \\mu,\\ x \\ge 0,\\ \\sum_{i=1}^n x_i = 1}\n",
"$$ Here, $\\Sigma$ is the covariance matrix representing risk, $r$\n",
"contains the expected returns of each stock, and $x$ represents the\n",
"portfolio weights. The constraints ensure the portfolio achieves the\n",
"target return $\\mu$, maintains non-negative weights (no short-selling),\n",
"and fully allocates the investment (weights sum to 1). The objective\n",
"function minimizes portfolio variance subject to these constraints,\n",
"balancing risk and return.\n",
"\n",
"> **In-class: Solving the Markowitz Model with Convex.jl**\n",
">\n",
"> In this exercise, we’ll implement the Markowitz portfolio optimization\n",
"> model using Convex.jl, which provides a domain-specific language for\n",
"> expressing convex optimization problems, and COSMO.jl as our solver.\n",
">\n",
"> First, let’s install and load the necessary packages:\n",
">\n",
"> ``` julia\n",
"> # Install packages if needed\n",
"> # using Pkg\n",
"> # Pkg.add([\"Convex\", \"COSMO\"])\n",
">\n",
"> using Convex, COSMO\n",
"> ```\n",
">\n",
"> Now, let’s formulate the Markowitz portfolio optimization problem\n",
"> using Convex.jl. We’ll create a function that takes in the expected\n",
"> returns `r`, covariance matrix `Σ`, and target return `μ`:\n",
">\n",
"> ``` julia\n",
"> \"\"\"\n",
"> Solve the Markowitz portfolio optimization model using Convex.jl.\n",
">\n",
"> Given a set of assets with expected returns `r` and covariance matrix `Σ`,\n",
"> find the portfolio with minimum risk (variance)\n",
"> that achieves a target return of `μ`.\n",
">\n",
"> Returns the optimal portfolio weights.\n",
"> \"\"\"\n",
"> function markowitz_portfolio(r, Σ, μ)\n",
"> n = length(r)\n",
"> \n",
"> # Define variables\n",
"> x = Variable(n)\n",
"> \n",
"> # Define objective: minimize risk (variance)\n",
"> # Hint: Use quadform() function to represent the quadratic term x'Σx\n",
"> objective = # Your code here\n",
"> \n",
"> # Define constraints\n",
"> constraints = [\n",
"> # 1. Expected return constraint: portfolio return ≥ μ\n",
"> # 2. Budget constraint: sum of weights = 1\n",
"> # 3. No short selling: all weights ≥ 0\n",
"> # Your code here\n",
"> ]\n",
"> \n",
"> # Create problem\n",
"> problem = minimize(objective, constraints)\n",
"> \n",
"> # Solve the problem with a silent optimizer\n",
"> solve!(problem, () -> COSMO.Optimizer(verbose=false), silent=true)\n",
"> \n",
"> # Return optimal portfolio weights\n",
"> return evaluate(x)\n",
"> end;\n",
"> ```\n",
">\n",
"> Use your solution to the previous question to solve the problem for a\n",
"> target return of 10% using our stock data:\n",
">\n",
"> ``` julia\n",
"> # Solve for target return of 10%\n",
"> target_return = 0.10\n",
"> optimal_weights = markowitz_portfolio(r, Σ, target_return)\n",
">\n",
"> # Calculate the achieved return and risk\n",
"> achieved_return = dot(r, optimal_weights)\n",
"> achieved_risk = sqrt(optimal_weights' * Σ * optimal_weights)\n",
">\n",
"> println(\"Optimal portfolio:\")\n",
"> println(\"Target return: $(round(target_return * 100, digits=2))%\")\n",
"> println(\"Achieved return: $(round(achieved_return * 100, digits=2))%\")\n",
"> println(\"Portfolio risk (std dev): $(round(achieved_risk, digits=4))\")\n",
"> ```\n",
">\n",
"> Optimal portfolio:\n",
"> Target return: 10.0%\n",
"> Achieved return: 10.0%\n",
"> Portfolio risk (std dev): 0.0312\n",
">\n",
"> > **Clicker Question 2**\n",
"> >\n",
"> > Which stock has the highest weight in the optimal portfolio?\n",
"> >\n",
"> > 1. NVDA\n",
"> > 2. META\n",
"> > 3. GOOG\n",
"> > 4. TTE\n",
">\n",
"> \n",
"\n",
"### 3.2 Implement a projected gradient minimizer\n",
"\n",
"Complete the implementation of the function below, which minimizes a\n",
"quadratic function $\\frac12 x^T Qx$ (with no or constant terms) over a\n",
"convex set $C$.\n",
"\n",
"``` julia\n",
"\"\"\"\n",
"Projected gradient method for the quadratic optimization problem\n",
"\n",
" minimize_{x} 1/2 x'Qx subj to x ∈ C.\n",
"\n",
"The function `proj(b)` compute the projection of the vector `b` onto the convex set 𝒞.\n",
"\"\"\"\n",
"function pgrad(Q, proj::Function; maxits=100, ϵ=1e-5)\n",
" # your code here\n",
" return x\n",
"end;\n",
"```\n",
"\n",
"### 3.3 Compute a minimum-risk portfolio\n",
"\n",
"Use the function `pgrad` to obtain the minimum-risk portfolio from a set\n",
"of assets with returns `r` and covariances `Σ`. Use `proj_intersection`\n",
"as the input projection function for `pgrad`.\n",
"\n",
"``` julia\n",
"\"\"\"\n",
"Given a set of `n` assets with expected returns `r` and covariance matrix `Σ`, compute the minimum-risk portfolio with expected return `μ`.\n",
"\"\"\"\n",
"function efficient_portfolio(r, Σ, μ)\n",
" n = length(r)\n",
" p1 = # your code here\n",
" p2 = # your code here \n",
" p = b -> proj_intersection(b, p1, p2, maxits=10000, ϵ=1e-8)\n",
" x = pgrad(Σ, p, maxits=1000)\n",
"end;\n",
"```\n",
"\n",
"Here we use the function `efficient_portfolio` to compute an optimal\n",
"portfolio with expected return of 10%. Show in a pie chart the\n",
"allocation of the portfolio among the stocks."
],
"id": "cfa1618d-e08f-4ba3-a2ef-cb0d49f03413"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"output_type": "display_data",
"metadata": {},
"data": {
"text/html": [
"\n",
""
]
}
}
],
"source": [
"xp1 = efficient_portfolio(r, Σ, .1)\n",
"pie(stocks, xp1, title=\"Portfolio Allocation\")"
],
"id": "30"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 3.4 Compute the efficient frontier (optional)\n",
"\n",
"The *efficient frontier* is the set of optimal portfolios that offer the\n",
"minimum risk for a given expected rate of return. Use the function\n",
"`efficient_portfolio` to compute the efficient frontier for expected\n",
"rates of return between 0 and 10%. Plot the objective value (risk) for\n",
"10 values of expected return between 0 and 10%.\n",
"\n",
"``` julia\n",
"# Define target returns\n",
"target_returns = range(0, 0.1, length=10)\n",
"\n",
"# Initialize arrays to store results\n",
"portfolio_risks = Float64[]\n",
"portfolio_returns = Float64[]\n",
"\n",
"# Loop over target returns\n",
"for μ in target_returns\n",
" # your code here\n",
"end\n",
"```\n",
"\n",
"Plot the efficient frontier."
],
"id": "e552fbb3-ab8d-45d6-90fd-371d4dd5854d"
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"output_type": "display_data",
"metadata": {},
"data": {
"text/html": [
"\n",
""
]
}
}
],
"source": [
"plot(portfolio_returns, portfolio_risks, label=\"Efficient Frontier\", \n",
" xlabel=\"Return\", ylabel=\"Risk\", title=\"Portfolio Risk vs Return\")"
],
"id": "34"
}
],
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernel_info": {
"name": "julia"
},
"kernelspec": {
"name": "julia",
"display_name": "Julia",
"language": "julia"
},
"language_info": {
"name": "julia",
"codemirror_mode": "julia",
"version": "1.11.4"
}
}
}