The smallest useful backtest
Learn enough Python to complete one reproducible loop.
- Read a CSV with parsed dates, sort the rows, and decide how to handle duplicates and missing observations.
- Compute arithmetic returns and a rolling indicator.
- Convert the indicator into a target position such as zero or one.
- Specify when the target can be acted on. Align the held position with the return interval it earns.
- Charge a cost when the position changes, compare with a simple benchmark, and reserve a later time period for evaluation.
Assume a signal becomes available at each close. For a simplified test, model execution at the following close. A signal first turning on at Monday's close leads to entry at Tuesday's close, so the first full close-to-close return it can earn runs from Tuesday to Wednesday. Friday's P&L can be checked by locating Thursday's executed position, Friday's return, and any new trade cost on Friday.
You should be able to point at one row and say which position was already held, what price movement it earned, and which fee was charged on that row. Do that manually before vectorising the calculation. For a long-or-cash strategy, three columns for signal, executed position and held position are usually clearer than a clever expression with multiple shifts. Add a fourth column for absolute position changes. A no-trade test should leave that column and total costs at zero. Those checks will take you further than learning advanced Python syntax before building anything.
A minimal signal with delayed execution
import pandas as pd
prices = pd.read_csv("prices.csv", parse_dates=["date"]).set_index("date")
close = prices.sort_index()["close"].dropna()
returns = close.pct_change(fill_method=None)
average = close.rolling(20, min_periods=20).mean()
target = (close > average).astype(float)
executed = target.shift(1, fill_value=0)
held = executed.shift(1, fill_value=0)
turnover = executed.diff().abs().fillna(0)
net = held * returns - 0.001 * turnover
print(pd.DataFrame({"target": target, "held": held, "net": net}).tail())