Naming Things
The problem
Chapter 4 ended with three loops that all computed averages. Copying code is fine once. By the third time you've copied it, a bug fixed in one copy is still alive in the other two.
A function is a named box of behaviour
func averagePrice(prices []float64) float64 {
sum := 0.0
for _, p := range prices {
sum += p
}
return sum / float64(len(prices))
}Reading the first line, left to right:
func- declaring a functionaveragePrice- its name(prices []float64)- it takes one input, calledprices, of type "slice of float64"float64- it gives back a float64{ ... }- the body
Use it:
func main() {
daily := []float64{100, 102, 101, 105}
weekly := []float64{98, 99, 103}
fmt.Printf("Daily: %.2f\n", averagePrice(daily))
fmt.Printf("Weekly: %.2f\n", averagePrice(weekly))
}One definition. Two uses. Fix a bug once.
Parameters and arguments
The names in the definition are parameters - placeholders. The values you pass in are arguments.
func profit(entry, exit, quantity float64) float64 {
return (exit - entry) * quantity
}
fmt.Println(profit(67410.50, 68200.00, 0.5))When consecutive parameters share a type, you write it once: (entry, exit, quantity float64) rather than repeating float64 three times.
Arguments are copies. Changing a parameter inside a function does not affect the caller's variable:
func tryToChange(x float64) {
x = 999
}
func main() {
price := 100.0
tryToChange(price)
fmt.Println(price) // still 100
}The function got its own copy in its own box. This is one of the most important things to understand about how programs work, and Chapter 11 covers the exception - pointers - in detail.
Returning more than one thing
Here Go differs from most languages, and it shapes everything about how Go is written:
func highLow(prices []float64) (float64, float64) {
high := prices[0]
low := prices[0]
for _, p := range prices {
if p > high {
high = p
}
if p < low {
low = p
}
}
return high, low
}
h, l := highLow(prices)
fmt.Printf("High %.2f, Low %.2f\n", h, l)Two return values, two variables to catch them. Don't need one? _ again:
_, low := highLow(prices)This feature is why Go doesn't need exceptions. A function returns its answer and whether it worked - side by side, visible in the signature. That's Chapter 6.
Named returns
You can name the return values:
func riskReward(entry, stop, target float64) (risk, reward, ratio float64) {
risk = entry - stop
reward = target - entry
ratio = reward / risk
return
}A bare return sends back whatever those named variables currently hold. Useful for documenting what comes back, in what order. Use it sparingly - in long functions the bare return makes it hard to see what's actually being returned.
Scope: where a name is visible
A variable exists inside the braces where it was created, and dies at the closing brace.
func main() {
x := 1
if true {
y := 2
fmt.Println(x, y) // fine - x from outside, y from here
}
fmt.Println(y) // ERROR: undefined: y
}This is why sum := 0.0 goes before the loop, not inside it - declared inside, it would be created fresh and destroyed on every pass, and the accumulation would never accumulate.
Functions calling functions
The real payoff:
func sum(values []float64) float64 {
total := 0.0
for _, v := range values {
total += v
}
return total
}
func mean(values []float64) float64 {
return sum(values) / float64(len(values))
}
func variance(values []float64) float64 {
m := mean(values)
squaredDiffs := 0.0
for _, v := range values {
diff := v - m
squaredDiffs += diff * diff
}
return squaredDiffs / float64(len(values)-1)
}
func stdDev(values []float64) float64 {
return math.Sqrt(variance(values))
}Each function does one thing. Each is short enough to check by eye. stdDev is one line because the work is elsewhere, and if mean is wrong you fix it in one place and everything above it becomes correct.
This is the actual skill. Not knowing syntax - decomposing a problem into pieces small enough that each one is obviously right.
How big should a function be?
Rules of thumb, not laws:
- If you can't see the whole thing on screen, it's probably too long.
- If the name needs "and" in it, it's doing two things.
calculateAndPrintshould be two functions. - If you can't name it clearly, you don't yet understand what it does - which is useful information.
- Three or four parameters is plenty. More usually means a struct is waiting to be born (Chapter 10).
Run It Yourself
A complete statistics toolkit built from small functions, each calling the one below it. Save as stats.go.
package main
import (
"fmt"
"math"
)
// --- the building blocks, each doing exactly one thing ---
func sum(values []float64) float64 {
total := 0.0
for _, v := range values {
total += v
}
return total
}
func mean(values []float64) float64 {
if len(values) == 0 {
return 0
}
return sum(values) / float64(len(values))
}
func variance(values []float64) float64 {
if len(values) < 2 {
return 0
}
m := mean(values)
squaredDiffs := 0.0
for _, v := range values {
diff := v - m
squaredDiffs += diff * diff
}
return squaredDiffs / float64(len(values)-1)
}
func stdDev(values []float64) float64 {
return math.Sqrt(variance(values))
}
// --- things built on top of those ---
func returns(prices []float64) []float64 {
if len(prices) < 2 {
return nil
}
out := make([]float64, 0, len(prices)-1)
for i := 1; i < len(prices); i++ {
out = append(out, (prices[i]-prices[i-1])/prices[i-1])
}
return out
}
func sharpeRatio(rets []float64, periodsPerYear float64) float64 {
sd := stdDev(rets)
if sd == 0 {
return 0
}
return (mean(rets) * periodsPerYear) / (sd * math.Sqrt(periodsPerYear))
}
func highLow(prices []float64) (high, low float64) {
if len(prices) == 0 {
return 0, 0
}
high, low = prices[0], prices[0]
for _, p := range prices {
if p > high {
high = p
}
if p < low {
low = p
}
}
return high, low
}
// expectancy returns three things at once - the shape Chapter 6 builds on.
func expectancy(rMultiples []float64) (avg, winRate float64, count int) {
if len(rMultiples) == 0 {
return 0, 0, 0
}
wins := 0
for _, r := range rMultiples {
if r > 0 {
wins++
}
}
return mean(rMultiples), float64(wins) / float64(len(rMultiples)), len(rMultiples)
}
func main() {
prices := []float64{
100.0, 102.5, 101.8, 104.2, 103.1,
106.7, 105.3, 108.9, 107.2, 110.5,
}
fmt.Println("=== price series ===")
high, low := highLow(prices)
fmt.Printf("count %d\n", len(prices))
fmt.Printf("high %.2f\n", high)
fmt.Printf("low %.2f\n", low)
fmt.Printf("mean %.2f\n", mean(prices))
rets := returns(prices)
fmt.Println("\n=== daily returns ===")
fmt.Printf("count %d (one fewer than prices - why?)\n", len(rets))
fmt.Printf("mean %.4f%%\n", mean(rets)*100)
fmt.Printf("stddev %.4f%%\n", stdDev(rets)*100)
fmt.Printf("sharpe %.2f (annualised from daily)\n", sharpeRatio(rets, 252))
fmt.Println("\n=== trade results, in R ===")
trades := []float64{1.5, -1.0, 2.3, -1.0, -1.0, 3.1, -1.0, 0.8}
avg, winRate, n := expectancy(trades)
fmt.Printf("trades %d\n", n)
fmt.Printf("expectancy %+.3fR per trade\n", avg)
fmt.Printf("win rate %.1f%%\n", winRate*100)
fmt.Printf("total %+.2fR\n", sum(trades))
fmt.Println("\n=== every function handles empty input ===")
var empty []float64
a, w, c := expectancy(empty)
fmt.Printf("mean(empty)=%.1f stdDev(empty)=%.1f expectancy=%.1f/%.1f/%d\n",
mean(empty), stdDev(empty), a, w, c)
}Notice the shape: stdDev is one line because variance does the work, variance is short because mean does the work, and mean is short because sum does. If `mean` is wrong, you fix it in one place and everything above it becomes correct. That decomposition is the actual skill this chapter is teaching - not the syntax.
Now break it on purpose.
- Change
varianceto divide bylen(values)instead oflen(values)-1. Run it. The Sharpe ratio moves. (Look up "Bessel's correction" to find out which one is right and why.) - Delete the
if len(values) < 2guard fromvarianceand call it with a one-element slice. What happens, and why is the guard there? - Add a
medianRfunction and use it alongsideexpectancy. On the trade list above, the mean and median tell noticeably different stories - which one would you rather a strategy showed you?
Exercises
5.1 Turn Chapter 4's max-drawdown calculation into maxDrawdown(prices []float64) float64.
5.2 Write returns(prices []float64) []float64 giving the percentage change between consecutive prices. The result has one fewer element than the input - make sure that's deliberate rather than accidental.
5.3 Write positionSize(equity, riskPct, entry, stop float64) float64 returning how many units to buy so the loss at the stop equals riskPct percent of equity. Guard against a zero stop distance.
5.4 Write sharpeRatio(returns []float64, periodsPerYear float64) float64 using mean and stdDev above. Annualise by multiplying the mean by periodsPerYear and the standard deviation by math.Sqrt(periodsPerYear).
5.5 Write expectancy(rMultiples []float64) (mean, winRate float64, count int) returning average R, the fraction above zero, and the number of trades - the summary from book one, Chapter 4.
5.6 Harder. Write movingAverage(prices []float64, window int) ([]float64, error) using the running-sum method, returning an error when the window is bigger than the input or less than 1. You'll need Chapter 6 for the error part - try it, then check your answer after reading it.
Solutions
5.1
func maxDrawdown(prices []float64) float64 {
if len(prices) == 0 {
return 0
}
runningMax := prices[0]
worst := 0.0
for _, p := range prices {
if p > runningMax {
runningMax = p
}
dd := (runningMax - p) / runningMax
if dd > worst {
worst = dd
}
}
return worst
}5.2
func returns(prices []float64) []float64 {
if len(prices) < 2 {
return []float64{}
}
out := make([]float64, 0, len(prices)-1)
for i := 1; i < len(prices); i++ {
out = append(out, (prices[i]-prices[i-1])/prices[i-1])
}
return out
}make([]float64, 0, n) creates an empty slice with room for n - Chapter 7 explains why that matters.
5.3
func positionSize(equity, riskPct, entry, stop float64) float64 {
riskPerUnit := math.Abs(entry - stop)
if riskPerUnit == 0 {
return 0
}
return (equity * riskPct / 100) / riskPerUnit
}math.Abs means it works for both long and short.
5.4
func sharpeRatio(returns []float64, periodsPerYear float64) float64 {
sd := stdDev(returns)
if sd == 0 {
return 0
}
return (mean(returns) * periodsPerYear) / (sd * math.Sqrt(periodsPerYear))
}5.5
func expectancy(rMultiples []float64) (float64, float64, int) {
n := len(rMultiples)
if n == 0 {
return 0, 0, 0
}
wins := 0
for _, r := range rMultiples {
if r > 0 {
wins++
}
}
return mean(rMultiples), float64(wins) / float64(n), n
}5.6 See Chapter 6 for the error idiom; the running-sum logic is Chapter 4's movingAverageFast with a guard clause on top.