Python code: 20 and 50 moving averages and the crossover (free download)

Python code: 20 and 50 moving averages and the crossover (free download)
There is one idea that shows up in every market video: when the average of the last twenty days of a price moves above the average of the last fifty days, you are told a buy signal has appeared, and when it slips below, a sell signal. It is called a moving-average crossover, and however complicated it sounds, the maths is one sum and one division repeated with patience. This article gives you a Python program that runs that sum on your own prices, prints it on screen in two neat tables, and at the end compares the result with the plainest option of all: buy once and never touch it again.
It is written for a business owner, an accountant or a warehouse clerk who wants to see the numbers with their own eyes, without installing new software or paying for a subscription. The package ships with invented sample prices and with a clear way to swap them for your own: a text file with one date column and one price column is all you need. You do not have to write code to use it, and you do not have to believe it either: the program itself shows you where the idea falls short.
⬇ Download the code (ZIP)Notice: all of this is educational material and it is not a recommendation to buy or to sell. The sample prices are invented and the historical data is quoted only for practice. A past result guarantees nothing about the future; before investing, ask a licensed professional.
| What the ZIP holds | What it is for |
|---|---|
| medias_y_cruces.py | The program: it reads the prices, computes both averages, flags the crossings and runs the final comparison. |
| datos/prices.csv | One hundred and eighty days of invented sample prices, so the program has something to work with. |
| salida_ejemplo.txt | The real output of the program, exactly as it comes out, so you can compare it with what you get on screen. |
| README.md | The written instructions, step by step, in case this article leaves something out. |
The price file: two columns and nothing else
The program reads a text file with a .csv extension. Any spreadsheet opens that format, and so does the plain text editor. It only needs two columns, with their names typed in the first row:
| Column | What goes in it |
|---|---|
| date | The day, written year-month-day, for example 2026-01-02. |
| close | The closing price of that day, with a decimal point and two decimals, for example 99.94. |
The first row is the header and you leave it alone. Every row after that is one day: the date first, then a comma, then the price. If your file carries extra columns the program ignores them; if either of these two is missing it stops and says so on screen instead of inventing a number. You can export your prices from Excel with the save-as-CSV option, or type them by hand in the text editor. To use your own data, replace the file inside the datos folder with yours, keeping the same name and the same folder, and run the program again.
Where the real numbers come from: the historical series used for practice across this family of articles come from CoinGecko, the open crypto data service whose address is api.coingecko.com, and from the SP500 index of the Federal Reserve of St. Louis, published at fred.stlouisfed.org/series/SP500. Both were downloaded on 2026-09-25 and are quoted here as historical practice data only. In this piece the file shipped inside the ZIP is invented on purpose: it lets anybody repeat the exercise without depending on a download or an internet connection.
What the program does
- It opens the price file inside the datos folder and reads the one hundred and eighty days it ships with.
- It works out the average of the last twenty days for every date. That is the short average: it moves fast because it only looks at the latest month of trading.
- It works out the average of the last fifty days. That is the long one: it moves slowly and shows the underlying trend.
- It compares the two day by day. On the day the short one moves from below the long one to above it, it writes a buy signal. On the day it moves from above to below, it writes a sell signal.
- It saves two files in the salida folder: medias.csv with both averages for every day, and senales.csv with every crossing it found, with its date and its price.
- At the end it sets aside an imaginary pot of a thousand units, follows the signals, and compares that with buying once and never selling. That is where the program says, with no decoration, which of the two ways finished ahead and by how much.
How to run it
There is nothing to install but Python. Get version 3.11 or newer from the official Python site and keep the default options. Then unzip the package into a comfortable folder, your desktop will do. Open the system terminal, move into that folder and type the name of the program:
python medias_y_cruces.py
On Windows the terminal is called Command Prompt or PowerShell; on Mac and Linux it is Terminal. The program uses the Python standard library only: there is no pandas to install, no numpy, nothing to pay for. It takes less than a second and needs no internet, because it works with the price file that is already inside the ZIP.
The code, explained
The program starts by writing down in one place everything you may want to adjust. The two average windows, how many days are shown on screen and the imaginary money of the example sit at the top with clear names. If you want to try a thirty-five day average, you change one number and that is it:
# The two average windows, the on-screen table and the money of the example
VENTANA_CORTA = 20 # average of the last 20 days: that is the short one, it reacts fast
VENTANA_LARGA = 50 # average of the last 50 days: that is the long one, it shows the background trend
FILAS_TABLA = 10 # how many days are shown on screen
CAPITAL_INICIAL = Decimal("1000.00") # imaginary money for the example, to compare both ways
CENTAVO = Decimal("0.01")
Two details in there save you a lot of grief. First, prices and money are handled with Decimal, the data type banks use: with ordinary binary decimals, adding up hundreds of prices drags cent-sized errors along until the table no longer adds up. Second, every path is built with pathlib from the folder of the file itself, so the program behaves the same on Windows, Linux and Mac without a single change.
A simple moving average is exactly what its name says: the average of the last N closes. The function takes the list of closes, the day being looked at and the size of the window; it slices the stretch of the list that belongs to that window and divides. While there are not enough days to fill the window, it returns an empty value instead of inventing a number:
def media_movil(cierres: list, indice: int, ventana: int):
"""Simple moving average: average of the last N closes up to the given day."""
if indice + 1 < ventana:
return None # there are not enough days yet for this average
return redondear(sum(cierres[indice - ventana + 1: indice + 1], Decimal("0")) / ventana)
That habit of returning an empty value is what avoids the classic mistake in this kind of sum: the first rows of the table come out with no average, because there is nothing to average yet, and that is correct. The fifty-day average only starts to exist on day fifty, never earlier.
This is the heart of the matter. The program walks the file day by day and compares where the two averages stand today against where they stood yesterday. Only two cases interest it:
def detectar_cruces(filas: list) -> list:
"""Finds the day the short average crosses the long one: up is a buy, down is a sell."""
senales = []
for indice in range(1, len(filas)):
anterior, actual = filas[indice - 1], filas[indice]
if None in (anterior["media20"], anterior["media50"],
actual["media20"], actual["media50"]):
continue # no crossing can be read until both averages exist
if actual["media20"] > actual["media50"] and anterior["media20"] <= anterior["media50"]:
tipo = "Buy"
elif actual["media20"] < actual["media50"] and anterior["media20"] >= anterior["media50"]:
tipo = "Sell"
else:
continue
senales.append({"tipo": tipo, "fecha": actual["fecha"], "cierre": actual["cierre"],
"media20": actual["media20"], "media50": actual["media50"]})
return senales
The important part is that a crossing is not spotted by looking at one single day but at the pair of days: today the short one is above and yesterday it was below. That condition is written with four comparisons and a subtraction of positions, nothing more. There is a fine detail too: while both averages do not exist yet, the program skips the day instead of comparing against a gap.
Then comes the uncomfortable part that most videos skip: comparing. The program takes the last price in the file, works out what the capital would be worth if it had followed the signals and what it would be worth if it had bought on the first signal date and never sold. Both returns are rounded to two decimals so they can be compared on equal terms:
ultimo = filas[-1]["cierre"]
final_senal = redondear(unidades * ultimo) if unidades > 0 else capital
# buy and hold: it enters on the first signal date and never sells again
arranque = senales[0] if senales else filas[0]
final_mantener = redondear(CAPITAL_INICIAL * ultimo / arranque["cierre"])
return {
"operaciones": operaciones,
"final_senal": final_senal,
"final_mantener": final_mantener,
"rent_senal": redondear((final_senal / CAPITAL_INICIAL - 1) * 100),
"rent_mantener": redondear((final_mantener / CAPITAL_INICIAL - 1) * 100),
"arranque": arranque,
}
That last block is what turns the exercise into an honest one. The program does not show off: it computes both ways and prints which one won, even when the winner is not the signal strategy.
What you will see on screen
When you run it, the screen first prints the legal notice, then the last ten days with their close and both averages, then the signals it found, and finally the comparison. A real piece of that output looks like this:
LAST DAYS (10) =============================================== Date Close 20-day avg 50-day avg ----------------------------------------------- 2026-06-21 120.85 120.12 115.92 2026-06-22 116.96 119.99 116.22 2026-06-23 118.49 119.76 116.53 2026-06-24 116.43 119.40 116.76 2026-06-25 117.39 119.13 117.00 2026-06-26 115.28 118.76 117.19 2026-06-27 115.53 118.42 117.37 2026-06-28 117.18 118.17 117.56 2026-06-29 116.97 118.11 117.75 2026-06-30 117.24 118.09 117.93
SIGNALS FOUND: 2 =========================================================== Signal Date Close 20-day avg 50-day avg ----------------------------------------------------------- Sell 2026-03-16 97.97 107.93 107.95 Buy 2026-04-29 100.66 97.91 97.76 ----------------------------------------------------------- Crossing up: the short one moves above the long one (buy signal). Crossing down: the short one moves below it (sell signal).
And the part that matters most, the end of the comparison:
SIGNALS VERSUS BUY AND HOLD
================================================================================
Starting capital: 1,000.00
Buy and hold enters on: 2026-03-16 at a price of 97.97
Trades of the signal strategy: 1
2026-04-29 Buy 100.66
Final capital following the signals: 1,164.71 (16.47 %)
Final capital buying and holding: 1,196.69 (19.67 %)
Buy and hold WON, by: 31.98
Honest note: the winner depends on the stretch you measure and on the first
signal; with other prices the other way may win. A past result guarantees
nothing.
The honest comparison: buy and hold won here
Let us have the truth of the exercise, because a program that only shows the pretty side is worth nothing. With these one hundred and eighty invented days, the signal strategy finished with 1,164.71 and buy and hold finished with 1,196.69. Buy and hold won, by 31.98. Put in return terms, nineteen point six seven per cent against sixteen point four seven per cent: the plainest way took almost two and a half points off the way that looks smarter.
Why does the strategy lose if it caught both crossings? Two easy reasons. The first: the strategy is out of the market part of the time. Between the sell signal in the middle of March and the buy signal at the end of April, the capital sat still while the price made its move, and that gap is never recovered. The second: the stretch being measured goes up. When a price climbs with hardly a pause, any rule that pulls you out of the market for a few days loses against sitting still. With another stretch, other prices and another first signal, the other way may win; the program says so itself in its honest note.
So it is worth saying plainly: this program is there to understand how a signal is computed, how it is checked and how a strategy is measured against the simplest alternative. It is not there to make money, and it cannot guess the future. A reader who walks away knowing that walks away far richer than with any promise.
Common mistakes and tips
- The file with the wrong name: the program looks for datos/prices.csv exactly. Call it prices2.csv and it stops and tells you. Rename your file, or edit that line.
- Dots against commas: the sample files use a decimal point, as billing systems do, while the on-screen tables print a comma, because the program formats them the local way. Both are the same number.
- Fewer rows than the long window: with fewer than fifty days the program still runs, but it finds no crossing and the table comes out half empty. Load a few months.
- Dates in the wrong order: the file must run from the oldest day to the newest. Reversed, the averages come out meaningless.
- Blank lines at the end: an empty row cuts the reading short. Open the file and delete the extra lines.
- Do not move the output folder: the program creates salida/medias.csv and salida/senales.csv by itself. If you open them in Excel and leave them open, the next run may fail when it tries to write.
When this is not enough
This program is a pair of training wheels. It reads one file, computes two averages and prints a table; it does not keep a stock ledger, it does not know what you paid for each item, it does not handle several warehouses, and it will not close the month for you. A stock ledger, in plain words, is the running record of everything that comes in and goes out, with its cost.
For a small shop still living in spreadsheets, Kardex Tauro is a free program that keeps inventory, purchases, sales and the stock ledger in one place, with reports you can hand to your accountant. The software is what you reach for when Excel and a script are no longer enough; this Python piece is what you use to understand the maths behind the signals before trusting anybody's chart.
Notice: remember that this is educational material and it is not a recommendation to buy or to sell. The sample prices are invented, the historical data is quoted only for practice, and a past result guarantees nothing about the future; ask a licensed professional before investing. The source of the real data is credited above, with its address, and the ZIP carries this same disclaimer in writing inside its instructions file.
⬇ Download the code (ZIP)Download the package, open the price file, replace it with your own numbers and run it again. Watching your own data go through the two averages teaches more than any video, and the honest comparison at the end is the part most people never get to see.