Give a calculation a name with def, feed it inputs, and get an answer back.
A function is a calculation with a name. You write it once with def, hand it inputs, and return hands an answer back.
In this lesson I build a return calculator, a pair of portfolio functions that call each other, and a profit function with a default argument.
Step 1. Name a calculation
def starts the function, the names in the brackets are the inputs, and the indented lines below are the body. return says what comes out.
def daily_return(prev, curr):"""The simple return from prev to curr, as a decimal."""return (curr - prev) / prevprint(daily_return(100, 110)) # -> 0.1print(daily_return(110, 99)) # -> -0.1print(f"{daily_return(150, 168.75):.2%}") # -> 12.50%
0.1
-0.1
12.50%
prev and curr are parameters. They are empty names until you call the function, then they hold what you passed in. The first call sets prev = 100 and curr = 110.
Running def on its own prints nothing. Only the call produces a value:
def daily_return(prev, curr):return (curr - prev) / prev # defining it produces no outputr = daily_return(100, 110) # calling it produces a valueprint(r) # -> 0.1
0.1
Step 2. Without return you get None
return sends the answer back. Drop it and the function runs and hands back None.
def gain(prev, curr): curr - prev # computed, then thrown awayprint(gain(100, 110)) # -> Nonedef gain(prev, curr):return curr - prev # now it comes backprint(gain(100, 110)) # -> 10
None
10
The same thing happens on a book of shares.
prices = [185.40, 410.20, 128.75]shares = [10, 4, 20]def book_value(prices, shares):sum(p * n for p, n inzip(prices, shares)) # added up, then thrown awayprint(book_value(prices, shares)) # -> Nonedef book_value(prices, shares):returnsum(p * n for p, n inzip(prices, shares))print(book_value(prices, shares)) # -> 6069.8
None
6069.8
Step 3. A function can call another function
Inside a function body you can call any other function. Here weights calls portfolio_value to get the total, then divides each position by it.
def portfolio_value(prices, shares):"""Cash value of the whole book."""returnsum(p * n for p, n inzip(prices, shares))def weights(prices, shares):"""Each position as a share of the total.""" total = portfolio_value(prices, shares) # calling the function abovereturn [p * n / total for p, n inzip(prices, shares)]prices = [185.40, 410.20, 128.75]shares = [10, 4, 20]print(round(portfolio_value(prices, shares), 2)) # -> 6069.8print([round(w, 3) for w in weights(prices, shares)]) # -> [0.305, 0.27, 0.424]
6069.8
[0.305, 0.27, 0.424]
The three positions are worth 1854.00, 1640.80 and 2575.00, so the weights add to 1.
Step 4. Default arguments
Put = after a parameter and it gets a value to fall back on. Callers can skip it.
def pnl(buy, sell, shares, cost_bps=0):"""Profit on a round trip, after cost_bps charged on both legs.""" gross = (sell - buy) * shares cost = (buy + sell) * shares * cost_bps /10_000# 1 bp = 0.01% = 1/10_000return gross - costprint(pnl(150, 168.75, 40)) # -> 750.0 <- cost_bps falls back to 0print(pnl(150, 168.75, 40, cost_bps=10)) # -> 737.25 <- 10 bp on 150 and on 168.75
750.0
737.25
Naming the argument at the call site, cost_bps=10, drops the need to remember the parameter order.
Your turn
Write net_return(prev, curr, cost_bps=0). It calls daily_return from Step 1 and subtracts the cost, expressed as a decimal. Check net_return(100, 110, cost_bps=20) gives 0.098.