Quant Terminal
C1-01C1·intro·~22 min

NumPy through computing returns

pythonnumpyreturnsvectorization

▸ Pretest — guess, even if you don't know

If a stock closes at $100 on day 1 and $102 on day 2, what's the 'return' from day 1 to day 2?

Why NumPy

You already know Python loops. Why do quants live in NumPy?

Because NumPy is vectorized — it applies one operation to a whole array at once, in compiled C code, instead of looping element by element in the Python interpreter. A Python for loop over 10 million prices takes seconds. The equivalent NumPy operation takes milliseconds. The difference compounds across thousands of strategy backtests — a backtest being a simulation of a trading strategy on historical data.

The other reason: most quantitative code is expressed naturally in array operations — applying an operation to a vector of prices, computing moments (summary numbers of a distribution, like the mean and the spread), fitting models. Loops obscure the math; NumPy mirrors it.

Returns — three conventions

A return — the percentage change in a price from one period to the next — is the basic unit quants work with. It puts a cheap stock and an expensive stock on the same footing.

Write the price series as P0,P1,P2,P_0, P_1, P_2, \ldots. Read PtP_t as "P sub t": the price at time tt. So P0P_0 is the first price and Pt1P_{t-1} ("P sub t minus one") is the price one step before PtP_t — yesterday's price, if each step is a day.

Why bother with three? Each has different properties:

A useful rule of thumb: for small returns (< 5%), rtr~tr_t \approx \tilde{r}_t. The symbol \approx reads "is approximately equal to" — for small moves, the two conventions give nearly the same number. They drift apart as moves get larger:

Simple returnLog return
+1%+0.995%
+5%+4.879%
+10%+9.531%
+100%+69.31%
−50%−69.31%

The asymmetry on the last two rows is informative. A double (+100%) and a halving (−50%) are exact mirror images in log space: +69.31% and −69.31%. In percent space they look like unrelated numbers.

NumPy mechanics

The basic NumPy idiom for return computation:

import numpy as np

prices = np.array([100.0, 102.0, 101.5, 103.0, 102.8])

# Simple returns: shift and subtract
simple_returns = prices[1:] / prices[:-1] - 1
# array([ 0.02      , -0.00490196,  0.01477833, -0.00194175])

# Log returns: shift and log-difference
log_returns = np.log(prices[1:]) - np.log(prices[:-1])
# array([ 0.01980263, -0.00491402,  0.01467038, -0.0019436 ])

Three things to notice:

  1. No loop. prices[1:] is every price except the first; prices[:-1] is every price except the last. Dividing them lines up each price with the one before it, and NumPy performs all the divisions in one vectorized pass.
  2. The output is one shorter than the input. That's because the first price has no "previous." You'll see this a lot — each lag (each step back in time an operation reaches) costs one observation off the front.
  3. Dtype awareness matters. Price arrays should be float64 (NumPy's default for float input). Integer arrays work for / (true division returns float), but integer dtypes bite you elsewhere — // floor-division, overflow in cumulative products, and silent casts in in-place ops. Check arr.dtype when results look off.

A small but important pattern: rolling windows

A rolling window computation slides a fixed-size window along the series and recomputes a statistic at every step — for example "the average of the last 20 days," refreshed each day. You'll want these constantly: rolling mean, rolling standard deviation, rolling Sharpe ratio (a running measure of return earned per unit of risk taken — defined properly later in the curriculum). The naive way:

# slow — Python loop
out = []
for i in range(window, len(returns)):
    out.append(returns[i-window:i].mean())

The vectorized way uses cumulative sums for means, or specialized libraries (pandas.rolling, numpy.lib.stride_tricks.sliding_window_view) for general operations. We'll use pandas for most of these in C2. For now, just know that loops over time series are something to avoid.

When NOT to use NumPy

For anything with labels (named columns, time-indexed rows, mixed types), use pandas — built on NumPy but adds the metadata. We'll meet pandas in C2-01.

For multi-dimensional matrices with linear algebra (eigenvalues, matrix inverse, factorizations), NumPy is fine, but SciPy (scipy.linalg) has more numerically stable routines for serious work.

For massive datasets that don't fit in memory — Polars (or Dask, or DuckDB) become better tools. NumPy holds everything in RAM.

For Phase 1 of this curriculum, plain NumPy + pandas covers ~95% of what we need.

⧉ Review card
Why use log returns instead of simple returns?
Log returns aggregate cleanly across time (5-day log return = sum of 5 daily log returns) and are closer to normally distributed. Simple returns aggregate across assets in a portfolio but not across time.
⧉ Review card
How do you compute simple returns of a NumPy price array without a loop?
`returns = prices[1:] / prices[:-1] - 1`. The output is one shorter than input because the first price has no previous value.
⧉ Review card
When do simple and log returns diverge meaningfully?
For small returns (<5%) they're almost identical. For ±10% they differ by ~0.5pp. For ±50% or larger, the difference is dramatic (and the asymmetry of simple returns becomes obvious).
⧉ Review card
Why is a Python for-loop over an array slow compared to NumPy?
Python loops execute one operation per iteration in the Python interpreter with object overhead. NumPy operations execute the same logic in pre-compiled C code on contiguous memory. The speedup is usually 10–100×.

Try it

The reading-on-mobile portion of this lesson ends here. The exercises below need a keyboard.

⚡ Try this first — wrong is fine

Before reading the next lesson, try this in your own Python environment (or on paper):

Given the price series [100, 103, 99, 102, 105, 101]:

  • Compute simple returns (5 values)
  • Compute log returns (5 values)
  • Compute the 3-day rolling mean of simple returns (3 values)
  • Without computing it, predict: which is higher — the arithmetic mean of simple returns (the plain average), or the geometric mean (the compounding-aware average — what constant return per period would produce the same final result)? Why?

Now check your work directly in the browser — the exercise below runs real Python (with NumPy) right here:

▮ EXERCISE · c1-01-ex1

Compute the simple returns and log returns of the given price series as NumPy arrays. No loops — use array slicing.

Predict before the next lesson

Tomorrow we'll meet pandas, which adds time-indexed data structures on top of NumPy. Predict:

Note your guesses. We'll check.

◈ Calibration check

How confident are you that you could write the NumPy returns code from scratch?

1 = guessing · 5 = could teach it

⏻ End of lesson

Mark it read to book its 4 review cards into your deck.

Sources & further reading