Skip to content

Introduction

To understand the various outcomes of a stock over the course of a year, it can be useful to simulate that stock's price movements given certain positive and negative business indicators. Without foresight as to what occurs, simulating a random walk can provide insight into what could happen given certain operating circumstances. This project will simulate a random walk for the stock price of a fictional movie theater chain, Cinecity Entertainment, and visualize a subset of sample price movements as well as a distribution of outcomes from a larger size of sample movements.

# numpy and matplotlib imported, seed set
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1)

# Simulate random walk 20 times
all_walks = []
for i in range(20) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +2.5 and -2.5)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 5)/2)))
        # Movements when price is above $30 (price moves randomly between +5 and -5)
        else :
            price = price + (randnum - 5)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)
np_aw_t1 = np.transpose(np.array(all_walks))

# Plot line graph displaying movements, display plot
plt.clf()
plt.plot(np_aw_t1)
plt.axhline(y=50, color='black', linestyle='dashed')
plt.yticks(ticks=[0, 25, 50, 75, 100])
plt.xlabel('Week Number')
plt.ylabel('Stock Price ($)')
plt.title('Visualization of Annual Stock Price Movement Simulations')
plt.show()

# Simulate random walk 1000 times
all_walks = []
for i in range(1000) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +2.5 and -2.5)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 5)/2)))
        # Movements when price is above $30 (price moves randomly between +5 and -5)
        else :
            price = price + (randnum - 5)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)

# Create and select last row from np_aw_t: ends
np_aw_t2 = np.transpose(np.array(all_walks))
ends = np_aw_t2[-1,:]

# Plot histogram of ends, display plot
plt.clf()
plt.hist(ends)
plt.axvline(x=50, color='black', linestyle='dashed')
plt.xticks(ticks=[0, 25, 50, 75, 100, 125])
plt.xlabel('Stock Price ($)')
plt.ylabel('Number of Instances')
plt.title('Distribution of Year End Stock Prices')
plt.show()

# Generate summary statistics
print("Mean - " + str(np.mean(ends)))
print("Median - " + str(np.median(ends)))
print("Standard Deviation - " + str(np.std(ends)))
print("Instances of Stock Price $100 or greater - " + str((np.mean(ends >= 100))*100) + "%")
print("Instances of Bankruptcies - " + str((np.mean(ends == 0))*100) + "%")
print("Instances of Stock Price above $50 starting price - " + str((np.mean(ends > 50))*100) + "%")

Executive Summary

This project simulates the stock price movements of a fictional movie theater chain, Cinecity Entertainment, to determine the probability that the stock price will double within the course of a year. Using a starting stock price of $50, a random walk simulates each week’s closing stock price over the course of a year by applying positive and negative price fluctuations and low probability exogenous shock events. Standard price fluctuations occur based on year-over-year increases/decreases in weekly movie revenues, which can be understood as a function of cinema attendance and thus correlates with company revenues. Due to strong seasonal fluctuations in cinema attendance and the cyclical pattern it creates, using week-over-week revenues would be inappropriate, thus the reason for using year-over-year weekly revenues. The magnitude of these price fluctuations varies based on stock price and thus the delta values are halved for lower stock prices. Two low probability exogenous shock events are included, a so-called black swan event in which the stock price rapidly declines by 90%, and a short squeeze in which the stock price rapidly quadruples and then returns to a value 20% above the initial price (note that the effects of the short squeeze condition do not appear in the movement visualization plot, but are nonetheless included in the final price outcomes). Finally, factoring in the possibility of bankruptcy, any instance in which the stock price reaches $0 will result in that price maintaining for the remaining weeks.

Based on 1,000 simulations of stock price movement, Cinecity’s stock price is at minimum double the starting price after 52 weeks in 2.2% of instances. Furthermore, 1.3% of instances gave the stock price after 52 weeks as $0, which would indicate bankruptcy; this appears to be attributable to the presence of a black swan event in all cases, since removing that condition caused no instances of a $0 stock price. Examining the distribution of year end stock prices, 50% of simulations produced a stock price higher than the starting price, with the remainder being at the starting price or lower. And examining the overall statistics of the final stock prices, the average price was $50.09, which represents a 0.2% increase in the stock price, the median price was $50.00, which represents no increase in the stock price, and with a standard deviation of $23.44, 68% of simulations saw end prices representing gains of up to or losses of no more than 47%. This suggests that while the odds of the stock price doubling over a year are quite low, more modest yet significant gains are plausible, though with a relatively high risk of a move to the downside. Furthermore, while there were no instances of bankruptcy outside of a black swan event, it would be prudent for the company to prepare for the possibility of such event, as the incidence of such event does not necessarily foretell bankruptcy, and also to maintain an overall healthy fiscal picture.

# numpy and matplotlib imported, seed set
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1)

# Simulate random walk 20 times
all_walks = []
for i in range(20) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +3 and -2)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 4)/2)))
        # Movements when price is above $30 (price moves randomly between +6 and -4)
        else :
            price = price + (randnum - 4)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)
np_aw_t1 = np.transpose(np.array(all_walks))

# Plot line graph displaying movements, display plot
plt.clf()
plt.plot(np_aw_t1)
plt.axhline(y=50, color='black', linestyle='dashed')
plt.yticks(ticks=[0, 25, 50, 75, 100, 125, 150, 175])
plt.xlabel('Week Number')
plt.ylabel('Stock Price ($)')
plt.title('Visualization of Annual Stock Price Movement Simulations')
plt.show()

# Simulate random walk 1000 times
all_walks = []
for i in range(1000) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +3 and -2)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 4)/2)))
        # Movements when price is above $30 (price moves randomly between +6 and -4)
        else :
            price = price + (randnum - 4)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)

# Create and select last row from np_aw_t: ends
np_aw_t2 = np.transpose(np.array(all_walks))
ends = np_aw_t2[-1,:]

# Plot histogram of ends, display plot
plt.clf()
plt.hist(ends)
plt.axvline(x=50, color='black', linestyle='dashed')
plt.xticks(ticks=[0, 25, 50, 75, 100, 125, 150, 175])
plt.xlabel('Stock Price ($)')
plt.ylabel('Number of Instances')
plt.title('Distribution of Year End Stock Prices')
plt.show()

# Generate summary statistics
print("Mean - " + str(np.mean(ends)))
print("Median - " + str(np.median(ends)))
print("Standard Deviation - " + str(np.std(ends)))
print("Instances of Stock Price $100 or greater - " + str((np.mean(ends >= 100))*100) + "%")
print("Instances of Bankruptcies - " + str((np.mean(ends == 0))*100) + "%")
print("Instances of Stock Price above $50 starting price - " + str((np.mean(ends > 50))*100) + "%")

The Bull Case

The initial simulations presented above were constructed such that there was even upside potential compared to downside potential. This was done by using the numpy random integer function to call integers randomly between 0 and 10, and add the difference between those integers and 5 to the stock price, such that calling an integer between 0 and 4 would lower the stock price, calling an integer between 6 and 10 would raise the stock price, and calling 5 would keep the stock price steady. With five integers between 0 and 4, and between 6 and 10, the odds of raising or lowering the stock price are even, which is why the mean and median stick close to the starting price of $50.

Altering the price change assignments can be done in such a way as to simulate a stronger business cycle than the neutral scenario presented initially, in this case by using the difference between the random integer and 4. Keeping the price change assignments the same, this has the effect of causing a stock price increase with six integers (5 to 10), but causing a stock price decrease with only four integers (0 to 3), and with the integer 4 keeping the stock price steady. The added weight to the positive side caused much more favorable outcomes, including a doubling of both the mean and median closing price, an incidence of doubled stock prices at 56%, and only 4.5% of simulations not closing above the starting price. Note that despite this strength, there is still a 0.2% rate of bankruptcy, again due exclusively to black swan events.

# numpy and matplotlib imported, seed set
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1)

# Simulate random walk 20 times
all_walks = []
for i in range(20) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +2 and -3)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 6)/2)))
        # Movements when price is above $30 (price moves randomly between +4 and -6)
        else :
            price = price + (randnum - 6)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)
np_aw_t1 = np.transpose(np.array(all_walks))

# Plot line graph displaying movements, display plot
plt.clf()
plt.plot(np_aw_t1)
plt.axhline(y=50, color='black', linestyle='dashed')
plt.yticks(ticks=[0, 10, 20, 30, 40, 50, 60, 70, 80])
plt.xlabel('Week Number')
plt.ylabel('Stock Price ($)')
plt.title('Visualization of Annual Stock Price Movement Simulations')
plt.show()

# Simulate random walk 1000 times
all_walks = []
for i in range(1000) :
    random_walk = [50]
    for x in range(52) :
        price = random_walk[-1]
        # Set randnum to use integers between 0 and 10
        randnum = np.random.randint(0,11)
        # Bankruptcy condition (constant price at $0)
        if price == 0 :
            price = price
        # Movements when price is at or below $30 (price moves randomly between +2 and -3)
        elif price <= 30 :
            price = max(0, (price + ((randnum - 6)/2)))
        # Movements when price is above $30 (price moves randomly between +4 and -6)
        else :
            price = price + (randnum - 6)
        # Black swan event condition (price reduced by 90%)
        if np.random.rand() >= 0.999 :
            price = price * 0.1
        # Short squeeze condition (price quadruples, then reduced by 70%, to ultimately arrive at 20% above initial price)
        if np.random.rand() <= 0.001 :
            price = price * 4
            price = price * 0.3
        random_walk.append(price)
    all_walks.append(random_walk)

# Create and select last row from np_aw_t: ends
np_aw_t2 = np.transpose(np.array(all_walks))
ends = np_aw_t2[-1,:]

# Plot histogram of ends, display plot
plt.clf()
plt.hist(ends)
plt.axvline(x=50, color='black', linestyle='dashed')
plt.xticks(ticks=[0, 10, 20, 30, 40, 50, 60, 70])
plt.xlabel('Stock Price ($)')
plt.ylabel('Number of Instances')
plt.title('Distribution of Year End Stock Prices')
plt.show()

# Generate summary statistics
print("Mean - " + str(np.mean(ends)))
print("Median - " + str(np.median(ends)))
print("Standard Deviation - " + str(np.std(ends)))
print("Instances of Stock Price $100 or greater - " + str((np.mean(ends >= 100))*100) + "%")
print("Instances of Bankruptcies - " + str((np.mean(ends == 0))*100) + "%")
print("Instances of Stock Price above $50 starting price - " + str((np.mean(ends > 50))*100) + "%")

The Bear Case

Just as one can alter the price change assignments to simulate a strong business cycle, so too can one alter them to simulate a weak business cycle. This set of simulations applied the same alterations as above, except by using the difference between the random integer and 6. This had the effect of reversing the weights, with a set of six integers (0 to 5) causing a decrease in the stock price, and a set of four integers (7 to 10) causing an increase in the stock price, and the integer 6 causing the stock price to hold steady. Applying this was enough to cause a significantly worse outcome, with the mean and median being reduced to a quarter of their values compared to the neutral weight scenario ($13.74 and $13 respectively), absolutely no cases of the stock price doubling, and only 1.6% of incidences resulting in the closing price being above the starting price. Worse still, the incidence of bankruptcy in this simulation is a relatively high 18.7%, and even with removing the black swan condition, the bankruptcy rate remains at 14.1%. This is an illustrative example of how a weak business cycle can cause business failures.

A note about the Random Walk

If one wishes to alter any random walk model, it is important to only alter those items that would not change the random walk itself. These include the random seed as well as the range of numbers called in a random integer function. Changing the range boundaries in any way, even if keeping the range the same, causes a different sequence of numbers to be called, and changes the outcomes and any conditions based on a random float. As can be seen by comparing the three movement visualizations above, keeping these elements constant allowed for the same black swan event incidences in the same simulations, whereas changing the range and keeping the price change assignments constant caused these simulations to not show the same black swan events.

Open the video in a new tab