Monte Carlo asset price simulation I'm trying to write a simplistic Monte Carlo simulator to predict asset prices. For example, if something has an initial value of 50 and an historic daily standard deviation of 2, what are the odds it will be 40, 40-50, 50-60 or greater than 60 in 25 days?

import random

start = 50
sd = 2
days = 25
bins = [40,50,60]

iterations = 1000
results = [0] * (len(bins) + 1)

GAUSS = True # distribution can be gaussian or lognormal

for _ in range(iterations):
    mc = start
    for _ in range(days):
        if GAUSS:
            mc += random.gauss(0, sd)
        else:
            if random.randint(0,1):
                mc += random.lognormvariate(0, sd)
            else:
                mc -= random.lognormvariate(0, sd)
        if mc &lt;= 0: # prices can't go below 0
            mc = 0

    for n, bin in enumerate(bins): # bin the iteration
        if mc &lt; bin:
            results[n] += 1
            break
    else:
        results[-1] += 1

final = [100 * r / float(iterations) for r in results]
print(final)
The values at the top of your script are constants so, per the style guide, should be UPPERCASE_WITH_UNDERSCORES:

BINS = [40, 50, 60]
DAYS = 25
GAUSS = True  # distribution can be gaussian or lognormal
ITERATIONS = 1000
START_PRICE = 50
STD_DEV = 2


Note that I've also made some of the names a little more meaningful, and added more Pythonic whitespace to the list. I've also ordered them alphabetically, although there may be a more sensible grouping.

It would be nice to add a comment explaining BINS - the rest are fairly obvious, but it took me a few reads to figure out that they're the top of each bin, and an additional higher bin will be automatically added. Consider adding a module docstring covering this.



Currently, all of the code is just in the body of the script. Instead (as it makes it easier to import and reuse this functionality elsewhere), it is conventional to define an "entry point" function (conventionally named main) and call that when the script is invoked directly:

# imports, constants, etc.

def main():  
    # do your thing

if __name__ == '__main__':
    main()




Rather than specify two distributions within the body of the code, I would extract the distribution function as a parameter to the simulation. In this case the functions would be:

def gaussian(std_dev):
    """Apply the Gaussian distribution."""
    return random.gauss(0, std_dev)

def lognormal(std_dev):
    """Apply the log-normal distribution."""
    return random.choice((1, -1)) * random.lognormvariate(0, std_dev)


Now if you wrote a function to do one iteration, it would be very simple:

def simulate(days, start_price, std_dev, distribution):
    """Simulate the asset for the specified number of days."""
    price = start_price
    for _ in range(days):
        price = max(0, price + distribution(std_dev))
    return price


You can then create more distribution functions in the future, and pass whichever you want to simulate; all it requires is a function that takes a single argument and returns a number.

Note that I have made the constants explicit parameters to these functions - this, again, makes reuse of the code easier. main would now look like:

def main():
    results = [0] * (len(BINS) + 1)
    dist = gaussian if GAUSS else lognormal
    for _ in range(ITERATIONS):
        final_price = simulate(DAYS, START_PRICE, STD_DEV, dist)
        ...
    ...
    print(final_results)  # you could return final_results instead


You could also abstract the binning of results into a function. I would avoid using the identifier bin, as this shadows a built-in function