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.

Fig. — The same 20-session moving average slope strategy on SINT: with the centred average the assistant proposed and with the corrected one, against buy and hold
Level 0 lessons
  1. 0.1 · Welcome and expectations

    1. 0.1.1 Systematic, algorithmic and quantitative trading versus discretionary trading
    2. 0.1.2 Realistic expectations: costs, time and risk of loss
    3. 0.1.3 Course structure, entry tests, personal repository and learning with AI
  2. 0.2 · Modern working environment

    1. 0.2.1 Installing Python with uv: versions and support cycle
    2. 0.2.2 Virtual environments and dependencies: uv init, uv add, pyproject.toml and lockfiles
    3. 0.2.3 Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Basic terminal, paths, environment variables and .env files
    5. 0.2.5 Git and GitHub: commits, branches, pull requests and .gitignore
    6. 0.2.6 Introduction to AI coding assistants: prompts, review and tests
  3. 0.3 · Language fundamentals

    1. 0.3.1 Variables, types and operators; floating point and decimal for prices and ticks
    2. 0.3.2 Strings, f-strings and formatting numbers and dates
    3. 0.3.3 Lists, tuples, dictionaries, sets and comprehensions
    4. 0.3.4 Control flow: if, elif, else, for, while and match-case
    5. 0.3.5 Functions: arguments, *args and **kwargs, lambdas and scope
    6. 0.3.6 Exceptions; modules and packages, with Python 3.15 lazy imports
  4. 0.4 · Intermediate Python

    1. 0.4.1 Object-oriented programming: classes, inheritance, composition and dataclasses
    2. 0.4.2 Typing: type hints, mypy or ty, and Pydantic for configurations
    3. 0.4.3 Iterators, generators, decorators and context managers
    4. 0.4.4 Files (CSV, JSON, YAML, TOML) and dates with time zones
    5. 0.4.5 Professional logging and testing with pytest
    6. 0.4.6 Concurrency: threading, multiprocessing, asyncio and free-threading
  5. 0.5 · Numerical computing and data

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorization, linear algebra and random generators
    2. 0.5.2 pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()
    3. 0.5.3 Selection, filtering, groupby, merge, pivot, resample and moving windows
    4. 0.5.4 Polars: lazy versus eager API, expressions, performance and when to pick it over pandas
    5. 0.5.5 DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet
  6. 0.6 · Visualisation

    1. 0.6.1 Object-oriented Matplotlib, Seaborn and Plotly: price, drawdown and candlestick charts
    2. 0.6.2 Quick dashboards with Streamlit: a monitoring dashboard in a single script
  7. 0.7 · Minimum mathematics and statistics

    1. 0.7.1 Simple and log returns, compound interest and descriptive statistics
    2. 0.7.2 Probability and distributions, correlation versus causation and simple regression
    3. 0.7.3 Intuitive linear algebra: vectors and matrices as groundwork for portfolios

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:

ModeWhat it doesExamplesWhat to review
AutocompleteSuggests the line or function you are writingCopilot in VS Code, CursorEvery suggestion before accepting it
ChatAnswers questions and writes snippets that you copyEditor chat, Claude, ChatGPTThe whole snippet and how it fits into your code
AgentReads the repository, edits files and runs commands and testsClaude Code, Codex, Cursor's agentThe 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:

  1. 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.
  2. After that, the AI proposes and you decide: first the test, then the code, a full read, tests and commit.
  3. 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:

IN [01]python
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())
OUT [01]SINT prices
1303 sessions; 2021-01-04 to 2025-12-31

The answer, with the typical look of an assistant's output (docstring, clear names, a comment that justifies the decision):

IN [02]python
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:

IN [03]python
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))
OUT [03]Backtest of the assistant's signal
Assistant            total +324.1%   Sharpe  2.10
Buy and hold         total  +43.5%   Sharpe  0.48

A 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:

IN [04]python
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}")
OUT [04]Which days the June 3, 2024 average uses
center=False: from 2024-05-07 to 2024-06-03
center=True : from 2024-05-20 to 2024-06-14

With 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:

IN [05]python
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:

IN [06]python
comprobar_sin_anticipacion(senal_pendiente_ia, cierre)
OUT [06]The assistant's signal fails
AssertionError: up to 2021-12-17: 9 signal days change when later data is added

Nine 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:

IN [07]python
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))
OUT [07]senales.py (corrected)
No look-ahead
Corrected            total   -8.9%   Sharpe -0.07

From +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. cierre is a pd.Series of daily closes with a sorted date index and no gaps.

Task: a function senal_pendiente(cierre, ventana=20) -> pd.Series that returns 1 if the simple moving average of ventana sessions rises relative to the previous day and 0 otherwise, with the same index as cierre.

Time rule: the signal for day t may only use closes up to and including t. No center=True, negative shift, bfill or 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:

IN [08]AGENTS.mdmarkdown
# 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:

ReviewRed flags
Look-aheadcenter=True, shift(-n), bfill, means or deviations over the whole sample, models fitted on all the data
LagSignal and return of the same day multiplied without shift(1)
CostsNo commission or slippage; daily trades at no cost
Gapsfillna(0) on prices, a dropna that misaligns series, holidays
DependenciesPackages you do not know: check them on PyPI before uv add
SecretsKeys written in the code or printed in logs (lesson 0.2.4)

Exercises

  1. 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
IN [09]python
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)
OUT [09]Comparing with the mean of the whole sample
Look-ahead: up to 2021-12-17: 115 signal days change when later data is added

Yes: 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.

  1. 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.

  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

  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. ↩

  2. 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

  1. 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=True is 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.

  2. 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.

  3. An assistant suggests uv add pandas-ta-pro to compute an indicator. What do you do before installing it?

    • AInstall it: if the assistant names it, it exists
    • BInstall it with pip instead of uv
    • 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.

  4. 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.

Resources

Documentation