- ago
Added a flex condition (respect for order) like "after" so as to be able to create strategies in building block format like "Shadow Trader" or Pullbacks.

exemple :



The alternative I see is to add a condition "N bars ago until N bars". But "after" would be much more flexible.
2
320
12 Replies

Reply

Bookmark

Sort
- ago
#1
After what? Can you formulate it more general and precisely.
0
- ago
#2
Here is the example of my pullback (we can simplify), which must be done in 50 bars.

0
- ago
#3
Have you tried the "Within the past N bars" qualifier?
0
Cone8
 ( 25.44% )
- ago
#4
If I understand correctly, Ramasa976 is asking for the conditions to follow a specific sequence. "1" has to happen 1st, then "2", then "3"... the number of bars ago is irrelevant - except that it all has to happen within the "50 bars" indicated.

It's easy to see where "3" would be violated - if RSI(20) didn't remain above 40, so that would "reset" the sequence to wait for "1" again. It's not clear what you should do if you went above 70 ("2") below and then above it again. Is that a reset? Probably not.

Thinking out loud... maybe this could be done with something called "Sequence Number" Qualifier. You assign the number to "SNQ". (Easy to say, much harder to implement.)

Another idea that might be more framework-friendly:
A "Sequencer" block (similar to "OR Divider") could be dropped between sequential conditions, top to bottom.
1
- ago
#5
Not only it's harder to implement, it would not be fairly intuitive to use (though a "Sequencer" block is a good idea). Such requests don't fit the simplicity of Blocks very well but may introduce ambiguity. And when it happens, the natural choice is to learn C# or use concierge service. Expressing such pattern with the PeakTroughCalculator (for example) might take a couple dozen lines of code only. Just my two cents.
0
Cone8
 ( 25.44% )
- ago
#6
After trying to scope it out with an example, I agree. I'm not saying it can't be done, but this is probably one of those things that you need to work out in custom code.

For sake of discussion, here's my example using most of Ramsa's block entry rules -

CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using System.Collections.Generic; using Steema.TeeChart.Styles; namespace WealthScript6 { public class MyStrategy : UserStrategyBase {     public MyStrategy() : base() { StartIndex = 10; } public override void Initialize(BarHistory bars) {          indicator = new RSI(bars.Close, 8);          PlotIndicator(indicator,new WLColor(0,0,0));          _startIndexList.Add(indicator); foreach(IndicatorBase ib in _startIndexList) if (ib.FirstValidIndex > StartIndex) StartIndex = ib.FirstValidIndex;          SetPaneMinMax(indicator.PaneTag, 20, 80);          DrawHorzLine(20, WLColor.Red, paneTag: indicator.PaneTag);          DrawHorzLine(70, WLColor.Blue, paneTag: indicator.PaneTag);          DrawHorzLine(50, WLColor.Red, paneTag: indicator.PaneTag);          DrawHorzLine(40, WLColor.Blue, 1, LineStyle.Dashed, indicator.PaneTag);          DrawHorzLine(55, WLColor.Green, paneTag: indicator.PaneTag);           } public override void Execute(BarHistory bars, int idx) {          int index = idx;          Position foundPosition0 = FindOpenPosition(0);          bool condition0;          if (foundPosition0 == null)          {                         condition0 = false;                          //start a sequence group             bool seqGroup0_reset = false;             {                if (indicator.CrossesOver(20.00, index))                {                   seqGroup0_0 = index;               // save the index of the event                }                          if (indicator.CrossesOver(70.00, index))                {                   seqGroup0_1 = index;                }                                if (indicator.CrossesUnder(50.00, index))                {                   seqGroup0_2 = index;                }                // this triggers on the way to 70, but the bar of the event will be out of sequence, so the condition will fail                if (indicator.CrossesOver(55.00, index))                {                   seqGroup0_3 = index;                }             }             WriteToDebugLog($"{index}: \t{seqGroup0_0}\t{seqGroup0_1}\t{seqGroup0_2}\t{seqGroup0_3}");                          // check the sequence             if (index - seqGroup0_0 <= seqGroup0_bars) //the pattern start is in range             {                if (seqGroup0_1 >= seqGroup0_0)                   if (seqGroup0_2 >= seqGroup0_1)                      if (seqGroup0_3 >= seqGroup0_2)                         condition0 = true;             }             else                seqGroup0_reset = true;                          if (seqGroup0_reset || condition0)            // reset if we trigger this bar             {                seqGroup0_0 = 0;                seqGroup0_1 = 0;                seqGroup0_2 = 0;             }                          if (condition0)             {                WriteToDebugLog("Triggered");                Backtester.CancelationCode = 1;                _transaction = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 0, "Buy At Market (1)");             }          }          else          {             {                Backtester.CancelationCode = 1;                ClosePosition(foundPosition0, OrderType.Market, 0, "Sell At Market (1)");             }          } }       int seqGroup0_bars = 50;       int seqGroup0_0 = 0;       int seqGroup0_1 = 0;       int seqGroup0_2 = 0;       int seqGroup0_3 = 0;        public override void NewWFOInterval(BarHistory bars) {          indicator = new RSI(bars.Close,20); }       private IndicatorBase indicator;       private Transaction _transaction; private List<IndicatorBase> _startIndexList = new List<IndicatorBase>(); } }


The idea is that you save the bar numbers where the event trigger. "Crosses" and "Turn Up/Down" events work fine with this methodology, but it falls apart when you add that "stays above" condition. It's not easy to work that into this hypothetical framework. It can be done, but it's hard to generalize.

Here you'll see a problem even that you wouldn't expect. Everything was going according to plan, but then Condition "2" AND "4" triggered on the same bar - so now 2 didn't come before 4 and we had to wait for the setup again, which finally triggered the trade.

(Again, the "remains > 40 condition" after "3" wasn't implemented here.)


1
- ago
#7
Post #4 yes condition 2 is not obligatory, but it would give something like this:



This corresponds exactly to the strategy of "Shadow Trader" with a less strong affirmation of the correction.

Post #5 For my case I called on the concierge and cone made me very sophisticated building blocks that I have difficulty injecting to simple codes. I am independent of the blocks for the moment.

Now to see if this interests WL community!
0
- ago
#8
Post #6
If I understand, even with C# there is no line of code to resolve this because the closing of the candle validates the two conditions, and there is no way for it to cancel a validated condition if the "previous" condition has not yet been validated.
0
Cone8
 ( 25.44% )
- ago
#9
Anything can be done if the time and effort is put to it.
With the example above, I was just trying to make an example of how it might work if output from block code. It's not a trivial task to generalize it for blocks!

QUOTE:
cone made me very sophisticated building blocks
Only because your requirements were very sophisticated!
0
- ago
#10
Thank you Ramsa976 for presenting the question, and Cone for demonstrating a way that it could be solved. This got me thinking about creating a state machine that could be used for identifying patterns in stock data and acting on those patterns.

For you C# users (perhaps advanced), here are two utility classes that represent a state machine and rules for each state (see the code). Along with this is a demo strategy. A rule has one or more named states to which it applies, an expression that is evaluated, and if true, a named new state for machine.

See the comments throughout for additional details.

State classes follow. Put these in your library of helper classes...
CODE:
using System; using System.Collections.Generic; using WealthLab.Backtest; using WealthLab.Core; namespace WLUtility.StateMachine { /// <summary> /// User strategy base state machine rule /// </summary> /// <typeparam name="TSource"></typeparam> public class UserStrategyBaseStateRule<TSource> where TSource : UserStrategyBase { public UserStrategyBaseStateRule(List<string> qualifyingStates, string transitionToState, Func<TSource, BarHistory, int, bool> expr) { QualifyingStates = qualifyingStates; TransitionToState = transitionToState; Expr = expr; } public UserStrategyBaseStateRule(string qualifyingState, string transitionToState, Func<TSource, BarHistory, int, bool> expr) { QualifyingStates = [qualifyingState]; TransitionToState = transitionToState; Expr = expr; } /// <summary> /// List of state names that this rule applies to. Use "*" to apply to all states. /// </summary> public List<string> QualifyingStates { get; } /// <summary> /// If the expression is true, the state machine will transition to this state. /// </summary> public string TransitionToState { get; } /// <summary> /// A function that takes a strategy as the source, a bar history, and a bar index and returns a boolean. /// </summary> public Func<TSource, BarHistory, int, bool> Expr { get; } } /// <summary> /// User strategy base state machine that uses rules to transition between states /// to attempt to find a final state or fail state. /// A final state indicates that the set of rules was satisfied and a fail state indicates that the set of rules was /// not satisfied. /// Typically, a final state would indicate a buy or sell signal and a fail state would indicate no action. /// </summary> /// <typeparam name="TSource"></typeparam> public class UserStrategyBaseStateMachine<TSource> where TSource : UserStrategyBase { private readonly string _failState; private readonly string _finalState; private readonly string _initialState; private readonly List<UserStrategyBaseStateRule<TSource>> _rules; private readonly TSource _source; public UserStrategyBaseStateMachine(TSource source, string initialState, string finalState, string failState, List<UserStrategyBaseStateRule<TSource>> rules) { _source = source; CurrentState = initialState; _initialState = initialState; _finalState = finalState; _failState = failState; _rules = rules; } private BarHistory Bars => _source.CurrentBars; public string CurrentState { get; private set; } private string GetNextState(int idx) { // check each rule to see if it applies foreach (var rule in _rules) { if ((rule.QualifyingStates.Contains(CurrentState) || rule.QualifyingStates.Contains("*")) && rule.Expr(_source, Bars, idx)) { // if the rule applies, transition to the next state return rule.TransitionToState; } } // no rule applied, stay in the current state return CurrentState; } /// <summary> /// Returns true if the current state is the final state. /// </summary> public bool IsFinalState() => CurrentState == _finalState; /// <summary> /// Returns true if the current state is the fail state. /// </summary> public bool IsFailState() => CurrentState == _failState; /// <summary> /// Execute the state machine for the given bar history index. /// A single execution may result in multiple state transitions. /// </summary> /// <param name="idx">The bar history index</param> /// <returns>True if a state transition occurred</returns> /// <exception cref="Exception"> /// An exception is thrown if for the given index /// you end up back at the start state. /// </exception> private bool Execute(int idx) { var startState = CurrentState; var priorState = CurrentState; // loop until no more state transitions occur for the given idx while (true) { CurrentState = GetNextState(idx); if (priorState == CurrentState) { // no transition occurred break; } // check for infinite loop if (CurrentState == startState) { throw new Exception("State machine is stuck in a loop"); } priorState = CurrentState; } return startState != CurrentState; } /// <summary> /// Execute the state machine for the given bar history index. /// </summary> /// <param name="startIndex">The starting bar history index</param> /// <param name="length"> /// A maximum of length bars will be evaluated starting at startIndex (inclusive) and ending at /// startIndex - length (exclusive) /// </param> /// <returns>Returns true if the final state, as denoted by the state machine's rules was achieved, false otherwise.</returns> public bool ObtainedFinalState(int startIndex, int length) { CurrentState = _initialState; // loop through the bars and execute the state machine starting at the given index // and working backwards for the given length. // if the final state is achieved, return true. // if the final state is not achieved or the fail state is not achieved, return false. // if the fail state is achieved at the start index, return false, otherwise continue to the next bar. // Hence, the rules work backwards from idx to idx - length for efficiency. // The alternative is start at the beginning (idx - length) and work forward, but that would be less efficient // because once it achieved a fail state it would have to go back and try again at idx + 1 and so on. for (var idx = startIndex; idx > startIndex - length && idx >= 0 && idx > _source.StartIndex; idx--) { var newState = Execute(idx); if (!newState && startIndex == idx) { return false; } if (CurrentState == _finalState) { return true; } if (CurrentState == _failState) { return false; } } return false; } } }


Test strategy...
CODE:
using System.Collections.Generic; using WealthLab.Backtest; using WealthLab.Core; using WealthLab.Indicators; using WLUtility.StateMachine; namespace WealthLabStrategies.MovingAverage { /// <summary> /// Wealth-Lab Strategy to test the user strategy base state machine. /// </summary> public class TestStateMachine : UserStrategyBase { private const int RsiBuyPatternSearchLength = 100; public TestStateMachine() { ParamRsiBuyLevel = AddParameter("RSI Buy Level", ParameterType.Double, 55, 45, 80); ParamRsiLowerCrossUpLevel = AddParameter("RSI Lower Cross Up Level", ParameterType.Double, 20, 10, 35); } private RSI Rsi { get; set; } private UserStrategyBaseStateMachine<TestStateMachine> RsiBuyPattern { get; set; } private Parameter ParamRsiLowerCrossUpLevel { get; } private double RsiLowerCrossUpLevel { get; set; } private Parameter ParamRsiBuyLevel { get; } private double RsiBuyLevel { get; set; } public override void Initialize(BarHistory bars) { const string initialState = "Cross Over For Buy"; const string finalState = "Final"; const string failState = "Fail"; RsiBuyLevel = ParamRsiBuyLevel.AsDouble; RsiLowerCrossUpLevel = ParamRsiLowerCrossUpLevel.AsDouble; var rsiBuyPatternRules = new List<UserStrategyBaseStateRule<TestStateMachine>> { // The rules work backwards from idx to idx - n for efficiency, so the rules are in reverse order. new(initialState, "Cross Under #1", (strategy, _, i) => strategy.Rsi.CrossesOver(RsiBuyLevel, i)), new("Cross Under #1", failState, (strategy, _, i) => strategy.Rsi[i] <= 40), new("Cross Under #1", "Cross Over #2", (strategy, _, i) => strategy.Rsi.CrossesUnder(50, i)), new("Cross Over #2", "Cross Over #1", (strategy, _, i) => strategy.Rsi.CrossesOver(70, i)), new("Cross Over #1", finalState, (strategy, _, i) => strategy.Rsi.CrossesOver(RsiLowerCrossUpLevel, i)) }; Rsi = RSI.Series(bars.Close, 14); RsiBuyPattern = new UserStrategyBaseStateMachine<TestStateMachine>(this, initialState, finalState, failState, rsiBuyPatternRules); var indicator = Rsi; SetPaneMinMax(indicator.PaneTag, 20, 80); DrawHorzLine(RsiLowerCrossUpLevel, WLColor.Red, paneTag: indicator.PaneTag); DrawHorzLine(70, WLColor.Blue, paneTag: indicator.PaneTag); DrawHorzLine(50, WLColor.Red, paneTag: indicator.PaneTag); DrawHorzLine(40, WLColor.Blue, 1, LineStyle.Dashed, indicator.PaneTag); DrawHorzLine(RsiBuyLevel, WLColor.Green, paneTag: indicator.PaneTag); PlotIndicator(Rsi); StartIndex = RsiBuyPatternSearchLength + 1; } public override void Execute(BarHistory bars, int idx) { var position = FindOpenPosition(0); if (position == null) { if (RsiBuyPattern.ObtainedFinalState(idx, RsiBuyPatternSearchLength)) { Backtester.CancelationCode = 1; PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 0, "Buy RSI Pattern"); } // other rules... } else { if (idx >= position.EntryBar + 4) { ClosePosition(position, OrderType.Market, 0, "Exit after 5 bars after signal"); } } } } }
1
Glitch8
 ( 11.81% )
- ago
#11
I’m envisioning a new kind of Block for this, let’s call it a Sequencer Block. You’d drop 2 or more Conditions inside and it would resolve to true if the Conditions occur, one after the other, in that order.

The challenge will be how to generate the code for this. I think it will need to create some backing TimeSeries for each Condition which will track at which bar each one resolves to true. Then, on any evaluation bar, it can look at those and see if the last Condition resolved to true at the current bar. If so, it can check the previous Conditions to make sure they resolve in the proper order with no resets in between.
3
Glitch8
 ( 11.81% )
- ago
#12
We could also add an optional parameter to the Sequencer Block that dictates that all the Conditions have to have occurred in N number of bars.
3

Reply

Bookmark

Sort