2
\$\begingroup\$

It is said that

Being consistent in percentage annual returns leads to larger amount over time.

That is for any given principal, getting 20% successively for 2 years is better than 30% for first year and 10% for second year even though both percentages add to 40. First case yields 44% while the later one yields 43%.

I have tried to simulate this for arbitrary number of years and arbitrary percentages using Python 3. The obvious added constraint is that sum of percentages is constant (otherwise, inconsistent arbitrarily high percentages will obviously beat consistent low percentages).

from operator import mul
from functools import reduce
import numpy as np
from collections import namedtuple
from typing import List, Tuple

def avg_cagr(percents: List[int]) -> float:
    '''Given (successive) % annual growth rates, returns average Compound Annual Growth Rate'''
    amount = reduce(mul, [1+p/100 for p in percents])
    return (amount**(1/len(percents)) - 1)*100

def dirichlet(n: int = 5, amount: float = 100) -> List[float]:
    '''Returns n random numbers which sum to "amount"'''
    random_returns = np.random.dirichlet(alpha=np.ones(n), size=1)[0]*amount
    return random_returns

def simulate_returns(n: int = 5, amount: float = 100, iterations: int = 40) -> List[Tuple[float, float]]:
    '''Generate bunch of random percentages that sum to a constant and compare average CAGR with their deviation'''
    net_returns = namedtuple('net_returns', 'deviation CAGR')
    results = []
    for _ in range(iterations):
        random_returns = dirichlet(n, amount)
        avg_returns = avg_cagr(random_returns)
        deviation = np.std(random_returns)
        results.append(net_returns(deviation, avg_returns))
    return sorted(results, key=lambda x: x.CAGR, reverse=True)

if __name__ == '__main__':
    results = simulate_returns()
    print('\n'.join(('dev=' + str(round(result.deviation, 2)) + ', ' + 'CAGR=' + str(round(result.CAGR, 2))) for result in results))

Key Ideas in the code:

  • Dirichlet distribution is used to generate bunch of random numbers that sum to a constant
  • Standard deviation is used to illustrate consistency

Result: Simulation agrees with maxim.

\$\endgroup\$
3
\$\begingroup\$

For me the code looks rather okay, I have just a few small remarks:

Formatting

The line

print('\n'.join(('dev=' + str(round(result.deviation, 2)) + ', ' + 'CAGR=' + str(round(result.CAGR, 2))) for result in results))

is very hard to read, could you possibly split it or use a function?

def format_result(result):
    dev = round(result.deviation,2)
    cagr = round(result.CAGR,2)
    return f'dev={str(dev)}, CAGR={str(cagr)}'

and then just

print('\n'.join(format_result(result) for result in results))

Performance

I'm unsure whether you need an extreme performance but it seems that potentially you might want to run the code large number of iterations.

namedtuple

namedtuple datastructure is extremely handy if you want to use it to make your code looks better. However, if you care about performance (i.e. you will be using many of them) you might be better off with usual tuple. Reference: link1, link2, link3.

list.append

In this section I'll be referring to results.append(net_returns(deviation, avg_returns)). append is rather a slow operation if you know a list size before the appending. I think your code would be faster if you allocated results list first and then just put each net return on it's place.

reverse=True

As tempting it is, it might have a performance hit as well. As one might see in source code the list is actually reversed to keep the stability. It should be better just to pass a negative value to your lambda:

return sorted(results, key=lambda x: -x.CAGR)
\$\endgroup\$
  • 1
    \$\begingroup\$ return $'dev={str(dev)}, CAGR={str(cagr)}' should probably be an f-string, so return f'dev={dev}, CAGR={cagr}' (string interpolation already calls str)? \$\endgroup\$ – Graipher Oct 29 '18 at 11:21
  • 1
    \$\begingroup\$ @Graipher, thank you this is exactly what I meant. \$\endgroup\$ – MaLiN2223 Oct 29 '18 at 17:34
0
\$\begingroup\$
def avg_cagr(percents: List[int]) -> float:
    '''Given (successive) % annual growth rates, returns average Compound Annual Growth Rate'''
    amount = reduce(mul, [1+p/100 for p in percents])
    return (amount**(1/len(percents)) - 1)*100

In a production environment you might want to accept a larger variety of input types for percents. E.g. many people use NumPym would this code also work if percents were passed as an array instead of list? You might also want to adopt a Numpy style or Google style code commenting (it is good practice to get used to a standard form of commenting on every class/function your write). To be clearer one thing is that highlighting to the user that percentages are percentage points (3) not decimal (0.03).

def dirichlet(n: int = 5, amount: float = 100) -> List[float]:
    '''Returns n random numbers which sum to "amount"'''
    random_returns = np.random.dirichlet(alpha=np.ones(n), size=1)[0]*amount
    return random_returns

What is the rationale for using the Dirichlet distribiution? Are you aware that if you use alpha=1 you are effectively using a uniform distribution, so are making your code overly complicated and less efficient?

I understand this is a simulation, possibly just a code exercise, but are you aware you can prove these things with simply mathematics, in which case the purpose seems somewhat moot?

Are you trying to solve this problem:

$$ \max_x \quad f(x) = (1+x_1)(1+x_2)..(1+x_n) $$ $$ subject \; to \quad \sum_i x_i = K $$ Using Lagrange multipliers: $$ L(x) = f(x) - \lambda(K-\sum_i x_i) $$ $$ \frac{\partial L}{\partial x_i} = 0 = \frac{\partial f}{\partial x_i} + \lambda \quad \forall \; i \quad \implies x_i = \frac{1}{K}$$ You further examine whether this is indeed the global maximum by showing f(x) is a concave function (or that its logarithm is). And it will hold for any choice of x_i regardless of what distribution you sample from.

\$\endgroup\$
  • \$\begingroup\$ I plan to update the code such that sum of percentages follows a gaussian rather than a constant. That part can only be simulated and can't be proved using simple mathematics. \$\endgroup\$ – kamalbanga Oct 28 '18 at 17:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.