What the program sees
The market does not hand my laptop a “buy” flag. It hands someone a tape of trades, which data vendors then pack into bars. A daily bar has five numbers I care about: open, high, low, close, and volume. That bundle is usually called OHLCV. My beginner pipeline only used the close for indicators, but I still stored the rest so I could pretend to fill at the next open instead of the same close I just used to make the decision.
| Date | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| 2024-01-02 | 185.2 | 186.9 | 184.1 | 185.6 | 52.1M |
| 2024-01-03 | 185.4 | 185.8 | 181.9 | 182.3 | 61.4M |
| 2024-01-04 | 182.1 | 184.0 | 181.5 | 183.7 | 48.8M |
| 2024-01-05 | 183.9 | 186.2 | 183.4 | 185.9 | 44.2M |
| 2024-01-08 | 186.0 | 187.5 | 185.1 | 187.1 | 39.6M |
Table 1. Example daily bars, made up so the page has a concrete shape. Real files look like this, only longer. Yahoo Finance and similar sources export this layout (Yahoo Finance, n.d.).
Four stages
01
Ingest
Read a CSV of daily bars. One row per date.
02
Transform
Compute SMA, RSI, and returns from closes only.
03
Decide
Apply a rule using yesterday’s indicator, not today’s.
04
Execute
Log a fill at the next open in the notebook. No broker.
I treated these as a pipeline, not as four separate apps. One script reads the file, computes columns, writes a signal column, then walks that column to grow a cash account. If I split it too early I ended up with three notebooks that did not agree on the date index.
Lookahead, in plain words
The most important engineering bug in this project is using information you would not have had at the time. If I compute today’s SMA including today’s close, then “buy today at today’s close,” I have cheated. In real life the close is the last print of the day. You cannot also trade at that print with a signal that needed that print. So the rule I used is:
- At the close of day t, compute SMA(t) and RSI(t).
- If the rule says buy, the fill is the open of day t + 1.
- Returns between t and t + 1 still belong to the old position.
That delay makes the backtest worse than the fantasy version. That is the point. Quantpedia’s write-ups of timing rules are careful about when the signal is known versus when the trade happens (Quantpedia, 2023). I copied that habit even though my series is only illustrative.
Pseudocode I actually followed
for each day t in the file:
sma50[t] = mean(close[t-49 : t]) # need 50 days
if close[t] > sma50[t]:
signal[t] = LONG
else:
signal[t] = CASH
position[t] = signal[t-1] # trade next day
if position[t] == LONG:
equity[t] = equity[t-1] * (1 + r[t])
else:
equity[t] = equity[t-1] # cash earns 0 hereCash earning 0% is a student shortcut. Faber’s monthly timing often parks in T-bills when the rule is out (Faber, 2007). I did not model a bill rate. If I had, the SMA rule would look a little better in the long cash stretches, not worse.
Costs and why I left a line for them
Every time the signal flips I should subtract a small cost: spread, commission, maybe slippage. I used a placeholder of 5 basis points per flip in one notebook (0.05% of the traded value). On 7 flips that is not huge. On 80 flips it would eat the whole project. This is why I prefer a slow 50-day rule over a 5-day rule for a first test. Faster rules look smarter until you bill them.

I did not connect a broker. For this study a CSV plus a notebook is the system. The architecture still matters: same formulas, same date alignment, same “decide on t, trade on t + 1” rule I would keep if I ever sent an order.