---
title: "0.2.6 AI coding assistants: prompts, review and tests"
description: "What AI coding assistants do, a generated signal with look-ahead bias, the test that catches it and the prompt that would have prevented it."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/ai-coding-assistants/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/ai-coding-assistants.png"
---

# 0.2.6 · 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.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.2 · Modern working environment
- Duration: 40 min
- Prerequisites: [0.1.3 · Course structure, entry tests, personal repository and learning with AI](https://pythonparatrading.com/en/python-trading-course/level-0/course-structure-and-learning-with-ai/), [0.2.3 · Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)](https://pythonparatrading.com/en/python-trading-course/level-0/code-editors-jupyter-marimo-notebooks/)

## By the end of this lesson

- Tell autocomplete, chat and agent apart, and know what context each one needs to propose useful code.
- Review an AI-generated function looking for the typical trading errors: look-ahead, costs, gaps and made-up dependencies.
- Write a truncation test that detects whether a signal uses data from after the date it is computed for.
- Write a prompt with context, data contract, time rule and tests, consistent with the course rules for using AI.

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

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:

```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())
```
```output
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):

```python {6}
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:

```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))
```
```output
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:

```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}")
```
```output
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:

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

```python raises
comprobar_sin_anticipacion(senal_pendiente_ia, cierre)
```
```output
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:

```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))
```
```output
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
others[^2]. For the course project, a few lines are enough:

```markdown title="AGENTS.md"
# 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) |

<aside class="callout" data-kind="riesgo">

Models make up package names. A study with 16 models and 576,000 code samples found nonexistent packages in at least
5.2% of the recommendations of commercial models and 21.7% of those of open models[^1]. Some attackers publish
malicious packages under those names. Before you `uv add` something you do not know, look it up on pypi.org and on the
project's website.

</aside>

## 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.**

```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)
```
```output
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.

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

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

[^1]: 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.

[^2]: 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.

## Self-assessment

1. Why does `cierre.rolling(20, center=True).mean()` introduce look-ahead bias into a signal?

   - 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
   - B. Because `center=True` is slower
   - C. Because a 20-session window is too short
   - D. It does not, if the position is shifted one day

   *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?

   - A. That the function is fast
   - B. That the signal computed with data up to a date matches the one computed with all the data for those same dates
   - C. That the signal has as many ones as zeros
   - D. That the backtest makes money

   *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?

   - A. Install it: if the assistant names it, it exists
   - B. Install it with `pip` instead of `uv`
   - 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
   - D. Ask the assistant to confirm that it exists

   *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?

   - A. Ask for the code, run it, and if the backtest looks good, commit
   - B. Ask the assistant for the code and the tests at the same time and commit
   - C. Write the code by hand and ask the assistant to approve it
   - D. Write the test, ask for the code, read all of it, run the tests and only then commit

   *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

### Research

- [Your first system in Python: moving-average crossover on SPY](https://pythonparatrading.com/en/first-system-moving-average-crossover/) · Research-0018 · 7 min

### Documentation

- [pandas: Series.rolling (center parameter)](https://pandas.pydata.org/docs/reference/api/pandas.Series.rolling.html) · pandas.pydata.org
- [GitHub Copilot documentation](https://docs.github.com/en/copilot) · docs.github.com
- [Claude Code: overview](https://code.claude.com/docs/en/overview) · code.claude.com

### Data

- [Level 0 data: daily prices of the fictional ETF SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Download · sint_diario.csv

Previous lesson: [0.2.5 · Git and GitHub: commits, branches, pull requests and .gitignore](https://pythonparatrading.com/en/python-trading-course/level-0/git-and-github-for-trading/)

Next lesson: [0.3.1 · Variables, types and operators; floating point and decimal for prices and ticks](https://pythonparatrading.com/en/python-trading-course/level-0/python-variables-types-and-decimal/)
