Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
Status: partially superseded by shipped functionality.
What landed:
AndWithThresholdRule exists in org.ta4j.core.rules (commit 5e5acc99) and provides fixed-window AND semantics for exactly two rules within one threshold window.What remains open from this PRD:
ChainRule semantics.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.
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.
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.
Two design approaches were considered:
Key Characteristics:
ChainLink abstractionChainRuleKey Characteristics:
ChainLink helper class (reuses existing infrastructure)ChainRule (familiar)ChainRuleRecommendation: AndWithThresholdRule is recommended due to its simpler API, clearer semantics, and explicit differentiation from ChainRule.
initialRule triggers at index i, each chain rule must have been satisfied within its own threshold window [i-threshold, i]ChainRule, the threshold does NOT reset for each chain rule/**
* 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);
}
}
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();
}
}
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
Rule rule = new AndWithThresholdRule(
initialRule,
5, // default threshold for all
ruleA, ruleB, ruleC // all use threshold=5
);
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
);
Rule rule = AndWithThresholdRule.builder()
.withInitialRule(initialRule)
.withDefaultThreshold(5)
.addRule(ruleA)
.addRule(ruleB)
.addRule(ruleC)
.build();
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();
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
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.
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.
startIndex = max(0, index - threshold)| 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 |
[index - threshold, index] (inclusive on both ends)An alternative design that reuses the ChainLink infrastructure from ChainRule but with fixed window semantics.
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
}
}
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
max(all chainLink thresholds)Pros:
Cons:
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
AndWithThresholdRule is recommended because:
FixedWindowChainRule might be better if:
pseudocode-AndWithThresholdRule.mdpseudocode-AndWithThresholdRule-Enhanced.mdpseudocode-FixedWindowChainRule.mdpseudocode-comparison.mdorg.ta4j.core.rules.AndWithThresholdRule (commit 5e5acc99): two-rule fixed-window check with a single threshold.