Master

Optimization and its dangers

Optimization finds the settings that maximize past performance, and it is one of the most seductive ways to fool yourself. The best settings you can find in historical data are usually not a real edge, they're the noise wearing a costume. The skill is knowing the difference.

What you will see

Penalized score: 3.0

About 2 min of reading

Try It

Optimization finds the settings that maximize past performance, and that is almost always a lie the data tells you. I search a grid for the best in-sample result, then penalize the number of knobs so a model cannot just memorize noise.

1

I run a grid search over fast and slow settings and print the best in-sample return, acknowledging this winner probably overfit the random patterns in the past.

def backtest(fast, slow, return_on_test): return return_on_test best = None for fast in range(10, 13): for slow in range(12, 15): ret = backtest(fast, slow, return_on_test=5.0) if best is None or ret > best[0]: best = (ret, fast, slow) print("Optimization and its dangers") print("Best in-sample: %.1f%% (fast=%d, slow=%d)" % (best[0], best[1], best[2]))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Optimization and its dangers Best in-sample: 5.0% (fast=10, slow=12)
The best settings in historical data are usually the noise wearing a costume. Search is easy; judging the search is the hard part.
2

I subtract a penalty for every parameter added so the optimizer is discouraged from memorizing the past, printing the penalized net score.

num_parameters = 4 return_on_test = 5.0 penalty = 0.5 score = return_on_test - penalty * num_parameters print("Penalized score: %.1f" % score)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Penalized score: 3.0
Simple models almost always generalize better to an unseen future than anything you can overfit. Penalize complexity and optimize on the net number.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Settings are searched on historical data, judged on fresh data, and scored by net returns after costs so overfitting can't hide.
1 Define the search space
↓
2 Search in-sample
↓
3 Judge out-of-sample
↓
4 Subtract costs
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

def backtest(fast, slow, return_on_test): sets nothing on screen
return return_on_test sets nothing on screen
best = None sets nothing on screen
for fast in range(10, 13): sets nothing on screen
for slow in range(12, 15): sets nothing on screen
ret = backtest(fast, slow, return_on_test=5.0) sets nothing on screen
if best is None or ret > best[0]: sets nothing on screen
best = (ret, fast, slow) sets nothing on screen
print("Optimization and its dangers") Optimization and its dangers
print("Best in-sample: %.1f%% (fast=%d, slow=%d)" % (best[0], best[1], best[2])) Best in-sample: 5.0% (fast=10, slow=12)
Why this works
Costs matter

Trading costs, taxes, and slippage quietly eat your returns. A strategy that looks fantastic before costs will very often lose money after them. Always optimize on the net number, because that's the number that actually lands in your account.

When you have a choice

In-sample vs out-of-sample

Split the data: use in-sample to search and out-of-sample to judge. If the best settings collapse on out-of-sample data, they were worthless, the search only made a prettier story.

Examples

Fitting the curve perfectly

Search 10,000 settings
Keep the one with the best past return

That winning setting probably overfit the noise. When you let the data pick the answer for you, you're almost always memorizing random patterns. The only defense is to check those settings on data they never got to train on, and to pretend nothing will survive.

What grid search really finds

best = None
for fast in range(10, 100):
    for slow in range(100, 500):
        ret = backtest(fast, slow)
        if ret > best:
            best = ret

This finds the historical winner, but it also finds the settings that happened to land on noise and got lucky. The safest winners are the ones that still look good after you throw them at fresh data. Search is easy; judging the search is the hard part.

Penalizing how many knobs you add

score = return_on_test - penalty * num_parameters

Every parameter you add lets the optimizer memorize the past a little more. Penalizing complexity keeps it from doing that, and simpler models almost always generalize better to a future they never saw than anything you can overfit.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Going live with a system
Previous Lesson
Going live with a system