- ago
I challenged my coders to develop a C# parallel optimizer-safe rotation framework. This is what they developed:
CODE:
using System; using System.Collections.Generic; using WealthLab.Backtest; using WealthLab.Core; namespace WealthScript1 {    /* Generic Parallel-Safe Rotation Framework A minimal, parallel-optimizer-safe stock rotation framework The only optimizable Strategy parameters are: 1. Minimum Hold Bars range 1..10 2. Positions To Hold range 5..20 Default ranking model: ---------------------- Symbols are ranked by a simple fixed-period price ROC: ROC % = 100 * (Close[idx] / Close[idx - RocPeriod] - 1) To build a custom rotation system, replace ComputeRankScore() and, if desired, IsEligibleForRotation(). The rotation, minimum-hold, deterministic ranking, position-count, and Backtester.Cache state-management framework can remain unchanged. */    public class GenericParallelSafeRotationFramework : UserStrategyBase    {       // Edit this constant directly if you want a different demonstration ROC period.       private readonly int _rocPeriod = 10;       // Instance-level immutable cache key. The cached object itself is created per backtest.       private readonly string _rotationStateCacheKey = "GenericParallelSafeRotationFramework.RotationState";       private class RotationState       {          public readonly HashSet<string> SelectedSymbols = new HashSet<string>(StringComparer.Ordinal);          public readonly Dictionary<string, double> RankBySymbol = new Dictionary<string, double>(StringComparer.Ordinal);          public readonly Dictionary<string, int> EntryBarBySymbol = new Dictionary<string, int>(StringComparer.Ordinal);       }       private class RotationCandidate       {          public BarHistory Bars;          public string Symbol;          public double Rank;          public RotationCandidate(BarHistory bars, double rank)          {             Bars = bars;             Symbol = bars.Symbol;             Rank = rank;          }       }       public GenericParallelSafeRotationFramework()       {          AddParameter("Minimum Hold Bars", ParameterType.Int32, 3, 1, 10, 1);          AddParameter("Positions To Hold", ParameterType.Int32, 10, 5, 20, 1);       }       public override void BacktestBegin()       {          Backtester.Cache[_rotationStateCacheKey] = new RotationState();       }       public override void Initialize(BarHistory bars)       {          StartIndex = _rocPeriod + 1;          TimeSeries rocSeries = BuildRocSeries(bars, _rocPeriod);          PlotTimeSeries(rocSeries, "ROC Rank %", "Rotation Rank", WLColor.Blue, PlotStyle.Line);          PlotTimeSeries(ConstantSeries(bars, 0.0), "Zero", "Rotation Rank", WLColor.Gray, PlotStyle.Line);       }       public override void PreExecute(DateTime dt, List<BarHistory> participants)       {          RotationState state = GetRotationState();          int minimumHoldBars = Parameters[0].AsInt;          int positionsToHold = Parameters[1].AsInt;          minimumHoldBars = ClampInt(minimumHoldBars, 1, 10);          positionsToHold = ClampInt(positionsToHold, 5, 20);          state.SelectedSymbols.Clear();          state.RankBySymbol.Clear();          List<RotationCandidate> candidates = new List<RotationCandidate>();          // Work from participants without sorting or mutating the participants list itself.          foreach (BarHistory bh in participants)          {             int idx = GetCurrentIndex(bh);             string symbol = bh.Symbol;             double rank = double.NegativeInfinity;             if (idx >= StartIndex)                rank = ComputeRankScore(bh, idx);             if (!IsValidNumber(rank))                rank = double.NegativeInfinity;             state.RankBySymbol[symbol] = rank;             bool hasOpenLong = HasOpenPosition(bh, PositionType.Long);             if (hasOpenLong && IsMinimumHoldProtected(state, symbol, idx, minimumHoldBars))             {                // Minimum-hold protection takes precedence over the target position count.                // This prevents optimizer-dependent churn and premature exits.                state.SelectedSymbols.Add(symbol);                continue;             }             if (IsEligibleForRotation(bh, idx, rank))                candidates.Add(new RotationCandidate(bh, rank));          }          // Deterministic ordering: best rank first; symbol ascending breaks ties.          candidates.Sort(CompareCandidates);          foreach (RotationCandidate candidate in candidates)          {             if (state.SelectedSymbols.Count >= positionsToHold)                break;             state.SelectedSymbols.Add(candidate.Symbol);          }       }       public override void Execute(BarHistory bars, int idx)       {          if (idx < StartIndex)             return;          RotationState state = GetRotationState();          int minimumHoldBars = Parameters[0].AsInt;          minimumHoldBars = ClampInt(minimumHoldBars, 1, 5);          string symbol = bars.Symbol;          bool selected = state.SelectedSymbols.Contains(symbol);          bool hasOpenLong = HasOpenPosition(bars, PositionType.Long);          if (!hasOpenLong)          {             if (selected)             {                Transaction t = PlaceTrade(bars, TransactionType.Buy, OrderType.Market);                if (t != null)                {                   double rank = 0.0;                   state.RankBySymbol.TryGetValue(symbol, out rank);                   t.Weight = ComputeTransactionWeight(rank);                   // This records the signal bar. For market orders this is typically                   // one bar before the actual fill bar, but it is deterministic and                   // sufficient for a simple minimum-hold rotation framework.                   state.EntryBarBySymbol[symbol] = idx;                }             }             return;          }          if (!selected)          {             if (!IsMinimumHoldProtected(state, symbol, idx, minimumHoldBars))             {                PlaceTrade(bars, TransactionType.Sell, OrderType.Market);                state.EntryBarBySymbol.Remove(symbol);             }          }       }       // --------------------------------------------------------------------       // Replace this method to create a different rotation system.       // The framework expects larger values to be better.       // --------------------------------------------------------------------       private double ComputeRankScore(BarHistory bars, int idx)       {          if (idx < _rocPeriod)             return double.NegativeInfinity;          double oldPrice = SafePrice(bars.Close[idx - _rocPeriod]);          double currentPrice = SafePrice(bars.Close[idx]);          return 100.0 * (currentPrice / oldPrice - 1.0);       }       // --------------------------------------------------------------------       // Optional extension point.       // Default behavior: every symbol with a valid ROC rank is eligible.       // For example, a long-only momentum system might require rank > 0.0.       // --------------------------------------------------------------------       private bool IsEligibleForRotation(BarHistory bars, int idx, double rank)       {          return IsValidNumber(rank) && rank > double.NegativeInfinity;       }       // --------------------------------------------------------------------       // Weight used by WealthLab when more trade candidates exist than capital.       // Since ROC can be negative, the transaction weight is shifted into a       // positive range while preserving rank order for ordinary ROC values.       // --------------------------------------------------------------------       private double ComputeTransactionWeight(double rank)       {          if (!IsValidNumber(rank))             return 0.0001;          return Math.Max(0.0001, 100.0 + rank);       }       private bool IsMinimumHoldProtected(RotationState state, string symbol, int idx, int minimumHoldBars)       {          int entryBar;          if (!state.EntryBarBySymbol.TryGetValue(symbol, out entryBar))             return false;          return idx - entryBar < minimumHoldBars;       }       private RotationState GetRotationState()       {          object obj;          if (!Backtester.Cache.TryGetValue(_rotationStateCacheKey, out obj) || obj == null)          {             obj = new RotationState();             Backtester.Cache[_rotationStateCacheKey] = obj;          }          return (RotationState)obj;       }       private int CompareCandidates(RotationCandidate a, RotationCandidate b)       {          int rankCompare = b.Rank.CompareTo(a.Rank);          if (rankCompare != 0)             return rankCompare;          return string.Compare(a.Symbol, b.Symbol, StringComparison.Ordinal);       }       private TimeSeries BuildRocSeries(BarHistory bars, int period)       {          TimeSeries ts = new TimeSeries(bars.DateTimes, 0.0);          for (int i = 0; i < bars.Count; i++)          {             if (i < period)             {                ts[i] = 0.0;                continue;             }             double oldPrice = SafePrice(bars.Close[i - period]);             double currentPrice = SafePrice(bars.Close[i]);             ts[i] = 100.0 * (currentPrice / oldPrice - 1.0);          }          return ts;       }       private TimeSeries ConstantSeries(BarHistory bars, double value)       {          TimeSeries ts = new TimeSeries(bars.DateTimes, value);          for (int i = 0; i < bars.Count; i++)             ts[i] = value;          return ts;       }       private double SafePrice(double price)       {          if (double.IsNaN(price) || double.IsInfinity(price) || price <= 0.0)             return 0.0000001;          return price;       }       private bool IsValidNumber(double x)       {          return !double.IsNaN(x) && !double.IsInfinity(x);       }       private int ClampInt(int x, int lo, int hi)       {          if (x < lo)             return lo;          if (x > hi)             return hi;          return x;       }    } }

I have tested it only minimally and I think it works properly. I am opening it up to the community to assess and, if found correct, use.

Vince
0
5
0 Replies

Reply

Bookmark

Sort
Currently there are no replies yet. Please check back later.