Quant Trading for Programmers 10: Run The First Minimal Backtest Loop
Quant Trading for Programmers 10: Run The First Minimal Backtest Loop
The previous articles can finally be connected.
Article 4 has A-share order rules. Article 8 has cleaned K-lines. Article 9 has factor signals. Article 10 builds the first minimal backtest loop: buy when buy_watch appears, sell when risk_watch appears, and record cash, positions, trades, and the equity curve along the way.

This Is Not A Full Backtest Engine
Start with the boundary.
This article does not build an event-driven engine. It does not handle matching queues, minute bars, slippage models, or multi-symbol portfolio optimization. It is only an explainable minimal loop.
It needs to prove several things:
- Cleaned K-lines can enter factor calculation.
- Factor signals can drive buy and sell actions.
- Buys pass through A-share board-lot and cash checks.
- Sells calculate fees.
- Every day has an equity-curve record.
- Backtest results can be reproduced by tests.
It is useful to state a few boundaries clearly. Backtesting is not simply “run a strategy on historical data”. At minimum, it separates signal generation, order constraints, execution assumptions, and account accounting. This article assumes daily close-price execution, not minute-level matching. It handles a single-symbol position, not portfolio-level cash sharing. But it already includes A-share board lots and fees, so it avoids obviously non-executable trades.
Backtest Result Objects
Chapter 10 adds app/mini_backtest.py.
The trade object:
@dataclass(frozen=True)
class MiniBacktestTrade:
trade_date: date
side: str
price: float
shares: int
amount: float
fee: float
reason: str
The backtest result object:
@dataclass(frozen=True)
class MiniBacktestResult:
symbol: str
initial_cash: float
final_equity: float
cash: float
shares: int
total_return: float
max_drawdown: float
trades: tuple[MiniBacktestTrade, ...] = field(default_factory=tuple)
equity_curve: tuple[dict[str, object], ...] = field(default_factory=tuple)
reason matters. A trade is not only buy or sell. We also need to know why it happened. Later strategy diagnostics will depend on this field.
Buying Uses A-Share Rules
The buy logic does not deduct cash directly. It reuses check_a_share_order() from article 4:
check = check_a_share_order(
side="buy",
price=bar.close,
shares=raw_shares,
available_cash=cash,
)
if check.accepted:
cash = round(cash - check.amount - check.fee.total, 2)
shares += check.normalized_shares
This handles board lots of 100 shares, fees, and insufficient cash.
If backtesting bypasses trading rules, it may buy 37 shares or drive cash negative. The final return may look good, but it would not be executable in A-shares.
Selling Also Deducts Fees
Selling currently liquidates the position and calculates sell-side fees:
fee = estimate_a_share_fee(round(bar.close * shares, 2), "sell")
amount = round(bar.close * shares, 2)
cash = round(cash + amount - fee.total, 2)
The article 4 sell-side fee includes commission, transfer fee, and stamp duty. Reusing it avoids writing one set of rules for backtesting and another for paper trading.
Record The Equity Curve Every Day
Whether there is a trade or not, equity is recorded:
equity = round(cash + shares * bar.close, 2)
equity_curve.append({
"trade_date": bar.trade_date.isoformat(),
"cash": cash,
"shares": shares,
"equity": equity,
"signal": factor.signal,
})
Backtesting is not only about final return. The equity curve tells us what happened along the path, and max drawdown is calculated from it:
def _max_drawdown(equity_values: list[float]) -> float:
peak = None
worst = 0.0
for value in equity_values:
peak = value if peak is None else max(peak, value)
if peak:
worst = min(worst, value / peak - 1)
return round(worst, 6)
Real Run Screenshot
Article 10 can directly run the current mainline integrated example:
uv run python -m scripts.chapter_examples factor-backtest --source sample
The screenshot below comes from the minimal-backtest section of the same command:

The output shows 000001.SZ starting with 100000, ending with final equity of 105031.28, total return around 5.03%, max drawdown around -2.58%, and 1 buy trade. The return number is not the point. The point is that the signal has become real account fields: buy date, shares, price, reason, cash, positions, and equity curve can all be reproduced by code.
Test The First Loop
Chapter 10 tests cover four scenarios.
With no data, cash stays unchanged:
result = run_signal_backtest("600519.SH", [], initial_cash=100000)
assert result.final_equity == 100000
assert result.trades == ()
A stable uptrend produces a buy:
closes = [10 + index * 0.1 for index in range(40)]
result = run_signal_backtest("600519.SH", _bars("600519.SH", closes), initial_cash=100000)
assert result.trades[0].side == "buy"
assert result.trades[0].shares % 100 == 0
A rise followed by a fall produces buy and sell:
assert [trade.side for trade in result.trades] == ["buy", "sell"]
assert result.shares == 0
A mismatched symbol should not trade. This prevents multi-symbol backtests from accidentally using another stock’s bars for the current symbol.
Hands-On Task
Clone the chapter 10 code:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-10
uv sync --extra dev
uv run pytest
Run only the backtest tests:
uv run pytest tests/test_mini_backtest.py
The full chapter 10 test suite passes with 168 passed, still with only the existing FastAPI deprecation warning.
Stage Review For Articles 6-10
The second group of five articles completes the loop from data to a minimal backtest.
Article 6 turns PostgreSQL schema and Alembic migrations into checkable objects, so platform table structure does not depend only on manual review.
Article 7 breaks A-share stock-universe construction into pure functions, handling symbol normalization, ST/delisting filters, deduplication, size limits, and source summaries.
Article 8 cleans raw K-lines into unified CleanMarketBar objects, explicitly handling dates, numbers, OHLC consistency, duplicate rows, and volume units.
Article 9 calculates return, moving averages, momentum, and volatility on clean K-lines, then generates the first explainable signals.
Article 10 connects those signals with A-share order rules and runs buy, sell, cash, positions, fees, equity curve, and max drawdown.
This group does not jump to “strategy-return optimization”. It first fills in the runnable foundation. The third group will continue with multi-symbol backtests, universe traversal, backtest metrics, strategy parameters, and result persistence.
Chapter Update And Repository
This chapter adds:
app/mini_backtest.py.- A connection between article 8 market-data cleaning, article 9 factor signals, and article 4 A-share order rules.
tests/test_mini_backtest.py, covering empty data, buys, sells, and symbol isolation.- A real current-mainline screenshot showing how article 9 signals enter the article 10 backtest.
- Backtest-boundary notes on signal generation, order constraints, execution assumptions, and account accounting.
- A stage review for articles 6-10.
Repository:
https://github.com/ax2/zi-quant-platform
Code for this chapter:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-10
uv sync --extra dev
uv run pytest tests/test_mini_backtest.py
Summary
The first backtest loop does not need to be complex, but it must be real.
It must respect the stock universe, data cleaning, factor windows, A-share board lots, fees, cash, and positions. Only after these basic constraints are in code does it make sense to talk about strategy optimization.
Follow ZiCode on WeChat
If this post was useful, you can follow later updates on WeChat as well.
X / Twitter
Follow @ax2_zicode
Faster technical notes, short thoughts, and new-post alerts are posted on X.
More in this column
- Quant Trading for Programmers 14: Save Strategy Candidates As Experiment Records
- Quant Trading for Programmers 13: Add A First Grid Search For Strategy Parameters
- Quant Trading for Programmers 12: Final Return Is Not Enough
- Quant Trading for Programmers 11: From One Stock To A Portfolio Backtest
- Quant Trading for Programmers 09: From K-Lines To The First Explainable Factor Signal
- Quant Trading for Programmers 08: Clean Raw K-Lines Into Trustworthy Market Bars
- Quant Trading for Programmers 07: Build A Clean A-Share Stock Universe First
- Quant Trading for Programmers 06: Make The Database Schema Explicit First