QTQuant 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 operates on arrays vectorized in C under the hood. A Python for loop over 10 million prices takes seconds. The equivalent NumPy operation takes milliseconds. The difference compounds across thousands of strategy backtests.

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

Returns — three conventions

For a price series P0,P1,P2,P_0, P_1, P_2, \ldots:

Why bother with three? Each has different properties:

A useful rule of thumb: for small returns (< 5%), rtr~tr_t \approx \tilde{r}_t. They diverge at larger magnitudes:

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

The asymmetry on the last two is informative: doubling and halving are mirror images in log space, but very different in percent space.

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. The slicing prices[1:] / prices[:-1] element-wise divides each price by the previous one. NumPy broadcasts this across the whole array.
  2. The output is one shorter than the input. That's because the first price has no "previous." You'll see this a lot — most return-based operations lose one observation per lag.
  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

You'll want rolling computations constantly — rolling mean, rolling standard deviation, rolling Sharpe. 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, or the geometric mean? 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