Optimization Tip and Trick
What is optimization?
Basically, our optimization system is all about generating multiple sets of parameter values to run the strategie against. This leads to some choices, which in turn have impacts on how you actually do implement a strategy.
Optimization Well Done: A Checklist
First of, lets have a look at the optimization checklist. These are the mandatory steps not to get tricked by the optimization system. We will have a look at the reasons why later in this article.
- An optimized parameter can be of any type, just remember to match its definition accordingly. For instance, the strategy won't run if you provide a custom boolean generator on a String parameter. (more on how to get it done here)
- Be carefull with numerical conversions: If you put values from 1.0 to 1.9 by steps of 0.1 inside an Integer, you'll get some repetead 1, some repeated 2, and nothing else.
- And most importantly: Never ever use an optimized parameter's value at field initialization. Only use them inside strategy methods. If you do use them at field initialization, you will only get the default value for that parameter, and not the optimized values.
Seems a bit obscure? Lets have a look at how it works, at what is field initialization and how it works, and finally, what one can do to circumvent the problem in his own strategies.
How does it work?
To better understand how one can use optimized parameters, let's have an inside look at how is optimization done on Algodeal grid.
When we receive a strategy, we first have to schedule it on the grid. We thus decode the strategy and decide if it contains any optimized parameter. If it does, we take the values for each parameter, and this gives us a map of parameters to their values. For instance, this map could look like this:
- ("smaLength" => {7,8} | "percentOfChange" => {0.01, 0.02, 0.03})
From this map of parameter values , we can guess two things: First, we compute the size of the optimization set. It is the product of each parameter value set's size. In our example, 2 * 3 = 6. We've got a 6 elements parameter set. We can also compute the whole parameter set, which is, in our example:
- ("smaLength" => 7 | "percentOfChange" => 0.01)
- ("smaLength" => 8 | "percentOfChange" => 0.01)
- ("smaLength" => 7 | "percentOfChange" => 0.02)
- ("smaLength" => 8 | "percentOfChange" => 0.02)
- ("smaLength" => 7 | "percentOfChange" => 0.03)
- ("smaLength" => 8 | "percentOfChange" => 0.03)
There we are: We have six backtests to run, and we know for each on which parameter set to run. We will now instanciate the strategies on the grid. And there lies the most tricky problem: Field initialization.
On Field Initialization
When we want to assign a parameter set on a strategy, we first
have to get an instance of this strategy to work on. We thus have
to instanciate it. While instanciating, the Java runtime is doing
any field initialization it has to do so that we get a proper
instance of strategy.
Thus, if you wrote these three lines:
@Optimized(from=7, to=8, step=1)
int smaLength;
SMA optimizedSma = newIndicator().sma().withLength(smaLength).get();
the runtime would first assign a default value to smaLength. Here, it is not specified, and would be 0. It would then assign an SMA to optimizedSma. To build it, it would have no other choice but to get the value it already knows for smaLength. This value is 0; thus the SMA would work on a 0 length... Not that good, is it?
Finally, we, on the grid, would get this instance of the strategy. On that instance, we would get the fields marked as Optimized, likely smaLenght, and replace their values with the ones we computed at scheduling. Lets say 7, for this smaLength. And now, we are owned: We've got an SMA with a 0 length, whereas you wanted to say, an SMA with optimized length from 7 to 8.
How to fix it?
There is one and only one thing one has to do to avoid this situation: Do not use optimized parameters at field initialization. But how can we use optimized parameters in our fields, then?
Let's see our example. Instead of initializing our SMA at first, we should have better initialized it later. But where's the best place to do it? This is where the onStrategyStart() comes in handy. Instead of building our indicator directly at field initialization, we will build it inside a method of the strategy. To build it once, lets do it in a once called method: onStrategyStart().
Our example simply becomes:
@Optimized(from=7, to=8, step=1)
int smaLength;
SMA optimizedSma;
onStrategyStart() {
optimizedSma = newIndicator().sma().withLength(smaLength).get();
}
And that's it. This method is guaranteed to work. You can use it as a one stop shop for all your optimized parameter problems, most likely array dimensions, collections building, indicators configuration, and so on. Even if you build your very own custom indicator it will work this way.
One remaining thing to keep in mind is to use the right method.
Here we have demonstrated a way using onStrategyStart(), so that
our code is called once. Support you wanted a brand new indicator
on each bar (not something one really wants...), you could have
built the indicator on onBarOpen() for instance. But remember that
this will get called once per bar, which is something we rarely
want.
Last thing to remember is that optimized parameter values are
injected once, and only once, just after field initialization and
just before onStrategyStart(). That means that if you modify an
optimized parameter, for instance if you increment an integer
field, it won't be reset to initial value during execution. This is
your coder responsibility to store it, or to use another variable
to increment for instance.
Want more example?
If things don't look that clear in you mind at that point, don't forget to have a look at our samples. They are all working samples of our code, and most of them demonstrate this kind of technique applied on simple examples.