Ta4j Wiki

Documentation, examples and further information of the ta4j project

View the Wiki On GitHub

This project is maintained by ta4j Organization

Fixed Window Rule Design - Product Requirements Document

Status Snapshot (2026-03-06)

Status: partially superseded by shipped functionality.

What landed:

What remains open from this PRD:

Overview

This document consolidates the design proposals for implementing fixed-window rule functionality in ta4j. The goal is to provide a rule that checks if multiple rules were ALL satisfied within a fixed time window from when an initial trigger rule was satisfied, as opposed to the existing ChainRule which uses resetting thresholds.

Problem Statement

The existing ChainRule implements a sequential confirmation pattern where each chain rule gets a new window starting from when the previous rule was satisfied. However, there are use cases where we need a simultaneous confirmation pattern where all rules must be satisfied within a single fixed window measured from the initial trigger point.

Use Case Example

A trading strategy might require:

All three confirmation rules must have been satisfied at some point within the 5-bar window starting from when the price crossed the moving average.

Design Comparison

Two design approaches were considered:

Key Characteristics:

Option 2: FixedWindowChainRule

Key Characteristics:

Recommendation: AndWithThresholdRule is recommended due to its simpler API, clearer semantics, and explicit differentiation from ChainRule.

Core Behavior

Helper Class: RuleWithThreshold

/**
 * Pairs a Rule with its threshold (number of bars to look back).
 * This allows per-rule threshold specification.
 */
public record RuleWithThreshold(Rule rule, int threshold) {
    public RuleWithThreshold {
        Objects.requireNonNull(rule, "rule cannot be null");
        if (threshold < 0) {
            throw new IllegalArgumentException("threshold must be >= 0");
        }
    }
    
    /**
     * Convenience factory for common case where threshold equals default
     */
    public static RuleWithThreshold of(Rule rule, int threshold) {
        return new RuleWithThreshold(rule, threshold);
    }
}

API Design Options

Option A: Varargs with RuleWithThreshold

public class AndWithThresholdRule extends AbstractRule {
    
    private final Rule initialRule;
    private final List<RuleWithThreshold> chainRules;
    private final int defaultThreshold;  // Used when threshold not specified
    
    /**
     * Constructor with default threshold for all rules
     * @param initialRule The rule that must trigger first
     * @param defaultThreshold Default number of bars to look back
     * @param chainRules The rules that must ALL be satisfied within their windows
     */
    public AndWithThresholdRule(Rule initialRule, int defaultThreshold, Rule... chainRules) {
        this(initialRule, defaultThreshold, 
             Arrays.stream(chainRules)
                   .map(rule -> new RuleWithThreshold(rule, defaultThreshold))
                   .collect(Collectors.toList()));
    }
    
    /**
     * Constructor with per-rule thresholds
     * @param initialRule The rule that must trigger first
     * @param defaultThreshold Default threshold (used if not specified in RuleWithThreshold)
     * @param chainRules Rules with their individual thresholds
     */
    public AndWithThresholdRule(Rule initialRule, int defaultThreshold, RuleWithThreshold... chainRules) {
        this(initialRule, defaultThreshold, Arrays.asList(chainRules));
    }
    
    public AndWithThresholdRule(Rule initialRule, int defaultThreshold, List<RuleWithThreshold> chainRules) {
        // Validation: defaultThreshold >= 0, chainRules not empty, etc.
        this.initialRule = Objects.requireNonNull(initialRule);
        this.defaultThreshold = defaultThreshold;
        this.chainRules = List.copyOf(chainRules);
    }
}
public class AndWithThresholdRule extends AbstractRule {
    
    private final Rule initialRule;
    private final List<RuleWithThreshold> chainRules;
    private final int defaultThreshold;
    
    /**
     * Fluent builder for constructing AndWithThresholdRule
     */
    public static class Builder {
        private Rule initialRule;
        private int defaultThreshold = -1;  // -1 means not set
        private final List<RuleWithThreshold> chainRules = new ArrayList<>();
        
        /**
         * Set the initial trigger rule
         */
        public Builder withInitialRule(Rule rule) {
            this.initialRule = Objects.requireNonNull(rule);
            return this;
        }
        
        /**
         * Set default threshold for all chain rules
         */
        public Builder withDefaultThreshold(int threshold) {
            if (threshold < 0) {
                throw new IllegalArgumentException("threshold must be >= 0");
            }
            this.defaultThreshold = threshold;
            return this;
        }
        
        /**
         * Add a chain rule with default threshold
         */
        public Builder addRule(Rule rule) {
            if (defaultThreshold < 0) {
                throw new IllegalStateException("defaultThreshold must be set before adding rules");
            }
            this.chainRules.add(new RuleWithThreshold(rule, defaultThreshold));
            return this;
        }
        
        /**
         * Add a chain rule with specific threshold
         */
        public Builder addRule(Rule rule, int threshold) {
            this.chainRules.add(new RuleWithThreshold(rule, threshold));
            return this;
        }
        
        /**
         * Add a chain rule using RuleWithThreshold
         */
        public Builder addRule(RuleWithThreshold ruleWithThreshold) {
            this.chainRules.add(ruleWithThreshold);
            return this;
        }
        
        /**
         * Build the AndWithThresholdRule
         */
        public AndWithThresholdRule build() {
            if (initialRule == null) {
                throw new IllegalStateException("initialRule must be set");
            }
            if (chainRules.isEmpty()) {
                throw new IllegalStateException("at least one chain rule must be added");
            }
            // If defaultThreshold not set, use max of all thresholds
            int effectiveDefault = defaultThreshold >= 0 
                ? defaultThreshold 
                : chainRules.stream().mapToInt(RuleWithThreshold::threshold).max().orElse(0);
            
            return new AndWithThresholdRule(initialRule, effectiveDefault, chainRules);
        }
    }
    
    public static Builder builder() {
        return new Builder();
    }
}

Core Algorithm

isSatisfied(index, tradingRecord):
    // Step 1: Check if initial rule is satisfied at current index
    if NOT initialRule.isSatisfied(index, tradingRecord):
        return false
    
    // Step 2: For each chain rule, check if it was satisfied within its threshold window
    for each ruleWithThreshold in chainRules:
        threshold = ruleWithThreshold.threshold()
        rule = ruleWithThreshold.rule()
        
        // Calculate this rule's window [startIndex, index]
        startIndex = max(0, index - threshold)
        
        ruleSatisfiedInWindow = false
        
        // Look backwards from current index to startIndex
        for i = index down to startIndex:
            if rule.isSatisfied(i, tradingRecord):
                ruleSatisfiedInWindow = true
                break  // Found it, no need to check further
        
        // If any chain rule was NOT satisfied in its window, fail
        if NOT ruleSatisfiedInWindow:
            return false
    
    // Step 3: All chain rules were satisfied within their respective windows
    return true

Usage Examples

Example 1: Varargs with Default Threshold

Rule rule = new AndWithThresholdRule(
    initialRule,
    5,  // default threshold for all
    ruleA, ruleB, ruleC  // all use threshold=5
);

Example 2: Varargs with Per-Rule Thresholds

Rule rule = new AndWithThresholdRule(
    initialRule,
    5,  // default threshold (used as fallback)
    RuleWithThreshold.of(ruleA, 5),  // uses threshold=5
    RuleWithThreshold.of(ruleB, 3),  // uses threshold=3
    RuleWithThreshold.of(ruleC, 2)   // uses threshold=2
);

Example 3: Fluent Builder - Same Threshold

Rule rule = AndWithThresholdRule.builder()
    .withInitialRule(initialRule)
    .withDefaultThreshold(5)
    .addRule(ruleA)
    .addRule(ruleB)
    .addRule(ruleC)
    .build();

Example 4: Fluent Builder - Mixed Thresholds

Rule rule = AndWithThresholdRule.builder()
    .withInitialRule(initialRule)
    .withDefaultThreshold(5)  // default for rules without explicit threshold
    .addRule(ruleA)           // uses default threshold=5
    .addRule(ruleB, 3)        // uses explicit threshold=3
    .addRule(ruleC, 2)        // uses explicit threshold=2
    .build();

Example 5: Fluent Builder - All Explicit Thresholds

Rule rule = AndWithThresholdRule.builder()
    .withInitialRule(initialRule)
    .addRule(ruleA, 5)
    .addRule(ruleB, 3)
    .addRule(ruleC, 2)
    .addRule(ruleD, 7)
    .build();
    // No default threshold needed when all are explicit

Example Scenarios

Scenario 1: Same Threshold for All Rules

Initial rule triggers at index 20, default threshold = 5
All rules use threshold = 5

Chain rules:
- Rule A must be satisfied somewhere in [15-20] (threshold=5)
- Rule B must be satisfied somewhere in [15-20] (threshold=5)
- Rule C must be satisfied somewhere in [15-20] (threshold=5)

All three rules must have been satisfied at some point within their windows.

Scenario 2: Different Thresholds per Rule

Initial rule triggers at index 20

Chain rules:
- Rule A must be satisfied somewhere in [15-20] (threshold=5)
- Rule B must be satisfied somewhere in [17-20] (threshold=3)
- Rule C must be satisfied somewhere in [18-20] (threshold=2)

Each rule has its own window, all measured from the same initial trigger point.

Edge Cases

  1. Window extends before series start: startIndex = max(0, index - threshold)
  2. Empty chain rules list: Should not be allowed - validation required
  3. Threshold = 0: Window is just the current index
  4. Multiple satisfactions: If a rule is satisfied multiple times in the window, that’s fine - we just need at least one

Comparison with ChainRule

Aspect ChainRule AndWithThresholdRule
Window behavior Resetting (each chain gets new window from previous) Fixed (all chains share same window from initial)
Window calculation startIndex = previousTriggerIndex - threshold startIndex = initialTriggerIndex - threshold
Use case Sequential confirmation pattern Simultaneous confirmation pattern

Design Decisions

Why RuleWithThreshold Record?

Default Threshold Behavior

Threshold Interpretation

Alternative Design: FixedWindowChainRule

Overview

An alternative design that reuses the ChainLink infrastructure from ChainRule but with fixed window semantics.

API Design

public class FixedWindowChainRule extends AbstractRule {
    
    private final Rule initialRule;
    private final List<ChainLink> chainLinks;
    private final int threshold;  // Fixed threshold for all links
    
    /**
     * Constructor with single threshold for all chain links
     * @param initialRule The rule that must trigger first
     * @param threshold The fixed number of bars to look back (applies to all links)
     * @param chainLinks The chain links that must ALL be satisfied within the window
     */
    public FixedWindowChainRule(Rule initialRule, int threshold, ChainLink... chainLinks) {
        this(initialRule, threshold, Arrays.asList(chainLinks));
    }
    
    public FixedWindowChainRule(Rule initialRule, int threshold, List<ChainLink> chainLinks) {
        // Note: threshold parameter overrides any thresholds in ChainLink objects
        // OR: We ignore ChainLink thresholds and use the fixed one
    }
}

Algorithm (Version 1: Single Fixed Threshold)

isSatisfied(index, tradingRecord):
    // Step 1: Check if initial rule is satisfied at current index
    if NOT initialRule.isSatisfied(index, tradingRecord):
        return false
    
    // Step 2: Calculate the fixed window [startIndex, index]
    startIndex = max(0, index - threshold)
    
    // Step 3: For each chain link, check if its rule was satisfied within the window
    for each chainLink in chainLinks:
        ruleSatisfiedInWindow = false
        
        // Look backwards from current index to startIndex
        for i = index down to startIndex:
            if chainLink.getRule().isSatisfied(i, tradingRecord):
                ruleSatisfiedInWindow = true
                break
        
        // If any chain link was NOT satisfied in the window, fail
        if NOT ruleSatisfiedInWindow:
            return false
    
    // Step 4: All chain links were satisfied within the window
    return true
isSatisfied(index, tradingRecord):
    // Step 1: Check if initial rule is satisfied at current index
    if NOT initialRule.isSatisfied(index, tradingRecord):
        return false
    
    // Step 2: For each chain link, check if its rule was satisfied within its max distance
    for each chainLink in chainLinks:
        linkThreshold = chainLink.getThreshold()
        startIndex = max(0, index - linkThreshold)
        
        ruleSatisfiedInWindow = false
        
        // Look backwards from current index to startIndex
        for i = index down to startIndex:
            if chainLink.getRule().isSatisfied(i, tradingRecord):
                ruleSatisfiedInWindow = true
                break
        
        // If any chain link was NOT satisfied in its window, fail
        if NOT ruleSatisfiedInWindow:
            return false
    
    // Step 3: All chain links were satisfied within their respective windows
    return true

Pros and Cons of FixedWindowChainRule

Pros:

Cons:

Alternative: Extend ChainRule with Mode Parameter

public class ChainRule extends AbstractRule {
    public enum Mode {
        RESETTING_THRESHOLD,  // Current behavior
        FIXED_WINDOW          // New behavior
    }
    
    private final Mode mode;
    
    public ChainRule(Mode mode, Rule initialRule, ChainLink... chainLinks) {
        // ...
    }
}

Pros: Single class, backwards compatible (default to RESETTING_THRESHOLD)
Cons: More complex class, might confuse users

Recommendation Summary

AndWithThresholdRule is recommended because:

  1. ✅ Simpler and clearer API
  2. ✅ No confusion about threshold semantics
  3. ✅ Explicitly different from ChainRule
  4. ✅ Easier to understand and use
  5. ✅ No dependency on ChainLink abstraction
  6. ✅ Supports per-rule thresholds with clean default behavior
  7. ✅ Multiple API styles (varargs and fluent builder) for different use cases

FixedWindowChainRule might be better if:

Implementation Considerations

Performance

Testing

Documentation

Questions for Discussion

  1. API Preference: Simple list of rules vs ChainLink-based?
  2. Threshold Flexibility: Single threshold for all vs per-rule max distances?
  3. Naming: Which name is clearer for the use case?
  4. Backwards Compatibility: Should we extend ChainRule with a mode parameter instead?
  5. Performance: Should we optimize for large thresholds or many rules?

References

Rationale Notes (2026-03-06)