Files
Miha Kralj dafc569f4c Add series algebra and signal predicates layer
Series arithmetic (lib/numerics):
- Add, Sub, Mul, Div, Lag, reusing BiInputIndicatorBase at period=1
- Div publishes 0.0 (not NaN) on a zero denominator, since a non-finite
  ComputeError result permanently poisons the base's running sum
- BiInputIndicatorBase gains Subscribe(a, b) implementing the
  strategy-primitives spec's time-join rule for event-chained two-input nodes

Phase 0 contracts (lib/core):
- TTradeContext, BracketFlags, ExitReason, TrendState, VolState
- SignalConstants (DefaultEpsilon, TruthThreshold), OutOfRangePolicy
- ConfirmOnClose: bar-confirmation wrapper (Section 8.6)

Signal predicates (lib/signals/predicates), Phase 1:
- Above, Below, Equal (ComparePredicateBase<TOp> generic comparison core)
- InsideChannel, OutsideChannel, ChannelPosition (new MultiInputBase,
  3-input time-join)
- IsRising, IsFalling (exclude current bar, confirm-then-push window)
- AtHighest, AtLowest (include current bar, rebuild-on-correction,
  mirrors channels/Dc)
- Hysteresis, PercentDistance

All new nodes ship with .cs, tests, and .md docs; indices and the
StrategyPrimitives.Spec.md are updated with placement decisions and
design notes. Full suite: 16730 tests, 0 failures, 0 warnings.
2026-09-25 06:45:03 -07:00

79 lines
5.0 KiB
Markdown

# ISRISING: Rising Predicate
> *Pine Script's `ta.rising` asks a deceptively simple question — has this value beaten everything it's seen in the last n bars — and the "excludes the current bar" detail is the whole implementation.*
| Property | Value |
| ---------------- | ------------------------------------------ |
| **Category** | Signals (Predicates) |
| **Inputs** | Source (single series) |
| **Parameters** | `n` (lookback, must be `>= 1`) |
| **Outputs** | Single crisp series (IsRising) |
| **Output range** | `{0.0, 1.0}` while hot; `NaN` during warmup |
| **Warmup** | `n + 1` bars |
| **PineScript** | `ta.rising(source, n)` |
- ISRISING tests whether the current value is strictly greater than the maximum of the n bars preceding it.
- **Similar:** [AtHighest](../athighest/AtHighest.md), [IsFalling](../isfalling/IsFalling.md) | **Trading note:** The breakout-of-recent-range test; `BecameTrue(IsRising(n))` on a source is the rising edge equivalent of a Breakout trigger.
- Part of the strategy-primitives signal algebra. The exact definition here — `current > max(previous n)`, current bar excluded, comparison strict — was confirmed against the "most common understanding" during the design of this predicate, superseding an earlier draft that read Pine's `ta.rising` as adjacent-pair monotonicity.
## Mathematical Foundation
$$
\text{IsRising}_t(n) = \begin{cases} 1.0 & \text{if } x_t > \max(x_{t-1}, \ldots, x_{t-n}) \\ 0.0 & \text{otherwise} \end{cases}
$$
The window is the **previous** n bars — the current bar is never compared against itself. A tie (`x_t` equal to the prior maximum) is `0.0`: the comparison is strict. Contrast with [AtHighest](../athighest/AtHighest.md), whose window *includes* the current bar and asks a different question ("is the current bar the maximum"), which by definition is always at least a tie for the maximum, never a strict-greater-than test against itself.
### Warmup
`NaN` until `n` confirmed prior bars exist — `n + 1` total bars observed (the first bar can never have `n` predecessors).
## Implementation Details
### Confirm-Then-Push: Why No Rebuild Is Needed on Correction
The window (backed by a `MonotonicDeque`, same data structure as `numerics/Highest`) holds only **confirmed prior bars**, never the currently forming one. A tick with `isNew = true` first checks whether the *previous* forming bar's now-settled value should join the window (it does, if one exists), pushing it in with `MonotonicDeque.PushMax` — then only after that does the new tick become the new pending value. A tick with `isNew = false` (a correction) only ever overwrites the pending value; it never touches the window, since the window was never told about the currently-forming bar in the first place.
This is the same confirm-then-push pattern as [`ConfirmOnClose`](../../../core/ConfirmOnClose.md), applied to a rolling window instead of a single value, and it means `IsRising` needs no `RebuildMax`-on-correction logic at all — unlike [AtHighest](../athighest/AtHighest.md), whose inclusive window does need to rebuild, because a correction there changes a value that is already inside the window.
## Performance Profile
| Operation | Count | Notes |
| :--- | :---: | :--- |
| Deque push (amortized) | ≤1 | Only when a bar closes |
| Deque query | 1 | `GetExtremum` |
| Comparison | 1 | `current > extremum` |
| **Total** | **O(1) amortized** | Same complexity class as `numerics/Highest` |
## Validation
| Test | Status |
| :--- | :---: |
| Warmup returns NaN for the first n bars | ✅ |
| n=1: strictly greater than the immediately preceding bar | ✅ |
| Tie returns false (strict comparison) | ✅ |
| Current bar is excluded from its own comparison | ✅ |
| Correction to the forming bar does not disturb the confirmed window | ✅ |
| NaN/Infinity substitution | ✅ |
| Batch/streaming parity | ✅ |
## Common Pitfalls
1. **The current bar is never part of the comparison.** `IsRising(n)` compares against the *previous* n bars only; a value tying its own immediately preceding value is not "rising," and a run of n identical values followed by a higher one is rising on that last bar, not sooner.
2. **Do not confuse with `AtHighest`.** `IsRising(n)` and `AtHighest(n)` ask genuinely different questions (exclusive vs. inclusive window) and use different `MonotonicDeque` correction strategies internally; they are not interchangeable even though both use "n" and a rolling maximum.
3. **Warmup is `n + 1`, not `n`.** The first bar can never have n predecessors, so the node needs to observe n+1 bars total before its first non-NaN output.
## Usage Examples
```csharp
// "Higher high than the last 5 confirmed bars"
var rising5 = new IsRising(5);
// Chained from another indicator
var trendAccelerating = new IsRising(new Roc(price, 10), 3);
```
## References
- TradingView Pine Script Reference: `ta.rising`.