- ago
I have a C# class, ResyncPort, that contains a static string array that is being shared among all strategy threads in a multithreaded optimization. That's no big deal.

But the same class, ResyncPort, also contains an instance of UserStrategyBase. Is it valid to share the same UserStrategyBase instance among all strategy threads, or does each thread require their own private instance of UserStrategyBase?

To put the question another way, can an instance of UserStrategyBase be a static variable in a multithreaded optimization?

I'm "guessing" each strategy thread has its own "unique" UserStrategyBase field variables for the WL simulation and therefore can't be shared. But I'm hoping I'm wrong.
0
332
Solved
8 Replies

Reply

Bookmark

Sort
Glitch8
 ( 10.59% )
- ago
#1
Quoting from the blog article “Anatomy of a WL Strategy”

“Wealth-Lab (WL) uses a different subclass of StrategyBase, named UserStrategyExecutor, to actually run your UserStrategyBase-derived Strategy. UserStrategyExecutor creates an instance of your Strategy for every symbol being backtested. In this way, each symbol of the backtest can maintain its own instance-level variables.”

https://www.wealth-lab.com/blog/anatomy-of-a-wl7-strategy
1
- ago
#2
QUOTE:
"... UserStrategyExecutor creates an instance of your Strategy for every symbol being backtested. In this way, each symbol of the backtest can maintain its own instance-level variables.”

So does that mean the reference (or pointer) to UserStrategyBase cannot be static and shared?

So I have a class, ResyncPort, in the above example that's declared as static. It encapsulates one data structure that I definitely want to share as static because I don't want to load that data from disk for every thread instance. However, it also encapsulates a reference to UserStrategyBase. Is that even allowed in a multithreaded optimization? Do you have a problem with this code example?
0
- ago
#3
@superticker - the basic answer is you shouldn't do that.

Consider:
One symbol being optimized in parallel where each thread uses a different set of parameter values.
Multiple symbols being optimized in parallel where each thread uses a different set of parameter values for each symbol.

In each case, since you're popping some UserStrategyBase instance into a static in the UserStrategyBase's constructor, then you're just going to have random overwrites of the value in your posTrack static variable. And, you're creating a new instance of ResyncPort for each UserStrategyBase and overwriting any existing value that was in the posTrack static variable. It is chaos. Even if WL is using thread local storage, I'm not sure you've got code that would work.

So, you've indicated you want to avoid repeated reads from disk. But, what is not clear is what that data is - is it per symbol, is just some set of data that is the same across all symbols, etc. Are you passing the UserStrategyBase instance into the ResyncPort for purposes of the symbol, the date ranges or whatever?
0
- ago
#4
QUOTE:
... you want to avoid repeated reads from disk. But,... is that data per symbol, is just some set of data that is the same across all symbols,...

That disk data is the same across all symbols; that disk data isn't changing. It's simply what symbols were owned from the previous day to direct real-time trading on the current day. It won't be changing throughout the trading day.

QUOTE:
Are you passing the UserStrategyBase instance into the ResyncPort for purposes of the symbol, the date ranges or whatever?

I think I see your point. In the ResyncPort code line below, usb. is the reference to UserStrategyBase.
CODE:
if (usb.HasOpenPosition(bars,positionType)) //Could this be a Sell?
So you're saying, because usb.HasOpenPosition will be different from one symbol to the next, usb. cannot be shared as a static variable. Is that correct? But the disk-based variable that's constant across symbols can be shared, right?

So how can I share the disk-based variable and not the usb. symbol-based variable and encapsulated both? I thought about ...
CODE:
   static string[] ownedSymbols; //disk-based constant variable    . . .    ResyncPort posTrack = new ResyncPort(this as UserStrategyBase, ref string[] ownedSymbols);
but that technique effectively breaks the encapsulation. The disk-based ownedSymbols variable is only used by the ResyncPort class, and not the strategy proper. ownedSymbols should be part of the ResyncPort encapsulation.

Application note: There's a test against ownedSymbols within ResyncPort to see if it's "null" and needs to read in the disk data; otherwise, it doesn't.
0
- ago
#5
You need separation of concerns. You need to narrow the purpose of the ResyncPort class. That is your cache class, obviously. Don't pollute it with trade logic like where you call usb.HasOpenPosition. Don't make ResyncPort decide if it should do something based on the current state of a particular UserStrategyBase instance. Just make it know what symbols were owned the previous day. That is it.

Also, you can't just keep creating ResyncPort instances in each UserStrategyBase constructor. You only need one.

So, rather that me piecemeal indicating what you should do, I can just write this. If you give me just a snippet of the file that would be a big help.
2
Best Answer
- ago
#6
Here you go. See comments in the code. Below is ResyncPort and TestResyncPort strategy to give it a whirl. Note that this code is writting using C# version 12.

The file contains the symbols. They can be one per line or multiple per line seperated by commas or a mix. Repeated symbols are not a problem

Example file:
CODE:
MARA, CLSK IBM, NFLX F MSFT, AMD,NVDA


ResyncPort...

CODE:
using System; using System.Collections.Generic; using System.IO; using System.Threading; namespace WLUtility; /// <summary> /// Small class to cache a file and check if a symbol is in the file. /// This is a singleton class. So, it essentially handles only one filename /// per WL8 instance. Also, you can modify the file and the cache will /// dynamically update. /// </summary> public class ResyncPort { private static readonly object SyncObject = new(); private ResyncPort() { } /// <summary> /// Contains the symbols in the file. /// We use a HashSet for fast lookups. Hence, /// the code assumes you wouldn't change the file /// in the middle of a backtest or optimization. /// </summary> private HashSet<string> Cache { get; } = []; /// <summary> /// Contains the full path to the file being cached. /// This class only supports one file at a time. /// </summary> private string FileName { get; init; } /// <summary> /// Our singleton instance. /// </summary> private static ResyncPort Current { get; set; } /// <summary> /// Watcher for the file changes. /// </summary> private FileSystemWatcher Watcher { get; set; } /// <summary> /// Build the cache from the file. /// </summary> /// <exception cref="FileLoadException"></exception> private void BuildCache() { // read a file and populate the cache string[] file; // because the file system watcher can be really cranky with // some editors we need to do retries var tries = 0; while (true) { try { file = File.ReadAllLines(FileName); break; } catch (Exception e) { if (++tries == 5) { throw new FileLoadException(e.Message, FileName); } Thread.Sleep(100); } } // we might already be locked when being created so locking again is fine // or, we might not be locked if the file changed. lock (SyncObject) { Cache.Clear(); foreach (var symbol in file) { // accomodate one per line or comma separated or a mix of the two if (string.IsNullOrWhiteSpace(symbol)) { continue; } var sym = symbol.Trim(); if (sym.Contains(',')) { var symSet = sym.Split(','); foreach (var s in symSet) { if (!string.IsNullOrWhiteSpace(s)) { Cache.Add(s.Trim()); } } } else { Cache.Add(symbol.Trim()); } } } } public bool HasSymbol(string symbol) => Cache.Contains(symbol); /// <summary> /// Get the singleton instance of this class. /// If it doesn't exist, create it, and initialize its /// cache from the content of filename. /// </summary> /// <param name="filename">The file to read</param> /// <returns>The singleton instance of ResyncPort</returns> public static ResyncPort Instance(string filename) { if (Current != null && Current.FileName == filename) { return Current; } lock (SyncObject) { // test again in case another thread created it if (Current != null && Current.FileName == filename) { return Current; } var rp = new ResyncPort {FileName = filename}; rp.BuildCache(); // Set event to detect changes in the file so we can rebuild the cache. rp.Watcher = new FileSystemWatcher(Path.GetDirectoryName(filename) ?? string.Empty, Path.GetFileName(filename)); rp.Watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size; rp.Watcher.EnableRaisingEvents = true; rp.Watcher.Changed += (sender, args) => Current.BuildCache(); Current = rp; } return Current; } }


Test class...

CODE:
using WealthLab.Backtest; using WealthLab.Core; using WLUtility; namespace WealthLabStrategies.Test; /// <summary> /// Simple class to test the ResyncPort class. /// </summary> public class TestResyncPort : UserStrategyBase { // change the path to a file on your system private const string PriorDaySymbolsFileName = @"C:\Users\Paul\Documents\WLPriorDaySymbols.txt"; public TestResyncPort() { ParamRSIOverSold = AddParameter("RSI Oversold", ParameterType.Double, 30, 20, 40, 2); ParamRSIOverBought = AddParameter("RSI Overbought", ParameterType.Double, 70, 60, 90, 2); } private ResyncPort PriorDaySymbols { get; set; } private Parameter ParamRSIOverBought { get; } private Parameter ParamRSIOverSold { get; } private double RsiOverSold { get; set; } private double RsiOverBought { get; set; } public override void Initialize(BarHistory bars) { PriorDaySymbols = ResyncPort.Instance(PriorDaySymbolsFileName); RsiOverBought = ParamRSIOverBought.AsDouble; RsiOverSold = ParamRSIOverSold.AsDouble; } public override void Execute(BarHistory bars, int idx) { if (idx == 1) { WriteToDebugLog(PriorDaySymbols.HasSymbol(bars.Symbol) ? $"Cache contains {bars.Symbol}" : $"Cache does not contain {bars.Symbol}"); } if (PriorDaySymbols.HasSymbol(bars.Symbol) == false) { return; } var position = FindOpenPosition(PositionType.Long); if (position == null) { if (bars.Close.CrossesOver(RsiOverSold, idx)) { PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, -1, "Buy"); } } else { if (bars.Close.CrossesUnder(RsiOverBought, idx)) { ClosePosition(position, OrderType.Market, 0, "Sell"); } } } }
0
- ago
#7
QUOTE:
You need separation of concerns. ... to narrow the purpose of the ResyncPort class. That is your cache class, .... Don't pollute it with trade logic like where you call usb.HasOpenPosition. Don't make ResyncPort decide if it should do something based on the current state of a particular UserStrategyBase instance....

Your implementation in Post #6 is more than I need, but I understand your point. There needs to be a separation of powers from what's shared across strategy threads verses what cannot be shared. So I need to take this one ResyncPort class and split it into two classes. Let me think how best to do that.

Thanks Paul for all the help. You are absolutely right. With a two-class implementation, I won't have to "break" encapsulation of any shared data object.

By the way, if you use StreamReader to read the file, you don't have all the retry complexity. The Stream classes take care buffering automatically. I think its uses StringBuilder to allocate a buffer size that's twice the typical disk cluster size.
0
- ago
#8
The retries are there because the read can have an exception on the file changed event callback of the file system watcher because the editor is still in the middle of closing out the file.
1

Reply

Bookmark

Sort