Your first strategy bot: a moving-average crossover in Python

Build it, backtest it, and read the report — step by step, with simulated money. The best place to start if you've never written a trading bot.

Illustrative crossover — simulated

A moving-average crossover is the "hello world" of strategy code. Two averages, one fast and one slow. When the fast one crosses above the slow one, you buy. When it crosses back below, you sell.

It is not a good strategy. It is a legible one — every line does something you can point at — and that makes it the right first thing to write.

This exact strategy ships as a CodeBull template called SMA Crossover (Beginner). Everything below is that template, so nothing here is code you cannot load and run.

The shape of a CodeBull strategy

A strategy is a Python class with one method the engine calls:

sma_crossover.pypython
class Strategy:
    def __init__(self):
        self.prices = {}
        self.position = {}
        self.fast_period = 10
        self.slow_period = 20

    def on_new_tick(self, symbol, price, ctx):
        ...

Three things follow from that shape and are worth internalising early:

  • on_new_tick is called once per price update, per symbol. Not once per day, not once per bar you asked for — once per update the engine has for that symbol.
  • You keep your own state. There is no framework object holding your price history. self.prices is a dictionary you fill, and if you do not fill it, you have no history.
  • ctx is how you touch the world. Orders, logging, current position. Nothing else reaches out of the strategy.

Keeping the history you need

Two moving averages over the last 10 and 20 prices need the last 20 prices. So the first thing on_new_tick does is store one:

sma_crossover.pypython
def on_new_tick(self, symbol, price, ctx):
    if symbol not in self.prices:
        self.prices[symbol] = []
        self.position[symbol] = False

    self.prices[symbol].append(price)

    max_history = self.slow_period + 5
    if len(self.prices[symbol]) > max_history:
        self.prices[symbol] = self.prices[symbol][-max_history:]

The trim matters more than it looks. A strategy that runs for a long time on a busy symbol and never drops old prices grows a list forever. Twenty-five prices is everything this strategy can use; the rest is memory you are paying for and never reading.

Waiting for enough data

Before there are 20 prices, a 20-period average is not a smaller average — it is a different number that means nothing. So the strategy refuses to act:

sma_crossover.pypython
    if len(self.prices[symbol]) < self.slow_period:
        ctx.log(f"📊 Warming up: {len(self.prices[symbol])}/{self.slow_period} prices collected")
        return

ctx.log writes to the run's log, which you can read afterwards. Logging the warm-up is not decoration: the first time a strategy of yours produces no trades at all, the log telling you it never left warm-up will save you an hour.

The averages, and the decision

sma_crossover.pypython
    prices = self.prices[symbol]
    fast_sma = sum(prices[-self.fast_period:]) / self.fast_period
    slow_sma = sum(prices[-self.slow_period:]) / self.slow_period

    if fast_sma > slow_sma and not self.position[symbol]:
        ctx.place_order(symbol, "buy", 10, "market")
        self.position[symbol] = True

    elif fast_sma < slow_sma and self.position[symbol]:
        ctx.place_order(symbol, "sell", 10, "market")
        self.position[symbol] = False

ctx.place_order(symbol, side, quantity, order_type) takes a share quantity, not a fraction of your cash — 10 means ten shares. The self.position[symbol] flag is what stops the strategy buying again on every single tick while the fast average stays above the slow one. Without it you would place an order per tick until the cash ran out.

That and not self.position[symbol] clause is doing the work of an entire state machine, which is why this strategy is a good first one and a bad second one.

Running it

  1. Open Strategies and load the SMA Crossover (Beginner) template.
  2. Pick a symbol. The template suggests AAPL, MSFT or GOOGL on daily bars over about 60 days, which is enough for the 20-period average to warm up and still leave room for a few crossings.
  3. Run the backtest. Read the report.

The report will tell you how many trades it made, what the equity curve did, and what each trade cost. All of it on simulated money.

What to expect, honestly

A crossover strategy makes money when a price trends and loses it when a price chops. That is not a subtle property — it is the whole behaviour of the rule. In a sideways market the fast average crosses the slow one repeatedly, each crossing produces a trade, each trade costs something, and the equity curve grinds downward while the price goes nowhere.

You will see this. It is the strategy working correctly.

The interesting question is not "did it make money over these 60 days" — the answer to that is mostly which 60 days you picked. The interesting question is whether it made the trades you expected it to make, on the days you expected. Open the trade list and check a few against the chart.

Three things to change next

  • The periods. 10 and 20 are the template's; try 5/50 and watch the trade count collapse.
  • The quantity. Ten shares is ten shares whether the price is $3 or $300. Sizing by value rather than count is the first real improvement.
  • A second condition. The classic complaint about crossovers is that they fire in flat markets. Adding a condition that requires the price to have actually moved is a rule you can write in five lines.

If you would rather assemble this without writing Python, the same logic exists as visual blocks, running on the same engine.

Once you have a report you like, read what a backtest can — and can't — tell you before you believe it.


Educational content, not financial advice. This strategy is a teaching example, not a recommendation, and results on simulated money do not predict real returns.

References

Build this in CodeBull

Write the strategy, backtest it on simulated money, read the report.

Get started free →