{adamcoding}
Part I
03
Chapter 3

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:

go
price := 67410.50
average := 67000.00

isAbove := price > average
fmt.Println(isAbove)        // true

The comparison operators:

OperatorMeaning
==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

go
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:

go
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.

OperatorMeaning
&&AND - both must be true
`\\`OR - at least one must be true
!NOT - flips it
go
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:

go
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     true

And one that catches everyone - De Morgan's laws:

   !(A && B)   is the same as   !A || !B
   !(A || B)   is the same as   !A && !B

The 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:

go
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:

go
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:

go
switch {
case rsi > 70:
	fmt.Println("Overbought")
case rsi < 30:
	fmt.Println("Oversold")
default:
	fmt.Println("Neutral")
}

A complete signal

go
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.

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.

  1. In decide, move the case s.inPosition branch to the bottom. Which scenario changes, and why? (Case order in a switch is the logic.)
  2. Change crossedUp && trendIsUp to crossedUp || trendIsUp. How many scenarios now say BUY that shouldn't?
  3. Add a scenario where price exactly equals trendMA. 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:

go
if !(price > stopLoss && price < takeProfit) {
	fmt.Println("Exit")
}

3.3 Predict each result before running:

go
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:

go
if fastMA > slowMA || price > trendMA && !inPosition {
	fmt.Println("BUY")
}

Solutions

3.1

go
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:

go
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

go
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:

go
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:

go
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.