Lesson 0.1.2 · 35 min
Realistic expectations: costs, time and risk of loss
Before looking for an edge, you need to know how much it costs to have one. Four simple calculations (costs, losses, bad years and years of data) that explain why most strategies never work live.
- Module
- 0.1 · Welcome and expectations
- Duration
- 35 min
- 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 it costs to trade
A strategy does not earn what its backtest says, but what is left after paying to trade it. Costs have five sources, and only the first one appears on the broker's fee schedule:
- Commissions: what the broker charges for each order. It is the most visible cost and, in liquid stocks, the smallest.
- Bid-ask spread: the difference between the price at which you can buy and the price at which you can sell. Each round-trip trade pays it once.
- Slippage: the difference between the signal price and the price actually executed. It grows with the size of the order and with the volatility of the moment.
- Financing: interest on leveraged positions, securities borrowing on short positions, rollover in forex and funding on crypto perpetuals.
- Taxes: in Spain, gains are taxed in the savings base of personal income tax (IRPF; level 7). They do not change the strategy, but they do change what is left at the end.
Mermaid source
flowchart TB B[Gross return] --> C1[− commissions] --> C2[− spread and slippage] --> C3[− financing] --> C4[− taxes] --> N[Net return]
If the first four are grouped into a cost per round-trip trade (buy and sell) , and the strategy makes round trips a year, the net return is approximately:
The formula is simple and its consequence harsh: the cost of each trade is multiplied by the turnover. In Python:
import pandas as pd
def rentabilidad_neta(bruta: float, operaciones: int,
coste: float) -> float:
"""Net annual return.
`coste`: cost of one round-trip trade, as a fraction.
"""
return bruta - operaciones * coste
operaciones = [1, 5, 10, 25, 50, 100]
costes = [0.001, 0.002, 0.005]
tabla = pd.DataFrame(
{f"cost {c:.1%}": [rentabilidad_neta(0.08, n, c) for n in operaciones]
for c in costes},
index=pd.Index(operaciones, name="trades/year"),
)
tabla.round(3) cost 0.1% cost 0.2% cost 0.5%
trades/year
1 0.079 0.078 0.075
5 0.075 0.070 0.055
10 0.070 0.060 0.030
25 0.055 0.030 -0.045
50 0.030 -0.020 -0.170
100 -0.020 -0.120 -0.420With 8% gross and 0.2% per trade, the strategy is still good at 10 trades a year and loses money from 40 on:
That is why high-turnover strategies need a much larger gross edge, and why a backtest without costs says nothing. From level 2 on, every backtest in the course includes costs.
Losses are not symmetric
If a €10,000 account loses 50%, €5,000 is left. Getting back to €10,000 does not take a 50% gain: you have to double what is left, a 100% gain. In general, after a loss the required gain is:
for perdida in (0.10, 0.20, 0.50, 0.75):
subida = 1 / (1 - perdida) - 1
print(f"After losing {perdida:.0%}, you need a gain of {subida:.1%}")After losing 10%, you need a gain of 11.1%
After losing 20%, you need a gain of 25.0%
After losing 50%, you need a gain of 100.0%
After losing 75%, you need a gain of 300.0%The figure at the top shows the full curve. Up to 20% the asymmetry is small; from 40% on, recovery requires returns that few strategies deliver in years. This is why risk management (level 4) matters more than looking for the rule that earns the most: a 60% drawdown is not fixed by a good streak.
Bad years with a good strategy
The Sharpe ratio measures the average return per unit of volatility: an annual Sharpe of 1 means that the average return of a year equals its standard deviation. If annual returns were normal, the probability of closing a year at a loss would be , where is the cumulative distribution function of the standard normal:
from statistics import NormalDist
for sharpe in (0.5, 1.0, 1.5):
p = NormalDist().cdf(-sharpe)
print(f"Sharpe {sharpe}: {p:.1%} of years at a loss")Sharpe 0.5: 30.9% of years at a loss
Sharpe 1.0: 15.9% of years at a loss
Sharpe 1.5: 6.7% of years at a lossA Sharpe of 1 sustained for years is an excellent result for a systematic strategy, and even so one year in six closes negative. Real returns have fatter tails than the normal distribution (level 1), so the real figure is usually somewhat worse.
A simulation makes it more concrete. Ten thousand ten-year paths of a strategy with a Sharpe of 0.5 (a 5% average return and 10% volatility), with independent, normal years:
import numpy as np
rng = np.random.default_rng(42)
anos = rng.normal(loc=0.05, scale=0.10, size=(10_000, 10))
en_perdidas = anos < 0
tres_seguidos = (
en_perdidas[:, :-2] & en_perdidas[:, 1:-1] & en_perdidas[:, 2:]
).any(axis=1)
capital_final = np.prod(1 + anos, axis=1)
print(f"At least one losing year: {en_perdidas.any(axis=1).mean():.1%}")
print(f"Three losing years in a row: {tres_seguidos.mean():.1%}")
print(f"Lose money over ten years: {(capital_final < 1).mean():.1%}")At least one losing year: 97.5%
Three losing years in a row: 16.8%
Lose money over ten years: 7.8%The strategy has a real edge by construction, and even so almost every path has losing years, and a non-negligible share has streaks of three losing years in a row. Whoever abandons a strategy after a bad year is, most of the time, abandoning a strategy that works.
Time: how much data you need
The other side of the problem: if bad years are normal, how many good years does it take to believe a backtest? A useful approximation relates the annual Sharpe to the t-statistic of the average return after years of data:
A t-statistic of 2 is the usual threshold for saying that a mean differs from zero with 95% confidence. Solving for :
for sharpe in (0.5, 0.75, 1.0):
anos_necesarios = (2 / sharpe) ** 2
print(f"Sharpe {sharpe}: {anos_necesarios:.0f} years of data")Sharpe 0.5: 16 years of data
Sharpe 0.75: 7 years of data
Sharpe 1.0: 4 years of dataWith a Sharpe of 0.5 (a good strategy at low frequency), you need about 16 years of data for the result not to be compatible with chance. And that is for one strategy: if you test a hundred variants and pick the best, the threshold rises (level 2, multiple testing). This calculation explains two ideas of the course: backtests of two or three years prove nothing, and parameters are chosen from hypotheses, not by searching for the maximum.
Learning time counts too. The full syllabus is about 750–850 hours: at 10–15 hours a week, between 12 and 18 months. Skipping levels 0 to 2 does not shorten that path; it only makes it more expensive.
Risk of loss and leverage
Leverage multiplies exposure and, with it, losses. With 10-to-1 leverage, a 10% fall in the asset wipes out 100% of the capital. Leveraged retail products (CFDs, options, futures, crypto perpetuals) make it possible to lose the entire deposit in a very short time.
The course does not recommend trading with real money until level 4, and always after weeks of paper trading (simulated trading with real prices) and with automatic risk limits.
Exercises
- A strategy makes 12% gross per year, makes 40 round-trip trades and each one costs 0.3%. What is its net return? At most, how many trades a year could it make without losing money?
Solution
Net: 12% − 40 × 0.3% = 12% − 12% = 0%. The break-even point is exactly 40 trades: 0.12 / 0.003 = 40. With any higher turnover, the strategy loses money even if its signal is good.
- A fund falls 35% in a crisis. What gain does it need to recover? If it then makes 8% a year, how many years does it take?
Solution
import math
subida = 1 / (1 - 0.35) - 1
anos_recuperacion = math.log(1 / (1 - 0.35)) / math.log(1.08)
print(f"Required gain: {subida:.1%}")
print(f"Years at 8% a year: {anos_recuperacion:.1f}")Required gain: 53.8%
Years at 8% a year: 5.6It needs somewhat more than 50% and, at 8% a year compounded, more than five and a half years just to get back to where it started.
- A strategy has an annual Sharpe of 0.8. What fraction of years closes at a loss, if annual returns are normal? How many years of data are needed for a t-statistic of 2?
Solution
from statistics import NormalDist
sharpe = 0.8
print(f"Losing years: {NormalDist().cdf(-sharpe):.1%}")
print(f"Years of data: {(2 / sharpe) ** 2:.2f}")Losing years: 21.2%
Years of data: 6.25Roughly one year in five closes at a loss, and you need a little over six years of data for a t-statistic of 2: more than most published backtests.
Each answer is folded under its question
Self-assessment
A strategy makes 10% gross per year with 30 round-trip trades and a cost of 0.25% per trade. What is its approximate net return?
- A9.75%
- B7.5%
- C2.5%
- D0%
Show answer
Answer C. 2.5%
Costs subtract 30 × 0.25% = 7.5 points: 10% − 7.5% = 2.5%. Turnover multiplies the cost of each trade.
An account falls 40%. What gain does it need to get back to where it started?
- A40%
- B50%
- C66.7%
- D80%
Show answer
Answer C. 66.7%
1 / (1 − 0.40) − 1 = 0.667. After the fall, 60% of the capital is left, and that 60% has to grow by two thirds to get back to 100%.
With normally distributed annual returns, what fraction of years does a strategy with a Sharpe of 1 close at a loss?
- AAlmost none
- BRoughly one in six
- CHalf
- DIt depends only on volatility
Show answer
Answer B. Roughly one in six
P(R < 0) = Φ(−1) ≈ 15.9%: one year in six. A good strategy has bad years; abandoning it because of one of them is a frequent mistake.
Roughly how many years of data are needed for a strategy with a Sharpe of 0.5 to have a t-statistic of 2?
- A2
- B4
- C8
- D16
Show answer
Answer D. 16
t ≈ SR · √T, so T = (2 / 0.5)² = 16 years. With modest Sharpe ratios, a few years of backtest cannot tell an edge apart from chance.
Key points
- Net return is gross return minus turnover times the cost of each trade: the more you trade, the more edge you need.
- Losses are not symmetric: after losing 50% you need 100% to recover. Limiting drawdown matters more than maximizing gains.
- With a Sharpe of 1, one year in six closes at a loss. Bad years do not prove that a strategy has stopped working.
- Telling a Sharpe of 0.5 apart from chance takes about 16 years of data: most short backtests prove nothing.
- Leverage multiplies losses just as it multiplies gains. In the EU, most retail CFD accounts lose money.