Lesson 0.2.6 · 40 min
Introduction to AI coding assistants: prompts, review and tests
An assistant writes a trading signal that looks correct in seconds. This lesson follows a complete case: the function it proposes, the spectacular backtest it produces, the test that proves it uses future data and the prompt that would have prevented it.
- Module
- 0.2 · Modern working environment
- Duration
- 40 min
- Prerequisites
- 0.1.3 · Course structure, entry tests, personal repository and learning with AI, 0.2.3 · Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
- Author
- Jesús Cuesta
- Updated
Level 0 lessons
0.1 · Welcome and expectations
0.2 · Modern working environment
- 0.2.1 Installing Python with uv: versions and support cycle
- 0.2.2 Virtual environments and dependencies: uv init, uv add, pyproject.toml and lockfiles
- 0.2.3 Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Basic terminal, paths, environment variables and .env files
- 0.2.5 Git and GitHub: commits, branches, pull requests and .gitignore
- 0.2.6 Introduction to AI coding assistants: prompts, review and tests
0.3 · Language fundamentals
- 0.3.1 Variables, types and operators; floating point and decimal for prices and ticks
- 0.3.2 Strings, f-strings and formatting numbers and dates
- 0.3.3 Lists, tuples, dictionaries, sets and comprehensions
- 0.3.4 Control flow: if, elif, else, for, while and match-case
- 0.3.5 Functions: arguments, *args and **kwargs, lambdas and scope
- 0.3.6 Exceptions; modules and packages, with Python 3.15 lazy imports
0.4 · Intermediate Python
- 0.4.1 Object-oriented programming: classes, inheritance, composition and dataclasses
- 0.4.2 Typing: type hints, mypy or ty, and Pydantic for configurations
- 0.4.3 Iterators, generators, decorators and context managers
- 0.4.4 Files (CSV, JSON, YAML, TOML) and dates with time zones
- 0.4.5 Professional logging and testing with pytest
- 0.4.6 Concurrency: threading, multiprocessing, asyncio and free-threading
0.5 · Numerical computing and data
- 0.5.1 NumPy 2: arrays, broadcasting, vectorization, linear algebra and random generators
- 0.5.2 pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()
- 0.5.3 Selection, filtering, groupby, merge, pivot, resample and moving windows
- 0.5.4 Polars: lazy versus eager API, expressions, performance and when to pick it over pandas
- 0.5.5 DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet
0.6 · Visualisation
0.7 · Minimum mathematics and statistics
What a coding assistant is
A coding assistant is a language model trained on huge amounts of code and text that proposes code from a description and from the context you give it. There are three ways to use one, which differ in how much it does on its own:
| Mode | What it does | Examples | What to review |
|---|---|---|---|
| Autocomplete | Suggests the line or function you are writing | Copilot in VS Code, Cursor | Every suggestion before accepting it |
| Chat | Answers questions and writes snippets that you copy | Editor chat, Claude, ChatGPT | The whole snippet and how it fits into your code |
| Agent | Reads the repository, edits files and runs commands and tests | Claude Code, Codex, Cursor's agent | The complete diff, like a pull request (lesson 0.2.5) |
All three share one property: the code they produce is plausible. It looks like code that would solve the problem, with reasonable names and convincing comments. Often it is also correct. But the model does not know your data, does not know at what time your signal is traded and has not run your backtest. Whatever you do not tell it, it decides without warning.
In trading this has a concrete cost. A normal programming error usually breaks something: an exception, an empty chart. A look-ahead error (lesson 0.1.1) breaks nothing: it produces a better backtest. It is the error that is hardest to see and the one that loses the most money, because it is convincing.
Lesson 0.1.3 set three rules for using assistants. They still stand, and this lesson explains the reason behind the second one:
- Level 0 without an assistant. You write the code for the exercises yourself. You can ask a chat conceptual questions. This lesson does not change that: it teaches you to judge generated code, which is exactly what you cannot do without knowing how to write it.
- After that, the AI proposes and you decide: first the test, then the code, a full read, tests and commit.
- Nothing sensitive in the prompt: no keys, no data under a restrictive license, no personal data.
The rest of the lesson is a complete case, of the kind that happens every day.
A case: the signal the assistant proposes
A user asks a chat, with no further context:
Write a pandas function that gives a buy signal when the 20-session moving average of the closing price has a positive slope. I want a smooth average with no lag.
The prices are those of SINT, the course's fictional ETF:
import pandas as pd
precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
index_col="fecha")
cierre = precios["cierre"]
rent = cierre.pct_change().fillna(0)
print(len(cierre), "sessions;", cierre.index[0].date(), "to",
cierre.index[-1].date())1303 sessions; 2021-01-04 to 2025-12-31The answer, with the typical look of an assistant's output (docstring, clear names, a comment that justifies the decision):
def senal_pendiente_ia(cierre: pd.Series,
ventana: int = 20) -> pd.Series:
"""1 if the moving average rises, 0 otherwise.
A centered average is used to remove the lag."""
media = cierre.rolling(ventana, center=True).mean()
return (media.diff() > 0).astype(int)The user does what lesson 0.1.1 taught: shifts the position one day so that it trades after the signal, and measures the result against buy and hold:
def backtest(senal: pd.Series) -> pd.Series:
"""Daily return; the signal at the close is traded the next
day."""
posicion = senal.shift(1).fillna(0)
return posicion * rent
def resumen(r: pd.Series) -> str:
total = (1 + r).prod() - 1
sharpe = r.mean() / r.std() * 252 ** 0.5
return f"total {total:+7.1%} Sharpe {sharpe:5.2f}"
r_ia = backtest(senal_pendiente_ia(cierre))
print(f"{'Assistant':<20}", resumen(r_ia))
print(f"{'Buy and hold':<20}", resumen(rent))Assistant total +324.1% Sharpe 2.10
Buy and hold total +43.5% Sharpe 0.48A Sharpe above 2 with a moving average, on an asset that over the same period stays at 0.48. The curve in the header figure (in amber) rises with almost no pullbacks. This is the moment to be suspicious: a rule this simple should not beat the market like that, least of all on an asset with a 25% drawdown in 2022.
The highlighted line is the problem: center=True. By default, rolling labels each window with its last day: the
average for June 3 uses the 20 closes that end on June 3. With center=True, the label is the center of the window:
the average for June 3 uses ten earlier closes, the close of that day and nine later ones. “No lag”, yes, because it
looks into the future. You can check which days go into each window by replacing the prices with their position in the
series:
pos = pd.Series(range(len(cierre)), index=cierre.index)
dia = pd.Timestamp("2024-06-03")
for centrada in (False, True):
v = pos.rolling(20, center=centrada)
primero = cierre.index[int(v.min()[dia])].date()
ultimo = cierre.index[int(v.max()[dia])].date()
print(f"center={centrada!s:<5}: from {primero} to {ultimo}")center=False: from 2024-05-07 to 2024-06-03
center=True : from 2024-05-20 to 2024-06-14With the centered average, the June 3 signal knows how the market closed up to June 14. Shifting the position one day does nothing: the future is already inside the signal, not in the lag. And nothing in the code gives it away at a glance; the comment even presents it as an advantage.
A test that detects it
Careful reading helps, but it does not scale: there are dozens of ways to sneak the future into a signal
(center=True, shift(-1), filling gaps backwards with bfill, normalizing with the mean of the whole sample…). A
test that does not depend on the form of the error is better. The idea: a day's signal cannot change when later data
is added. If it is computed with the data up to a date and then with all the data, the signals for those dates must
match:
def comprobar_sin_anticipacion(funcion, cierre: pd.Series,
cortes=(250, 500, 750, 1000)):
completa = funcion(cierre)
for n in cortes:
parcial = funcion(cierre.iloc[:n])
distintos = int((parcial != completa.iloc[:n]).sum())
assert distintos == 0, (
f"up to {cierre.index[n - 1].date()}: {distintos} signal "
"days change when later data is added")It is a function with simple assert statements: if the condition does not hold, it raises an error with the message.
With pytest (lesson 0.4.5) it becomes a test that runs automatically before every commit. Applied to the assistant's
function:
comprobar_sin_anticipacion(senal_pendiente_ia, cierre)AssertionError: up to 2021-12-17: 9 signal days change when later data is addedNine signals from December 2021 change when the closes after December 17 are added. The test knows nothing about centered averages: it only checks a property that every honest signal satisfies. That is why it also works for the next function an assistant proposes to you, whatever its error.
The fix is to remove center=True: each day's average uses that day and the 19 before it. It has lag, and that lag is
the price of not knowing the future. The corrected version passes the test:
def senal_pendiente(cierre: pd.Series,
ventana: int = 20) -> pd.Series:
"""1 if the moving average (past data only) rises; 0 otherwise."""
media = cierre.rolling(ventana).mean()
return (media.diff() > 0).astype(int)
comprobar_sin_anticipacion(senal_pendiente, cierre)
print("No look-ahead")
r_ok = backtest(senal_pendiente(cierre))
print(f"{'Corrected':<20}", resumen(r_ok))No look-ahead
Corrected total -8.9% Sharpe -0.07From +324% to −8.9%. The real strategy does not beat buy and hold; the assistant's never existed. The three curves in the header figure come from these cells. Notice that the corrected version is worse: a well-made test often worsens the result, because it removes a false edge. If your backtest improves after you fix an error, check twice.
The prompt that would have prevented it
The first prompt left the important decisions to the model. A better one gives the context, the data contract, the explicit time rule and the test it has to pass:
Context: daily backtest in pandas 3.0.
cierreis apd.Seriesof daily closes with a sorted date index and no gaps.Task: a function
senal_pendiente(cierre, ventana=20) -> pd.Seriesthat returns 1 if the simple moving average ofventanasessions rises relative to the previous day and 0 otherwise, with the same index ascierre.Time rule: the signal for day t may only use closes up to and including t. No
center=True, negativeshift,bfillor whole-sample statistics. The position is shifted outside the function.It must pass this test: (paste
comprobar_sin_anticipacion).No new dependencies. Explain in one sentence what each line does.
This is what rule 2 of the course asks for: you write the test, beforehand, and the assistant receives it as part of the task. If the same assistant wrote the test and the function, the test could inherit the same misunderstanding. The last line, asking it to explain each line, guarantees nothing, but it makes the full read you have to do anyway easier.
With an agent: instructions and permissions
An agent does not receive a standalone prompt: it works inside the repository, reads files, runs commands and changes several files at once. That changes two things.
The first is context. What you would repeat in every prompt in a chat is written once in an instructions file in the
repository, which the agent reads when it starts: CLAUDE.md in Claude Code, AGENTS.md in Codex and many
others1Note 1 · Claude Code Docs, “Cómo Claude recuerda tu proyecto” (How Claude remembers your project,
code.claude.com/docs/es/memory), and the open AGENTS.md format (agents.md), which lists the compatible agents;
accessed October 21, 2026.. For the course project, a few lines are enough:
# ppt-curso project rules
- Python 3.14 with uv. Dependencies only with `uv add`, never pip.
- Signals: the value for day t uses data up to the close of t.
The position is the signal shifted one day. Forbidden:
center=True, negative shift, bfill and whole-sample statistics.
- Every new signal passes comprobar_sin_anticipacion (tests/).
- Before finishing: `uv run pytest`. Do not change a test to
make it pass.
- Do not read or print `.env`. No keys in the code.The line about tests is not decoration: an agent asked to “make the tests pass” can do it by changing the test instead of the code. That is why the diff is reviewed in full, tests included.
The second is permissions. An agent that runs commands can read any file you have access to, including .env. While
you are learning: paper keys or none, work on a branch and changes that go in through a pull request, like yours
(lesson 0.2.5). Never an agent with access to a real brokerage account.
What to review in generated code
When reviewing generated code for trading, this list covers the most common errors:
| Review | Red flags |
|---|---|
| Look-ahead | center=True, shift(-n), bfill, means or deviations over the whole sample, models fitted on all the data |
| Lag | Signal and return of the same day multiplied without shift(1) |
| Costs | No commission or slippage; daily trades at no cost |
| Gaps | fillna(0) on prices, a dropna that misaligns series, holidays |
| Dependencies | Packages you do not know: check them on PyPI before uv add |
| Secrets | Keys written in the code or printed in logs (lesson 0.2.4) |
Exercises
- Without running it, say whether this signal has look-ahead bias and why; then check it with
comprobar_sin_anticipacion:(cierre > cierre.mean()).astype(int).
Solution
def senal_sobre_media_total(cierre: pd.Series) -> pd.Series:
return (cierre > cierre.mean()).astype(int)
try:
comprobar_sin_anticipacion(senal_sobre_media_total, cierre)
except AssertionError as error:
print("Look-ahead:", error)Look-ahead: up to 2021-12-17: 115 signal days change when later data is addedYes: cierre.mean() is the mean of the five years, so the 2021 signal depends on 2025 prices. It is one of the errors
in the table (whole-sample statistics), and the test detects it without knowing anything about it.
- An assistant fills the gaps in a price series with
cierre.bfill(). Why is that look-ahead? What would you replace it with?
Solution
bfill fills each gap with the next known value: the price on a holiday or a day without data would be the close
of a later day. If the signal uses that value, it knows something about the future. The alternative is ffill (the
last known value, which did exist at that moment) or, better still, not making up prices: leave the gap and do not
trade that day. Gap cleaning is covered in level 1.
- Write, without sending it, the prompt you would give an assistant for a function that computes the annualized
20-session rolling volatility from
cierre. Include context, contract, time rule and at least one test.
Solution
An example. Context: pandas 3.0, cierre is a daily pd.Series with a sorted date index. Task:
volatilidad_movil(cierre, ventana=20) -> pd.Series, sample standard deviation (ddof=1, lesson 0.2.2) of the
simple returns over the last ventana sessions, multiplied by the square root of 252, with the same index. Time rule:
the value for day t only uses closes up to t; the first sessions without a full window, NaN. Tests: the truncation
test; with a constant price series, the volatility is 0; with a series that alternates +1% and −1%, the volatility is
positive and stable. No new dependencies.
Footnotes
-
Claude Code Docs, “Cómo Claude recuerda tu proyecto” (How Claude remembers your project, code.claude.com/docs/es/memory), and the open AGENTS.md format (agents.md), which lists the compatible agents; accessed October 21, 2026. ↩
-
J. Spracklen et al., “We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs” (arXiv:2406.10279), accessed October 21, 2026. ↩
Each answer is folded under its question
Self-assessment
Why does
cierre.rolling(20, center=True).mean()introduce look-ahead bias into a signal?- ABecause the average for day t includes the prices of the following days: today's value depends on closes that do not exist yet
- BBecause
center=Trueis slower - CBecause a 20-session window is too short
- DIt does not, if the position is shifted one day
Show answer
Answer A. Because the average for day t includes the prices of the following days: today's value depends on closes that do not exist yet
With
center=True, the window's label is its center: the average for day t uses ten earlier sessions and nine later ones. Shifting the position one day does not fix it, because the future is already inside the signal.What does the lesson's truncation test check?
- AThat the function is fast
- BThat the signal computed with data up to a date matches the one computed with all the data for those same dates
- CThat the signal has as many ones as zeros
- DThat the backtest makes money
Show answer
Answer B. That the signal computed with data up to a date matches the one computed with all the data for those same dates
If adding future data changes the signal on past days, the signal was using information that did not exist at the time. It is a property every signal must satisfy, whatever the rule.
An assistant suggests
uv add pandas-ta-proto compute an indicator. What do you do before installing it?- AInstall it: if the assistant names it, it exists
- BInstall it with
pipinstead ofuv - CCheck on PyPI and on the project's website that the package exists, who publishes it and whether it is the one you are looking for
- DAsk the assistant to confirm that it exists
Show answer
Answer C. Check on PyPI and on the project's website that the package exists, who publishes it and whether it is the one you are looking for
Models often make up package names, and some attackers publish malicious packages under those names. The assistant cannot check this for you: you check it at the source.
According to the course rules, what is the correct order from level 1 on?
- AAsk for the code, run it, and if the backtest looks good, commit
- BAsk the assistant for the code and the tests at the same time and commit
- CWrite the code by hand and ask the assistant to approve it
- DWrite the test, ask for the code, read all of it, run the tests and only then commit
Show answer
Answer D. Write the test, ask for the code, read all of it, run the tests and only then commit
The test, written by you beforehand, sets what the code has to satisfy. If the same assistant writes both the code and the test, the test can inherit the same error. A backtest that “looks good” is not a test: the one in this lesson looked spectacular.
Key points
- Autocomplete, chat and agent: the more the assistant acts on its own, the more context it needs and the more there is to review.
- Generated code is plausible, not necessarily correct. In trading, the typical error is using future data without it showing.
- A spectacular backtest is a reason for suspicion, not for a commit.
- The truncation test (the signal up to t does not change when later data is added) detects look-ahead in any signal.
- A good prompt gives the data contract, the time rule and the tests the code must pass. At level 0, you write the code yourself.