Broker API HubMenu

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.

Independent and unofficialEducational onlyInvented data onlyCode run

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

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.