Chapter 8 of 10
Build a trading bot
A trading bot is a set of rules that a computer follows without flinching. Below you can write one, run it for two simulated years against the market from the earlier chapters, and see what it really earned after the spread, slippage and fees.
What a trading bot actually is
Strip away the marketing and every automated strategy, from a hobby script to a hedge fund, has the same four parts.
- A signal. A rule that says when the odds favour buying or selling. "The price is above its 50-day average." "The stock fell three standard deviations in a day." "This company's earnings surprised." The signal is the part people argue about, and the part that is hardest to get right.
- Sizing. How much to buy when the signal fires. All in, a fixed amount, or more when the signal is stronger and less when markets are wild. Sizing decides whether a good idea makes money or a bad week ends the account.
- Execution. How the order actually reaches the market. A market order is fast and pays the spread; a limit order is cheaper but may never fill. Big orders get sliced up so they do not push the price against themselves.
- Risk limits. The rules that override everything else: a maximum position, a daily loss at which it stops, a kill switch. In 2012 a market maker called Knight Capital lost about $440 million in 45 minutes when old code went live by accident. Risk limits exist for that morning.
The bot below has all four, kept simple. The signal is whatever rule you pick or write. Sizing is all in or all out (you can change that in code). Execution is a market order sent the moment a trading day closes, straight into the simulated order book, so it pays whatever the book charges. The risk limit is that it cannot borrow: it can only hold as many shares as its account can pay for.
The bots that really run markets
Most of the trading volume in US stocks is placed by computers, but very little of it looks like the bots in online ads. Professional algorithms come in a few families.
- Execution algorithms do not try to predict anything. A pension fund that needs to buy two million shares hands the order to an algorithm whose only job is to do it cheaply. TWAP (time-weighted average price) spreads the order evenly across the day. VWAP (volume-weighted average price) trades more when the whole market trades more, so the order hides in the crowd. Others chase "implementation shortfall", the gap between the price when you decided and the price you got.
- Market makers, from Chapter 4, quote both sides thousands of times a second and earn the spread, minus whatever informed traders take from them.
- Statistical arbitrage funds look for tiny, temporary mispricings between related things: two similar stocks, an index and its members, a fund and what it holds. Each trade earns almost nothing; they do millions.
- Trend followers (often called managed futures or CTAs) run rules a lot like the moving-average bot below, across hundreds of markets at once, because one market alone is far too noisy to bet on.
What separates them from a hobby bot is mostly unglamorous: clean data that includes companies which went bust, careful accounting for every cost, servers placed in the same building as the exchange (colocation) so their orders arrive microseconds sooner, and teams whose whole job is to prove the strategy wrong before money goes in.
A daily bot like this one is not competing on speed; it cannot. Whatever edge it has has to come from the signal, and it has to survive the costs.
The bot lab
500 trading days, about two years, in a simulated market. Changes rerun automatically.
How to read the result
The first number everyone looks at is the return. It is also the least informative on its own. Read the panel in this order instead.
Compare with buy and hold. The dashed line is what you would have had by buying on day one and doing nothing. In a market that drifts upward, a bot that sits in cash half the time will usually trail it, simply because it missed half the rise. That is not automatically bad (see the next point), but if your bot cannot beat doing nothing, the work bought you nothing.
Then look at the risk. The worst drawdown is the deepest fall from a peak before a new high. A bot that made 15% with a 10% drawdown may be better than buying and holding for 25% with a 40% drawdown, because almost nobody actually sits through a 40% fall without selling at the bottom. The Sharpe ratio rolls return and bumpiness into one number: the average daily return divided by how much daily returns swing, scaled to a year (252 trading days). Treat anything above 1 over a short test with suspicion.
Then count the trades. Ten trades in two years is a sample of ten. One lucky trade can make the whole result. The win rate is easy to misread: a strategy that wins 80% of the time and loses big the other 20% is a classic way to blow up.
Then look at what you paid. Spread and slippage is the money lost by crossing the spread and walking the book, measured against the mid price at the moment the bot traded. Commissions are the fee per order. Both are certain; the profit they pay for is not.
Costs are the silent partner
Every round trip, a buy and the sell that closes it, costs at least one full spread plus two commissions. For a liquid stock that might be a few hundredths of a percent. It sounds like nothing until it compounds: a bot making 50 round trips a year at 0.1% each gives up about 5% a year before it earns anything, which is roughly half of what the stock market has returned on average over the long run.
Size makes it worse. Switch the account to $10M and run the same strategy. The bot's market orders are now much bigger than what sits at the best price, so each one eats through several levels of the book and pays more per share. That is price impact, and it is why strategies that look wonderful on paper often have a "capacity": a size beyond which they stop working. It is also why execution algorithms exist.
The simulation leaves out one more cost you would face for real: tax. In the US, gains on investments held a year or less are taxed as ordinary income, while gains on investments held longer are taxed at lower long-term rates. A strategy that trades often turns what could be long-term gains into short-term ones, every year.
Things to try
- Run the coin flip on a few markets. Sometimes it wins. Keep that in mind every time something else wins.
- Pick the flat or falling market and watch "beating buy and hold" become easy, because buying and holding is losing. Beating a falling benchmark while still losing money is not much of a victory.
- Turn on the sluggish market and try a breakout with short windows, or a moving-average crossover with a fast average of 2 or 3 days. Here the simulated market really does absorb news slowly, so short trends persist, and momentum rules have a genuine edge. Then raise the commission and watch the edge shrink.
- Try mean reversion in the normal market. It wins often and still tends to trail buying and holding: most of its time is spent in cash, waiting for a drop.
- Write your own. Open the code, change a rule, and run it. Use
ctx.log()to see what your bot was thinking.
The code
A bot is a function called onBar. It runs once at the close of every trading day and receives ctx, short for context. Anything you declare outside onBar persists from one day to the next, so you can keep your own state. Orders are market orders that fill immediately against the book; the price you get is whatever is there.
| Name | What it is |
|---|---|
ctx.day | Trading day, from 0 to 499. |
ctx.open, high, low, close, volume | Today's bar: first, highest, lowest and last trade price, and shares traded. |
ctx.closes | Every closing price so far, oldest first. Includes 120 days of history before day 0, so long averages work immediately. |
ctx.bid, ctx.ask, ctx.price | Best bid, best ask and last trade right now. |
ctx.position, ctx.cash, ctx.equity | Shares held (negative if short), cash, and total account value. |
ctx.maxShares() | The most shares the account can hold, long or short, without borrowing. |
ctx.buy(n), ctx.sell(n) | Send a market order. Orders beyond the limit are trimmed. |
ctx.setPosition(n) | Buy or sell whatever it takes to hold exactly n shares. |
ctx.sma(n), ctx.ema(n) | Simple and exponential moving average of the last n closes. |
ctx.stdev(n) | Standard deviation of the last n closes: how much they spread out. |
ctx.highest(n), ctx.lowest(n) | Highest and lowest close over the last n days, today included. |
ctx.rsi(n) | Relative strength index, 0 to 100: the share of recent movement that was upward. |
ctx.random() | A random number between 0 and 1, the same sequence every run of a market. |
ctx.log(...) | Write a line to the log under the results (first 200 lines). |
Your code runs in a separate background thread in your browser. If it runs longer than eight seconds, it is stopped, so an accidental endless loop will not freeze the page. Nothing you write leaves your computer.
What this simulation leaves out
The market here follows real rules, but it is one invented stock with traders built from a handful of formulas. Real prices jump overnight on earnings and news; real volatility clusters into calm years and violent weeks; spreads widen in a crisis exactly when you most need to trade; shorting costs a borrow fee; companies pay dividends, split and occasionally go to zero. A strategy that works here has passed a far easier test than the real one.
That is the less important problem, though. The bigger one is that any single backtest, here or on real data, is one sample of a noisy process, and the more ideas you try on it, the more likely the winner is just the luckiest. The next chapter is about that trap.