Making Decisions
The problem
You have a price and a moving average. If the price is above the average and you're not already in a position, you want to buy. That's a decision, and decisions are how a program stops being a calculator and starts being a strategy.
Booleans
A bool holds exactly one of two values: true or false. You mostly make them by comparing things:
price := 67410.50
average := 67000.00
isAbove := price > average
fmt.Println(isAbove) // trueThe comparison operators:
| Operator | Meaning |
|---|---|
== | equal to |
!= | not equal to |
< > | less than, greater than |
<= >= | less than or equal, greater than or equal |
Note == (comparison) versus = (assignment). Mixing them up is the classic beginner bug in most languages. Go helps: if price = 100 won't compile, because assignment isn't an expression in Go. One fewer thing to get wrong.
if
price := 67410.50
average := 67000.00
if price > average {
fmt.Println("Price is above the average")
}The braces are mandatory, even for one line. And the opening brace must be on the same line as the if - Go is strict about this, for reasons involving semicolon insertion that you can safely ignore. Just put it there.
Add alternatives:
if price > average {
fmt.Println("Above")
} else if price < average {
fmt.Println("Below")
} else {
fmt.Println("Exactly equal, which for floats is suspicious")
}Combining conditions
Real signals need more than one thing to be true at once.
| Operator | Meaning | ||
|---|---|---|---|
&& | AND - both must be true | ||
| `\ | \ | ` | OR - at least one must be true |
! | NOT - flips it |
price := 67410.50
fastMA := 67200.00
slowMA := 67000.00
inPosition := false
if fastMA > slowMA && price > slowMA && !inPosition {
fmt.Println("BUY")
}Read that as: the fast average is above the slow one, AND the price is above the slow one, AND we are not already in a position. That's a complete entry condition, and it looks almost exactly like the English description - which is the sign you've written it well.
Short-circuiting, and why it matters
Go stops evaluating as soon as the answer is certain.
With &&, if the first part is false, the answer is false regardless of the rest - so the rest is never evaluated. With ||, if the first part is true, the rest is skipped.
This isn't just an optimisation; you'll use it deliberately:
if len(prices) > 20 && prices[19] > prices[0] {
// ...
}If there are fewer than 20 prices, prices[19] doesn't exist and reaching for it would crash the program. But it's never reached, because the first condition already answered the question. Order your conditions so the cheap, protective ones come first.
Truth tables
Worth internalising, because compound conditions get confusing fast:
A B A && B A || B !A
─────────────────────────────────────────
true true true true false
true false false true false
false true false true true
false false false false trueAnd one that catches everyone - De Morgan's laws:
!(A && B) is the same as !A || !B
!(A || B) is the same as !A && !BThe NOT flips the operator as it moves inside. "It's not the case that both are true" means "at least one is false." Getting this wrong is how you write a filter that silently blocks every trade.
if with a statement
Go lets you run something first, then test its result, with the variable scoped to the if:
if change := price - previousClose; change > 0 {
fmt.Printf("Up %.2f\n", change)
} else {
fmt.Printf("Down %.2f\n", -change)
}change exists inside the if and the else, and nowhere else. This is idiomatic Go and you'll see it constantly - especially with errors, in Chapter 6.
switch
When you're comparing one thing against many possibilities, a chain of else if gets ugly. switch is cleaner:
switch signal {
case "BUY":
fmt.Println("Opening long")
case "SELL":
fmt.Println("Opening short")
case "HOLD":
fmt.Println("Doing nothing")
default:
fmt.Println("Unknown signal:", signal)
}Unlike C, Java, or JavaScript, Go's switch does not fall through to the next case. No break needed. This removes an entire genre of bug.
You can also switch on conditions rather than a value, which reads beautifully for classification:
switch {
case rsi > 70:
fmt.Println("Overbought")
case rsi < 30:
fmt.Println("Oversold")
default:
fmt.Println("Neutral")
}A complete signal
package main
import "fmt"
func main() {
price := 67410.50
fastMA := 67200.00
slowMA := 67000.00
trendMA := 66000.00
atr := 850.0
inPosition := false
trendIsUp := price > trendMA
crossedUp := fastMA > slowMA
volatilityOK := atr < 1000.0
switch {
case inPosition:
fmt.Println("HOLD - already in a position")
case crossedUp && trendIsUp && volatilityOK:
fmt.Println("BUY")
case crossedUp && trendIsUp && !volatilityOK:
fmt.Println("SKIP - signal valid but volatility too high")
default:
fmt.Println("WAIT")
}
}Notice that naming the intermediate booleans - trendIsUp, crossedUp, volatilityOK - makes the logic readable without a comment. A well-named boolean is documentation that can't go out of date. This habit matters more than it looks; a condition written as one long chain of && is where subtle logic bugs hide.
Run It Yourself
A complete signal generator you can run and poke at. Save as signal.go.
package main
import "fmt"
// A scenario is one moment in the market we want to classify.
type scenario struct {
label string
price float64
fastMA float64
slowMA float64
trendMA float64
atr float64
inPosition bool
}
func decide(s scenario) string {
trendIsUp := s.price > s.trendMA
crossedUp := s.fastMA > s.slowMA
volatilityOK := s.atr < 1000.0
switch {
case s.inPosition:
return "HOLD - already in a position"
case crossedUp && trendIsUp && volatilityOK:
return "BUY"
case crossedUp && trendIsUp && !volatilityOK:
return "SKIP - signal valid, volatility too high"
case crossedUp && !trendIsUp:
return "WAIT - cross up but below the trend filter"
default:
return "WAIT"
}
}
func main() {
scenarios := []scenario{
{"clean buy signal", 67410, 67200, 67000, 66000, 850, false},
{"same, but already long", 67410, 67200, 67000, 66000, 850, true},
{"same, but volatile", 67410, 67200, 67000, 66000, 1500, false},
{"cross up, below trend", 65000, 67200, 67000, 66000, 850, false},
{"no cross at all", 67410, 66800, 67000, 66000, 850, false},
}
fmt.Printf("%-26s %-10s %s\n", "scenario", "price", "decision")
fmt.Println("------------------------------------------------------------")
for _, s := range scenarios {
fmt.Printf("%-26s %-10.0f %s\n", s.label, s.price, decide(s))
}
fmt.Println("\n=== truth table for && and || ===")
fmt.Printf("%-8s %-8s %-10s %-10s %s\n", "A", "B", "A && B", "A || B", "!A")
for _, a := range []bool{true, false} {
for _, b := range []bool{true, false} {
fmt.Printf("%-8t %-8t %-10t %-10t %t\n", a, b, a && b, a || b, !a)
}
}
fmt.Println("\n=== precedence: && binds tighter than || ===")
fmt.Printf("true && false || true = %t\n", true && false || true)
fmt.Printf("true && (false || true) = %t\n", true && (false || true))
}Now break it on purpose.
- In
decide, move thecase s.inPositionbranch to the bottom. Which scenario changes, and why? (Case order in aswitchis the logic.) - Change
crossedUp && trendIsUptocrossedUp || trendIsUp. How many scenarios now say BUY that shouldn't? - Add a scenario where
priceexactly equalstrendMA. Does it buy? Should it? This is Chapter 2's warning about>versus>=on floats.
Exercises
3.1 Write a program with variables for price, stopLoss and takeProfit. Print whether the position should be closed at a loss, closed at a profit, or held.
3.2 Rewrite this using De Morgan's laws so there's no ! on the outside:
if !(price > stopLoss && price < takeProfit) {
fmt.Println("Exit")
}3.3 Predict each result before running:
fmt.Println(true && false || true)
fmt.Println(true && (false || true))
fmt.Println(!true || !false)
fmt.Println(!(true || false))3.4 Write a classifier that takes a changePct and prints: "Crash" below -5, "Down" from -5 to -1, "Flat" from -1 to 1, "Up" from 1 to 5, "Spike" above 5. Use switch with conditions.
3.5 This has a bug that lets a trade through when it shouldn't. Find it, explain why, and fix it:
if fastMA > slowMA || price > trendMA && !inPosition {
fmt.Println("BUY")
}Solutions
3.1
switch {
case price <= stopLoss:
fmt.Println("Close at a loss")
case price >= takeProfit:
fmt.Println("Close at a profit")
default:
fmt.Println("Hold")
}Order matters here: check the stop first, which is the conservative choice when both could be true.
3.2 !(A && B) becomes !A || !B, and each comparison flips:
if price <= stopLoss || price >= takeProfit {
fmt.Println("Exit")
}3.3 true - true - true - false. The first is (true && false) || true because && binds tighter than ||. That precedence rule is exactly what makes exercise 3.5 a bug.
3.4
switch {
case changePct < -5:
fmt.Println("Crash")
case changePct < -1:
fmt.Println("Down")
case changePct < 1:
fmt.Println("Flat")
case changePct < 5:
fmt.Println("Up")
default:
fmt.Println("Spike")
}Cases are checked in order, so each one only needs to rule out what the ones above already caught.
3.5 && binds tighter than ||, so Go reads it as:
if fastMA > slowMA || (price > trendMA && !inPosition) {Which means a fast/slow cross alone triggers a buy - even when already in a position. The !inPosition guard only protects the second branch. Fix it with explicit parentheses:
if (fastMA > slowMA || price > trendMA) && !inPosition {The lesson: when you mix && and ||, always parenthesise. Relying on precedence is how you write a bug that reads as correct.