Is there a way to make strategy optimization multi-threaded?
Author: leszek
Creation Date: 5/8/2014 5:44 PM
profile picture

leszek

#1
I would love to be able to execute each strategy run on a separate thread during optimization. I tried for example to have additional manual strategy runs in my optimizer's NextRun() call and add additional OptmizationResult objects to the OptimizationResultList in my optimizer's RunCompleted call but that doesn't seem to work - the added results don't show up in the UI.
profile picture

Eugene

#2
You probably can't parallelize optimizations with the current API: NextRun() enforces a "serial" model of execution. There's a registered feature request on exposing a better API, though:

Open Issues > "(98348) Optimizer API not sufficient to create parallelized, multi-threaded optimizers. Leverage benefits of multi-core CPUs in Optimizer."
profile picture

leszek

#3
I'm getting close to accomplishing a workaround. Basically In my optimizer's Initialize() I create an array of WealthScript instances of my strategy and in NextRun() I run them on separate threads with different stategy parameter values. I then select the best one based on results and pump those parameter values into the main WealthScript. But the issue I'm running into is the fact that I use multiple symbols in my script and GetExternalSymbol() fails with an null reference exception. The exception is very easy to repro in a standard script as well:
CODE:
Please log in to see this code.

profile picture

Eugene

#4
I like the idea. If it works out, please share some sample code with the community.

1a. Try the overloaded call of GetExternalSymbol that accepts a DataSet name. If this doesn't work, too...

1b. ...Try GetAllDataForSymbol instead of GetExternalSymbol. I'm not sure if this Reflection-driven method is going to help in your context, but nonetheless give it a whack:

GetAllDataForSymbol

2. runDonor/RunDonorNew are obsolete; they've become extension methods:

CODE:
Please log in to see this code.


Should've updated the documentation to reflect the switchover to extension methods in v2013.05 but never came to it.
profile picture

leszek

#5
I will definitively share my code with the community if I get it to work. GetAlldataForSymbol worked. However if I try to either synchronize the new bars or set context to the new symbol I still get a null reference exception. Uncommenting any one of the two commented lines below repros the issue in Wealth-Lab:

CODE:
Please log in to see this code.
profile picture

Eugene

#6
QUOTE:
set context to the new symbol

If SetContext fails in your model, then it would do so w/o GetAllDataForSymbol which does not have anything to do with SetContext's failure.
profile picture

leszek

#7
So just to be clear. A strategy code like this:
CODE:
Please log in to see this code.

Is not supported. Correct?

Thanks!
Leszek
profile picture

Eugene

#8
To be precise:
CODE:
Please log in to see this code.


QUOTE:
Is not supported. Correct?

Yes, that's right. Frankly, I can't tell you why it throws this exception on SetContext.
profile picture

leszek

#9
Ok, here's the multi-threaded optimizer. It's an exhaustive optimizer. On the first run it will check if a strategy can be run on multiple threads and if it cannot, it will show a dialog box to such effect and stop the optimization.

CODE:
Please log in to see this code.
profile picture

Eugene

#10
Impressive job! Thank you for sharing. This is certainly going to be useful to the community.
profile picture

LenMoz

#11
I tried the multi-threaded optimizer. Thank you for your efforts.

Unfortunately, it hasn't worked for me. I get the "This strategy cannot be run in multi-threaded mode..." message for the three of my strategies that I've tried. The simplest uses only "FundamentalDataSeries", "BuyAtMarket", and "SellAtMarket".

I compiled the optimizer with SharpDevelop and copied the DLL to the "Wealth-Lab Pro 6" folder. Do I need to do something else?



profile picture

leszek

#12
It will not work for anything that requires loading of external symbols, whether stocks, indexes, or anything else (this including implicit loads in various extensions in your case the FundamentalDataSeries).

Eugene, here's the call stack from the error if it helps any:

CODE:
Please log in to see this code.
profile picture

LenMoz

#13
Thanks for the speedy response. That restriction limits its usefulness for my strategies, however.
profile picture

leszek

#14
Ok, figured it out how to deal with external symbols. I trap the loading event from the trading executor (ExternalSymbolRequested) and pipe the loading through the main script. See the "OnLoadSymbol" call in the updated script above.

LenMoz, if you post an example of your script that uses the fundamental data series I could try to fix that as well.
Leszek
profile picture

LenMoz

#15
Here it is, Leszek...
CODE:
Please log in to see this code.
profile picture

leszek

#16
I don't see a corresponding event for loading fundamentals in the executor. I tried to create a new FundamentalsLoader from scratch but it requires an IDataHost for its source and I don't know how to create it. Maybe Eugene will have some ideas but for now it looks like loading fundamental data is not supported.

Leszek
profile picture

leszek

#17
Here's the source code with 2 bug fixes - fixed run count and handling of optimizations with parameters removed in the optimization window:
CODE:
Please log in to see this code.
profile picture

Eugene

#18
Leszek,

I figured out how to create a FundamentalsLoader through Reflection. The complete code is below.

Gave the optimizer a try and didn't understand the output. Compared to the Exhaustive optimizer, the output results in much less runs on the Results tab. On the screenshot below, you can see the first results of an optimization of the built-in "Moving Average Crossover" Strategy where each parameter has a step of 1. However, the output shows gaps. Do you have an idea why this happens? Thanks.



CODE:
Please log in to see this code.
profile picture

leszek

#19
QUOTE:
I figured out how to create a FundamentalsLoader through Reflection. The complete code is below.

Great stuff! Thank you!
QUOTE:
Gave the optimizer a try and didn't understand the output. Compared to the Exhaustive optimizer, the output results in much less runs on the Results tab. On the screenshot below, you can see the first results of an optimization of the built-in "Moving Average Crossover" Strategy where each parameter has a step of 1. However, the output shows gaps. Do you have an idea why this happens? Thanks.

Yes, this is an issue - If you look at the overloaded RunCompleted(OptimizationResultList results) function the OptimizationResultList does have an Add(). However when I tried to use it all the results that were added were ignored and did not show up in the output. The only way I found to add to the results list was to actually run a set of parameters through the main thread. So what the code does is that for every set of 64 runs it selects the best set of parameters based on net profit and re-runs that set on the main thread so that this particular set shows up in the UI:
CODE:
Please log in to see this code.

As a result the UI will show 1/64 results. If you have any additional Reflection magic to allow captured results be inserted into the UI bypassing the main thread then I could show all results instead of just 1/64 of them.
profile picture

leszek

#20
Ok, I figured out a way to inject results into the UI. It is a hack as it assumes a fixed UI layout but for now it works. I also don't populate all the columns. See AddResultsToUI() function below:
CODE:
Please log in to see this code.
profile picture

Eugene

#21
Do you see any performance improvement over the built-in Exhaustive optimizer? I didn't notice because my 6-core CPU is loaded up to 20% during a (multi-threaded) optimization - just like before. When optimization starts, there's a brief spike in CPU load (100% for say 10 seconds) but this is where it ends.
profile picture

leszek

#22
I see a 2x to 3x improvement on my system - also a 6 core. I noticed that the longer the optimization (more runs) the higher is the improvement over the standard exhaustive one, so for cases with up to 1000 runs or so it probably doesn't make a difference. I'll see if I can profile it.
profile picture

leszek

#23
I fixed some low hanging fruits for optimizing perf. Try the current code below. However the biggest reason why you're not saturating the CPU is memory management when running the scripts. Any script that will cache objects in member variables instead of re-creating them each time in the Execute() call will have a higher benefit in running multi-threaded.

CODE:
Please log in to see this code.
profile picture

leszek

#24
Fixed populating all scorecard columns in the results UI. I now use the configured scorecard to generate the output from results. Note this only supports the two build-in scorecards - basic and extended. If you want to support any external ones from various extensions you will have to add them manually to the GetSelectedScorecard() function. The reason is that the selected scorecard is a private (inaccessible) variable on the optimization host so I can't get access to it directly and I have to create my own instances.:
CODE:
Please log in to see this code.
profile picture

leszek

#25
To follow up from the last post. Here's an example on how to add the MS123 Scorecards (http://www2.wealth-lab.com/WL5Wiki/CommunityScorecard.ashx) to the optimizer:
CODE:
Please log in to see this code.
profile picture

Panache

#26
I know I'm late finding this. First, thank you very much for all the work you obviously put into this. In a quick test, the optimization time was 1/4 of what it took for an Exhaustive optimization.

However, I'm still seeing the same thing Eugene did in post #18 (1/4 the number of runs required, no increased CPU usage and Results showing 3/4 of the combinations of Strategy Parameters with 0's). Most troubling is that in my test, the Strategy Parameters showing the highest Net Profit using the Parallel Optimizer only result in the 20th best Net Profit of an Exhaustive optimization.

I compiled your code in post #24 with Visual Studio 2013 Express, added the necessary references, built it as Multi-Core Optimizer.dll and saved the .dll to Program Files>Fidelity Investments>Wealth-Lab Pro 6. The optimizer shows up in Wealth-Lab Pro as Parallel Optimizer (Exhaustive). Is there something I need to do differently?

Assuming others are getting the correct results, perhaps someone could post the .dll as a .zip file, so I can eliminate the possibility of compilation issues.


profile picture

electricessence

#27
Has anyone figured out how to make this work right? I've tried this code. I understand it well enough. But the fact that WL has a single instance of a WealthScript per optimizer seems to doom all optimizers to single threaded performance?

And just a note, this code needs to check .IsEnabled for the parameters.
profile picture

Eugene

#28
There may be an easier way. After confirmation of your WLP entitlement, install alternative optimizers created by community members LenMoz and Panache: Particle Swarm Optimizer and Exhaustive to Local Maximum Optimizer. They're very fast (the 2nd one can reduce optimization time by factor of ten and even more compared to Exhaustive).
profile picture

electricessence

#29
Ok, the swarm optimizer is definitely faster, but clearly not optimized for parallelization. Again, my question is: is there any chance in the near future that the Optimizer class could facilitate using the TPL? I was hoping for this when I started tweaking the above optimizer and because 'this.WealthScript' is singular for each optimizer, there's just no way.

I would highly recommend looking at https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library and finding a way to implement.
profile picture

Eugene

#30
QUOTE:
Again, my question is: is there any chance in the near future that the Optimizer class could facilitate using the TPL?

There's a snowball's chance in hell, neither in the near nor distant future. WealthScript processing is not optimized for parallel processing. Due to incorrect results when parallelized, our company even had to withdraw the build of Monte Carlo Lab which used TPL. That's why I purposefully recommended you the fastest single-thread optimizers out there.