Random Walks in Practice

Module 9.5 · Three companies, and not one of them is any different

TipToday’s question

You buy one share at $10. You’ll sell if it climbs to $13, and you’ll sell if it falls to $8. If neither happens in a year, you’re still holding it. What’s the chance you sell at your target? Write down a guess. The target is only $1 farther away than the stop, so does that matter much?

NoteReading and code

This practices textbook Module 9.5, “Random Walk,” with a new problem. It uses the same steps as Project 4, so you can use today’s code as a guide. The loops are also good practice for Exam 2. Download the complete R script after coding along.

A toy stock market

Here is our whole model of a stock: each trading day, the price moves by a random amount between 20 cents down and 20 cents up.

runif(1, -0.2, 0.2) picks that amount. In Module 9.5, each step was a coin flip with two possible results. Here, any amount in the range can happen, but the idea is the same: new position = old position + random step.

This isn’t how real stocks behave. Real prices move by percentages, jump on news, and depend on what other people do. It’s a random walk wearing a stock market costume, and that’s enough to teach us something.

Watch three companies

Start three companies at $10, with exactly the same rule. This code draws the graph inside the loop, so it redraws every day and the lines grow as you watch. Sys.sleep() pauses briefly between days. Run it in Positron, then run it again.

days <- 250   # about one year of trading days
x <- 1:days
y1 <- numeric(days)
y2 <- numeric(days)
y3 <- numeric(days)
y1[1] <- 10
y2[1] <- 10
y3[1] <- 10

for (i in 2:days) {
  y1[i] <- y1[i - 1] + runif(1, -0.2, 0.2)
  y2[i] <- y2[i - 1] + runif(1, -0.2, 0.2)
  y3[i] <- y3[i - 1] + runif(1, -0.2, 0.2)

  plot(x[1:i], y1[1:i], type = "l", col = "red",
       xlim = c(1, days), ylim = c(4, 16),
       xlab = "Trading day", ylab = "Price ($)")
  lines(x[1:i], y2[1:i], col = "blue")
  lines(x[1:i], y3[1:i], col = "darkgreen")

  Sys.sleep(0.05)
}

A web page can’t animate, so here is how one year turned out, with the graph drawn once at the end:

set.seed(10)
days <- 250
x <- 1:days
y1 <- numeric(days)
y2 <- numeric(days)
y3 <- numeric(days)
y1[1] <- 10
y2[1] <- 10
y3[1] <- 10

for (i in 2:days) {
  y1[i] <- y1[i - 1] + runif(1, -0.2, 0.2)
  y2[i] <- y2[i - 1] + runif(1, -0.2, 0.2)
  y3[i] <- y3[i - 1] + runif(1, -0.2, 0.2)
}

plot(x, y1, type = "l", col = "red", ylim = c(4, 16),
     xlab = "Trading day", ylab = "Price ($)",
     main = "Three companies, one rule")
lines(x, y2, col = "blue")
lines(x, y3, col = "darkgreen")
abline(h = 10, lty = 3, col = "gray50")

Which company would you say is the best run? If you read a news story about the winner, what would it say? Nothing is different about these companies. The rule is identical, and the winner just got the luckier draws. Random walks make patterns that look like trends and stories.

Add a rule for selling

Now hold one share, and add our rule: sell at $13 or at $8. This is the same setup as Project 4, in different clothes:

Lost hiker Toy stock
Position y, starting at 0 Price, starting at $10
The road, 20 steps north The target, $3 above the start
The river, 10 steps south The stop, $2 below the start
240 minutes of daylight 250 trading days
Ending: "road", "river", or "lost" Ending: "target", "stop", or "holding"

Before running anything: what’s the fewest number of days it could take to reach the target? The stop?

The price moves at most 20 cents a day. Reaching the target takes at least $3 ÷ $0.20 = 15 days, and reaching the stop takes at least $2 ÷ $0.20 = 10 days.

Once the price reaches either line, the simulation should stop. break leaves a loop immediately, and after a break, i still holds the pass the loop stopped on. That’s how we know how many days we held the share.

set.seed(15)
target <- 13
stop_price <- 8
days <- 250

price <- numeric(days + 1)
price[1] <- 10
outcome <- "holding"

for (i in 2:(days + 1)) {
  price[i] <- price[i - 1] + runif(1, -0.2, 0.2)
  if (price[i] >= target) {
    outcome <- "target"
    break
  }
  if (price[i] <= stop_price) {
    outcome <- "stop"
    break
  }
}

days_held <- i - 1
outcome
[1] "target"
days_held
[1] 85

The vector has room for every day, but only the entries up to i were used, so plot 1:i:

plot(1:i, price[1:i], type = "l", ylim = c(7, 14),
     xlab = "Day (1 is the day we bought)", ylab = "Price ($)",
     main = "One share with a sell rule")
abline(h = target, col = "darkgreen", lwd = 2)
abline(h = stop_price, col = "red", lwd = 2)

Change the seed and run it a few times. One run is one story. It can’t tell us the chance of hitting the target, just as one hike can’t tell you the chance of reaching the road.

Simulate many shares

We’ve seen this pattern in every Monte Carlo problem so far: run one experiment per pass through the loop, and store one result per run. This is a loop inside a loop. The outer loop picks a share, and the inner loop walks through its days.

We only need each share’s ending, so price can be a single number that we keep updating. There’s no need to keep every day.

set.seed(2)
shares <- 1000
outcome <- character(shares)

for (share in 1:shares) {
  price <- 10
  outcome[share] <- "holding"
  for (day in 1:days) {
    price <- price + runif(1, -0.2, 0.2)
    if (price >= target) {
      outcome[share] <- "target"
      break
    }
    if (price <= stop_price) {
      outcome[share] <- "stop"
      break
    }
  }
}

c(target = mean(outcome == "target"),
  stop = mean(outcome == "stop"),
  holding = mean(outcome == "holding"))
 target    stop holding 
  0.111   0.254   0.635 

mean(outcome == "target") is the same trick as before: the comparison gives TRUEs and FALSEs, and their mean is the fraction that are TRUE. The three fractions add to 1.

How does this compare with your guess? The stop is hit far more often than the target, even though the walk has no preference for up or down. Why do you think that is?

How much should we trust the estimate?

Change the seed from 2 to 3 and run the loop again. On the page, here’s that second run:

 target    stop holding 
  0.098   0.274   0.628 

Which digits changed? With 1,000 shares, “about 10%” is fair, and “11.1%” claims more than we know.

If we have time: a tiny edge

Suppose the company is very slightly better than a fair walk. Change the top of the range from 20 cents to 21 cents, so the price drifts up by an average of half a cent a day:

set.seed(2)
outcome <- character(shares)

for (share in 1:shares) {
  price <- 10
  outcome[share] <- "holding"
  for (day in 1:days) {
    price <- price + runif(1, -0.2, 0.21)
    if (price >= target) {
      outcome[share] <- "target"
      break
    }
    if (price <= stop_price) {
      outcome[share] <- "stop"
      break
    }
  }
}

c(target = mean(outcome == "target"),
  stop = mean(outcome == "stop"),
  holding = mean(outcome == "holding"))
 target    stop holding 
  0.277   0.105   0.618 

Would you notice half a cent on any one day? Probably not, but look at what it does over a year. Small differences in a rule can add up to large differences in the result.

ImportantWhat this practiced
  • New position = old position + random step, whether the walker is a hiker or a price.
  • break ends a run early. After it, the loop counter tells you when the run stopped.
  • One run is a story; many runs give an estimate. Store one result per run, then summarize with mean().
  • Estimates vary. Change the seed to see how many digits you can trust.
  • For Project 4: today’s first two loops match your Sections 2 and 3. The exact check and the sweep over p_north are still yours to work out.
Back to top