library(dplyr) # mutate(), tibble(), if_else(), group_by(), summarize()
library(purrr) # pmap()
library(tidyr) # unnest()
= 10000
num_sim
# Create a function to get yearly return (both average and compounded)
<- function(mu, sigma, num_year) {
create_simul_ret = tibble(year_num = 1:num_year,
df ret = rnorm(num_year, mean = mu, sd = sigma)) |>
mutate(ann_ret = 1 + ret,
cum_ret = cumsum(ann_ret) / year_num, # Function for mean annualized returns
compounded_ret = cumprod(ann_ret)^(1/year_num), # Function to get compounded returns
posit_cum_ret = if_else(cum_ret > 1, 1, 0),
posit_comp_ret = if_else(compounded_ret > 1, 1, 0))
return(df)
}
# To get all different random return numbers, I need to fetch the data from the row
<- tibble(sim_num = 1:num_sim, mu = 0.03, sigma = 0.1, num_year = 30) |>
df mutate(sim_ret = pmap(list(mu, sigma, num_year), create_simul_ret)) |>
unnest(cols = c(sim_ret)) |> # large df with 30 rows/year per simulation
group_by(year_num) |> # group by year to calculate average returns
summarize(avg_num_pos_ret = mean(posit_cum_ret),
avg_num_comp_pos_ret = mean(posit_comp_ret))
This is attempt to start a series of puzzles for quant. The source for these puzzles is this noteworthy substack on quant finance
Puzzle 1
An investment strategy has an annualized expected return of 3% and an annualized volatility of 10%. Assume returns are normally distributed and independent from one year to the next. After how many years will the mean annualized returns of a portfolio be positive with at least 91.3% probability? What about the compounded returns?
Our previous post on the random behavior of assets is a good start.
To calculate the mean annualized returns, we will use \[Ret = \frac {\sum_{i=1}^{n} R_i}{n}\]
- \(R_i\) is the annual return for the year i
- \(n\) is the number of year.
Solving using R
Using the mean annualized returns
year_num | avg_num_pos_ret | avg_num_comp_pos_ret |
---|---|---|
1 | 0.6091 | 0.6091 |
2 | 0.6605 | 0.6475 |
3 | 0.6962 | 0.6759 |
4 | 0.7210 | 0.6948 |
5 | 0.7429 | 0.7126 |
6 | 0.7660 | 0.7332 |
7 | 0.7830 | 0.7481 |
8 | 0.7982 | 0.7581 |
9 | 0.8114 | 0.7733 |
10 | 0.8254 | 0.7828 |
11 | 0.8374 | 0.7974 |
12 | 0.8489 | 0.8077 |
13 | 0.8613 | 0.8188 |
14 | 0.8680 | 0.8257 |
15 | 0.8742 | 0.8334 |
16 | 0.8803 | 0.8410 |
17 | 0.8896 | 0.8484 |
18 | 0.8950 | 0.8540 |
19 | 0.8990 | 0.8601 |
20 | 0.9083 | 0.8662 |
21 | 0.9109 | 0.8700 |
22 | 0.9156 | 0.8758 |
23 | 0.9201 | 0.8809 |
24 | 0.9295 | 0.8872 |
25 | 0.9325 | 0.8939 |
26 | 0.9355 | 0.8997 |
27 | 0.9400 | 0.9036 |
28 | 0.9431 | 0.9062 |
29 | 0.9447 | 0.9110 |
30 | 0.9464 | 0.9136 |
Solving Using Python
import random
#Computes a moving average from a gaussian sample over n trials
def gauss_moving_average(mu,sigma,n):
= []
moving_average_list for i in range(n):
if i == 0:
moving_average_list.append(random.gauss(mu,sigma))else:
-1]*i+random.gauss(mu,sigma))/(i+1))
moving_average_list.append((moving_average_list[ireturn moving_average_list
#Outputs a list of guassian moving average time series
def run_sims(mu, sigma, n, years):
= []
sims for i in range(n):
sims.append(gauss_moving_average(mu,sigma,years))return sims
#Outputs the percentage of positive entries in a column of a matrix
def percent_positive_column(matrix,column):
= len(matrix)
n = 0
percent_positive for i in range(n):
if matrix[i][column] > 0:
+=(1/n)
percent_positivereturn percent_positive*100
#Runs the simulation and checks for our confidence level
= run_sims(3,10,100000, 100)
matrix for i in range(100):
= percent_positive_column(matrix, i)
conf print(conf)
if conf >= 91.3:
print("Year",i+1, "has positive returns with probability", conf)
break
61.93999999998159
66.66699999996007
70.11999999994435
72.92899999993158
75.1649999999214
77.24099999991195
79.06299999990367
80.68199999989629
81.95799999989049
83.17599999988494
84.27499999987994
85.2739999998754
86.20499999987116
87.1399999998669
87.92999999986331
88.63799999986009
89.30399999985706
90.13199999985329
90.65399999985091
91.18599999984849
91.68499999984621
Year 21 has positive returns with probability 91.68499999984621