Technique explained as code
Day of the week rule
Sort daily price changes by weekday and one day will always come out on top. This page finds such a pattern in prices that were built to have none, then watches it vanish.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
Teaching material only. This page explains how a technique works as code and how to test an idea honestly. It is not investment advice, not a recommendation and not an invitation to trade. Every price on it is invented, so no result here says anything about any real market, and nothing here suggests that a technique makes money.
What it is
A calendar rule acts on the date and nothing else: hold over Thursdays, stay out on Mondays, and so on. This one sorts every daily change by its weekday, takes the average for each, and picks the highest.
It is on this site as a demonstration, not as a technique to study for its own sake. It is the simplest way to see a pattern appear from nothing.
The idea behind it
Calendar effects have been written about for decades, and some had plausible stories attached. The trouble is that the method for finding them guarantees a find. Five weekdays cannot all have the same average. One must be highest, and with a few hundred days the gap between highest and lowest will look meaningful.
The invented price file was generated without any reference to the weekday. Every day is treated the same. So whatever this script finds, we know in advance it is luck. That is the advantage of testing on data whose rules you wrote yourself.
The code
The script does what an honest test should: it finds the pattern in the first half of the prices, then tests it on the second half, which played no part in the choice.
Data the script reads: invented-prices.csv. Invented values, never a real market. Save it in one folder and run the script from that folder.
The file that was run: day_of_week.py
# A day of the week rule, used to show how easily a pattern is found by chance.
# Prices come from invented-prices.csv: a random walk made by a script, never a real market.
# The script that made them treats every weekday the same. Any weekday pattern below is luck.
import csv
from datetime import date
NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
weekday, closes = [], []
with open("invented-prices.csv", newline="") as f:
for row in csv.DictReader(f):
weekday.append(date.fromisoformat(row["date"]).weekday()) # 0 is Monday
closes.append(float(row["close"]))
def average(values):
return sum(values) / len(values)
def changes_by_weekday(first, last):
"""Group each day's change from the previous close under its weekday."""
groups = {d: [] for d in range(5)}
for day in range(max(first, 1), last):
groups[weekday[day]].append(closes[day] - closes[day - 1])
return groups
COST = 0.10 # points paid on every completed trade, a stand in for the spread
HALF = len(closes) // 2
# Step 1. Look at the first half of the prices and find the weekday with the highest average change.
first_half = changes_by_weekday(0, HALF)
print("First half, average change by weekday:")
for d in range(5):
print(f" {NAMES[d]:10} {average(first_half[d]):+.3f} over {len(first_half[d])} days")
pick = max(range(5), key=lambda d: average(first_half[d]))
print("Highest in the first half:", NAMES[pick])
# Step 2. Test that weekday on the second half, which played no part in choosing it.
# The rule: buy at the close before that weekday, sell at its close, pay the cost each time.
second_half = changes_by_weekday(HALF, len(closes))
trades = second_half[pick]
print("Second half, the same weekday:")
print(" Average change:", f"{average(trades):+.3f}", "over", len(trades), "days")
print(" Trades:", len(trades))
print(" Total points before costs:", round(sum(trades), 2))
print(" Total points after costs:", round(sum(trades) - COST * len(trades), 2))
print(" Simply holding over the second half:", round(closes[-1] - closes[HALF] - COST, 2))
# Step 3. Where did that weekday come among the five in the second half?
order = sorted(range(5), key=lambda d: average(second_half[d]), reverse=True)
print(" Its place among the five weekdays in the second half:", order.index(pick) + 1)What the run printed
First half, average change by weekday: Monday -0.169 over 74 days Tuesday -0.014 over 75 days Wednesday +0.022 over 75 days Thursday +0.096 over 75 days Friday +0.077 over 75 days Highest in the first half: Thursday Second half, the same weekday: Average change: -0.111 over 75 days Trades: 75 Total points before costs: -8.33 Total points after costs: -15.83 Simply holding over the second half: -8.55 Its place among the five weekdays in the second half: 4
Output of the run on , copied as printed. Every input to this run was invented, so the result says nothing about any real market.
How to read it
In the first half the weekday averages are spread out, with Thursday highest and Monday lowest. Shown only this table, most people would start to think of reasons why Mondays are weak. There are none. The file has no weekday effect in it.
In the second half the same weekday had an average change below zero and came fourth of the five. The pattern did not weaken. It was never there.
A rule that acted on it paid a cost on every one of its trades and ended well below zero, and below simply holding. Had the test stopped at the first half, none of that would have been seen. It says nothing about any real market.
How it fails or misleads
- The search guarantees a result. Whenever you sort outcomes into groups, one group comes first. Being first is not evidence.
- There are many calendars to try: weekday, day of the month, month of the year, the days around a holiday. Each one tried is another chance for luck to look like a pattern.
- Any effect would have to be larger than the cost. A rule that trades every week pays the cost every week, and in the output above the costs come to nearly half of the loss.
- A story is easy to invent afterwards. A plausible reason for a pattern is not a test of it.
- Testing on fresh data is the cure, and it only works once. If you test five ideas on the second half and keep the one that survives, you have searched the second half too.
The page on how a backtest lies shows the mistakes that make any rule look better in a test than it is.
What to try changing
These are programming exercises. Each one changes the code and asks you to explain what you see. None of them is a search for settings to use.
- Swap the halves: find the highest weekday in the second half and test it on the first.
- Shuffle the list of weekdays with random.shuffle and a fixed seed, so each change is paired with a random weekday, and run it again. Is the spread between highest and lowest any smaller?
- Do the same for the day of the month, 1 to 31. With more groups and fewer days in each, how large does the gap between highest and lowest become?
- Split the prices into three parts instead of two and see how often the same weekday is highest in more than one part.
- Print the number of days behind each average and work out what a difference of a few days would do to it.
How this page was made
Each script on this page was run with Python 3.12 on Windows, standard library only, from a folder holding the data files listed. No network and no account. Each exited with code 0, and what is shown is everything it printed.
The data files whose names begin with the word invented are written by make_invented_data.py from fixed seeds, so anyone can make the same files again and get the same output. This site is not published if the code or a data file has changed since the run.