Interfaces
The problem
You have three strategies: a moving-average crossover, a breakout, a mean-reversion rule. You want one backtester that runs any of them.
You could write three backtesters. You could write one with a switch over strategy names, and edit it every time you add a strategy. Both are bad, and the second is worse because it means the backtester needs to know about every strategy that will ever exist.
What you want is to say: "give me anything that can produce a signal from a bar, and I'll run it." That's an interface.
An interface is a list of methods
type Strategy interface {
OnBar(view MarketView) *Signal
Name() string
}That's it. No fields, no implementation - a contract. "Anything with these two methods is a Strategy."
Now the backtester takes the interface:
func RunBacktest(s Strategy, bars []Bar) Result {
fmt.Printf("Running %s over %d bars\n", s.Name(), len(bars))
// ...
}And it works with anything satisfying the contract, including strategies written years later by someone else.
The unusual part: nobody declares anything
In Java or C# you'd write class EmaCross implements Strategy. In Go you write nothing:
type EmaCross struct {
fast, slow *MovingAverage
}
func (e *EmaCross) Name() string { return "EMA Cross" }
func (e *EmaCross) OnBar(view MarketView) *Signal {
// ...
return nil
}*EmaCross now satisfies Strategy - because it has the methods. No declaration, no import of the interface, nothing.
This is implicit satisfaction, and it inverts who's in charge. In most languages the type must know about the interface in advance. In Go, the consumer defines the interface it needs, and any existing type that happens to fit can be passed in - including types from libraries that have never heard of your interface.
The practical consequence: you don't design an interface hierarchy up front. You write concrete code, notice two things have the same shape, and extract an interface afterwards.
Small interfaces are better
The most-used interface in Go is one method:
type Writer interface {
Write(p []byte) (n int, err error)
}Files satisfy it. Network connections satisfy it. bytes.Buffer satisfies it. Compression wrappers satisfy it. Anything that can accept bytes.
That's why a function taking an io.Writer can write to a file, a socket, a buffer, or standard output without knowing which. The fewer methods an interface demands, the more things can satisfy it and the more useful it is.
Go's proverb: the bigger the interface, the weaker the abstraction. If your interface has eight methods, ask whether it's really one thing.
error is an interface
Chapter 6 said errors are values. Here's what they actually are:
type error interface {
Error() string
}Anything with an Error() string method is an error. That's the whole mechanism, and it's why you can define your own:
type InsufficientDataError struct {
Need, Have int
}
func (e *InsufficientDataError) Error() string {
return fmt.Sprintf("need %d bars, have %d", e.Need, e.Have)
}Return &InsufficientDataError{Need: 200, Have: 50} anywhere an error is expected, and callers can extract the numbers with errors.As.
Type assertions and type switches
Sometimes you need the concrete type back out:
var s Strategy = &EmaCross{}
if ema, ok := s.(*EmaCross); ok {
fmt.Println("fast window:", ema.fast.buf.Size())
}s.(*EmaCross) is a type assertion. The comma-ok form is safe; without it, a wrong guess panics.
For several possibilities, a type switch:
switch v := s.(type) {
case *EmaCross:
fmt.Println("crossover with fast window", v.fast.buf.Size())
case *Breakout:
fmt.Println("breakout with lookback", v.lookback)
default:
fmt.Printf("some other strategy: %T\n", v)
}Use these sparingly. Reaching for the concrete type usually means the interface is missing a method.
See It Work: an interface value is two things
An interface variable holds a type and a value, side by side:
var s Strategy = &EmaCross{...}
┌──────────────┬──────────────┐
│ type: │ value: │
│ *EmaCross │ 0xc000012345 │
└──────────────┴──────────────┘Print both:
var s Strategy = &EmaCross{}
fmt.Printf("%T %p\n", s, s) // *main.EmaCross 0xc000012345This two-part structure explains Go's most notorious trap.
See It Work: the nil that isn't nil
Run this. It's the single most confusing thing in Go, and seeing it once inoculates you.
package main
import "fmt"
type MyError struct{ Code int }
func (e *MyError) Error() string { return fmt.Sprintf("error %d", e.Code) }
// Looks harmless. Is a trap.
func doWork(fail bool) error {
var e *MyError // nil pointer
if fail {
e = &MyError{Code: 42}
}
return e // returning the pointer, always
}
func main() {
err := doWork(false)
fmt.Printf("err == nil? %t\n", err == nil)
fmt.Printf("type: %T\n", err)
fmt.Printf("value: %v\n", err)
if err != nil {
fmt.Println("...so this branch runs, even though nothing failed")
}
}err == nil? false
type: *main.MyError
value: <nil>The error is not nil, even though the pointer inside it is.
The interface has a type (*MyError) and a value (nil). An interface only equals nil when both halves are empty. Here the type half is filled in, so the comparison fails, and every if err != nil in your program takes the wrong branch.
The fix is to never return a concrete error pointer type from a function declared to return error:
func doWork(fail bool) error {
if fail {
return &MyError{Code: 42}
}
return nil // an actual nil interface
}Return literal nil on the success path. Always.
Interfaces in the backtester
Here's the shape the whole book has been building toward:
type Strategy interface {
Name() string
OnBar(view MarketView) *Signal
}
type CostModel interface {
FillPrice(quoted float64, side int) float64
Commission(price, qty float64) float64
}
type Sizer interface {
Size(equity, riskPerUnit float64) float64
}
func RunBacktest(s Strategy, c CostModel, z Sizer, bars []Bar) Result {
// knows nothing about any specific strategy, cost model or sizing rule
}Three interfaces, each tiny. You can now swap in a pessimistic cost model, or fixed-size instead of risk-based sizing, without touching the backtester. And you can write a fake CostModel that charges nothing, for tests.
That last point is the underrated one: interfaces are what make code testable, because they let you substitute a predictable stand-in for something awkward.
Exercises
14.1 Define Strategy and implement two: EmaCross and AlwaysFlat (which never signals). Write a function taking a []Strategy and printing each one's name.
14.2 Define Indicator with Update(bar Bar) (float64, bool). Make MovingAverage from Chapter 8 satisfy it. Then write ATR satisfying the same interface and run both through one loop.
14.3 Write a custom error type with fields, return it, and extract the fields at the call site with errors.As.
14.4 Run the nil-interface demo. Then write a version where err == nil is correctly true, and explain in a comment what you changed.
14.5 Write Reporter with Report(r Result) error, and two implementations: one printing to the console, one writing CSV. Note how the backtester needs no changes to gain a new output format.
14.6 Harder. Make your Result type satisfy fmt.Stringer and json.Marshaler. Confirm fmt.Println picks up the first automatically.
Solutions
14.3
var insufficientErr *InsufficientDataError
if errors.As(err, &insufficientErr) {
fmt.Printf("short by %d bars\n", insufficientErr.Need-insufficientErr.Have)
}errors.As searches the %w chain from Chapter 6 and fills in your variable if it finds a matching type. errors.Is compares against a specific value; errors.As extracts by type.
14.4 Change return e to an explicit return nil on the success path. Returning a typed nil pointer as an error is the bug.
14.6 fmt checks whether its argument implements Stringer and calls String() if so. That's a type assertion happening inside the standard library - the mechanism you learned above, used on your behalf.