soma.cell

Moving averages over a price series: simple MA window, full-series mean, and a momentum signal from an SMA crossover

Uses range(), avg(), slicing by index, sum() reductions.

A Soma program from the finance part of the verified corpus: 64 lines, cells MovingAverage, MovingAverageTests. It passes soma check with soma 3.2.4.

How it is built

The cell MovingAverage declares 4 signals in its face: sma(prices: List, window: Int) -> List, mean(prices: List) -> Float, latest_sma(prices: List, window: Int) -> Float and crossover(prices: List, fast: Int, slow: Int) -> String. Handlers: sma (line 14), mean (line 27), latest_sma (line 32) and crossover (line 42).

The file also holds 1 test cell (MovingAverageTests) with 6 assertions, run by soma test.

Generated from soma describe on this file.

The source

moving_average.cell

// Moving averages over a price series: simple MA window, full-series mean,
// and a momentum signal from an SMA crossover. Uses range(), avg(), slicing
// by index, sum() reductions.

cell MovingAverage {
    face {
        signal sma(prices: List, window: Int) -> List
        signal mean(prices: List) -> Float
        signal latest_sma(prices: List, window: Int) -> Float
        signal crossover(prices: List, fast: Int, slow: Int) -> String
    }

    // simple moving average → list of window-means, one per valid position
    on sma(prices: List, window: Int) {
        let out = list()
        let n = len(prices)
        for i in range(0, n - window + 1) {
            let acc = 0.0
            for j in range(i, i + window) {
                acc = acc + prices[j]
            }
            out = push(out, acc / window)
        }
        return out
    }

    on mean(prices: List) {
        return sum(prices) / len(prices)
    }

    // most recent window mean
    on latest_sma(prices: List, window: Int) {
        let n = len(prices)
        let acc = 0.0
        for j in range(n - window, n) {
            acc = acc + prices[j]
        }
        return acc / window
    }

    // golden/death cross signal from the last value of two SMAs
    on crossover(prices: List, fast: Int, slow: Int) {
        let f = latest_sma(prices, fast)
        let s = latest_sma(prices, slow)
        if f > s { return "bullish" }
        if f < s { return "bearish" }
        return "neutral"
    }
}

cell test MovingAverageTests {
    rules {
        // SMA of [1,2,3,4] window 2 → [1.5, 2.5, 3.5]
        assert len(sma(list(1.0, 2.0, 3.0, 4.0), 2)) == 3
        assert abs(sma(list(1.0, 2.0, 3.0, 4.0), 2)[0] - 1.5) < 0.0001
        assert abs(sma(list(1.0, 2.0, 3.0, 4.0), 2)[2] - 3.5) < 0.0001
        assert abs(mean(list(2.0, 4.0, 6.0)) - 4.0) < 0.0001
        // rising series → fast SMA above slow → bullish
        assert crossover(list(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), 2, 4) == "bullish"
        // falling series → bearish
        assert crossover(list(6.0, 5.0, 4.0, 3.0, 2.0, 1.0), 2, 4) == "bearish"
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check moving_average.cell
✓ All checks passed.
$ soma test moving_average.cell
test MovingAverageTests ...
  ✓ assert len(sma(list(1.0, 2.0, 3.0, 4.0), 2)) == 3
  ✓ assert abs(sma(list(1.0, 2.0, 3.0, 4.0), 2)[0] - 1.5) < 0.0001
  ✓ assert abs(sma(list(1.0, 2.0, 3.0, 4.0), 2)[2] - 3.5) < 0.0001
  ✓ assert abs(mean(list(2.0, 4.0, 6.0)) - 4.0) < 0.0001
  ✓ assert crossover(list(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), 2, 4) == "bullish"
  ✓ assert crossover(list(6.0, 5.0, 4.0, 3.0, 2.0, 1.0), 2, 4) == "bearish"

6 tests: 6 passed, 0 failed
$ soma verify moving_average.cell
Verifying moving_average.cell...
No state machine in this program: nothing to prove beyond `soma check` (invariants and lifecycles are what verify proves).
VERIFY OK (vacuous — no state machine)

Builtins used

abs, len, list, push, range, sum

Source file: /corpus/finance/moving_average.cell · examples/corpus/finance/moving_average.cell