What is a function?

Give a calculation a name, call it with your own numbers, set a default argument, and call one function from inside another.

A function is a calculation with a name. You write the steps once, then run them on whatever numbers you hand it.

In this lesson I turn the return formula into a named function, give an argument a default, call one function from inside another, and show that a function cannot change the caller’s objects.

Step 1. A calculation with a name

function(prev, curr) lists the inputs. The body sits in the braces. R hands back the value of the last expression it evaluates, so the formula on its own line is the answer.

daily_return <- function(prev, curr) {   # two inputs: yesterday's close, today's close
  (curr - prev) / prev                   # last expression, so this is what comes back
}

That block printed nothing. Defining a function stores it under a name, the same way <- stores a number. Nothing is computed until you call it.

print(class(daily_return))                       # -> [1] "function"
[1] "function"
print(daily_return(185.40, 187.20))              # -> [1] 0.009708738
[1] 0.009708738
print(daily_return(187.20, 184.90))              # -> [1] -0.01228632
[1] -0.01228632
print(round(daily_return(185.40, 187.20), 4))    # -> [1] 0.0097
[1] 0.0097

The price rose from 185.40 to 187.20, a gain of 0.97%, then fell to 184.90, a loss of 1.23%.

return() says the same thing out loud. Both spellings give the same number.

daily_return_2 <- function(prev, curr) {
  return((curr - prev) / prev)                                           # explicit
}

print(daily_return_2(185.40, 187.20))                                    # -> [1] 0.009708738
[1] 0.009708738
print(daily_return(185.40, 187.20) == daily_return_2(185.40, 187.20))    # -> [1] TRUE
[1] TRUE

I use the short form. return() stops the function and hands back a value at that point, which is how you leave early from inside an if.

Nothing in the body assumes a single number, so feeding it two vectors gives three returns at once.

closes <- c(185.40, 187.20, 184.90, 188.10)   # four closing prices in order
prev   <- closes[1:3]                         # the first three, playing yesterday
curr   <- closes[2:4]                         # the last three, playing today

print(round(daily_return(prev, curr), 4))     # -> [1]  0.0097 -0.0123  0.0173
[1]  0.0097 -0.0123  0.0173

Step 2. An argument with a default

Put = 0 after an argument name and the caller may leave it out. Here cost_bps is the trading cost in basis points, one hundredth of a percent, charged on the buy and on the sell.

pnl <- function(buy, sell, shares, cost_bps = 0) {
  gross <- (sell - buy) * shares                     # profit before any cost
  cost  <- (buy + sell) * shares * cost_bps / 10000  # cost on both legs of the trade
  gross - cost                                       # the value that comes back
}

print(pnl(150, 168.75, 40))                  # -> [1] 750
[1] 750
print(pnl(150, 168.75, 40, cost_bps = 10))   # -> [1] 737.25
[1] 737.25

The first call left cost_bps out, so R used 0 and the trade made 750. The second named the argument at the call site: cost_bps = 10 charges 10 basis points on 12750 of turnover, which is 12.75.

Naming an argument also means you do not have to remember the order. pnl(150, 168.75, 40, 10) gives the same 737.25, but the reader has to count positions to see what the 10 is.

Step 3. One function calling another

A function body can call any other function, including one you wrote.

portfolio_value <- function(prices, shares) {
  sum(prices * shares)                                 # price times shares, then added up
}

weights <- function(prices, shares) {
  prices * shares / portfolio_value(prices, shares)    # each position over the total
}

prices <- c(185.40, 410.20, 128.75)          # one price per holding
shares <- c(10, 4, 20)                       # shares held of each

print(portfolio_value(prices, shares))       # -> [1] 6069.8
[1] 6069.8
print(round(weights(prices, shares), 3))     # -> [1] 0.305 0.270 0.424
[1] 0.305 0.270 0.424
print(sum(weights(prices, shares)))          # -> [1] 1
[1] 1

weights() never adds anything up itself. It asks portfolio_value() for the total. Change how the total is computed and both functions change together.

Step 4. A function works on copies

R passes arguments by value. The function gets a copy, and assigning to that copy leaves the caller’s object alone.

bump <- function(x) {
  x <- x + 1                     # changes the copy that lives inside the call
  x
}

shares_held <- 10                # the caller's own object
print(bump(shares_held))         # -> [1] 11
[1] 11
print(shares_held)               # -> [1] 10
[1] 10

bump() handed back 11. shares_held is still 10. If you want the new value, catch it: shares_held <- bump(shares_held).

Your turn

Write net_return(first, last, cost_bps = 0). It returns (last - first) / first minus the cost, where the cost is cost_bps / 10000. Call it on 50 and 44 with no cost, then with 25 basis points.

net_return <- function(first, last, cost_bps = 0) {
  gross <- (last - first) / first        # the return before costs
  gross - cost_bps / 10000               # 25 bps is 0.0025
}

print(net_return(50, 44))                  # -> [1] -0.12
print(net_return(50, 44, cost_bps = 25))   # -> [1] -0.1225