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.
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:
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_tickis 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.pricesis a dictionary you fill, and if you do not fill it, you have no history. ctxis 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:
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:
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
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
- Open Strategies and load the SMA Crossover (Beginner) template.
- 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.
- 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.
10and20are the template's; try5/50and 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
- CodeBull visual block reference — The same logic, without writing Python.